mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-04-20 05:52:24 +00:00
Merge pull request #545 from hamid-gh98/main
🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
This commit is contained in:
commit
94fad02737
23 changed files with 274 additions and 206 deletions
33
README.md
33
README.md
|
@ -9,7 +9,6 @@
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||||
|
|
||||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||||
|
|
||||||
**Buy Me a Coffee :**
|
**Buy Me a Coffee :**
|
||||||
|
@ -24,11 +23,12 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||||
|
|
||||||
# Install custom version
|
# Install custom version
|
||||||
|
|
||||||
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.0`:
|
To install your desired version you can add the version to the end of install command. Example for ver `v1.6.1`:
|
||||||
|
|
||||||
```
|
```
|
||||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.0
|
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.1
|
||||||
```
|
```
|
||||||
|
|
||||||
# SSL
|
# SSL
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -37,7 +37,7 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
|
||||||
certbot renew --dry-run
|
certbot renew --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
or you can use x-ui menu then number `16` (`SSL Certificate Management`)
|
You also can use `x-ui` menu then select `16. SSL Certificate Management`
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
|
@ -57,23 +57,26 @@ or you can use x-ui menu then number `16` (`SSL Certificate Management`)
|
||||||
- Support export/import database from panel
|
- Support export/import database from panel
|
||||||
|
|
||||||
# Manual Install & Upgrade
|
# Manual Install & Upgrade
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click for Manual Install details</summary>
|
<summary>Click for Manual Install details</summary>
|
||||||
|
|
||||||
1. To download the latest version of the compressed package directly to your server, run the following command:
|
1. To download the latest version of the compressed package directly to your server, run the following command:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-amd64.tar.gz
|
ARCH=$(uname -m)
|
||||||
|
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
|
||||||
|
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: If your server's CPU architecture is `arm64`, modify the URL by substituting `amd64` with your respective CPU architecture.
|
|
||||||
|
|
||||||
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
|
2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
|
||||||
cd /root/
|
cd /root/
|
||||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||||
tar zxvf x-ui-linux-amd64.tar.gz
|
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||||
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
|
||||||
cp x-ui/x-ui.sh /usr/bin/x-ui
|
cp x-ui/x-ui.sh /usr/bin/x-ui
|
||||||
cp -f x-ui/x-ui.service /etc/systemd/system/
|
cp -f x-ui/x-ui.service /etc/systemd/system/
|
||||||
|
@ -82,14 +85,16 @@ systemctl daemon-reload
|
||||||
systemctl enable x-ui
|
systemctl enable x-ui
|
||||||
systemctl restart x-ui
|
systemctl restart x-ui
|
||||||
```
|
```
|
||||||
Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar zxvf x-ui-linux-amd64.tar.gz` with your respective CPU architecture.
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# Install with Docker
|
# Install with Docker
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click for Docker details</summary>
|
<summary>Click for Docker details</summary>
|
||||||
|
|
||||||
1. Install Docker:
|
1. Install Docker:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
bash <(curl -sSL https://get.docker.com)
|
bash <(curl -sSL https://get.docker.com)
|
||||||
```
|
```
|
||||||
|
@ -119,9 +124,11 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z
|
||||||
--name 3x-ui \
|
--name 3x-ui \
|
||||||
ghcr.io/mhsanaei/3x-ui:latest
|
ghcr.io/mhsanaei/3x-ui:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# Default settings
|
# Default settings
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click for Default settings details</summary>
|
<summary>Click for Default settings details</summary>
|
||||||
|
|
||||||
|
@ -171,7 +178,7 @@ If you want to use routing to WARP follow steps as below:
|
||||||
2. Install WARP on **socks proxy mode**:
|
2. Install WARP on **socks proxy mode**:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
|
bash <(curl -sSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
|
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
|
||||||
|
@ -216,16 +223,18 @@ Reference syntax:
|
||||||
- CPU threshold notification
|
- CPU threshold notification
|
||||||
- Threshold for Expiration time and Traffic to report in advance
|
- Threshold for Expiration time and Traffic to report in advance
|
||||||
- Support client report menu if client's telegram username added to the user's configurations
|
- Support client report menu if client's telegram username added to the user's configurations
|
||||||
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||||
- Menu based bot
|
- Menu based bot
|
||||||
- Search client by email ( only admin )
|
- Search client by email ( only admin )
|
||||||
- Check all inbounds
|
- Check all inbounds
|
||||||
- Check server status
|
- Check server status
|
||||||
- Check depleted users
|
- Check depleted users
|
||||||
- Receive backup by request and in periodic reports
|
- Receive backup by request and in periodic reports
|
||||||
|
- Multi language bot
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# API routes
|
# API routes
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click for API routes details</summary>
|
<summary>Click for API routes details</summary>
|
||||||
|
|
||||||
|
@ -261,6 +270,7 @@ Reference syntax:
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# Environment Variables
|
# Environment Variables
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Click for Environment Variables details</summary>
|
<summary>Click for Environment Variables details</summary>
|
||||||
|
|
||||||
|
@ -276,6 +286,7 @@ Example:
|
||||||
```sh
|
```sh
|
||||||
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
# A Special Thanks To
|
# A Special Thanks To
|
||||||
|
|
17
sub/sub.go
17
sub/sub.go
|
@ -7,10 +7,10 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"x-ui/config"
|
"x-ui/config"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/util/common"
|
"x-ui/util/common"
|
||||||
|
"x-ui/web/middleware"
|
||||||
"x-ui/web/network"
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if subDomain != "" {
|
if subDomain != "" {
|
||||||
validateDomain := func(c *gin.Context) {
|
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||||
host := strings.Split(c.Request.Host, ":")[0]
|
|
||||||
|
|
||||||
if host != subDomain {
|
|
||||||
c.AbortWithStatus(http.StatusForbidden)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
|
|
||||||
engine.Use(validateDomain)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g := engine.Group(subPath)
|
g := engine.Group(subPath)
|
||||||
|
@ -116,11 +105,13 @@ func (s *Server) Start() (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||||
listener, err := net.Listen("tcp", listenAddr)
|
listener, err := net.Listen("tcp", listenAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if certFile != "" || keyFile != "" {
|
if certFile != "" || keyFile != "" {
|
||||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -168,6 +168,7 @@ class AllSetting {
|
||||||
|
|
||||||
constructor(data) {
|
constructor(data) {
|
||||||
this.webListen = "";
|
this.webListen = "";
|
||||||
|
this.webDomain = "";
|
||||||
this.webPort = 2053;
|
this.webPort = 2053;
|
||||||
this.webCertFile = "";
|
this.webCertFile = "";
|
||||||
this.webKeyFile = "";
|
this.webKeyFile = "";
|
||||||
|
@ -187,7 +188,7 @@ class AllSetting {
|
||||||
this.subEnable = false;
|
this.subEnable = false;
|
||||||
this.subListen = "";
|
this.subListen = "";
|
||||||
this.subPort = "2096";
|
this.subPort = "2096";
|
||||||
this.subPath = "sub/";
|
this.subPath = "/sub/";
|
||||||
this.subDomain = "";
|
this.subDomain = "";
|
||||||
this.subCertFile = "";
|
this.subCertFile = "";
|
||||||
this.subKeyFile = "";
|
this.subKeyFile = "";
|
||||||
|
|
|
@ -135,3 +135,21 @@ function doAllItemsExist(array1, array2) {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildURL({ host, port, isTLS, base, path }) {
|
||||||
|
if (!host || host.length === 0) host = window.location.hostname;
|
||||||
|
if (!port || port.length === 0) port = window.location.port;
|
||||||
|
|
||||||
|
if (isTLS === undefined) isTLS = window.location.protocol === "https:";
|
||||||
|
|
||||||
|
const protocol = isTLS ? "https:" : "http:";
|
||||||
|
|
||||||
|
port = String(port);
|
||||||
|
if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) {
|
||||||
|
port = "";
|
||||||
|
} else {
|
||||||
|
port = `:${port}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${protocol}//${host}${port}${base}${path}`;
|
||||||
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
|
||||||
}
|
}
|
||||||
jsonObj(c, inbounds, nil)
|
jsonObj(c, inbounds, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) getInbound(c *gin.Context) {
|
func (a *InboundController) getInbound(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -168,6 +169,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Log Cleared", nil)
|
jsonMsg(c, "Log Cleared", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
func (a *InboundController) addInboundClient(c *gin.Context) {
|
||||||
data := &model.Inbound{}
|
data := &model.Inbound{}
|
||||||
err := c.ShouldBind(data)
|
err := c.ShouldBind(data)
|
||||||
|
|
|
@ -65,77 +65,42 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||||
expireDiff, err := a.settingService.GetExpireDiff()
|
type settingFunc func() (interface{}, error)
|
||||||
|
|
||||||
|
settings := map[string]settingFunc{
|
||||||
|
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
|
||||||
|
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
|
||||||
|
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
|
||||||
|
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
|
||||||
|
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
|
||||||
|
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
|
||||||
|
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
|
||||||
|
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
|
||||||
|
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
|
||||||
|
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
|
||||||
|
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
|
||||||
|
for key, fn := range settings {
|
||||||
|
value, err := fn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
trafficDiff, err := a.settingService.GetTrafficDiff()
|
result[key] = value
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defaultCert, err := a.settingService.GetCertFile()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defaultKey, err := a.settingService.GetKeyFile()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tgBotEnable, err := a.settingService.GetTgbotenabled()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subEnable, err := a.settingService.GetSubEnable()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subPort, err := a.settingService.GetSubPort()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subPath, err := a.settingService.GetSubPath()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subDomain, err := a.settingService.GetSubDomain()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subKeyFile, err := a.settingService.GetSubKeyFile()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
subCertFile, err := a.settingService.GetSubCertFile()
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subTLS := false
|
subTLS := false
|
||||||
if subKeyFile != "" || subCertFile != "" {
|
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
|
||||||
subTLS = true
|
subTLS = true
|
||||||
}
|
}
|
||||||
result := map[string]interface{}{
|
result["subTLS"] = subTLS
|
||||||
"expireDiff": expireDiff,
|
|
||||||
"trafficDiff": trafficDiff,
|
delete(result, "subKeyFile")
|
||||||
"defaultCert": defaultCert,
|
delete(result, "subCertFile")
|
||||||
"defaultKey": defaultKey,
|
|
||||||
"tgBotEnable": tgBotEnable,
|
|
||||||
"subEnable": subEnable,
|
|
||||||
"subPort": subPort,
|
|
||||||
"subPath": subPath,
|
|
||||||
"subDomain": subDomain,
|
|
||||||
"subTLS": subTLS,
|
|
||||||
}
|
|
||||||
jsonObj(c, result, nil)
|
jsonObj(c, result, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ type Pager struct {
|
||||||
|
|
||||||
type AllSetting struct {
|
type AllSetting struct {
|
||||||
WebListen string `json:"webListen" form:"webListen"`
|
WebListen string `json:"webListen" form:"webListen"`
|
||||||
|
WebDomain string `json:"webDomain" form:"webDomain"`
|
||||||
WebPort int `json:"webPort" form:"webPort"`
|
WebPort int `json:"webPort" form:"webPort"`
|
||||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||||
|
|
|
@ -18,7 +18,6 @@ type HashStorage struct {
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
Data map[string]HashEntry
|
Data map[string]HashEntry
|
||||||
Expiration time.Duration
|
Expiration time.Duration
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHashStorage(expiration time.Duration) *HashStorage {
|
func NewHashStorage(expiration time.Duration) *HashStorage {
|
||||||
|
@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
|
||||||
return md5HashString
|
return md5HashString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
func (h *HashStorage) GetValue(hash string) (string, bool) {
|
||||||
h.RLock()
|
h.RLock()
|
||||||
defer h.RUnlock()
|
defer h.RUnlock()
|
||||||
|
|
|
@ -85,16 +85,12 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
genSubLink(subID) {
|
genSubLink(subID) {
|
||||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
return buildURL({ host, port, isTLS, base, path: subID });
|
||||||
subPort = app.subSettings.port;
|
|
||||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
|
||||||
subPath = app.subSettings.path;
|
|
||||||
return protocol + hostName + port + subPath + subID;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
if (qrModal.client.subId){
|
if (qrModal.client && qrModal.client.subId) {
|
||||||
qrModal.subId = qrModal.client.subId;
|
qrModal.subId = qrModal.client.subId;
|
||||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,12 +253,8 @@
|
||||||
infoModal.visible = false;
|
infoModal.visible = false;
|
||||||
},
|
},
|
||||||
genSubLink(subID) {
|
genSubLink(subID) {
|
||||||
protocol = app.subSettings.tls ? "https://" : "http://";
|
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||||
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
|
return buildURL({ host, port, isTLS, base, path: subID });
|
||||||
subPort = app.subSettings.port;
|
|
||||||
port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort);
|
|
||||||
subPath = app.subSettings.path;
|
|
||||||
return protocol + hostName + port + subPath + subID;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@
|
||||||
set multiDomain(value) {
|
set multiDomain(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
inModal.inbound.stream.tls.server = "";
|
inModal.inbound.stream.tls.server = "";
|
||||||
inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}];
|
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
|
||||||
} else {
|
} else {
|
||||||
inModal.inbound.stream.tls.server = "";
|
inModal.inbound.stream.tls.server = "";
|
||||||
inModal.inbound.stream.tls.settings.domains = [];
|
inModal.inbound.stream.tls.settings.domains = [];
|
||||||
|
|
|
@ -311,7 +311,7 @@
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'UID', width: 120, dataIndex: "id" },
|
{ title: 'UUID', width: 120, dataIndex: "id" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const innerTrojanColumns = [
|
const innerTrojanColumns = [
|
||||||
|
|
|
@ -91,6 +91,7 @@
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
|
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||||
|
@ -306,23 +307,37 @@
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'>
|
</a-collapse>
|
||||||
|
</a-tab-pane>
|
||||||
|
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
|
||||||
<a-row :xs="24" :sm="24" :lg="12">
|
<a-row :xs="24" :sm="24" :lg="12">
|
||||||
<h2 class="collapse-title">
|
<h2 class="collapse-title">
|
||||||
<a-icon type="warning"></a-icon>
|
<a-icon type="warning"></a-icon>
|
||||||
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
{{ i18n "pages.settings.templates.manualListsDesc" }}
|
||||||
</h2>
|
</h2>
|
||||||
</a-row>
|
</a-row>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
|
<a-collapse>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
|
<setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
|
</a-collapse-panel>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item>
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item>
|
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
|
||||||
|
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
|
||||||
|
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
|
||||||
|
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
|
||||||
|
</a-collapse-panel>
|
||||||
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
|
||||||
|
<setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
|
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
|
||||||
<a-collapse>
|
<a-collapse>
|
||||||
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
|
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
|
||||||
|
@ -335,7 +350,7 @@
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
|
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
@ -391,9 +406,9 @@
|
||||||
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
|
||||||
|
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
|
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||||
|
@ -522,7 +537,7 @@
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.user = {};
|
this.user = {};
|
||||||
window.location.replace(basePath + "logout")
|
window.location.replace(basePath + "logout");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async restartPanel() {
|
async restartPanel() {
|
||||||
|
@ -541,12 +556,10 @@
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
await PromiseUtil.sleep(5000);
|
await PromiseUtil.sleep(5000);
|
||||||
let protocol = "http://";
|
const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
|
||||||
if (this.allSetting.webCertFile !== "") {
|
const isTLS = webCertFile !== "" || webKeyFile !== "";
|
||||||
protocol = "https://";
|
const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
|
||||||
}
|
window.location.replace(url);
|
||||||
const { host } = window.location;
|
|
||||||
window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchUserSecret() {
|
async fetchUserSecret() {
|
||||||
|
|
21
web/middleware/domainValidator.go
Normal file
21
web/middleware/domainValidator.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DomainValidatorMiddleware(domain string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
host := strings.Split(c.Request.Host, ":")[0]
|
||||||
|
|
||||||
|
if host != domain {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
34
web/middleware/redirect.go
Normal file
34
web/middleware/redirect.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RedirectMiddleware(basePath string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// Redirect from old '/xui' path to '/panel'
|
||||||
|
redirects := map[string]string{
|
||||||
|
"panel/API": "panel/api",
|
||||||
|
"xui/API": "panel/api",
|
||||||
|
"xui": "panel",
|
||||||
|
}
|
||||||
|
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
for from, to := range redirects {
|
||||||
|
from, to = basePath+from, basePath+to
|
||||||
|
|
||||||
|
if strings.HasPrefix(path, from) {
|
||||||
|
newPath := to + path[len(from):]
|
||||||
|
|
||||||
|
c.Redirect(http.StatusMovedPermanently, newPath)
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1185,6 +1185,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
|
||||||
}
|
}
|
||||||
return InboundClientIps.Ips, nil
|
return InboundClientIps.Ips, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) ClearClientIps(clientEmail string) error {
|
func (s *InboundService) ClearClientIps(clientEmail string) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@ var xrayTemplateConfig string
|
||||||
var defaultValueMap = map[string]string{
|
var defaultValueMap = map[string]string{
|
||||||
"xrayTemplateConfig": xrayTemplateConfig,
|
"xrayTemplateConfig": xrayTemplateConfig,
|
||||||
"webListen": "",
|
"webListen": "",
|
||||||
|
"webDomain": "",
|
||||||
"webPort": "2053",
|
"webPort": "2053",
|
||||||
"webCertFile": "",
|
"webCertFile": "",
|
||||||
"webKeyFile": "",
|
"webKeyFile": "",
|
||||||
|
@ -44,7 +45,7 @@ var defaultValueMap = map[string]string{
|
||||||
"subEnable": "false",
|
"subEnable": "false",
|
||||||
"subListen": "",
|
"subListen": "",
|
||||||
"subPort": "2096",
|
"subPort": "2096",
|
||||||
"subPath": "sub/",
|
"subPath": "/sub/",
|
||||||
"subDomain": "",
|
"subDomain": "",
|
||||||
"subCertFile": "",
|
"subCertFile": "",
|
||||||
"subKeyFile": "",
|
"subKeyFile": "",
|
||||||
|
@ -225,6 +226,10 @@ func (s *SettingService) GetListen() (string, error) {
|
||||||
return s.getString("webListen")
|
return s.getString("webListen")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetWebDomain() (string, error) {
|
||||||
|
return s.getString("webDomain")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgBotToken() (string, error) {
|
func (s *SettingService) GetTgBotToken() (string, error) {
|
||||||
return s.getString("tgBotToken")
|
return s.getString("tgBotToken")
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,6 +77,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if tgBotid != "" {
|
||||||
for _, adminId := range strings.Split(tgBotid, ",") {
|
for _, adminId := range strings.Split(tgBotid, ",") {
|
||||||
id, err := strconv.Atoi(adminId)
|
id, err := strconv.Atoi(adminId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -85,6 +86,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||||
}
|
}
|
||||||
adminIds = append(adminIds, int64(id))
|
adminIds = append(adminIds, int64(id))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bot, err = telego.NewBot(tgBottoken)
|
bot, err = telego.NewBot(tgBottoken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
|
||||||
msg := ""
|
msg, onlyMessage := "", false
|
||||||
|
|
||||||
command, commandArgs := tu.ParseCommand(message.Text)
|
command, commandArgs := tu.ParseCommand(message.Text)
|
||||||
|
|
||||||
|
@ -204,8 +206,13 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||||
}
|
}
|
||||||
msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
|
msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
|
||||||
case "status":
|
case "status":
|
||||||
|
onlyMessage = true
|
||||||
msg += t.I18nBot("tgbot.commands.status")
|
msg += t.I18nBot("tgbot.commands.status")
|
||||||
|
case "id":
|
||||||
|
onlyMessage = true
|
||||||
|
msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10))
|
||||||
case "usage":
|
case "usage":
|
||||||
|
onlyMessage = true
|
||||||
if len(commandArgs) > 0 {
|
if len(commandArgs) > 0 {
|
||||||
if isAdmin {
|
if isAdmin {
|
||||||
t.searchClient(chatId, commandArgs[0])
|
t.searchClient(chatId, commandArgs[0])
|
||||||
|
@ -216,6 +223,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||||
msg += t.I18nBot("tgbot.commands.usage")
|
msg += t.I18nBot("tgbot.commands.usage")
|
||||||
}
|
}
|
||||||
case "inbound":
|
case "inbound":
|
||||||
|
onlyMessage = true
|
||||||
if isAdmin && len(commandArgs) > 0 {
|
if isAdmin && len(commandArgs) > 0 {
|
||||||
t.searchInbound(chatId, commandArgs[0])
|
t.searchInbound(chatId, commandArgs[0])
|
||||||
} else {
|
} else {
|
||||||
|
@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||||
default:
|
default:
|
||||||
msg += t.I18nBot("tgbot.commands.unknown")
|
msg += t.I18nBot("tgbot.commands.unknown")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if onlyMessage {
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
t.SendAnswer(chatId, msg, isAdmin)
|
t.SendAnswer(chatId, msg, isAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||||
if !isRunning {
|
if !isRunning {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if msg == "" {
|
if msg == "" {
|
||||||
logger.Info("[tgbot] message is empty!")
|
logger.Info("[tgbot] message is empty!")
|
||||||
return
|
return
|
||||||
|
@ -723,7 +737,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
|
||||||
|
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||||
if (traffic.Enable) {
|
if traffic.Enable {
|
||||||
output += t.I18nBot("tgbot.messages.active")
|
output += t.I18nBot("tgbot.messages.active")
|
||||||
if flag {
|
if flag {
|
||||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||||
|
@ -791,6 +805,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+email)
|
||||||
output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId)
|
output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId)
|
||||||
|
output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||||
|
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
|
@ -860,7 +875,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
|
||||||
|
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||||
if (traffic.Enable) {
|
if traffic.Enable {
|
||||||
output += t.I18nBot("tgbot.messages.active")
|
output += t.I18nBot("tgbot.messages.active")
|
||||||
if flag {
|
if flag {
|
||||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||||
|
@ -958,7 +973,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
||||||
|
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||||
if (traffic.Enable) {
|
if traffic.Enable {
|
||||||
output += t.I18nBot("tgbot.messages.active")
|
output += t.I18nBot("tgbot.messages.active")
|
||||||
if flag {
|
if flag {
|
||||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||||
|
@ -1018,7 +1033,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
|
||||||
|
|
||||||
output := ""
|
output := ""
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||||
if (traffic.Enable) {
|
if traffic.Enable {
|
||||||
output += t.I18nBot("tgbot.messages.active")
|
output += t.I18nBot("tgbot.messages.active")
|
||||||
if flag {
|
if flag {
|
||||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||||
|
@ -1138,7 +1153,7 @@ func (t *Tgbot) getExhausted() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
|
||||||
if (traffic.Enable) {
|
if traffic.Enable {
|
||||||
output += t.I18nBot("tgbot.messages.active")
|
output += t.I18nBot("tgbot.messages.active")
|
||||||
if flag {
|
if flag {
|
||||||
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
"setDefaultCert" = "Set cert from panel"
|
"setDefaultCert" = "Set cert from panel"
|
||||||
"xtlsDesc" = "Xray core needs to be 1.7.5"
|
"xtlsDesc" = "Xray core needs to be 1.7.5"
|
||||||
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
|
"realityDesc" = "Xray core needs to be 1.8.0 or higher."
|
||||||
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )"
|
"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
|
||||||
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
|
"subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
|
@ -221,6 +221,8 @@
|
||||||
"TGBotSettings" = "Telegram Bot Settings"
|
"TGBotSettings" = "Telegram Bot Settings"
|
||||||
"panelListeningIP" = "Panel Listening IP"
|
"panelListeningIP" = "Panel Listening IP"
|
||||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
||||||
|
"panelListeningDomain" = "Panel Listening Domain"
|
||||||
|
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||||
"panelPort" = "Panel Port"
|
"panelPort" = "Panel Port"
|
||||||
"panelPortDesc" = "The port used to display this panel"
|
"panelPortDesc" = "The port used to display this panel"
|
||||||
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||||
|
@ -238,7 +240,7 @@
|
||||||
"telegramToken" = "Telegram Token"
|
"telegramToken" = "Telegram Token"
|
||||||
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
|
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
|
||||||
"telegramChatId" = "Telegram Admin Chat IDs"
|
"telegramChatId" = "Telegram Admin Chat IDs"
|
||||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs."
|
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs."
|
||||||
"telegramNotifyTime" = "Telegram bot notification time"
|
"telegramNotifyTime" = "Telegram bot notification time"
|
||||||
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||||
"tgNotifyBackup" = "Database Backup"
|
"tgNotifyBackup" = "Database Backup"
|
||||||
|
@ -397,6 +399,7 @@
|
||||||
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
||||||
"status" = "✅ Bot is ok!"
|
"status" = "✅ Bot is ok!"
|
||||||
"usage" = "❗ Please provide a text to search!"
|
"usage" = "❗ Please provide a text to search!"
|
||||||
|
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
|
||||||
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
|
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
|
||||||
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
"setDefaultCert" = "استفاده از گواهی پنل"
|
"setDefaultCert" = "استفاده از گواهی پنل"
|
||||||
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
|
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
|
||||||
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
|
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
|
||||||
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
|
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
|
||||||
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
|
@ -221,6 +221,8 @@
|
||||||
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
"TGBotSettings" = "تنظیمات ربات تلگرام"
|
||||||
"panelListeningIP" = "محدودیت آی پی پنل"
|
"panelListeningIP" = "محدودیت آی پی پنل"
|
||||||
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
"panelListeningIPDesc" = "برای استفاده از تمام آیپیها به طور پیش فرض خالی بگذارید"
|
||||||
|
"panelListeningDomain" = "محدودیت دامین پنل"
|
||||||
|
"panelListeningDomainDesc" = "برای استفاده از تمام دامنهها و آیپیها به طور پیش فرض خالی بگذارید"
|
||||||
"panelPort" = "پورت پنل"
|
"panelPort" = "پورت پنل"
|
||||||
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
|
||||||
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
|
||||||
|
@ -238,7 +240,7 @@
|
||||||
"telegramToken" = "توکن تلگرام"
|
"telegramToken" = "توکن تلگرام"
|
||||||
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
|
||||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
"telegramChatId" = "آی دی تلگرام مدیریت"
|
||||||
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
|
||||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
|
||||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
||||||
|
@ -397,6 +399,7 @@
|
||||||
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
|
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
|
||||||
"status" = "✅ ربات در حالت عادی است!"
|
"status" = "✅ ربات در حالت عادی است!"
|
||||||
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
|
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
|
||||||
|
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
|
||||||
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودیها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
|
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودیها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
|
||||||
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
|
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
"setDefaultCert" = "Установить сертификат с панели"
|
"setDefaultCert" = "Установить сертификат с панели"
|
||||||
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
|
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
|
||||||
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
|
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
|
||||||
"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)"
|
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
|
||||||
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
|
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
|
@ -221,6 +221,8 @@
|
||||||
"TGBotSettings" = "Настройки Telegram бота"
|
"TGBotSettings" = "Настройки Telegram бота"
|
||||||
"panelListeningIP" = "IP адрес панели"
|
"panelListeningIP" = "IP адрес панели"
|
||||||
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
||||||
|
"panelListeningDomain" = "Домен прослушивания панели"
|
||||||
|
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
|
||||||
"panelPort" = "Порт панели"
|
"panelPort" = "Порт панели"
|
||||||
"panelPortDesc" = "Порт, используемый для отображения этой панели"
|
"panelPortDesc" = "Порт, используемый для отображения этой панели"
|
||||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||||
|
@ -238,7 +240,7 @@
|
||||||
"telegramToken" = "Токен Telegram бота"
|
"telegramToken" = "Токен Telegram бота"
|
||||||
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
|
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
|
||||||
"telegramChatId" = "Telegram ID админа бота"
|
"telegramChatId" = "Telegram ID админа бота"
|
||||||
"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID"
|
"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
|
||||||
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
|
"telegramNotifyTime" = "Частота уведомлений бота Telegram"
|
||||||
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
|
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
|
||||||
"tgNotifyBackup" = "Резервное копирование базы данных"
|
"tgNotifyBackup" = "Резервное копирование базы данных"
|
||||||
|
@ -397,6 +399,7 @@
|
||||||
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
|
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
|
||||||
"status" = "✅ Бот работает нормально!"
|
"status" = "✅ Бот работает нормально!"
|
||||||
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
|
"usage" = "❗ Пожалуйста, укажите текст для поиска!"
|
||||||
|
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
|
||||||
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
|
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
|
||||||
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
|
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@
|
||||||
"setDefaultCert" = "从面板设置证书"
|
"setDefaultCert" = "从面板设置证书"
|
||||||
"xtlsDesc" = "Xray核心需要1.7.5"
|
"xtlsDesc" = "Xray核心需要1.7.5"
|
||||||
"realityDesc" = "Xray核心需要1.8.0及以上版本"
|
"realityDesc" = "Xray核心需要1.8.0及以上版本"
|
||||||
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)"
|
"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
|
||||||
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
|
||||||
|
|
||||||
[pages.client]
|
[pages.client]
|
||||||
|
@ -221,6 +221,8 @@
|
||||||
"TGBotSettings" = "TG提醒相关设置"
|
"TGBotSettings" = "TG提醒相关设置"
|
||||||
"panelListeningIP" = "面板监听 IP"
|
"panelListeningIP" = "面板监听 IP"
|
||||||
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
"panelListeningIPDesc" = "默认留空监听所有 IP"
|
||||||
|
"panelListeningDomain" = "面板监听域名"
|
||||||
|
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
|
||||||
"panelPort" = "面板监听端口"
|
"panelPort" = "面板监听端口"
|
||||||
"panelPortDesc" = "重启面板生效"
|
"panelPortDesc" = "重启面板生效"
|
||||||
"publicKeyPath" = "面板证书公钥文件路径"
|
"publicKeyPath" = "面板证书公钥文件路径"
|
||||||
|
@ -238,7 +240,7 @@
|
||||||
"telegramToken" = "电报机器人TOKEN"
|
"telegramToken" = "电报机器人TOKEN"
|
||||||
"telegramTokenDesc" = "重启面板生效"
|
"telegramTokenDesc" = "重启面板生效"
|
||||||
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
||||||
"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
|
"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
|
||||||
"telegramNotifyTime" = "电报机器人通知时间"
|
"telegramNotifyTime" = "电报机器人通知时间"
|
||||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||||
"tgNotifyBackup" = "数据库备份"
|
"tgNotifyBackup" = "数据库备份"
|
||||||
|
@ -397,6 +399,7 @@
|
||||||
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
|
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
|
||||||
"status" = "✅ 机器人正常运行!"
|
"status" = "✅ 机器人正常运行!"
|
||||||
"usage" = "❗ 请输入要搜索的文本!"
|
"usage" = "❗ 请输入要搜索的文本!"
|
||||||
|
"getID" = "🆔 您的ID为:<code>{{ .ID }}</code>"
|
||||||
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
|
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n<code>/inbound [Remark]</code>"
|
||||||
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
|
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。"
|
||||||
|
|
||||||
|
|
34
web/web.go
34
web/web.go
|
@ -19,6 +19,7 @@ import (
|
||||||
"x-ui/web/controller"
|
"x-ui/web/controller"
|
||||||
"x-ui/web/job"
|
"x-ui/web/job"
|
||||||
"x-ui/web/locale"
|
"x-ui/web/locale"
|
||||||
|
"x-ui/web/middleware"
|
||||||
"x-ui/web/network"
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func redirectMiddleware(basePath string) gin.HandlerFunc {
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// Redirect from old '/xui' path to '/panel'
|
|
||||||
path := c.Request.URL.Path
|
|
||||||
redirects := map[string]string{
|
|
||||||
"panel/API": "panel/api",
|
|
||||||
"xui/API": "panel/api",
|
|
||||||
"xui": "panel",
|
|
||||||
}
|
|
||||||
for from, to := range redirects {
|
|
||||||
from, to = basePath+from, basePath+to
|
|
||||||
if strings.HasPrefix(path, from) {
|
|
||||||
newPath := to + path[len(from):]
|
|
||||||
c.Redirect(http.StatusMovedPermanently, newPath)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
if config.IsDebug() {
|
if config.IsDebug() {
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.SetMode(gin.DebugMode)
|
||||||
|
@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
|
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
|
|
||||||
|
webDomain, err := s.settingService.GetWebDomain()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if webDomain != "" {
|
||||||
|
engine.Use(middleware.DomainValidatorMiddleware(webDomain))
|
||||||
|
}
|
||||||
|
|
||||||
secret, err := s.settingService.GetSecret()
|
secret, err := s.settingService.GetSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the redirect middleware (`/xui` to `/panel`)
|
// Apply the redirect middleware (`/xui` to `/panel`)
|
||||||
engine.Use(redirectMiddleware(basePath))
|
engine.Use(middleware.RedirectMiddleware(basePath))
|
||||||
|
|
||||||
g := engine.Group(basePath)
|
g := engine.Group(basePath)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue