Browse Source

alireza update pack

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 year ago
parent
commit
e1da43053d

+ 4 - 2
README.md

@@ -79,6 +79,8 @@ Set the robot-related parameters in the panel background, including:
 
 Reference syntax:
 
+- 30 * * * * * //Notify at the 30s of each point
+- 0 */10 * * * * //Notify at the first second of each 10 minutes
 - @hourly // hourly notification
 - @daily // Daily notification (00:00 in the morning)
 - @every 8h // notify every 8 hours
@@ -89,13 +91,13 @@ Reference syntax:
 - Login notification
 - CPU threshold notification
 - Threshold for Expiration time and Traffic to report in advance
-- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
+- Support client report menu if client's telegram username added to the user's configurations
 - Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
 - Menu based bot
 - Search client by email ( only admin )
 - Check all inbounds
 - Check server status
-- Check Exhausted users
+- Check depleted users
 - Receive backup by request and in periodic reports
 
 # A Special Thanks To

+ 5 - 2
database/model/model.go

@@ -44,9 +44,9 @@ type Inbound struct {
 	Sniffing       string   `json:"sniffing" form:"sniffing"`
 }
 type InboundClientIps struct {
-	Id       int    `json:"id" gorm:"primaryKey;autoIncrement"`
+	Id          int    `json:"id" gorm:"primaryKey;autoIncrement"`
 	ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
-	Ips string `json:"ips" form:"ips"`
+	Ips         string `json:"ips" form:"ips"`
 }
 
 func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
@@ -80,4 +80,7 @@ type Client struct {
 	LimitIP    int    `json:"limitIp"`
 	TotalGB    int64  `json:"totalGB" form:"totalGB"`
 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
+	Enable     bool   `json:"enable" from:"enable"`
+	TgID       string `json:"tgId" from:"tgId"`
+	SubID      string `json:"subId" from:"subId"`
 }

+ 52 - 5
web/assets/css/custom.css

@@ -156,6 +156,12 @@
     padding:16px;
 }
 
+.ant-table-expand-icon-th,
+.ant-table-row-expand-icon-cell {
+    width: 30px;
+    min-width: 30px;
+}
+
 .ant-menu-dark,
 .ant-menu-dark .ant-menu-sub,
 .ant-layout-header,
