Resolve merge conflicts with upstream/main

This commit is contained in:
Dikiy13371 2025-12-06 18:20:52 +03:00
commit c76c0bd7cb
46 changed files with 1222 additions and 1146 deletions

View file

@ -85,7 +85,7 @@ jobs:
cd x-ui/bin cd x-ui/bin
# Download dependencies # Download dependencies
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/" Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
if [ "${{ matrix.platform }}" == "amd64" ]; then if [ "${{ matrix.platform }}" == "amd64" ]; then
wget -q ${Xray_URL}Xray-linux-64.zip wget -q ${Xray_URL}Xray-linux-64.zip
unzip Xray-linux-64.zip unzip Xray-linux-64.zip
@ -183,7 +183,7 @@ jobs:
cd x-ui\bin cd x-ui\bin
# Download Xray for Windows # Download Xray for Windows
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/" $Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip" Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath . Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
Remove-Item "Xray-windows-64.zip" Remove-Item "Xray-windows-64.zip"

View file

@ -27,7 +27,7 @@ case $1 in
esac esac
mkdir -p build/bin mkdir -p build/bin
cd build/bin cd build/bin
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip" wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.12.2/Xray-linux-${ARCH}.zip"
unzip "Xray-linux-${ARCH}.zip" unzip "Xray-linux-${ARCH}.zip"
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
mv xray "xray-linux-${FNAME}" mv xray "xray-linux-${FNAME}"

View file

@ -19,7 +19,7 @@
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols. **3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
Test test test Test test test
> [!IMPORTANT] > [!IMPORTANT]
> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment. > This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment.
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features. As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.

View file

@ -1 +1 @@
2.8.4 2.8.5

View file

@ -3,11 +3,13 @@ package database
import ( import (
"errors" "errors"
"fmt" "fmt"
"os"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger"
) )
var db *gorm.DB var db *gorm.DB
@ -38,6 +40,18 @@ func GetDB() *gorm.DB {
return db return db
} }
// CloseDB закрывает соединение с БД.
func CloseDB() error {
if db == nil {
return nil
}
sqlDB, err := db.DB()
if err != nil {
return err
}
return sqlDB.Close()
}
// IsNotFound — хелпер для проверки "запись не найдена". // IsNotFound — хелпер для проверки "запись не найдена".
func IsNotFound(err error) bool { func IsNotFound(err error) bool {
return errors.Is(err, gorm.ErrRecordNotFound) return errors.Is(err, gorm.ErrRecordNotFound)
@ -84,3 +98,29 @@ func SeedAdmin() error {
} }
return db.Create(&admin).Error return db.Create(&admin).Error
} }
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
// and runs a PRAGMA integrity_check to ensure the file is structurally sound.
// It does not mutate global state or run migrations.
func ValidateSQLiteDB(dbPath string) error {
if _, err := os.Stat(dbPath); err != nil { // file must exist
return err
}
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
return err
}
sqlDB, err := gdb.DB()
if err != nil {
return err
}
defer sqlDB.Close()
var res string
if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
return err
}
if res != "ok" {
return errors.New("sqlite integrity check failed: " + res)
}
return nil
}

76
go.mod
View file

@ -1,33 +1,33 @@
module github.com/mhsanaei/3x-ui/v2 module github.com/mhsanaei/3x-ui/v2
go 1.25.1 go 1.25.5
require ( require (
github.com/gin-contrib/gzip v1.2.3 github.com/gin-contrib/gzip v1.2.5
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/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/mymmrac/telego v1.3.0 github.com/mymmrac/telego v1.3.1
github.com/nicksnyder/go-i18n/v2 v2.6.0 github.com/nicksnyder/go-i18n/v2 v2.6.0
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.11
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.68.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250911.0 github.com/xtls/xray-core v1.251202.0
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.45.0
golang.org/x/sys v0.36.0 golang.org/x/sys v0.38.0
golang.org/x/text v0.29.0 golang.org/x/text v0.31.0
google.golang.org/grpc v1.75.1 google.golang.org/grpc v1.77.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.1
) )
require ( require (
@ -41,24 +41,24 @@ require (
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect github.com/bytedance/sonic v1.14.2 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cloudflare/circl v1.6.1 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect github.com/cloudwego/base64x v0.1.6 // indirect
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
github.com/ebitengine/purego v0.9.0 // indirect github.com/ebitengine/purego v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect github.com/gin-contrib/sse v1.1.0 // indirect
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
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.19.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
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
@ -68,48 +68,46 @@ require (
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/juju/ratelimit v1.0.2 // indirect github.com/juju/ratelimit v1.0.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // indirect github.com/miekg/dns v1.1.68 // indirect
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/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.6.0 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.57.1 // indirect
github.com/refraction-networking/utls v1.8.0 // indirect github.com/refraction-networking/utls v1.8.1 // 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.13 // 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.16 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.1 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
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-20251116175510-cd53f7d50237 // 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.23.0 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.44.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.17.0 // indirect golang.org/x/sync v0.18.0 // indirect
golang.org/x/time v0.13.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.39.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-20251202230838-ff82c1b0f217 // 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
) )

177
go.sum
View file

@ -1,22 +1,21 @@
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
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=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w= github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc= github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -25,14 +24,14 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k= github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U= github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c= github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
@ -45,8 +44,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
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=
@ -60,12 +59,12 @@ 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.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
@ -117,8 +116,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI= github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk= github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@ -127,8 +126,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA= github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
@ -138,8 +137,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg= github.com/mymmrac/telego v1.3.1 h1:dI5D8LKWBw241W02LmJqoSLZXW3tuLokxVoNbIZUYQg=
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8= github.com/mymmrac/telego v1.3.1/go.mod h1:3D0h4jJ3OzubY/gI4xDIGx4jkY26fmcPyqx0Lq24+zI=
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ= github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE= github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@ -154,12 +153,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
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.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -169,40 +168,42 @@ 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.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.13/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.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
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=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/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= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU= github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs= github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0= github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
@ -211,68 +212,68 @@ 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-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno= github.com/xtls/xray-core v1.251202.0 h1:VwoBnq9IRTbYWEBhR0CqEw2cNjTlXYH6WxzKbSjx+XE=
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k= github.com/xtls/xray-core v1.251202.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw= golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
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-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
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=
@ -282,8 +283,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=

View file

@ -15,7 +15,7 @@ cur_dir=$(pwd)
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
source /etc/os-release source /etc/os-release
release=$ID release=$ID
elif [[ -f /usr/lib/os-release ]]; then elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release source /usr/lib/os-release
release=$ID release=$ID
else else
@ -26,14 +26,14 @@ echo "The OS release is: $release"
arch() { arch() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;; x86_64 | x64 | amd64) echo 'amd64' ;;
i*86 | x86) echo '386' ;; i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;; armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;; armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;; armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;; s390x) echo 's390x' ;;
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac esac
} }
@ -41,26 +41,30 @@ echo "Arch: $(arch)"
install_base() { install_base() {
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum -y update && yum install -y -q wget curl tar tzdata dnf -y update && dnf install -y -q wget curl tar tzdata
;; ;;
fedora | amzn | virtuozzo) centos)
dnf -y update && dnf install -y -q wget curl tar tzdata if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum install -y wget curl tar tzdata
else
dnf -y update && dnf install -y -q wget curl tar tzdata
fi
;; ;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;; ;;
opensuse-tumbleweed | opensuse-leap) opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y wget curl tar timezone zypper refresh && zypper -q install -y wget curl tar timezone
;; ;;
alpine) alpine)
apk update && apk add wget curl tar tzdata apk update && apk add wget curl tar tzdata
;; ;;
*) *)
apt-get update && apt-get install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
esac esac
} }
@ -77,11 +81,11 @@ config_after_install() {
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local URL_lists=( local URL_lists=(
"https://api4.ipify.org" "https://api4.ipify.org"
"https://ipv4.icanhazip.com" "https://ipv4.icanhazip.com"
"https://v4.api.ipinfo.io/ip" "https://v4.api.ipinfo.io/ip"
"https://ipv4.myexternalip.com/raw" "https://ipv4.myexternalip.com/raw"
"https://4.ident.me" "https://4.ident.me"
"https://check-host.net/ip" "https://check-host.net/ip"
) )
local server_ip="" local server_ip=""
for ip_address in "${URL_lists[@]}"; do for ip_address in "${URL_lists[@]}"; do
@ -90,13 +94,13 @@ config_after_install() {
break break
fi fi
done done
if [[ ${#existing_webBasePath} -lt 4 ]]; then if [[ ${#existing_webBasePath} -lt 4 ]]; then
if [[ "$existing_hasDefaultCredential" == "true" ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_webBasePath=$(gen_random_string 18) local config_webBasePath=$(gen_random_string 18)
local config_username=$(gen_random_string 10) local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10) local config_password=$(gen_random_string 10)
read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -rp "Please set up the panel port: " config_port read -rp "Please set up the panel port: " config_port
@ -105,7 +109,7 @@ config_after_install() {
local config_port=$(shuf -i 1024-62000 -n 1) local config_port=$(shuf -i 1024-62000 -n 1)
echo -e "${yellow}Generated random port: ${config_port}${plain}" echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi fi
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
echo -e "This is a fresh installation, generating random login info for security concerns:" echo -e "This is a fresh installation, generating random login info for security concerns:"
echo -e "###############################################" echo -e "###############################################"
@ -126,7 +130,7 @@ config_after_install() {
if [[ "$existing_hasDefaultCredential" == "true" ]]; then if [[ "$existing_hasDefaultCredential" == "true" ]]; then
local config_username=$(gen_random_string 10) local config_username=$(gen_random_string 10)
local config_password=$(gen_random_string 10) local config_password=$(gen_random_string 10)
echo -e "${yellow}Default credentials detected. Security update required...${plain}" echo -e "${yellow}Default credentials detected. Security update required...${plain}"
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
echo -e "Generated new random login credentials:" echo -e "Generated new random login credentials:"
@ -138,13 +142,13 @@ config_after_install() {
echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}" echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}"
fi fi
fi fi
/usr/local/x-ui/x-ui migrate /usr/local/x-ui/x-ui migrate
} }
install_x-ui() { install_x-ui() {
cd /usr/local/ cd /usr/local/
# Download resources # Download resources
if [ $# == 0 ]; then if [ $# == 0 ]; then
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
@ -166,12 +170,12 @@ install_x-ui() {
tag_version=$1 tag_version=$1
tag_version_numeric=${tag_version#v} tag_version_numeric=${tag_version#v}
min_version="2.3.5" min_version="2.3.5"
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}" echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
exit 1 exit 1
fi fi
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz" url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
echo -e "Beginning to install x-ui $1" echo -e "Beginning to install x-ui $1"
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url} wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
@ -185,7 +189,7 @@ install_x-ui() {
echo -e "${red}Failed to download x-ui.sh${plain}" echo -e "${red}Failed to download x-ui.sh${plain}"
exit 1 exit 1
fi fi
# Stop x-ui service and remove old resources # Stop x-ui service and remove old resources
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
@ -195,7 +199,7 @@ install_x-ui() {
fi fi
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
fi fi
# Extract resources and set permissions # Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz tar zxvf x-ui-linux-$(arch).tar.gz
rm x-ui-linux-$(arch).tar.gz -f rm x-ui-linux-$(arch).tar.gz -f
@ -203,19 +207,19 @@ install_x-ui() {
cd x-ui cd x-ui
chmod +x x-ui chmod +x x-ui
chmod +x x-ui.sh chmod +x x-ui.sh
# Check the system's architecture and rename the file accordingly # Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm mv bin/xray-linux-$(arch) bin/xray-linux-arm
chmod +x bin/xray-linux-arm chmod +x bin/xray-linux-arm
fi fi
chmod +x x-ui bin/xray-linux-$(arch) chmod +x x-ui bin/xray-linux-$(arch)
# Update x-ui cli and se set permission # Update x-ui cli and se set permission
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@ -231,7 +235,7 @@ install_x-ui() {
systemctl enable x-ui systemctl enable x-ui
systemctl start x-ui systemctl start x-ui
fi fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐
@ -248,7 +252,7 @@ install_x-ui() {
${blue}x-ui log${plain} - Check logs │ ${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │ ${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │ ${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │ ${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │ ${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘" └───────────────────────────────────────────────────────┘"

View file

@ -1,21 +1,29 @@
// Package logger provides logging functionality for the 3x-ui panel with // Package logger provides logging functionality for the 3x-ui panel with
// buffered log storage and multiple log levels. // dual-backend logging (console/syslog and file) and buffered log storage for web UI.
package logger package logger
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"runtime"
"time" "time"
"github.com/mhsanaei/3x-ui/v2/config"
"github.com/op/go-logging" "github.com/op/go-logging"
) )
var ( const (
logger *logging.Logger maxLogBufferSize = 10240 // Maximum log entries kept in memory
logFileName = "3xui.log" // Log file name
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
)
// addToBuffer appends a log entry into the in-memory ring buffer used for var (
// retrieving recent logs via the web UI. It keeps the buffer bounded to avoid logger *logging.Logger
// uncontrolled growth. logFile *os.File
// logBuffer maintains recent log entries in memory for web UI retrieval
logBuffer []struct { logBuffer []struct {
time string time string
level logging.Level level logging.Level
@ -23,37 +31,100 @@ var (
} }
) )
func init() { // InitLogger initializes dual logging backends: console/syslog and file.
InitLogger(logging.INFO) // Console logging uses the specified level, file logging always uses DEBUG level.
}
// InitLogger initializes the logger with the specified logging level.
func InitLogger(level logging.Level) { func InitLogger(level logging.Level) {
newLogger := logging.MustGetLogger("x-ui") newLogger := logging.MustGetLogger("x-ui")
var err error backends := make([]logging.Backend, 0, 2)
var backend logging.Backend
var format logging.Formatter
ppid := os.Getppid()
backend, err = logging.NewSyslogBackend("") // Console/syslog backend with configurable level
if err != nil { if consoleBackend := initDefaultBackend(); consoleBackend != nil {
println(err) leveledBackend := logging.AddModuleLevel(consoleBackend)
backend = logging.NewLogBackend(os.Stderr, "", 0) leveledBackend.SetLevel(level, "x-ui")
} backends = append(backends, leveledBackend)
if ppid > 0 && err != nil {
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
} else {
format = logging.MustStringFormatter(`%{level} - %{message}`)
} }
backendFormatter := logging.NewBackendFormatter(backend, format) // File backend with DEBUG level for comprehensive logging
backendLeveled := logging.AddModuleLevel(backendFormatter) if fileBackend := initFileBackend(); fileBackend != nil {
backendLeveled.SetLevel(level, "x-ui") leveledBackend := logging.AddModuleLevel(fileBackend)
newLogger.SetBackend(backendLeveled) leveledBackend.SetLevel(logging.DEBUG, "x-ui")
backends = append(backends, leveledBackend)
}
multiBackend := logging.MultiLogger(backends...)
newLogger.SetBackend(multiBackend)
logger = newLogger logger = newLogger
} }
// initDefaultBackend creates the console/syslog logging backend.
// Windows: Uses stderr directly (no syslog support)
// Unix-like: Attempts syslog, falls back to stderr
func initDefaultBackend() logging.Backend {
var backend logging.Backend
includeTime := false
if runtime.GOOS == "windows" {
// Windows: Use stderr directly (no syslog support)
backend = logging.NewLogBackend(os.Stderr, "", 0)
includeTime = true
} else {
// Unix-like: Try syslog, fallback to stderr
if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
backend = logging.NewLogBackend(os.Stderr, "", 0)
includeTime = os.Getppid() > 0
} else {
backend = syslogBackend
}
}
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
}
// initFileBackend creates the file logging backend.
// Creates log directory and truncates log file on startup for fresh logs.
func initFileBackend() logging.Backend {
logDir := config.GetLogFolder()
if err := os.MkdirAll(logDir, 0o750); err != nil {
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
return nil
}
logPath := filepath.Join(logDir, logFileName)
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
return nil
}
// Close previous log file if exists
if logFile != nil {
_ = logFile.Close()
}
logFile = file
backend := logging.NewLogBackend(file, "", 0)
return logging.NewBackendFormatter(backend, newFormatter(true))
}
// newFormatter creates a log formatter with optional timestamp.
func newFormatter(withTime bool) logging.Formatter {
format := `%{level} - %{message}`
if withTime {
format = `%{time:` + timeFormat + `} %{level} - %{message}`
}
return logging.MustStringFormatter(format)
}
// CloseLogger closes the log file and cleans up resources.
// Should be called during application shutdown.
func CloseLogger() {
if logFile != nil {
_ = logFile.Close()
logFile = nil
}
}
// Debug logs a debug message and adds it to the log buffer. // Debug logs a debug message and adds it to the log buffer.
func Debug(args ...any) { func Debug(args ...any) {
logger.Debug(args...) logger.Debug(args...)
@ -114,9 +185,10 @@ func Errorf(format string, args ...any) {
addToBuffer("ERROR", fmt.Sprintf(format, args...)) addToBuffer("ERROR", fmt.Sprintf(format, args...))
} }
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
func addToBuffer(level string, newLog string) { func addToBuffer(level string, newLog string) {
t := time.Now() t := time.Now()
if len(logBuffer) >= 10240 { if len(logBuffer) >= maxLogBufferSize {
logBuffer = logBuffer[1:] logBuffer = logBuffer[1:]
} }
@ -126,7 +198,7 @@ func addToBuffer(level string, newLog string) {
level logging.Level level logging.Level
log string log string
}{ }{
time: t.Format("2006/01/02 15:04:05"), time: t.Format(timeFormat),
level: logLevel, level: logLevel,
log: newLog, log: newLog,
}) })

22
main.go
View file

@ -78,6 +78,10 @@ func runWebServer() {
case syscall.SIGHUP: case syscall.SIGHUP:
logger.Info("Received SIGHUP signal. Restarting servers...") logger.Info("Received SIGHUP signal. Restarting servers...")
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
service.StopBot()
// --
err := server.Stop() err := server.Stop()
if err != nil { if err != nil {
logger.Debug("Error stopping web server:", err) logger.Debug("Error stopping web server:", err)
@ -106,6 +110,10 @@ func runWebServer() {
log.Println("Sub server restarted successfully.") log.Println("Sub server restarted successfully.")
default: default:
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
service.StopBot()
// ------------------------------------------------------------
server.Stop() server.Stop()
subServer.Stop() subServer.Stop()
log.Println("Shutting down servers.") log.Println("Shutting down servers.")
@ -321,6 +329,20 @@ func updateCert(publicKey string, privateKey string) {
} else { } else {
fmt.Println("set certificate private key success") fmt.Println("set certificate private key success")
} }
err = settingService.SetSubCertFile(publicKey)
if err != nil {
fmt.Println("set certificate for subscription public key failed:", err)
} else {
fmt.Println("set certificate for subscription public key success")
}
err = settingService.SetSubKeyFile(privateKey)
if err != nil {
fmt.Println("set certificate for subscription private key failed:", err)
} else {
fmt.Println("set certificate for subscription private key success")
}
} else { } else {
fmt.Println("both public and private key should be entered.") fmt.Println("both public and private key should be entered.")
} }

View file

@ -103,7 +103,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if basePath != "/" && !strings.HasSuffix(basePath, "/") { if basePath != "/" && !strings.HasSuffix(basePath, "/") {
basePath += "/" basePath += "/"
} }
logger.Debug("sub: Setting base_path to:", basePath) // logger.Debug("sub: Setting base_path to:", basePath)
engine.Use(func(c *gin.Context) { engine.Use(func(c *gin.Context) {
c.Set("base_path", basePath) c.Set("base_path", basePath)
}) })

376
update.sh
View file

@ -9,233 +9,237 @@ plain='\033[0m'
# Don't edit this config # Don't edit this config
b_source="${BASH_SOURCE[0]}" b_source="${BASH_SOURCE[0]}"
while [ -h "$b_source" ]; do while [ -h "$b_source" ]; do
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
b_source="$(readlink "$b_source")" b_source="$(readlink "$b_source")"
[[ $b_source != /* ]] && b_source="$b_dir/$b_source" [[ $b_source != /* ]] && b_source="$b_dir/$b_source"
done done
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)" cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
script_name=$(basename "$0") script_name=$(basename "$0")
# Check command exist function # Check command exist function
_command_exists() { _command_exists() {
type "$1" &>/dev/null type "$1" &>/dev/null
} }
# Fail, log and exit script function # Fail, log and exit script function
_fail() { _fail() {
local msg=${1} local msg=${1}
echo -e "${red}${msg}${plain}" echo -e "${red}${msg}${plain}"
exit 2 exit 2
} }
# check root # check root
[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege." [[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
if _command_exists wget; then if _command_exists wget; then
wget_bin=$(which wget) wget_bin=$(which wget)
else else
_fail "ERROR: Command 'wget' not found." _fail "ERROR: Command 'wget' not found."
fi fi
if _command_exists curl; then if _command_exists curl; then
curl_bin=$(which curl) curl_bin=$(which curl)
else else
_fail "ERROR: Command 'curl' not found." _fail "ERROR: Command 'curl' not found."
fi fi
# Check OS and set release variable # Check OS and set release variable
if [[ -f /etc/os-release ]]; then if [[ -f /etc/os-release ]]; then
source /etc/os-release source /etc/os-release
release=$ID release=$ID
elif [[ -f /usr/lib/os-release ]]; then elif [[ -f /usr/lib/os-release ]]; then
source /usr/lib/os-release source /usr/lib/os-release
release=$ID release=$ID
else else
_fail "Failed to check the system OS, please contact the author!" _fail "Failed to check the system OS, please contact the author!"
fi fi
echo "The OS release is: $release" echo "The OS release is: $release"
arch() { arch() {
case "$(uname -m)" in case "$(uname -m)" in
x86_64 | x64 | amd64) echo 'amd64' ;; x86_64 | x64 | amd64) echo 'amd64' ;;
i*86 | x86) echo '386' ;; i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;; armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;; armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;; armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;; s390x) echo 's390x' ;;
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;; *) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
esac esac
} }
echo "Arch: $(arch)" echo "Arch: $(arch)"
install_base() { install_base() {
echo -e "${green}Updating and install dependency packages...${plain}" echo -e "${green}Updating and install dependency packages...${plain}"
case "${release}" in case "${release}" in
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1 apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1 dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
;; ;;
fedora | amzn | virtuozzo) centos)
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1 if [[ "${VERSION_ID}" =~ ^7 ]]; then
;; yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1
arch | manjaro | parch) else
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1 dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
;; fi
opensuse-tumbleweed | opensuse-leap) ;;
zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1 arch | manjaro | parch)
;; pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1
alpine) ;;
apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1 opensuse-tumbleweed | opensuse-leap)
;; zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1
*) ;;
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1 alpine)
;; apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1
esac ;;
*)
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1
;;
esac
} }
config_after_update() { config_after_update() {
echo -e "${yellow}x-ui settings:${plain}" echo -e "${yellow}x-ui settings:${plain}"
/usr/local/x-ui/x-ui setting -show true /usr/local/x-ui/x-ui setting -show true
/usr/local/x-ui/x-ui migrate /usr/local/x-ui/x-ui migrate
} }
update_x-ui() { update_x-ui() {
cd /usr/local/ cd /usr/local/
if [ -f "/usr/local/x-ui/x-ui" ]; then if [ -f "/usr/local/x-ui/x-ui" ]; then
current_xui_version=$(/usr/local/x-ui/x-ui -v) current_xui_version=$(/usr/local/x-ui/x-ui -v)
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}" echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
else else
_fail "ERROR: Current x-ui version: unknown" _fail "ERROR: Current x-ui version: unknown"
fi fi
echo -e "${green}Downloading new x-ui version...${plain}" echo -e "${green}Downloading new x-ui version...${plain}"
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then if [[ ! -n "$tag_version" ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$tag_version" ]]; then if [[ ! -n "$tag_version" ]]; then
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later" _fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
fi fi
fi fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..." echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
${wget_bin} -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${wget_bin} -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch version with IPv4...${plain}" echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
${wget_bin} --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null ${wget_bin} --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub" _fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
fi fi
fi fi
if [[ -e /usr/local/x-ui/ ]]; then if [[ -e /usr/local/x-ui/ ]]; then
echo -e "${green}Stopping x-ui...${plain}" echo -e "${green}Stopping x-ui...${plain}"
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
if [ -f "/etc/init.d/x-ui" ]; then if [ -f "/etc/init.d/x-ui" ]; then
rc-service x-ui stop >/dev/null 2>&1 rc-service x-ui stop >/dev/null 2>&1
rc-update del x-ui >/dev/null 2>&1 rc-update del x-ui >/dev/null 2>&1
echo -e "${green}Removing old service unit version...${plain}" echo -e "${green}Removing old service unit version...${plain}"
rm -f /etc/init.d/x-ui >/dev/null 2>&1 rm -f /etc/init.d/x-ui >/dev/null 2>&1
else else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui service unit not installed." _fail "ERROR: x-ui service unit not installed."
fi fi
else else
if [ -f "/etc/systemd/system/x-ui.service" ]; then if [ -f "/etc/systemd/system/x-ui.service" ]; then
systemctl stop x-ui >/dev/null 2>&1 systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1 systemctl disable x-ui >/dev/null 2>&1
echo -e "${green}Removing old systemd unit version...${plain}" echo -e "${green}Removing old systemd unit version...${plain}"
rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1 rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1
else else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui systemd unit not installed." _fail "ERROR: x-ui systemd unit not installed."
fi fi
fi fi
echo -e "${green}Removing old x-ui version...${plain}" echo -e "${green}Removing old x-ui version...${plain}"
rm /usr/bin/x-ui -f >/dev/null 2>&1 rm /usr/bin/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1 rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1 rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1 rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}" echo -e "${green}Removing old xray version...${plain}"
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1 rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}" echo -e "${green}Removing old README and LICENSE file...${plain}"
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1 rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1 rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1
else else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
_fail "ERROR: x-ui not installed." _fail "ERROR: x-ui not installed."
fi fi
echo -e "${green}Installing new x-ui version...${plain}" echo -e "${green}Installing new x-ui version...${plain}"
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1 tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1 rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
cd x-ui >/dev/null 2>&1 cd x-ui >/dev/null 2>&1
chmod +x x-ui >/dev/null 2>&1 chmod +x x-ui >/dev/null 2>&1
# Check the system's architecture and rename the file accordingly # Check the system's architecture and rename the file accordingly
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1 mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
chmod +x bin/xray-linux-arm >/dev/null 2>&1 chmod +x bin/xray-linux-arm >/dev/null 2>&1
fi fi
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1 chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
echo -e "${green}Downloading and installing x-ui.sh script...${plain}" echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
${wget_bin} -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 ${wget_bin} -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}" echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
${wget_bin} --inet4-only -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1 ${wget_bin} --inet4-only -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub" _fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
fi fi
fi fi
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1 chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
chmod +x /usr/bin/x-ui >/dev/null 2>&1 chmod +x /usr/bin/x-ui >/dev/null 2>&1
echo -e "${green}Changing owner...${plain}" echo -e "${green}Changing owner...${plain}"
chown -R root:root /usr/local/x-ui >/dev/null 2>&1 chown -R root:root /usr/local/x-ui >/dev/null 2>&1
if [ -f "/usr/local/x-ui/bin/config.json" ]; then if [ -f "/usr/local/x-ui/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}" echo -e "${green}Changing on config file permissions...${plain}"
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1 chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
fi fi
if [[ $release == "alpine" ]]; then if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}" echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1 ${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
${wget_bin} --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1 ${wget_bin} --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub" _fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
fi fi
fi fi
chmod +x /etc/init.d/x-ui >/dev/null 2>&1 chmod +x /etc/init.d/x-ui >/dev/null 2>&1
chown root:root /etc/init.d/x-ui >/dev/null 2>&1 chown root:root /etc/init.d/x-ui >/dev/null 2>&1
rc-update add x-ui >/dev/null 2>&1 rc-update add x-ui >/dev/null 2>&1
rc-service x-ui start >/dev/null 2>&1 rc-service x-ui start >/dev/null 2>&1
else else
echo -e "${green}Installing systemd unit...${plain}" echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1 cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1 chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1 systemctl daemon-reload >/dev/null 2>&1
systemctl enable x-ui >/dev/null 2>&1 systemctl enable x-ui >/dev/null 2>&1
systemctl start x-ui >/dev/null 2>&1 systemctl start x-ui >/dev/null 2>&1
fi fi
config_after_update config_after_update
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..." echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
echo -e "" echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌───────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain} ${blue}x-ui control menu usages (subcommands):${plain}
│ │ │ │
${blue}x-ui${plain} - Admin Management Script │ ${blue}x-ui${plain} - Admin Management Script │
${blue}x-ui start${plain} - Start │ ${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │ ${blue}x-ui stop${plain} - Stop │
@ -247,7 +251,7 @@ update_x-ui() {
${blue}x-ui log${plain} - Check logs │ ${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │ ${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │ ${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui install${plain} - Install │ ${blue}x-ui install${plain} - Install │
${blue}x-ui uninstall${plain} - Uninstall │ ${blue}x-ui uninstall${plain} - Uninstall │
└───────────────────────────────────────────────────────┘" └───────────────────────────────────────────────────────┘"

View file

@ -23,29 +23,22 @@ type Config struct {
// FetchVlessFlags returns map[email]enabled // FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) { func FetchVlessFlags(cfg Config) (map[string]bool, error) {
if cfg.Host == "" {
return nil, fmt.Errorf("LDAP host is required")
}
if cfg.BaseDN == "" {
return nil, fmt.Errorf("LDAP base DN is required")
}
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn var conn *ldap.Conn
var err error var err error
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: true}) conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else { } else {
conn, err = ldap.Dial("tcp", addr) conn, err = ldap.Dial("tcp", addr)
} }
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to connect to LDAP server %s: %w", addr, err) return nil, err
} }
defer conn.Close() defer conn.Close()
if cfg.BindDN != "" { if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return nil, fmt.Errorf("failed to bind with DN %s: %w", cfg.BindDN, err) return nil, err
} }
} }
@ -70,9 +63,8 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
res, err := conn.Search(req) res, err := conn.Search(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("LDAP search failed: %w", err) return nil, err
} }
result := make(map[string]bool, len(res.Entries)) result := make(map[string]bool, len(res.Entries))
for _, e := range res.Entries { for _, e := range res.Entries {
user := e.GetAttributeValue(cfg.UserAttr) user := e.GetAttributeValue(cfg.UserAttr)
@ -97,9 +89,6 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) { func AuthenticateUser(cfg Config, username, password string) (bool, error) {
if cfg.Host == "" {
return false, fmt.Errorf("LDAP host is required")
}
if cfg.BaseDN == "" { if cfg.BaseDN == "" {
return false, fmt.Errorf("LDAP base DN is required") return false, fmt.Errorf("LDAP base DN is required")
} }
@ -114,7 +103,7 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
var conn *ldap.Conn var conn *ldap.Conn
var err error var err error
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: true}) conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else { } else {
conn, err = ldap.Dial("tcp", addr) conn, err = ldap.Dial("tcp", addr)
} }
@ -130,6 +119,9 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
} }
} }
if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)"
}
if cfg.UserFilter == "" { if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)" cfg.UserFilter = "(objectClass=person)"
} }
@ -148,14 +140,11 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
) )
res, err := conn.Search(req) res, err := conn.Search(req)
if err != nil { if err != nil {
return false, fmt.Errorf("LDAP search failed for user %s: %w", username, err) return false, err
} }
if len(res.Entries) == 0 { if len(res.Entries) == 0 {
return false, nil return false, nil
} }
if len(res.Entries) > 1 {
return false, fmt.Errorf("multiple entries found for user %s", username)
}
userDN := res.Entries[0].DN userDN := res.Entries[0].DN
// Try to bind as the user // Try to bind as the user
if err := conn.Bind(userDN, password); err != nil { if err := conn.Bind(userDN, password); err != nil {

View file

@ -729,8 +729,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
target = 'google.com:443', target = '',
serverNames = 'google.com,www.google.com', serverNames = '',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
maxClientVer = '', maxClientVer = '',
@ -740,6 +740,14 @@ class RealityStreamSettings extends XrayCommonClass {
settings = new RealityStreamSettings.Settings() settings = new RealityStreamSettings.Settings()
) { ) {
super(); super();
// If target/serverNames are not provided, use random values
if (!target && !serverNames) {
const randomTarget = typeof getRandomRealityTarget !== 'undefined'
? getRandomRealityTarget()
: { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
target = randomTarget.target;
serverNames = randomTarget.sni;
}
this.show = show; this.show = show;
this.xver = xver; this.xver = xver;
this.target = target; this.target = target;

View file

@ -0,0 +1,31 @@
// List of popular services for VLESS Reality Target/SNI randomization
const REALITY_TARGETS = [
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
];
/**
* Returns a random Reality target configuration from the predefined list
* @returns {Object} Object with target and sni properties
*/
function getRandomRealityTarget() {
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
const selected = REALITY_TARGETS[randomIndex];
// Return a copy to avoid reference issues
return {
target: selected.target,
sni: selected.sni
};
}

View file

@ -1,151 +0,0 @@
const oneMinute = 1000 * 60; // MilliseConds in a Minute
const oneHour = oneMinute * 60; // The milliseconds of one hour
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
const oneWeek = oneDay * 7; // The milliseconds per week
const oneMonth = oneDay * 30; // The milliseconds of a month
/**
* Decrease according to the number of days
*
* @param days to reduce the number of days to be reduced
*/
Date.prototype.minusDays = function (days) {
return this.minusMillis(oneDay * days);
};
/**
* Increase according to the number of days
*
* @param days The number of days to be increased
*/
Date.prototype.plusDays = function (days) {
return this.plusMillis(oneDay * days);
};
/**
* A few
*
* @param hours to be reduced
*/
Date.prototype.minusHours = function (hours) {
return this.minusMillis(oneHour * hours);
};
/**
* Increase hourly
*
* @param hours to increase the number of hours
*/
Date.prototype.plusHours = function (hours) {
return this.plusMillis(oneHour * hours);
};
/**
* Make reduction in minutes
*
* @param minutes to reduce the number of minutes
*/
Date.prototype.minusMinutes = function (minutes) {
return this.minusMillis(oneMinute * minutes);
};
/**
* Add in minutes
*
* @param minutes to increase the number of minutes
*/
Date.prototype.plusMinutes = function (minutes) {
return this.plusMillis(oneMinute * minutes);
};
/**
* Decrease in milliseconds
*
* @param millis to reduce the milliseconds
*/
Date.prototype.minusMillis = function(millis) {
let time = this.getTime() - millis;
let newDate = new Date();
newDate.setTime(time);
return newDate;
};
/**
* Add in milliseconds to increase
*
* @param millis to increase the milliseconds to increase
*/
Date.prototype.plusMillis = function(millis) {
let time = this.getTime() + millis;
let newDate = new Date();
newDate.setTime(time);
return newDate;
};
/**
* Setting time is 00: 00: 00.000 on the day
*/
Date.prototype.setMinTime = function () {
this.setHours(0);
this.setMinutes(0);
this.setSeconds(0);
this.setMilliseconds(0);
return this;
};
/**
* Setting time is 23: 59: 59.999 on the same day
*/
Date.prototype.setMaxTime = function () {
this.setHours(23);
this.setMinutes(59);
this.setSeconds(59);
this.setMilliseconds(999);
return this;
};
/**
* Formatting date
*/
Date.prototype.formatDate = function () {
return this.getFullYear() + "-" + NumberFormatter.addZero(this.getMonth() + 1) + "-" + NumberFormatter.addZero(this.getDate());
};
/**
* Format time
*/
Date.prototype.formatTime = function () {
return NumberFormatter.addZero(this.getHours()) + ":" + NumberFormatter.addZero(this.getMinutes()) + ":" + NumberFormatter.addZero(this.getSeconds());
};
/**
* Formatting date plus time
*
* @param split Date and time separation symbols, default is a space
*/
Date.prototype.formatDateTime = function (split = ' ') {
return this.formatDate() + split + this.formatTime();
};
class DateUtil {
// String to date object
static parseDate(str) {
return new Date(str.replace(/-/g, '/'));
}
static formatMillis(millis) {
return moment(millis).format('YYYY-M-D HH:mm:ss');
}
static firstDayOfMonth() {
const date = new Date();
date.setDate(1);
date.setMinTime();
return date;
}
static convertToJalalian(date) {
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
}
}

View file

@ -882,4 +882,35 @@ class FileManager {
link.remove(); link.remove();
} }
}
class IntlUtil {
static formatDate(date) {
const language = LanguageManager.getLanguage()
let intlOptions = {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric"
}
const intl = new Intl.DateTimeFormat(
language,
intlOptions
)
return intl.format(new Date(date))
}
static formatRelativeTime(date) {
const language = LanguageManager.getLanguage()
const now = new Date()
const diff = Math.round((date - now) / (1000 * 60 * 60 * 24))
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
return formatter.format(diff, 'day');
}
} }

View file

@ -44,7 +44,6 @@
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qs/qs.min.js"></script> <script src="{{ .base_path }}assets/qs/qs.min.js"></script>
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script> <script>
const basePath = '{{ .base_path }}'; const basePath = '{{ .base_path }}';

View file

@ -111,20 +111,12 @@
<template v-if="client.expiryTime !=0 && client.reset >0"> <template v-if="client.expiryTime !=0 && client.reset >0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }} <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
<span v-else>
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
</template>
</span>
</template> </template>
<table> <table>
<tr class="tr-table-box"> <tr class="tr-table-box">
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td> <td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
<td class="infinite-bar tr-table-bar"> <td class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</td> </td>
@ -136,18 +128,10 @@
<template v-else> <template v-else>
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }} <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
<span v-else>
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
</template>
</span>
</template> </template>
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag> <a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
</a-popover> </a-popover>
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag"> <a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
@ -232,20 +216,12 @@
</tr> </tr>
<tr> <tr>
<template v-if="client.expiryTime !=0 && client.reset >0"> <template v-if="client.expiryTime !=0 && client.reset >0">
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ remainedDays(client.expiryTime) ]] </td> <td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
<td width="120px" class="infinite-bar"> <td width="120px" class="infinite-bar">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }} <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
<span v-else>
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
</template>
</span>
</template> </template>
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" /> <a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
</a-popover> </a-popover>
@ -256,18 +232,10 @@
<td colspan="3" :style="{ textAlign: 'center' }"> <td colspan="3" :style="{ textAlign: 'center' }">
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }} <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
</span> <span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
<span v-else>
<template v-if="app.datepicker === 'gregorian'">
[[ DateUtil.formatMillis(client._expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
</template>
</span>
</template> </template>
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag> <a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
</a-popover> </a-popover>
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag"> <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
@ -289,12 +257,7 @@
</template> </template>
<template slot="createdAt" slot-scope="text, client, index"> <template slot="createdAt" slot-scope="text, client, index">
<template v-if="client.created_at"> <template v-if="client.created_at">
<template v-if="app.datepicker === 'gregorian'"> [[ IntlUtil.formatDate(client.created_at) ]]
[[ DateUtil.formatMillis(client.created_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
</template>
</template> </template>
<template v-else> <template v-else>
- -
@ -302,12 +265,7 @@
</template> </template>
<template slot="updatedAt" slot-scope="text, client, index"> <template slot="updatedAt" slot-scope="text, client, index">
<template v-if="client.updated_at"> <template v-if="client.updated_at">
<template v-if="app.datepicker === 'gregorian'"> [[ IntlUtil.formatDate(client.updated_at) ]]
[[ DateUtil.formatMillis(client.updated_at) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
</template>
</template> </template>
<template v-else> <template v-else>
- -

View file

@ -52,9 +52,7 @@
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> <br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0"> <span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong> <strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
<span v-if="datepicker == 'gregorian'">[[ <span>[[ IntlUtil.formatDate(dbInbound.lastTrafficResetTime) ]]</span>
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
<span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
</span> </span>
</template> </template>
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }} {{ i18n "pages.inbounds.periodicTrafficResetTitle" }}

View file

@ -12,10 +12,26 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Target'> <a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> Target <a-icon @click="randomizeRealityTarget()"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.target"></a-input> <a-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='SNI'> <a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template> SNI <a-icon @click="randomizeRealityTarget()"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input> <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Time Diff (ms)'> <a-form-item label='Max Time Diff (ms)'>

View file

@ -384,15 +384,12 @@
</template> </template>
<template slot="expiryTime" slot-scope="text, dbInbound"> <template slot="expiryTime" slot-scope="text, dbInbound">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" v-if="app.datepicker === 'gregorian'"> <template slot="content">
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]] [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
</template>
<template v-else slot="content">
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template> </template>
<a-tag :style="{ minWidth: '50px' }" <a-tag :style="{ minWidth: '50px' }"
:color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)"> :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ remainedDays(dbInbound._expiryTime) ]] [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime) ]]
</a-tag> </a-tag>
</a-popover> </a-popover>
<a-tag v-else color="purple" class="infinite-tag"> <a-tag v-else color="purple" class="infinite-tag">
@ -549,12 +546,7 @@
<td> <td>
<a-tag :style="{ minWidth: '50px', textAlign: 'center' }" <a-tag :style="{ minWidth: '50px', textAlign: 'center' }"
v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'"> v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
<template v-if="app.datepicker === 'gregorian'"> [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
</template>
</a-tag> </a-tag>
<a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag"> <a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
@ -602,6 +594,7 @@
{{template "page/body_scripts" .}} {{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}} {{template "component/aSidebar" .}}
@ -1406,13 +1399,6 @@
if (remainedSeconds >= resetSeconds) return 0; if (remainedSeconds >= resetSeconds) return 0;
return 100 * (1 - (remainedSeconds / resetSeconds)); return 100 * (1 - (remainedSeconds / resetSeconds));
}, },
remainedDays(expTime) {
if (expTime == 0) return null;
if (expTime < 0) return TimeFormatter.formatSecond(expTime / -1000);
now = new Date().getTime();
if (expTime < now) return '{{ i18n "depleted" }}';
return TimeFormatter.formatSecond((expTime - now) / 1000);
},
statsExpColor(dbInbound, email) { statsExpColor(dbInbound, email) {
if (email.length == 0) return '#7a316f'; if (email.length == 0) return '#7a316f';
clientStats = dbInbound.clientStats.find(stats => stats.email === email); clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@ -1457,10 +1443,7 @@
formatLastOnline(email) { formatLastOnline(email) {
const ts = this.getLastOnline(email) const ts = this.getLastOnline(email)
if (!ts) return '-' if (!ts) return '-'
if (this.datepicker === 'gregorian') { return IntlUtil.formatDate(ts)
return DateUtil.formatMillis(ts)
}
return DateUtil.convertToJalalian(moment(ts))
}, },
isRemovable(dbInboundId) { isRemovable(dbInboundId) {
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1; return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;

View file

@ -846,7 +846,7 @@
formattedLogs += ` formattedLogs += `
<tr ${outboundColor}> <tr ${outboundColor}>
<td><b>${new Date(log.DateTime).toLocaleString()}</b></td> <td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
<td>${log.FromAddress}</td> <td>${log.FromAddress}</td>
<td>${log.ToAddress}</td> <td>${log.ToAddress}</td>
<td>${log.Inbound}</td> <td>${log.Inbound}</td>

View file

@ -199,12 +199,7 @@
<td>{{ i18n "pages.inbounds.createdAt" }}</td> <td>{{ i18n "pages.inbounds.createdAt" }}</td>
<td> <td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at"> <template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
<template v-if="app.datepicker === 'gregorian'"> <a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.created_at) ]]</a-tag>
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
</template>
</template> </template>
<template v-else> <template v-else>
<a-tag>-</a-tag> <a-tag>-</a-tag>
@ -215,12 +210,7 @@
<td>{{ i18n "pages.inbounds.updatedAt" }}</td> <td>{{ i18n "pages.inbounds.updatedAt" }}</td>
<td> <td>
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at"> <template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
<template v-if="app.datepicker === 'gregorian'"> <a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.updated_at) ]]</a-tag>
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
</template>
<template v-else>
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
</template>
</template> </template>
<template v-else> <template v-else>
<a-tag>-</a-tag> <a-tag>-</a-tag>
@ -282,12 +272,7 @@
<td> <td>
<template v-if="infoModal.clientSettings.expiryTime > 0"> <template v-if="infoModal.clientSettings.expiryTime > 0">
<a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)"> <a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
<template v-if="app.datepicker === 'gregorian'"> [[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime) ]]
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
</template>
<template v-else>
[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.expiryTime)) ]]
</template>
</a-tag> </a-tag>
</template> </template>
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }} <a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}

View file

@ -158,6 +158,13 @@
this.inbound.stream.reality.mldsa65Seed = ''; this.inbound.stream.reality.mldsa65Seed = '';
this.inbound.stream.reality.settings.mldsa65Verify = ''; this.inbound.stream.reality.settings.mldsa65Verify = '';
}, },
randomizeRealityTarget() {
if (typeof getRandomRealityTarget !== 'undefined') {
const randomTarget = getRandomRealityTarget();
this.inbound.stream.reality.target = randomTarget.target;
this.inbound.stream.reality.serverNames = randomTarget.sni;
}
},
async getNewEchCert() { async getNewEchCert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni }); const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });

View file

@ -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 != "";

View file

@ -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>

View file

@ -4,7 +4,6 @@
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script> <script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
@ -141,17 +140,7 @@
<a-descriptions-item <a-descriptions-item
label='{{ i18n "lastOnline" }}'> label='{{ i18n "lastOnline" }}'>
<template v-if="app.lastOnlineMs > 0"> <template v-if="app.lastOnlineMs > 0">
<template [[ IntlUtil.formatDate(app.lastOnlineMs) ]]
v-if="app.datepicker === 'gregorian'">
[[
DateUtil.formatMillis(app.lastOnlineMs)
]]
</template>
<template v-else>
[[
DateUtil.convertToJalalian(moment(app.lastOnlineMs))
]]
</template>
</template> </template>
<template v-else> <template v-else>
<span>-</span> <span>-</span>
@ -163,17 +152,7 @@
{{ i18n "subscription.noExpiry" }} {{ i18n "subscription.noExpiry" }}
</template> </template>
<template v-else> <template v-else>
<template [[ IntlUtil.formatDate(app.expireMs) ]]
v-if="app.datepicker === 'gregorian'">
[[
DateUtil.formatMillis(app.expireMs)
]]
</template>
<template v-else>
[[
DateUtil.convertToJalalian(moment(app.expireMs))
]]
</template>
</template> </template>
</a-descriptions-item> </a-descriptions-item>
</a-descriptions> </a-descriptions>

View file

@ -27,8 +27,7 @@ type LdapSyncJob struct {
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 {
logger.Warning("Failed to get string setting:", err) panic(err)
return ""
} }
return v return v
} }
@ -36,8 +35,7 @@ func mustGetString(fn func() (string, error)) string {
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 {
logger.Warning("Failed to get int setting:", err) panic(err)
return 0
} }
return v return v
} }
@ -45,8 +43,7 @@ func mustGetInt(fn func() (int, error)) int {
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 {
logger.Warning("Failed to get bool setting:", err) panic(err)
return false
} }
return v return v
} }
@ -60,46 +57,26 @@ func mustGetStringOr(fn func() (string, error), fallback string) string {
} }
func NewLdapSyncJob() *LdapSyncJob { func NewLdapSyncJob() *LdapSyncJob {
return &LdapSyncJob{ return new(LdapSyncJob)
settingService: service.SettingService{},
inboundService: service.InboundService{},
xrayService: service.XrayService{},
}
} }
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 { if err != nil || !enabled {
logger.Warning("Failed to get LDAP enable setting:", err) logger.Warning("LDAP disabled or failed to fetch flag")
return
}
if !enabled {
logger.Debug("LDAP sync is disabled")
return return
} }
// --- LDAP fetch --- // --- LDAP fetch ---
host := mustGetString(j.settingService.GetLdapHost)
if host == "" {
logger.Warning("LDAP host is not configured")
return
}
baseDN := mustGetString(j.settingService.GetLdapBaseDN)
if baseDN == "" {
logger.Warning("LDAP base DN is not configured")
return
}
cfg := ldaputil.Config{ cfg := ldaputil.Config{
Host: host, 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: baseDN, 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)),
@ -109,27 +86,16 @@ func (j *LdapSyncJob) Run() {
flags, err := ldaputil.FetchVlessFlags(cfg) flags, err := ldaputil.FetchVlessFlags(cfg)
if err != nil { if err != nil {
logger.Warningf("LDAP fetch failed: %v", 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 ---
inboundTagsStr := mustGetString(j.settingService.GetLdapInboundTags) inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
if inboundTagsStr == "" {
logger.Debug("LDAP inbound tags not configured, skipping sync")
return
}
inboundTags := splitCsv(inboundTagsStr)
if len(inboundTags) == 0 {
logger.Debug("No LDAP inbound tags configured, skipping sync")
return
}
inbounds, err := j.inboundService.GetAllInbounds() inbounds, err := j.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warningf("Failed to get inbounds: %v", err) logger.Warning("Failed to get inbounds:", err)
return return
} }
@ -191,7 +157,6 @@ func (j *LdapSyncJob) Run() {
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 {

View file

@ -794,17 +794,17 @@ func (s *ServerService) GetXrayLogs(
for i, part := range parts { for i, part := range parts {
if i == 0 { if i == 0 {
dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1]) dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
if err != nil { if err != nil {
continue continue
} }
entry.DateTime = dateTime entry.DateTime = dateTime.UTC()
} }
if part == "from" { if part == "from" {
entry.FromAddress = parts[i+1] entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
} else if part == "accepted" { } else if part == "accepted" {
entry.ToAddress = parts[i+1] entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
} else if strings.HasPrefix(part, "[") { } else if strings.HasPrefix(part, "[") {
entry.Inbound = part[1:] entry.Inbound = part[1:]
} else if strings.HasSuffix(part, "]") { } else if strings.HasSuffix(part, "]") {
@ -941,13 +941,26 @@ func (s *ServerService) ImportDB(file multipart.File) error {
return common.NewErrorf("Error saving db: %v", err) return common.NewErrorf("Error saving db: %v", err)
} }
// Check if we can init the db or not // Close temp file before opening via sqlite
if err = database.InitDB(tempPath); err != nil { if err = tempFile.Close(); err != nil {
return common.NewErrorf("Error checking db: %v", err) return common.NewErrorf("Error closing temporary db file: %v", err)
}
tempFile = nil
// Validate integrity (no migrations / side effects)
if err = database.ValidateSQLiteDB(tempPath); err != nil {
return common.NewErrorf("Invalid or corrupt db file: %v", err)
} }
// Stop Xray // Stop Xray (ignore error but log)
s.StopXrayService() if errStop := s.StopXrayService(); errStop != nil {
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
}
// Close existing DB to release file locks (especially on Windows)
if errClose := database.CloseDB(); errClose != nil {
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
}
// Backup the current database for fallback // Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath()) fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
@ -982,7 +995,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
return common.NewErrorf("Error moving db file: %v", err) return common.NewErrorf("Error moving db file: %v", err)
} }
// Migrate DB // Open & migrate new DB
if err = database.InitDB(config.GetDBPath()); err != nil { if err = database.InitDB(config.GetDBPath()); err != nil {
if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil { if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename) return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)

View file

@ -571,10 +571,18 @@ func (s *SettingService) GetSubDomain() (string, error) {
return s.getString("subDomain") return s.getString("subDomain")
} }
func (s *SettingService) SetSubCertFile(subCertFile string) error {
return s.setString("subCertFile", subCertFile)
}
func (s *SettingService) GetSubCertFile() (string, error) { func (s *SettingService) GetSubCertFile() (string, error) {
return s.getString("subCertFile") return s.getString("subCertFile")
} }
func (s *SettingService) SetSubKeyFile(subKeyFile string) error {
return s.setString("subKeyFile", subKeyFile)
}
func (s *SettingService) GetSubKeyFile() (string, error) { func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile") return s.getString("subKeyFile")
} }

View file

@ -38,7 +38,15 @@ import (
) )
var ( var (
bot *telego.Bot bot *telego.Bot
// botCancel stores the function to cancel the context, stopping Long Polling gracefully.
botCancel context.CancelFunc
// tgBotMutex protects concurrent access to botCancel variable
tgBotMutex sync.Mutex
// botWG waits for the OnReceive Long Polling goroutine to finish.
botWG sync.WaitGroup
botHandler *th.BotHandler botHandler *th.BotHandler
adminIds []int64 adminIds []int64
isRunning bool isRunning bool
@ -202,7 +210,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
// Parse admin IDs from comma-separated string // Parse admin IDs from comma-separated string
if tgBotID != "" { if tgBotID != "" {
for _, adminID := range strings.Split(tgBotID, ",") { for _, adminID := range strings.Split(tgBotID, ",") {
id, err := strconv.Atoi(adminID) id, err := strconv.ParseInt(adminID, 10, 64)
if err != nil { if err != nil {
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
return err return err
@ -306,8 +314,13 @@ func (t *Tgbot) SetHostname() {
hostname = host hostname = host
} }
// Stop stops the Telegram bot and cleans up resources. // Stop safely stops the Telegram bot's Long Polling operation.
// This method now calls the global StopBot function and cleans up other resources.
func (t *Tgbot) Stop() { func (t *Tgbot) Stop() {
// Call the global StopBot function to gracefully shut down Long Polling
StopBot()
// Stop the bot handler (in case the goroutine hasn't exited yet)
if botHandler != nil { if botHandler != nil {
botHandler.Stop() botHandler.Stop()
} }
@ -316,6 +329,27 @@ func (t *Tgbot) Stop() {
adminIds = nil adminIds = nil
} }
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
// This is the global function called from main.go's signal handler and t.Stop().
func StopBot() {
tgBotMutex.Lock()
defer tgBotMutex.Unlock()
if botCancel != nil {
logger.Info("Sending cancellation signal to Telegram bot...")
// Calling botCancel() cancels the context passed to UpdatesViaLongPolling,
// which stops the Long Polling operation and closes the updates channel,
// allowing the th.Start() goroutine to exit cleanly.
botCancel()
botCancel = nil
// Giving the goroutine a small delay to exit cleanly.
botWG.Wait()
logger.Info("Telegram bot successfully stopped.")
}
}
// encodeQuery encodes the query string if it's longer than 64 characters. // encodeQuery encodes the query string if it's longer than 64 characters.
func (t *Tgbot) encodeQuery(query string) string { func (t *Tgbot) encodeQuery(query string) string {
// NOTE: we only need to hash for more than 64 chars // NOTE: we only need to hash for more than 64 chars
@ -345,188 +379,207 @@ func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 30, // Increased timeout to reduce API calls Timeout: 30, // Increased timeout to reduce API calls
} }
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
tgBotMutex.Lock()
updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params) // Create a context with cancellation and store the cancel function.
var ctx context.Context
botHandler, _ = th.NewBotHandler(bot, updates) // Check if botCancel is already set (to prevent race condition overwrite and goroutine leak)
if botCancel == nil {
ctx, botCancel = context.WithCancel(context.Background())
} else {
// If botCancel is already set, use a non-cancellable context for this redundant call.
// This prevents overwriting the active botCancel and causing a goroutine leak from the previous call.
logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.")
ctx = context.Background() // <<< ИЗМЕНЕНИЕ
}
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { tgBotMutex.Unlock()
delete(userStates, message.Chat.ID)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
return nil
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { // Get updates channel using the context.
// Use goroutine with worker pool for concurrent command processing updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
go func() { botWG.Go(func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
botHandler, _ = th.NewBotHandler(bot, updates)
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
}() return nil
return nil }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
}, th.AnyCommand())
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent callback processing // Use goroutine with worker pool for concurrent command processing
go func() { go func() {
messageWorkerPool <- struct{}{} // Acquire worker messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, query.Message.GetChat().ID)
t.answerCallback(&query, checkAdmin(query.From.ID))
}()
return nil
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
if userState, exists := userStates[message.Chat.ID]; exists {
switch userState {
case "awaiting_id":
if client_Id == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return nil
}
client_Id = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Id) {
userStates[message.Chat.ID] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_tr":
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_TrPassword) {
userStates[message.Chat.ID] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_sh":
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_ShPassword) {
userStates[message.Chat.ID] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_email":
if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Email = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Email) {
userStates[message.Chat.ID] = "awaiting_email"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_comment":
if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Comment = strings.TrimSpace(message.Text)
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) }()
t.addClient(message.Chat.ID, message_text) return nil
} }, th.AnyCommand())
} else { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
if message.UsersShared != nil { // Use goroutine with worker pool for concurrent callback processing
if checkAdmin(message.From.ID) { go func() {
for _, sharedUser := range message.UsersShared.Users { messageWorkerPool <- struct{}{} // Acquire worker
userID := sharedUser.UserID defer func() { <-messageWorkerPool }() // Release worker
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
if needRestart { delete(userStates, query.Message.GetChat().ID)
t.xrayService.SetToNeedRestart() t.answerCallback(&query, checkAdmin(query.From.ID))
} }()
output := "" return nil
if err != nil { }, th.AnyCallbackQueryWithMessage())
output += t.I18nBot("tgbot.messages.selectUserFailed")
} else { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
output += t.I18nBot("tgbot.messages.userSaved") if userState, exists := userStates[message.Chat.ID]; exists {
} switch userState {
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) case "awaiting_id":
if client_Id == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
return nil
}
client_Id = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Id) {
userStates[message.Chat.ID] = "awaiting_id"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_tr":
if client_TrPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_TrPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_TrPassword) {
userStates[message.Chat.ID] = "awaiting_password_tr"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_password_sh":
if client_ShPassword == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_ShPassword = strings.TrimSpace(message.Text)
if t.isSingleWord(client_ShPassword) {
userStates[message.Chat.ID] = "awaiting_password_sh"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_email":
if client_Email == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Email = strings.TrimSpace(message.Text)
if t.isSingleWord(client_Email) {
userStates[message.Chat.ID] = "awaiting_email"
cancel_btn_markup := tu.InlineKeyboard(
tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
),
)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
} else {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
case "awaiting_comment":
if client_Comment == strings.TrimSpace(message.Text) {
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
return nil
}
client_Comment = strings.TrimSpace(message.Text)
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text)
}
} else {
if message.UsersShared != nil {
if checkAdmin(message.From.ID) {
for _, sharedUser := range message.UsersShared.Users {
userID := sharedUser.UserID
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
if needRestart {
t.xrayService.SetToNeedRestart()
}
output := ""
if err != nil {
output += t.I18nBot("tgbot.messages.selectUserFailed")
} else {
output += t.I18nBot("tgbot.messages.userSaved")
}
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
}
} else {
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
} }
} else {
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
} }
} }
} return nil
return nil }, th.AnyMessage())
}, th.AnyMessage())
botHandler.Start() botHandler.Start()
})
} }
// answerCommand processes incoming command messages from Telegram users. // answerCommand processes incoming command messages from Telegram users.
@ -852,8 +905,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "add_client_limit_traffic_c": case "add_client_limit_traffic_c":
limitTraffic, _ := strconv.Atoi(dataArray[1]) limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64)
client_TotalGB = int64(limitTraffic) * 1024 * 1024 * 1024 client_TotalGB = limitTraffic * 1024 * 1024 * 1024
messageId := callbackQuery.Message.GetMessageID() messageId := callbackQuery.Message.GetMessageID()
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID) inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
if err != nil { if err != nil {
@ -957,7 +1010,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
case "reset_exp_c": case "reset_exp_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2]) days, err := strconv.ParseInt(dataArray[2], 10, 64)
if err == nil { if err == nil {
var date int64 var date int64
if days > 0 { if days > 0 {
@ -1062,7 +1115,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
case "add_client_reset_exp_c": case "add_client_reset_exp_c":
client_ExpiryTime = 0 client_ExpiryTime = 0
days, _ := strconv.Atoi(dataArray[1]) days, _ := strconv.ParseInt(dataArray[1], 10, 64)
var date int64 var date int64
if client_ExpiryTime > 0 { if client_ExpiryTime > 0 {
if client_ExpiryTime-time.Now().Unix()*1000 < 0 { if client_ExpiryTime-time.Now().Unix()*1000 < 0 {
@ -2899,10 +2952,12 @@ func (t *Tgbot) clientInfoMsg(
} }
status := t.I18nBot("tgbot.offline") status := t.I18nBot("tgbot.offline")
isOnline := false
if p.IsRunning() { if p.IsRunning() {
for _, online := range p.GetOnlineClients() { for _, online := range p.GetOnlineClients() {
if online == traffic.Email { if online == traffic.Email {
status = t.I18nBot("tgbot.online") status = t.I18nBot("tgbot.online")
isOnline = true
break break
} }
} }
@ -2915,6 +2970,9 @@ func (t *Tgbot) clientInfoMsg(
} }
if printOnline { if printOnline {
output += t.I18nBot("tgbot.messages.online", "Status=="+status) output += t.I18nBot("tgbot.messages.online", "Status=="+status)
if !isOnline && traffic.LastOnline > 0 {
output += t.I18nBot("tgbot.messages.lastOnline", "Time=="+time.UnixMilli(traffic.LastOnline).Format("2006-01-02 15:04:05"))
}
} }
if printActive { if printActive {
output += t.I18nBot("tgbot.messages.active", "Enable=="+active) output += t.I18nBot("tgbot.messages.active", "Enable=="+active)

View file

@ -106,7 +106,7 @@
"invalidFormData" = "تنسيق البيانات المدخلة مش صحيح." "invalidFormData" = "تنسيق البيانات المدخلة مش صحيح."
"emptyUsername" = "اسم المستخدم مطلوب" "emptyUsername" = "اسم المستخدم مطلوب"
"emptyPassword" = "الباسورد مطلوب" "emptyPassword" = "الباسورد مطلوب"
"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح." "wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."
"successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح." "successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح."
[pages.index] [pages.index]
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "بيانات الأدمن" "admin" = "بيانات الأدمن"
"twoFactor" = "المصادقة الثنائية" "twoFactor" = "المصادقة الثنائية"
"twoFactorEnable" = "تفعيل المصادقة الثنائية" "twoFactorEnable" = "تفعيل المصادقة الثنائية"
"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان." "twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."
"twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية" "twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية"
"twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية" "twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية"
"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:" "twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:"
@ -663,6 +663,7 @@
"active" = "💡 مفعل: {{ .Enable }}\r\n" "active" = "💡 مفعل: {{ .Enable }}\r\n"
"enabled" = "🚨 مفعل: {{ .Enable }}\r\n" "enabled" = "🚨 مفعل: {{ .Enable }}\r\n"
"online" = "🌐 حالة الاتصال: {{ .Status }}\r\n" "online" = "🌐 حالة الاتصال: {{ .Status }}\r\n"
"lastOnline" = "🔙 آخر متصل: {{ .Time }}\r\n"
"email" = "📧 الإيميل: {{ .Email }}\r\n" "email" = "📧 الإيميل: {{ .Email }}\r\n"
"upload" = "🔼 رفع: ↑{{ .Upload }}\r\n" "upload" = "🔼 رفع: ↑{{ .Upload }}\r\n"
"download" = "🔽 تنزيل: ↓{{ .Download }}\r\n" "download" = "🔽 تنزيل: ↓{{ .Download }}\r\n"

View file

@ -663,6 +663,7 @@
"active" = "💡 Active: {{ .Enable }}\r\n" "active" = "💡 Active: {{ .Enable }}\r\n"
"enabled" = "🚨 Enabled: {{ .Enable }}\r\n" "enabled" = "🚨 Enabled: {{ .Enable }}\r\n"
"online" = "🌐 Connection status: {{ .Status }}\r\n" "online" = "🌐 Connection status: {{ .Status }}\r\n"
"lastOnline" = "🔙 Last online: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" "inbound_client_data_id" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!"
"inbound_client_data_pass" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 Password: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!" "inbound_client_data_pass" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 Password: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!"
"cancel" = "❌ Process Canceled! \n\nYou can /start again anytime. 🔄" "cancel" = "❌ Process Canceled! \n\nYou can /start again anytime. 🔄"
"error_add_client" = "⚠️ Error:\n\n {{ .error }}" "error_add_client" = "⚠️ Error:\n\n {{ .error }}"
"using_default_value" = "Okay, I'll stick with the default value. 😊" "using_default_value" = "Okay, I'll stick with the default value. 😊"
"incorrect_input" ="Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫" "incorrect_input" = "Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫"
"AreYouSure" = "Are you sure? 🤔" "AreYouSure" = "Are you sure? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠 Error: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠 Error: [ {{ .ErrorMessage }} ]"

View file

@ -663,6 +663,7 @@
"active" = "💡 Activo: {{ .Enable }}\r\n" "active" = "💡 Activo: {{ .Enable }}\r\n"
"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n" "enabled" = "🚨 Habilitado: {{ .Enable }}\r\n"
"online" = "🌐 Estado de conexión: {{ .Status }}\r\n" "online" = "🌐 Estado de conexión: {{ .Status }}\r\n"
"lastOnline" = "🔙 Última conexión: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n" "upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
"download" = "🔽 Bajada: ↓{{ .Download }}\r\n" "download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" "inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!" "inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
"cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄" "cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄"
"error_add_client" = "⚠️ Error:\n\n {{ .error }}" "error_add_client" = "⚠️ Error:\n\n {{ .error }}"
"using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊" "using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊"
"incorrect_input" ="Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫" "incorrect_input" = "Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫"
"AreYouSure" = "¿Estás seguro? 🤔" "AreYouSure" = "¿Estás seguro? 🤔"
"SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito" "SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito"
"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠 Error: [ {{ .ErrorMessage }} ]"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است" "invalidFormData" = "اطلاعات به‌درستی وارد نشده‌است"
"emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌" "emptyUsername" = "لطفا یک نام‌کاربری وارد کنید‌"
"emptyPassword" = "لطفا یک رمزعبور وارد کنید" "emptyPassword" = "لطفا یک رمزعبور وارد کنید"
"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است." "wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحله‌ای نامعتبر است."
"successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید." "successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید."
[pages.index] [pages.index]
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "اعتبارنامه‌های ادمین" "admin" = "اعتبارنامه‌های ادمین"
"twoFactor" = "احراز هویت دو مرحله‌ای" "twoFactor" = "احراز هویت دو مرحله‌ای"
"twoFactorEnable" = "فعال‌سازی 2FA" "twoFactorEnable" = "فعال‌سازی 2FA"
"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند." "twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم می‌کند."
"twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای" "twoFactorModalSetTitle" = "فعال‌سازی احراز هویت دو مرحله‌ای"
"twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای" "twoFactorModalDeleteTitle" = "غیرفعال‌سازی احراز هویت دو مرحله‌ای"
"twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:" "twoFactorModalSteps" = "برای راه‌اندازی احراز هویت دو مرحله‌ای، مراحل زیر را انجام دهید:"
@ -663,6 +663,7 @@
"active" = "💡 فعال: {{ .Enable }}\r\n" "active" = "💡 فعال: {{ .Enable }}\r\n"
"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n" "enabled" = "🚨 وضعیت: {{ .Enable }}\r\n"
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n" "online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
"lastOnline" = "🔙 آخرین فعالیت: {{ .Time }}\r\n"
"email" = "📧 ایمیل: {{ .Email }}\r\n" "email" = "📧 ایمیل: {{ .Email }}\r\n"
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n" "upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
"download" = "🔽 دانلود↓: {{ .Download }}\r\n" "download" = "🔽 دانلود↓: {{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" "inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!"
"inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!" "inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون می‌تونی مشتری را به ورودی اضافه کنی!"
"cancel" = "❌ فرآیند لغو شد! \n\nمیتوانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄" "cancel" = "❌ فرآیند لغو شد! \n\nمیتوانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄"
"error_add_client" = "⚠️ خطا:\n\n {{ .error }}" "error_add_client" = "⚠️ خطا:\n\n {{ .error }}"
"using_default_value" = "باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊" "using_default_value" = "باشه، از مقدار پیش‌فرض استفاده می‌کنم. 😊"
"incorrect_input" ="ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫" "incorrect_input" = "ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫"
"AreYouSure" = "مطمئنی؟ 🤔" "AreYouSure" = "مطمئنی؟ 🤔"
"SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز" "SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیت‌آمیز"
"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠 خطا: [ {{ .ErrorMessage }} ]"

View file

@ -106,12 +106,12 @@
"invalidFormData" = "Format data input tidak valid." "invalidFormData" = "Format data input tidak valid."
"emptyUsername" = "Nama Pengguna diperlukan" "emptyUsername" = "Nama Pengguna diperlukan"
"emptyPassword" = "Kata Sandi diperlukan" "emptyPassword" = "Kata Sandi diperlukan"
"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid." "wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."
"successLogin" = "Anda telah berhasil masuk ke akun Anda." "successLogin" = "Anda telah berhasil masuk ke akun Anda."
[pages.index] [pages.index]
"title" = "Ikhtisar" "title" = "Ikhtisar"
"cpu" = "CPU" "cpu" = "CPU"
"logicalProcessors" = "Prosesor logis" "logicalProcessors" = "Prosesor logis"
"frequency" = "Frekuensi" "frequency" = "Frekuensi"
"swap" = "Swap" "swap" = "Swap"
@ -663,6 +663,7 @@
"active" = "💡 Aktif: {{ .Enable }}\r\n" "active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n" "enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
"online" = "🌐 Status Koneksi: {{ .Status }}\r\n" "online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
"lastOnline" = "🔙 Terakhir online: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n" "upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
"download" = "🔽 Unduh: ↓{{ .Download }}\r\n" "download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" "inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
"inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!" "inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
"cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄" "cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄"
"error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}" "error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}"
"using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊" "using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊"
"incorrect_input" ="Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫" "incorrect_input" = "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫"
"AreYouSure" = "Apakah kamu yakin? 🤔" "AreYouSure" = "Apakah kamu yakin? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠 Kesalahan: [ {{ .ErrorMessage }} ]"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "データ形式エラー" "invalidFormData" = "データ形式エラー"
"emptyUsername" = "ユーザー名を入力してください" "emptyUsername" = "ユーザー名を入力してください"
"emptyPassword" = "パスワードを入力してください" "emptyPassword" = "パスワードを入力してください"
"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。" "wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"
"successLogin" = "アカウントに正常にログインしました。" "successLogin" = "アカウントに正常にログインしました。"
[pages.index] [pages.index]
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理者の資格情報" "admin" = "管理者の資格情報"
"twoFactor" = "二段階認証" "twoFactor" = "二段階認証"
"twoFactorEnable" = "2FAを有効化" "twoFactorEnable" = "2FAを有効化"
"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。" "twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"
"twoFactorModalSetTitle" = "二段階認証を有効にする" "twoFactorModalSetTitle" = "二段階認証を有効にする"
"twoFactorModalDeleteTitle" = "二段階認証を無効にする" "twoFactorModalDeleteTitle" = "二段階認証を無効にする"
"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:" "twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:"
@ -663,6 +663,7 @@
"active" = "💡 有効:{{ .Enable }}\r\n" "active" = "💡 有効:{{ .Enable }}\r\n"
"enabled" = "🚨 有効化済み:{{ .Enable }}\r\n" "enabled" = "🚨 有効化済み:{{ .Enable }}\r\n"
"online" = "🌐 接続ステータス:{{ .Status }}\r\n" "online" = "🌐 接続ステータス:{{ .Status }}\r\n"
"lastOnline" = "🔙 最終オンライン: {{ .Time }}\r\n"
"email" = "📧 メール:{{ .Email }}\r\n" "email" = "📧 メール:{{ .Email }}\r\n"
"upload" = "🔼 アップロード↑:{{ .Upload }}\r\n" "upload" = "🔼 アップロード↑:{{ .Upload }}\r\n"
"download" = "🔽 ダウンロード↓:{{ .Download }}\r\n" "download" = "🔽 ダウンロード↓:{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます" "inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます"
"inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます" "inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます"
"cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄" "cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄"
"error_add_client" = "⚠️ エラー:\n\n {{ .error }}" "error_add_client" = "⚠️ エラー:\n\n {{ .error }}"
"using_default_value" = "わかりました、デフォルト値を使用します。 😊" "using_default_value" = "わかりました、デフォルト値を使用します。 😊"
"incorrect_input" ="入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫" "incorrect_input" = "入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫"
"AreYouSure" = "本当にいいですか?🤔" "AreYouSure" = "本当にいいですか?🤔"
"SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功" "SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 エラー: [ {{ .ErrorMessage }} ]"

View file

@ -106,12 +106,12 @@
"invalidFormData" = "O formato dos dados de entrada é inválido." "invalidFormData" = "O formato dos dados de entrada é inválido."
"emptyUsername" = "Nome de usuário é obrigatório" "emptyUsername" = "Nome de usuário é obrigatório"
"emptyPassword" = "Senha é obrigatória" "emptyPassword" = "Senha é obrigatória"
"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido." "wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."
"successLogin" = "Você entrou na sua conta com sucesso." "successLogin" = "Você entrou na sua conta com sucesso."
[pages.index] [pages.index]
"title" = "Visão Geral" "title" = "Visão Geral"
"cpu" = "CPU" "cpu" = "CPU"
"logicalProcessors" = "Processadores lógicos" "logicalProcessors" = "Processadores lógicos"
"frequency" = "Frequência" "frequency" = "Frequência"
"swap" = "Swap" "swap" = "Swap"
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Credenciais de administrador" "admin" = "Credenciais de administrador"
"twoFactor" = "Autenticação de dois fatores" "twoFactor" = "Autenticação de dois fatores"
"twoFactorEnable" = "Ativar 2FA" "twoFactorEnable" = "Ativar 2FA"
"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança." "twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."
"twoFactorModalSetTitle" = "Ativar autenticação de dois fatores" "twoFactorModalSetTitle" = "Ativar autenticação de dois fatores"
"twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores" "twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores"
"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:" "twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:"
@ -663,6 +663,7 @@
"active" = "💡 Ativo: {{ .Enable }}\r\n" "active" = "💡 Ativo: {{ .Enable }}\r\n"
"enabled" = "🚨 Ativado: {{ .Enable }}\r\n" "enabled" = "🚨 Ativado: {{ .Enable }}\r\n"
"online" = "🌐 Status da conexão: {{ .Status }}\r\n" "online" = "🌐 Status da conexão: {{ .Status }}\r\n"
"lastOnline" = "🔙 Última vez online: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" "inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!" "inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
"cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄" "cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄"
"error_add_client" = "⚠️ Erro:\n\n {{ .error }}" "error_add_client" = "⚠️ Erro:\n\n {{ .error }}"
"using_default_value" = "Tudo bem, vou manter o valor padrão. 😊" "using_default_value" = "Tudo bem, vou manter o valor padrão. 😊"
"incorrect_input" ="Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫" "incorrect_input" = "Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫"
"AreYouSure" = "Você tem certeza? 🤔" "AreYouSure" = "Você tem certeza? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠 Erro: [ {{ .ErrorMessage }} ]"

View file

@ -32,7 +32,7 @@
"copySuccess" = "Скопировано" "copySuccess" = "Скопировано"
"sure" = "Да" "sure" = "Да"
"encryption" = "Шифрование" "encryption" = "Шифрование"
"useIPv4ForHost" = "Использовать IPv4 для хоста" "useIPv4ForHost" = "Использовать IPv4 для подключения к хосту"
"transmission" = "Транспорт" "transmission" = "Транспорт"
"host" = "Хост" "host" = "Хост"
"path" = "Путь" "path" = "Путь"
@ -46,8 +46,8 @@
"online" = "Онлайн" "online" = "Онлайн"
"domainName" = "Домен" "domainName" = "Домен"
"monitor" = "Мониторинг IP" "monitor" = "Мониторинг IP"
"certificate" = "SSL сертификат" "certificate" = "SSL-сертификат"
"fail" = "Ошибка" "fail" = "Сбой"
"comment" = "Комментарий" "comment" = "Комментарий"
"success" = "Успешно" "success" = "Успешно"
"lastOnline" = "Был(а) в сети" "lastOnline" = "Был(а) в сети"
@ -55,17 +55,17 @@
"install" = "Установка" "install" = "Установка"
"clients" = "Клиенты" "clients" = "Клиенты"
"usage" = "Использование" "usage" = "Использование"
"twoFactorCode" = "Код" "twoFactorCode" = "Код 2FA"
"remained" = "Остаток" "remained" = "Остаток"
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности" "secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения" "secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата."
"secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту." "secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак."
"secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных." "secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных."
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт." "secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт."
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным." "secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI."
"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." "secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес." "secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
"emptyDnsDesc" = "Нет добавленных DNS-серверов." "emptyDnsDesc" = "Нет добавленных DNS-серверов."
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов." "emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
"emptyBalancersDesc" = "Нет добавленных балансировщиков." "emptyBalancersDesc" = "Нет добавленных балансировщиков."
@ -83,15 +83,15 @@
"individualLinks" = "Индивидуальные ссылки" "individualLinks" = "Индивидуальные ссылки"
"active" = "Активна" "active" = "Активна"
"inactive" = "Неактивна" "inactive" = "Неактивна"
"unlimited" = "Безлимит" "unlimited" = "Неограниченно"
"noExpiry" = "Без срока" "noExpiry" = "Бессрочно"
[menu] [menu]
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темная" "dark" = "Темная"
"ultraDark" = "Очень темная" "ultraDark" = "Очень темная"
"dashboard" = "Дашборд" "dashboard" = "Дашборд"
"inbounds" = "Инбаунды" "inbounds" = "Подключения"
"settings" = "Настройки" "settings" = "Настройки"
"xray" = "Настройки Xray" "xray" = "Настройки Xray"
"logout" = "Выход" "logout" = "Выход"
@ -99,7 +99,7 @@
[pages.login] [pages.login]
"hello" = "Привет!" "hello" = "Привет!"
"title" = "Приветствие!" "title" = "Добро пожаловать!"
"loginAgain" = "Сессия истекла. Войдите в систему снова" "loginAgain" = "Сессия истекла. Войдите в систему снова"
[pages.login.toasts] [pages.login.toasts]
@ -107,7 +107,7 @@
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверные данные учетной записи." "wrongUsernameOrPassword" = "Неверные данные учетной записи."
"successLogin" = "Вы успешно вошли в аккаунт" "successLogin" = "Вход выполнен успешно"
[pages.index] [pages.index]
"title" = "Дашборд" "title" = "Дашборд"
@ -122,7 +122,7 @@
"stopXray" = "Остановить" "stopXray" = "Остановить"
"restartXray" = "Перезапустить" "restartXray" = "Перезапустить"
"xraySwitch" = "Выбор версии" "xraySwitch" = "Выбор версии"
"xraySwitchClick" = "Выберите желаемую версию" "xraySwitchClick" = "Выберите нужную версию"
"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
"xrayStatusUnknown" = "Неизвестно" "xrayStatusUnknown" = "Неизвестно"
"xrayStatusRunning" = "Запущен" "xrayStatusRunning" = "Запущен"
@ -134,7 +134,7 @@
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionCount" = "Количество соединений" "connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера" "ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Переключить видимость IP-адресов сервера" "toggleIpVisibility" = "Скрыть или показать IP-адреса сервера"
"overallSpeed" = "Общая скорость передачи трафика" "overallSpeed" = "Общая скорость передачи трафика"
"upload" = "Отправка" "upload" = "Отправка"
"download" = "Загрузка" "download" = "Загрузка"
@ -168,10 +168,10 @@
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Общий трафик" "allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время" "allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Инбаунды" "title" = "Подключения"
"totalDownUp" = "Объем отправленного/полученного трафика" "totalDownUp" = "Отправлено/получено"
"totalUsage" = "Всего трафика" "totalUsage" = "Всего трафика"
"inboundCount" = "Всего инбаундов" "inboundCount" = "Всего подключений"
"operate" = "Меню" "operate" = "Меню"
"enable" = "Включить" "enable" = "Включить"
"remark" = "Примечание" "remark" = "Примечание"
@ -185,13 +185,13 @@
"createdAt" = "Создано" "createdAt" = "Создано"
"updatedAt" = "Обновлено" "updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика" "resetTraffic" = "Сброс трафика"
"addInbound" = "Создать инбаунд" "addInbound" = "Создать подключение"
"generalActions" = "Общие действия" "generalActions" = "Общие действия"
"autoRefresh" = "Автообновление" "autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал" "autoRefreshInterval" = "Интервал"
"modifyInbound" = "Изменить инбаунд" "modifyInbound" = "Изменить подключение"
"deleteInbound" = "Удалить инбаунд" "deleteInbound" = "Удалить подключение"
"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?" "deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
"deleteClient" = "Удалить клиента" "deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
@ -214,11 +214,11 @@
"export" = "Экспорт ссылок" "export" = "Экспорт ссылок"
"clone" = "Клонировать" "clone" = "Клонировать"
"cloneInbound" = "Клонировать" "cloneInbound" = "Клонировать"
"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания" "cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано" "cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сброс трафика всех инбаундов" "resetAllTraffic" = "Сброс трафика всех подключений"
"resetAllTrafficTitle" = "Сброс трафика всех инбаундов" "resetAllTrafficTitle" = "Сброс трафика всех подключений"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?" "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
"resetInboundClientTraffics" = "Сброс трафика клиента" "resetInboundClientTraffics" = "Сброс трафика клиента"
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов" "resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
@ -231,7 +231,7 @@
"email" = "Email" "email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email" "emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "Лимит по количеству IP" "IPLimit" = "Лимит по количеству IP"
"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 отключить)" "IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 отключить)"
"IPLimitlog" = "Лог IP-адресов" "IPLimitlog" = "Лог IP-адресов"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)" "IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
"IPLimitlogclear" = "Очистить лог" "IPLimitlogclear" = "Очистить лог"
@ -240,19 +240,19 @@
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
"info" = "Информация" "info" = "Информация"
"same" = "Тот же" "same" = "Тот же"
"inboundData" = "Данные инбаундов" "inboundData" = "Данные подключений"
"exportInbound" = "Экспорт инбаундов" "exportInbound" = "Экспорт подключений"
"import" = "Импортировать" "import" = "Импортировать"
"importInbound" = "Импорт инбаундов" "importInbound" = "Импорт подключений"
"periodicTrafficResetTitle" = "Сброс трафика" "periodicTrafficResetTitle" = "Сброс трафика"
"periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы" "periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы"
"lastReset" = "Последний сброс" "lastReset" = "Последний сброс"
[pages.client] [pages.client]
"add" = "Создать клиента" "add" = "Добавить клиента"
"edit" = "Редактировать клиента" "edit" = "Редактировать клиента"
"submitAdd" = "Добавить" "submitAdd" = "Добавить"
"submitEdit" = "Сохранить" "submitEdit" = "Сохранить изменения"
"clientCount" = "Количество клиентов" "clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько" "bulk" = "Добавить несколько"
"method" = "Метод" "method" = "Метод"
@ -276,13 +276,13 @@
"obtain" = "Получить" "obtain" = "Получить"
"updateSuccess" = "Обновление прошло успешно" "updateSuccess" = "Обновление прошло успешно"
"logCleanSuccess" = "Лог был очищен" "logCleanSuccess" = "Лог был очищен"
"inboundsUpdateSuccess" = "Инбаунды успешно обновлены" "inboundsUpdateSuccess" = "Подключения успешно обновлены"
"inboundUpdateSuccess" = "Инбаунд успешно обновлено" "inboundUpdateSuccess" = "Подключение успешно обновлено"
"inboundCreateSuccess" = "Инбаунд успешно создано" "inboundCreateSuccess" = "Подключение успешно создано"
"inboundDeleteSuccess" = "Инбаунд успешно удалено" "inboundDeleteSuccess" = "Подключение успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)" "inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент инбаунда удалён" "inboundClientDeleteSuccess" = "Клиент подключения удалён"
"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён" "inboundClientUpdateSuccess" = "Клиент подключения обновлён"
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
"resetAllTrafficSuccess" = "Весь трафик сброшен" "resetAllTrafficSuccess" = "Весь трафик сброшен"
@ -310,7 +310,7 @@
[pages.settings] [pages.settings]
"title" = "Настройки" "title" = "Настройки"
"save" = "Сохранить" "save" = "Сохранить"
"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу." "infoDesc" = "Сохраните изменения и перезапустите панель для их применения."
"restartPanel" = "Перезапуск панели" "restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
"restartPanelSuccess" = "Панель успешно перезапущена" "restartPanelSuccess" = "Панель успешно перезапущена"
@ -318,11 +318,11 @@
"resetDefaultConfig" = "Восстановить настройки по умолчанию" "resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Панель" "panelSettings" = "Панель"
"securitySettings" = "Учетная запись" "securitySettings" = "Учетная запись"
"TGBotSettings" = "Telegram" "TGBotSettings" = "Telegram-Бот"
"panelListeningIP" = "IP-адрес для управления панелью" "panelListeningIP" = "IP-адрес для управления панелью"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен панели" "panelListeningDomain" = "Домен панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов" "panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP."
"panelPort" = "Порт панели" "panelPort" = "Порт панели"
"panelPortDesc" = "Порт, на котором работает панель" "panelPortDesc" = "Порт, на котором работает панель"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@ -332,11 +332,11 @@
"panelUrlPath" = "Корневой путь URL адреса панели" "panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц" "pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить" "pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения" "remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Выбор даты" "datepicker" = "Тип календаря"
"datepickerPlaceholder" = "Выберите дату" "datepickerPlaceholder" = "Выберите дату"
"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время" "datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем."
"sampleRemark" = "Пример примечания" "sampleRemark" = "Пример примечания"
"oldUsername" = "Текущий логин" "oldUsername" = "Текущий логин"
"currentPassword" = "Текущий пароль" "currentPassword" = "Текущий пароль"
@ -346,7 +346,7 @@
"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота" "telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
"telegramToken" = "Токен Telegram бота" "telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramProxy" = "Прокси Socks5" "telegramProxy" = "Прокси-сервер Socks5"
"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству." "telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
"telegramAPIServer" = "API-сервер Telegram" "telegramAPIServer" = "API-сервер Telegram"
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию." "telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
@ -451,11 +451,11 @@
"RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"Torrent" = "Заблокировать BitTorrent" "Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Инбаунды" "Inbounds" = "Входящие подключения"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
"Outbounds" = "Аутбаунды" "Outbounds" = "Исходящие подключения"
"Balancers" = "Балансировщик" "Balancers" = "Балансировщик"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера"
"Routings" = "Маршрутизация" "Routings" = "Маршрутизация"
"RoutingsDesc" = "Важен приоритет каждого правила!" "RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все" "completeTemplate" = "Все"
@ -486,8 +486,8 @@
"down" = "Опустить вниз" "down" = "Опустить вниз"
"source" = "Источник" "source" = "Источник"
"dest" = "Пункт назначения" "dest" = "Пункт назначения"
"inbound" = "Инбаунд" "inbound" = "Входящее подключение"
"outbound" = "Аутбаунд" "outbound" = "Исходящее подключение"
"balancer" = "Балансировщик" "balancer" = "Балансировщик"
"info" = "Информация" "info" = "Информация"
"add" = "Создать правило" "add" = "Создать правило"
@ -495,9 +495,9 @@
"useComma" = "Элементы, разделённые запятыми" "useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound] [pages.xray.outbound]
"addOutbound" = "Создать аутбаунд" "addOutbound" = "Создать исходящее подключение"
"addReverse" = "Создать реверс-прокси" "addReverse" = "Создать реверс-прокси"
"editOutbound" = "Изменить аутбаунд" "editOutbound" = "Изменить исходящее подключение"
"editReverse" = "Редактировать реверс-прокси" "editReverse" = "Редактировать реверс-прокси"
"tag" = "Тег" "tag" = "Тег"
"tagDesc" = "Уникальный тег" "tagDesc" = "Уникальный тег"
@ -511,7 +511,7 @@
"intercon" = "Соединение" "intercon" = "Соединение"
"settings" = "Настройки" "settings" = "Настройки"
"accountInfo" = "Информация об учетной записи" "accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус аутбаунда" "outboundStatus" = "Статус исходящего подключения"
"sendThrough" = "Отправить через" "sendThrough" = "Отправить через"
[pages.xray.balancer] [pages.xray.balancer]
@ -587,8 +587,8 @@
"modifyUser" = "Вы успешно изменили учетные данные администратора." "modifyUser" = "Вы успешно изменили учетные данные администратора."
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда" "getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения"
"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда" "resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Клавиатура закрыта." "keyboardClosed" = "❌ Клавиатура закрыта."
@ -596,7 +596,7 @@
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
"wentWrong" = "❌ Что-то пошло не так..." "wentWrong" = "❌ Что-то пошло не так..."
"noIpRecord" = "❗ Нет записей об IP-адресе." "noIpRecord" = "❗ Нет записей об IP-адресе."
"noInbounds" = "❗ У вас не настроено ни одного инбаунда." "noInbounds" = "❗ У вас не настроено ни одного входящего подключения."
"unlimited" = "♾ Безлимит" "unlimited" = "♾ Безлимит"
"add" = "Добавить" "add" = "Добавить"
"month" = "Месяц" "month" = "Месяц"
@ -606,7 +606,7 @@
"hours" = "Часов" "hours" = "Часов"
"minutes" = "Минуты" "minutes" = "Минуты"
"unknown" = "Неизвестно" "unknown" = "Неизвестно"
"inbounds" = "Инбаунды" "inbounds" = "Входящие подключения"
"clients" = "Клиенты" "clients" = "Клиенты"
"offline" = "🔴 Офлайн" "offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн" "online" = "🟢 Онлайн"
@ -620,7 +620,7 @@
"status" = "✅ Бот функционирует нормально." "status" = "✅ Бот функционирует нормально."
"usage" = "❗ Пожалуйста, укажите email для поиска." "usage" = "❗ Пожалуйста, укажите email для поиска."
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>" "getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>" "helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>" "helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
"restartUsage" = "\r\n\r\n<code>/restart</code>" "restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Ядро Xray успешно перезапущено." "restartSuccess" = "✅ Ядро Xray успешно перезапущено."
@ -656,13 +656,14 @@
"username" = "👤 Имя пользователя: {{ .Username }}\r\n" "username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"password" = "👤 Пароль: {{ .Password }}\r\n" "password" = "👤 Пароль: {{ .Password }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n" "time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n" "inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .Time }}\r\n" "expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
"active" = "💡 Активен: {{ .Enable }}\r\n" "active" = "💡 Активен: {{ .Enable }}\r\n"
"enabled" = "🚨 Активен: {{ .Enable }}\r\n" "enabled" = "🚨 Активен: {{ .Enable }}\r\n"
"online" = "🌐 Статус соединения: {{ .Status }}\r\n" "online" = "🌐 Статус соединения: {{ .Status }}\r\n"
"lastOnline" = "🔙 Был(а) в сети: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n" "upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n" "download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
@ -685,12 +686,12 @@
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." "comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий."
"inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" "inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" "inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" "error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌" "using_default_value" = "Используется значение по умолчанию👌"
"incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" "incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
"AreYouSure" = "Вы уверены? 🤔" "AreYouSure" = "Вы уверены? 🤔"
"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно" "SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно"
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
@ -707,7 +708,7 @@
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
"dbBackup" = "📂 Бэкап БД" "dbBackup" = "📂 Бэкап БД"
"serverUsage" = "💻 Состояние сервера" "serverUsage" = "💻 Состояние сервера"
"getInbounds" = "🔌 Инбаунды" "getInbounds" = "🔌 Входящие подключения"
"depleteSoon" = "⚠️ Скоро конец" "depleteSoon" = "⚠️ Скоро конец"
"clientUsage" = "Статистика клиента" "clientUsage" = "Статистика клиента"
"onlines" = "🟢 Онлайн" "onlines" = "🟢 Онлайн"
@ -731,7 +732,7 @@
"allClients" = "👥 Все клиенты" "allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент" "addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️" "submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенныи ✅" "submitEnable" = "Добавить включенным ✅"
"use_default" = "🏷️ Использовать по умолчанию" "use_default" = "🏷️ Использовать по умолчанию"
"change_id" = "⚙️🔑 ID" "change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Пароль" "change_password" = "⚙️🔑 Пароль"
@ -743,7 +744,7 @@
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Успешно!" "successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции." "errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить инбаунды." "getInboundsFailed" = "❌ Не удалось получить входящие подключения."
"getClientsFailed" = "❌ Не удалось получить клиентов." "getClientsFailed" = "❌ Не удалось получить клиентов."
"canceled" = "❌ {{ .Email }}: Операция отменена." "canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
@ -760,5 +761,5 @@
"enableSuccess" = "✅ {{ .Email }}: Включено успешно." "enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно." "disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>" "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}" "chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}"
"chooseInbound" = "Выберите инбаунд" "chooseInbound" = "Выберите входящее подключение"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "Girdi verisi formatı geçersiz." "invalidFormData" = "Girdi verisi formatı geçersiz."
"emptyUsername" = "Kullanıcı adı gerekli" "emptyUsername" = "Kullanıcı adı gerekli"
"emptyPassword" = "Şifre gerekli" "emptyPassword" = "Şifre gerekli"
"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu." "wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."
"successLogin" = "Hesabınıza başarıyla giriş yaptınız." "successLogin" = "Hesabınıza başarıyla giriş yaptınız."
[pages.index] [pages.index]
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Yönetici kimlik bilgileri" "admin" = "Yönetici kimlik bilgileri"
"twoFactor" = "İki adımlı doğrulama" "twoFactor" = "İki adımlı doğrulama"
"twoFactorEnable" = "2FA'yı Etkinleştir" "twoFactorEnable" = "2FA'yı Etkinleştir"
"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler." "twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."
"twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir" "twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir"
"twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak" "twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak"
"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:" "twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:"
@ -663,6 +663,7 @@
"active" = "💡 Aktif: {{ .Enable }}\r\n" "active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Etkin: {{ .Enable }}\r\n" "enabled" = "🚨 Etkin: {{ .Enable }}\r\n"
"online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n" "online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n"
"lastOnline" = "🔙 Son çevrimiçi: {{ .Time }}\r\n"
"email" = "📧 E-posta: {{ .Email }}\r\n" "email" = "📧 E-posta: {{ .Email }}\r\n"
"upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n" "upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n"
"download" = "🔽 İndirme: ↓{{ .Download }}\r\n" "download" = "🔽 İndirme: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" "inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
"inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!" "inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
"cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄" "cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄"
"error_add_client" = "⚠️ Hata:\n\n {{ .error }}" "error_add_client" = "⚠️ Hata:\n\n {{ .error }}"
"using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊" "using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊"
"incorrect_input" ="Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫" "incorrect_input" = "Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫"
"AreYouSure" = "Emin misin? 🤔" "AreYouSure" = "Emin misin? 🤔"
"SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı" "SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı"
"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠 Hata: [ {{ .ErrorMessage }} ]"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "Формат вхідних даних недійсний." "invalidFormData" = "Формат вхідних даних недійсний."
"emptyUsername" = "Потрібне ім'я користувача" "emptyUsername" = "Потрібне ім'я користувача"
"emptyPassword" = "Потрібен пароль" "emptyPassword" = "Потрібен пароль"
"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації." "wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."
"successLogin" = "Ви успішно увійшли до свого облікового запису." "successLogin" = "Ви успішно увійшли до свого облікового запису."
[pages.index] [pages.index]
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "Облікові дані адміністратора" "admin" = "Облікові дані адміністратора"
"twoFactor" = "Двофакторна аутентифікація" "twoFactor" = "Двофакторна аутентифікація"
"twoFactorEnable" = "Увімкнути 2FA" "twoFactorEnable" = "Увімкнути 2FA"
"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки." "twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."
"twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію" "twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію"
"twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію" "twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію"
"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:" "twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:"
@ -663,6 +663,7 @@
"active" = "💡 Активний: {{ .Enable }}\r\n" "active" = "💡 Активний: {{ .Enable }}\r\n"
"enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n" "enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n"
"online" = "🌐 Стан підключення: {{ .Status }}\r\n" "online" = "🌐 Стан підключення: {{ .Status }}\r\n"
"lastOnline" = "🔙 Був(ла) онлайн: {{ .Time }}\r\n"
"email" = "📧 Електронна пошта: {{ .Email }}\r\n" "email" = "📧 Електронна пошта: {{ .Email }}\r\n"
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n" "upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
"download" = "🔽 Download: ↓{{ .Download }}\r\n" "download" = "🔽 Download: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" "inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
"inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!" "inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
"cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄" "cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄"
"error_add_client" = "⚠️ Помилка:\n\n {{ .error }}" "error_add_client" = "⚠️ Помилка:\n\n {{ .error }}"
"using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊" "using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊"
"incorrect_input" ="Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫" "incorrect_input" = "Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫"
"AreYouSure" = "Ви впевнені? 🤔" "AreYouSure" = "Ви впевнені? 🤔"
"SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно" "SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно"
"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠 Помилка: [ {{ .ErrorMessage }} ]"

View file

@ -663,6 +663,7 @@
"active" = "💡 Đang hoạt động: {{ .Enable }}\r\n" "active" = "💡 Đang hoạt động: {{ .Enable }}\r\n"
"enabled" = "🚨 Đã bật: {{ .Enable }}\r\n" "enabled" = "🚨 Đã bật: {{ .Enable }}\r\n"
"online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n" "online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n"
"lastOnline" = "🔙 Lần online gần nhất: {{ .Time }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n" "email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n" "upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n" "download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" "inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
"inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!" "inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
"cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄" "cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄"
"error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}" "error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}"
"using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊" "using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊"
"incorrect_input" ="Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫" "incorrect_input" = "Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫"
"AreYouSure" = "Bạn có chắc không? 🤔" "AreYouSure" = "Bạn có chắc không? 🤔"
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công" "SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công"
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠 Lỗi: [ {{ .ErrorMessage }} ]"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "数据格式错误" "invalidFormData" = "数据格式错误"
"emptyUsername" = "请输入用户名" "emptyUsername" = "请输入用户名"
"emptyPassword" = "请输入密码" "emptyPassword" = "请输入密码"
"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。" "wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"
"successLogin" = "您已成功登录您的账户。" "successLogin" = "您已成功登录您的账户。"
[pages.index] [pages.index]
@ -242,7 +242,7 @@
"same" = "相同" "same" = "相同"
"inboundData" = "入站数据" "inboundData" = "入站数据"
"exportInbound" = "导出入站规则" "exportInbound" = "导出入站规则"
"import"="导入" "import" = "导入"
"importInbound" = "导入入站规则" "importInbound" = "导入入站规则"
"periodicTrafficResetTitle" = "流量重置" "periodicTrafficResetTitle" = "流量重置"
"periodicTrafficResetDesc" = "按指定间隔自动重置流量计数器" "periodicTrafficResetDesc" = "按指定间隔自动重置流量计数器"
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理员凭据" "admin" = "管理员凭据"
"twoFactor" = "双重验证" "twoFactor" = "双重验证"
"twoFactorEnable" = "启用2FA" "twoFactorEnable" = "启用2FA"
"twoFactorEnableDesc" = "增加额外的验证层以提高安全性。" "twoFactorEnableDesc" = "增加额外的验证层以提高安全性。"
"twoFactorModalSetTitle" = "启用双重认证" "twoFactorModalSetTitle" = "启用双重认证"
"twoFactorModalDeleteTitle" = "停用双重认证" "twoFactorModalDeleteTitle" = "停用双重认证"
"twoFactorModalSteps" = "要设定双重认证,请执行以下步骤:" "twoFactorModalSteps" = "要设定双重认证,请执行以下步骤:"
@ -663,6 +663,7 @@
"active" = "💡 激活:{{ .Enable }}\r\n" "active" = "💡 激活:{{ .Enable }}\r\n"
"enabled" = "🚨 已启用:{{ .Enable }}\r\n" "enabled" = "🚨 已启用:{{ .Enable }}\r\n"
"online" = "🌐 连接状态:{{ .Status }}\r\n" "online" = "🌐 连接状态:{{ .Status }}\r\n"
"lastOnline" = "🔙 上次在线: {{ .Time }}\r\n"
"email" = "📧 邮箱:{{ .Email }}\r\n" "email" = "📧 邮箱:{{ .Email }}\r\n"
"upload" = "🔼 上传↑:{{ .Upload }}\r\n" "upload" = "🔼 上传↑:{{ .Upload }}\r\n"
"download" = "🔽 下载↓:{{ .Download }}\r\n" "download" = "🔽 下载↓:{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了" "inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了"
"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密码: {{ .ClientPass }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了" "inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密码: {{ .ClientPass }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了"
"cancel" = "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄" "cancel" = "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄"
"error_add_client" = "⚠️ 错误:\n\n {{ .error }}" "error_add_client" = "⚠️ 错误:\n\n {{ .error }}"
"using_default_value" = "好的,我会使用默认值。 😊" "using_default_value" = "好的,我会使用默认值。 😊"
"incorrect_input" ="您的输入无效。\n短语应连续输入不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫" "incorrect_input" = "您的输入无效。\n短语应连续输入不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫"
"AreYouSure" = "你确定吗?🤔" "AreYouSure" = "你确定吗?🤔"
"SuccessResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功" "SuccessResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功"
"FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠 错误: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠 错误: [ {{ .ErrorMessage }} ]"

View file

@ -106,7 +106,7 @@
"invalidFormData" = "資料格式錯誤" "invalidFormData" = "資料格式錯誤"
"emptyUsername" = "請輸入使用者名稱" "emptyUsername" = "請輸入使用者名稱"
"emptyPassword" = "請輸入密碼" "emptyPassword" = "請輸入密碼"
"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。" "wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"
"successLogin" = "您已成功登入您的帳戶。" "successLogin" = "您已成功登入您的帳戶。"
[pages.index] [pages.index]
@ -242,7 +242,7 @@
"same" = "相同" "same" = "相同"
"inboundData" = "入站資料" "inboundData" = "入站資料"
"exportInbound" = "匯出入站規則" "exportInbound" = "匯出入站規則"
"import"="匯入" "import" = "匯入"
"importInbound" = "匯入入站規則" "importInbound" = "匯入入站規則"
"periodicTrafficResetTitle" = "流量重置" "periodicTrafficResetTitle" = "流量重置"
"periodicTrafficResetDesc" = "按指定間隔自動重置流量計數器" "periodicTrafficResetDesc" = "按指定間隔自動重置流量計數器"
@ -565,9 +565,9 @@
[pages.settings.security] [pages.settings.security]
"admin" = "管理員憑證" "admin" = "管理員憑證"
"twoFactor" = "雙重驗證" "twoFactor" = "雙重驗證"
"twoFactorEnable" = "啟用2FA" "twoFactorEnable" = "啟用2FA"
"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。" "twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"
"twoFactorModalSetTitle" = "啟用雙重認證" "twoFactorModalSetTitle" = "啟用雙重認證"
"twoFactorModalDeleteTitle" = "停用雙重認證" "twoFactorModalDeleteTitle" = "停用雙重認證"
"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:" "twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
@ -663,6 +663,7 @@
"active" = "💡 啟用:{{ .Enable }}\r\n" "active" = "💡 啟用:{{ .Enable }}\r\n"
"enabled" = "🚨 已啟用:{{ .Enable }}\r\n" "enabled" = "🚨 已啟用:{{ .Enable }}\r\n"
"online" = "🌐 連線狀態:{{ .Status }}\r\n" "online" = "🌐 連線狀態:{{ .Status }}\r\n"
"lastOnline" = "🔙 上次上線: {{ .Time }}\r\n"
"email" = "📧 郵箱:{{ .Email }}\r\n" "email" = "📧 郵箱:{{ .Email }}\r\n"
"upload" = "🔼 上傳↑:{{ .Upload }}\r\n" "upload" = "🔼 上傳↑:{{ .Upload }}\r\n"
"download" = "🔽 下載↓:{{ .Download }}\r\n" "download" = "🔽 下載↓:{{ .Download }}\r\n"
@ -688,9 +689,9 @@
"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了" "inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了"
"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了" "inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了"
"cancel" = "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄" "cancel" = "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄"
"error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}" "error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}"
"using_default_value" = "好的,我會使用預設值。 😊" "using_default_value" = "好的,我會使用預設值。 😊"
"incorrect_input" ="您的輸入無效。\n短語應連續輸入不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫" "incorrect_input" = "您的輸入無效。\n短語應連續輸入不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫"
"AreYouSure" = "你確定嗎?🤔" "AreYouSure" = "你確定嗎?🤔"
"SuccessResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功" "SuccessResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
"FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 錯誤: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠 錯誤: [ {{ .ErrorMessage }} ]"

149
x-ui.sh
View file

@ -509,12 +509,16 @@ enable_bbr() {
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum -y update && yum -y install ca-certificates
;;
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install ca-certificates dnf -y update && dnf -y install ca-certificates
;; ;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum -y install ca-certificates
else
dnf -y update && dnf -y install ca-certificates
fi
;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates pacman -Sy --noconfirm ca-certificates
;; ;;
@ -863,10 +867,32 @@ delete_ports() {
fi fi
} }
update_all_geofiles() {
update_main_geofiles
update_ir_geofiles
update_ru_geofiles
}
update_main_geofiles() {
wget -O geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
}
update_ir_geofiles() {
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
}
update_ru_geofiles() {
wget -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
}
update_geo() { update_geo() {
echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)" echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)"
echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)" echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)"
echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)" echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)"
echo -e "${green}\t4.${plain} All"
echo -e "${green}\t0.${plain} Back to Main Menu" echo -e "${green}\t0.${plain} Back to Main Menu"
read -rp "Choose an option: " choice read -rp "Choose an option: " choice
@ -877,41 +903,25 @@ update_geo() {
show_menu show_menu
;; ;;
1) 1)
if [[ $release == "alpine" ]]; then update_main_geofiles
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip.dat geosite.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}" echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
restart restart
;; ;;
2) 2)
if [[ $release == "alpine" ]]; then update_ir_geofiles
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_IR.dat geosite_IR.dat
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}" echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
restart restart
;; ;;
3) 3)
if [[ $release == "alpine" ]]; then update_ru_geofiles
rc-service x-ui stop
else
systemctl stop x-ui
fi
rm -f geoip_RU.dat geosite_RU.dat
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}" echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
restart restart
;; ;;
4)
update_all_geofiles
echo -e "${green}All geo files have been updated successfully!${plain}"
restart
;;
*) *)
echo -e "${red}Invalid option. Please select a valid number.${plain}\n" echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
update_geo update_geo
@ -1067,12 +1077,16 @@ ssl_cert_issue() {
ubuntu | debian | armbian) ubuntu | debian | armbian)
apt-get update && apt-get install socat -y apt-get update && apt-get install socat -y
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum -y update && yum -y install socat
;;
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install socat dnf -y update && dnf -y install socat
;; ;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum -y install socat
else
dnf -y update && dnf -y install socat
fi
;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Sy --noconfirm socat pacman -Sy --noconfirm socat
;; ;;
@ -1080,7 +1094,7 @@ ssl_cert_issue() {
zypper refresh && zypper -q install -y socat zypper refresh && zypper -q install -y socat
;; ;;
alpine) alpine)
apk add socat apk add socat curl openssl
;; ;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
@ -1531,13 +1545,17 @@ install_iplimit() {
armbian) armbian)
apt-get update && apt-get install fail2ban -y apt-get update && apt-get install fail2ban -y
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum update -y && yum install epel-release -y
yum -y install fail2ban
;;
fedora | amzn | virtuozzo)
dnf -y update && dnf -y install fail2ban dnf -y update && dnf -y install fail2ban
;; ;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum update -y && yum install epel-release -y
yum -y install fail2ban
else
dnf -y update && dnf -y install fail2ban
fi
;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Syu --noconfirm fail2ban pacman -Syu --noconfirm fail2ban
;; ;;
@ -1631,14 +1649,19 @@ remove_iplimit() {
apt-get purge -y fail2ban -y apt-get purge -y fail2ban -y
apt-get autoremove -y apt-get autoremove -y
;; ;;
centos | rhel | almalinux | rocky | ol) fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
yum remove fail2ban -y
yum autoremove -y
;;
fedora | amzn | virtuozzo)
dnf remove fail2ban -y dnf remove fail2ban -y
dnf autoremove -y dnf autoremove -y
;; ;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum remove fail2ban -y
yum autoremove -y
else
dnf remove fail2ban -y
dnf autoremove -y
fi
;;
arch | manjaro | parch) arch | manjaro | parch)
pacman -Rns --noconfirm fail2ban pacman -Rns --noconfirm fail2ban
;; ;;
@ -1869,24 +1892,25 @@ SSH_port_forwarding() {
} }
show_usage() { show_usage() {
echo -e "┌───────────────────────────────────────────────────────┐ echo -e "┌────────────────────────────────────────────────────────────────┐
${blue}x-ui control menu usages (subcommands):${plain} ${blue}x-ui control menu usages (subcommands):${plain}
│ │ │ │
${blue}x-ui${plain} - Admin Management Script │ ${blue}x-ui${plain} - Admin Management Script │
${blue}x-ui start${plain} - Start │ ${blue}x-ui start${plain} - Start │
${blue}x-ui stop${plain} - Stop │ ${blue}x-ui stop${plain} - Stop │
${blue}x-ui restart${plain} - Restart │ ${blue}x-ui restart${plain} - Restart │
${blue}x-ui status${plain} - Current Status │ ${blue}x-ui status${plain} - Current Status │
${blue}x-ui settings${plain} - Current Settings │ ${blue}x-ui settings${plain} - Current Settings │
${blue}x-ui enable${plain} - Enable Autostart on OS Startup │ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
${blue}x-ui disable${plain} - Disable Autostart on OS Startup │ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
${blue}x-ui log${plain} - Check logs │ ${blue}x-ui log${plain} - Check logs │
${blue}x-ui banlog${plain} - Check Fail2ban ban logs │ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
${blue}x-ui update${plain} - Update │ ${blue}x-ui update${plain} - Update │
${blue}x-ui legacy${plain} - legacy version │ ${blue}x-ui update-all-geofiles${plain} - Update all geo files │
${blue}x-ui install${plain} - Install │ ${blue}x-ui legacy${plain} - Legacy version │
${blue}x-ui uninstall${plain} - Uninstall │ ${blue}x-ui install${plain} - Install │
└───────────────────────────────────────────────────────┘" ${blue}x-ui uninstall${plain} - Uninstall │
└────────────────────────────────────────────────────────────────┘"
} }
show_menu() { show_menu() {
@ -2056,6 +2080,9 @@ if [[ $# > 0 ]]; then
"uninstall") "uninstall")
check_install 0 && uninstall 0 check_install 0 && uninstall 0
;; ;;
"update-all-geofiles")
check_install 0 && update_all_geofiles 0 && restart 0
;;
*) show_usage ;; *) show_usage ;;
esac esac
else else