mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-13 19:49:12 +00:00
Compare commits
14 commits
61406c809a
...
f7f54086f6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
f7f54086f6 | ||
![]() |
00baeffe74 | ||
![]() |
b578a33518 | ||
![]() |
8153e0ac05 | ||
![]() |
3b262cf180 | ||
![]() |
4c7249c451 | ||
![]() |
edd8b12988 | ||
![]() |
5e953bae45 | ||
![]() |
747af376f2 | ||
![]() |
a3ccccfe52 | ||
![]() |
3299d15f28 | ||
![]() |
ae82373457 | ||
![]() |
d65233cc2c | ||
![]() |
11dc06863e |
22 changed files with 1028 additions and 416 deletions
|
@ -37,6 +37,7 @@ func initModels() error {
|
||||||
&model.InboundClientIps{},
|
&model.InboundClientIps{},
|
||||||
&xray.ClientTraffic{},
|
&xray.ClientTraffic{},
|
||||||
&model.HistoryOfSeeders{},
|
&model.HistoryOfSeeders{},
|
||||||
|
&model.Server{},
|
||||||
}
|
}
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if err := db.AutoMigrate(model); err != nil {
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
|
|
|
@ -119,3 +119,12 @@ type Client struct {
|
||||||
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
CreatedAt int64 `json:"created_at,omitempty"` // Creation timestamp
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
UpdatedAt int64 `json:"updated_at,omitempty"` // Last update timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `json:"name" gorm:"unique;not null"`
|
||||||
|
Address string `json:"address" gorm:"not null"`
|
||||||
|
Port int `json:"port" gorm:"not null"`
|
||||||
|
APIKey string `json:"apiKey" gorm:"not null"`
|
||||||
|
Enable bool `json:"enable" gorm:"default:true"`
|
||||||
|
}
|
||||||
|
|
20
go.mod
20
go.mod
|
@ -6,7 +6,7 @@ require (
|
||||||
github.com/gin-contrib/gzip v1.2.3
|
github.com/gin-contrib/gzip v1.2.3
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
github.com/goccy/go-json v0.10.5
|
github.com/goccy/go-json v0.10.5
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
@ -15,7 +15,7 @@ require (
|
||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8
|
github.com/shirou/gopsutil/v4 v4.25.9
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/valyala/fasthttp v1.66.0
|
github.com/valyala/fasthttp v1.66.0
|
||||||
github.com/xlzd/gotp v0.1.0
|
github.com/xlzd/gotp v0.1.0
|
||||||
|
@ -24,7 +24,7 @@ require (
|
||||||
golang.org/x/crypto v0.42.0
|
golang.org/x/crypto v0.42.0
|
||||||
golang.org/x/sys v0.36.0
|
golang.org/x/sys v0.36.0
|
||||||
golang.org/x/text v0.29.0
|
golang.org/x/text v0.29.0
|
||||||
google.golang.org/grpc v1.75.1
|
google.golang.org/grpc v1.76.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.0
|
gorm.io/gorm v1.31.0
|
||||||
)
|
)
|
||||||
|
@ -45,7 +45,7 @@ require (
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||||
github.com/google/btree v1.1.3 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
|
@ -68,13 +68,14 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/sagernet/sing v0.7.10 // indirect
|
github.com/sagernet/sing v0.7.12 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
|
@ -86,9 +87,8 @@ require (
|
||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
github.com/valyala/fastjson v1.6.4 // indirect
|
||||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/arch v0.21.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/mod v0.28.0 // indirect
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
|
@ -98,8 +98,8 @@ require (
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.10 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
40
go.sum
40
go.sum
|
@ -2,8 +2,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
@ -39,8 +39,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
@ -54,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
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/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||||
|
@ -148,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
|
@ -158,14 +158,14 @@ 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/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
|
github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
|
||||||
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.7.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
@ -200,8 +200,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic=
|
||||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||||
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
||||||
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
|
@ -256,12 +256,12 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|
|
@ -140,6 +140,13 @@ config_after_install() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/usr/local/x-ui/x-ui migrate
|
/usr/local/x-ui/x-ui migrate
|
||||||
|
|
||||||
|
local existing_apiKey=$(/usr/local/x-ui/x-ui setting -show true | grep -oP 'ApiKey: \K.*')
|
||||||
|
if [[ -z "$existing_apiKey" ]]; then
|
||||||
|
local config_apiKey=$(gen_random_string 32)
|
||||||
|
/usr/local/x-ui/x-ui setting -apiKey "${config_apiKey}"
|
||||||
|
echo -e "${green}Generated random API Key: ${config_apiKey}${plain}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_x-ui() {
|
install_x-ui() {
|
||||||
|
|
16
main.go
16
main.go
|
@ -240,7 +240,8 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
// updateSetting updates various panel settings including port, credentials, base path, listen IP, and two-factor authentication.
|
||||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
||||||
|
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Database initialization failed:", err)
|
fmt.Println("Database initialization failed:", err)
|
||||||
|
@ -250,6 +251,15 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
userService := service.UserService{}
|
userService := service.UserService{}
|
||||||
|
|
||||||
|
if apiKey != "" {
|
||||||
|
err := settingService.SetAPIKey(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to set API Key:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if port > 0 {
|
if port > 0 {
|
||||||
err := settingService.SetPort(port)
|
err := settingService.SetPort(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -402,9 +412,11 @@ func main() {
|
||||||
var show bool
|
var show bool
|
||||||
var getCert bool
|
var getCert bool
|
||||||
var resetTwoFactor bool
|
var resetTwoFactor bool
|
||||||
|
var apiKey string
|
||||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||||
|
settingCmd.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
||||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||||
|
@ -454,7 +466,7 @@ func main() {
|
||||||
if reset {
|
if reset {
|
||||||
resetSetting()
|
resetSetting()
|
||||||
} else {
|
} else {
|
||||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
||||||
}
|
}
|
||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
|
|
|
@ -162,26 +162,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
switch inbound.Protocol {
|
serverService := service.MultiServerService{}
|
||||||
case "vmess":
|
servers, err := serverService.GetServers()
|
||||||
return s.genVmessLink(inbound, email)
|
if err != nil {
|
||||||
case "vless":
|
logger.Warning("Failed to get servers for subscription:", err)
|
||||||
return s.genVlessLink(inbound, email)
|
return ""
|
||||||
case "trojan":
|
|
||||||
return s.genTrojanLink(inbound, email)
|
|
||||||
case "shadowsocks":
|
|
||||||
return s.genShadowsocksLink(inbound, email)
|
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
var links []string
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var link string
|
||||||
|
switch inbound.Protocol {
|
||||||
|
case "vmess":
|
||||||
|
link = s.genVmessLink(inbound, email, server)
|
||||||
|
case "vless":
|
||||||
|
link = s.genVlessLink(inbound, email, server)
|
||||||
|
case "trojan":
|
||||||
|
link = s.genTrojanLink(inbound, email, server)
|
||||||
|
case "shadowsocks":
|
||||||
|
link = s.genShadowsocksLink(inbound, email, server)
|
||||||
|
}
|
||||||
|
if link != "" {
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(links, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
if inbound.Protocol != model.VMESS {
|
if inbound.Protocol != model.VMESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
obj := map[string]any{
|
obj := map[string]any{
|
||||||
"v": "2",
|
"v": "2",
|
||||||
"add": s.address,
|
"add": server.Address,
|
||||||
"port": inbound.Port,
|
"port": inbound.Port,
|
||||||
"type": "none",
|
"type": "none",
|
||||||
}
|
}
|
||||||
|
@ -294,7 +311,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
newObj[key] = value
|
newObj[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
newObj["add"] = ep["dest"].(string)
|
newObj["add"] = ep["dest"].(string)
|
||||||
newObj["port"] = int(ep["port"].(float64))
|
newObj["port"] = int(ep["port"].(float64))
|
||||||
|
|
||||||
|
@ -310,14 +327,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
obj["ps"] = s.genRemark(inbound, email, "")
|
obj["ps"] = s.genRemark(inbound, email, "", server.Name)
|
||||||
|
|
||||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -497,7 +514,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
@ -518,12 +535,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Trojan {
|
if inbound.Protocol != model.Trojan {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -692,7 +709,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
@ -714,12 +731,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Shadowsocks {
|
if inbound.Protocol != model.Shadowsocks {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
@ -859,7 +876,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
@ -880,17 +897,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, serverName string) string {
|
||||||
separationChar := string(s.remarkModel[0])
|
separationChar := string(s.remarkModel[0])
|
||||||
orderChars := s.remarkModel[1:]
|
orderChars := s.remarkModel[1:]
|
||||||
orders := map[byte]string{
|
orders := map[byte]string{
|
||||||
'i': "",
|
'i': "",
|
||||||
'e': "",
|
'e': "",
|
||||||
'o': "",
|
'o': "",
|
||||||
|
's': "",
|
||||||
}
|
}
|
||||||
if len(email) > 0 {
|
if len(email) > 0 {
|
||||||
orders['e'] = email
|
orders['e'] = email
|
||||||
|
@ -901,6 +919,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||||
if len(extra) > 0 {
|
if len(extra) > 0 {
|
||||||
orders['o'] = extra
|
orders['o'] = extra
|
||||||
}
|
}
|
||||||
|
if len(serverName) > 0 {
|
||||||
|
orders['s'] = serverName
|
||||||
|
}
|
||||||
|
|
||||||
var remark []string
|
var remark []string
|
||||||
for i := 0; i < len(orderChars); i++ {
|
for i := 0; i < len(orderChars); i++ {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
89
web/controller/multi_server_controller.go
Normal file
89
web/controller/multi_server_controller.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"x-ui/database/model"
|
||||||
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiServerController struct {
|
||||||
|
multiServerService service.MultiServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMultiServerController(g *gin.RouterGroup) *MultiServerController {
|
||||||
|
c := &MultiServerController{}
|
||||||
|
c.initRouter(g)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
|
||||||
|
g = g.Group("/server")
|
||||||
|
|
||||||
|
g.GET("/list", c.getServers)
|
||||||
|
g.POST("/add", c.addServer)
|
||||||
|
g.POST("/del/:id", c.delServer)
|
||||||
|
g.POST("/update/:id", c.updateServer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) getServers(ctx *gin.Context) {
|
||||||
|
servers, err := c.multiServerService.GetServers()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to get servers", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(ctx, servers, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) addServer(ctx *gin.Context) {
|
||||||
|
server := &model.Server{}
|
||||||
|
err := ctx.ShouldBind(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.AddServer(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to add server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server added successfully", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) delServer(ctx *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.DeleteServer(id)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to delete server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server deleted successfully", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *MultiServerController) updateServer(ctx *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(ctx.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
server := &model.Server{
|
||||||
|
Id: id,
|
||||||
|
}
|
||||||
|
err = ctx.ShouldBind(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Invalid data", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = c.multiServerService.UpdateServer(server)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(ctx, "Failed to update server", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(ctx, "Server updated successfully", nil)
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/inbounds", a.inbounds)
|
g.GET("/inbounds", a.inbounds)
|
||||||
|
g.GET("/servers", a.servers)
|
||||||
g.GET("/settings", a.settings)
|
g.GET("/settings", a.settings)
|
||||||
g.GET("/xray", a.xraySettings)
|
g.GET("/xray", a.xraySettings)
|
||||||
|
|
||||||
|
@ -52,3 +53,7 @@ func (a *XUIController) settings(c *gin.Context) {
|
||||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||||
html(c, "xray.html", "pages.xray.title", nil)
|
html(c, "xray.html", "pages.xray.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *XUIController) servers(c *gin.Context) {
|
||||||
|
html(c, "servers.html", "Servers", nil)
|
||||||
|
}
|
||||||
|
|
|
@ -54,6 +54,11 @@
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
title: '{{ i18n "menu.inbounds"}}'
|
title: '{{ i18n "menu.inbounds"}}'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '{{ .base_path }}panel/servers',
|
||||||
|
icon: 'cloud-server',
|
||||||
|
title: 'Servers'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '{{ .base_path }}panel/settings',
|
key: '{{ .base_path }}panel/settings',
|
||||||
icon: 'setting',
|
icon: 'setting',
|
||||||
|
|
165
web/html/servers.html
Normal file
165
web/html/servers.html
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
{{template "header" .}}
|
||||||
|
|
||||||
|
<div id="app" class="row" v-cloak>
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Server Management</h3>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button class="btn btn-primary" @click="showAddModal">Add Server</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>#</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Address</th>
|
||||||
|
<th>Port</th>
|
||||||
|
<th>Enabled</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(server, index) in servers">
|
||||||
|
<td>{{index + 1}}</td>
|
||||||
|
<td>{{server.name}}</td>
|
||||||
|
<td>{{server.address}}</td>
|
||||||
|
<td>{{server.port}}</td>
|
||||||
|
<td>
|
||||||
|
<span v-if="server.enable" class="badge bg-success">Yes</span>
|
||||||
|
<span v-else class="badge bg-danger">No</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-info btn-sm" @click="showEditModal(server)">Edit</button>
|
||||||
|
<button class="btn btn-danger btn-sm" @click="deleteServer(server.id)">Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add/Edit Modal -->
|
||||||
|
<div class="modal fade" id="serverModal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">{{modal.title}}</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.name">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Address (IP or Domain)</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.address">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Port</label>
|
||||||
|
<input type="number" class="form-control" v-model.number="modal.server.port">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>API Key</label>
|
||||||
|
<input type="text" class="form-control" v-model="modal.server.apiKey">
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input type="checkbox" class="form-check-input" v-model="modal.server.enable">
|
||||||
|
<label class="form-check-label">Enabled</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" @click="saveServer">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
servers: [],
|
||||||
|
modal: {
|
||||||
|
title: '',
|
||||||
|
server: {
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
port: 0,
|
||||||
|
apiKey: '',
|
||||||
|
enable: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadServers() {
|
||||||
|
axios.get('{{.base_path}}server/list')
|
||||||
|
.then(response => {
|
||||||
|
this.servers = response.data.obj;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
showAddModal() {
|
||||||
|
this.modal.title = 'Add Server';
|
||||||
|
this.modal.server = {
|
||||||
|
name: '',
|
||||||
|
address: '',
|
||||||
|
port: 0,
|
||||||
|
apiKey: '',
|
||||||
|
enable: true
|
||||||
|
};
|
||||||
|
$('#serverModal').modal('show');
|
||||||
|
},
|
||||||
|
showEditModal(server) {
|
||||||
|
this.modal.title = 'Edit Server';
|
||||||
|
this.modal.server = Object.assign({}, server);
|
||||||
|
$('#serverModal').modal('show');
|
||||||
|
},
|
||||||
|
saveServer() {
|
||||||
|
let url = '{{.base_path}}server/add';
|
||||||
|
if (this.modal.server.id) {
|
||||||
|
url = `{{.base_path}}server/update/${this.modal.server.id}`;
|
||||||
|
}
|
||||||
|
axios.post(url, this.modal.server)
|
||||||
|
.then(response => {
|
||||||
|
alert(response.data.msg);
|
||||||
|
$('#serverModal').modal('hide');
|
||||||
|
this.loadServers();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteServer(id) {
|
||||||
|
if (!confirm('Are you sure you want to delete this server?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
axios.post(`{{.base_path}}server/del/${id}`)
|
||||||
|
.then(response => {
|
||||||
|
alert(response.data.msg);
|
||||||
|
this.loadServers();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
alert(error.response.data.msg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadServers();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{{template "footer" .}}
|
|
@ -9,19 +9,20 @@
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||||
message='{{ i18n "secAlertTitle" }}'
|
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
||||||
color="red"
|
|
||||||
show-icon closable>
|
|
||||||
<template slot="description">
|
<template slot="description">
|
||||||
<b>{{ i18n "secAlertConf" }}</b>
|
<b>{{ i18n "secAlertConf" }}</b>
|
||||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
<ul>
|
||||||
|
<li v-for="a in confAlerts">[[ a ]]</li>
|
||||||
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<template>
|
<template>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
<a-card
|
||||||
|
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
@ -31,17 +32,19 @@
|
||||||
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||||
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
||||||
<a-space direction="horizontal">
|
<a-space direction="horizontal">
|
||||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
|
||||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
"pages.settings.save" }}</a-button>
|
||||||
|
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
|
||||||
|
"pages.settings.restartPanel" }}</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="14">
|
<a-col :xs="24" :sm="14">
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
<a-back-top :target="() => document.getElementById('content-layout')"
|
||||||
|
visibility-height="200"></a-back-top>
|
||||||
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
||||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
||||||
show-icon>
|
|
||||||
</a-alert>
|
</a-alert>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -132,7 +135,8 @@
|
||||||
fragment: {
|
fragment: {
|
||||||
packets: "tlshello",
|
packets: "tlshello",
|
||||||
length: "100-200",
|
length: "100-200",
|
||||||
interval: "10-20"
|
interval: "10-20",
|
||||||
|
maxSplit: "300-400"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamSettings: {
|
streamSettings: {
|
||||||
|
@ -381,11 +385,11 @@
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
ldapInboundTagList: {
|
ldapInboundTagList: {
|
||||||
get: function() {
|
get: function () {
|
||||||
const csv = this.allSetting.ldapInboundTags || "";
|
const csv = this.allSetting.ldapInboundTags || "";
|
||||||
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
|
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||||
},
|
},
|
||||||
set: function(list) {
|
set: function (list) {
|
||||||
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
|
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -425,6 +429,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fragmentMaxSplit: {
|
||||||
|
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
|
||||||
|
set: function (v) {
|
||||||
|
if (v != "") {
|
||||||
|
newFragment = JSON.parse(this.allSetting.subJsonFragment);
|
||||||
|
newFragment.settings.fragment.maxSplit = v;
|
||||||
|
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
noises: {
|
noises: {
|
||||||
get() {
|
get() {
|
||||||
return this.allSetting?.subJsonNoises != "";
|
return this.allSetting?.subJsonNoises != "";
|
||||||
|
|
|
@ -5,13 +5,10 @@
|
||||||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||||
<template #control>
|
<template #control>
|
||||||
<a-input
|
<a-input type="text" v-model="allSetting.subJsonPath"
|
||||||
type="text"
|
|
||||||
v-model="allSetting.subJsonPath"
|
|
||||||
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||||
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
|
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
|
||||||
placeholder="/json/"
|
placeholder="/json/"></a-input>
|
||||||
></a-input>
|
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
|
@ -53,6 +50,12 @@
|
||||||
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
|
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
|
<a-setting-list-item paddings="small">
|
||||||
|
<template #title>MaxSplit</template>
|
||||||
|
<template #control>
|
||||||
|
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
|
||||||
|
</template>
|
||||||
|
</a-setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
|
@ -74,7 +77,8 @@
|
||||||
<a-select :value="noise.type" :style="{ width: '100%' }"
|
<a-select :value="noise.type" :style="{ width: '100%' }"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||||
@change="(value) => updateNoiseType(index, value)">
|
@change="(value) => updateNoiseType(index, value)">
|
||||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p">
|
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
|
||||||
|
:key="p">
|
||||||
<span>[[ p ]]</span>
|
<span>[[ p ]]</span>
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
|
|
@ -1,421 +1,421 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
"strings"
|
||||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
||||||
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"strconv"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
|
||||||
|
|
||||||
type LdapSyncJob struct {
|
type LdapSyncJob struct {
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helper functions for mustGet ---
|
// --- Helper functions for mustGet ---
|
||||||
func mustGetString(fn func() (string, error)) string {
|
func mustGetString(fn func() (string, error)) string {
|
||||||
v, err := fn()
|
v, err := fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetInt(fn func() (int, error)) int {
|
func mustGetInt(fn func() (int, error)) int {
|
||||||
v, err := fn()
|
v, err := fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetBool(fn func() (bool, error)) bool {
|
func mustGetBool(fn func() (bool, error)) bool {
|
||||||
v, err := fn()
|
v, err := fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
func mustGetStringOr(fn func() (string, error), fallback string) string {
|
func mustGetStringOr(fn func() (string, error), fallback string) string {
|
||||||
v, err := fn()
|
v, err := fn()
|
||||||
if err != nil || v == "" {
|
if err != nil || v == "" {
|
||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func NewLdapSyncJob() *LdapSyncJob {
|
func NewLdapSyncJob() *LdapSyncJob {
|
||||||
return new(LdapSyncJob)
|
return new(LdapSyncJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (j *LdapSyncJob) Run() {
|
func (j *LdapSyncJob) Run() {
|
||||||
logger.Info("LDAP sync job started")
|
logger.Info("LDAP sync job started")
|
||||||
|
|
||||||
enabled, err := j.settingService.GetLdapEnable()
|
enabled, err := j.settingService.GetLdapEnable()
|
||||||
if err != nil || !enabled {
|
if err != nil || !enabled {
|
||||||
logger.Warning("LDAP disabled or failed to fetch flag")
|
logger.Warning("LDAP disabled or failed to fetch flag")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- LDAP fetch ---
|
// --- LDAP fetch ---
|
||||||
cfg := ldaputil.Config{
|
cfg := ldaputil.Config{
|
||||||
Host: mustGetString(j.settingService.GetLdapHost),
|
Host: mustGetString(j.settingService.GetLdapHost),
|
||||||
Port: mustGetInt(j.settingService.GetLdapPort),
|
Port: mustGetInt(j.settingService.GetLdapPort),
|
||||||
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
|
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
|
||||||
BindDN: mustGetString(j.settingService.GetLdapBindDN),
|
BindDN: mustGetString(j.settingService.GetLdapBindDN),
|
||||||
Password: mustGetString(j.settingService.GetLdapPassword),
|
Password: mustGetString(j.settingService.GetLdapPassword),
|
||||||
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
|
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
|
||||||
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
|
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
|
||||||
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
|
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
|
||||||
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
|
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
|
||||||
TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
|
TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
|
||||||
Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
|
Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
|
||||||
}
|
}
|
||||||
|
|
||||||
flags, err := ldaputil.FetchVlessFlags(cfg)
|
flags, err := ldaputil.FetchVlessFlags(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("LDAP fetch failed:", err)
|
logger.Warning("LDAP fetch failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Infof("Fetched %d LDAP flags", len(flags))
|
logger.Infof("Fetched %d LDAP flags", len(flags))
|
||||||
|
|
||||||
// --- Load all inbounds and all clients once ---
|
// --- Load all inbounds and all clients once ---
|
||||||
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
|
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
|
||||||
inbounds, err := j.inboundService.GetAllInbounds()
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Failed to get inbounds:", err)
|
logger.Warning("Failed to get inbounds:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
allClients := map[string]*model.Client{} // email -> client
|
allClients := map[string]*model.Client{} // email -> client
|
||||||
inboundMap := map[string]*model.Inbound{} // tag -> inbound
|
inboundMap := map[string]*model.Inbound{} // tag -> inbound
|
||||||
for _, ib := range inbounds {
|
for _, ib := range inbounds {
|
||||||
inboundMap[ib.Tag] = ib
|
inboundMap[ib.Tag] = ib
|
||||||
clients, _ := j.inboundService.GetClients(ib)
|
clients, _ := j.inboundService.GetClients(ib)
|
||||||
for i := range clients {
|
for i := range clients {
|
||||||
allClients[clients[i].Email] = &clients[i]
|
allClients[clients[i].Email] = &clients[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Prepare batch operations ---
|
// --- Prepare batch operations ---
|
||||||
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
|
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
|
||||||
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
|
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
|
||||||
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
|
||||||
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
|
||||||
|
|
||||||
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
|
clientsToCreate := map[string][]model.Client{} // tag -> []new clients
|
||||||
clientsToEnable := map[string][]string{} // tag -> []email
|
clientsToEnable := map[string][]string{} // tag -> []email
|
||||||
clientsToDisable := map[string][]string{} // tag -> []email
|
clientsToDisable := map[string][]string{} // tag -> []email
|
||||||
|
|
||||||
for email, allowed := range flags {
|
for email, allowed := range flags {
|
||||||
exists := allClients[email] != nil
|
exists := allClients[email] != nil
|
||||||
for _, tag := range inboundTags {
|
for _, tag := range inboundTags {
|
||||||
if !exists && allowed && autoCreate {
|
if !exists && allowed && autoCreate {
|
||||||
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
|
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
|
||||||
clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
|
clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
|
||||||
} else if exists {
|
} else if exists {
|
||||||
if allowed && !allClients[email].Enable {
|
if allowed && !allClients[email].Enable {
|
||||||
clientsToEnable[tag] = append(clientsToEnable[tag], email)
|
clientsToEnable[tag] = append(clientsToEnable[tag], email)
|
||||||
} else if !allowed && allClients[email].Enable {
|
} else if !allowed && allClients[email].Enable {
|
||||||
clientsToDisable[tag] = append(clientsToDisable[tag], email)
|
clientsToDisable[tag] = append(clientsToDisable[tag], email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Execute batch create ---
|
// --- Execute batch create ---
|
||||||
for tag, newClients := range clientsToCreate {
|
for tag, newClients := range clientsToCreate {
|
||||||
if len(newClients) == 0 {
|
if len(newClients) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
payload := &model.Inbound{Id: inboundMap[tag].Id}
|
payload := &model.Inbound{Id: inboundMap[tag].Id}
|
||||||
payload.Settings = j.clientsToJSON(newClients)
|
payload.Settings = j.clientsToJSON(newClients)
|
||||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
|
logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
|
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Execute enable/disable batch ---
|
// --- Execute enable/disable batch ---
|
||||||
for tag, emails := range clientsToEnable {
|
for tag, emails := range clientsToEnable {
|
||||||
j.batchSetEnable(inboundMap[tag], emails, true)
|
j.batchSetEnable(inboundMap[tag], emails, true)
|
||||||
}
|
}
|
||||||
for tag, emails := range clientsToDisable {
|
for tag, emails := range clientsToDisable {
|
||||||
j.batchSetEnable(inboundMap[tag], emails, false)
|
j.batchSetEnable(inboundMap[tag], emails, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Auto delete clients not in LDAP ---
|
// --- Auto delete clients not in LDAP ---
|
||||||
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
|
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
|
||||||
if autoDelete {
|
if autoDelete {
|
||||||
ldapEmailSet := map[string]struct{}{}
|
ldapEmailSet := map[string]struct{}{}
|
||||||
for e := range flags {
|
for e := range flags {
|
||||||
ldapEmailSet[e] = struct{}{}
|
ldapEmailSet[e] = struct{}{}
|
||||||
}
|
}
|
||||||
for _, tag := range inboundTags {
|
for _, tag := range inboundTags {
|
||||||
j.deleteClientsNotInLDAP(tag, ldapEmailSet)
|
j.deleteClientsNotInLDAP(tag, ldapEmailSet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func splitCsv(s string) []string {
|
func splitCsv(s string) []string {
|
||||||
if s == "" {
|
if s == "" {
|
||||||
return DefaultTruthyValues
|
return DefaultTruthyValues
|
||||||
}
|
}
|
||||||
parts := strings.Split(s, ",")
|
parts := strings.Split(s, ",")
|
||||||
out := make([]string, 0, len(parts))
|
out := make([]string, 0, len(parts))
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
v := strings.TrimSpace(p)
|
v := strings.TrimSpace(p)
|
||||||
if v != "" {
|
if v != "" {
|
||||||
out = append(out, v)
|
out = append(out, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// buildClient creates a new client for auto-create
|
// buildClient creates a new client for auto-create
|
||||||
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
|
||||||
c := model.Client{
|
c := model.Client{
|
||||||
Email: email,
|
Email: email,
|
||||||
Enable: true,
|
Enable: true,
|
||||||
LimitIP: defLimitIP,
|
LimitIP: defLimitIP,
|
||||||
TotalGB: int64(defGB),
|
TotalGB: int64(defGB),
|
||||||
}
|
}
|
||||||
if defExpiryDays > 0 {
|
if defExpiryDays > 0 {
|
||||||
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
||||||
}
|
}
|
||||||
switch ib.Protocol {
|
switch ib.Protocol {
|
||||||
case model.Trojan, model.Shadowsocks:
|
case model.Trojan, model.Shadowsocks:
|
||||||
c.Password = uuid.NewString()
|
c.Password = uuid.NewString()
|
||||||
default:
|
default:
|
||||||
c.ID = uuid.NewString()
|
c.ID = uuid.NewString()
|
||||||
}
|
}
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// batchSetEnable enables/disables clients in batch through a single call
|
// batchSetEnable enables/disables clients in batch through a single call
|
||||||
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare JSON for mass update
|
// Prepare JSON for mass update
|
||||||
clients := make([]model.Client, 0, len(emails))
|
clients := make([]model.Client, 0, len(emails))
|
||||||
for _, email := range emails {
|
for _, email := range emails {
|
||||||
clients = append(clients, model.Client{
|
clients = append(clients, model.Client{
|
||||||
Email: email,
|
Email: email,
|
||||||
Enable: enable,
|
Enable: enable,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
payload := &model.Inbound{
|
payload := &model.Inbound{
|
||||||
Id: ib.Id,
|
Id: ib.Id,
|
||||||
Settings: j.clientsToJSON(clients),
|
Settings: j.clientsToJSON(clients),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a single AddInboundClient call to update enable
|
// Use a single AddInboundClient call to update enable
|
||||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
|
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
|
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
|
||||||
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
|
||||||
inbounds, err := j.inboundService.GetAllInbounds()
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Failed to get inbounds for deletion:", err)
|
logger.Warning("Failed to get inbounds for deletion:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
batchSize := 50 // clients in 1 batch
|
batchSize := 50 // clients in 1 batch
|
||||||
restartNeeded := false
|
restartNeeded := false
|
||||||
|
|
||||||
for _, ib := range inbounds {
|
for _, ib := range inbounds {
|
||||||
if ib.Tag != inboundTag {
|
if ib.Tag != inboundTag {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
clients, err := j.inboundService.GetClients(ib)
|
clients, err := j.inboundService.GetClients(ib)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
|
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect clients for deletion
|
// Collect clients for deletion
|
||||||
toDelete := []model.Client{}
|
toDelete := []model.Client{}
|
||||||
for _, c := range clients {
|
for _, c := range clients {
|
||||||
if _, ok := ldapEmails[c.Email]; !ok {
|
if _, ok := ldapEmails[c.Email]; !ok {
|
||||||
toDelete = append(toDelete, c)
|
toDelete = append(toDelete, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toDelete) == 0 {
|
if len(toDelete) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete in batches
|
// Delete in batches
|
||||||
for i := 0; i < len(toDelete); i += batchSize {
|
for i := 0; i < len(toDelete); i += batchSize {
|
||||||
end := i + batchSize
|
end := i + batchSize
|
||||||
if end > len(toDelete) {
|
if end > len(toDelete) {
|
||||||
end = len(toDelete)
|
end = len(toDelete)
|
||||||
}
|
}
|
||||||
batch := toDelete[i:end]
|
batch := toDelete[i:end]
|
||||||
|
|
||||||
for _, c := range batch {
|
for _, c := range batch {
|
||||||
var clientKey string
|
var clientKey string
|
||||||
switch ib.Protocol {
|
switch ib.Protocol {
|
||||||
case model.Trojan:
|
case model.Trojan:
|
||||||
clientKey = c.Password
|
clientKey = c.Password
|
||||||
case model.Shadowsocks:
|
case model.Shadowsocks:
|
||||||
clientKey = c.Email
|
clientKey = c.Email
|
||||||
default: // vless/vmess
|
default: // vless/vmess
|
||||||
clientKey = c.ID
|
clientKey = c.ID
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
|
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
|
||||||
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
|
||||||
c.Email, ib.Id, ib.Tag, err)
|
c.Email, ib.Id, ib.Tag, err)
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
|
||||||
c.Email, ib.Id, ib.Tag)
|
c.Email, ib.Id, ib.Tag)
|
||||||
// do not restart here
|
// do not restart here
|
||||||
restartNeeded = true
|
restartNeeded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// One time after all batches
|
// One time after all batches
|
||||||
if restartNeeded {
|
if restartNeeded {
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
logger.Info("Xray restart scheduled after batch deletion")
|
logger.Info("Xray restart scheduled after batch deletion")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// clientsToJSON serializes an array of clients to JSON
|
// clientsToJSON serializes an array of clients to JSON
|
||||||
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
|
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
|
||||||
b := strings.Builder{}
|
b := strings.Builder{}
|
||||||
b.WriteString("{\"clients\":[")
|
b.WriteString("{\"clients\":[")
|
||||||
for i, c := range clients {
|
for i, c := range clients {
|
||||||
if i > 0 { b.WriteString(",") }
|
if i > 0 {
|
||||||
b.WriteString(j.clientToJSON(c))
|
b.WriteString(",")
|
||||||
}
|
}
|
||||||
b.WriteString("]}")
|
b.WriteString(j.clientToJSON(c))
|
||||||
return b.String()
|
}
|
||||||
|
b.WriteString("]}")
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ensureClientExists adds client with defaults to inbound tag if not present
|
// ensureClientExists adds client with defaults to inbound tag if not present
|
||||||
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
|
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
|
||||||
inbounds, err := j.inboundService.GetAllInbounds()
|
inbounds, err := j.inboundService.GetAllInbounds()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("ensureClientExists: get inbounds failed:", err)
|
logger.Warning("ensureClientExists: get inbounds failed:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var target *model.Inbound
|
var target *model.Inbound
|
||||||
for _, ib := range inbounds {
|
for _, ib := range inbounds {
|
||||||
if ib.Tag == inboundTag {
|
if ib.Tag == inboundTag {
|
||||||
target = ib
|
target = ib
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if target == nil {
|
if target == nil {
|
||||||
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
|
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// check if email already exists in this inbound
|
// check if email already exists in this inbound
|
||||||
clients, err := j.inboundService.GetClients(target)
|
clients, err := j.inboundService.GetClients(target)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, c := range clients {
|
for _, c := range clients {
|
||||||
if c.Email == email {
|
if c.Email == email {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// build new client according to protocol
|
// build new client according to protocol
|
||||||
newClient := model.Client{
|
newClient := model.Client{
|
||||||
Email: email,
|
Email: email,
|
||||||
Enable: true,
|
Enable: true,
|
||||||
LimitIP: defLimitIP,
|
LimitIP: defLimitIP,
|
||||||
TotalGB: int64(defGB),
|
TotalGB: int64(defGB),
|
||||||
}
|
}
|
||||||
if defExpiryDays > 0 {
|
if defExpiryDays > 0 {
|
||||||
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
|
||||||
}
|
}
|
||||||
|
|
||||||
switch target.Protocol {
|
switch target.Protocol {
|
||||||
case model.Trojan:
|
case model.Trojan:
|
||||||
newClient.Password = uuid.NewString()
|
newClient.Password = uuid.NewString()
|
||||||
case model.Shadowsocks:
|
case model.Shadowsocks:
|
||||||
newClient.Password = uuid.NewString()
|
newClient.Password = uuid.NewString()
|
||||||
default: // VMESS/VLESS and others using ID
|
default: // VMESS/VLESS and others using ID
|
||||||
newClient.ID = uuid.NewString()
|
newClient.ID = uuid.NewString()
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare inbound payload with only the new client
|
// prepare inbound payload with only the new client
|
||||||
payload := &model.Inbound{Id: target.Id}
|
payload := &model.Inbound{Id: target.Id}
|
||||||
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
|
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
|
||||||
|
|
||||||
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
if _, err := j.inboundService.AddInboundClient(payload); err != nil {
|
||||||
logger.Warning("ensureClientExists: add client failed:", err)
|
logger.Warning("ensureClientExists: add client failed:", err)
|
||||||
} else {
|
} else {
|
||||||
j.xrayService.SetToNeedRestart()
|
j.xrayService.SetToNeedRestart()
|
||||||
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
|
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clientToJSON serializes minimal client fields to JSON object string without extra deps
|
// clientToJSON serializes minimal client fields to JSON object string without extra deps
|
||||||
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
func (j *LdapSyncJob) clientToJSON(c model.Client) string {
|
||||||
// construct minimal JSON manually to avoid importing json for simple case
|
// construct minimal JSON manually to avoid importing json for simple case
|
||||||
b := strings.Builder{}
|
b := strings.Builder{}
|
||||||
b.WriteString("{")
|
b.WriteString("{")
|
||||||
if c.ID != "" {
|
if c.ID != "" {
|
||||||
b.WriteString("\"id\":\"")
|
b.WriteString("\"id\":\"")
|
||||||
b.WriteString(c.ID)
|
b.WriteString(c.ID)
|
||||||
b.WriteString("\",")
|
b.WriteString("\",")
|
||||||
}
|
}
|
||||||
if c.Password != "" {
|
if c.Password != "" {
|
||||||
b.WriteString("\"password\":\"")
|
b.WriteString("\"password\":\"")
|
||||||
b.WriteString(c.Password)
|
b.WriteString(c.Password)
|
||||||
b.WriteString("\",")
|
b.WriteString("\",")
|
||||||
}
|
}
|
||||||
b.WriteString("\"email\":\"")
|
b.WriteString("\"email\":\"")
|
||||||
b.WriteString(c.Email)
|
b.WriteString(c.Email)
|
||||||
b.WriteString("\",")
|
b.WriteString("\",")
|
||||||
b.WriteString("\"enable\":")
|
b.WriteString("\"enable\":")
|
||||||
if c.Enable { b.WriteString("true") } else { b.WriteString("false") }
|
if c.Enable {
|
||||||
b.WriteString(",")
|
b.WriteString("true")
|
||||||
b.WriteString("\"limitIp\":")
|
} else {
|
||||||
b.WriteString(strconv.Itoa(c.LimitIP))
|
b.WriteString("false")
|
||||||
b.WriteString(",")
|
}
|
||||||
b.WriteString("\"totalGB\":")
|
b.WriteString(",")
|
||||||
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
|
b.WriteString("\"limitIp\":")
|
||||||
if c.ExpiryTime > 0 {
|
b.WriteString(strconv.Itoa(c.LimitIP))
|
||||||
b.WriteString(",\"expiryTime\":")
|
b.WriteString(",")
|
||||||
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
|
b.WriteString("\"totalGB\":")
|
||||||
}
|
b.WriteString(strconv.FormatInt(c.TotalGB, 10))
|
||||||
b.WriteString("}")
|
if c.ExpiryTime > 0 {
|
||||||
return b.String()
|
b.WriteString(",\"expiryTime\":")
|
||||||
|
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
|
||||||
|
}
|
||||||
|
b.WriteString("}")
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
34
web/middleware/auth.go
Normal file
34
web/middleware/auth.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"x-ui/web/service"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiAuth() gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
apiKey := c.GetHeader("Api-Key")
|
||||||
|
if apiKey == "" {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "API key is required"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
panelAPIKey, err := settingService.GetAPIKey()
|
||||||
|
if err != nil || panelAPIKey == "" {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "API key not configured on the panel"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if apiKey != panelAPIKey {
|
||||||
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,8 +3,11 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -673,6 +676,11 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", "/panel/inbound/api/addClient", bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -761,6 +769,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/%d/delClient/%s", inboundId, clientId), nil)
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -936,6 +949,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
logger.Debug("Client old email not found")
|
logger.Debug("Client old email not found")
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/updateClient/%s", clientId), bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2380,6 +2399,44 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
||||||
|
|
||||||
return validEmails, extraEmails, nil
|
return validEmails, extraEmails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) syncWithSlaves(method string, path string, body io.Reader) {
|
||||||
|
serverService := MultiServerService{}
|
||||||
|
servers, err := serverService.GetServers()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get servers for syncing:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s:%d%s", server.Address, server.Port, path)
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to create request for server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Api-Key", server.APIKey)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to send request to server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
logger.Warningf("Failed to sync with server %s. Status: %s, Body: %s", server.Name, resp.Status, string(bodyBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||||
oldInbound, err := s.GetInbound(inboundId)
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -2471,4 +2528,5 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
72
web/service/inbound_service_sync_test.go
Normal file
72
web/service/inbound_service_sync_test.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInboundServiceSync(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
// Mock server to simulate a slave
|
||||||
|
var receivedApiKey string
|
||||||
|
var receivedBody []byte
|
||||||
|
mockSlave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedApiKey = r.Header.Get("Api-Key")
|
||||||
|
receivedBody, _ = io.ReadAll(r.Body)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer mockSlave.Close()
|
||||||
|
|
||||||
|
// Add the mock slave to the database
|
||||||
|
multiServerService := MultiServerService{}
|
||||||
|
mockSlaveURL, _ := url.Parse(mockSlave.URL)
|
||||||
|
mockSlavePort, _ := strconv.Atoi(mockSlaveURL.Port())
|
||||||
|
slaveServer := &model.Server{
|
||||||
|
Name: "mock-slave",
|
||||||
|
Address: mockSlaveURL.Hostname(),
|
||||||
|
Port: mockSlavePort,
|
||||||
|
APIKey: "slave-api-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
multiServerService.AddServer(slaveServer)
|
||||||
|
|
||||||
|
// Create a test inbound and client
|
||||||
|
inboundService := InboundService{}
|
||||||
|
db := database.GetDB()
|
||||||
|
testInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Remark: "test-inbound",
|
||||||
|
Enable: true,
|
||||||
|
Settings: `{"clients":[]}`,
|
||||||
|
}
|
||||||
|
db.Create(testInbound)
|
||||||
|
|
||||||
|
clientData := model.Client{
|
||||||
|
Email: "test@example.com",
|
||||||
|
ID: "test-id",
|
||||||
|
}
|
||||||
|
clientBytes, _ := json.Marshal([]model.Client{clientData})
|
||||||
|
inboundData := &model.Inbound{
|
||||||
|
Id: testInbound.Id,
|
||||||
|
Settings: string(clientBytes),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test AddInboundClient sync
|
||||||
|
inboundService.AddInboundClient(inboundData)
|
||||||
|
|
||||||
|
assert.Equal(t, "slave-api-key", receivedApiKey)
|
||||||
|
var receivedInbound model.Inbound
|
||||||
|
json.Unmarshal(receivedBody, &receivedInbound)
|
||||||
|
assert.Equal(t, 1, receivedInbound.Id)
|
||||||
|
}
|
37
web/service/multi_server_service.go
Normal file
37
web/service/multi_server_service.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiServerService struct{}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServers() ([]*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var servers []*model.Server
|
||||||
|
err := db.Find(&servers).Error
|
||||||
|
return servers, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var server model.Server
|
||||||
|
err := db.First(&server, id).Error
|
||||||
|
return &server, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) AddServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Create(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) UpdateServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Save(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) DeleteServer(id int) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Delete(&model.Server{}, id).Error
|
||||||
|
}
|
63
web/service/multi_server_service_test.go
Normal file
63
web/service/multi_server_service_test.go
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
dbPath := "test.db"
|
||||||
|
os.Remove(dbPath)
|
||||||
|
database.InitDB(dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
db, _ := database.GetDB().DB()
|
||||||
|
db.Close()
|
||||||
|
os.Remove("test.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiServerService(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
service := MultiServerService{}
|
||||||
|
|
||||||
|
// Test AddServer
|
||||||
|
server := &model.Server{
|
||||||
|
Name: "test-server",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Port: 54321,
|
||||||
|
APIKey: "test-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
err := service.AddServer(server)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GetServer
|
||||||
|
retrievedServer, err := service.GetServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, server.Name, retrievedServer.Name)
|
||||||
|
|
||||||
|
// Test GetServers
|
||||||
|
servers, err := service.GetServers()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, servers, 1)
|
||||||
|
|
||||||
|
// Test UpdateServer
|
||||||
|
retrievedServer.Name = "updated-server"
|
||||||
|
err = service.UpdateServer(retrievedServer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updatedServer, _ := service.GetServer(server.Id)
|
||||||
|
assert.Equal(t, "updated-server", updatedServer.Name)
|
||||||
|
|
||||||
|
// Test DeleteServer
|
||||||
|
err = service.DeleteServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = service.GetServer(server.Id)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
|
@ -204,6 +204,21 @@ func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||||
return setting, nil
|
return setting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetAPIKey() (string, error) {
|
||||||
|
setting, err := s.getSetting("ApiKey")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if setting == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetAPIKey(apiKey string) error {
|
||||||
|
return s.saveSetting("ApiKey", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) saveSetting(key string, value string) error {
|
func (s *SettingService) saveSetting(key string, value string) error {
|
||||||
setting, err := s.getSetting(key)
|
setting, err := s.getSetting(key)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
|
|
||||||
[pages.login]
|
[pages.login]
|
||||||
"hello" = "Привет!"
|
"hello" = "Привет!"
|
||||||
"title" = "Приветствие!"
|
"title" = "Добро пожаловать!"
|
||||||
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
||||||
|
|
||||||
[pages.login.toasts]
|
[pages.login.toasts]
|
||||||
|
|
Loading…
Reference in a new issue