@@ -174,6 +180,7 @@
 
 .ant-card-dark:hover {
     border-color: #e8e8e8;
+    box-shadow: 0 2px 8px rgba(255,255,255,.15);
 }
 
 .ant-card-dark .ant-table-thead th {
@@ -216,20 +223,25 @@
 
 .ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
 .ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
-.ant-card-dark .ant-calendar-date:hover {
+.ant-card-dark .ant-calendar-date:hover,
+.ant-card-dark .ant-select-dropdown-menu-item-active,
+.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
     background-color: #004488;
 }
 
-.ant-card-dark tbody .ant-table-expanded-row {
+.ant-card-dark tbody .ant-table-expanded-row,
+.ant-card-dark .ant-calendar-time-picker-inner {
     color: hsla(0,0%,100%,.65);
     background-color: #1a212a; 
 }
 
 .ant-card-dark .ant-input,
 .ant-card-dark .ant-input-number,
+.ant-card-dark .ant-input-number-handler-wrap,
 .ant-card-dark .ant-calendar-input,
 .ant-card-dark .ant-select-dropdown-menu-item-selected,
-.ant-card-dark .ant-select-selection {
+.ant-card-dark .ant-select-selection,
+.ant-card-dark .ant-calendar-picker-clear {
     color: hsla(0,0%,100%,.65);
     background-color: #2e3b52;
 }
@@ -239,6 +251,12 @@
     background-color: #161b22;
 }
 
+.ant-dropdown-menu-dark,
+.ant-card-dark .ant-modal-content {
+    border: 1px solid rgba(255, 255, 255, 0.65);
+    box-shadow: 0 2px 8px rgba(255,255,255,.15);
+}
+
 .ant-card-dark .ant-modal-content,
 .ant-card-dark .ant-modal-body,
 .ant-card-dark .ant-modal-header,
@@ -280,6 +298,12 @@
     border: 1px solid hsla(0,0%,100%,.30);
 }
 
+.ant-card-dark .ant-tag {
+    color: hsla(0,0%,100%,.65);
+    background: rgba(255,255,255,.04);
+    border-color: #434343;
+}
+
 .ant-card-dark .ant-tag-blue {
     color: #3c9ae8;
     background: #111d2c;
@@ -334,6 +358,29 @@
     color:  hsla(0,0%,100%,.65);
     background-color: #073763;
     border-color: #1890ff;
-    text-shadow: 0 -1px 0 rgba(0,0,0,.12);
-    box-shadow: 0 2px 0 rgba(0,0,0,.045);
+    text-shadow: 0 -1px 0 rgba(255,255,255,.12);
+    box-shadow: 0 2px 0 rgba(255,255,255,.045);
+}
+.ant-card-dark .ant-btn-primary:hover {
+    background-color: #40a9ff;
+    border-color: #40a9ff;
+}
+
+.ant-dark .ant-popover-content {
+    border: 1px solid #e8e8e8;
+    border-radius: 4px;
+    box-shadow: 0 2px 8px rgba(255,255,255,.15);
+}
+
+.ant-dark .ant-popover-inner {
+    background: #222a37;
+}
+
+.ant-dark .ant-popover-title,
+.ant-dark .ant-popover-inner-content {
+    color: hsla(0,0%,100%,.65);
+}
+
+.ant-dark .ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow {
+    border-color: transparent #2e3b52 #2e3b52 transparent;
 }

+ 2 - 2
web/assets/js/model/models.js

@@ -171,13 +171,13 @@ class AllSetting {
         this.webCertFile = "";
         this.webKeyFile = "";
         this.webBasePath = "/";
+        this.expireDiff = "";
+        this.trafficDiff = "";
         this.tgBotEnable = false;
         this.tgBotToken = "";
         this.tgBotChatId = "";
         this.tgRunTime = "@daily";
         this.tgBotBackup = false;
-        this.tgExpireDiff = "";
-        this.tgTrafficDiff = "";
         this.tgCpu = "";
         this.xrayTemplateConfig = "";
 

+ 144 - 113
web/assets/js/model/xray.js

@@ -92,6 +92,7 @@ const UTLS_FINGERPRINT = {
 };
 
 const ALPN_OPTION = {
+    H3: "h3",
     H2: "h2",
     HTTP1: "http/1.1",
 };
@@ -166,20 +167,20 @@ class XrayCommonClass {
 }
 
 class TcpStreamSettings extends XrayCommonClass {
-    constructor(
-        type = 'none',
-        acceptProxyProtocol = false,
-        request = new TcpStreamSettings.TcpRequest(),
-        response = new TcpStreamSettings.TcpResponse(),
-    ) {
+    constructor(acceptProxyProtocol=false,
+                type='none',
+                request=new TcpStreamSettings.TcpRequest(),
+                response=new TcpStreamSettings.TcpResponse(),
+                ) {
         super();
+        this.acceptProxyProtocol = acceptProxyProtocol;
         this.type = type;
         this.request = request;
         this.response = response;
         this.acceptProxyProtocol = acceptProxyProtocol;
     }
 
-    static fromJson(json = {}) {
+    static fromJson(json={}) {
         let header = json.header;
         if (!header) {
             header = {};
@@ -194,6 +195,7 @@ class TcpStreamSettings extends XrayCommonClass {
 
     toJson() {
         return {
+            acceptProxyProtocol: this.acceptProxyProtocol,
             header: {
                 type: this.type,
                 request: this.type === 'http' ? this.request.toJson() : undefined,
@@ -205,10 +207,10 @@ class TcpStreamSettings extends XrayCommonClass {
 }
 
 TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
-    constructor(version = '1.1',
-        method = 'GET',
-        path = ['/'],
-        headers = [],
+    constructor(version='1.1',
+                method='GET',
+                path=['/'],
+                headers=[],
     ) {
         super();
         this.version = version;
@@ -242,7 +244,7 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
         this.headers.splice(index, 1);
     }
 
-    static fromJson(json = {}) {
+    static fromJson(json={}) {
         return new TcpStreamSettings.TcpRequest(
             json.version,
             json.method,
@@ -261,10 +263,10 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
 };
 
 TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
-    constructor(version = '1.1',
-        status = '200',
-        reason = 'OK',
-        headers = [],
+    constructor(version='1.1',
+                status='200',
+                reason='OK',
+                headers=[],
     ) {
         super();
         this.version = version;
@@ -281,7 +283,7 @@ TcpStreamSettings.TcpResponse = class extends XrayCommonClass {
         this.headers.splice(index, 1);
     }
 
-    static fromJson(json = {}) {
+    static fromJson(json={}) {
         return new TcpStreamSettings.TcpResponse(
             json.version,
             json.status,
@@ -474,9 +476,13 @@ class GrpcStreamSettings extends XrayCommonClass {
 }
 
 class TlsStreamSettings extends XrayCommonClass {
-    constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS10, maxVersion = TLS_VERSION_OPTION.TLS12,
-        cipherSuites = '',
-        certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
+    constructor(serverName='',
+                minVersion = TLS_VERSION_OPTION.TLS12,
+                maxVersion = TLS_VERSION_OPTION.TLS13,
+                cipherSuites = '',
+                certificates=[new TlsStreamSettings.Cert()],
+                alpn=[],
+                settings=[new TlsStreamSettings.Settings()]) {
         super();
         this.server = serverName;
         this.minVersion = minVersion;
@@ -484,7 +490,7 @@ class TlsStreamSettings extends XrayCommonClass {
         this.cipherSuites = cipherSuites;
         this.certs = certificates;
         this.alpn = alpn;
-		this.settings = settings;
+        this.settings = settings;
     }
 
     addCert(cert) {
@@ -497,15 +503,15 @@ class TlsStreamSettings extends XrayCommonClass {
 
     static fromJson(json={}) {
         let certs;
-		let settings;
+        let settings;
         if (!ObjectUtil.isEmpty(json.certificates)) {
             certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
         }
+
 		if (!ObjectUtil.isEmpty(json.settings)) {
             let values = json.settings[0];
             settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
         }
-
         return new TlsStreamSettings(
             json.serverName,
             json.minVersion,
@@ -513,7 +519,7 @@ class TlsStreamSettings extends XrayCommonClass {
             json.cipherSuites,
             certs,
             json.alpn,
-			settings,
+            settings,
         );
     }
 
@@ -526,7 +532,6 @@ class TlsStreamSettings extends XrayCommonClass {
             certificates: TlsStreamSettings.toJsonArray(this.certs),
             alpn: this.alpn,
             settings: TlsStreamSettings.toJsonArray(this.settings),
-			
         };
     }
 }
@@ -573,40 +578,39 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
 };
 
 TlsStreamSettings.Settings = class extends XrayCommonClass {
-  constructor(allowInsecure = false, fingerprint = '', serverName = '') {
-    super();
-    this.allowInsecure = allowInsecure;
-    this.fingerprint = fingerprint;
-    this.serverName = serverName;
-  }
-  static fromJson(json = {}) {
-    return new TlsStreamSettings.Settings(
-      json.allowInsecure,
-      json.fingerprint,
-      json.servername,
-    );
-  }
-  toJson() {
-    return {
-      allowInsecure: this.allowInsecure,
-      fingerprint: this.fingerprint,
-      serverName: this.serverName,
-    };
-  }
+    constructor(allowInsecure = false, fingerprint = '', serverName = '') {
+        super();
+        this.allowInsecure = allowInsecure;
+        this.fingerprint = fingerprint;
+        this.serverName = serverName;
+    }
+    static fromJson(json = {}) {
+        return new TlsStreamSettings.Settings(
+            json.allowInsecure,
+            json.fingerprint,
+            json.servername,
+        );
+    }
+    toJson() {
+        return {
+            allowInsecure: this.allowInsecure,
+            fingerprint: this.fingerprint,
+            serverName: this.serverName,
+        };
+    }
 };
 
-
 class StreamSettings extends XrayCommonClass {
     constructor(network='tcp',
-		security='none',
-		tlsSettings=new TlsStreamSettings(),
-		tcpSettings=new TcpStreamSettings(),
-		kcpSettings=new KcpStreamSettings(),
-		wsSettings=new WsStreamSettings(),
-		httpSettings=new HttpStreamSettings(),
-		quicSettings=new QuicStreamSettings(),
-		grpcSettings=new GrpcStreamSettings(),
-		) {
+                security='none',
+                tlsSettings=new TlsStreamSettings(),
+                tcpSettings=new TcpStreamSettings(),
+                kcpSettings=new KcpStreamSettings(),
+                wsSettings=new WsStreamSettings(),
+                httpSettings=new HttpStreamSettings(),
+                quicSettings=new QuicStreamSettings(),
+                grpcSettings=new GrpcStreamSettings(),
+                ) {
         super();
         this.network = network;
         this.security = security;
@@ -728,14 +732,15 @@ class Inbound extends XrayCommonClass {
     get protocol() {
         return this._protocol;
     }
-    
+
     set protocol(protocol) {
         this._protocol = protocol;
         this.settings = Inbound.Settings.getSettings(protocol);
         if (protocol === Protocols.TROJAN) {
-            this.tls = false;
+            this.tls = true;
         }
     }
+
     get tls() {
         return this.stream.security === 'tls';
     }
@@ -918,16 +923,16 @@ class Inbound extends XrayCommonClass {
     isExpiry(index) {
         switch (this.protocol) {
             case Protocols.VMESS:
-                if(this.settings.vmesses[index]._expiryTime != null)
-                    return this.settings.vmesses[index]._expiryTime < new Date().getTime();
+                if(this.settings.vmesses[index].expiryTime > 0)
+                    return this.settings.vmesses[index].expiryTime < new Date().getTime();
                 return false
             case Protocols.VLESS:
-                if(this.settings.vlesses[index]._expiryTime != null)
-                    return this.settings.vlesses[index]._expiryTime < new Date().getTime();
+                if(this.settings.vlesses[index].expiryTime > 0)
+                    return this.settings.vlesses[index].expiryTime < new Date().getTime();
                 return false
                 case Protocols.TROJAN:
-                    if(this.settings.trojans[index]._expiryTime != null)
-                        return this.settings.trojans[index]._expiryTime < new Date().getTime();
+                    if(this.settings.trojans[index].expiryTime > 0)
+                        return this.settings.trojans[index].expiryTime < new Date().getTime();
                     return false
             default:
                 return false;
@@ -955,7 +960,7 @@ class Inbound extends XrayCommonClass {
                 return false;
         }
     }
-	
+
     //this is used for xtls-rprx-vision
     canEnableTlsFlow() {
         if ((this.stream.security === 'tls') && (this.network === "tcp")) {
@@ -968,11 +973,10 @@ class Inbound extends XrayCommonClass {
         }
         return false;
     }
-	
+
     canSetTls() {
         return this.canEnableTls();
     }
-     
 
     canEnableXTLS() {
         switch (this.protocol) {
@@ -989,7 +993,7 @@ class Inbound extends XrayCommonClass {
         switch (this.protocol) {
             case Protocols.VMESS:
             case Protocols.VLESS:
-			case Protocols.TROJAN:
+            case Protocols.TROJAN:
                 return true;
             default:
                 return false;
@@ -1065,7 +1069,7 @@ class Inbound extends XrayCommonClass {
                 address = this.stream.tls.server;
             }
         }
-		
+        
         let obj = {
             v: '2',
             ps: remark,
@@ -1078,7 +1082,7 @@ class Inbound extends XrayCommonClass {
             host: host,
             path: path,
             tls: this.stream.security,
-			sni: this.stream.tls.settings[0]['serverName'],
+            sni: this.stream.tls.settings[0]['serverName'],
             fp: this.stream.tls.settings[0]['fingerprint'],
             alpn: this.stream.tls.alpn.join(','),
             allowInsecure: this.stream.tls.settings[0].allowInsecure,
@@ -1148,25 +1152,25 @@ class Inbound extends XrayCommonClass {
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
                 address = this.stream.tls.server;
 			}
-			if (this.stream.tls.settings[0]['serverName'] !== ''){
+            if (this.stream.tls.settings[0]['serverName'] !== ''){
                 params.set("sni", this.stream.tls.settings[0]['serverName']);
             }
             if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
                 params.set("flow", this.settings.vlesses[clientIndex].flow);
             }
         }
-		
-		if (this.XTLS) {
+
+        if (this.XTLS) {
             params.set("security", "xtls");
             params.set("alpn", this.stream.tls.alpn);
             if(this.stream.tls.settings[0].allowInsecure){
                 params.set("allowInsecure", "1");
             }
-			if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
-				address = this.stream.tls.server;
-			}
-			params.set("flow", this.settings.vlesses[clientIndex].flow);
-		}
+            if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
+                address = this.stream.tls.server;
+            }
+            params.set("flow", this.settings.vlesses[clientIndex].flow);
+        }
 
         const link = `vless://${uuid}@${address}:${port}`;
         const url = new URL(link);
@@ -1177,13 +1181,13 @@ class Inbound extends XrayCommonClass {
         return url.toString();
     }
 
-    genSSLink(address = '', remark = '') {
+    genSSLink(address='', remark='') {
         let settings = this.settings;
         const server = this.stream.tls.server;
         if (!ObjectUtil.isEmpty(server)) {
             address = server;
         }
-			return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
+        return 'ss://' + safeBase64(settings.method + ':' + settings.password) + `@${address}:${this.port}#${encodeURIComponent(remark)}`;
     }
 
     genTrojanLink(address = '', remark = '', clientIndex = 0) {
@@ -1191,7 +1195,7 @@ class Inbound extends XrayCommonClass {
         const port = this.port;
         const type = this.stream.network;
         const params = new Map();
-		params.set("type", this.stream.network);
+        params.set("type", this.stream.network);
         switch (type) {
             case "tcp":
                 const tcp = this.stream.tcp;
@@ -1246,12 +1250,12 @@ class Inbound extends XrayCommonClass {
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
                 address = this.stream.tls.server;
-			}
-			if (this.stream.tls.settings[0]['serverName'] !== ''){
-                params.set("sni", this.stream.tls.settings[0]['serverName']);
             }
+            if (this.stream.tls.settings[0]['serverName'] !== ''){
+                params.set("sni", this.stream.tls.settings[0]['serverName']);
+			}
         }
-		
+
 		if (this.XTLS) {
             params.set("security", "xtls");
             params.set("alpn", this.stream.tls.alpn);
@@ -1259,11 +1263,11 @@ class Inbound extends XrayCommonClass {
                 params.set("allowInsecure", "1");
             }
             if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
-            address = this.stream.tls.server;
-            }
-			params.set("flow", this.settings.trojans[clientIndex].flow);
-		}
-        
+                address = this.stream.tls.server;
+			}
+            params.set("flow", this.settings.trojans[clientIndex].flow);
+        }
+
         const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
         const url = new URL(link);
         for (const [key, value] of params) {
@@ -1294,8 +1298,9 @@ class Inbound extends XrayCommonClass {
             default: return '';
         }
     }
+
     genInboundLinks(address = '', remark = '') {
-    let link = '';
+        let link = '';
         switch (this.protocol) {
             case Protocols.VMESS:
             case Protocols.VLESS:
@@ -1308,7 +1313,7 @@ class Inbound extends XrayCommonClass {
                 return (this.genSSLink(address, remark) + '\r\n');
             default: return '';
         }
-}
+    }
 
     static fromJson(json={}) {
         return new Inbound(
@@ -1423,7 +1428,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
     }
 };
 Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
-    constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
+    constructor(id=RandomUtil.randomUUID(), alterId=0, email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
         super();
         this.id = id;
         this.alterId = alterId;
@@ -1431,6 +1436,9 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
         this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.expiryTime = expiryTime;
+        this.enable = enable;
+        this.tgId = tgId;
+        this.subId = subId;
     }
 
     static fromJson(json={}) {
@@ -1441,13 +1449,18 @@ Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
             json.limitIp,
             json.totalGB,
             json.expiryTime,
-
+            json.enable,
+            json.tgId,
+            json.subId,
         );
     }
     get _expiryTime() {
         if (this.expiryTime === 0 || this.expiryTime === "") {
             return null;
         }
+        if (this.expiryTime < 0){
+            return this.expiryTime / -86400000;
+        }
         return moment(this.expiryTime);
     }
 
@@ -1475,7 +1488,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
                 fallbacks=[],) {
         super(protocol);
         this.vlesses = vlesses;
-        this.decryption = 'none';
+        this.decryption = 'none'; // Using decryption is not implemented here
         this.fallbacks = fallbacks;
     }
 
@@ -1487,6 +1500,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
         this.fallbacks.splice(index, 1);
     }
 
+    // decryption should be set to static value
     static fromJson(json={}) {
         return new Inbound.VLESSSettings(
             Protocols.VLESS,
@@ -1506,8 +1520,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
 
 };
 Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
-
-    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
+    constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
         super();
         this.id = id;
         this.flow = flow;
@@ -1515,7 +1528,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
         this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.expiryTime = expiryTime;
-
+        this.enable = enable;
+        this.tgId = tgId;
+        this.subId = subId;
     }
 
     static fromJson(json={}) {
@@ -1526,14 +1541,19 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
             json.limitIp,
             json.totalGB,
             json.expiryTime,
-
+            json.enable,
+            json.tgId,
+            json.subId,
         );
-    }
+      }
 
     get _expiryTime() {
         if (this.expiryTime === 0 || this.expiryTime === "") {
             return null;
         }
+        if (this.expiryTime < 0){
+            return this.expiryTime / -86400000;
+        }
         return moment(this.expiryTime);
     }
 
@@ -1553,7 +1573,7 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
     }
 };
 Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
-    constructor(name="", alpn='', path='', dest='', xver=0) {
+    constructor(name="", alpn=[], path='', dest='', xver=0) {
         super();
         this.name = name;
         this.alpn = alpn;
@@ -1593,8 +1613,8 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
 
 Inbound.TrojanSettings = class extends Inbound.Settings {
     constructor(protocol,
-		trojans=[new Inbound.TrojanSettings.Trojan()],
-		fallbacks=[],) {
+                trojans=[new Inbound.TrojanSettings.Trojan()],
+                fallbacks=[],) {
         super(protocol);
         this.trojans = trojans;
         this.fallbacks = fallbacks;
@@ -1623,7 +1643,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
     }
 };
 Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
-    constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
+    constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime=0, enable=true, tgId='', subId='') {
         super();
         this.password = password;
         this.flow = flow;
@@ -1631,6 +1651,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
         this.limitIp = limitIp;
         this.totalGB = totalGB;
         this.expiryTime = expiryTime;
+        this.enable = enable;
+        this.tgId = tgId;
+        this.subId = subId;
     }
 
     toJson() {
@@ -1641,10 +1664,13 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             limitIp: this.limitIp,
             totalGB: this.totalGB,
             expiryTime: this.expiryTime,
+            enable: this.enable,
+            tgId: this.tgId,
+            subId: this.subId,
         };
     }
 
-    static fromJson(json={}) {
+    static fromJson(json = {}) {
         return new Inbound.TrojanSettings.Trojan(
             json.password,
             json.flow,
@@ -1652,7 +1678,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             json.limitIp,
             json.totalGB,
             json.expiryTime,
-
+            json.enable,
+            json.tgId,
+            json.subId,
         );
     }
 
@@ -1660,6 +1688,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
         if (this.expiryTime === 0 || this.expiryTime === "") {
             return null;
         }
+        if (this.expiryTime < 0){
+            return this.expiryTime / -86400000;
+        }
         return moment(this.expiryTime);
     }
 
@@ -1681,7 +1712,7 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
 };
 
 Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
-    constructor(name="", alpn='', path='', dest='', xver=0) {
+    constructor(name="", alpn=[], path='', dest='', xver=0) {
         super();
         this.name = name;
         this.alpn = alpn;
@@ -1721,9 +1752,9 @@ Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
 
 Inbound.ShadowsocksSettings = class extends Inbound.Settings {
     constructor(protocol,
-        method = SSMethods.BLAKE3_AES_256_GCM,
-        password = RandomUtil.randomSeq(44),
-        network = 'tcp,udp'
+                method=SSMethods.BLAKE3_AES_256_GCM,
+                password=RandomUtil.randomSeq(44),
+                network='tcp,udp'
     ) {
         super(protocol);
         this.method = method;
@@ -1731,7 +1762,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
         this.network = network;
     }
 
-    static fromJson(json = {}) {
+    static fromJson(json={}) {
         return new Inbound.ShadowsocksSettings(
             Protocols.SHADOWSOCKS,
             json.method,
@@ -1755,7 +1786,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
         this.address = address;
         this.port = port;
         this.network = network;
-		this.followRedirect = followRedirect;
+        this.followRedirect = followRedirect;
     }
 
     static fromJson(json={}) {
@@ -1764,7 +1795,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
             json.address,
             json.port,
             json.network,
-			json.followRedirect,
+            json.followRedirect,
         );
     }
 
@@ -1773,7 +1804,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
             address: this.address,
             port: this.port,
             network: this.network,
-			followRedirect: this.followRedirect,
+            followRedirect: this.followRedirect,
         };
     }
 };

+ 40 - 43
web/controller/api.go

@@ -3,77 +3,74 @@ package controller
 import "github.com/gin-gonic/gin"
 
 type APIController struct {
-    BaseController
-    inboundController *InboundController
-    settingController *SettingController
+	BaseController
+	inboundController *InboundController
 }
 
 func NewAPIController(g *gin.RouterGroup) *APIController {
-    a := &APIController{}
-    a.initRouter(g)
-    return a
+	a := &APIController{}
+	a.initRouter(g)
+	return a
 }
 
 func (a *APIController) initRouter(g *gin.RouterGroup) {
-    g = g.Group("/xui/API/inbounds")
-    g.Use(a.checkLogin)
-
-    g.POST("/list", a.getAllInbounds)
-    g.GET("/get/:id", a.getSingleInbound)
-    g.POST("/add", a.addInbound)
-    g.POST("/del/:id", a.delInbound)
-    g.POST("/update/:id", a.updateInbound)
-    g.POST("/clientIps/:email", a.getClientIps)
-    g.POST("/clearClientIps/:email", a.clearClientIps)
-    g.POST("/addClient/", a.addInboundClient)
-    g.POST("/delClient/:email", a.delInboundClient)
-    g.POST("/updateClient/:index", a.updateInboundClient)
-    g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
-
-    a.inboundController = NewInboundController(g)
+	g = g.Group("/xui/API/inbounds")
+	g.Use(a.checkLogin)
+
+	g.POST("/list", a.getAllInbounds)
+	g.GET("/get/:id", a.getSingleInbound)
+	g.POST("/add", a.addInbound)
+	g.POST("/del/:id", a.delInbound)
+	g.POST("/update/:id", a.updateInbound)
+	g.POST("/clientIps/:email", a.getClientIps)
+	g.POST("/clearClientIps/:email", a.clearClientIps)
+	g.POST("/addClient/", a.addInboundClient)
+	g.POST("/delClient/:email", a.delInboundClient)
+	g.POST("/updateClient/:index", a.updateInboundClient)
+	g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
+	g.POST("/resetAllTraffics", a.resetAllTraffics)
+	g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
+
+	a.inboundController = NewInboundController(g)
 }
-
-
 func (a *APIController) getAllInbounds(c *gin.Context) {
-	    a.inboundController.getInbounds(c)
+	a.inboundController.getInbounds(c)
 }
-
 func (a *APIController) getSingleInbound(c *gin.Context) {
-    a.inboundController.getInbound(c)
+	a.inboundController.getInbound(c)
 }
-
 func (a *APIController) addInbound(c *gin.Context) {
-    a.inboundController.addInbound(c)
+	a.inboundController.addInbound(c)
 }
-
 func (a *APIController) delInbound(c *gin.Context) {
-    a.inboundController.delInbound(c)
+	a.inboundController.delInbound(c)
 }
-
 func (a *APIController) updateInbound(c *gin.Context) {
-    a.inboundController.updateInbound(c)
+	a.inboundController.updateInbound(c)
 }
 
 func (a *APIController) getClientIps(c *gin.Context) {
-    a.inboundController.getClientIps(c)
+	a.inboundController.getClientIps(c)
 }
 
 func (a *APIController) clearClientIps(c *gin.Context) {
-    a.inboundController.clearClientIps(c)
+	a.inboundController.clearClientIps(c)
 }
-
 func (a *APIController) addInboundClient(c *gin.Context) {
-    a.inboundController.addInboundClient(c)
+	a.inboundController.addInboundClient(c)
 }
-
 func (a *APIController) delInboundClient(c *gin.Context) {
-    a.inboundController.delInboundClient(c)
+	a.inboundController.delInboundClient(c)
 }
-
 func (a *APIController) updateInboundClient(c *gin.Context) {
-    a.inboundController.updateInboundClient(c)
+	a.inboundController.updateInboundClient(c)
 }
-
 func (a *APIController) resetClientTraffic(c *gin.Context) {
-    a.inboundController.resetClientTraffic(c)
+	a.inboundController.resetClientTraffic(c)
+}
+func (a *APIController) resetAllTraffics(c *gin.Context) {
+	a.inboundController.resetAllTraffics(c)
+}
+func (a *APIController) resetAllClientTraffics(c *gin.Context) {
+	a.inboundController.resetAllClientTraffics(c)
 }

+ 27 - 1
web/controller/inbound.go

@@ -37,6 +37,8 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
 	g.POST("/delClient/:email", a.delInboundClient)
 	g.POST("/updateClient/:index", a.updateInboundClient)
 	g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
+	g.POST("/resetAllTraffics", a.resetAllTraffics)
+	g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
 
 }
 
@@ -131,7 +133,7 @@ func (a *InboundController) updateInbound(c *gin.Context) {
 func (a *InboundController) getClientIps(c *gin.Context) {
 	email := c.Param("email")
 
-	ips , err := a.inboundService.GetInboundClientIps(email)
+	ips, err := a.inboundService.GetInboundClientIps(email)
 	if err != nil {
 		jsonObj(c, "No IP Record", nil)
 		return
@@ -230,3 +232,27 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
 		a.xrayService.SetToNeedRestart()
 	}
 }
+
+func (a *InboundController) resetAllTraffics(c *gin.Context) {
+	err := a.inboundService.ResetAllTraffics()
+	if err != nil {
+		jsonMsg(c, "something worng!", err)
+		return
+	}
+	jsonMsg(c, "All traffics reseted", nil)
+}
+
+func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
+	id, err := strconv.Atoi(c.Param("id"))
+	if err != nil {
+		jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
+		return
+	}
+
+	err = a.inboundService.ResetAllClientTraffics(id)
+	if err != nil {
+		jsonMsg(c, "something worng!", err)
+		return
+	}
+	jsonMsg(c, "All traffics of client reseted", nil)
+}

+ 3 - 2
web/controller/server.go

@@ -38,7 +38,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
 	g.POST("/stopXrayService", a.stopXrayService)
 	g.POST("/restartXrayService", a.restartXrayService)
 	g.POST("/installXray/:version", a.installXray)
-	g.POST("/logs", a.getLogs)
+	g.POST("/logs/:count", a.getLogs)
 }
 
 func (a *ServerController) refreshStatus() {
@@ -109,7 +109,8 @@ func (a *ServerController) restartXrayService(c *gin.Context) {
 }
 
 func (a *ServerController) getLogs(c *gin.Context) {
-	logs, err := a.serverService.GetLogs()
+	count := c.Param("count")
+	logs, err := a.serverService.GetLogs(count)
 	if err != nil {
 		jsonMsg(c, I18n(c, "getLogs"), err)
 		return

+ 31 - 0
web/controller/setting.go

@@ -33,6 +33,7 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
 	g = g.Group("/setting")
 
 	g.POST("/all", a.getAllSetting)
+	g.POST("/defaultSettings", a.getDefaultSettings)
 	g.POST("/update", a.updateSetting)
 	g.POST("/updateUser", a.updateUser)
 	g.POST("/restartPanel", a.restartPanel)
@@ -47,6 +48,36 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
 	jsonObj(c, allSetting, nil)
 }
 
+func (a *SettingController) getDefaultSettings(c *gin.Context) {
+	expireDiff, err := a.settingService.GetExpireDiff()
+	if err != nil {
+		jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
+		return
+	}
+	trafficDiff, err := a.settingService.GetTrafficDiff()
+	if err != nil {
+		jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
+		return
+	}
+	defaultCert, err := a.settingService.GetCertFile()
+	if err != nil {
+		jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
+		return
+	}
+	defaultKey, err := a.settingService.GetKeyFile()
+	if err != nil {
+		jsonMsg(c, I18n(c, "pages.setting.toasts.getSetting"), err)
+		return
+	}
+	result := map[string]interface{}{
+		"expireDiff":  expireDiff,
+		"trafficDiff": trafficDiff,
+		"defaultCert": defaultCert,
+		"defaultKey":  defaultKey,
+	}
+	jsonObj(c, result, nil)
+}
+
 func (a *SettingController) updateSetting(c *gin.Context) {
 	allSetting := &entity.AllSetting{}
 	err := c.ShouldBind(allSetting)

+ 42 - 0
web/controller/sub.go

@@ -0,0 +1,42 @@
+package controller
+
+import (
+	"encoding/base64"
+	"strings"
+	"x-ui/web/service"
+
+	"github.com/gin-gonic/gin"
+)
+
+type SUBController struct {
+	BaseController
+
+	subService service.SubService
+}
+
+func NewSUBController(g *gin.RouterGroup) *SUBController {
+	a := &SUBController{}
+	a.initRouter(g)
+	return a
+}
+
+func (a *SUBController) initRouter(g *gin.RouterGroup) {
+	g = g.Group("/sub")
+
+	g.GET("/:subid", a.subs)
+}
+
+func (a *SUBController) subs(c *gin.Context) {
+	subId := c.Param("subid")
+	host := strings.Split(c.Request.Host, ":")[0]
+	subs, err := a.subService.GetSubs(subId, host)
+	if err != nil {
+		c.String(400, "Error!")
+	} else {
+		result := ""
+		for _, sub := range subs {
+			result += sub + "\n"
+		}
+		c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
+	}
+}

+ 2 - 2
web/entity/entity.go

@@ -32,13 +32,13 @@ type AllSetting struct {
 	WebCertFile        string `json:"webCertFile" form:"webCertFile"`
 	WebKeyFile         string `json:"webKeyFile" form:"webKeyFile"`
 	WebBasePath        string `json:"webBasePath" form:"webBasePath"`
+	ExpireDiff         int    `json:"expireDiff" form:"expireDiff"`
+	TrafficDiff        int    `json:"trafficDiff" form:"trafficDiff"`
 	TgBotEnable        bool   `json:"tgBotEnable" form:"tgBotEnable"`
 	TgBotToken         string `json:"tgBotToken" form:"tgBotToken"`
 	TgBotChatId        string `json:"tgBotChatId" form:"tgBotChatId"`
 	TgRunTime          string `json:"tgRunTime" form:"tgRunTime"`
 	TgBotBackup        bool   `json:"tgBotBackup" form:"tgBotBackup"`
-	TgExpireDiff       int    `json:"tgExpireDiff" form:"tgExpireDiff"`
-	TgTrafficDiff      int    `json:"tgTrafficDiff" form:"tgTrafficDiff"`
 	TgCpu              int    `json:"tgCpu" form:"tgCpu"`
 	XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
 	TimeLocation       string `json:"timeLocation" form:"timeLocation"`

+ 1 - 0
web/html/common/head.html

@@ -7,6 +7,7 @@
     <link rel="stylesheet" href="{{ .base_path }}assets/[email protected]/antd.min.css">
     <link rel="stylesheet" href="{{ .base_path }}assets/[email protected]/theme-chalk/display.css">
     <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
+    <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
     <style>
         [v-cloak] {
             display: none;

+ 32 - 10
web/html/xui/client_bulk_modal.html

@@ -10,8 +10,7 @@
                 <a-select-option :value="1">Random+Prefix</a-select-option>
                 <a-select-option :value="2">Random+Prefix+Num</a-select-option>
                 <a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
-                <a-select-option :value="4">Random+Prefix+Num@Telegram Username</a-select-option>
-                <a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
+                <a-select-option :value="4">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
             </a-select>
         </a-form-item><br />
         <a-form-item v-if="clientsBulkModal.emailMethod>1">
@@ -27,15 +26,19 @@
             <a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
         </a-form-item>
         <a-form-item v-if="clientsBulkModal.emailMethod>2">
-            <span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
-            <span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
+            <span slot="label">{{ i18n "pages.client.postfix" }}</span>
             <a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
         </a-form-item>
-
         <a-form-item v-if="clientsBulkModal.emailMethod < 2">
             <span slot="label">{{ i18n "pages.client.clientCount" }}</span>
             <a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
         </a-form-item>
+        <a-form-item label="Subscription">
+            <a-input v-model.trim="clientsBulkModal.subId"></a-input>
+        </a-form-item>
+        <a-form-item label="Telegram ID">
+            <a-input v-model.trim="clientsBulkModal.tgId"></a-input>
+        </a-form-item>
         <a-form-item>
             <span slot="label">
                 <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
@@ -48,7 +51,13 @@
             </span>
         <a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
         </a-form-item>
-        <a-form-item>
+        <a-form-item label="{{ i18n "pages.client.delayedStart" }}">
+            <a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
+        </a-form-item>
+        <a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientsBulkModal.delayedStart">
+            <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
+        </a-form-item>
+        <a-form-item v-else>
             <span slot="label">
                 <span >{{ i18n "pages.inbounds.expireDate" }}</span>
                 <a-tooltip>
@@ -83,6 +92,9 @@
         lastNum: 1,
         emailPrefix: "",
         emailPostfix: "",
+        subId: "",
+        tgId: "",
+        delayedStart: false,
         ok() {
             method=clientsBulkModal.emailMethod;
             if(method>1){
@@ -94,11 +106,13 @@
             }
             prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
             useNum=(method>1);
-            postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
+            postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? clientsBulkModal.emailPostfix : "";
             for (let i = start; i < end; i++) {
                 newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
-                if(method==5) newClient.email = "";
+                if(method==4) newClient.email = "";
                 newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
+                newClient.subId = clientsBulkModal.subId;
+                newClient.tgId = clientsBulkModal.tgId;
                 newClient._totalGB = clientsBulkModal.totalGB;
                 newClient._expiryTime = clientsBulkModal.expiryTime;
                 clientsBulkModal.clients.push(newClient);
@@ -112,16 +126,18 @@
             this.confirm = confirm;
             this.quantity = 1;
             this.totalGB = 0;
-            this.expiryTime = '';
+            this.expiryTime = 0;
             this.emailMethod= 0;
             this.firstNum= 1;
             this.lastNum= 1;
             this.emailPrefix= "";
             this.emailPostfix= "";
-
+            this.subId= "";
+            this.tgId= "";
             this.dbInbound = new DBInbound(dbInbound);
             this.inbound = dbInbound.toInbound();
             this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
+            this.delayedStart = false;
         },
         getClients(protocol, clientSettings) {
             switch(protocol){
@@ -156,6 +172,12 @@
             get inbound() {
                 return this.clientsBulkModal.inbound;
             },
+            get delayedExpireDays() {
+                return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
+            },
+            set delayedExpireDays(days){
+                this.clientsBulkModal.expiryTime = -86400000 * days;
+            },
         },
     });
 </script>

+ 16 - 4
web/html/xui/client_modal.html

@@ -1,7 +1,7 @@
 {{define "clientsModal"}}
 <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
          :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
-		 :class="siderDrawer.isDarkTheme ? darkClass : ''"
+         :class="siderDrawer.isDarkTheme ? darkClass : ''"
          :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
     {{template "form/client"}}
 </a-modal>
@@ -19,6 +19,7 @@
         index: null,
         clientIps: null,
         isExpired: false,
+        delayedStart: false,
         ok() {
             ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
         },
@@ -32,8 +33,13 @@
             this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
             this.index = index === null ? this.clients.length : index;
             this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
+            this.delayedStart = false;
             if (!isEdit){
                 this.addClient(this.inbound.protocol, this.clients);
+            } else {
+                if (this.clients[index].expiryTime < 0){
+                    this.delayedStart = true;
+                }
             }
             this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
             this.confirm = confirm;
@@ -82,7 +88,7 @@
             },
             get isTrafficExhausted() {
                 if(!clientStats) return false
-                if(clientStats.total == 0) return false
+                if(clientStats.total <= 0) return false
                 if(clientStats.up + clientStats.down < clientStats.total) return false
                 return true
             },
@@ -91,10 +97,16 @@
             },
             get statsColor() {
                 if(!clientStats) return 'blue'
-                if(clientStats.total === 0) return 'blue'
+                if(clientStats.total <= 0) return 'blue'
                 else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
                 else return 'red'
-            }
+            },
+            get delayedExpireDays() {
+                return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
+            },
+            set delayedExpireDays(days){
+                this.client.expiryTime = -86400000 * days;
+            },
         },
         methods: {
             getNewEmail(client) {

+ 20 - 5
web/html/xui/form/client.html

@@ -15,6 +15,9 @@
         </span>
         <a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
     </a-form-item>
+    <a-form-item label="{{ i18n "pages.inbounds.enable" }}">
+        <a-switch v-model="client.enable"></a-switch>
+    </a-form-item>
     <a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
         <a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
     </a-form-item>
@@ -59,8 +62,14 @@
 			</a-textarea>
 		</a-form>
 	</a-form-item>
+    <a-form-item label="Subscription" v-if="client.email">
+        <a-input v-model.trim="client.subId"></a-input>
+    </a-form-item>
+    <a-form-item label="Telegram Username" v-if="client.email">
+        <a-input v-model.trim="client.tgId"></a-input>
+    </a-form-item>
     <a-form-item v-if="inbound.XTLS" label="Flow">
-        <a-select v-model="client.flow" style="width: 150px">
+        <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
             <a-select-option value="">{{ i18n "none" }}</a-select-option>
             <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
         </a-select>
@@ -83,7 +92,7 @@
         </span>
         <a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number>
         <template v-if="isEdit && clientStats">
-            	{{ i18n "usage" }}: 
+            <span>{{ i18n "usage" }}:</span>
             <a-tag :color="statsColor">
                 [[ sizeFormat(clientStats.up) ]] / 
                 [[ sizeFormat(clientStats.down) ]]
@@ -91,7 +100,13 @@
             </a-tag>
         </template>
     </a-form-item>
-    <a-form-item>
+    <a-form-item label="{{ i18n "pages.client.delayedStart" }}">
+        <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
+    </a-form-item>
+    <a-form-item label="{{ i18n "pages.client.expireDays" }}" v-if="clientModal.delayedStart">
+        <a-input type="number" v-model.number="delayedExpireDays" :min="0"></a-input>
+    </a-form-item>
+    <a-form-item v-else>
         <span slot="label">
             <span >{{ i18n "pages.inbounds.expireDate" }}</span>
             <a-tooltip>
@@ -102,8 +117,8 @@
             </a-tooltip>
         </span>
         <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
-						:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"	
-                        v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
+                       :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
+                       v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
         <a-tag color="red" v-if="isExpiry">Expired</a-tag>
     </a-form-item>
 </a-form>

+ 1 - 1
web/html/xui/form/protocol/trojan.html

@@ -76,7 +76,7 @@
         </table>
     </a-collapse-panel>
 </a-collapse>
-<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
+<template v-if="inbound.isTcp">
     <a-form layout="inline">
         <a-form-item label="Fallbacks">
             <a-row>

+ 1 - 1
web/html/xui/form/protocol/vless.html

@@ -82,7 +82,7 @@
         </table>
     </a-collapse-panel>
 </a-collapse>
-<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
+<template v-if="inbound.isTcp">
     <a-form layout="inline">
         <a-form-item label="Fallbacks">
             <a-row>

+ 1 - 0
web/html/xui/form/tls_settings.html

@@ -61,6 +61,7 @@
         <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
             <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
         </a-form-item>
+        <a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
     </template>
     <template v-else>
         <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>

+ 6 - 2
web/html/xui/inbound_client_table.html

@@ -21,9 +21,12 @@
         <a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
     </a-tooltip>
 </template>
+<template slot="enable" slot-scope="text, client, index">
+    <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
+</template>   
 <template slot="client" slot-scope="text, client">
     [[ client.email ]]
-    <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
+    <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
 </template>                                    
 <template slot="traffic" slot-scope="text, client">
     <a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
@@ -34,11 +37,12 @@
     <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
 </template>                                    
 <template slot="expiryTime" slot-scope="text, client, index">
-    <template v-if="client._expiryTime > 0">
+    <template v-if="client.expiryTime > 0">
         <a-tag :color="isExpiry(record, index)? 'red' : 'blue'">
             [[ DateUtil.formatMillis(client._expiryTime) ]]
         </a-tag>
     </template>
+    <a-tag v-else-if="client.expiryTime < 0" color="cyan">[[ client._expiryTime ]] {{ i18n "pages.client.days" }}</a-tag>
     <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
 </template>
 {{end}}

+ 42 - 12
web/html/xui/inbound_info_modal.html

@@ -59,13 +59,25 @@
     </table>
     <template v-if="infoModal.clientSettings">
     <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
-	<table style="margin-bottom: 10px;">
+    <table style="margin-bottom: 10px;">
         <tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
             <td>[[ col ]]</td>
-            <td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>    
-	</table>
+            <td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
+        </tr>
+        <tr>
+            <td>{{ i18n "status" }}</td>
+            <td>
+                <a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
+                <a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
+                <a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
+            </td>
+        </tr>
+    </table>
     <table style="margin-bottom: 10px; width: 100%;">
-            <tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>    
+            <tr>
+                <th>{{ i18n "usage" }}</th>
+                <th>{{ i18n "pages.inbounds.totalFlow" }}</th>
+                <th>{{ i18n "pages.inbounds.expireDate" }}</th>
         <tr>
             <td>
                 <a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
@@ -84,12 +96,19 @@
                         [[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
                     </a-tag>
                 </template>
+                <a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
                 <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
             </td>
-            <td>
-                <a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
-                <a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
-            </td>
+        </tr>
+    </table>
+    <table v-if="infoModal.clientSettings.subId + infoModal.clientSettings.tgId" style="margin-bottom: 10px;">
+        <tr v-if="infoModal.clientSettings.subId">
+            <td>Subscription link</td>
+            <td><a :href="[[ subBase + infoModal.clientSettings.subId ]]" target="_blank">[[ subBase + infoModal.clientSettings.subId ]]</a></td>
+        </tr>
+        <tr v-if="infoModal.clientSettings.tgId">
+            <td>Telegram Username</td>
+            <td><a :href="[[ tgBase + infoModal.clientSettings.tgId ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></td>
         </tr>
     </table>
     </template>
@@ -160,13 +179,12 @@
     </div>
 </a-modal>
 <script>
-
     const infoModal = {
         visible: false,
         inbound: new Inbound(),
         dbInbound: new DBInbound(),
         settings: null,
-        clientSettings: new Inbound.Settings(),
+        clientSettings: null,
         clientStats: [],
         upStats: 0,
         downStats: 0,
@@ -209,12 +227,24 @@
             get inbound() {
                 return this.infoModal.inbound;
             },
-            get isEnable() {
+            get isActive() {
                 if(infoModal.clientStats){
                     return infoModal.clientStats.enable;
                 }
                 return infoModal.dbInbound.isEnable;
-            }
+            },
+            get isEnable() {
+                if(infoModal.clientSettings){
+                    return infoModal.clientSettings.enable;
+                }
+                return infoModal.dbInbound.isEnable;
+            },
+            get subBase() {
+                return window.location.protocol + "//" + window.location.hostname + (window.location.port ? ":" + window.location.port:"") + "/sub/";
+            },
+            get tgBase() {
+                return "https://t.me/"
+            },
         },
         methods: {
             copyTextToClipboard(elmentId,content) {

+ 4 - 0
web/html/xui/inbound_modal.html

@@ -96,6 +96,10 @@
                 clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
                 return clientStats ? clientStats['enable'] : true
             },
+            setDefaultCertData(){
+                inModal.inbound.stream.tls.certs[0].certFile = app.defaultCert;
+                inModal.inbound.stream.tls.certs[0].keyFile = app.defaultKey;
+            },
             getNewEmail(client) {
                 var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
                 var string = '';

+ 176 - 69
web/html/xui/inbounds.html

@@ -41,8 +41,24 @@
                             <a-col :xs="24" :sm="24" :lg="12">
                                 {{ i18n "clients" }}:
                                 <a-tag color="green">[[ total.clients ]]</a-tag>
-                                <a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
-                                <a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
+                                <a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <template slot="content">
+                                        <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
+                                    </template>
+                                    <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
+                                </a-popover>
+                                <a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <template slot="content">
+                                        <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
+                                    </template>
+                                    <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
+                                </a-popover>
+                                <a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                    <template slot="content">
+                                        <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
+                                    </template>
+                                    <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
+                                </a-popover>
                             </a-col>
                         </a-row>
                     </a-card>
@@ -52,7 +68,7 @@
                         <div slot="title">
                             <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
                             <a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
-							<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
+                            <a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
                         </div>
                         <a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
                         <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
@@ -64,8 +80,8 @@
                             <template slot="action" slot-scope="text, dbInbound">
                                 <a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
                                 <a-dropdown :trigger="['click']">
-                                     <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
-                                    <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
+                                    <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
+                                    <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
                                         <a-menu-item v-if="dbInbound.isSS" key="qrcode">
                                             <a-icon type="qrcode"></a-icon>
                                             {{ i18n "qrCode" }}
@@ -76,13 +92,17 @@
                                         </a-menu-item>
                                         <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
                                             <a-menu-item key="addClient">
-                                                <a-icon type="user"></a-icon>
+                                                <a-icon type="user-add"></a-icon>
                                                 {{ i18n "pages.client.add"}}
                                             </a-menu-item>
                                             <a-menu-item key="addBulkClient">
-                                                <a-icon type="team"></a-icon>
+                                                <a-icon type="usergroup-add"></a-icon>
                                                 {{ i18n "pages.client.bulk"}}
                                             </a-menu-item>
+                                            <a-menu-item key="resetClients">
+                                                <a-icon type="file-done"></a-icon>
+                                                {{ i18n "pages.inbounds.resetAllClientTraffics"}}
+                                            </a-menu-item>
                                             <a-menu-item key="export">
                                                 <a-icon type="export"></a-icon>
                                                 {{ i18n "pages.inbounds.export"}}
@@ -97,7 +117,7 @@
                                         <a-menu-item key="resetTraffic">
                                             <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
                                         </a-menu-item>
-										<a-menu-item key="clone">
+                                        <a-menu-item key="clone">
                                             <a-icon type="block"></a-icon> {{ i18n "pages.inbounds.Clone"}}
                                         </a-menu-item>
                                         <a-menu-item key="delete">
@@ -109,7 +129,35 @@
                                 </a-dropdown>
                             </template>
                             <template slot="protocol" slot-scope="text, dbInbound">
-                                <a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
+                                <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
+                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                                    <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXTLS" color="cyan">XTLS</a-tag>
+                                </template>
+                            </template>
+                            <template slot="clients" slot-scope="text, dbInbound">
+                                <template v-if="clientCount[dbInbound.id]">
+                                    <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
+                                    <a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                        <template slot="content">
+                                            <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
+                                        </template>
+                                        <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
+                                    </a-popover>
+                                    <a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                        <template slot="content">
+                                            <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
+                                        </template>
+                                        <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
+                                    </a-popover>
+                                    <a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
+                                        <template slot="content">
+                                            <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
+                                        </template>
+                                        <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
+                                    </a-popover>
+                                </template>
                             </template>
                             <template slot="traffic" slot-scope="text, dbInbound">
                                 <a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
@@ -119,14 +167,6 @@
                                 </template>
                                 <a-tag v-else color="green">{{ i18n "unlimited" }}</a-tag>
                             </template>
-                            <template slot="stream" slot-scope="text, dbInbound, index">
-                                <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
-                                    <a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
-                                    <a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
-                                    <a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
-                                </template>
-                                <template v-else>{{ i18n "none" }}</template>
-                            </template>
                             <template slot="enable" slot-scope="text, dbInbound">
                                 <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
                             </template>
@@ -191,26 +231,26 @@
         align: 'center',
         width: 80,
         dataIndex: "remark",
-    }, {
-        title: '{{ i18n "pages.inbounds.protocol" }}',
-        align: 'center',
-        width: 50,
-        scopedSlots: { customRender: 'protocol' },
     }, {
         title: '{{ i18n "pages.inbounds.port" }}',
         align: 'center',
         dataIndex: "port",
         width: 40,
+    }, {
+        title: '{{ i18n "pages.inbounds.protocol" }}',
+        align: 'left',
+        width: 70,
+        scopedSlots: { customRender: 'protocol' },
+    }, {
+        title: '{{ i18n "clients" }}',
+        align: 'left',
+        width: 50,
+        scopedSlots: { customRender: 'clients' },
     }, {
         title: '{{ i18n "pages.inbounds.traffic" }}↑|↓',
         align: 'center',
-        width: 150,
+        width: 120,
         scopedSlots: { customRender: 'traffic' },
-    }, {
-        title: '{{ i18n "pages.inbounds.transportConfig" }}',
-        align: 'center',
-        width: 60,
-        scopedSlots: { customRender: 'stream' },
     }, {
         title: '{{ i18n "pages.inbounds.expireDate" }}',
         align: 'center',
@@ -220,15 +260,18 @@
 
     const innerColumns = [
         { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
-        { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
-        { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
+        { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
+        { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
+        { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
-        { title: 'UID', width: 150, dataIndex: "id" },
+        { title: 'UID', width: 120, dataIndex: "id" },
     ];
+
     const innerTrojanColumns = [
         { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
-        { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
-        { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
+        { title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
+        { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
+        { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, scopedSlots: { customRender: 'traffic' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
         { title: 'Password', width: 100, dataIndex: "password" },
     ];
@@ -243,6 +286,11 @@
             dbInbounds: [],
             searchKey: '',
             searchedInbounds: [],
+            expireDiff: 0,
+            trafficDiff: 0,
+            defaultCert: '',
+            defaultKey: '',
+            clientCount: {},
         },
         methods: {
             loading(spinning=true) {
@@ -258,16 +306,65 @@
                 this.setInbounds(msg.obj);
                 this.searchKey = '';
             },
+            async getDefaultSettings() {
+                this.loading();
+                const msg = await HttpUtil.post('/xui/setting/defaultSettings');
+                this.loading(false);
+                if (!msg.success) {
+                    return;
+                }
+                this.expireDiff = msg.obj.expireDiff * 86400000;
+                this.trafficDiff = msg.obj.trafficDiff * 1073741824;
+                this.defaultCert = msg.obj.defaultCert;
+                this.defaultKey = msg.obj.defaultKey;
+            },
             setInbounds(dbInbounds) {
                 this.inbounds.splice(0);
                 this.dbInbounds.splice(0);
                 this.searchedInbounds.splice(0);
                 for (const inbound of dbInbounds) {
                     const dbInbound = new DBInbound(inbound);
-                    this.inbounds.push(dbInbound.toInbound());
+                    to_inbound = dbInbound.toInbound()
+                    this.inbounds.push(to_inbound);
                     this.dbInbounds.push(dbInbound);
                     this.searchedInbounds.push(dbInbound);
+                    if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
+                        this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
+                    }
+                }
+            },
+            getClientCounts(dbInbound,inbound){
+                let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
+                clients = this.getClients(dbInbound.protocol, inbound.settings);
+                clientStats = dbInbound.clientStats
+                now = new Date().getTime()
+                if(clients){
+                    clientCount = clients.length;
+                    if(dbInbound.enable){
+                        clients.forEach(client => {
+                            client.enable ? active.push(client.email) : deactive.push(client.email);
+                        });
+                        clientStats.forEach(client => {
+                            if(!client.enable) {
+                                depleted.push(client.email);
+                            } else {
+                                if ((client.expiryTime > 0 && (client.expiryTime-now < this.expireDiff)) ||
+                                (client.total > 0 && (client.total-client.up+client.down < this.trafficDiff ))) expiring.push(client.email);
+                            }
+                        });
+                    } else {
+                        clients.forEach(client => {
+                            deactive.push(client.email);
+                        });
+                    }
                 }
+                return {
+                    clients: clientCount,
+                    active: active,
+                    deactive: deactive,
+                    depleted: depleted,
+                    expiring: expiring,
+                };
             },
             searchInbounds(key) {
                 if (ObjectUtil.isEmpty(key)) {
@@ -315,7 +412,10 @@
                     case "resetTraffic":
                         this.resetTraffic(dbInbound.id);
                         break;
-					case "clone":
+                    case "resetClients":
+                        this.resetAllClientTraffics(dbInbound.id);
+                        break;
+                    case "clone":
                         this.openCloneInbound(dbInbound);
                         break;
                     case "delete":
@@ -477,7 +577,7 @@
                     id: dbInbound.id,
                     settings: inbound.settings.toString(),
                 };
-                await this.submit('/xui/inbound/addClient', data);
+                await this.submit('/xui/inbound/addClient/', data);
             },
             async updateClient(inbound, dbInbound, index) {
                 const data = {
@@ -501,22 +601,6 @@
                         this.updateInbound(inbound, dbInbound);
                     },
                 });
-            },
-			resetAllTraffic() {
-                    this.$confirm({
-                        title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
-                        content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
-                        okText: '{{ i18n "pages.inbounds.resetAllTrafficOkText"}}',
-                        cancelText: '{{ i18n "pages.inbounds.resetAllTrafficCancelText"}}',
-                        onOk: async () => {
-                            for (const dbInbound of this.dbInbounds) {
-                                const inbound = dbInbound.toInbound();
-                                dbInbound.up = 0;
-                                dbInbound.down = 0;
-                                this.updateInbound(inbound, dbInbound);
-                            }
-                        },
-                    });
             },
             delInbound(dbInboundId) {
                 this.$confirm({
@@ -567,6 +651,16 @@
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                 this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
             },
+            async switchEnableClient(dbInboundId, client) {
+                this.loading()
+                dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+                inbound = dbInbound.toInbound();
+                clients = this.getClients(dbInbound.protocol, inbound.settings);
+                index = this.findIndexOfClient(clients, client);
+                clients[index].enable = ! clients[index].enable
+                await this.updateClient(inbound, dbInbound, index);
+                this.loading(false);
+            },
             async submit(url, data) {
                 const msg = await HttpUtil.postWithModal(url, data);
                 if (msg.success) {
@@ -592,6 +686,26 @@
                     onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
                 })
             },
+            resetAllTraffic() {
+                this.$confirm({
+                    title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
+                    content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
+                    class: siderDrawer.isDarkTheme ? darkClass : '',
+                    okText: '{{ i18n "reset"}}',
+                    cancelText: '{{ i18n "cancel"}}',
+                    onOk: () => this.submit('/xui/inbound/resetAllTraffics'),
+                });
+            },
+            resetAllClientTraffics(dbInboundId) {
+                this.$confirm({
+                    title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
+                    content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
+                    class: siderDrawer.isDarkTheme ? darkClass : '',
+                    okText: '{{ i18n "reset"}}',
+                    cancelText: '{{ i18n "cancel"}}',
+                    onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
+                })
+            },
             isExpiry(dbInbound, index) {
                 return dbInbound.toInbound().isExpiry(index)
             },
@@ -635,37 +749,30 @@
             }, 500)
         },
         mounted() {
+            this.getDefaultSettings();
             this.getDBInbounds();
         },
         computed: {
             total() {
                 let down = 0, up = 0;
-                let clients = 0, active = 0, deactive = 0;
+                let clients = 0, deactive = [], depleted = [], expiring = [];
                 this.dbInbounds.forEach(dbInbound => {
                     down += dbInbound.down;
                     up += dbInbound.up;
-                    inbound = dbInbound.toInbound();
-                    clients = this.getClients(dbInbound.protocol, inbound.settings);
-                    if(clients){
-                        if(dbInbound.enable){
-                            isClientEnable = false;
-                            clients.forEach(client => {
-                                isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
-                                isClientEnable ? active++ : deactive++;
-                            });
-                        } else {
-                            deactive += clients.length;
-                        }
-                    } else {
-                        dbInbound.enable ? active++ : deactive++;
+                    if (this.clientCount[dbInbound.id]) {
+                        clients += this.clientCount[dbInbound.id].clients;
+                        deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
+                        depleted = depleted.concat(this.clientCount[dbInbound.id].depleted);
+                        expiring = expiring.concat(this.clientCount[dbInbound.id].expiring);
                     }
                 });
                 return {
                     down: down,
                     up: up,
-                    clients: active + deactive,
-                    active: active,
+                    clients: clients,
                     deactive: deactive,
+                    depleted: depleted,
+                    expiring: expiring,
                 };
             }
         },

+ 31 - 10
web/html/xui/index.html

@@ -199,11 +199,30 @@
              :class="siderDrawer.isDarkTheme ? darkClass : ''"
              width="800px"
              footer="">
-        <table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
-            <tr v-for="log , index in logModal.logs">
-                <td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
-            </tr>
-        </table>
+        <a-form layout="inline">
+            <a-form-item label="Count">
+                <a-select v-model="logModal.rows"
+                style="width: 80px"
+                @change="openLogs(logModal.rows)"
+                :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
+                    <a-select-option value="10">10</a-select-option>
+                    <a-select-option value="20">20</a-select-option>
+                    <a-select-option value="50">50</a-select-option>
+                    <a-select-option value="100">100</a-select-option>
+                </a-select>
+            </a-form-item>
+            <a-form-item>
+                <button class="ant-btn ant-btn-primary" @click="openLogs(logModal.rows)"><a-icon type="sync"></a-icon> Reload</button>
+            </a-form-item>
+            <a-form-item>
+                <a-button type="primary" style="margin-bottom: 10px;"
+                :href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs)" download="x-ui.log">
+                    {{ i18n "download" }} x-ui.log
+                </a-button>
+            </a-form-item>
+       </a-form>
+        <a-input type="textarea" v-model="logModal.logs" disabled="true"
+                :autosize="{ minRows: 10, maxRows: 22}"></a-input>
     </a-modal>
 </a-layout>
 {{template "js" .}}
@@ -301,9 +320,11 @@
     const logModal = {
         visible: false,
         logs: '',
-        show(logs) {
+        rows: 20,
+        show(logs, rows) {
             this.visible = true;
-            this.logs = logs;
+            this.rows = rows;
+            this.logs = logs.join("\n");
         },
         hide() {
             this.visible = false;
@@ -377,14 +398,14 @@
                     return;
                 }
             },
-            async openLogs(){
+            async openLogs(rows){
                 this.loading(true);
-                const msg = await HttpUtil.post('server/logs');
+                const msg = await HttpUtil.post('server/logs/'+rows);
                 this.loading(false);
                 if (!msg.success) {
                     return;
                 }
-                logModal.show(msg.obj);
+                logModal.show(msg.obj,rows);
             }
         },
         async mounted() {

+ 2 - 2
web/html/xui/setting.html

@@ -44,6 +44,8 @@
                                 <setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
+                                <setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}'  v-model="allSetting.expireDiff" :min="0"></setting-list-item>
+                                <setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}'  v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
                                 <a-list-item>
                                     <a-row style="padding: 20px">
                                         <a-col :lg="24" :xl="12">
@@ -122,8 +124,6 @@
                                 <setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}'  v-model="allSetting.tgBotChatId"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}'  v-model="allSetting.tgRunTime"></setting-list-item>
                                 <setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}'  v-model="allSetting.tgBotBackup"></setting-list-item>
-                                <setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}'  v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
-                                <setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}'  v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
                                 <setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}'  v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
                             </a-list>
                         </a-tab-pane>

+ 99 - 27
web/service/inbound.go

@@ -394,11 +394,16 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
 	if len(traffics) == 0 {
 		return nil
 	}
-	db := database.GetDB()
-	dbInbound := db.Model(model.Inbound{})
 
+	traffics, err = s.adjustTraffics(traffics)
+	if err != nil {
+		return err
+	}
+
+	db := database.GetDB()
 	db = db.Model(xray.ClientTraffic{})
 	tx := db.Begin()
+
 	defer func() {
 		if err != nil {
 			tx.Rollback()
@@ -406,7 +411,20 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
 			tx.Commit()
 		}
 	}()
+
+	err = tx.Save(traffics).Error
+	if err != nil {
+		logger.Warning("AddClientTraffic update data ", err)
+	}
+
+	return nil
+}
+
+func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) {
+	db := database.GetDB()
+	dbInbound := db.Model(model.Inbound{})
 	txInbound := dbInbound.Begin()
+
 	defer func() {
 		if err != nil {
 			txInbound.Rollback()
@@ -415,22 +433,23 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
 		}
 	}()
 
-	for _, traffic := range traffics {
+	for traffic_index, traffic := range traffics {
 		inbound := &model.Inbound{}
-		client := &xray.ClientTraffic{}
-		err := tx.Where("email = ?", traffic.Email).First(client).Error
+		client_traffic := &xray.ClientTraffic{}
+		err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
 		if err != nil {
 			if err == gorm.ErrRecordNotFound {
 				logger.Warning(err, traffic.Email)
 			}
 			continue
 		}
+		client_traffic.Up += traffic.Up
+		client_traffic.Down += traffic.Down
 
-		err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
+		err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
 		if err != nil {
 			if err == gorm.ErrRecordNotFound {
 				logger.Warning(err, traffic.Email)
-
 			}
 			continue
 		}
@@ -438,29 +457,35 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
 		settings := map[string][]model.Client{}
 		json.Unmarshal([]byte(inbound.Settings), &settings)
 		clients := settings["clients"]
-		for _, client := range clients {
+		needUpdate := false
+		for client_index, client := range clients {
 			if traffic.Email == client.Email {
-				traffic.ExpiryTime = client.ExpiryTime
-				traffic.Total = client.TotalGB
+				if client.ExpiryTime < 0 {
+					clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
+					needUpdate = true
+				}
+				client_traffic.ExpiryTime = client.ExpiryTime
+				client_traffic.Total = client.TotalGB
+				break
 			}
 		}
-		if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
-			UpdateColumns(map[string]interface{}{
-				"enable":      true,
-				"expiry_time": traffic.ExpiryTime,
-				"total":       traffic.Total,
-				"up":          gorm.Expr("up + ?", traffic.Up),
-				"down":        gorm.Expr("down + ?", traffic.Down)}).RowsAffected == 0 {
-			err = tx.Create(traffic).Error
-		}
 
-		if err != nil {
-			logger.Warning("AddClientTraffic update data ", err)
-			continue
+		if needUpdate {
+			settings["clients"] = clients
+			modifiedSettings, err := json.MarshalIndent(settings, "", "  ")
+			if err != nil {
+				return nil, err
+			}
+
+			err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
+			if err != nil {
+				return nil, err
+			}
 		}
 
+		traffics[traffic_index] = client_traffic
 	}
-	return
+	return traffics, nil
 }
 
 func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -545,11 +570,58 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
 	}
 	return nil
 }
-func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
+
+func (s *InboundService) ResetAllClientTraffics(id int) error {
 	db := database.GetDB()
-	var traffics []*xray.ClientTraffic
 
-	err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
+	result := db.Model(xray.ClientTraffic{}).
+		Where("inbound_id = ?", id).
+		Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
+
+	err := result.Error
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *InboundService) ResetAllTraffics() error {
+	db := database.GetDB()
+
+	result := db.Model(model.Inbound{}).
+		Where("user_id > ?", 0).
+		Updates(map[string]interface{}{"up": 0, "down": 0})
+
+	err := result.Error
+
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
+	db := database.GetDB()
+	var inbounds []*model.Inbound
+	err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
+	if err != nil && err != gorm.ErrRecordNotFound {
+		return nil, err
+	}
+	var emails []string
+	for _, inbound := range inbounds {
+		clients, err := s.getClients(inbound)
+		if err != nil {
+			logger.Error("Unable to get clients from inbound")
+		}
+		for _, client := range clients {
+			if client.TgID == tguname {
+				emails = append(emails, client.Email)
+			}
+		}
+	}
+	var traffics []*xray.ClientTraffic
+	err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
 	if err != nil {
 		if err == gorm.ErrRecordNotFound {
 			logger.Warning(err)
@@ -643,4 +715,4 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
 		return nil, err
 	}
 	return inbounds, nil
-}
+}

+ 2 - 2
web/service/server.go

@@ -327,11 +327,11 @@ func (s *ServerService) UpdateXray(version string) error {
 
 }
 
-func (s *ServerService) GetLogs() ([]string, error) {
+func (s *ServerService) GetLogs(count string) ([]string, error) {
 	// Define the journalctl command and its arguments
 	var cmdArgs []string
 	if runtime.GOOS == "linux" {
-		cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", "100"}
+		cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", count}
 	} else {
 		return []string{"Unsupported operating system"}, nil
 	}

+ 18 - 18
web/service/setting.go

@@ -28,14 +28,14 @@ var defaultValueMap = map[string]string{
 	"webKeyFile":         "",
 	"secret":             random.Seq(32),
 	"webBasePath":        "/",
+	"expireDiff":         "0",
+	"trafficDiff":        "0",
 	"timeLocation":       "Asia/Tehran",
 	"tgBotEnable":        "false",
 	"tgBotToken":         "",
 	"tgBotChatId":        "",
 	"tgRunTime":          "@daily",
 	"tgBotBackup":        "false",
-	"tgExpireDiff":       "0",
-	"tgTrafficDiff":      "0",
 	"tgCpu":              "0",
 }
 
@@ -238,22 +238,6 @@ func (s *SettingService) SetTgBotBackup(value bool) error {
 	return s.setBool("tgBotBackup", value)
 }
 
-func (s *SettingService) GetTgExpireDiff() (int, error) {
-	return s.getInt("tgExpireDiff")
-}
-
-func (s *SettingService) SetTgExpireDiff(value int) error {
-	return s.setInt("tgExpireDiff", value)
-}
-
-func (s *SettingService) GetTgTrafficDiff() (int, error) {
-	return s.getInt("tgTrafficDiff")
-}
-
-func (s *SettingService) SetTgTrafficDiff(value int) error {
-	return s.setInt("tgTrafficDiff", value)
-}
-
 func (s *SettingService) GetTgCpu() (int, error) {
 	return s.getInt("tgCpu")
 }
@@ -278,6 +262,22 @@ func (s *SettingService) GetKeyFile() (string, error) {
 	return s.getString("webKeyFile")
 }
 
+func (s *SettingService) GetExpireDiff() (int, error) {
+	return s.getInt("expireDiff")
+}
+
+func (s *SettingService) SetExpireDiff(value int) error {
+	return s.setInt("expireDiff", value)
+}
+
+func (s *SettingService) GetTrafficDiff() (int, error) {
+	return s.getInt("trafficDiff")
+}
+
+func (s *SettingService) SetgetTrafficDiff(value int) error {
+	return s.setInt("trafficDiff", value)
+}
+
 func (s *SettingService) GetSecret() ([]byte, error) {
 	secret, err := s.getString("secret")
 	if secret == defaultValueMap["secret"] {

+ 505 - 0
web/service/sub.go

@@ -0,0 +1,505 @@
+package service
+
+import (
+	"encoding/base64"
+	"fmt"
+	"net/url"
+	"strings"
+	"x-ui/database"
+	"x-ui/database/model"
+	"x-ui/logger"
+
+	"github.com/goccy/go-json"
+	"gorm.io/gorm"
+)
+
+type SubService struct {
+	address        string
+	inboundService InboundService
+}
+
+func (s *SubService) GetSubs(subId string, host string) ([]string, error) {
+	s.address = host
+	var result []string
+	inbounds, err := s.getInboundsBySubId(subId)
+	if err != nil {
+		return nil, err
+	}
+	for _, inbound := range inbounds {
+		clients, err := s.inboundService.getClients(inbound)
+		if err != nil {
+			logger.Error("SubService - GetSub: Unable to get clients from inbound")
+		}
+		if clients == nil {
+			continue
+		}
+		for _, client := range clients {
+			if client.SubID == subId {
+				link := s.getLink(inbound, client.Email)
+				result = append(result, link)
+			}
+		}
+	}
+	return result, nil
+}
+
+func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
+	db := database.GetDB()
+	var inbounds []*model.Inbound
+	err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId)).Find(&inbounds).Error
+	if err != nil && err != gorm.ErrRecordNotFound {
+		return nil, err
+	}
+	return inbounds, nil
+}
+
+func (s *SubService) getLink(inbound *model.Inbound, email string) string {
+	switch inbound.Protocol {
+	case "vmess":
+		return s.genVmessLink(inbound, email)
+	case "vless":
+		return s.genVlessLink(inbound, email)
+	case "trojan":
+		return s.genTrojanLink(inbound, email)
+	}
+	return ""
+}
+
+func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
+	address := s.address
+	if inbound.Protocol != model.VMess {
+		return ""
+	}
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+	network, _ := stream["network"].(string)
+	typeStr := "none"
+	host := ""
+	path := ""
+	sni := ""
+	fp := ""
+	var alpn []string
+	allowInsecure := false
+	switch network {
+	case "tcp":
+		tcp, _ := stream["tcpSettings"].(map[string]interface{})
+		header, _ := tcp["header"].(map[string]interface{})
+		typeStr, _ = header["type"].(string)
+		if typeStr == "http" {
+			request := header["request"].(map[string]interface{})
+			requestPath, _ := request["path"].([]interface{})
+			path = requestPath[0].(string)
+			headers, _ := request["headers"].(map[string]interface{})
+			host = searchHost(headers)
+		}
+	case "kcp":
+		kcp, _ := stream["kcpSettings"].(map[string]interface{})
+		header, _ := kcp["header"].(map[string]interface{})
+		typeStr, _ = header["type"].(string)
+		path, _ = kcp["seed"].(string)
+	case "ws":
+		ws, _ := stream["wsSettings"].(map[string]interface{})
+		path = ws["path"].(string)
+		headers, _ := ws["headers"].(map[string]interface{})
+		host = searchHost(headers)
+	case "http":
+		network = "h2"
+		http, _ := stream["httpSettings"].(map[string]interface{})
+		path, _ = http["path"].(string)
+		host = searchHost(http)
+	case "quic":
+		quic, _ := stream["quicSettings"].(map[string]interface{})
+		header := quic["header"].(map[string]interface{})
+		typeStr, _ = header["type"].(string)
+		host, _ = quic["security"].(string)
+		path, _ = quic["key"].(string)
+	case "grpc":
+		grpc, _ := stream["grpcSettings"].(map[string]interface{})
+		path = grpc["serviceName"].(string)
+	}
+
+	security, _ := stream["security"].(string)
+	if security == "tls" {
+		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+		alpns, _ := tlsSetting["alpn"].([]interface{})
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		tlsSettings, _ := searchKey(tlsSetting, "settings")
+		if tlsSetting != nil {
+			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+				sni, _ = sniValue.(string)
+			}
+			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+				fp, _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+				allowInsecure, _ = insecure.(bool)
+			}
+		}
+		serverName, _ := tlsSetting["serverName"].(string)
+		if serverName != "" {
+			address = serverName
+		}
+	}
+
+	clients, _ := s.inboundService.getClients(inbound)
+	clientIndex := -1
+	for i, client := range clients {
+		if client.Email == email {
+			clientIndex = i
+			break
+		}
+	}
+
+	obj := map[string]interface{}{
+		"v":             "2",
+		"ps":            email,
+		"add":           address,
+		"port":          inbound.Port,
+		"id":            clients[clientIndex].ID,
+		"aid":           clients[clientIndex].AlterIds,
+		"net":           network,
+		"type":          typeStr,
+		"host":          host,
+		"path":          path,
+		"tls":           security,
+		"sni":           sni,
+		"fp":            fp,
+		"alpn":          strings.Join(alpn, ","),
+		"allowInsecure": allowInsecure,
+	}
+	jsonStr, _ := json.MarshalIndent(obj, "", "  ")
+	return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
+}
+
+func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
+	address := s.address
+	if inbound.Protocol != model.VLESS {
+		return ""
+	}
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+	clients, _ := s.inboundService.getClients(inbound)
+	clientIndex := -1
+	for i, client := range clients {
+		if client.Email == email {
+			clientIndex = i
+			break
+		}
+	}
+	uuid := clients[clientIndex].ID
+	port := inbound.Port
+	streamNetwork := stream["network"].(string)
+	params := make(map[string]string)
+	params["type"] = streamNetwork
+
+	switch streamNetwork {
+	case "tcp":
+		tcp, _ := stream["tcpSettings"].(map[string]interface{})
+		header, _ := tcp["header"].(map[string]interface{})
+		typeStr, _ := header["type"].(string)
+		if typeStr == "http" {
+			request := header["request"].(map[string]interface{})
+			requestPath, _ := request["path"].([]interface{})
+			params["path"] = requestPath[0].(string)
+			headers, _ := request["headers"].(map[string]interface{})
+			params["host"] = searchHost(headers)
+			params["headerType"] = "http"
+		}
+	case "kcp":
+		kcp, _ := stream["kcpSettings"].(map[string]interface{})
+		header, _ := kcp["header"].(map[string]interface{})
+		params["headerType"] = header["type"].(string)
+		params["seed"] = kcp["seed"].(string)
+	case "ws":
+		ws, _ := stream["wsSettings"].(map[string]interface{})
+		params["path"] = ws["path"].(string)
+		headers, _ := ws["headers"].(map[string]interface{})
+		params["host"] = searchHost(headers)
+	case "http":
+		http, _ := stream["httpSettings"].(map[string]interface{})
+		params["path"] = http["path"].(string)
+		params["host"] = searchHost(http)
+	case "quic":
+		quic, _ := stream["quicSettings"].(map[string]interface{})
+		params["quicSecurity"] = quic["security"].(string)
+		params["key"] = quic["key"].(string)
+		header := quic["header"].(map[string]interface{})
+		params["headerType"] = header["type"].(string)
+	case "grpc":
+		grpc, _ := stream["grpcSettings"].(map[string]interface{})
+		params["serviceName"] = grpc["serviceName"].(string)
+	}
+
+	security, _ := stream["security"].(string)
+	if security == "tls" {
+		params["security"] = "tls"
+		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+		alpns, _ := tlsSetting["alpn"].([]interface{})
+		var alpn []string
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		if len(alpn) > 0 {
+			params["alpn"] = strings.Join(alpn, ",")
+		}
+		tlsSettings, _ := searchKey(tlsSetting, "settings")
+		if tlsSetting != nil {
+			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+				params["sni"], _ = sniValue.(string)
+			}
+			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+				if insecure.(bool) {
+					params["allowInsecure"] = "1"
+				}
+			}
+		}
+
+		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+			params["flow"] = clients[clientIndex].Flow
+		}
+
+		serverName, _ := tlsSetting["serverName"].(string)
+		if serverName != "" {
+			address = serverName
+		}
+	}
+
+	if security == "xtls" {
+		params["security"] = "xtls"
+		xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
+		alpns, _ := xtlsSetting["alpn"].([]interface{})
+		var alpn []string
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		if len(alpn) > 0 {
+			params["alpn"] = strings.Join(alpn, ",")
+		}
+
+		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
+		if xtlsSetting != nil {
+			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
+				params["sni"], _ = sniValue.(string)
+			}
+			if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
+				if insecure.(bool) {
+					params["allowInsecure"] = "1"
+				}
+			}
+		}
+
+		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+			params["flow"] = clients[clientIndex].Flow
+		}
+
+		serverName, _ := xtlsSetting["serverName"].(string)
+		if serverName != "" {
+			address = serverName
+		}
+	}
+
+	link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
+	url, _ := url.Parse(link)
+	q := url.Query()
+
+	for k, v := range params {
+		q.Add(k, v)
+	}
+
+	// Set the new query values on the URL
+	url.RawQuery = q.Encode()
+
+	url.Fragment = email
+	return url.String()
+}
+
+func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
+	address := s.address
+	if inbound.Protocol != model.Trojan {
+		return ""
+	}
+	var stream map[string]interface{}
+	json.Unmarshal([]byte(inbound.StreamSettings), &stream)
+	clients, _ := s.inboundService.getClients(inbound)
+	clientIndex := -1
+	for i, client := range clients {
+		if client.Email == email {
+			clientIndex = i
+			break
+		}
+	}
+	password := clients[clientIndex].Password
+	port := inbound.Port
+	streamNetwork := stream["network"].(string)
+	params := make(map[string]string)
+	params["type"] = streamNetwork
+
+	switch streamNetwork {
+	case "tcp":
+		tcp, _ := stream["tcpSettings"].(map[string]interface{})
+		header, _ := tcp["header"].(map[string]interface{})
+		typeStr, _ := header["type"].(string)
+		if typeStr == "http" {
+			request := header["request"].(map[string]interface{})
+			requestPath, _ := request["path"].([]interface{})
+			params["path"] = requestPath[0].(string)
+			headers, _ := request["headers"].(map[string]interface{})
+			params["host"] = searchHost(headers)
+			params["headerType"] = "http"
+		}
+	case "kcp":
+		kcp, _ := stream["kcpSettings"].(map[string]interface{})
+		header, _ := kcp["header"].(map[string]interface{})
+		params["headerType"] = header["type"].(string)
+		params["seed"] = kcp["seed"].(string)
+	case "ws":
+		ws, _ := stream["wsSettings"].(map[string]interface{})
+		params["path"] = ws["path"].(string)
+		headers, _ := ws["headers"].(map[string]interface{})
+		params["host"] = searchHost(headers)
+	case "http":
+		http, _ := stream["httpSettings"].(map[string]interface{})
+		params["path"] = http["path"].(string)
+		params["host"] = searchHost(http)
+	case "quic":
+		quic, _ := stream["quicSettings"].(map[string]interface{})
+		params["quicSecurity"] = quic["security"].(string)
+		params["key"] = quic["key"].(string)
+		header := quic["header"].(map[string]interface{})
+		params["headerType"] = header["type"].(string)
+	case "grpc":
+		grpc, _ := stream["grpcSettings"].(map[string]interface{})
+		params["serviceName"] = grpc["serviceName"].(string)
+	}
+
+	security, _ := stream["security"].(string)
+	if security == "tls" {
+		params["security"] = "tls"
+		tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
+		alpns, _ := tlsSetting["alpn"].([]interface{})
+		var alpn []string
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		if len(alpn) > 0 {
+			params["alpn"] = strings.Join(alpn, ",")
+		}
+		tlsSettings, _ := searchKey(tlsSetting, "settings")
+		if tlsSetting != nil {
+			if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
+				params["sni"], _ = sniValue.(string)
+			}
+			if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
+				if insecure.(bool) {
+					params["allowInsecure"] = "1"
+				}
+			}
+		}
+
+		serverName, _ := tlsSetting["serverName"].(string)
+		if serverName != "" {
+			address = serverName
+		}
+	}
+
+	if security == "xtls" {
+		params["security"] = "xtls"
+		xtlsSetting, _ := stream["xtlsSettings"].(map[string]interface{})
+		alpns, _ := xtlsSetting["alpn"].([]interface{})
+		var alpn []string
+		for _, a := range alpns {
+			alpn = append(alpn, a.(string))
+		}
+		if len(alpn) > 0 {
+			params["alpn"] = strings.Join(alpn, ",")
+		}
+
+		xtlsSettings, _ := searchKey(xtlsSetting, "settings")
+		if xtlsSetting != nil {
+			if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
+				params["sni"], _ = sniValue.(string)
+			}
+			if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+			if insecure, ok := searchKey(xtlsSettings, "allowInsecure"); ok {
+				if insecure.(bool) {
+					params["allowInsecure"] = "1"
+				}
+			}
+		}
+
+		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+			params["flow"] = clients[clientIndex].Flow
+		}
+
+		serverName, _ := xtlsSetting["serverName"].(string)
+		if serverName != "" {
+			address = serverName
+		}
+	}
+
+	link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
+
+	url, _ := url.Parse(link)
+	q := url.Query()
+
+	for k, v := range params {
+		q.Add(k, v)
+	}
+
+	// Set the new query values on the URL
+	url.RawQuery = q.Encode()
+
+	url.Fragment = email
+	return url.String()
+}
+
+func searchKey(data interface{}, key string) (interface{}, bool) {
+	switch val := data.(type) {
+	case map[string]interface{}:
+		for k, v := range val {
+			if k == key {
+				return v, true
+			}
+			if result, ok := searchKey(v, key); ok {
+				return result, true
+			}
+		}
+	case []interface{}:
+		for _, v := range val {
+			if result, ok := searchKey(v, key); ok {
+				return result, true
+			}
+		}
+	}
+	return nil, false
+}
+
+func searchHost(headers interface{}) string {
+	data, _ := headers.(map[string]interface{})
+	for k, v := range data {
+		if strings.EqualFold(k, "host") {
+			switch v.(type) {
+			case []interface{}:
+				hosts, _ := v.([]interface{})
+				return hosts[0].(string)
+			case interface{}:
+				return v.(string)
+			}
+		}
+	}
+
+	return ""
+}

+ 24 - 8
web/service/tgbot.go

@@ -160,14 +160,14 @@ func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bo
 		t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
 	case "inbounds":
 		t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
-	case "exhausted_soon":
+	case "deplete_soon":
 		t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
 	case "get_backup":
 		t.sendBackup(callbackQuery.From.ID)
 	case "client_traffic":
 		t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
 	case "client_commands":
-		t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
+		t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess/vless and Password for Trojan.")
 	case "commands":
 		t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
 	}
@@ -190,7 +190,7 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
 		),
 		tgbotapi.NewInlineKeyboardRow(
 			tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
-			tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
+			tgbotapi.NewInlineKeyboardButtonData("Deplete soon", "deplete_soon"),
 		),
 		tgbotapi.NewInlineKeyboardRow(
 			tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
@@ -363,6 +363,11 @@ func (t *Tgbot) getInboundUsages() string {
 }
 
 func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
+	if len(tgUserName) == 0 {
+		msg := "Your configuration is not found!\nYou should configure your telegram username and ask Admin to add it to your configuration."
+		t.SendMsgToTgbot(chatId, msg)
+		return
+	}
 	traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
 	if err != nil {
 		logger.Warning(err)
@@ -373,11 +378,14 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
 	if len(traffics) == 0 {
 		msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
 		t.SendMsgToTgbot(chatId, msg)
+		return
 	}
 	for _, traffic := range traffics {
 		expiryTime := ""
 		if traffic.ExpiryTime == 0 {
 			expiryTime = "♾Unlimited"
+		} else if traffic.ExpiryTime < 0 {
+			expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
 		} else {
 			expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 		}
@@ -412,6 +420,8 @@ func (t *Tgbot) searchClient(chatId int64, email string) {
 		expiryTime := ""
 		if traffic.ExpiryTime == 0 {
 			expiryTime = "♾Unlimited"
+		} else if traffic.ExpiryTime < 0 {
+			expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
 		} else {
 			expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 		}
@@ -450,6 +460,8 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 			expiryTime := ""
 			if traffic.ExpiryTime == 0 {
 				expiryTime = "♾Unlimited"
+			} else if traffic.ExpiryTime < 0 {
+				expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
 			} else {
 				expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 			}
@@ -483,6 +495,8 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
 	expiryTime := ""
 	if traffic.ExpiryTime == 0 {
 		expiryTime = "♾Unlimited"
+	} else if traffic.ExpiryTime < 0 {
+		expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
 	} else {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 	}
@@ -507,13 +521,13 @@ func (t *Tgbot) getExhausted() string {
 	var disabledInbounds []model.Inbound
 	var disabledClients []xray.ClientTraffic
 	output := ""
-	TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
+	TrafficThreshold, err := t.settingService.GetTrafficDiff()
 	if err == nil && TrafficThreshold > 0 {
 		trDiff = int64(TrafficThreshold) * 1073741824
 	}
-	ExpireThreshold, err := t.settingService.GetTgExpireDiff()
+	ExpireThreshold, err := t.settingService.GetExpireDiff()
 	if err == nil && ExpireThreshold > 0 {
-		exDiff = int64(ExpireThreshold) * 84600000
+		exDiff = int64(ExpireThreshold) * 86400000
 	}
 	inbounds, err := t.inboundService.GetAllInbounds()
 	if err != nil {
@@ -541,7 +555,7 @@ func (t *Tgbot) getExhausted() string {
 			disabledInbounds = append(disabledInbounds, *inbound)
 		}
 	}
-	output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
+	output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
 	if len(exhaustedInbounds) > 0 {
 		output += "Exhausted Inbounds:\r\n"
 		for _, inbound := range exhaustedInbounds {
@@ -553,13 +567,15 @@ func (t *Tgbot) getExhausted() string {
 			}
 		}
 	}
-	output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
+	output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Exhausted: %d\r\n🔜 Deplete soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
 	if len(exhaustedClients) > 0 {
 		output += "Exhausted Clients:\r\n"
 		for _, traffic := range exhaustedClients {
 			expiryTime := ""
 			if traffic.ExpiryTime == 0 {
 				expiryTime = "♾Unlimited"
+			} else if traffic.ExpiryTime < 0 {
+				expiryTime += fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
 			} else {
 				expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 			}

+ 24 - 3
web/service/xray.go

@@ -84,15 +84,16 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 		clients, ok := settings["clients"].([]interface{})
 		if ok {
 			// check users active or not
-
 			clientStats := inbound.ClientStats
 			for _, clientTraffic := range clientStats {
 
+				indexDecrease := 0
 				for index, client := range clients {
 					c := client.(map[string]interface{})
 					if c["email"] == clientTraffic.Email {
 						if !clientTraffic.Enable {
-							clients = RemoveIndex(clients, index)
+							clients = RemoveIndex(clients, index-indexDecrease)
+							indexDecrease++
 							logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
 
 						}
@@ -101,7 +102,27 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 				}
 
 			}
-			settings["clients"] = clients
+
+			// clear client config for additional parameters
+			var final_clients []interface{}
+			for _, client := range clients {
+
+				c := client.(map[string]interface{})
+
+				if c["enable"] != nil {
+					if enable, ok := c["enable"].(bool); ok && !enable {
+						continue
+					}
+				}
+				for key := range c {
+					if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
+						delete(c, key)
+					}
+				}
+				final_clients = append(final_clients, interface{}(c))
+			}
+
+			settings["clients"] = final_clients
 			modifiedSettings, err := json.Marshal(settings)
 			if err != nil {
 				return nil, err

+ 14 - 4
web/translation/translate.en_US.toml

@@ -33,8 +33,11 @@
 "host" = "Host"
 "path" = "Path"
 "camouflage" = "Camouflage"
+"status" = "Status"
 "enabled" = "Enabled"
 "disabled" = "Disabled"
+"depleted" = "Depleted"
+"depletingSoon" = "Depleting soon"
 "domainName" = "Domain name"
 "additional" = "Alter"
 "monitor" = "Listen IP"
@@ -140,11 +143,15 @@
 "resetAllTrafficCancelText" = "Cancel"
 "IPLimit" = "IP Limit"
 "IPLimitDesc" = "disable inbound if more than entered count (0 for disable limit ip)"
+"resetAllClientTraffics" = "Reset Clients Traffic"
+"resetAllClientTrafficTitle" = "Reset all clients traffic"
+"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
 "Email" = "Email"
 "EmailDesc" = "The Email Must Be Completely Unique"
 "IPLimitlog" = "IP Log"
 "IPLimitlogDesc" = "IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)"
 "IPLimitlogclear" = "Clear The Log"
+"setDefaultCert" = "Set cert from panel"
 
 [pages.client]
 "add" = "Add client"
@@ -158,6 +165,9 @@
 "last" = "Last"
 "prefix" = "Prefix"
 "postfix" = "postfix"
+"delayedStart" = "Start after first use"
+"expireDays" = "Expire days"
+"days" = "day(s)"
 
 [pages.inbounds.toasts]
 "obtain" = "Obtain"
@@ -231,10 +241,10 @@
 "telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
 "tgNotifyBackup" = "Database backup"
 "tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
-"tgNotifyExpireTimeDiff" = "Remained time threshold"
-"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
-"tgNotifyTrafficDiff" = "Remained traffic threshold"
-"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
+"expireTimeDiff" = "Exhaustion time threshold"
+"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
+"trafficDiff" = "Exhaustion traffic threshold"
+"trafficDiffDesc" = "Detect exhaustion before finishing traffic (unit:GB)"
 "tgNotifyCpu" = "CPU percentage alert threshold"
 "tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
 "timeZonee" = "Time Zone"

+ 18 - 10
web/translation/translate.fa_IR.toml

@@ -33,8 +33,11 @@
 "host" = "آدرس"
 "path" = "مسیر"
 "camouflage" = "استتار"
+"status" = "وضعیت"
 "enabled" = "فعال"
 "disabled" = "غیرفعال"
+"depleted" = "منقضی"
+"depletingSoon" = "در حال انقضا"
 "domainName" = "آدرس دامنه"
 "additional" = "آی دی جایگزین"
 "monitor" = "آی پی اتصال"
@@ -133,11 +136,12 @@
 "cloneInbound" = "ایجاد"
 "cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
 "cloneInboundOk" = "ساختن شبیه ساز"
-"resetAllTraffic" = "ریست ترافیک کل ورودی ها"
-"resetAllTrafficTitle" = "ریست ترافیک کل ورودی ها"
-"resetAllTrafficContent" = "آیا مطمئن هستید که تمام ترافیک ورودی ها را ریست می کنید؟"
-"resetAllTrafficOkText" = "بله"
-"resetAllTrafficCancelText" = "انصراف"
+"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
+"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
+"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
+"resetAllClientTraffics" = "ریست ترافیک کاربران"
+"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
+"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
 "IPLimit" = "محدودیت ای پی"
 "IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
 "Email" = "ایمیل"
@@ -145,6 +149,7 @@
 "IPLimitlog" = "گزارش ها"
 "IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
 "IPLimitlogclear" = "پاک کردن گزارش ها"
+"setDefaultCert" = "استفاده از گواهی پنل"
 
 [pages.client]
 "add" = "کاربر جدید"
@@ -158,6 +163,9 @@
 "last" = "تا"
 "prefix" = "پیشوند"
 "postfix" = "پسوند"
+"delayedStart" = "شروع بعد از اولین استفاده"
+"expireDays" = "روزهای اعتبار"
+"days" = "(روز)"
 
 [pages.inbounds.toasts]
 "obtain" = "Obtain"
@@ -228,13 +236,13 @@
 "telegramChatId" = "آی دی تلگرام مدیریت"
 "telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
 "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
-"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
+"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
 "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
 "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
-"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
-"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
-"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
-"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
+"expireTimeDiff" = "آستانه زمان باقی مانده"
+"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
+"trafficDiff" = "آستانه ترافیک باقی مانده"
+"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
 "tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
 "tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
 "timeZonee" = "منظقه زمانی"

+ 16 - 8
web/translation/translate.zh_Hans.toml

@@ -33,8 +33,11 @@
 "host" = "主持人"
 "path" = "小路"
 "camouflage" = "伪装"
+"status" = "状态"
 "enabled" = "开启"
 "disabled" = "关闭"
+"depleted" = "耗尽"
+"depletingSoon" = "即将耗尽"
 "domainName" = "域名"
 "additional" = "额外"
 "monitor" = "监听"
@@ -69,8 +72,8 @@
 "memory" = "内存"
 "hard" = "硬盘"
 "xrayStatus" = "xray 状态"
-"stopXray" = "停止 Xray"
-"restartXray" = "重启 Xray"
+"stopXray" = "停止"
+"restartXray" = "重启"
 "xraySwitch" = "切换版本"
 "xraySwitchClick" = "点击你想切换的版本"
 "xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
@@ -136,8 +139,9 @@
 "resetAllTraffic" = "重置所有入站流量"
 "resetAllTrafficTitle" = "重置所有入站流量"
 "resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
-"resetAllTrafficOkText" = "确认"
-"resetAllTrafficCancelText" = "取消"
+"resetAllClientTraffics" = "重置客户端流量"
+"resetAllClientTrafficTitle" = "重置所有客户端流量"
+"resetAllClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
 "IPLimit" = "IP限制"
 "IPLimitDesc" = "如果超过输入的计数则禁用入站(0 表示禁用限制 ip)"
 "Email" = "电子邮件"
@@ -145,6 +149,7 @@
 "IPLimitlog" = "IP日志"
 "IPLimitlogDesc" = "IP 历史日志 (通过IP限制禁用inbound之前,需要清空日志)"
 "IPLimitlogclear" = "清除日志"
+"setDefaultCert" = "从面板设置证书"
 
 [pages.client]
 "add" = "添加客户端"
@@ -158,6 +163,9 @@
 "last" = "最后"
 "prefix" = "前缀"
 "postfix" = "后缀"
+"delayedStart" = "首次使用后开始"
+"expireDays" = "过期天数"
+"days" = "天"
 
 [pages.inbounds.toasts]
 "obtain" = "获取"
@@ -231,10 +239,10 @@
 "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
 "tgNotifyBackup" = "数据库备份"
 "tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
-"tgNotifyExpireTimeDiff" = "剩余时间阈值"
-"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
-"tgNotifyTrafficDiff" = "剩余流量阈值"
-"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知(单位:GB)"
+"expireTimeDiff" = "耗尽时间阈值"
+"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
+"trafficDiff" = "耗尽流量阈值"
+"trafficDiffDesc" = "完成流量前检测耗尽(单位:GB)"
 "tgNotifyCpu" = "CPU 百分比警报阈值"
 "tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
 "timeZonee" = "时区"

+ 10 - 3
web/web.go

@@ -33,6 +33,9 @@ import (
 //go:embed assets/*
 var assetsFS embed.FS
 
+//go:embed assets/favicon.ico
+var favicon []byte
+
 //go:embed html/*
 var htmlFS embed.FS
 
@@ -85,6 +88,7 @@ type Server struct {
 	server *controller.ServerController
 	xui    *controller.XUIController
 	api    *controller.APIController
+	sub    *controller.SUBController
 
 	xrayService    service.XrayService
 	settingService service.SettingService
@@ -156,9 +160,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	}
 
 	engine := gin.Default()
-	
+
 	// Add favicon
-	engine.StaticFile("/favicon.ico", "web/assets/favicon.ico")
+	engine.GET("/favicon.ico", func(c *gin.Context) {
+		c.Data(200, "image/x-icon", favicon)
+	})
 
 	secret, err := s.settingService.GetSecret()
 	if err != nil {
@@ -211,6 +217,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	s.server = controller.NewServerController(g)
 	s.xui = controller.NewXUIController(g)
 	s.api = controller.NewAPIController(g)
+	s.sub = controller.NewSUBController(g)
 
 	return engine, nil
 }
@@ -312,7 +319,7 @@ func (s *Server) startTask() {
 
 	// Check the inbound traffic every 30 seconds that the traffic exceeds and expires
 	s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
-	
+
 	// check client ips from log file every 10 sec
 	s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())