mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-26 18:14:50 +00:00
Compare commits
20 commits
e818cccfe3
...
2626491ccd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2626491ccd | ||
|
|
f60682a6b7 | ||
|
|
50bd7a8040 | ||
|
|
7465768ff7 | ||
|
|
edd8b12988 | ||
|
|
5b00a52c65 | ||
|
|
151f1173a1 | ||
|
|
e262132b9d | ||
|
|
ca0a7aeb5a | ||
|
|
7447cec17e | ||
|
|
0ffd27c0aa | ||
|
|
054cb1dea0 | ||
|
|
3757ae0b11 | ||
|
|
5e953bae45 | ||
|
|
747af376f2 | ||
|
|
a3ccccfe52 | ||
|
|
3299d15f28 | ||
|
|
ae82373457 | ||
|
|
d65233cc2c | ||
|
|
11dc06863e |
65 changed files with 882 additions and 261 deletions
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
**3X-UI** — لوحة تحكم متقدمة مفتوحة المصدر تعتمد على الويب مصممة لإدارة خادم Xray-core. توفر واجهة سهلة الاستخدام لتكوين ومراقبة بروتوكولات VPN والوكيل المختلفة.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
**3X-UI** — panel de control avanzado basado en web de código abierto diseñado para gestionar el servidor Xray-core. Ofrece una interfaz fácil de usar para configurar y monitorear varios protocolos VPN y proxy.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
**3X-UI** — یک پنل کنترل پیشرفته مبتنی بر وب با کد باز که برای مدیریت سرور Xray-core طراحی شده است. این پنل یک رابط کاربری آسان برای پیکربندی و نظارت بر پروتکلهای مختلف VPN و پراکسی ارائه میدهد.
|
||||||
|
|
||||||
|
|
|
||||||
12
README.md
12
README.md
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**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.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
**3X-UI** — продвинутая панель управления с открытым исходным кодом на основе веб-интерфейса, разработанная для управления сервером Xray-core. Предоставляет удобный интерфейс для настройки и мониторинга различных VPN и прокси-протоколов.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,13 @@
|
||||||
</picture>
|
</picture>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||||
[](https://github.com/MHSanaei/3x-ui/actions)
|
[](https://github.com/MHSanaei/3x-ui/actions)
|
||||||
[](#)
|
[](#)
|
||||||
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
[](https://github.com/MHSanaei/3x-ui/releases/latest)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
[](https://pkg.go.dev/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
[](https://goreportcard.com/report/github.com/mhsanaei/3x-ui/v2)
|
||||||
|
|
||||||
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
**3X-UI** — 一个基于网页的高级开源控制面板,专为管理 Xray-core 服务器而设计。它提供了用户友好的界面,用于配置和监控各种 VPN 和代理协议。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.8.1
|
2.8.2
|
||||||
|
|
@ -9,10 +9,10 @@ import (
|
||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
@ -35,6 +35,7 @@ func initModels() error {
|
||||||
&model.InboundClientIps{},
|
&model.InboundClientIps{},
|
||||||
&xray.ClientTraffic{},
|
&xray.ClientTraffic{},
|
||||||
&model.HistoryOfSeeders{},
|
&model.HistoryOfSeeders{},
|
||||||
|
&model.Server{},
|
||||||
}
|
}
|
||||||
for _, model := range models {
|
for _, model := range models {
|
||||||
if err := db.AutoMigrate(model); err != nil {
|
if err := db.AutoMigrate(model); err != nil {
|
||||||
|
|
@ -141,6 +142,9 @@ func InitDB(dbPath string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
isUsersEmpty, err := isTableEmpty("users")
|
isUsersEmpty, err := isTableEmpty("users")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := initUser(); err != nil {
|
if err := initUser(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,8 @@ package model
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|
@ -115,3 +115,12 @@ type VLESSSettings struct {
|
||||||
Encryption string `json:"encryption"`
|
Encryption string `json:"encryption"`
|
||||||
Fallbacks []any `json:"fallbacks"`
|
Fallbacks []any `json:"fallbacks"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -1,4 +1,4 @@
|
||||||
module x-ui
|
module github.com/mhsanaei/3x-ui/v2
|
||||||
|
|
||||||
go 1.25.1
|
go 1.25.1
|
||||||
|
|
||||||
|
|
@ -64,6 +64,7 @@ require (
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/quic-go/qpack v0.5.1 // indirect
|
github.com/quic-go/qpack v0.5.1 // indirect
|
||||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,13 @@ config_after_install() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/usr/local/x-ui/x-ui migrate
|
/usr/local/x-ui/x-ui migrate
|
||||||
|
|
||||||
|
local existing_apiKey=$(/usr/local/x-ui/x-ui setting -show true | grep -oP 'ApiKey: \K.*')
|
||||||
|
if [[ -z "$existing_apiKey" ]]; then
|
||||||
|
local config_apiKey=$(gen_random_string 32)
|
||||||
|
/usr/local/x-ui/x-ui setting -apiKey "${config_apiKey}"
|
||||||
|
echo -e "${green}Generated random API Key: ${config_apiKey}${plain}"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
install_x-ui() {
|
install_x-ui() {
|
||||||
|
|
|
||||||
31
main.go
31
main.go
|
|
@ -9,14 +9,14 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/sub"
|
"github.com/mhsanaei/3x-ui/v2/sub"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"x-ui/web"
|
"github.com/mhsanaei/3x-ui/v2/web"
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
|
|
@ -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())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Database initialization failed:", err)
|
fmt.Println("Database initialization failed:", err)
|
||||||
|
|
@ -242,6 +242,15 @@ func updateSetting(port int, username string, password string, webBasePath strin
|
||||||
settingService := service.SettingService{}
|
settingService := service.SettingService{}
|
||||||
userService := service.UserService{}
|
userService := service.UserService{}
|
||||||
|
|
||||||
|
if apiKey != "" {
|
||||||
|
err := settingService.SetAPIKey(apiKey)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to set API Key:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("API Key set successfully: %v\n", apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if port > 0 {
|
if port > 0 {
|
||||||
err := settingService.SetPort(port)
|
err := settingService.SetPort(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -388,9 +397,11 @@ func main() {
|
||||||
var show bool
|
var show bool
|
||||||
var getCert bool
|
var getCert bool
|
||||||
var resetTwoFactor bool
|
var resetTwoFactor bool
|
||||||
|
var apiKey string
|
||||||
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
settingCmd.BoolVar(&reset, "reset", false, "Reset all settings")
|
||||||
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
settingCmd.BoolVar(&show, "show", false, "Display current settings")
|
||||||
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
settingCmd.IntVar(&port, "port", 0, "Set panel port number")
|
||||||
|
settingCmd.StringVar(&apiKey, "apiKey", "", "Set API Key")
|
||||||
settingCmd.StringVar(&username, "username", "", "Set login username")
|
settingCmd.StringVar(&username, "username", "", "Set login username")
|
||||||
settingCmd.StringVar(&password, "password", "", "Set login password")
|
settingCmd.StringVar(&password, "password", "", "Set login password")
|
||||||
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
settingCmd.StringVar(&webBasePath, "webBasePath", "", "Set base path for Panel")
|
||||||
|
|
@ -440,7 +451,7 @@ func main() {
|
||||||
if reset {
|
if reset {
|
||||||
resetSetting()
|
resetSetting()
|
||||||
} else {
|
} else {
|
||||||
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor)
|
updateSetting(port, username, password, webBasePath, listenIP, resetTwoFactor, apiKey)
|
||||||
}
|
}
|
||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
|
|
|
||||||
14
sub/sub.go
14
sub/sub.go
|
|
@ -13,13 +13,13 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
webpkg "x-ui/web"
|
webpkg "github.com/mhsanaei/3x-ui/v2/web"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||||
"x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"x-ui/config"
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,12 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed default.json
|
//go:embed default.json
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,13 @@ import (
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubService struct {
|
type SubService struct {
|
||||||
|
|
@ -159,26 +159,43 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) getLink(inbound *model.Inbound, email string) string {
|
||||||
switch inbound.Protocol {
|
serverService := service.MultiServerService{}
|
||||||
case "vmess":
|
servers, err := serverService.GetServers()
|
||||||
return s.genVmessLink(inbound, email)
|
if err != nil {
|
||||||
case "vless":
|
logger.Warning("Failed to get servers for subscription:", err)
|
||||||
return s.genVlessLink(inbound, email)
|
return ""
|
||||||
case "trojan":
|
|
||||||
return s.genTrojanLink(inbound, email)
|
|
||||||
case "shadowsocks":
|
|
||||||
return s.genShadowsocksLink(inbound, email)
|
|
||||||
}
|
}
|
||||||
return ""
|
|
||||||
|
var links []string
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var link string
|
||||||
|
switch inbound.Protocol {
|
||||||
|
case "vmess":
|
||||||
|
link = s.genVmessLink(inbound, email, server)
|
||||||
|
case "vless":
|
||||||
|
link = s.genVlessLink(inbound, email, server)
|
||||||
|
case "trojan":
|
||||||
|
link = s.genTrojanLink(inbound, email, server)
|
||||||
|
case "shadowsocks":
|
||||||
|
link = s.genShadowsocksLink(inbound, email, server)
|
||||||
|
}
|
||||||
|
if link != "" {
|
||||||
|
links = append(links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(links, "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVmessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
if inbound.Protocol != model.VMESS {
|
if inbound.Protocol != model.VMESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
obj := map[string]any{
|
obj := map[string]any{
|
||||||
"v": "2",
|
"v": "2",
|
||||||
"add": s.address,
|
"add": server.Address,
|
||||||
"port": inbound.Port,
|
"port": inbound.Port,
|
||||||
"type": "none",
|
"type": "none",
|
||||||
}
|
}
|
||||||
|
|
@ -291,7 +308,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
newObj[key] = value
|
newObj[key] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
newObj["add"] = ep["dest"].(string)
|
newObj["add"] = ep["dest"].(string)
|
||||||
newObj["port"] = int(ep["port"].(float64))
|
newObj["port"] = int(ep["port"].(float64))
|
||||||
|
|
||||||
|
|
@ -307,14 +324,14 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
|
|
||||||
obj["ps"] = s.genRemark(inbound, email, "")
|
obj["ps"] = s.genRemark(inbound, email, "", server.Name)
|
||||||
|
|
||||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||||
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genVlessLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -493,7 +510,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -514,12 +531,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Trojan {
|
if inbound.Protocol != model.Trojan {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -688,7 +705,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -710,12 +727,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, server *model.Server) string {
|
||||||
address := s.address
|
address := server.Address
|
||||||
if inbound.Protocol != model.Shadowsocks {
|
if inbound.Protocol != model.Shadowsocks {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
@ -855,7 +872,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string), server.Name)
|
||||||
|
|
||||||
if index > 0 {
|
if index > 0 {
|
||||||
links += "\n"
|
links += "\n"
|
||||||
|
|
@ -876,17 +893,18 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||||
// Set the new query values on the URL
|
// Set the new query values on the URL
|
||||||
url.RawQuery = q.Encode()
|
url.RawQuery = q.Encode()
|
||||||
|
|
||||||
url.Fragment = s.genRemark(inbound, email, "")
|
url.Fragment = s.genRemark(inbound, email, "", server.Name)
|
||||||
return url.String()
|
return url.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string, serverName string) string {
|
||||||
separationChar := string(s.remarkModel[0])
|
separationChar := string(s.remarkModel[0])
|
||||||
orderChars := s.remarkModel[1:]
|
orderChars := s.remarkModel[1:]
|
||||||
orders := map[byte]string{
|
orders := map[byte]string{
|
||||||
'i': "",
|
'i': "",
|
||||||
'e': "",
|
'e': "",
|
||||||
'o': "",
|
'o': "",
|
||||||
|
's': "",
|
||||||
}
|
}
|
||||||
if len(email) > 0 {
|
if len(email) > 0 {
|
||||||
orders['e'] = email
|
orders['e'] = email
|
||||||
|
|
@ -897,6 +915,9 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||||
if len(extra) > 0 {
|
if len(extra) > 0 {
|
||||||
orders['o'] = extra
|
orders['o'] = extra
|
||||||
}
|
}
|
||||||
|
if len(serverName) > 0 {
|
||||||
|
orders['s'] = serverName
|
||||||
|
}
|
||||||
|
|
||||||
var remark []string
|
var remark []string
|
||||||
for i := 0; i < len(orderChars); i++ {
|
for i := 0; i < len(orderChars); i++ {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewErrorf(format string, a ...any) error {
|
func NewErrorf(format string, a ...any) error {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ package controller
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,10 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"x-ui/database/model"
|
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -108,8 +109,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := false
|
inbound, needRestart, err := a.inboundService.AddInbound(inbound)
|
||||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -126,8 +126,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundDeleteSuccess"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.DelInbound(id)
|
||||||
needRestart, err = a.inboundService.DelInbound(id)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -152,8 +151,7 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundUpdateSuccess"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
needRestart := true
|
inbound, needRestart, err := a.inboundService.UpdateInbound(inbound)
|
||||||
inbound, needRestart, err = a.inboundService.UpdateInbound(inbound)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -195,9 +193,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.AddInboundClient(data)
|
||||||
|
|
||||||
needRestart, err = a.inboundService.AddInboundClient(data)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -216,9 +212,7 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
|
||||||
}
|
}
|
||||||
clientId := c.Param("clientId")
|
clientId := c.Param("clientId")
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.DelInboundClient(id, clientId)
|
||||||
|
|
||||||
needRestart, err = a.inboundService.DelInboundClient(id, clientId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
@ -239,9 +233,7 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
needRestart := true
|
needRestart, err := a.inboundService.UpdateInboundClient(inbound, clientId)
|
||||||
|
|
||||||
needRestart, err = a.inboundService.UpdateInboundClient(inbound, clientId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,18 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginForm struct {
|
type LoginForm struct {
|
||||||
Username string `json:"username" form:"username"`
|
Username string `json:"username" form:"username"`
|
||||||
Password string `json:"password" form:"password"`
|
Password string `json:"password" form:"password"`
|
||||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexController struct {
|
type IndexController struct {
|
||||||
|
|
|
||||||
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)
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,8 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
|
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.GET("/inbounds", a.inbounds)
|
g.GET("/inbounds", a.inbounds)
|
||||||
|
g.GET("/servers", a.servers)
|
||||||
g.GET("/settings", a.settings)
|
g.GET("/settings", a.settings)
|
||||||
g.GET("/xray", a.xraySettings)
|
g.GET("/xray", a.xraySettings)
|
||||||
|
|
||||||
|
|
@ -49,3 +50,7 @@ func (a *XUIController) settings(c *gin.Context) {
|
||||||
func (a *XUIController) xraySettings(c *gin.Context) {
|
func (a *XUIController) xraySettings(c *gin.Context) {
|
||||||
html(c, "xray.html", "pages.xray.title", nil)
|
html(c, "xray.html", "pages.xray.title", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *XUIController) servers(c *gin.Context) {
|
||||||
|
html(c, "servers.html", "Servers", nil)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,11 @@
|
||||||
icon: 'user',
|
icon: 'user',
|
||||||
title: '{{ i18n "menu.inbounds"}}'
|
title: '{{ i18n "menu.inbounds"}}'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: '{{ .base_path }}panel/servers',
|
||||||
|
icon: 'cloud-server',
|
||||||
|
title: 'Servers'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: '{{ .base_path }}panel/settings',
|
key: '{{ .base_path }}panel/settings',
|
||||||
icon: 'setting',
|
icon: 'setting',
|
||||||
|
|
|
||||||
|
|
@ -431,12 +431,12 @@
|
||||||
CPU History
|
CPU History
|
||||||
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
|
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
|
||||||
@change="fetchCpuHistoryBucket">
|
@change="fetchCpuHistoryBucket">
|
||||||
<a-select-option :value="2">2s</a-select-option>
|
<a-select-option :value="2">2m</a-select-option>
|
||||||
<a-select-option :value="30">30s</a-select-option>
|
<a-select-option :value="30">30m</a-select-option>
|
||||||
<a-select-option :value="60">1m</a-select-option>
|
<a-select-option :value="60">1h</a-select-option>
|
||||||
<a-select-option :value="120">2m</a-select-option>
|
<a-select-option :value="120">2h</a-select-option>
|
||||||
<a-select-option :value="180">3m</a-select-option>
|
<a-select-option :value="180">3h</a-select-option>
|
||||||
<a-select-option :value="300">5m</a-select-option>
|
<a-select-option :value="300">5h</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<div style="padding:16px">
|
<div style="padding:16px">
|
||||||
|
|
@ -1124,4 +1124,4 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{{ template "page/body_end" .}}
|
{{ template "page/body_end" .}}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,29 @@
|
||||||
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
:mask-closable="false" :footer="null" :class="themeSwitcher.currentTheme">
|
||||||
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
<a-list class="ant-dns-presets-list" bordered :style="{ width: '100%' }">
|
||||||
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
|
||||||
<a-row justify="space-between" align="middle">
|
<div class="ant-dns-presets-line">
|
||||||
<a-col :span="12">
|
<a-space direction="horizontal" size="small" align="center">
|
||||||
<a-space direction="vertical" size="small">
|
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
|
||||||
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
|
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
|
||||||
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
|
</a-space>
|
||||||
</a-space>
|
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
||||||
</a-col>
|
</div>
|
||||||
<a-col :span="12" :style="{ textAlign: 'right' }">
|
|
||||||
<a-button type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
|
|
||||||
</a-col>
|
|
||||||
</a-row>
|
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.ant-dns-presets-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-dns-presets-install {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.dark .ant-dns-presets-list {
|
.dark .ant-dns-presets-list {
|
||||||
border-color: var(--dark-color-stroke)
|
border-color: var(--dark-color-stroke)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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" .}}
|
||||||
|
|
@ -48,7 +48,13 @@
|
||||||
<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 type="text" v-model="allSetting.subPath"></a-input>
|
<a-input
|
||||||
|
type="text"
|
||||||
|
v-model="allSetting.subPath"
|
||||||
|
@input="allSetting.subPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||||
|
@blur="allSetting.subPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subPath)"
|
||||||
|
placeholder="/sub/"
|
||||||
|
></a-input>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
|
|
@ -102,4 +108,4 @@
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,13 @@
|
||||||
<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 type="text" v-model="allSetting.subJsonPath"></a-input>
|
<a-input
|
||||||
|
type="text"
|
||||||
|
v-model="allSetting.subJsonPath"
|
||||||
|
@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)"
|
||||||
|
placeholder="/json/"
|
||||||
|
></a-input>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,10 @@ import (
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckClientIpJob struct {
|
type CheckClientIpJob struct {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckHashStorageJob struct {
|
type CheckHashStorageJob struct {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckXrayRunningJob struct {
|
type CheckXrayRunningJob struct {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClearLogsJob struct{}
|
type ClearLogsJob struct{}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Period string
|
type Period string
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,10 @@ package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
|
@ -49,10 +49,10 @@ func InitLocalizer(i18nFS embed.FS, settingService SettingService) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTemplateData(params []string, seperator ...string) map[string]any {
|
func createTemplateData(params []string, separator ...string) map[string]any {
|
||||||
var sep string = "=="
|
var sep string = "=="
|
||||||
if len(seperator) > 0 {
|
if len(separator) > 0 {
|
||||||
sep = seperator[0]
|
sep = separator[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
templateData := make(map[string]any)
|
templateData := make(map[string]any)
|
||||||
|
|
|
||||||
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,18 +1,21 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
@ -617,6 +620,11 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||||
}
|
}
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", "/panel/inbound/api/addClient", bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -705,6 +713,11 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
|
||||||
s.xrayApi.Close()
|
s.xrayApi.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/%d/delClient/%s", inboundId, clientId), nil)
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -880,6 +893,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||||
logger.Debug("Client old email not found")
|
logger.Debug("Client old email not found")
|
||||||
needRestart = true
|
needRestart = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
body, _ := json.Marshal(data)
|
||||||
|
s.syncWithSlaves("POST", fmt.Sprintf("/panel/inbound/api/updateClient/%s", clientId), bytes.NewReader(body))
|
||||||
|
}
|
||||||
|
|
||||||
return needRestart, tx.Save(oldInbound).Error
|
return needRestart, tx.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1953,7 +1972,6 @@ func (s *InboundService) GetClientTrafficByEmail(email string) (traffic *xray.Cl
|
||||||
if t != nil && client != nil {
|
if t != nil && client != nil {
|
||||||
t.Enable = client.Enable
|
t.Enable = client.Enable
|
||||||
t.SubId = client.SubID
|
t.SubId = client.SubID
|
||||||
t.UUID = client.ID
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
@ -1995,7 +2013,6 @@ func (s *InboundService) GetClientTrafficByID(id string) ([]xray.ClientTraffic,
|
||||||
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
if ct, client, e := s.GetClientByEmail(traffics[i].Email); e == nil && ct != nil && client != nil {
|
||||||
traffics[i].Enable = client.Enable
|
traffics[i].Enable = client.Enable
|
||||||
traffics[i].SubId = client.SubID
|
traffics[i].SubId = client.SubID
|
||||||
traffics[i].UUID = client.ID
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return traffics, err
|
return traffics, err
|
||||||
|
|
@ -2093,6 +2110,9 @@ func (s *InboundService) MigrationRequirements() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tx.Commit()
|
tx.Commit()
|
||||||
|
if dbErr := db.Exec(`VACUUM "main"`).Error; dbErr != nil {
|
||||||
|
logger.Warningf("VACUUM failed: %v", dbErr)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
}
|
}
|
||||||
|
|
@ -2295,6 +2315,44 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
||||||
|
|
||||||
return validEmails, extraEmails, nil
|
return validEmails, extraEmails, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) syncWithSlaves(method string, path string, body io.Reader) {
|
||||||
|
serverService := MultiServerService{}
|
||||||
|
servers, err := serverService.GetServers()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Failed to get servers for syncing:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, server := range servers {
|
||||||
|
if !server.Enable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("http://%s:%d%s", server.Address, server.Port, path)
|
||||||
|
req, err := http.NewRequest(method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to create request for server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Api-Key", server.APIKey)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warningf("Failed to send request to server %s: %v", server.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||||
|
logger.Warningf("Failed to sync with server %s. Status: %s, Body: %s", server.Name, resp.Status, string(bodyBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||||
oldInbound, err := s.GetInbound(inboundId)
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -2386,4 +2444,5 @@ func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (b
|
||||||
}
|
}
|
||||||
|
|
||||||
return needRestart, db.Save(oldInbound).Error
|
return needRestart, db.Save(oldInbound).Error
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
72
web/service/inbound_service_sync_test.go
Normal file
72
web/service/inbound_service_sync_test.go
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInboundServiceSync(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
// Mock server to simulate a slave
|
||||||
|
var receivedApiKey string
|
||||||
|
var receivedBody []byte
|
||||||
|
mockSlave := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
receivedApiKey = r.Header.Get("Api-Key")
|
||||||
|
receivedBody, _ = io.ReadAll(r.Body)
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}))
|
||||||
|
defer mockSlave.Close()
|
||||||
|
|
||||||
|
// Add the mock slave to the database
|
||||||
|
multiServerService := MultiServerService{}
|
||||||
|
mockSlaveURL, _ := url.Parse(mockSlave.URL)
|
||||||
|
mockSlavePort, _ := strconv.Atoi(mockSlaveURL.Port())
|
||||||
|
slaveServer := &model.Server{
|
||||||
|
Name: "mock-slave",
|
||||||
|
Address: mockSlaveURL.Hostname(),
|
||||||
|
Port: mockSlavePort,
|
||||||
|
APIKey: "slave-api-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
multiServerService.AddServer(slaveServer)
|
||||||
|
|
||||||
|
// Create a test inbound and client
|
||||||
|
inboundService := InboundService{}
|
||||||
|
db := database.GetDB()
|
||||||
|
testInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Remark: "test-inbound",
|
||||||
|
Enable: true,
|
||||||
|
Settings: `{"clients":[]}`,
|
||||||
|
}
|
||||||
|
db.Create(testInbound)
|
||||||
|
|
||||||
|
clientData := model.Client{
|
||||||
|
Email: "test@example.com",
|
||||||
|
ID: "test-id",
|
||||||
|
}
|
||||||
|
clientBytes, _ := json.Marshal([]model.Client{clientData})
|
||||||
|
inboundData := &model.Inbound{
|
||||||
|
Id: testInbound.Id,
|
||||||
|
Settings: string(clientBytes),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test AddInboundClient sync
|
||||||
|
inboundService.AddInboundClient(inboundData)
|
||||||
|
|
||||||
|
assert.Equal(t, "slave-api-key", receivedApiKey)
|
||||||
|
var receivedInbound model.Inbound
|
||||||
|
json.Unmarshal(receivedBody, &receivedInbound)
|
||||||
|
assert.Equal(t, 1, receivedInbound.Id)
|
||||||
|
}
|
||||||
37
web/service/multi_server_service.go
Normal file
37
web/service/multi_server_service.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MultiServerService struct{}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServers() ([]*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var servers []*model.Server
|
||||||
|
err := db.Find(&servers).Error
|
||||||
|
return servers, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) GetServer(id int) (*model.Server, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var server model.Server
|
||||||
|
err := db.First(&server, id).Error
|
||||||
|
return &server, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) AddServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Create(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) UpdateServer(server *model.Server) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Save(server).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *MultiServerService) DeleteServer(id int) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Delete(&model.Server{}, id).Error
|
||||||
|
}
|
||||||
63
web/service/multi_server_service_test.go
Normal file
63
web/service/multi_server_service_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"x-ui/database"
|
||||||
|
"x-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
dbPath := "test.db"
|
||||||
|
os.Remove(dbPath)
|
||||||
|
database.InitDB(dbPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func teardown() {
|
||||||
|
db, _ := database.GetDB().DB()
|
||||||
|
db.Close()
|
||||||
|
os.Remove("test.db")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiServerService(t *testing.T) {
|
||||||
|
setup()
|
||||||
|
defer teardown()
|
||||||
|
|
||||||
|
service := MultiServerService{}
|
||||||
|
|
||||||
|
// Test AddServer
|
||||||
|
server := &model.Server{
|
||||||
|
Name: "test-server",
|
||||||
|
Address: "127.0.0.1",
|
||||||
|
Port: 54321,
|
||||||
|
APIKey: "test-key",
|
||||||
|
Enable: true,
|
||||||
|
}
|
||||||
|
err := service.AddServer(server)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Test GetServer
|
||||||
|
retrievedServer, err := service.GetServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, server.Name, retrievedServer.Name)
|
||||||
|
|
||||||
|
// Test GetServers
|
||||||
|
servers, err := service.GetServers()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, servers, 1)
|
||||||
|
|
||||||
|
// Test UpdateServer
|
||||||
|
retrievedServer.Name = "updated-server"
|
||||||
|
err = service.UpdateServer(retrievedServer)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
updatedServer, _ := service.GetServer(server.Id)
|
||||||
|
assert.Equal(t, "updated-server", updatedServer.Name)
|
||||||
|
|
||||||
|
// Test DeleteServer
|
||||||
|
err = service.DeleteServer(server.Id)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = service.GetServer(server.Id)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PanelService struct{}
|
type PanelService struct{}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/util/sys"
|
"github.com/mhsanaei/3x-ui/v2/util/sys"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
|
|
||||||
|
|
@ -10,14 +10,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/v2/util/random"
|
||||||
"x-ui/util/reflect_util"
|
"github.com/mhsanaei/3x-ui/v2/util/reflect_util"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed config.json
|
//go:embed config.json
|
||||||
|
|
@ -181,6 +181,21 @@ func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||||
return setting, nil
|
return setting, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetAPIKey() (string, error) {
|
||||||
|
setting, err := s.getSetting("ApiKey")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if setting == nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetAPIKey(apiKey string) error {
|
||||||
|
return s.saveSetting("ApiKey", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) saveSetting(key string, value string) error {
|
func (s *SettingService) saveSetting(key string, value string) error {
|
||||||
setting, err := s.getSetting(key)
|
setting, err := s.getSetting(key)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
|
||||||
|
|
@ -18,14 +18,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/v2/web/global"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
|
|
@ -856,7 +856,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
if len(dataArray) == 3 {
|
if len(dataArray) == 3 {
|
||||||
days, err := strconv.Atoi(dataArray[2])
|
days, err := strconv.Atoi(dataArray[2])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
var date int64 = 0
|
var date int64
|
||||||
if days > 0 {
|
if days > 0 {
|
||||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -960,7 +960,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
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.Atoi(dataArray[1])
|
||||||
var date int64 = 0
|
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 {
|
||||||
date = -int64(days * 24 * 60 * 60000)
|
date = -int64(days * 24 * 60 * 60000)
|
||||||
|
|
@ -1581,23 +1581,6 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
)
|
)
|
||||||
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
|
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
|
||||||
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
||||||
default:
|
|
||||||
// dynamic callbacks
|
|
||||||
if strings.HasPrefix(callbackQuery.Data, "client_sub_links ") {
|
|
||||||
email := strings.TrimPrefix(callbackQuery.Data, "client_sub_links ")
|
|
||||||
t.sendClientSubLinks(chatId, email)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(callbackQuery.Data, "client_individual_links ") {
|
|
||||||
email := strings.TrimPrefix(callbackQuery.Data, "client_individual_links ")
|
|
||||||
t.sendClientIndividualLinks(chatId, email)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(callbackQuery.Data, "client_qr_links ") {
|
|
||||||
email := strings.TrimPrefix(callbackQuery.Data, "client_qr_links ")
|
|
||||||
t.sendClientQRLinks(chatId, email)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
case "add_client_ch_default_traffic":
|
case "add_client_ch_default_traffic":
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
|
|
@ -1813,6 +1796,22 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||||
t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_sub_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientSubLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_individual_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientIndividualLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_qr_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientQRLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,10 @@ package service
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/v2/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||||
|
|
||||||
"github.com/xlzd/gotp"
|
"github.com/xlzd/gotp"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WarpService struct {
|
type WarpService struct {
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type XraySettingService struct {
|
type XraySettingService struct {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
|
||||||
20
web/web.go
20
web/web.go
|
|
@ -14,15 +14,15 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
"x-ui/web/controller"
|
"github.com/mhsanaei/3x-ui/v2/web/controller"
|
||||||
"x-ui/web/job"
|
"github.com/mhsanaei/3x-ui/v2/web/job"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/v2/web/locale"
|
||||||
"x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||||
"x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/v2/web/network"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
|
|
@ -252,7 +252,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g := engine.Group(basePath)
|
g := engine.Group(basePath)
|
||||||
|
|
||||||
s.index = controller.NewIndexController(g)
|
s.index = controller.NewIndexController(g)
|
||||||
s.server = controller.NewServerController(g)
|
s.server = controller.NewMultiServerController(g)
|
||||||
s.panel = controller.NewXUIController(g)
|
s.panel = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,12 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
"math"
|
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/app/proxyman/command"
|
"github.com/xtls/xray-core/app/proxyman/command"
|
||||||
statsService "github.com/xtls/xray-core/app/stats/command"
|
statsService "github.com/xtls/xray-core/app/stats/command"
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ type ClientTraffic struct {
|
||||||
InboundId int `json:"inboundId" form:"inboundId"`
|
InboundId int `json:"inboundId" form:"inboundId"`
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"`
|
||||||
Email string `json:"email" form:"email" gorm:"unique"`
|
Email string `json:"email" form:"email" gorm:"unique"`
|
||||||
UUID string `json:"uuid" form:"uuid" gorm:"unique;type:char(36)"`
|
|
||||||
SubId string `json:"subId" form:"subId" gorm:"-"`
|
SubId string `json:"subId" form:"subId" gorm:"-"`
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"`
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package xray
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package xray
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/v2/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InboundConfig struct {
|
type InboundConfig struct {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLogWriter() *LogWriter {
|
func NewLogWriter() *LogWriter {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/v2/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/v2/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetBinaryName() string {
|
func GetBinaryName() string {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue