add fully function Multi User With ExpireDate & Traffic

This commit is contained in:
Hossin Asaadi
2022-11-12 08:25:29 -05:00
parent 77d24fbede
commit 47a48316ca
10 changed files with 166 additions and 53 deletions

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"path" "path"
"x-ui/config" "x-ui/config"
"x-ui/xray"
"x-ui/database/model" "x-ui/database/model"
) )
@@ -43,6 +44,9 @@ func initSetting() error {
func initInboundClientIps() error { func initInboundClientIps() error {
return db.AutoMigrate(&model.InboundClientIps{}) return db.AutoMigrate(&model.InboundClientIps{})
} }
func initClientTraffic() error {
return db.AutoMigrate(&xray.ClientTraffic{})
}
func InitDB(dbPath string) error { func InitDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
@@ -83,7 +87,11 @@ func InitDB(dbPath string) error {
if err != nil { if err != nil {
return err return err
} }
err = initClientTraffic()
if err != nil {
return err
}
return nil return nil
} }

View File

@@ -32,7 +32,7 @@ type Inbound struct {
Remark string `json:"remark" form:"remark"` Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
ClientStats string `json:"clientStats" form:"clientStats"` ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"`
// config part // config part
Listen string `json:"listen" form:"listen"` Listen string `json:"listen" form:"listen"`
@@ -76,6 +76,6 @@ type Client struct {
Email string `json:"email"` Email string `json:"email"`
LimitIP int `json:"limitIp"` LimitIP int `json:"limitIp"`
Security string `json:"security"` Security string `json:"security"`
Total int64 `json:"total" form:"total"` TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
} }

View File

@@ -125,10 +125,7 @@ class DBInbound {
if (!ObjectUtil.isEmpty(this.sniffing)) { if (!ObjectUtil.isEmpty(this.sniffing)) {
sniffing = JSON.parse(this.sniffing); sniffing = JSON.parse(this.sniffing);
} }
let clientStats = {};
if (!ObjectUtil.isEmpty(this.clientStats)) {
clientStats = JSON.parse(this.clientStats);
}
const config = { const config = {
port: this.port, port: this.port,
listen: this.listen, listen: this.listen,
@@ -137,7 +134,7 @@ class DBInbound {
streamSettings: streamSettings, streamSettings: streamSettings,
tag: this.tag, tag: this.tag,
sniffing: sniffing, sniffing: sniffing,
clientStats: clientStats, clientStats: this.clientStats,
}; };
return Inbound.fromJson(config); return Inbound.fromJson(config);
} }

View File

@@ -2,8 +2,8 @@
<a-form layout="inline"> <a-form layout="inline">
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses" <a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
:key="`vmess-${index}`"> :key="`vmess-${index}`">
<a-collapse-panel :header="vmess.email == '' ? 'Add User' : vmess.email"> <a-collapse-panel :header="getHeaderText(vmess.email)">
<a-tag v-if="isExpiry(index)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is Expired And Disabled</a-tag> <a-tag v-if="isExpiry(index) || (getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label="Email"> <a-form-item label="Email">
@@ -56,7 +56,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="vmess.totalGB" :min="0"></a-input-number> <a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -73,7 +73,7 @@
</a-form-item> </a-form-item>
<a-form layout="inline"> <a-form layout="inline">
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag> <a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<a-form v-if="vmess.totalGB > 0"> <a-form v-if="vmess._totalGB > 0">
<a-tag color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag> <a-tag color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
</a-form> </a-form>
</a-form> </a-form>

View File

@@ -116,14 +116,51 @@
return this.inbound.isExpiry(index) return this.inbound.isExpiry(index)
}, },
getUpStats(email) { getUpStats(email) {
console.log(email,this.inbound.clientStats[email]) clientStats = this.inbound.clientStats
if(this.inbound.clientStats[email]) if(clientStats.length > 0)
return this.inbound.clientStats[email]["Up"] {
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
}, },
getDownStats(email) { getDownStats(email) {
if(this.inbound.clientStats[email]) clientStats = this.inbound.clientStats
return this.inbound.clientStats[email]["Down"] if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
},
isClientEnable(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['enable']
}
}
}
},
getHeaderText(email) {
if(email == "")
return "Add Client"
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
}, },

View File

