Browse Source

Merge branch 'MHSanaei:main' into main

Mohammad Movaghari 1 year ago
parent
commit
c575425292
47 changed files with 1971 additions and 506 deletions
  1. 27 3
      README.md
  2. 1 1
      config/version
  3. 1 1
      database/db.go
  4. 5 2
      database/model/model.go
  5. 18 18
      go.mod
  6. 38 42
      go.sum
  7. 8 24
      install.sh
  8. BIN
      media/1.png
  9. BIN
      media/2.png
  10. BIN
      media/3.png
  11. BIN
      media/4.png
  12. 52 5
      web/assets/css/custom.css
  13. 2 2
      web/assets/js/model/models.js
  14. 303 119
      web/assets/js/model/xray.js
  15. 53 0
      web/assets/js/util/utils.js
  16. 40 43
      web/controller/api.go
  17. 27 1
      web/controller/inbound.go
  18. 28 2
      web/controller/server.go
  19. 31 0
      web/controller/setting.go
  20. 42 0
      web/controller/sub.go
  21. 2 2
      web/entity/entity.go
  22. 1 0
      web/html/common/head.html
  23. 32 10
      web/html/xui/client_bulk_modal.html
  24. 16 4
      web/html/xui/client_modal.html
  25. 20 5
      web/html/xui/form/client.html
  26. 3 2
      web/html/xui/form/protocol/trojan.html
  27. 14 13
      web/html/xui/form/protocol/vless.html
  28. 2 1
      web/html/xui/form/protocol/vmess.html
  29. 1 1
      web/html/xui/form/stream/stream_tcp.html
  30. 53 2
      web/html/xui/form/tls_settings.html
  31. 6 2
      web/html/xui/inbound_client_table.html
  32. 43 13
      web/html/xui/inbound_info_modal.html
  33. 4 0
      web/html/xui/inbound_modal.html
  34. 178 70
      web/html/xui/inbounds.html
  35. 67 26
      web/html/xui/index.html
  36. 2 2
      web/html/xui/setting.html
  37. 113 32
      web/service/inbound.go
  38. 43 2
      web/service/server.go
  39. 18 18
      web/service/setting.go
  40. 555 0
      web/service/sub.go
  41. 26 10
      web/service/tgbot.go
  42. 24 3
      web/service/xray.go
  43. 16 4
      web/translation/translate.en_US.toml
  44. 20 10
      web/translation/translate.fa_IR.toml
  45. 18 8
      web/translation/translate.zh_Hans.toml
  46. 10 3
      web/web.go
  47. 8 0
      x-ui.sh

+ 27 - 3
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,18 +91,40 @@ 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
 
+
+## API routes
+
+- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
+- `/xui/API/inbounds` base for following actions:
+
+| Method | Path | Action |
+| ------------- | ------------- | ------------- |
+| GET | "/list" | Get all inbounds |
+| GET | "/get/:id" | Get inbound with inbound.id |
+| POST | "/add" | Add inbound |
+| POST | "/del/:id" | Delete Inbound |
+| POST | "/update/:id" | Update Inbound |
+| POST | "/clientIps/:email" | Client Ip address |
+| POST | "/clearClientIps/:email" | Clear Client Ip address |
+| POST | "/addClient/" | Add Client to inbound |
+| POST | "/delClient/:email" | Delete Client |
+| POST | "/updateClient/:index" | Update Client |
+| POST | "/:id/resetClientTraffic/:email" | Reset Client's Traffic |
+| POST | "/resetAllTraffics" | Reset traffics of all inbounds |
+| POST | "/resetAllClientTraffics/:id" | Reset traffics of all clients in an inbound |
+
 # A Special Thanks To
 - [alireza0](https://github.com/alireza0/)
-- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
+- [FranzKafkaYu](https://github.com/FranzKafkaYu)
 
 # Suggestion System
 - Ubuntu 20.04+

+ 1 - 1
config/version

@@ -1 +1 @@
-1.1.4
+1.2.2

+ 1 - 1
database/db.go

@@ -92,7 +92,7 @@ func InitDB(dbPath string) error {
 	if err != nil {
 		return err
 	}
-	
+
 	return nil
 }
 

+ 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" form:"enable"`
+	TgID       string `json:"tgId" form:"tgId"`
+	SubID      string `json:"subId" form:"subId"`
 }

+ 18 - 18
go.mod

@@ -8,6 +8,7 @@ require (
 	github.com/gin-gonic/gin v1.9.0
 	github.com/go-cmd/cmd v1.4.1
 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
+	github.com/goccy/go-json v0.10.2
 	github.com/nicksnyder/go-i18n/v2 v2.2.1
 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
 	github.com/pelletier/go-toml/v2 v2.0.7
@@ -15,22 +16,21 @@ require (
 	github.com/shirou/gopsutil/v3 v3.23.3
 	github.com/xtls/xray-core v1.8.0
 	go.uber.org/atomic v1.10.0
-	golang.org/x/text v0.8.0
+	golang.org/x/text v0.9.0
 	google.golang.org/grpc v1.54.0
-	gorm.io/driver/sqlite v1.4.4
-	gorm.io/gorm v1.24.6
+	gorm.io/driver/sqlite v1.5.0
+	gorm.io/gorm v1.25.0
 )
 
 require (
 	github.com/BurntSushi/toml v1.2.1 // indirect
-	github.com/bytedance/sonic v1.8.2 // indirect
+	github.com/bytedance/sonic v1.8.7 // indirect
 	github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-ole/go-ole v1.2.6 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-playground/validator/v10 v10.11.2 // indirect
-	github.com/goccy/go-json v0.10.0 // indirect
+	github.com/go-playground/validator/v10 v10.12.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/gorilla/context v1.1.1 // indirect
 	github.com/gorilla/securecookie v1.1.1 // indirect
@@ -39,25 +39,25 @@ require (
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
-	github.com/leodido/go-urn v1.2.1 // indirect
-	github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de // indirect
-	github.com/mattn/go-isatty v0.0.17 // indirect
+	github.com/leodido/go-urn v1.2.3 // indirect
+	github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
+	github.com/mattn/go-isatty v0.0.18 // indirect
 	github.com/mattn/go-sqlite3 v1.14.16 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
-	github.com/pires/go-proxyproto v0.6.2 // indirect
+	github.com/pires/go-proxyproto v0.7.0 // indirect
 	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
-	github.com/shoenig/go-m1cpu v0.1.4 // indirect
+	github.com/shoenig/go-m1cpu v0.1.5 // indirect
 	github.com/tklauser/go-sysconf v0.3.11 // indirect
 	github.com/tklauser/numcpus v0.6.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
-	github.com/ugorji/go/codec v1.2.10 // indirect
+	github.com/ugorji/go/codec v1.2.11 // indirect
 	github.com/yusufpapurcu/wmi v1.2.2 // indirect
-	golang.org/x/arch v0.2.0 // indirect
-	golang.org/x/crypto v0.7.0 // indirect
-	golang.org/x/net v0.8.0 // indirect
-	golang.org/x/sys v0.6.0 // indirect
-	google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
-	google.golang.org/protobuf v1.29.1 // indirect
+	golang.org/x/arch v0.3.0 // indirect
+	golang.org/x/crypto v0.8.0 // indirect
+	golang.org/x/net v0.9.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )

+ 38 - 42
go.sum

@@ -9,8 +9,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
 github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
 github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
 github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.8.2 h1:Eq1oE3xWIBE3tj2ZtJFK1rDAx7+uA4bRytozVhXMHKY=
-github.com/bytedance/sonic v1.8.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
+github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
 github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
 github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
@@ -41,14 +41,14 @@ github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+
 github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
 github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
 github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
-github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
-github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/go-playground/validator/v10 v10.12.0 h1:E4gtWgxWxp8YSxExrQFv5BpCahla0PVF2oTTEYaWQGI=
+github.com/go-playground/validator/v10 v10.12.0/go.mod h1:hCAPuzYvKdP33pxWa+2+6AIKXEKqjIUyqsNCtbsSJrA=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
 github.com/go-test/deep v1.0.7 h1:/VSMRlnY/JSyqxQUzQLKVMAskpY/NZKFA5j2P+0pP2M=
-github.com/goccy/go-json v0.10.0 h1:mXKd9Qw4NuzShiRlOXKews24ufknHO7gx30lsDyokKA=
-github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
 github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
 github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@@ -73,7 +73,6 @@ github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/z
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
-github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@@ -84,18 +83,16 @@ github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
 github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
-github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
-github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
-github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/leodido/go-urn v1.2.3 h1:6BE2vPT0lqoz3fmOesHZiaiFh7889ssCo2GMvLCfiuA=
+github.com/leodido/go-urn v1.2.3/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
 github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
-github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de h1:V53FWzU6KAZVi1tPp5UIsMoUWJ2/PNwYIDXnu7QuBCE=
-github.com/lufia/plan9stats v0.0.0-20230110061619-bbe2e5e100de/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
+github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
+github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
 github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
-github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
-github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
+github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
 github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
 github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
@@ -116,8 +113,8 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v
 github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
 github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
 github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
-github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
-github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
+github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
+github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -132,15 +129,15 @@ github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:UL
 github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
 github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
 github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
-github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
 github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
 github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
 github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
 github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
 github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
 github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
-github.com/shoenig/go-m1cpu v0.1.4 h1:SZPIgRM2sEF9NJy50mRHu9PKGwxyyTTJIWvCtgVbozs=
 github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
+github.com/shoenig/go-m1cpu v0.1.5 h1:LF57Z/Fpb/WdGLjt2HZilNnmZOxg/q2bSKTQhgbrLrQ=
+github.com/shoenig/go-m1cpu v0.1.5/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
 github.com/shoenig/test v0.6.3 h1:GVXWJFk9PiOjN0KoJ7VrJGH6uLPnqxR7/fe3HUPfE0c=
 github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -148,7 +145,6 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -165,8 +161,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
-github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
-github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
 github.com/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
 github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
@@ -179,14 +175,14 @@ go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfG
 go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
 go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
 golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.2.0 h1:W1sUEHXiJTfjaFJ5SLo0N6lZn+0eO5gWD1MFeTGqQEY=
-golang.org/x/arch v0.2.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
-golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -196,8 +192,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
-golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -213,10 +209,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -224,8 +220,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
-golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -236,16 +232,16 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
-google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
 google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
 google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
-google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -253,11 +249,11 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
-gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
-gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
-gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
-gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/driver/sqlite v1.5.0 h1:zKYbzRCpBrT1bNijRnxLDJWPjVfImGEn0lSnUY5gZ+c=
+gorm.io/driver/sqlite v1.5.0/go.mod h1:kDMDfntV9u/vuMmz8APHtHF0b4nyBB7sfCieC6G8k8I=
+gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
+gorm.io/gorm v1.25.0 h1:+KtYtb2roDz14EQe4bla8CbQlmb9dN3VejSai3lprfU=
+gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
 gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
 lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
 rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

+ 8 - 24
install.sh

@@ -66,33 +66,17 @@ else
     echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
 fi
 
-# This function installs the base packages required for most scripts
 install_base() {
-    # Store the package names in a variable for easy modification
-    local packages="wget curl tar"
-
-    # Check for the package managers and install the packages if they are not already installed
-    if ! command -v wget >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1 || ! command -v tar >/dev/null 2>&1; then
-        if command -v apt >/dev/null 2>&1; then
-            apt-get update && apt-get install -y $packages
-        elif command -v dnf >/dev/null 2>&1; then
-            dnf install -y $packages
-        elif command -v yum >/dev/null 2>&1; then
-            yum install -y $packages
-        else
-            echo "ERROR: No package managers found. Please install wget, curl, and tar manually."
-            return 1
-        fi
-
-        # Print a confirmation message after the installation is complete
-        echo "The following packages have been successfully installed: $packages"
-    else
-        # Print a message confirming that the packages are already installed
-        echo "The following packages are already installed: $packages"
-    fi
+    case "${release}" in
+        centos|fedora)
+            yum install -y -q wget curl tar
+            ;;
+        *)
+            apt install -y -q wget curl tar
+            ;;
+    esac
 }
 
-
 #This function will be called when user installed x-ui out of sercurity
 config_after_install() {
     echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"

BIN
media/1.png


BIN
media/2.png


BIN
media/3.png


BIN
media/4.png


+ 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 = "";
 

+ 303 - 119
web/assets/js/model/xray.js

@@ -91,7 +91,11 @@ const UTLS_FINGERPRINT = {
     UTLS_RANDOMIZED: "randomized",
 };
 
+const bytesToHex = e => Array.from(e).map(e => e.toString(16).padStart(2, 0)).join('');
+const hexToBytes = e => new Uint8Array(e.match(/[0-9a-f]{2}/gi).map(e => parseInt(e, 16)));
+
 const ALPN_OPTION = {
+    H3: "h3",
     H2: "h2",
     HTTP1: "http/1.1",
 };
@@ -105,7 +109,6 @@ Object.freeze(XTLS_FLOW_CONTROL);
 Object.freeze(TLS_FLOW_CONTROL);
 Object.freeze(TLS_VERSION_OPTION);
 Object.freeze(TLS_CIPHER_OPTION);
-Object.freeze(UTLS_FINGERPRINT);
 Object.freeze(ALPN_OPTION);
 
 class XrayCommonClass {
@@ -166,27 +169,25 @@ 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 = {};
         }
-        return new TcpStreamSettings(
+        return new TcpStreamSettings(json.acceptProxyProtocol,
             header.type,
-            json.acceptProxyProtocol,
             TcpStreamSettings.TcpRequest.fromJson(header.request),
             TcpStreamSettings.TcpResponse.fromJson(header.response),
         );
@@ -194,21 +195,21 @@ class TcpStreamSettings extends XrayCommonClass {
 
     toJson() {
         return {
+            acceptProxyProtocol: this.acceptProxyProtocol,
             header: {
                 type: this.type,
                 request: this.type === 'http' ? this.request.toJson() : undefined,
                 response: this.type === 'http' ? this.response.toJson() : undefined,
             },
-            acceptProxyProtocol: this.acceptProxyProtocol,
         };
     }
 }
 
 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 +243,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 +262,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 +282,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 +475,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 +489,7 @@ class TlsStreamSettings extends XrayCommonClass {
         this.cipherSuites = cipherSuites;
         this.certs = certificates;
         this.alpn = alpn;
-		this.settings = settings;
+        this.settings = settings;
     }
 
     addCert(cert) {
@@ -497,15 +502,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 +518,7 @@ class TlsStreamSettings extends XrayCommonClass {
             json.cipherSuites,
             certs,
             json.alpn,
-			settings,
+            settings,
         );
     }
 
@@ -526,7 +531,6 @@ class TlsStreamSettings extends XrayCommonClass {
             certificates: TlsStreamSettings.toJsonArray(this.certs),
             alpn: this.alpn,
             settings: TlsStreamSettings.toJsonArray(this.settings),
-			
         };
     }
 }
@@ -573,44 +577,105 @@ 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 RealityStreamSettings extends XrayCommonClass {
+    
+    constructor(
+        show = false,xver = 0,
+        fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX,
+        dest = 'yahoo.com:443',
+        serverNames = 'yahoo.com,www.yahoo.com',
+        privateKey = RandomUtil.randomX25519PrivateKey(),
+        publicKey = '',
+        minClient = '',
+        maxClient = '',
+        maxTimediff = 0,
+        shortIds = RandomUtil.randowShortId() 
+        )   
+        {
+        super();
+        this.show = show;
+        this.xver = xver;
+        this.fingerprint = fingerprint;
+        this.dest = dest;
+        this.serverNames = serverNames instanceof Array ? serverNames.join(",") : serverNames;
+        this.privateKey = privateKey;
+        this.publicKey = RandomUtil.randomX25519PublicKey(this.privateKey);
+        this.minClient = minClient;
+        this.maxClient = maxClient;
+        this.maxTimediff = maxTimediff;
+        this.shortIds = shortIds instanceof Array ? shortIds.join(",") : shortIds; 
+        }
+        static fromJson(json = {}) {
+        return new RealityStreamSettings(
+            json.show,
+            json.xver,
+            json.fingerprint,
+            json.dest,
+            json.serverNames,
+            json.privateKey,
+            json.publicKey,
+            json.minClient,
+            json.maxClient,
+            json.maxTimediff,
+            json.shortIds  
+        );
+        }
+        toJson() {
+        return {
+            show: this.show,
+            xver: this.xver,
+            fingerprint: this.fingerprint,
+            dest: this.dest,
+            serverNames: this.serverNames.split(/,|,|\s+/),
+            privateKey: this.privateKey,
+            publicKey: this.publicKey,
+            minClient: this.minClient,
+            maxClient: this.maxClient,
+            maxTimediff: this.maxTimediff,
+            shortIds: this.shortIds.split(/,|,|\s+/)
+            };
+        }
+    }
 
 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(),
+        realitySettings = new RealityStreamSettings(),
+        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;
         this.tls = tlsSettings;
+        this.reality = realitySettings;
         this.tcp = tcpSettings;
         this.kcp = kcpSettings;
         this.ws = wsSettings;
@@ -643,17 +708,34 @@ class StreamSettings extends XrayCommonClass {
         }
     }
 
-    static fromJson(json={}) {
-        let tls;
+    //for Reality
+    get isReality() {
+        return this.security === "reality";
+    }
+
+    set isReality(isReality) {
+        if (isReality) {
+            this.security = "reality";
+        } else {
+            this.security = "none";
+        }
+    }
+    
+    static fromJson(json = {}) {
+        let tls, reality;
         if (json.security === "xtls") {
             tls = TlsStreamSettings.fromJson(json.XTLSSettings);
-        } else {
+        } else if (json.security === "tls") {
             tls = TlsStreamSettings.fromJson(json.tlsSettings);
         }
+        if (json.security === "reality") {
+            reality = RealityStreamSettings.fromJson(json.realitySettings)
+        }
         return new StreamSettings(
             json.network,
             json.security,
             tls,
+            reality,
             TcpStreamSettings.fromJson(json.tcpSettings),
             KcpStreamSettings.fromJson(json.kcpSettings),
             WsStreamSettings.fromJson(json.wsSettings),
@@ -671,6 +753,7 @@ class StreamSettings extends XrayCommonClass {
             tlsSettings: this.isTls ? this.tls.toJson() : undefined,
             XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined,
             tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
+            realitySettings: this.isReality ? this.reality.toJson() : undefined,
             kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
             wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
             httpSettings: network === 'http' ? this.http.toJson() : undefined,
@@ -728,20 +811,23 @@ 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';
     }
 
     set tls(isTls) {
         if (isTls) {
+            this.xtls = false;
+            this.reality = false;
             this.stream.security = 'tls';
         } else {
             this.stream.security = 'none';
@@ -754,12 +840,32 @@ class Inbound extends XrayCommonClass {
 
     set XTLS(isXTLS) {
         if (isXTLS) {
+            this.xtls = false;
+            this.reality = false;
             this.stream.security = 'xtls';
         } else {
             this.stream.security = 'none';
         }
     }
 
+    //for Reality
+    get reality() {
+        if (this.stream.security === "reality") {
+            return this.network === "tcp" || this.network === "grpc" || this.network === "http";
+        }
+        return false;
+    }
+
+    set reality(isReality) {
+        if (isReality) {
+            this.tls = false;
+            this.xtls = false;
+            this.stream.security = "reality";
+        } else {
+            this.stream.security = "none";
+        }
+    }
+
     get network() {
         return this.stream.network;
     }
@@ -918,16 +1024,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,10 +1061,21 @@ class Inbound extends XrayCommonClass {
                 return false;
         }
     }
-	
+
+    canEnableReality() {
+        switch (this.protocol) {
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+                break;
+            default:
+                return false;
+        }
+        return this.network === "tcp" || this.network === "grpc" || this.network === "http";
+    }
+
     //this is used for xtls-rprx-vision
     canEnableTlsFlow() {
-        if ((this.stream.security === 'tls') && (this.network === "tcp")) {
+        if (((this.stream.security === 'tls') || (this.stream.security === 'reality')) && (this.network === "tcp")) {
             switch (this.protocol) {
                 case Protocols.VLESS:
                     return true;
@@ -968,11 +1085,10 @@ class Inbound extends XrayCommonClass {
         }
         return false;
     }
-	
+
     canSetTls() {
         return this.canEnableTls();
     }
-     
 
     canEnableXTLS() {
         switch (this.protocol) {
@@ -989,7 +1105,8 @@ class Inbound extends XrayCommonClass {
         switch (this.protocol) {
             case Protocols.VMESS:
             case Protocols.VLESS:
-			case Protocols.TROJAN:
+            case Protocols.TROJAN:
+            case Protocols.SHADOWSOCKS:
                 return true;
             default:
                 return false;
@@ -1065,7 +1182,7 @@ class Inbound extends XrayCommonClass {
                 address = this.stream.tls.server;
             }
         }
-		
+        
         let obj = {
             v: '2',
             ps: remark,
@@ -1078,7 +1195,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,26 +1265,46 @@ 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);
+        }
 
+        if (this.reality) {
+            params.set("security", "reality");
+            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
+                params.set("sni", this.stream.reality.serverNames.split(/,|,|\s+/)[0]);
+            }
+            if (this.stream.reality.publicKey != "") {
+                //params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
+                params.set("pbk", this.stream.reality.publicKey);
+            }
+            if (this.stream.network === 'tcp') {
+                params.set("flow", this.settings.vlesses[clientIndex].flow);
+            }
+            if (this.stream.reality.shortIds != "") {
+                params.set("sid", this.stream.reality.shortIds);
+            }
+            if (this.stream.reality.fingerprint != "") {
+                params.set("fp", this.stream.reality.fingerprint);
+            }
+        }
+        
         const link = `vless://${uuid}@${address}:${port}`;
         const url = new URL(link);
         for (const [key, value] of params) {
@@ -1177,13 +1314,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 +1328,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 +1383,32 @@ 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 (this.reality) {
+            params.set("security", "reality");
+            if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
+                params.set("sni", this.stream.reality.serverNames.split(/,|,|\s+/)[0]);
+            }
+            if (this.stream.reality.publicKey != "") {
+                //params.set("pbk", Ed25519.getPublicKey(this.stream.reality.privateKey));
+                params.set("pbk", this.stream.reality.publicKey);
+            }
+            if (this.stream.network === 'tcp') {
+                params.set("flow", this.settings.trojans[clientIndex].flow);
+            }
+            if (this.stream.reality.shortIds != "") {
+                params.set("sid", this.stream.reality.shortIds);
+            }
+            if (this.stream.reality.fingerprint != "") {
+                params.set("fp", this.stream.reality.fingerprint);
             }
         }
-		
+
 		if (this.XTLS) {
             params.set("security", "xtls");
             params.set("alpn", this.stream.tls.alpn);
@@ -1259,11 +1416,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 +1451,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 +1466,7 @@ class Inbound extends XrayCommonClass {
                 return (this.genSSLink(address, remark) + '\r\n');
             default: return '';
         }
-}
+    }
 
     static fromJson(json={}) {
         return new Inbound(
@@ -1423,7 +1581,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 +1589,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 +1602,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 +1641,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 +1653,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,
@@ -1499,15 +1666,14 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
     toJson() {
         return {
             clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
-            decryption: this.decryption,
+            decryption: 'none',
             fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
         };
     }
 
 };
 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 +1681,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 +1694,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);
     }
 
@@ -1593,8 +1766,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 +1796,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 +1804,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 +1817,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 +1831,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
             json.limitIp,
             json.totalGB,
             json.expiryTime,
-
+            json.enable,
+            json.tgId,
+            json.subId,
         );
     }
 
@@ -1660,6 +1841,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);
     }
 
@@ -1721,9 +1905,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 +1915,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 +1939,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 +1948,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
             json.address,
             json.port,
             json.network,
-			json.followRedirect,
+            json.followRedirect,
         );
     }
 
@@ -1773,7 +1957,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
             address: this.address,
             port: this.port,
             network: this.network,
-			followRedirect: this.followRedirect,
+            followRedirect: this.followRedirect,
         };
     }
 };

