Browse Source

Merge pull request #545 from hamid-gh98/main

🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
Ho3ein 1 year ago
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)
 
 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:
 
 **Buy Me a Coffee :**
@@ -24,11 +23,12 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
 
 # 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
 
 ```
@@ -37,8 +37,8 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
 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
 
 - 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
 
 # Manual Install & Upgrade
+
 <details>
   <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:
 
 ```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:
 
 ```sh
+ARCH=$(uname -m)
+[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
 cd /root/
 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
 cp x-ui/x-ui.sh /usr/bin/x-ui
 cp -f x-ui/x-ui.service /etc/systemd/system/
@@ -82,14 +85,16 @@ systemctl daemon-reload
 systemctl enable 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>
-   
+
 # Install with Docker
+
 <details>
   <summary>Click for Docker details</summary>
 
 1. Install Docker:
+
    ```sh
    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
    cd 3x-ui
    ```
-   
+
 3. Start the Service
 
    ```sh
@@ -119,12 +124,14 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z
       --name 3x-ui \
       ghcr.io/mhsanaei/3x-ui:latest
    ```
+
 </details>
- 
+
 # Default settings
+
 <details>
   <summary>Click for Default settings details</summary>
-   
+
 - Port: 2053
 - 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
@@ -141,10 +148,10 @@ After you set ssl on settings
 </details>
 
 # Xray Configurations:
-   
+
 <details>
   <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)
 
 - [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 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)
-   
+
 </details>
-   
+
 # [WARP Configuration](https://github.com/fscarmen/warp) (Optional)
-   
+
 <details>
   <summary>Click for WARP Configuration details</summary>
-   
+
 If you want to use routing to WARP follow steps as below:
 
 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**:
 
    ```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)
@@ -181,14 +188,14 @@ If you want to use routing to WARP follow steps as below:
    - Block Ads
    - Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
    - Fix Google 403 error
-   
+
 </details>
 
 # Telegram Bot
-   
+
 <details>
   <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)
 Set the robot-related parameters in the panel background, including:
 
@@ -216,19 +223,21 @@ Reference syntax:
 - CPU threshold notification
 - 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 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
 - Search client by email ( only admin )
 - Check all inbounds
 - Check server status
 - Check depleted users
 - Receive backup by request and in periodic reports
+- Multi language bot
 </details>
 
 # API routes
+
 <details>
   <summary>Click for API routes details</summary>
-   
+
 - `/login` with `PUSH` user data: `{username: '', password: ''}` for login
 - `/panel/api/inbounds` base for following actions:
 
@@ -261,6 +270,7 @@ Reference syntax:
 </details>
 
 # Environment Variables
+
 <details>
   <summary>Click for Environment Variables details</summary>
 
@@ -276,6 +286,7 @@ Example:
 ```sh
 XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
 ```
+
 </details>
 
 # A Special Thanks To

+ 5 - 14
sub/sub.go

@@ -7,10 +7,10 @@ import (
 	"net"
 	"net/http"
 	"strconv"
-	"strings"
 	"x-ui/config"
 	"x-ui/logger"
 	"x-ui/util/common"
+	"x-ui/web/middleware"
 	"x-ui/web/network"
 	"x-ui/web/service"
 
@@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	}
 
 	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)
@@ -116,11 +105,13 @@ func (s *Server) Start() (err error) {
 	if err != nil {
 		return err
 	}
+
 	listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
 	listener, err := net.Listen("tcp", listenAddr)
 	if err != nil {
 		return err
 	}
+
 	if certFile != "" || keyFile != "" {
 		cert, err := tls.LoadX509KeyPair(certFile, keyFile)
 		if err != nil {
@@ -168,4 +159,4 @@ func (s *Server) Stop() error {
 
 func (s *Server) GetCtx() context.Context {
 	return s.ctx
-}
+}

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

@@ -168,6 +168,7 @@ class AllSetting {
 
     constructor(data) {
         this.webListen = "";
+        this.webDomain = "";
         this.webPort = 2053;
         this.webCertFile = "";
         this.webKeyFile = "";
@@ -187,7 +188,7 @@ class AllSetting {
         this.subEnable = false;
         this.subListen = "";
         this.subPort = "2096";
-        this.subPath = "sub/";
+        this.subPath = "/sub/";
         this.subDomain = "";
         this.subCertFile = "";
         this.subKeyFile = "";

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

@@ -135,3 +135,21 @@ function doAllItemsExist(array1, array2) {
     }
     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)
 }
+
 func (a *InboundController) getInbound(c *gin.Context) {
 	id, err := strconv.Atoi(c.Param("id"))
 	if err != nil {
@@ -168,6 +169,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
 	}
 	jsonMsg(c, "Log Cleared", nil)
 }
+
 func (a *InboundController) addInboundClient(c *gin.Context) {
 	data := &model.Inbound{}
 	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) {
-	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
-	if subKeyFile != "" || subCertFile != "" {
+	if result["subKeyFile"] != "" || result["subCertFile"] != "" {
 		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)
 }
 

+ 1 - 0
web/entity/entity.go

@@ -28,6 +28,7 @@ type Pager struct {
 
 type AllSetting struct {
 	WebListen          string `json:"webListen" form:"webListen"`
+	WebDomain          string `json:"webDomain" form:"webDomain"`
 	WebPort            int    `json:"webPort" form:"webPort"`
 	WebCertFile        string `json:"webCertFile" form:"webCertFile"`
 	WebKeyFile         string `json:"webKeyFile" form:"webKeyFile"`

+ 0 - 2
web/global/hashStorage.go

@@ -18,7 +18,6 @@ type HashStorage struct {
 	sync.RWMutex
 	Data       map[string]HashEntry
 	Expiration time.Duration
-
 }
 
 func NewHashStorage(expiration time.Duration) *HashStorage {
@@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
 	return md5HashString
 }
 
-
 func (h *HashStorage) GetValue(hash string) (string, bool) {
 	h.RLock()
 	defer h.RUnlock()

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

@@ -68,8 +68,8 @@
             qrModal: qrModal,
         },
         methods: {
-            copyToClipboard(elmentId,content) {
-                this.qrModal.clipboard = new ClipboardJS('#'+elmentId, {
+            copyToClipboard(elmentId, content) {
+                this.qrModal.clipboard = new ClipboardJS('#' + elmentId, {
                     text: () => content,
                 });
                 this.qrModal.clipboard.on('success', () => {
@@ -77,29 +77,25 @@
                     this.qrModal.clipboard.destroy();
                 });
             },
-            setQrCode(elmentId,content) {
+            setQrCode(elmentId, content) {
                 new QRious({
-                        element: document.querySelector('#'+elmentId),
-                        size: 260,
-                        value: content,
-                    });
+                    element: document.querySelector('#' + elmentId),
+                    size: 260,
+                    value: content,
+                });
             },
             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() {
-            if (qrModal.client.subId){
+            if (qrModal.client && 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;
         },
         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) {
                 if (value) {
                     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 {
                     inModal.inbound.stream.tls.server = "";
                     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.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
         { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
-        { title: 'UID', width: 120, dataIndex: "id" },
+        { title: 'UUID', width: 120, dataIndex: "id" },
     ];
 
     const innerTrojanColumns = [

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

@@ -91,6 +91,7 @@
                             </a-row>
                             <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.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="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>
@@ -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.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
                                             </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>
                                     </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-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>
@@ -335,7 +350,7 @@
                                             </a-collapse-panel>
                                         </a-collapse>
                                     </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>
                                     </a-tab-pane>
                                 </a-tabs>
@@ -391,9 +406,9 @@
                             <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="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="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.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>
@@ -522,7 +537,7 @@
                 this.loading(false);
                 if (msg.success) {
                     this.user = {};
-                    window.location.replace(basePath + "logout")
+                    window.location.replace(basePath + "logout");
                 }
             },
             async restartPanel() {
@@ -541,12 +556,10 @@
                 if (msg.success) {
                     this.loading(true);
                     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() {

+ 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
 }
+
 func (s *InboundService) ClearClientIps(clientEmail string) error {
 	db := database.GetDB()
 

+ 6 - 1
web/service/setting.go

@@ -24,6 +24,7 @@ var xrayTemplateConfig string
 var defaultValueMap = map[string]string{
 	"xrayTemplateConfig": xrayTemplateConfig,
 	"webListen":          "",
+	"webDomain":          "",
 	"webPort":            "2053",
 	"webCertFile":        "",
 	"webKeyFile":         "",
@@ -44,7 +45,7 @@ var defaultValueMap = map[string]string{
 	"subEnable":          "false",
 	"subListen":          "",
 	"subPort":            "2096",
-	"subPath":            "sub/",
+	"subPath":            "/sub/",
 	"subDomain":          "",
 	"subCertFile":        "",
 	"subKeyFile":         "",
@@ -225,6 +226,10 @@ func (s *SettingService) GetListen() (string, error) {
 	return s.getString("webListen")
 }
 
+func (s *SettingService) GetWebDomain() (string, error) {
+	return s.getString("webDomain")
+}
+
 func (s *SettingService) GetTgBotToken() (string, error) {
 	return s.getString("tgBotToken")
 }

+ 31 - 16
web/service/tgbot.go

@@ -77,13 +77,15 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
 		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)
@@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() {
 }
 
 func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
-	msg := ""
+	msg, onlyMessage := "", false
 
 	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")
 	case "status":
+		onlyMessage = true
 		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":
+		onlyMessage = true
 		if len(commandArgs) > 0 {
 			if isAdmin {
 				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")
 		}
 	case "inbound":
+		onlyMessage = true
 		if isAdmin && len(commandArgs) > 0 {
 			t.searchInbound(chatId, commandArgs[0])
 		} else {
@@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
 	default:
 		msg += t.I18nBot("tgbot.commands.unknown")
 	}
+
+	if onlyMessage {
+		t.SendMsgToTgbot(chatId, msg)
+		return
+	}
 	t.SendAnswer(chatId, msg, isAdmin)
 }
 
@@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
 	if !isRunning {
 		return
 	}
+
 	if msg == "" {
 		logger.Info("[tgbot] message is empty!")
 		return
@@ -723,7 +737,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
 
 		output := ""
 		output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-		if (traffic.Enable) {
+		if traffic.Enable {
 			output += t.I18nBot("tgbot.messages.active")
 			if flag {
 				output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@@ -791,6 +805,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
 	output := ""
 	output += t.I18nBot("tgbot.messages.email", "Email=="+email)
 	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(
 		tu.InlineKeyboardRow(
@@ -840,7 +855,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
 	flag := false
 	diff := traffic.ExpiryTime/1000 - now
 	if traffic.ExpiryTime == 0 {
-	expiryTime = t.I18nBot("tgbot.unlimited")
+		expiryTime = t.I18nBot("tgbot.unlimited")
 	} else if diff > 172800 || !traffic.Enable {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 	} else if traffic.ExpiryTime < 0 {
@@ -860,7 +875,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
 
 	output := ""
 	output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-	if (traffic.Enable) {
+	if traffic.Enable {
 		output += t.I18nBot("tgbot.messages.active")
 		if flag {
 			output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@@ -918,7 +933,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 		t.SendMsgToTgbot(chatId, msg)
 		return
 	}
-	
+
 	now := time.Now().Unix()
 	for _, inbound := range inbouds {
 		info := ""
@@ -958,7 +973,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
 
 			output := ""
 			output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-			if (traffic.Enable) {
+			if traffic.Enable {
 				output += t.I18nBot("tgbot.messages.active")
 				if flag {
 					output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@@ -998,7 +1013,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
 	flag := false
 	diff := traffic.ExpiryTime/1000 - now
 	if traffic.ExpiryTime == 0 {
-	expiryTime = t.I18nBot("tgbot.unlimited")
+		expiryTime = t.I18nBot("tgbot.unlimited")
 	} else if diff > 172800 || !traffic.Enable {
 		expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
 	} else if traffic.ExpiryTime < 0 {
@@ -1018,7 +1033,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
 
 	output := ""
 	output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-	if (traffic.Enable) {
+	if traffic.Enable {
 		output += t.I18nBot("tgbot.messages.active")
 		if flag {
 			output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@@ -1117,7 +1132,7 @@ func (t *Tgbot) getExhausted() string {
 		for _, traffic := range exhaustedClients {
 			expiryTime := ""
 			flag := false
-			diff := (traffic.ExpiryTime - now)/1000
+			diff := (traffic.ExpiryTime - now) / 1000
 			if traffic.ExpiryTime == 0 {
 				expiryTime = t.I18nBot("tgbot.unlimited")
 			} else if diff > 172800 || !traffic.Enable {
@@ -1138,7 +1153,7 @@ func (t *Tgbot) getExhausted() string {
 			}
 
 			output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
-			if (traffic.Enable) {
+			if traffic.Enable {
 				output += t.I18nBot("tgbot.messages.active")
 				if flag {
 					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"
 "xtlsDesc" = "Xray core needs to be 1.7.5"
 "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"
 
 [pages.client]
@@ -221,6 +221,8 @@
 "TGBotSettings" = "Telegram Bot Settings"
 "panelListeningIP" = "Panel Listening IP"
 "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"
 "panelPortDesc" = "The port used to display this panel"
 "publicKeyPath" = "Panel Certificate Public Key File Path"
@@ -238,7 +240,7 @@
 "telegramToken" = "Telegram Token"
 "telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
 "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"
 "telegramNotifyTimeDesc" = "Use Crontab timing format."
 "tgNotifyBackup" = "Database Backup"
@@ -397,6 +399,7 @@
 "welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
 "status" = "✅ Bot is ok!"
 "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>"
 "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" = "استفاده از گواهی پنل"
 "xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
 "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
-"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)"
+"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
 "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
 
 [pages.client]
@@ -221,6 +221,8 @@
 "TGBotSettings" = "تنظیمات ربات تلگرام"
 "panelListeningIP" = "محدودیت آی پی پنل"
 "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
+"panelListeningDomain" = "محدودیت دامین پنل"
+"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
 "panelPort" = "پورت پنل"
 "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
 "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
@@ -238,7 +240,7 @@
 "telegramToken" = "توکن تلگرام"
 "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
 "telegramChatId" = "آی دی تلگرام مدیریت"
-"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
+"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
 "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
 "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
 "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
@@ -397,6 +399,7 @@
 "welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
 "status" = "✅ ربات در حالت عادی است!"
 "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
+"getID" = "🆔 شناسه شما: <code>{{ .ID }}</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 استفاده کنید."
 

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

@@ -168,7 +168,7 @@
 "setDefaultCert" = "Установить сертификат с панели"
 "xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
 "realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
-"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)"
+"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
 "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
 
 [pages.client]
@@ -221,6 +221,8 @@
 "TGBotSettings" = "Настройки Telegram бота"
 "panelListeningIP" = "IP адрес панели"
 "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
+"panelListeningDomain" = "Домен прослушивания панели"
+"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
 "panelPort" = "Порт панели"
 "panelPortDesc" = "Порт, используемый для отображения этой панели"
 "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@@ -238,7 +240,7 @@
 "telegramToken" = "Токен Telegram бота"
 "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
 "telegramChatId" = "Telegram ID админа бота"
-"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID"
+"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
 "telegramNotifyTime" = "Частота уведомлений бота Telegram"
 "telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
 "tgNotifyBackup" = "Резервное копирование базы данных"
@@ -397,6 +399,7 @@
 "welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
 "status" = "✅ Бот работает нормально!"
 "usage" = "❗ Пожалуйста, укажите текст для поиска!"
+"getID" = "🆔 Ваш ID: <code>{{ .ID }}</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."
 

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

@@ -168,7 +168,7 @@
 "setDefaultCert" = "从面板设置证书"
 "xtlsDesc" = "Xray核心需要1.7.5"
 "realityDesc" = "Xray核心需要1.8.0及以上版本"
-"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)"
+"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)"
 "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
 
 [pages.client]
@@ -221,6 +221,8 @@
 "TGBotSettings" = "TG提醒相关设置"
 "panelListeningIP" = "面板监听 IP"
 "panelListeningIPDesc" = "默认留空监听所有 IP"
+"panelListeningDomain" = "面板监听域名"
+"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
 "panelPort" = "面板监听端口"
 "panelPortDesc" = "重启面板生效"
 "publicKeyPath" = "面板证书公钥文件路径"
@@ -238,7 +240,7 @@
 "telegramToken" = "电报机器人TOKEN"
 "telegramTokenDesc" = "重启面板生效"
 "telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
-"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。"
+"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
 "telegramNotifyTime" = "电报机器人通知时间"
 "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
 "tgNotifyBackup" = "数据库备份"
@@ -397,6 +399,7 @@
 "welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
 "status" = "✅ 机器人正常运行!"
 "usage" = "❗ 请输入要搜索的文本!"
+"getID" = "🆔 您的ID为:<code>{{ .ID }}</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,请使用密码。"
 

+ 11 - 23
web/web.go

@@ -19,6 +19,7 @@ import (
 	"x-ui/web/controller"
 	"x-ui/web/job"
 	"x-ui/web/locale"
+	"x-ui/web/middleware"
 	"x-ui/web/network"
 	"x-ui/web/service"
 
@@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
 	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) {
 	if config.IsDebug() {
 		gin.SetMode(gin.DebugMode)
@@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 
 	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()
 	if err != nil {
 		return nil, err
@@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
 	}
 
 	// Apply the redirect middleware (`/xui` to `/panel`)
-	engine.Use(redirectMiddleware(basePath))
+	engine.Use(middleware.RedirectMiddleware(basePath))
 
 	g := engine.Group(basePath)