@@ -15,7 +15,15 @@ func NewCheckInboundJob() *CheckInboundJob {
} }
func (j *CheckInboundJob) Run() { func (j *CheckInboundJob) Run() {
count, err := j.inboundService.DisableInvalidInbounds() count, err := j.inboundService.DisableInvalidClients()
if err != nil {
logger.Warning("disable invalid Client err:", err)
} else if count > 0 {
logger.Debugf("disabled %v Client", count)
j.xrayService.SetToNeedRestart()
}
count, err = j.inboundService.DisableInvalidInbounds()
if err != nil { if err != nil {
logger.Warning("disable invalid inbounds err:", err) logger.Warning("disable invalid inbounds err:", err)
} else if count > 0 { } else if count > 0 {

View File

@@ -18,16 +18,6 @@ func (j *XrayTrafficJob) Run() {
if !j.xrayService.IsXrayRunning() { if !j.xrayService.IsXrayRunning() {
return return
} }
traffics, err := j.xrayService.GetXrayTraffic()
if err != nil {
logger.Warning("get xray traffic failed:", err)
return
}
err = j.inboundService.AddTraffic(traffics)
if err != nil {
logger.Warning("add traffic failed:", err)
}
// get Client Traffic // get Client Traffic
@@ -40,5 +30,17 @@ func (j *XrayTrafficJob) Run() {
if err != nil { if err != nil {
logger.Warning("add client traffic failed:", err) logger.Warning("add client traffic failed:", err)
} }
traffics, err := j.xrayService.GetXrayTraffic()
if err != nil {
logger.Warning("get xray traffic failed:", err)
return
}
err = j.inboundService.AddTraffic(traffics)
if err != nil {
logger.Warning("add traffic failed:", err)
}
} }

View File

@@ -3,8 +3,8 @@ package service
import ( import (
"fmt" "fmt"
"time" "time"
"encoding/json"
"x-ui/database" "x-ui/database"
"encoding/json"
"x-ui/database/model" "x-ui/database/model"
"x-ui/util/common" "x-ui/util/common"
"x-ui/xray" "x-ui/xray"
@@ -18,7 +18,7 @@ type InboundService struct {
func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) { func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Where("user_id = ?", userId).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
@@ -28,7 +28,7 @@ func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) { func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbounds []*model.Inbound var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Find(&inbounds).Error err := db.Model(model.Inbound{}).Preload("ClientStats").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && err != gorm.ErrRecordNotFound {
return nil, err return nil, err
} }
@@ -172,7 +172,7 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
return nil return nil
} }
db := database.GetDB() db := database.GetDB()
db = db.Model(model.Inbound{}) db = db.Model(xray.ClientTraffic{})
tx := db.Begin() tx := db.Begin()
defer func() { defer func() {
if err != nil { if err != nil {
@@ -184,24 +184,30 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
for _, traffic := range traffics { for _, traffic := range traffics {
inbound := &model.Inbound{} inbound := &model.Inbound{}
err := tx.Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error err := db.Model(model.Inbound{}).Where("settings like ?", "%" + traffic.Email + "%").First(inbound).Error
clientStats := map[string]*xray.ClientTraffic{} traffic.InboundId = inbound.Id
json.Unmarshal([]byte(inbound.ClientStats), &clientStats) if err != nil {
return err
if _, ok := clientStats[traffic.Email]; ok {
clientStats[traffic.Email].Up = clientStats[traffic.Email].Up + traffic.Up
clientStats[traffic.Email].Down = clientStats[traffic.Email].Down + traffic.Down
}else{
clientStats[traffic.Email] = traffic
} }
jsonClientStats, err := json.Marshal(clientStats) // get settings clients
settings := map[string][]model.Client{}
// if clientStats[traffic.Email] json.Unmarshal([]byte(inbound.Settings), &settings)
err = tx.Where("settings like ?", "%" + traffic.Email + "%"). clients := settings["clients"]
Update("client_stats", jsonClientStats). for _, client := range clients {
Error if traffic.Email == client.Email {
traffic.ExpiryTime = client.ExpiryTime
traffic.Total = client.TotalGB
}
}
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
UpdateColumn("enable", true).
UpdateColumn("expiry_time", traffic.ExpiryTime).
UpdateColumn("total",traffic.Total).
UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).RowsAffected == 0 {
err = tx.Create(traffic).Error
}
if err != nil { if err != nil {
return err return err
} }
@@ -220,6 +226,16 @@ func (s *InboundService) DisableInvalidInbounds() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) DisableInvalidClients() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
result := db.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error
count := result.RowsAffected
return count, err
}
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) { func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
db := database.GetDB() db := database.GetDB()

View File

@@ -4,9 +4,11 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"sync" "sync"
"time"
"x-ui/logger" "x-ui/logger"
"x-ui/xray" "x-ui/xray"
"x-ui/database/model"
"go.uber.org/atomic" "go.uber.org/atomic"
) )
@@ -51,6 +53,9 @@ func (s *XrayService) GetXrayVersion() string {
} }
return p.GetVersion() return p.GetVersion()
} }
func RemoveIndex(s []model.Client, index int) []model.Client {
return append(s[:index], s[index+1:]...)
}
func (s *XrayService) GetXrayConfig() (*xray.Config, error) { func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
templateConfig, err := s.settingService.GetXrayConfigTemplate() templateConfig, err := s.settingService.GetXrayConfigTemplate()
@@ -72,6 +77,41 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if !inbound.Enable { if !inbound.Enable {
continue continue
} }
// get settings clients
settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
// check users active or not
now := time.Now().Unix() * 1000
clientStats := inbound.ClientStats
for _, clientTraffic := range clientStats {
for index, client := range clients {
if client.Email == clientTraffic.Email {
totalUsage := clientTraffic.Up + clientTraffic.Down
if totalUsage > client.TotalGB || (client.ExpiryTime > 0 && client.ExpiryTime <= now){
clients = RemoveIndex(clients,index)
logger.Debug("Remove Inbound User",client.Email)
}
}
}
}
settings["clients"] = clients
modifiedSettings, err := json.Marshal(settings)
if err != nil {
return nil, err
}
inbound.Settings = string(modifiedSettings)
inboundConfig := inbound.GenXrayInboundConfig() inboundConfig := inbound.GenXrayInboundConfig()
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig) xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
} }

View File

@@ -1,7 +1,12 @@
package xray package xray
type ClientTraffic struct { type ClientTraffic struct {
Email string Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Up int64 InboundId int `json:"inboundId" form:"inboundId"`
Down int64 Enable bool `json:"enable" form:"enable"`
Email string `json:"email" form:"email" gorm:"unique"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
Total int64 `json:"total" form:"total"`
} }