+ 53 - 0
web/assets/js/util/utils.js

@@ -89,6 +89,31 @@ const seq = [
     'U', 'V', 'W', 'X', 'Y', 'Z'
 ];
 
+const shortIdSeq = [
+    'a', 'b', 'c', 'd', 'e', 'f',
+    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+];
+
+const x25519Map = new Map(
+    [
+        ['EH2FWe-Ij_FFAa2u9__-aiErLvVIneP601GOCdlyPWw', "goY3OtfaA4UYbiz7Hn0NysI5QJrK0VT_Chg6RLgUPQU"],
+        ['cKI_6DoMSP1IepeWWXrG3G9nkehl94KYBhagU50g2U0', "VigpKFbSLnHLzBWobZaS1IBmw--giJ51w92y723ajnU"],
+        ['qM2SNyK3NyHB6deWpEP3ITyCGKQFRTna_mlKP0w1QH0', "HYyIGuyNFslmcnNT7mrDdmuXwn4cm7smE_FZbYguKHQ"],
+        ['qCWg5GMEDFd3n1nxDswlIpOHoPUXMLuMOIiLUVzubkI', "rJFC3dUjJxMnVZiUGzmf_LFsJUwFWY-CU5RQgFOHCWM"],
+        ['4NOBxDrEsOhNI3Y3EnVIy_TN-uyBoAjQw6QM0YsOi0s', "CbcY9qc4YuMDJDyyL0OITlU824TBg1O84ClPy27e2RM"],
+        ['eBvFb0M4HpSOwWjtXV8zliiEs_hg56zX4a2LpuuqpEI', "CjulQ2qVIky7ImIfysgQhNX7s_drGLheCGSkVHcLZhc"],
+        ['yEpOzQV04NNcycWVeWtRNTzv5TS-ynTuKRacZCH-6U8', "O9RSr5gSdok2K_tobQnf_scyKVqnCx6C4Jrl7_rCZEQ"],
+        ['CNt6TAUVCwqM6xIBHyni0K3Zqbn2htKQLvLb6XDgh0s', "d9cGLVBrDFS02L2OvkqyqwFZ1Ux3AHs28ehl4Rwiyl0"],
+        ['EInKw-6Wr0rAHXlxxDuZU5mByIzcD3Z-_iWPzXlUL1k', "LlYD2nNVAvyjNvjZGZh4R8PkMIwkc6EycPTvR2LE0nQ"],
+        ['GKIKo7rcXVyle-EUHtGIDtYnDsI6osQmOUl3DTJRAGc', "VcqHivYGGoBkcxOI6cSSjQmneltstkb2OhvO53dyhEM"],
+        ['-FVDzv68IC17fJVlNDlhrrgX44WeBfbhwjWpCQVXGHE', "PGG2EYOvsFt2lAQTD7lqHeRxz2KxvllEDKcUrtizPBU"],
+        ['0H3OJEYEu6XW7woqy7cKh2vzg6YHkbF_xSDTHKyrsn4', "mzevpYbS8kXengBY5p7tt56QE4tS3lwlwRemmkcQeyc"],
+        ['8F8XywN6ci44ES6em2Z0fYYxyptB9uaXY9Hc1WSSPE4', "qCZUdWQZ2H33vWXnOkG8NpxBeq3qn5QWXlfCOWBNkkc"],
+        ['IN0dqfkC10dj-ifRHrg2PmmOrzYs697ajGMwcLbu-1g', "2UW_EO3r7uczPGUUlpJBnMDpDmWUHE2yDzCmXS4sckE"],
+        ['uIcmks5rAhvBe4dRaJOdeSqgxLGGMZhsGk4J4PEKL2s', "F9WJV_74IZp0Ide4hWjiJXk9FRtBUBkUr3mzU-q1lzk"],
+    ]
+);
+
 class RandomUtil {
 
     static randomIntRange(min, max) {
@@ -107,6 +132,14 @@ class RandomUtil {
         return str;
     }
 
+    static randomShortIdSeq(count) {
+        let str = '';
+        for (let i = 0; i < count; ++i) {
+            str += shortIdSeq[this.randomInt(16)];
+        }
+        return str;
+    }
+
     static randomLowerAndNum(count) {
         let str = '';
         for (let i = 0; i < count; ++i) {
@@ -137,6 +170,26 @@ class RandomUtil {
         });
     }
 
+    static randowShortId() {
+        let str = '';
+        str += this.randomShortIdSeq(8)
+        return str;
+    }
+
+    static randomX25519PrivateKey() {
+        let num = x25519Map.size;
+        let index = this.randomInt(num);
+        let cntr = 0;
+        for (let key of x25519Map.keys()) {
+            if (cntr++ === index) {
+                return key;
+            }
+        }
+    }
+
+    static randomX25519PublicKey(key) {
+        return x25519Map.get(key)
+    }
     static randomText() {
         var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
         var string = '';

+ 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.GET("/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)
+}

+ 28 - 2
web/controller/server.go

@@ -38,7 +38,9 @@ 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)
+	g.POST("/getConfigJson", a.getConfigJson)
+	g.GET("/getDb", a.getDb)
 }
 
 func (a *ServerController) refreshStatus() {
@@ -109,10 +111,34 @@ 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
 	}
 	jsonObj(c, logs, nil)
 }
+
+func (a *ServerController) getConfigJson(c *gin.Context) {
+	configJson, err := a.serverService.GetConfigJson()
+	if err != nil {
+		jsonMsg(c, I18n(c, "getLogs"), err)
+		return
+	}
+	jsonObj(c, configJson, nil)
+}
+
+func (a *ServerController) getDb(c *gin.Context) {
+	db, err := a.serverService.GetDb()
+	if err != nil {
+		jsonMsg(c, I18n(c, "getLogs"), err)
+		return
+	}
+	// Set the headers for the response
+	c.Header("Content-Type", "application/octet-stream")
+	c.Header("Content-Disposition", "attachment; filename=xui.db")
+
+	// Write the file contents to the response
+	c.Writer.Write(db)
+}

+ 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>
@@ -23,6 +26,12 @@
     </a-form-item>
     <a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
         <a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
+    </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>
 		<span slot="label">
@@ -60,7 +69,7 @@
 		</a-form>
 	</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>

+ 3 - 2
web/html/xui/form/protocol/trojan.html

@@ -32,7 +32,7 @@
                 <a-input type="number" v-model.number="client.limitIp" min="0"  style="width: 70px;" ></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>
@@ -60,6 +60,7 @@
                 </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>
         </a-form-item>
     </a-collapse-panel>
