mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 05:23:09 +00:00
add fully function Multi User With ExpireDate & Traffic
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user