Parcourir la source

Merge pull request #545 from hamid-gh98/main

🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
Ho3ein il y a 1 an
Parent
commit
94fad02737

+ 38 - 27
README.md

@@ -9,7 +9,6 @@
 [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
 [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](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,8 +37,8 @@ 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
 
 
 - System Status Monitoring
 - System Status Monitoring
@@ -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)
    ```
    ```
@@ -100,7 +105,7 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z
    git clone https://github.com/MHSanaei/3x-ui.git
    git clone https://github.com/MHSanaei/3x-ui.git
    cd 3x-ui
    cd 3x-ui
    ```
    ```
-   
+
 3. Start the Service
 3. Start the Service
 
 
    ```sh
    ```sh
@@ -119,12 +124,14 @@ 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>
-   
+
 - Port: 2053
 - Port: 2053
 - username and password will be generated randomly if you skip to modify your own security(x-ui "7")
 - username and password will be generated randomly if you skip to modify your own security(x-ui "7")
 - database path: /etc/x-ui/x-ui.db
 - database path: /etc/x-ui/x-ui.db
@@ -141,10 +148,10 @@ After you set ssl on settings
 </details>
 </details>
 
 
 # Xray Configurations:
 # Xray Configurations:
-   
+
 <details>
 <details>
   <summary>Click for Xray Configurations details</summary>
   <summary>Click for Xray Configurations details</summary>
-   
+
 **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
 **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
 
 
 - [traffic](./media/configs/traffic.json)
 - [traffic](./media/configs/traffic.json)
@@ -152,14 +159,14 @@ After you set ssl on settings
 - [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
 - [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json)
 - [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
 - [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json)
 - [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
 - [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json)
-   
+
 </details>
 </details>
-   
+
 # [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
 # [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
-   
+
 <details>
 <details>
   <summary>Click for WARP Configuration details</summary>
   <summary>Click for WARP Configuration details</summary>
-   
+
 If you want to use routing to WARP follow steps as below:
 If you want to use routing to WARP follow steps as below:
 
 
 1. If you already installed warp, you can uninstall using below command:
 1. If you already installed warp, you can uninstall using below command:
@@ -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)
@@ -181,14 +188,14 @@ If you want to use routing to WARP follow steps as below:
    - Block Ads
    - Block Ads
    - Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
    - Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
    - Fix Google 403 error
    - Fix Google 403 error
-   
+
 </details>
 </details>
 
 
 # Telegram Bot
 # Telegram Bot
-   
+
 <details>
 <details>
   <summary>Click for Telegram Bot details</summary>
   <summary>Click for Telegram Bot details</summary>
-   
+
 X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
 X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
 Set the robot-related parameters in the panel background, including:
 Set the robot-related parameters in the panel background, including:
 
 
@@ -216,19 +223,21 @@ 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>
-   
+
 - `/login` with `PUSH` user data: `{username: '', password: ''}` for login
 - `/login` with `PUSH` user data: `{username: '', password: ''}` for login
 - `/panel/api/inbounds` base for following actions:
 - `/panel/api/inbounds` base for following actions:
 
 
@@ -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

+ 5 - 14
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) {
-			host := strings.Split(c.Request.Host, ":")[0]
-
-			if host != subDomain {
-				c.AbortWithStatus(http.StatusForbidden)
-				return
-			}
-
-			c.Next()
-		}
-
-		engine.Use(validateDomain)
+		engine.Use(middleware.DomainValidatorMiddleware(subDomain))
 	}
 	}
 
 
 	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,4 +159,4 @@ func (s *Server) Stop() error {
 
 
 func (s *Server) GetCtx() context.Context {
 func (s *Server) GetCtx() context.Context {
 	return s.ctx
 	return s.ctx
-}
+}

+ 2 - 1
web/assets/js/model/models.js

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

+ 18 - 0
web/assets/js/util/common.js

@@ -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}`;
+}

+ 2 - 0
web/controller/inbound.go

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

+ 31 - 66
web/controller/setting.go

@@ -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()
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
-		return
-	}
-	trafficDiff, err := a.settingService.GetTrafficDiff()
-	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
+	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() },
 	}
 	}
-	subCertFile, err := a.settingService.GetSubCertFile()
-	if err != nil {
-		jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
-		return
+
+	result := make(map[string]interface{})
+
+	for key, fn := range settings {
+		value, err := fn()
+		if err != nil {
+			jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
+			return
+		}
+		result[key] = value
 	}
 	}
+
 	subTLS := false
 	subTLS := false
-	if subKeyFile != "" || subCertFile != "" {
+	if result["subKeyFile"] != "" || result["subCertFile"] != "" {
 		subTLS = true
 		subTLS = true
 	}
 	}
-	result := map[string]interface{}{
-		"expireDiff":  expireDiff,
-		"trafficDiff": trafficDiff,
-		"defaultCert": defaultCert,
-		"defaultKey":  defaultKey,
-		"tgBotEnable": tgBotEnable,
-		"subEnable":   subEnable,
-		"subPort":     subPort,
-		"subPath":     subPath,
-		"subDomain":   subDomain,
-		"subTLS":      subTLS,
-	}
+	result["subTLS"] = subTLS
+
+	delete(result, "subKeyFile")
+	delete(result, "subCertFile")
+
 	jsonObj(c, result, nil)
 	jsonObj(c, result, nil)
 }
 }
 
 

+ 1 - 0
web/entity/entity.go

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

+ 0 - 2
web/global/hashStorage.go

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

+ 13 - 17
web/html/common/qrcode_modal.html

@@ -68,8 +68,8 @@
             qrModal: qrModal,
             qrModal: qrModal,
         },
         },
         methods: {
         methods: {
-            copyToClipboard(elmentId,content) {
-                this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
+            copyToClipboard(elmentId, content) {
+                this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
                     text: () => content,
                     text: () => content,
                 });
                 });
                 this.qrModal.clipboard.on('success', () => {
                 this.qrModal.clipboard.on('success', () => {
@@ -77,29 +77,25 @@
                     this.qrModal.clipboard.destroy();
                     this.qrModal.clipboard.destroy();
                 });
                 });
             },
             },
-            setQrCode(elmentId,content) {
+            setQrCode(elmentId, content) {
                 new QRious({
                 new QRious({
-                        element: document.querySelector('#'+elmentId),
-                        size: 260,
-                        value: content,
-                    });
+                    element: document.querySelector('#' + elmentId),
+                    size: 260,
+                    value: content,
+                });
             },
             },
             genSubLink(subID) {
             genSubLink(subID) {
-                protocol = app.subSettings.tls ? "https://" : "http://";
-                hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
-                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;
+                const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
+                return buildURL({ host, port, isTLS, base, path: 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));
             }
             }
-            qrModal.qrcodes.forEach((element,index) => {
-                this.setQrCode("qrCode-"+index, element.link);     
+            qrModal.qrcodes.forEach((element, index) => {
+                this.setQrCode("qrCode-" + index, element.link);
             });
             });
         }
         }
     });
     });

+ 2 - 6
web/html/xui/inbound_info_modal.html

@@ -253,12 +253,8 @@
             infoModal.visible = false;
             infoModal.visible = false;
         },
         },
         genSubLink(subID) {
         genSubLink(subID) {
-            protocol = app.subSettings.tls ? "https://" : "http://";
-            hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain;
-            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;
+            const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
+            return buildURL({ host, port, isTLS, base, path: subID });
         }
         }
     };
     };
 
 

+ 1 - 1
web/html/xui/inbound_modal.html

@@ -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 = [];

+ 1 - 1
web/html/xui/inbounds.html

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

+ 36 - 23
web/html/xui/settings.html

@@ -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-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.manualListsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item>
+                                        </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">
+                                            <h2 class="collapse-title">
+                                                <a-icon type="warning"></a-icon>
+                                                {{ i18n "pages.settings.templates.manualListsDesc" }}
+                                            </h2>
+                                        </a-row>
+                                        <a-collapse>
+                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
+                                                <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
+                                            </a-collapse-panel>
+                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
+                                                <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://";
-                    if (this.allSetting.webCertFile !== "") {
-                        protocol = "https://";
-                    }
-                    const { host } = window.location;
-                    window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
+                    const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
+                    const isTLS = webCertFile !== "" || webKeyFile !== "";
+                    const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
+                    window.location.replace(url);
                 }
                 }
             },
             },
             async fetchUserSecret() {
             async fetchUserSecret() {

+ 21 - 0
web/middleware/domainValidator.go

@@ -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 - 0
web/middleware/redirect.go

@@ -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()
+	}
+}

+ 1 - 0
web/service/inbound.go

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

+ 6 - 1
web/service/setting.go

@@ -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")
 }
 }

+ 31 - 16
web/service/tgbot.go

@@ -77,13 +77,15 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
 		return err
 		return err
 	}
 	}
 
 
-	for _, adminId := range strings.Split(tgBotid, ",") {
-		id, err := strconv.Atoi(adminId)
-		if err != nil {
-			logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
-			return err
+	if tgBotid != "" {
+		for _, adminId := range strings.Split(tgBotid, ",") {
+			id, err := strconv.Atoi(adminId)
+			if err != nil {
+				logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
+				return err
+			}
+			adminIds = append(adminIds, int64(id))
 		}
 		}
-		adminIds = append(adminIds, int64(id))
 	}
 	}
 
 
 	bot, err = telego.NewBot(tgBottoken)
 	bot, err = telego.NewBot(tgBottoken)
@@ -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(
@@ -840,7 +855,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
 	flag := false
 	flag := false
 	diff := traffic.ExpiryTime/1000 - now
 	diff := traffic.ExpiryTime/1000 - now
 	if traffic.ExpiryTime == 0 {
 	if traffic.ExpiryTime == 0 {
-	expiryTime = t.I18nBot("tgbot.unlimited")
+		expiryTime = t.I18nBot("tgbot.unlimited")
 	} else if diff > 172800 || !traffic.Enable {
 	} else if diff > 172800 || !traffic.Enable {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 	} else if traffic.ExpiryTime < 0 {
 	} else if traffic.ExpiryTime < 0 {
@@ -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)
@@ -918,7 +933,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 		t.SendMsgToTgbot(chatId, msg)
 		t.SendMsgToTgbot(chatId, msg)
 		return
 		return
 	}
 	}
-	
+
 	now := time.Now().Unix()
 	now := time.Now().Unix()
 	for _, inbound := range inbouds {
 	for _, inbound := range inbouds {
 		info := ""
 		info := ""
@@ -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)
@@ -998,7 +1013,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
 	flag := false
 	flag := false
 	diff := traffic.ExpiryTime/1000 - now
 	diff := traffic.ExpiryTime/1000 - now
 	if traffic.ExpiryTime == 0 {
 	if traffic.ExpiryTime == 0 {
-	expiryTime = t.I18nBot("tgbot.unlimited")
+		expiryTime = t.I18nBot("tgbot.unlimited")
 	} else if diff > 172800 || !traffic.Enable {
 	} else if diff > 172800 || !traffic.Enable {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 	} else if traffic.ExpiryTime < 0 {
 	} else if traffic.ExpiryTime < 0 {
@@ -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)
@@ -1117,7 +1132,7 @@ func (t *Tgbot) getExhausted() string {
 		for _, traffic := range exhaustedClients {
 		for _, traffic := range exhaustedClients {
 			expiryTime := ""
 			expiryTime := ""
 			flag := false
 			flag := false
-			diff := (traffic.ExpiryTime - now)/1000
+			diff := (traffic.ExpiryTime - now) / 1000
 			if traffic.ExpiryTime == 0 {
 			if traffic.ExpiryTime == 0 {
 				expiryTime = t.I18nBot("tgbot.unlimited")
 				expiryTime = t.I18nBot("tgbot.unlimited")
 			} else if diff > 172800 || !traffic.Enable {
 			} else if diff > 172800 || !traffic.Enable {
@@ -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)

+ 5 - 2
web/translation/translate.en_US.toml

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

+ 5 - 2
web/translation/translate.fa_IR.toml

@@ -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 استفاده کنید."
 
 

+ 5 - 2
web/translation/translate.ru_RU.toml

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

+ 5 - 2
web/translation/translate.zh_Hans.toml

@@ -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,请使用密码。"
 
 

+ 11 - 23
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)