@@ -76,7 +77,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>

+ 14 - 13
web/html/xui/form/protocol/vless.html

@@ -20,25 +20,25 @@
             <a-input v-model.trim="client.id"  style="width: 300px;" ></a-input>
         </a-form-item>
 		<a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input type="number" v-model.number="client.limitIp" min="0"  style="width: 70px;" ></a-input>
+            <span slot="label">
+                <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
+                <a-tooltip>
+                    <template slot="title">
+                        <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
+                    </template>
+                    <a-icon type="question-circle" theme="filled"></a-icon>
+                </a-tooltip>
+            </span>
+            <a-input type="number" v-model.number="client.limitIp" min="0"  style="width: 70px;" ></a-input>
 		</a-form-item>
 		<a-form-item v-if="inbound.XTLS" label="Flow">
-            <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
+            <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
                 <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
                 <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
             </a-select>
         </a-form-item>
         <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
-            <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
+            <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
                 <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
                 <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
             </a-select>
@@ -66,6 +66,7 @@
                 </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>
         </a-form-item>
     </a-collapse-panel>
@@ -82,7 +83,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>

+ 2 - 1
web/html/xui/form/protocol/vmess.html

@@ -57,7 +57,8 @@
                 </a-tooltip>
             </span>
             <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
-                           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-form-item>
     </a-collapse-panel>
 </a-collapse>

+ 1 - 1
web/html/xui/form/stream/stream_tcp.html

@@ -4,7 +4,7 @@
     <a-form-item label="AcceptProxyProtocol">
         <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
     </a-form-item>
-    <a-form-item label="HTTP Camouflage">
+    <a-form-item label="HTTP {{ i18n "camouflage" }}">
         <a-switch
                 :checked="inbound.stream.tcp.type === 'http'"
                 @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">

+ 53 - 2
web/html/xui/form/tls_settings.html

@@ -1,11 +1,32 @@
 {{define "form/tlsSettings"}}
 <!-- tls enable -->
 <a-form layout="inline" v-if="inbound.canSetTls()">
-    <a-form-item label="TLS">
+    <a-form-item v-if="inbound.canEnableTls()" label="TLS">
         <a-switch v-model="inbound.tls">
         </a-switch>
     </a-form-item>
-    <a-form-item v-if="inbound.canEnableXTLS()" label="XTLS">
+    <a-form-item v-if="inbound.canEnableReality()">
+        <span slot="label">
+            Reality
+            <a-tooltip>
+                <template slot="title">
+                  <span>{{ i18n "pages.inbounds.Realitydec" }}</span>
+                </template>
+                <a-icon type="question-circle" theme="filled"></a-icon>
+            </a-tooltip>
+        </span>
+        <a-switch v-model="inbound.reality"></a-switch>
+    </a-form-item>
+    <a-form-item v-if="inbound.canEnableXTLS()">
+        <span slot="label">
+            XTLS
+            <a-tooltip>
+                <template slot="title">
+                  <span>{{ i18n "pages.inbounds.XTLSdec" }}</span>
+                </template>
+                <a-icon type="question-circle" theme="filled"></a-icon>
+            </a-tooltip>
+        </span>
         <a-switch v-model="inbound.XTLS"></a-switch>
     </a-form-item>
 </a-form>
@@ -61,6 +82,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" }}'>
@@ -71,4 +93,33 @@
         </a-form-item>
     </template>
 </a-form>
+<a-form v-else-if="inbound.reality" layout="inline">
+    <a-form-item label="show">
+        <a-switch v-model="inbound.stream.reality.show">
+        </a-switch>
+    </a-form-item>
+    <a-form-item label="xver">
+        <a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
+    </a-form-item>
+    <a-form-item label="uTLS" >
+        <a-select v-model="inbound.stream.reality.fingerprint" style="width: 135px">
+            <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
+        </a-select>
+    </a-form-item>
+	<a-form-item label="dest">
+        <a-input v-model.trim="inbound.stream.reality.dest" style="width: 360px"></a-input>
+    </a-form-item>
+    <a-form-item label="serverNames">
+        <a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 360px"></a-input>
+    </a-form-item>
+    <a-form-item label="privateKey">
+        <a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 360px"></a-input>
+    </a-form-item>
+    <a-form-item label="publicKey">
+        <a-input v-model.trim="inbound.stream.reality.publicKey" style="width: 360px"></a-input>
+    </a-form-item>
+    <a-form-item label="shortIds">
+        <a-input v-model.trim="inbound.stream.reality.shortIds"></a-input>
+    </a-form-item>
+</a-form>
 {{end}}

+ 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}}

+ 43 - 13
web/html/xui/inbound_info_modal.html

@@ -49,7 +49,7 @@
                     tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
                     tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
                 </td>
