mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-08-23 11:26:52 +00:00
Compare commits
9 commits
fe2f170ac6
...
57d87241b5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
57d87241b5 | ||
![]() |
2198e7a28f | ||
![]() |
6b23b416a7 | ||
![]() |
16f53ce4c2 | ||
![]() |
27445b30e9 | ||
![]() |
3299d15f28 | ||
![]() |
ae82373457 | ||
![]() |
d65233cc2c | ||
![]() |
11dc06863e |
39 changed files with 867 additions and 192 deletions
|
@ -1,7 +1,7 @@
|
|||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ func initModels() error {
|
|||
&model.InboundClientIps{},
|
||||
&xray.ClientTraffic{},
|
||||
&model.HistoryOfSeeders{},
|
||||
&model.Server{},
|
||||
}
|
||||
for _, model := range models {
|
||||
if err := db.AutoMigrate(model); err != nil {
|
||||
|
|
|
@ -105,3 +105,12 @@ type Client struct {
|
|||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" gorm:"unique;not null"`
|
||||
Address string `json:"address" gorm:"not null"`
|
||||
Port int `json:"port" gorm:"not null"`
|
||||
APIKey string `json:"apiKey" gorm:"not null"`
|
||||
Enable bool `json:"enable" gorm:"default:true"`
|
||||
}
|
||||
|
|
9
go.mod
9
go.mod
|
@ -1,6 +1,6 @@
|
|||
module x-ui
|
||||
|
||||
go 1.24.5
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
|
@ -15,7 +15,7 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/valyala/fasthttp v1.64.0
|
||||
github.com/valyala/fasthttp v1.65.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250803.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
|
@ -56,11 +56,12 @@ require (
|
|||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.30 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
|
@ -92,7 +93,7 @@ require (
|
|||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
|
|
12
go.sum
12
go.sum
|
@ -95,8 +95,8 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32
|
|||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
|
||||
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
@ -162,8 +162,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
|
|||
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/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
|
@ -226,8 +226,8 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
|
|||
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/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a h1:tPE/Kp+x9dMSwUm/uM0JKK0IfdiJkwAbSMSeZBXXJXc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250811230008-5f3141c8851a/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
|
|
|
@ -130,6 +130,13 @@ config_after_install() {
|
|||
fi
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
|
||||
local existing_apiKey=$(/usr/local/x-ui/x-ui setting -show true | grep -oP 'ApiKey: \K.*')
|
||||
if [[ -z "$existing_apiKey" ]]; then
|
||||
local config_apiKey=$(gen_random_string 32)
|
||||
/usr/local/x-ui/x-ui setting -apiKey "${config_apiKey}"
|
||||
echo -e "${green}Generated random API Key: ${config_apiKey}${plain}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
|
|
15
main.go
15
main.go
|
@ -232,7 +232,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
|||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool) {
|
||||
func updateSetting(port int, username string, password string, webBasePath string, listenIP string, resetTwoFactor bool, apiKey string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println("Database initialization failed:", err)
|
||||
|
@ -242,6 +242,15 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
|||
settingService := service.SettingService{}
|
||||
userService := service.UserService{}
|
||||
|
||||
if apiKey != "" {
|
||||
err := settingService.SetAPIKey(apiKey)
|
||||
if err != nil {
|
||||
fmt.Println("Failed to set API Key:", err)
|
||||
} else {
|
||||
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
if port > 0 {
|
||||
err := settingService.SetPort(port)
|
||||
if err != nil {
|
||||
|
@ -388,9 +397,11 @@ func main() {
|
|||
var show bool
|
||||
var getCert bool
|
||||
var resetTwoFactor bool
|
||||
var apiKey string
|
||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||
settingCmd.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||
|
@ -440,7 +451,7 @@ func main() {
|
|||
if reset {
|
||||
resetSetting()
|
||||
} else {
|
||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
||||
}
|
||||
if show {
|
||||
showSetting(show)
|
||||
|
|
|
@ -209,9 +209,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
|||
var streamSettings map[string]any
|
||||
json.Unmarshal([]byte(stream), &streamSettings)
|
||||
security, _ := streamSettings["security"].(string)
|
||||
if security == "tls" {
|
||||
switch security {
|
||||
case "tls":
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
|
||||
} else if security == "reality" {
|
||||
case "reality":
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
|
|
|
@ -154,26 +154,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
|
|||
}
|
||||
|
||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
return s.genVmessLink(inbound, email)
|
||||
case "vless":
|
||||
return s.genVlessLink(inbound, email)
|
||||
case "trojan":
|
||||
return s.genTrojanLink(inbound, email)
|
||||
case "shadowsocks":
|
||||
return s.genShadowsocksLink(inbound, email)
|
||||
serverService := service.MultiServerService{}
|
||||
servers, err := serverService.GetServers()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get servers for subscription:", err)
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
|
||||
var links []string
|
||||
for _, server := range servers {
|
||||
if !server.Enable {
|
||||
continue
|
||||
}
|
||||
var link string
|
||||
switch inbound.Protocol {
|
||||
case "vmess":
|
||||
link = s.genVmessLink(inbound, email, server)
|
||||
case "vless":
|
||||
link = s.genVlessLink(inbound, email, server)
|
||||
case "trojan":
|
||||
link = s.genTrojanLink(inbound, email, server)
|
||||
case "shadowsocks":
|
||||
link = s.genShadowsocksLink(inbound, email, server)
|
||||
}
|
||||
if link != "" {
|
||||
links = append(links, link)
|
||||
}
|
||||
}
|
||||
return strings.Join(links, "\n")
|
||||
}
|
||||
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
if inbound.Protocol != model.VMESS {
|
||||
return ""
|
||||
}
|
||||
obj := map[string]any{
|
||||
"v": "2",
|
||||
"add": s.address,
|
||||
"add": server.Address,
|
||||
"port": inbound.Port,
|
||||
"type": "none",
|
||||
}
|
||||
|
@ -286,7 +303,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
newObj[key] = value
|
||||
}
|
||||
}
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
newObj["add"] = ep["dest"].(string)
|
||||
newObj["port"] = int(ep["port"].(float64))
|
||||
|
||||
|
@ -302,14 +319,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
|||
return links
|
||||
}
|
||||
|
||||
obj["ps"] = s.genRemark(inbound, email, "")
|
||||
obj["ps"] = s.genRemark(inbound, email, "", server.Name)
|
||||
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
|
@ -482,7 +499,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -503,12 +520,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.Trojan {
|
||||
return ""
|
||||
}
|
||||
|
@ -677,7 +694,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -699,12 +716,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||
address := s.address
|
||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||
address := server.Address
|
||||
if inbound.Protocol != model.Shadowsocks {
|
||||
return ""
|
||||
}
|
||||
|
@ -844,7 +861,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
|
@ -865,17 +882,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
|||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, serverName string) string {
|
||||
separationChar := string(s.remarkModel[0])
|
||||
orderChars := s.remarkModel[1:]
|
||||
orders := map[byte]string{
|
||||
'i': "",
|
||||
'e': "",
|
||||
'o': "",
|
||||
's': "",
|
||||
}
|
||||
if len(email) > 0 {
|
||||
orders['e'] = email
|
||||
|
@ -886,6 +904,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
|||
if len(extra) > 0 {
|
||||
orders['o'] = extra
|
||||
}
|
||||
if len(serverName) > 0 {
|
||||
orders['s'] = serverName
|
||||
}
|
||||
|
||||
var remark []string
|
||||
for i := 0; i < len(orderChars); i++ {
|
||||
|
|
|
@ -995,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
|
|||
network = 'udp',
|
||||
address = '',
|
||||
port = 53,
|
||||
nonIPQuery = 'drop',
|
||||
nonIPQuery = 'reject',
|
||||
blockTypes = []
|
||||
) {
|
||||
super();
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
|
@ -32,15 +33,26 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/update/:id", a.updateInbound)
|
||||
g.POST("/clientIps/:email", a.getClientIps)
|
||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
|
||||
// Routes for UI
|
||||
g.POST("/addClient", a.addInboundClient)
|
||||
g.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
g.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
|
||||
// Routes for API (for slave servers)
|
||||
apiGroup := g.Group("/api")
|
||||
apiGroup.Use(middleware.ApiAuth())
|
||||
{
|
||||
apiGroup.POST("/addClient", a.addInboundClient)
|
||||
apiGroup.POST("/:id/delClient/:clientId", a.delInboundClient)
|
||||
apiGroup.POST("/updateClient/:clientId", a.updateInboundClient)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
|
|
89
web/controller/multi_server_controller.go
Normal file
89
web/controller/multi_server_controller.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"x-ui/database/model"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type MultiServerController struct {
|
||||
multiServerService service.MultiServerService
|
||||
}
|
||||
|
||||
func NewMultiServerController(g *gin.RouterGroup) *MultiServerController {
|
||||
c := &MultiServerController{}
|
||||
c.initRouter(g)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *MultiServerController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/server")
|
||||
|
||||
g.GET("/list", c.getServers)
|
||||
g.POST("/add", c.addServer)
|
||||
g.POST("/del/:id", c.delServer)
|
||||
g.POST("/update/:id", c.updateServer)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) getServers(ctx *gin.Context) {
|
||||
servers, err := c.multiServerService.GetServers()
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to get servers", err)
|
||||
return
|
||||
}
|
||||
jsonObj(ctx, servers, nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) addServer(ctx *gin.Context) {
|
||||
server := &model.Server{}
|
||||
err := ctx.ShouldBind(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid data", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.AddServer(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to add server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server added successfully", nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) delServer(ctx *gin.Context) {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid ID", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.DeleteServer(id)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to delete server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server deleted successfully", nil)
|
||||
}
|
||||
|
||||
func (c *MultiServerController) updateServer(ctx *gin.Context) {
|
||||
id, err := strconv.Atoi(ctx.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid ID", err)
|
||||
return
|
||||
}
|
||||
server := &model.Server{
|
||||
Id: id,
|
||||
}
|
||||
err = ctx.ShouldBind(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Invalid data", err)
|
||||
return
|
||||
}
|
||||
err = c.multiServerService.UpdateServer(server)
|
||||
if err != nil {
|
||||
jsonMsg(ctx, "Failed to update server", err)
|
||||
return
|
||||
}
|
||||
jsonMsg(ctx, "Server updated successfully", nil)
|
||||
}
|
|
@ -24,6 +24,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||
|
||||
g.GET("/", a.index)
|
||||
g.GET("/inbounds", a.inbounds)
|
||||
g.GET("/servers", a.servers)
|
||||
g.GET("/settings", a.settings)
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
|
@ -47,3 +48,7 @@ func (a *XUIController) settings(c *gin.Context) {
|
|||
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||
html(c, "xray.html", "pages.xray.title", nil)
|
||||
}
|
||||
|
||||
func (a *XUIController) servers(c *gin.Context) {
|
||||
html(c, "servers.html", "Servers", nil)
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@ package entity
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
"math"
|
||||
|
||||
"x-ui/util/common"
|
||||
)
|
||||
|
@ -39,8 +39,8 @@ type AllSetting struct {
|
|||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
|
|
|
@ -54,6 +54,11 @@
|
|||
icon: 'user',
|
||||
title: '{{ i18n "menu.inbounds"}}'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/servers',
|
||||
icon: 'cloud-server',
|
||||
title: 'Servers'
|
||||
},
|
||||
{
|
||||
key: '{{ .base_path }}panel/settings',
|
||||
icon: 'setting',
|
||||
|
|
|
@ -105,7 +105,7 @@
|
|||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
|
||||
|
|
165
web/html/servers.html
Normal file
165
web/html/servers.html
Normal file
|
@ -0,0 +1,165 @@
|
|||
{{template "header" .}}
|
||||
|
||||
<div id="app" class="row" v-cloak>
|
||||
<div class="col-md-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Server Management</h3>
|
||||
<div class="card-tools">
|
||||
<button class="btn btn-primary" @click="showAddModal">Add Server</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Name</th>
|
||||
<th>Address</th>
|
||||
<th>Port</th>
|
||||
<th>Enabled</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(server, index) in servers">
|
||||
<td>{{index + 1}}</td>
|
||||
<td>{{server.name}}</td>
|
||||
<td>{{server.address}}</td>
|
||||
<td>{{server.port}}</td>
|
||||
<td>
|
||||
<span v-if="server.enable" class="badge bg-success">Yes</span>
|
||||
<span v-else class="badge bg-danger">No</span>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-info btn-sm" @click="showEditModal(server)">Edit</button>
|
||||
<button class="btn btn-danger btn-sm" @click="deleteServer(server.id)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add/Edit Modal -->
|
||||
<div class="modal fade" id="serverModal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{modal.title}}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Address (IP or Domain)</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Port</label>
|
||||
<input type="number" class="form-control" v-model.number="modal.server.port">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>API Key</label>
|
||||
<input type="text" class="form-control" v-model="modal.server.apiKey">
|
||||
</div>
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input" v-model="modal.server.enable">
|
||||
<label class="form-check-label">Enabled</label>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" @click="saveServer">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
servers: [],
|
||||
modal: {
|
||||
title: '',
|
||||
server: {
|
||||
name: '',
|
||||
address: '',
|
||||
port: 0,
|
||||
apiKey: '',
|
||||
enable: true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadServers() {
|
||||
axios.get('{{.base_path}}server/list')
|
||||
.then(response => {
|
||||
this.servers = response.data.obj;
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
},
|
||||
showAddModal() {
|
||||
this.modal.title = 'Add Server';
|
||||
this.modal.server = {
|
||||
name: '',
|
||||
address: '',
|
||||
port: 0,
|
||||
apiKey: '',
|
||||
enable: true
|
||||
};
|
||||
$('#serverModal').modal('show');
|
||||
},
|
||||
showEditModal(server) {
|
||||
this.modal.title = 'Edit Server';
|
||||
this.modal.server = Object.assign({}, server);
|
||||
$('#serverModal').modal('show');
|
||||
},
|
||||
saveServer() {
|
||||
let url = '{{.base_path}}server/add';
|
||||
if (this.modal.server.id) {
|
||||
url = `{{.base_path}}server/update/${this.modal.server.id}`;
|
||||
}
|
||||
axios.post(url, this.modal.server)
|
||||
.then(response => {
|
||||
alert(response.data.msg);
|
||||
$('#serverModal').modal('hide');
|
||||
this.loadServers();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
},
|
||||
deleteServer(id) {
|
||||
if (!confirm('Are you sure you want to delete this server?')) {
|
||||
return;
|
||||
}
|
||||
axios.post(`{{.base_path}}server/del/${id}`)
|
||||
.then(response => {
|
||||
alert(response.data.msg);
|
||||
this.loadServers();
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.response.data.msg);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadServers();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{{template "footer" .}}
|
|
@ -11,7 +11,6 @@ import (
|
|||
"sort"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
|
@ -58,21 +57,21 @@ func (j *CheckClientIpJob) Run() {
|
|||
func (j *CheckClientIpJob) clearAccessLog() {
|
||||
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
j.checkError(err)
|
||||
defer logAccessP.Close()
|
||||
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
j.checkError(err)
|
||||
|
||||
file, err := os.Open(accessLogPath)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(logAccessP, file)
|
||||
j.checkError(err)
|
||||
|
||||
logAccessP.Close()
|
||||
file.Close()
|
||||
|
||||
err = os.Truncate(accessLogPath, 0)
|
||||
j.checkError(err)
|
||||
|
||||
j.lastClear = time.Now().Unix()
|
||||
}
|
||||
|
||||
|
@ -193,10 +192,6 @@ func (j *CheckClientIpJob) checkError(e error) {
|
|||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) contains(s []string, str string) bool {
|
||||
return slices.Contains(s, str)
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
db := database.GetDB()
|
||||
InboundClientIps := &model.InboundClientIps{}
|
||||
|
|
34
web/middleware/auth.go
Normal file
34
web/middleware/auth.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"x-ui/web/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func ApiAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
apiKey := c.GetHeader("Api-Key")
|
||||
if apiKey == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "API key is required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
panelAPIKey, err := settingService.GetAPIKey()
|
||||
if err != nil || panelAPIKey == "" {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "API key not configured on the panel"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
if apiKey != panelAPIKey {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid API key"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
|
@ -1,8 +1,11 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -177,15 +180,16 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
|||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
if client.Password == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
if client.Email == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
if client.ID == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
|
@ -436,15 +440,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
switch oldInbound.Protocol {
|
||||
case "trojan":
|
||||
if client.Password == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
if client.Email == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
if client.ID == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
|
@ -511,6 +516,11 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
|||
}
|
||||
s.xrayApi.Close()
|
||||
|
||||
if err == nil {
|
||||
body, _ := json.Marshal(data)
|
||||
s.syncWithSlaves("POST", "/panel/inbound/api/addClient", bytes.NewReader(body))
|
||||
}
|
||||
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -599,6 +609,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
|||
s.xrayApi.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/%d/delClient/%s", inboundId, clientId), nil)
|
||||
}
|
||||
|
||||
return needRestart, db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -631,13 +646,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
clientIndex := -1
|
||||
for index, oldClient := range oldClients {
|
||||
oldClientId := ""
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
switch oldInbound.Protocol {
|
||||
case "trojan":
|
||||
oldClientId = oldClient.Password
|
||||
newClientId = clients[0].Password
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
oldClientId = oldClient.Email
|
||||
newClientId = clients[0].Email
|
||||
} else {
|
||||
default:
|
||||
oldClientId = oldClient.ID
|
||||
newClientId = clients[0].ID
|
||||
}
|
||||
|
@ -753,6 +769,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
|||
logger.Debug("Client old email not found")
|
||||
needRestart = true
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
body, _ := json.Marshal(data)
|
||||
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/updateClient/%s", clientId), bytes.NewReader(body))
|
||||
}
|
||||
|
||||
return needRestart, tx.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
|
@ -1244,11 +1266,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
|
|||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
|
@ -1328,11 +1351,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
|
|||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
clientOldEnabled = oldClient.Enable
|
||||
|
@ -1391,11 +1415,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
|||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
|
@ -1448,11 +1473,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
|||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
|
@ -1508,11 +1534,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
|||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
|
@ -2075,3 +2102,41 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
|||
|
||||
return validEmails, extraEmails, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) syncWithSlaves(method string, path string, body io.Reader) {
|
||||
serverService := MultiServerService{}
|
||||
servers, err := serverService.GetServers()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get servers for syncing:", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, server := range servers {
|
||||
if !server.Enable {
|
||||
continue
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("http://%s:%d%s", server.Address, server.Port, path)
|
||||
req, err := http.NewRequest(method, url, body)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to create request for server %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Api-Key", server.APIKey)
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to send request to server %s: %v", server.Name, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
logger.Warningf("Failed to sync with server %s. Status: %s, Body: %s", server.Name, resp.Status, string(bodyBytes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
72
web/service/inbound_service_sync_test.go
Normal file
72
web/service/inbound_service_sync_test.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInboundServiceSync(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// Mock server to simulate a slave
|
||||
var receivedApiKey string
|
||||
var receivedBody []byte
|
||||
mockSlave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedApiKey = r.Header.Get("Api-Key")
|
||||
receivedBody, _ = io.ReadAll(r.Body)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer mockSlave.Close()
|
||||
|
||||
// Add the mock slave to the database
|
||||
multiServerService := MultiServerService{}
|
||||
mockSlaveURL, _ := url.Parse(mockSlave.URL)
|
||||
mockSlavePort, _ := strconv.Atoi(mockSlaveURL.Port())
|
||||
slaveServer := &model.Server{
|
||||
Name: "mock-slave",
|
||||
Address: mockSlaveURL.Hostname(),
|
||||
Port: mockSlavePort,
|
||||
APIKey: "slave-api-key",
|
||||
Enable: true,
|
||||
}
|
||||
multiServerService.AddServer(slaveServer)
|
||||
|
||||
// Create a test inbound and client
|
||||
inboundService := InboundService{}
|
||||
db := database.GetDB()
|
||||
testInbound := &model.Inbound{
|
||||
UserId: 1,
|
||||
Remark: "test-inbound",
|
||||
Enable: true,
|
||||
Settings: `{"clients":[]}`,
|
||||
}
|
||||
db.Create(testInbound)
|
||||
|
||||
clientData := model.Client{
|
||||
Email: "test@example.com",
|
||||
ID: "test-id",
|
||||
}
|
||||
clientBytes, _ := json.Marshal([]model.Client{clientData})
|
||||
inboundData := &model.Inbound{
|
||||
Id: testInbound.Id,
|
||||
Settings: string(clientBytes),
|
||||
}
|
||||
|
||||
// Test AddInboundClient sync
|
||||
inboundService.AddInboundClient(inboundData)
|
||||
|
||||
assert.Equal(t, "slave-api-key", receivedApiKey)
|
||||
var receivedInbound model.Inbound
|
||||
json.Unmarshal(receivedBody, &receivedInbound)
|
||||
assert.Equal(t, 1, receivedInbound.Id)
|
||||
}
|
37
web/service/multi_server_service.go
Normal file
37
web/service/multi_server_service.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
)
|
||||
|
||||
type MultiServerService struct{}
|
||||
|
||||
func (s *MultiServerService) GetServers() ([]*model.Server, error) {
|
||||
db := database.GetDB()
|
||||
var servers []*model.Server
|
||||
err := db.Find(&servers).Error
|
||||
return servers, err
|
||||
}
|
||||
|
||||
func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
|
||||
db := database.GetDB()
|
||||
var server model.Server
|
||||
err := db.First(&server, id).Error
|
||||
return &server, err
|
||||
}
|
||||
|
||||
func (s *MultiServerService) AddServer(server *model.Server) error {
|
||||
db := database.GetDB()
|
||||
return db.Create(server).Error
|
||||
}
|
||||
|
||||
func (s *MultiServerService) UpdateServer(server *model.Server) error {
|
||||
db := database.GetDB()
|
||||
return db.Save(server).Error
|
||||
}
|
||||
|
||||
func (s *MultiServerService) DeleteServer(id int) error {
|
||||
db := database.GetDB()
|
||||
return db.Delete(&model.Server{}, id).Error
|
||||
}
|
63
web/service/multi_server_service_test.go
Normal file
63
web/service/multi_server_service_test.go
Normal file
|
@ -0,0 +1,63 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func setup() {
|
||||
dbPath := "test.db"
|
||||
os.Remove(dbPath)
|
||||
database.InitDB(dbPath)
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
db, _ := database.GetDB().DB()
|
||||
db.Close()
|
||||
os.Remove("test.db")
|
||||
}
|
||||
|
||||
func TestMultiServerService(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
service := MultiServerService{}
|
||||
|
||||
// Test AddServer
|
||||
server := &model.Server{
|
||||
Name: "test-server",
|
||||
Address: "127.0.0.1",
|
||||
Port: 54321,
|
||||
APIKey: "test-key",
|
||||
Enable: true,
|
||||
}
|
||||
err := service.AddServer(server)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Test GetServer
|
||||
retrievedServer, err := service.GetServer(server.Id)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, server.Name, retrievedServer.Name)
|
||||
|
||||
// Test GetServers
|
||||
servers, err := service.GetServers()
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, servers, 1)
|
||||
|
||||
// Test UpdateServer
|
||||
retrievedServer.Name = "updated-server"
|
||||
err = service.UpdateServer(retrievedServer)
|
||||
assert.NoError(t, err)
|
||||
updatedServer, _ := service.GetServer(server.Id)
|
||||
assert.Equal(t, "updated-server", updatedServer.Name)
|
||||
|
||||
// Test DeleteServer
|
||||
err = service.DeleteServer(server.Id)
|
||||
assert.NoError(t, err)
|
||||
_, err = service.GetServer(server.Id)
|
||||
assert.Error(t, err)
|
||||
}
|
|
@ -180,6 +180,21 @@ func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
|||
return setting, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetAPIKey() (string, error) {
|
||||
setting, err := s.getSetting("ApiKey")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if setting == nil {
|
||||
return "", nil
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) SetAPIKey(apiKey string) error {
|
||||
return s.saveSetting("ApiKey", apiKey)
|
||||
}
|
||||
|
||||
func (s *SettingService) saveSetting(key string, value string) error {
|
||||
setting, err := s.getSetting(key)
|
||||
db := database.GetDB()
|
||||
|
|
|
@ -40,7 +40,6 @@ var (
|
|||
isRunning bool
|
||||
hostname string
|
||||
hashStorage *global.HashStorage
|
||||
handler *th.Handler
|
||||
|
||||
// clients data to adding new client
|
||||
receiver_inbound_ID int
|
||||
|
@ -641,13 +640,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -704,6 +704,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
|
@ -715,13 +719,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -844,13 +849,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -919,6 +925,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
|
@ -930,13 +940,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -1035,13 +1046,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -1101,6 +1113,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
|
@ -1112,13 +1128,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
|
@ -1288,6 +1305,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
}
|
||||
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
|
||||
}
|
||||
|
@ -1524,6 +1545,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
||||
case "add_client_default_ip_limit":
|
||||
|
@ -1534,6 +1559,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
||||
case "add_client_submit_disable":
|
||||
|
@ -1598,6 +1627,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
return
|
||||
}
|
||||
valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
|
||||
return
|
||||
}
|
||||
|
||||
for _, valid_emails := range valid_emails {
|
||||
traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
|
||||
|
@ -1760,6 +1793,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
|
|||
}
|
||||
|
||||
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
|
||||
if err != nil {
|
||||
logger.Warning("BuildJSONForProtocol run failed:", err)
|
||||
return false, errors.New("failed to build JSON for protocol")
|
||||
}
|
||||
|
||||
newInbound := &model.Inbound{
|
||||
Id: receiver_inbound_ID,
|
||||
|
@ -2008,10 +2045,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
|
|||
}
|
||||
|
||||
msg := ""
|
||||
if status == LoginSuccess {
|
||||
switch status {
|
||||
case LoginSuccess:
|
||||
msg += t.I18nBot("tgbot.messages.loginSuccess")
|
||||
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
} else if status == LoginFail {
|
||||
case LoginFail:
|
||||
msg += t.I18nBot("tgbot.messages.loginFailed")
|
||||
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
|
||||
|
@ -2171,6 +2209,22 @@ func (t *Tgbot) clientInfoMsg(
|
|||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
if diff > 0 {
|
||||
days := diff / 86400
|
||||
hours := (diff % 86400) / 3600
|
||||
minutes := (diff % 3600) / 60
|
||||
remainingTime := ""
|
||||
if days > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
|
||||
}
|
||||
if hours > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
|
||||
}
|
||||
if minutes > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
|
||||
}
|
||||
expiryTime += fmt.Sprintf(" (%s)", remainingTime)
|
||||
}
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
|
|
|
@ -561,24 +561,25 @@
|
|||
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!"
|
||||
"noResult" = "❗ مفيش نتيجة!"
|
||||
"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!"
|
||||
"wentWrong" = "❌ حصل خطأ!"
|
||||
"noIpRecord" = "❗ مفيش سجل IP!"
|
||||
"noInbounds" = "❗ مفيش إدخال متواجد!"
|
||||
"unlimited" = "♾ غير محدود (إعادة ضبط)"
|
||||
"add" = "أضف"
|
||||
"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
|
||||
"noResult" = "❗ لا يوجد نتائج!"
|
||||
"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
|
||||
"wentWrong" = "❌ حدث خطأ ما!"
|
||||
"noIpRecord" = "❗ لا يوجد سجل IP!"
|
||||
"noInbounds" = "❗ لم يتم العثور على أي وارد!"
|
||||
"unlimited" = "♾ غير محدود (إعادة تعيين)"
|
||||
"add" = "إضافة"
|
||||
"month" = "شهر"
|
||||
"months" = "شهور"
|
||||
"months" = "أشهر"
|
||||
"day" = "يوم"
|
||||
"days" = "أيام"
|
||||
"hours" = "ساعات"
|
||||
"unknown" = "مش معروف"
|
||||
"inbounds" = "الإدخالات"
|
||||
"minutes" = "دقائق"
|
||||
"unknown" = "غير معروف"
|
||||
"inbounds" = "الواردات"
|
||||
"clients" = "العملاء"
|
||||
"offline" = "🔴 أوفلاين"
|
||||
"online" = "🟢 أونلاين"
|
||||
"offline" = "🔴 غير متصل"
|
||||
"online" = "🟢 متصل"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ أمر مش معروف."
|
||||
|
|
|
@ -574,6 +574,7 @@
|
|||
"day" = "Day"
|
||||
"days" = "Days"
|
||||
"hours" = "Hours"
|
||||
"minutes" = "Minutes"
|
||||
"unknown" = "Unknown"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clients"
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
"invalidFormData" = "El formato de los datos de entrada es inválido."
|
||||
"emptyUsername" = "Por favor ingresa el nombre de usuario."
|
||||
"emptyPassword" = "Por favor ingresa la contraseña."
|
||||
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
|
||||
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
|
||||
"successLogin" = "Has iniciado sesión en tu cuenta correctamente."
|
||||
|
||||
[pages.index]
|
||||
|
@ -535,9 +535,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "Credenciales de administrador"
|
||||
"twoFactor" = "Autenticación de dos factores"
|
||||
"twoFactorEnable" = "Habilitar 2FA"
|
||||
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
|
||||
"twoFactor" = "Autenticación de dos factores"
|
||||
"twoFactorEnable" = "Habilitar 2FA"
|
||||
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
|
||||
"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
|
||||
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
|
||||
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
|
||||
|
@ -561,23 +561,24 @@
|
|||
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
|
||||
"noResult" = "❗ ¡Sin resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
|
||||
"keyboardClosed" = "❌ Teclado cerrado!"
|
||||
"noResult" = "❗ ¡No hay resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
|
||||
"wentWrong" = "❌ ¡Algo salió mal!"
|
||||
"noIpRecord" = "❗ ¡Sin Registro de IP!"
|
||||
"noIpRecord" = "❗ ¡No hay registro de IP!"
|
||||
"noInbounds" = "❗ ¡No se encontraron entradas!"
|
||||
"unlimited" = "♾ Ilimitado"
|
||||
"add" = "Agregar"
|
||||
"unlimited" = "♾ Ilimitado (Restablecer)"
|
||||
"add" = "Añadir"
|
||||
"month" = "Mes"
|
||||
"months" = "Meses"
|
||||
"day" = "Día"
|
||||
"days" = "Días"
|
||||
"hours" = "Horas"
|
||||
"minutes" = "Minutos"
|
||||
"unknown" = "Desconocido"
|
||||
"inbounds" = "Entradas"
|
||||
"clients" = "Clientes"
|
||||
"offline" = "🔴 Sin conexión"
|
||||
"offline" = "🔴 Desconectado"
|
||||
"online" = "🟢 En línea"
|
||||
|
||||
[tgbot.commands]
|
||||
|
|
|
@ -561,22 +561,23 @@
|
|||
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
|
||||
"noResult" = "❗ نتیجهای یافت نشد!"
|
||||
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||
"keyboardClosed" = "❌ صفحه کلید بسته شد!"
|
||||
"noResult" = "❗ نتیجه ای یافت نشد!"
|
||||
"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
|
||||
"wentWrong" = "❌ مشکلی پیش آمد!"
|
||||
"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ - نامحدود(ریست)"
|
||||
"add" = "اضافه کردن"
|
||||
"unlimited" = "♾ نامحدود(ریست)"
|
||||
"add" = "افزودن"
|
||||
"month" = "ماه"
|
||||
"months" = "ماه"
|
||||
"months" = "ماه"
|
||||
"day" = "روز"
|
||||
"days" = "روز"
|
||||
"hours" = "ساعت"
|
||||
"hours" = "ساعت"
|
||||
"minutes" = "دقیقه"
|
||||
"unknown" = "نامشخص"
|
||||
"inbounds" = "ورودیها"
|
||||
"clients" = "کلاینتها"
|
||||
"inbounds" = "ورودی ها"
|
||||
"clients" = "کاربران"
|
||||
"offline" = "🔴 آفلاین"
|
||||
"online" = "🟢 آنلاین"
|
||||
|
||||
|
|
|
@ -561,21 +561,22 @@
|
|||
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
|
||||
"keyboardClosed" = "❌ Keyboard ditutup!"
|
||||
"noResult" = "❗ Tidak ada hasil!"
|
||||
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
|
||||
"wentWrong" = "❌ Ada yang salah!"
|
||||
"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
|
||||
"wentWrong" = "❌ Terjadi kesalahan!"
|
||||
"noIpRecord" = "❗ Tidak ada Catatan IP!"
|
||||
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
|
||||
"unlimited" = "♾ Tak terbatas"
|
||||
"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
|
||||
"unlimited" = "♾ Tidak terbatas (Reset)"
|
||||
"add" = "Tambah"
|
||||
"month" = "Bulan"
|
||||
"months" = "Bulan"
|
||||
"day" = "Hari"
|
||||
"days" = "Hari"
|
||||
"hours" = "Jam"
|
||||
"minutes" = "Menit"
|
||||
"unknown" = "Tidak diketahui"
|
||||
"inbounds" = "Masuk"
|
||||
"inbounds" = "Inbound"
|
||||
"clients" = "Klien"
|
||||
"offline" = "🔴 Offline"
|
||||
"online" = "🟢 Online"
|
||||
|
|
|
@ -561,21 +561,22 @@
|
|||
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ カスタムキーボードが閉じられました!"
|
||||
"keyboardClosed" = "❌ キーボードを閉じました!"
|
||||
"noResult" = "❗ 結果がありません!"
|
||||
"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!"
|
||||
"wentWrong" = "❌ 問題が発生しました!"
|
||||
"noIpRecord" = "❗ IP記録がありません!"
|
||||
"noInbounds" = "❗ インバウンド接続が見つかりません!"
|
||||
"unlimited" = "♾ 無制限"
|
||||
"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!"
|
||||
"wentWrong" = "❌ 何かがうまくいかなかった!"
|
||||
"noIpRecord" = "❗ IPレコードがありません!"
|
||||
"noInbounds" = "❗ インバウンドが見つかりません!"
|
||||
"unlimited" = "♾ 無制限(リセット)"
|
||||
"add" = "追加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"months" = "ヶ月"
|
||||
"day" = "日"
|
||||
"days" = "日"
|
||||
"days" = "日間"
|
||||
"hours" = "時間"
|
||||
"minutes" = "分"
|
||||
"unknown" = "不明"
|
||||
"inbounds" = "インバウンド接続"
|
||||
"inbounds" = "インバウンド"
|
||||
"clients" = "クライアント"
|
||||
"offline" = "🔴 オフライン"
|
||||
"online" = "🟢 オンライン"
|
||||
|
|
|
@ -561,21 +561,22 @@
|
|||
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Teclado personalizado fechado!"
|
||||
"keyboardClosed" = "❌ Teclado fechado!"
|
||||
"noResult" = "❗ Nenhum resultado!"
|
||||
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
|
||||
"wentWrong" = "❌ Algo deu errado!"
|
||||
"noIpRecord" = "❗ Nenhum registro de IP!"
|
||||
"noInbounds" = "❗ Nenhuma entrada encontrada!"
|
||||
"unlimited" = "♾ Ilimitado (Reiniciar)"
|
||||
"noInbounds" = "❗ Nenhum inbound encontrado!"
|
||||
"unlimited" = "♾ Ilimitado (Reset)"
|
||||
"add" = "Adicionar"
|
||||
"month" = "Mês"
|
||||
"months" = "Meses"
|
||||
"day" = "Dia"
|
||||
"days" = "Dias"
|
||||
"hours" = "Horas"
|
||||
"minutes" = "Minutos"
|
||||
"unknown" = "Desconhecido"
|
||||
"inbounds" = "Entradas"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clientes"
|
||||
"offline" = "🔴 Offline"
|
||||
"online" = "🟢 Online"
|
||||
|
|
|
@ -574,6 +574,7 @@
|
|||
"day" = "День"
|
||||
"days" = "Дней"
|
||||
"hours" = "Часов"
|
||||
"minutes" = "Минуты"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Инбаунды"
|
||||
"clients" = "Клиенты"
|
||||
|
|
|
@ -561,22 +561,23 @@
|
|||
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Özel klavye kapalı!"
|
||||
"keyboardClosed" = "❌ Klavye kapatıldı!"
|
||||
"noResult" = "❗ Sonuç yok!"
|
||||
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
|
||||
"wentWrong" = "❌ Bir şeyler yanlış gitti!"
|
||||
"noIpRecord" = "❗ IP Kaydı yok!"
|
||||
"noInbounds" = "❗ Gelen bulunamadı!"
|
||||
"unlimited" = "♾ Sınırsız(Sıfırla)"
|
||||
"noIpRecord" = "❗ IP Kaydı Yok!"
|
||||
"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
|
||||
"unlimited" = "♾ Sınırsız (Sıfırla)"
|
||||
"add" = "Ekle"
|
||||
"month" = "Ay"
|
||||
"months" = "Aylar"
|
||||
"day" = "Gün"
|
||||
"days" = "Günler"
|
||||
"hours" = "Saatler"
|
||||
"unknown" = "Bilinmiyor"
|
||||
"minutes" = "Dakika"
|
||||
"unknown" = "Bilinmeyen"
|
||||
"inbounds" = "Gelenler"
|
||||
"clients" = "Müşteriler"
|
||||
"clients" = "İstemciler"
|
||||
"offline" = "🔴 Çevrimdışı"
|
||||
"online" = "🟢 Çevrimiçi"
|
||||
|
||||
|
|
|
@ -561,19 +561,20 @@
|
|||
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
|
||||
"keyboardClosed" = "❌ Клавіатуру закрито!"
|
||||
"noResult" = "❗ Немає результату!"
|
||||
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
|
||||
"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
|
||||
"wentWrong" = "❌ Щось пішло не так!"
|
||||
"noIpRecord" = "❗ Немає IP-запису!"
|
||||
"noInbounds" = "❗ Вхідних не знайдено!"
|
||||
"unlimited" = "♾ Необмежений (скинути)"
|
||||
"noIpRecord" = "❗ Немає запису IP!"
|
||||
"noInbounds" = "❗ Вхідні не знайдені!"
|
||||
"unlimited" = "♾ Необмежено (Скинути)"
|
||||
"add" = "Додати"
|
||||
"month" = "Місяць"
|
||||
"months" = "Місяці"
|
||||
"day" = "День"
|
||||
"days" = "Дні"
|
||||
"hours" = "Годинник"
|
||||
"hours" = "Години"
|
||||
"minutes" = "Хвилини"
|
||||
"unknown" = "Невідомо"
|
||||
"inbounds" = "Вхідні"
|
||||
"clients" = "Клієнти"
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
|
||||
"emptyUsername" = "Vui lòng nhập tên người dùng."
|
||||
"emptyPassword" = "Vui lòng nhập mật khẩu."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
|
||||
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
|
||||
|
||||
[pages.index]
|
||||
|
@ -535,9 +535,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "Thông tin đăng nhập quản trị viên"
|
||||
"twoFactor" = "Xác thực hai yếu tố"
|
||||
"twoFactorEnable" = "Bật 2FA"
|
||||
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
|
||||
"twoFactor" = "Xác thực hai yếu tố"
|
||||
"twoFactorEnable" = "Bật 2FA"
|
||||
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
|
||||
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
|
||||
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
|
||||
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
|
||||
|
@ -561,22 +561,23 @@
|
|||
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
|
||||
"keyboardClosed" = "❌ Bàn phím đã đóng!"
|
||||
"noResult" = "❗ Không có kết quả!"
|
||||
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
|
||||
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
|
||||
"wentWrong" = "❌ Đã xảy ra lỗi!"
|
||||
"noIpRecord" = "❗ Không có bản ghi IP!"
|
||||
"noInbounds" = "❗ Không tìm thấy inbound!"
|
||||
"unlimited" = "♾ Không giới hạn"
|
||||
"unlimited" = "♾ Không giới hạn (Đặt lại)"
|
||||
"add" = "Thêm"
|
||||
"month" = "Tháng"
|
||||
"months" = "Tháng"
|
||||
"day" = "Ngày"
|
||||
"days" = "Ngày"
|
||||
"hours" = "Giờ"
|
||||
"unknown" = "Không rõ"
|
||||
"inbounds" = "Vào"
|
||||
"clients" = "Các người dùng"
|
||||
"minutes" = "Phút"
|
||||
"unknown" = "Không xác định"
|
||||
"inbounds" = "Inbound"
|
||||
"clients" = "Client"
|
||||
"offline" = "🔴 Ngoại tuyến"
|
||||
"online" = "🟢 Trực tuyến"
|
||||
|
||||
|
|
|
@ -563,19 +563,20 @@
|
|||
[tgbot]
|
||||
"keyboardClosed" = "❌ 自定义键盘已关闭!"
|
||||
"noResult" = "❗ 没有结果!"
|
||||
"noQuery" = "❌ 未找到查询!请重新使用命令!"
|
||||
"noQuery" = "❌ 未找到查询!请再次使用该命令!"
|
||||
"wentWrong" = "❌ 出了点问题!"
|
||||
"noIpRecord" = "❗ 没有 IP 记录!"
|
||||
"noInbounds" = "❗ 没有找到入站连接!"
|
||||
"unlimited" = "♾ 无限制"
|
||||
"noIpRecord" = "❗ 没有IP记录!"
|
||||
"noInbounds" = "❗ 未找到入站!"
|
||||
"unlimited" = "♾ 无限(重置)"
|
||||
"add" = "添加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"hours" = "小时"
|
||||
"minutes" = "分钟"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站连接"
|
||||
"inbounds" = "入站"
|
||||
"clients" = "客户端"
|
||||
"offline" = "🔴 离线"
|
||||
"online" = "🟢 在线"
|
||||
|
|
|
@ -563,22 +563,23 @@
|
|||
[tgbot]
|
||||
"keyboardClosed" = "❌ 自定義鍵盤已關閉!"
|
||||
"noResult" = "❗ 沒有結果!"
|
||||
"noQuery" = "❌ 未找到查詢!請重新使用命令!"
|
||||
"noQuery" = "❌ 未找到查詢!請再次使用該命令!"
|
||||
"wentWrong" = "❌ 出了點問題!"
|
||||
"noIpRecord" = "❗ 沒有 IP 記錄!"
|
||||
"noInbounds" = "❗ 沒有找到入站連線!"
|
||||
"unlimited" = "♾ 無限制"
|
||||
"add" = "新增"
|
||||
"noIpRecord" = "❗ 沒有IP記錄!"
|
||||
"noInbounds" = "❗ 未找到入站!"
|
||||
"unlimited" = "♾ 無限(重置)"
|
||||
"add" = "添加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"hours" = "小時"
|
||||
"minutes" = "分鐘"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站連線"
|
||||
"inbounds" = "入站"
|
||||
"clients" = "客戶端"
|
||||
"offline" = "🔴 離線"
|
||||
"online" = "🟢 線上"
|
||||
"online" = "🟢 在線"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ 未知命令"
|
||||
|
|
|
@ -229,7 +229,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
g := engine.Group(basePath)
|
||||
|
||||
s.index = controller.NewIndexController(g)
|
||||
s.server = controller.NewServerController(g)
|
||||
s.server = controller.NewMultiServerController(g)
|
||||
s.panel = controller.NewXUIController(g)
|
||||
s.api = controller.NewAPIController(g)
|
||||
|
||||
|
|
Loading…
Reference in a new issue