-                <td v-else-if="inbound.xtls">
+                <td v-else-if="inbound.XTLS">
                     xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
                     xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
                 </td>
@@ -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:"") + basePath + "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 = '';

+ 178 - 70
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,36 @@
                                 </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>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</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 +168,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 +232,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: 80,
+        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,17 +261,20 @@
 
     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: 120, 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: 120, scopedSlots: { customRender: 'traffic' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
-        { title: 'Password', width: 100, dataIndex: "password" },
+        { title: 'Password', width: 120, dataIndex: "password" },
     ];
 
     const app = new Vue({
@@ -243,6 +287,11 @@
             dbInbounds: [],
             searchKey: '',
             searchedInbounds: [],
+            expireDiff: 0,
+            trafficDiff: 0,
+            defaultCert: '',
+            defaultKey: '',
+            clientCount: {},
         },
         methods: {
             loading(spinning=true) {
@@ -258,17 +307,66 @@
                 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)) {
                     this.searchedInbounds = this.dbInbounds.slice();
@@ -315,7 +413,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 +578,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 +602,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 +652,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 +687,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 +750,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,
                 };
             }
         },

+ 67 - 26
web/html/xui/index.html

@@ -74,6 +74,25 @@
             </transition>
             <transition name="list" appear>
                 <a-row>
+                    <a-col :sm="24" :md="12">
+                        <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
+                            3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
+                            Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
+                            Telegram: <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
+                        </a-card>
+                    </a-col>
+                    <a-col :sm="24" :md="12">
+                        <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
+                            {{ i18n "pages.index.operationHours" }}:
+                            <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
+                            <a-tooltip>
+                                <template slot="title">
+                                    {{ i18n "pages.index.operationHoursDesc" }}
+                                </template>
+                                <a-icon type="question-circle" theme="filled"></a-icon>
+                            </a-tooltip>
+                        </a-card>
+                    </a-col>
                     <a-col :sm="24" :md="12">
                         <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
                             {{ i18n "pages.index.xrayStatus" }}:
@@ -84,7 +103,6 @@
                                 </template>
                                 <a-icon type="question-circle" theme="filled"></a-icon>
                             </a-tooltip>
-                            <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
                             <a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
                             <a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>                    
                             <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
@@ -92,14 +110,10 @@
                     </a-col>
                     <a-col :sm="24" :md="12">
                         <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
-                            {{ i18n "pages.index.operationHours" }}:
-                            <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
-                            <a-tooltip>
-                                <template slot="title">
-                                    {{ i18n "pages.index.operationHoursDesc" }}
-                                </template>
-                                <a-icon type="question-circle" theme="filled"></a-icon>
-                            </a-tooltip>
+                            {{ i18n "menu.link" }}:
+                            <a-tag color="blue" style="cursor: pointer;" @click="openLogs(20)">Log Reports</a-tag>
+                            <a-tag color="blue" style="cursor: pointer;" @click="openConfig">Config</a-tag>
+                            <a-tag color="blue" style="cursor: pointer;" @click="getBackup">Backup</a-tag>
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
@@ -169,13 +183,6 @@
                                 </a-col>
                             </a-row>
                         </a-card>
-                    </a-col>
-					<a-col :sm="24" :md="12">
-                        <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
-							 3x-ui: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
-							<a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">Telegram</a-tag></a>
-							<a-tag color="blue" style="cursor: pointer;" @click="openLogs">Log Reports</a-tag>
-                        </a-card>
                     </a-col>
                 </a-row>
             </transition>
@@ -199,14 +206,34 @@
              :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" .}}
+{{template "textModal"}}
 <script>
 
     const State = {
@@ -301,9 +328,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 +406,26 @@
                     return;
                 }
             },
-            async openLogs(){
+            async openLogs(rows){
+                this.loading(true);
+                const msg = await HttpUtil.post('server/logs/'+rows);
+                this.loading(false);
+                if (!msg.success) {
+                    return;
+                }
+                logModal.show(msg.obj,rows);
+            },
+            async openConfig(){
                 this.loading(true);
-                const msg = await HttpUtil.post('server/logs');
+                const msg = await HttpUtil.post('server/getConfigJson');
                 this.loading(false);
                 if (!msg.success) {
                     return;
                 }
-                logModal.show(msg.obj);
+                txtModal.show('config.json',JSON.stringify(msg.obj, null, 2),'config.json');
+            },
+            getBackup(){
+                window.location = basePath + 'server/getDb';
             }
         },
         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>

+ 113 - 32
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,52 +433,68 @@ 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
 		}
-		// get settings clients
-		settings := map[string][]model.Client{}
-		json.Unmarshal([]byte(inbound.Settings), &settings)
-		clients := settings["clients"]
-		for _, client := range clients {
-			if traffic.Email == client.Email {
-				traffic.ExpiryTime = client.ExpiryTime
-				traffic.Total = client.TotalGB
+		// get clients
+		clients, err := s.getClients(inbound)
+		needUpdate := false
+		if err == nil {
+			for client_index, client := range clients {
+				if traffic.Email == client.Email {
+					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 := map[string]interface{}{}
+			json.Unmarshal([]byte(inbound.Settings), &settings)
+
+			// Convert clients to []interface to update clients in settings
+			var clientsInterface []interface{}
+			for _, c := range clients {
+				clientsInterface = append(clientsInterface, interface{}(c))
+			}
+
+			settings["clients"] = clientsInterface
+			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 +579,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 +724,4 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
 		return nil, err
 	}
 	return inbounds, nil
-}
+}

+ 43 - 2
web/service/server.go

@@ -13,6 +13,7 @@ import (
 	"runtime"
 	"strings"
 	"time"
+	"x-ui/config"
 	"x-ui/logger"
 	"x-ui/util/sys"
 	"x-ui/xray"
@@ -327,11 +328,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
 	}
@@ -349,3 +350,43 @@ func (s *ServerService) GetLogs() ([]string, error) {
 
 	return lines, nil
 }
+
+func (s *ServerService) GetConfigJson() (interface{}, error) {
+	// Open the file for reading
+	file, err := os.Open(xray.GetConfigPath())
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	// Read the file contents
+	fileContents, err := io.ReadAll(file)
+	if err != nil {
+		return nil, err
+	}
+
+	var jsonData interface{}
+	err = json.Unmarshal(fileContents, &jsonData)
+	if err != nil {
+		return nil, err
+	}
+
+	return jsonData, nil
+}
+
+func (s *ServerService) GetDb() ([]byte, error) {
+	// Open the file for reading
+	file, err := os.Open(config.GetDBPath())
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	// Read the file contents
+	fileContents, err := io.ReadAll(file)
+	if err != nil {
+		return nil, err
+	}
+
+	return fileContents, 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"] {

+ 555 - 0
web/service/sub.go

@@ -0,0 +1,555 @@
+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 == "reality" {
+		params["security"] = "reality"
+		realitySettings, _ := stream["realitySettings"].(map[string]interface{})
+		if realitySettings != nil {
+			if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
+				sNames, _ := sniValue.([]interface{})
+				params["sni"], _ = sNames[0].(string)
+			}
+			if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
+				params["pbk"], _ = pbkValue.(string)
+			}
+			if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
+				shortIds, _ := sidValue.([]interface{})
+				params["sid"], _ = shortIds[0].(string)
+			}
+			if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+		}
+
+		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+			params["flow"] = clients[clientIndex].Flow
+		}
+	}
+
+	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 == "reality" {
+		params["security"] = "reality"
+		realitySettings, _ := stream["realitySettings"].(map[string]interface{})
+		if realitySettings != nil {
+			if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
+				sNames, _ := sniValue.([]interface{})
+				params["sni"], _ = sNames[0].(string)
+			}
+			if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
+				params["pbk"], _ = pbkValue.(string)
+			}
+			if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
+				shortIds, _ := sidValue.([]interface{})
+				params["sid"], _ = shortIds[0].(string)
+			}
+			if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
+				params["fp"], _ = fpValue.(string)
+			}
+		}
+
+		if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
+			params["flow"] = clients[clientIndex].Flow
+		}
+	}
+
+	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 ""
+}

+ 26 - 10
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 {
@@ -522,14 +536,14 @@ func (t *Tgbot) getExhausted() string {
 	for _, inbound := range inbounds {
 		if inbound.Enable {
 			if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
-				(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
+				(inbound.Total > 0 && (inbound.Total-(inbound.Up+inbound.Down) < trDiff)) {
 				exhaustedInbounds = append(exhaustedInbounds, *inbound)
 			}
 			if len(inbound.ClientStats) > 0 {
 				for _, client := range inbound.ClientStats {
 					if client.Enable {
 						if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
-							(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
+							(client.Total > 0 && (client.Total-(client.Up+client.Down) < trDiff)) {
 							exhaustedClients = append(exhaustedClients, client)
 						}
 					} else {
@@ -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

+ 16 - 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,17 @@
 "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"
+"XTLSdec" = "Xray core needs to be 1.7.5 and below"
+"Realitydec" = "Xray core needs to be 1.8.0 and above"
 
 [pages.client]
 "add" = "Add client"
@@ -158,6 +167,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 +243,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"

+ 20 - 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,9 @@
 "IPLimitlog" = "گزارش ها"
 "IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
 "IPLimitlogclear" = "پاک کردن گزارش ها"
+"setDefaultCert" = "استفاده از گواهی پنل"
+"XTLSdec" = "هسته Xray باید 1.7.5 و کمتر باشد"
+"Realitydec" = "هسته Xray باید 1.8.0 و بالاتر باشد"
 
 [pages.client]
 "add" = "کاربر جدید"
@@ -158,6 +165,9 @@
 "last" = "تا"
 "prefix" = "پیشوند"
 "postfix" = "پسوند"
+"delayedStart" = "شروع بعد از اولین استفاده"
+"expireDays" = "روزهای اعتبار"
+"days" = "(روز)"
 
 [pages.inbounds.toasts]
 "obtain" = "Obtain"
@@ -228,13 +238,13 @@
 "telegramChatId" = "آی دی تلگرام مدیریت"
 "telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
 "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
-"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
+"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
 "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
 "tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
-"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
-"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
-"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
-"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
+"expireTimeDiff" = "آستانه زمان باقی مانده"
+"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
+"trafficDiff" = "آستانه ترافیک باقی مانده"
+"trafficDiffDesc" = "فاصله زمانی هشدار تا رسیدن به اتمام ترافیک (واحد: گیگابایت)"
 "tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
 "tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
 "timeZonee" = "منظقه زمانی"

+ 18 - 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,9 @@
 "IPLimitlog" = "IP日志"
 "IPLimitlogDesc" = "IP 历史日志 (通过IP限制禁用inbound之前,需要清空日志)"
 "IPLimitlogclear" = "清除日志"
+"setDefaultCert" = "从面板设置证书"
+"XTLSdec" = "Xray核心需要1.7.5及以下版本"
+"Realitydec" = "Xray核心需要1.8.0及以上版本"
 
 [pages.client]
 "add" = "添加客户端"
@@ -158,6 +165,9 @@
 "last" = "最后"
 "prefix" = "前缀"
 "postfix" = "后缀"
+"delayedStart" = "首次使用后开始"
+"expireDays" = "过期天数"
+"days" = "天"
 
 [pages.inbounds.toasts]
 "obtain" = "获取"
@@ -231,10 +241,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())
 

+ 8 - 0
x-ui.sh

@@ -455,6 +455,14 @@ ssl_cert_issue() {
 }
 
 open_ports() {
+if ! command -v ufw &> /dev/null
+then
+    echo "ufw firewall is not installed. Installing now..."
+    sudo apt-get update
+    sudo apt-get install -y ufw
+else
+    echo "ufw firewall is already installed"
+fi
 
   # Check if the firewall is inactive
   if sudo ufw status | grep -q "Status: active"; then