From c6f15cd53fa79c1f55a4bef332c986f7cd46e295 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Sat, 6 Jun 2026 16:22:41 +0200 Subject: [PATCH] refactor(api)!: move /panel/setting and /panel/xray under /panel/api Settings and Xray config endpoints now live at /panel/api/setting/* and /panel/api/xray/*, registered under the existing /panel/api group so they inherit the same Bearer-or-session auth (checkAPIAuth) as the rest of the API. An API token is a full-admin credential, so this just makes the surface consistent. The SPA page routes /panel/settings and /panel/xray are unchanged. BREAKING CHANGE: the old /panel/setting/* and /panel/xray/* paths are removed. External callers must switch to the /panel/api/ prefix. Frontend call sites, API docs, the dev proxy, and the route-documentation test are updated to match. --- frontend/public/openapi.json | 1903 ++++++++++++++++- frontend/src/api/queries/useAllSettings.ts | 4 +- frontend/src/hooks/useClients.ts | 2 +- frontend/src/hooks/useDatepicker.ts | 2 +- frontend/src/hooks/useXraySetting.ts | 14 +- frontend/src/pages/api-docs/endpoints.ts | 46 +- .../pages/inbounds/form/useSecurityActions.ts | 2 +- frontend/src/pages/inbounds/useInbounds.ts | 2 +- frontend/src/pages/index/BackupModal.tsx | 2 +- frontend/src/pages/index/IndexPage.tsx | 2 +- frontend/src/pages/settings/SecurityTab.tsx | 10 +- frontend/src/pages/settings/SettingsPage.tsx | 2 +- .../src/pages/xray/overrides/NordModal.tsx | 12 +- .../src/pages/xray/overrides/WarpModal.tsx | 10 +- frontend/vite.config.js | 2 +- web/controller/api.go | 22 +- web/controller/api_docs_test.go | 4 +- web/controller/xui.go | 8 +- 18 files changed, 1928 insertions(+), 121 deletions(-) diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index 057ee436..259050a4 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -3,7 +3,7 @@ "info": { "title": "3X-UI Panel API", "version": "3.x", - "description": "Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with an API token from Settings → Security → API Token (Bearer). All endpoints under /panel/api/* honour both modes." + "description": "Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with an API token from Settings → Security → API Token (Bearer). All endpoints under /panel/api/* honour both modes — an API token is a full-admin credential, so treat it like the panel password." }, "servers": [ { @@ -24,6 +24,1790 @@ "name": "3x-ui", "description": "Session cookie set by POST /login. Browser-only." } + }, + "schemas": { + "AllSetting": { + "description": "AllSetting contains all configuration settings for the 3x-ui panel including web server, Telegram bot, and subscription settings.", + "properties": { + "datepicker": { + "description": "Date picker format", + "type": "string" + }, + "expireDiff": { + "description": "Expiration warning threshold in days", + "minimum": 0, + "type": "integer" + }, + "externalTrafficInformEnable": { + "description": "Enable external traffic reporting", + "type": "boolean" + }, + "externalTrafficInformURI": { + "description": "URI for external traffic reporting", + "type": "string" + }, + "ldapAutoCreate": { + "type": "boolean" + }, + "ldapAutoDelete": { + "type": "boolean" + }, + "ldapBaseDN": { + "type": "string" + }, + "ldapBindDN": { + "type": "string" + }, + "ldapDefaultExpiryDays": { + "minimum": 0, + "type": "integer" + }, + "ldapDefaultLimitIP": { + "minimum": 0, + "type": "integer" + }, + "ldapDefaultTotalGB": { + "minimum": 0, + "type": "integer" + }, + "ldapEnable": { + "description": "LDAP settings", + "type": "boolean" + }, + "ldapFlagField": { + "description": "Generic flag configuration", + "type": "string" + }, + "ldapHost": { + "type": "string" + }, + "ldapInboundTags": { + "type": "string" + }, + "ldapInvertFlag": { + "type": "boolean" + }, + "ldapPassword": { + "type": "string" + }, + "ldapPort": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ldapSyncCron": { + "type": "string" + }, + "ldapTruthyValues": { + "type": "string" + }, + "ldapUseTLS": { + "type": "boolean" + }, + "ldapUserAttr": { + "description": "e.g., mail or uid", + "type": "string" + }, + "ldapUserFilter": { + "type": "string" + }, + "ldapVlessField": { + "type": "string" + }, + "pageSize": { + "description": "UI settings\nNumber of items per page in lists (0 disables pagination)", + "maximum": 1000, + "minimum": 0, + "type": "integer" + }, + "panelProxy": { + "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "type": "string" + }, + "remarkModel": { + "description": "Remark model pattern for inbounds", + "type": "string" + }, + "restartXrayOnClientDisable": { + "description": "Restart Xray when clients are auto-disabled by expiry/traffic limit", + "type": "boolean" + }, + "sessionMaxAge": { + "description": "Session maximum age in minutes (cap at one year)", + "maximum": 525600, + "minimum": 1, + "type": "integer" + }, + "subAnnounce": { + "description": "Subscription announce", + "type": "string" + }, + "subCertFile": { + "description": "SSL certificate file for subscription server", + "type": "string" + }, + "subClashEnable": { + "description": "Enable Clash/Mihomo subscription endpoint", + "type": "boolean" + }, + "subClashEnableRouting": { + "description": "Enable global routing rules for Clash/Mihomo", + "type": "boolean" + }, + "subClashPath": { + "description": "Path for Clash/Mihomo subscription endpoint", + "type": "string" + }, + "subClashRules": { + "description": "Clash/Mihomo global routing rules", + "type": "string" + }, + "subClashURI": { + "description": "Clash/Mihomo subscription server URI", + "type": "string" + }, + "subDomain": { + "description": "Domain for subscription server validation", + "type": "string" + }, + "subEmailInRemark": { + "description": "Include email in subscription remark/name", + "type": "boolean" + }, + "subEnable": { + "description": "Subscription server settings\nEnable subscription server", + "type": "boolean" + }, + "subEnableRouting": { + "description": "Enable routing for subscription", + "type": "boolean" + }, + "subEncrypt": { + "description": "Encrypt subscription responses", + "type": "boolean" + }, + "subJsonEnable": { + "description": "Enable JSON subscription endpoint", + "type": "boolean" + }, + "subJsonFinalMask": { + "description": "JSON subscription global finalmask (tcp/udp masks + quicParams)", + "type": "string" + }, + "subJsonMux": { + "description": "JSON subscription mux configuration", + "type": "string" + }, + "subJsonPath": { + "description": "Path for JSON subscription endpoint", + "type": "string" + }, + "subJsonRules": { + "type": "string" + }, + "subJsonURI": { + "description": "JSON subscription server URI", + "type": "string" + }, + "subKeyFile": { + "description": "SSL private key file for subscription server", + "type": "string" + }, + "subListen": { + "description": "Subscription server listen IP", + "type": "string" + }, + "subPath": { + "description": "Base path for subscription URLs", + "type": "string" + }, + "subPort": { + "description": "Subscription server port", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "subProfileUrl": { + "description": "Subscription profile URL", + "type": "string" + }, + "subRoutingRules": { + "description": "Subscription global routing rules (Only for Happ)", + "type": "string" + }, + "subShowInfo": { + "description": "Show client information in subscriptions", + "type": "boolean" + }, + "subSupportUrl": { + "description": "Subscription support URL", + "type": "string" + }, + "subTitle": { + "description": "Subscription title", + "type": "string" + }, + "subURI": { + "description": "Subscription server URI", + "type": "string" + }, + "subUpdates": { + "description": "Subscription update interval in minutes", + "maximum": 525600, + "minimum": 0, + "type": "integer" + }, + "tgBotAPIServer": { + "description": "Custom API server for Telegram bot", + "type": "string" + }, + "tgBotBackup": { + "description": "Enable database backup via Telegram", + "type": "boolean" + }, + "tgBotChatId": { + "description": "Telegram chat ID for notifications", + "type": "string" + }, + "tgBotEnable": { + "description": "Telegram bot settings\nEnable Telegram bot notifications", + "type": "boolean" + }, + "tgBotLoginNotify": { + "description": "Send login notifications", + "type": "boolean" + }, + "tgBotProxy": { + "description": "Proxy URL for Telegram bot", + "type": "string" + }, + "tgBotToken": { + "description": "Telegram bot token", + "type": "string" + }, + "tgCpu": { + "description": "CPU usage threshold for alerts (percent)", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "tgLang": { + "description": "Telegram bot language", + "type": "string" + }, + "tgRunTime": { + "description": "Cron schedule for Telegram notifications", + "type": "string" + }, + "timeLocation": { + "description": "Security settings\nTime zone location", + "type": "string" + }, + "trafficDiff": { + "description": "Traffic warning threshold percentage", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "trustedProxyCIDRs": { + "description": "Trusted reverse proxy IPs/CIDRs for forwarded headers", + "type": "string" + }, + "twoFactorEnable": { + "description": "Enable two-factor authentication", + "type": "boolean" + }, + "twoFactorToken": { + "description": "Two-factor authentication token", + "type": "string" + }, + "webBasePath": { + "description": "Base path for web panel URLs", + "type": "string" + }, + "webCertFile": { + "description": "Path to SSL certificate file for web server", + "type": "string" + }, + "webDomain": { + "description": "Web server domain for domain validation", + "type": "string" + }, + "webKeyFile": { + "description": "Path to SSL private key file for web server", + "type": "string" + }, + "webListen": { + "description": "Web server settings\nWeb server listen IP address", + "type": "string" + }, + "webPort": { + "description": "Web server port number", + "maximum": 65535, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "datepicker", + "expireDiff", + "externalTrafficInformEnable", + "externalTrafficInformURI", + "ldapAutoCreate", + "ldapAutoDelete", + "ldapBaseDN", + "ldapBindDN", + "ldapDefaultExpiryDays", + "ldapDefaultLimitIP", + "ldapDefaultTotalGB", + "ldapEnable", + "ldapFlagField", + "ldapHost", + "ldapInboundTags", + "ldapInvertFlag", + "ldapPassword", + "ldapPort", + "ldapSyncCron", + "ldapTruthyValues", + "ldapUseTLS", + "ldapUserAttr", + "ldapUserFilter", + "ldapVlessField", + "pageSize", + "panelProxy", + "remarkModel", + "restartXrayOnClientDisable", + "sessionMaxAge", + "subAnnounce", + "subCertFile", + "subClashEnable", + "subClashEnableRouting", + "subClashPath", + "subClashRules", + "subClashURI", + "subDomain", + "subEmailInRemark", + "subEnable", + "subEnableRouting", + "subEncrypt", + "subJsonEnable", + "subJsonFinalMask", + "subJsonMux", + "subJsonPath", + "subJsonRules", + "subJsonURI", + "subKeyFile", + "subListen", + "subPath", + "subPort", + "subProfileUrl", + "subRoutingRules", + "subShowInfo", + "subSupportUrl", + "subTitle", + "subURI", + "subUpdates", + "tgBotAPIServer", + "tgBotBackup", + "tgBotChatId", + "tgBotEnable", + "tgBotLoginNotify", + "tgBotProxy", + "tgBotToken", + "tgCpu", + "tgLang", + "tgRunTime", + "timeLocation", + "trafficDiff", + "trustedProxyCIDRs", + "twoFactorEnable", + "twoFactorToken", + "webBasePath", + "webCertFile", + "webDomain", + "webKeyFile", + "webListen", + "webPort" + ], + "type": "object" + }, + "AllSettingView": { + "description": "AllSettingView is the browser-safe settings read model. Secret values\nare redacted from the embedded write model and represented by presence\nflags so the UI can show configured/not configured state.", + "properties": { + "datepicker": { + "description": "Date picker format", + "type": "string" + }, + "expireDiff": { + "description": "Expiration warning threshold in days", + "minimum": 0, + "type": "integer" + }, + "externalTrafficInformEnable": { + "description": "Enable external traffic reporting", + "type": "boolean" + }, + "externalTrafficInformURI": { + "description": "URI for external traffic reporting", + "type": "string" + }, + "hasApiToken": { + "type": "boolean" + }, + "hasLdapPassword": { + "type": "boolean" + }, + "hasNordSecret": { + "type": "boolean" + }, + "hasTgBotToken": { + "type": "boolean" + }, + "hasTwoFactorToken": { + "type": "boolean" + }, + "hasWarpSecret": { + "type": "boolean" + }, + "ldapAutoCreate": { + "type": "boolean" + }, + "ldapAutoDelete": { + "type": "boolean" + }, + "ldapBaseDN": { + "type": "string" + }, + "ldapBindDN": { + "type": "string" + }, + "ldapDefaultExpiryDays": { + "minimum": 0, + "type": "integer" + }, + "ldapDefaultLimitIP": { + "minimum": 0, + "type": "integer" + }, + "ldapDefaultTotalGB": { + "minimum": 0, + "type": "integer" + }, + "ldapEnable": { + "description": "LDAP settings", + "type": "boolean" + }, + "ldapFlagField": { + "description": "Generic flag configuration", + "type": "string" + }, + "ldapHost": { + "type": "string" + }, + "ldapInboundTags": { + "type": "string" + }, + "ldapInvertFlag": { + "type": "boolean" + }, + "ldapPassword": { + "type": "string" + }, + "ldapPort": { + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "ldapSyncCron": { + "type": "string" + }, + "ldapTruthyValues": { + "type": "string" + }, + "ldapUseTLS": { + "type": "boolean" + }, + "ldapUserAttr": { + "description": "e.g., mail or uid", + "type": "string" + }, + "ldapUserFilter": { + "type": "string" + }, + "ldapVlessField": { + "type": "string" + }, + "pageSize": { + "description": "UI settings\nNumber of items per page in lists (0 disables pagination)", + "maximum": 1000, + "minimum": 0, + "type": "integer" + }, + "panelProxy": { + "description": "Proxy URL for the panel's own outbound requests (GitHub/Telegram)", + "type": "string" + }, + "remarkModel": { + "description": "Remark model pattern for inbounds", + "type": "string" + }, + "restartXrayOnClientDisable": { + "description": "Restart Xray when clients are auto-disabled by expiry/traffic limit", + "type": "boolean" + }, + "sessionMaxAge": { + "description": "Session maximum age in minutes (cap at one year)", + "maximum": 525600, + "minimum": 1, + "type": "integer" + }, + "subAnnounce": { + "description": "Subscription announce", + "type": "string" + }, + "subCertFile": { + "description": "SSL certificate file for subscription server", + "type": "string" + }, + "subClashEnable": { + "description": "Enable Clash/Mihomo subscription endpoint", + "type": "boolean" + }, + "subClashEnableRouting": { + "description": "Enable global routing rules for Clash/Mihomo", + "type": "boolean" + }, + "subClashPath": { + "description": "Path for Clash/Mihomo subscription endpoint", + "type": "string" + }, + "subClashRules": { + "description": "Clash/Mihomo global routing rules", + "type": "string" + }, + "subClashURI": { + "description": "Clash/Mihomo subscription server URI", + "type": "string" + }, + "subDomain": { + "description": "Domain for subscription server validation", + "type": "string" + }, + "subEmailInRemark": { + "description": "Include email in subscription remark/name", + "type": "boolean" + }, + "subEnable": { + "description": "Subscription server settings\nEnable subscription server", + "type": "boolean" + }, + "subEnableRouting": { + "description": "Enable routing for subscription", + "type": "boolean" + }, + "subEncrypt": { + "description": "Encrypt subscription responses", + "type": "boolean" + }, + "subJsonEnable": { + "description": "Enable JSON subscription endpoint", + "type": "boolean" + }, + "subJsonFinalMask": { + "description": "JSON subscription global finalmask (tcp/udp masks + quicParams)", + "type": "string" + }, + "subJsonMux": { + "description": "JSON subscription mux configuration", + "type": "string" + }, + "subJsonPath": { + "description": "Path for JSON subscription endpoint", + "type": "string" + }, + "subJsonRules": { + "type": "string" + }, + "subJsonURI": { + "description": "JSON subscription server URI", + "type": "string" + }, + "subKeyFile": { + "description": "SSL private key file for subscription server", + "type": "string" + }, + "subListen": { + "description": "Subscription server listen IP", + "type": "string" + }, + "subPath": { + "description": "Base path for subscription URLs", + "type": "string" + }, + "subPort": { + "description": "Subscription server port", + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "subProfileUrl": { + "description": "Subscription profile URL", + "type": "string" + }, + "subRoutingRules": { + "description": "Subscription global routing rules (Only for Happ)", + "type": "string" + }, + "subShowInfo": { + "description": "Show client information in subscriptions", + "type": "boolean" + }, + "subSupportUrl": { + "description": "Subscription support URL", + "type": "string" + }, + "subTitle": { + "description": "Subscription title", + "type": "string" + }, + "subURI": { + "description": "Subscription server URI", + "type": "string" + }, + "subUpdates": { + "description": "Subscription update interval in minutes", + "maximum": 525600, + "minimum": 0, + "type": "integer" + }, + "tgBotAPIServer": { + "description": "Custom API server for Telegram bot", + "type": "string" + }, + "tgBotBackup": { + "description": "Enable database backup via Telegram", + "type": "boolean" + }, + "tgBotChatId": { + "description": "Telegram chat ID for notifications", + "type": "string" + }, + "tgBotEnable": { + "description": "Telegram bot settings\nEnable Telegram bot notifications", + "type": "boolean" + }, + "tgBotLoginNotify": { + "description": "Send login notifications", + "type": "boolean" + }, + "tgBotProxy": { + "description": "Proxy URL for Telegram bot", + "type": "string" + }, + "tgBotToken": { + "description": "Telegram bot token", + "type": "string" + }, + "tgCpu": { + "description": "CPU usage threshold for alerts (percent)", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "tgLang": { + "description": "Telegram bot language", + "type": "string" + }, + "tgRunTime": { + "description": "Cron schedule for Telegram notifications", + "type": "string" + }, + "timeLocation": { + "description": "Security settings\nTime zone location", + "type": "string" + }, + "trafficDiff": { + "description": "Traffic warning threshold percentage", + "maximum": 100, + "minimum": 0, + "type": "integer" + }, + "trustedProxyCIDRs": { + "description": "Trusted reverse proxy IPs/CIDRs for forwarded headers", + "type": "string" + }, + "twoFactorEnable": { + "description": "Enable two-factor authentication", + "type": "boolean" + }, + "twoFactorToken": { + "description": "Two-factor authentication token", + "type": "string" + }, + "webBasePath": { + "description": "Base path for web panel URLs", + "type": "string" + }, + "webCertFile": { + "description": "Path to SSL certificate file for web server", + "type": "string" + }, + "webDomain": { + "description": "Web server domain for domain validation", + "type": "string" + }, + "webKeyFile": { + "description": "Path to SSL private key file for web server", + "type": "string" + }, + "webListen": { + "description": "Web server settings\nWeb server listen IP address", + "type": "string" + }, + "webPort": { + "description": "Web server port number", + "maximum": 65535, + "minimum": 1, + "type": "integer" + } + }, + "required": [ + "datepicker", + "expireDiff", + "externalTrafficInformEnable", + "externalTrafficInformURI", + "hasApiToken", + "hasLdapPassword", + "hasNordSecret", + "hasTgBotToken", + "hasTwoFactorToken", + "hasWarpSecret", + "ldapAutoCreate", + "ldapAutoDelete", + "ldapBaseDN", + "ldapBindDN", + "ldapDefaultExpiryDays", + "ldapDefaultLimitIP", + "ldapDefaultTotalGB", + "ldapEnable", + "ldapFlagField", + "ldapHost", + "ldapInboundTags", + "ldapInvertFlag", + "ldapPassword", + "ldapPort", + "ldapSyncCron", + "ldapTruthyValues", + "ldapUseTLS", + "ldapUserAttr", + "ldapUserFilter", + "ldapVlessField", + "pageSize", + "panelProxy", + "remarkModel", + "restartXrayOnClientDisable", + "sessionMaxAge", + "subAnnounce", + "subCertFile", + "subClashEnable", + "subClashEnableRouting", + "subClashPath", + "subClashRules", + "subClashURI", + "subDomain", + "subEmailInRemark", + "subEnable", + "subEnableRouting", + "subEncrypt", + "subJsonEnable", + "subJsonFinalMask", + "subJsonMux", + "subJsonPath", + "subJsonRules", + "subJsonURI", + "subKeyFile", + "subListen", + "subPath", + "subPort", + "subProfileUrl", + "subRoutingRules", + "subShowInfo", + "subSupportUrl", + "subTitle", + "subURI", + "subUpdates", + "tgBotAPIServer", + "tgBotBackup", + "tgBotChatId", + "tgBotEnable", + "tgBotLoginNotify", + "tgBotProxy", + "tgBotToken", + "tgCpu", + "tgLang", + "tgRunTime", + "timeLocation", + "trafficDiff", + "trustedProxyCIDRs", + "twoFactorEnable", + "twoFactorToken", + "webBasePath", + "webCertFile", + "webDomain", + "webKeyFile", + "webListen", + "webPort" + ], + "type": "object" + }, + "ApiToken": { + "properties": { + "createdAt": { + "type": "integer" + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "token": { + "description": "SHA-256 hash; the plaintext is shown only once at creation", + "type": "string" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "name", + "token" + ], + "type": "object" + }, + "ApiTokenView": { + "properties": { + "createdAt": { + "example": 1736000000, + "type": "integer" + }, + "enabled": { + "example": true, + "type": "boolean" + }, + "id": { + "example": 2, + "type": "integer" + }, + "name": { + "example": "central-panel-a", + "type": "string" + }, + "token": { + "example": "new-token-string", + "type": "string" + } + }, + "required": [ + "createdAt", + "enabled", + "id", + "name" + ], + "type": "object" + }, + "Client": { + "description": "Client represents a client configuration for Xray inbounds with traffic limits and settings.", + "properties": { + "auth": { + "description": "Auth password (Hysteria)", + "type": "string" + }, + "comment": { + "description": "Client comment", + "type": "string" + }, + "created_at": { + "description": "Creation timestamp", + "type": "integer" + }, + "email": { + "description": "Client email identifier", + "type": "string" + }, + "enable": { + "description": "Whether the client is enabled", + "type": "boolean" + }, + "expiryTime": { + "description": "Expiration timestamp", + "type": "integer" + }, + "flow": { + "description": "Flow control (XTLS)", + "type": "string" + }, + "group": { + "description": "Logical grouping label", + "type": "string" + }, + "id": { + "description": "Unique client identifier", + "type": "string" + }, + "limitIp": { + "description": "IP limit for this client", + "type": "integer" + }, + "password": { + "description": "Client password", + "type": "string" + }, + "reset": { + "description": "Reset period in days", + "type": "integer" + }, + "reverse": { + "allOf": [ + { + "$ref": "#/components/schemas/ClientReverse" + } + ], + "description": "VLESS simple reverse proxy settings", + "nullable": true + }, + "security": { + "description": "Security method (e.g., \"auto\", \"aes-128-gcm\")", + "type": "string" + }, + "subId": { + "description": "Subscription identifier", + "type": "string" + }, + "tgId": { + "description": "Telegram user ID for notifications", + "type": "integer" + }, + "totalGB": { + "description": "Total traffic limit in GB", + "type": "integer" + }, + "updated_at": { + "description": "Last update timestamp", + "type": "integer" + } + }, + "required": [ + "comment", + "email", + "enable", + "expiryTime", + "limitIp", + "reset", + "security", + "subId", + "tgId", + "totalGB" + ], + "type": "object" + }, + "ClientInbound": { + "properties": { + "clientId": { + "type": "integer" + }, + "createdAt": { + "type": "integer" + }, + "flowOverride": { + "type": "string" + }, + "inboundId": { + "type": "integer" + } + }, + "required": [ + "clientId", + "createdAt", + "flowOverride", + "inboundId" + ], + "type": "object" + }, + "ClientRecord": { + "properties": { + "auth": { + "type": "string" + }, + "comment": { + "type": "string" + }, + "createdAt": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "enable": { + "type": "boolean" + }, + "expiryTime": { + "type": "integer" + }, + "flow": { + "type": "string" + }, + "group": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "limitIp": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "reset": { + "type": "integer" + }, + "reverse": {}, + "security": { + "type": "string" + }, + "subId": { + "type": "string" + }, + "tgId": { + "type": "integer" + }, + "totalGB": { + "type": "integer" + }, + "updatedAt": { + "type": "integer" + }, + "uuid": { + "type": "string" + } + }, + "required": [ + "auth", + "comment", + "createdAt", + "email", + "enable", + "expiryTime", + "flow", + "group", + "id", + "limitIp", + "password", + "reset", + "reverse", + "security", + "subId", + "tgId", + "totalGB", + "updatedAt", + "uuid" + ], + "type": "object" + }, + "ClientReverse": { + "properties": { + "tag": { + "type": "string" + } + }, + "required": [ + "tag" + ], + "type": "object" + }, + "ClientTraffic": { + "description": "ClientTraffic represents traffic statistics and limits for a specific client.\nIt tracks upload/download usage, expiry times, and online status for inbound clients.", + "properties": { + "down": { + "example": 2097152, + "type": "integer" + }, + "email": { + "example": "user1", + "type": "string" + }, + "enable": { + "example": true, + "type": "boolean" + }, + "expiryTime": { + "example": 1735689600000, + "type": "integer" + }, + "id": { + "example": 14825, + "type": "integer" + }, + "inboundId": { + "example": 1, + "type": "integer" + }, + "lastOnline": { + "example": 1735680000000, + "type": "integer" + }, + "reset": { + "example": 0, + "type": "integer" + }, + "subId": { + "example": "i7tvdpeffi0hvvf1", + "type": "string" + }, + "total": { + "example": 10737418240, + "type": "integer" + }, + "up": { + "example": 1048576, + "type": "integer" + }, + "uuid": { + "example": "e18c9a96-71bf-48d4-933f-8b9a46d4290c", + "type": "string" + } + }, + "required": [ + "down", + "email", + "enable", + "expiryTime", + "id", + "inboundId", + "lastOnline", + "reset", + "subId", + "total", + "up", + "uuid" + ], + "type": "object" + }, + "CustomGeoResource": { + "properties": { + "alias": { + "type": "string" + }, + "createdAt": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "lastModified": { + "type": "string" + }, + "lastUpdatedAt": { + "type": "integer" + }, + "localPath": { + "type": "string" + }, + "type": { + "type": "string" + }, + "updatedAt": { + "type": "integer" + }, + "url": { + "type": "string" + } + }, + "required": [ + "alias", + "createdAt", + "id", + "lastModified", + "lastUpdatedAt", + "localPath", + "type", + "updatedAt", + "url" + ], + "type": "object" + }, + "FallbackParentInfo": { + "description": "FallbackParentInfo carries everything the frontend needs to rewrite a\nchild inbound's client link: where to connect (the master's address\nand port) and which path matched on the master's fallbacks array.\nThe frontend already has the master inbound in its dbInbounds list,\nso we only ship identifiers + the match path here.", + "properties": { + "masterId": { + "type": "integer" + }, + "path": { + "type": "string" + } + }, + "required": [ + "masterId" + ], + "type": "object" + }, + "HistoryOfSeeders": { + "description": "HistoryOfSeeders tracks which database seeders have been executed to prevent re-running.", + "properties": { + "id": { + "type": "integer" + }, + "seederName": { + "type": "string" + } + }, + "required": [ + "id", + "seederName" + ], + "type": "object" + }, + "Inbound": { + "description": "Inbound represents an Xray inbound configuration with traffic statistics and settings.", + "properties": { + "clientStats": { + "description": "Client traffic statistics", + "items": { + "$ref": "#/components/schemas/ClientTraffic" + }, + "type": "array" + }, + "down": { + "description": "Download traffic in bytes", + "type": "integer" + }, + "enable": { + "description": "Whether the inbound is enabled", + "example": true, + "type": "boolean" + }, + "expiryTime": { + "description": "Expiration timestamp", + "type": "integer" + }, + "fallbackParent": { + "allOf": [ + { + "$ref": "#/components/schemas/FallbackParentInfo" + } + ], + "description": "FallbackParent is populated by the API layer when this inbound is\nattached as a fallback child of a VLESS/Trojan TCP-TLS master.\nThe frontend uses it to rewrite client-share links so they advertise\nthe master's externally reachable endpoint instead of the child's\nloopback listen. Not persisted.", + "nullable": true + }, + "id": { + "description": "Unique identifier", + "example": 1, + "type": "integer" + }, + "lastTrafficResetTime": { + "description": "Last traffic reset timestamp", + "type": "integer" + }, + "listen": { + "description": "Xray configuration fields", + "type": "string" + }, + "nodeId": { + "nullable": true, + "type": "integer" + }, + "originNodeGuid": { + "description": "OriginNodeGuid is the panelGuid of the node that physically hosts this\ninbound, propagated up across hops (#4983). Empty for an inbound that\nlives on this panel's own xray; set to the originating node's GUID when\nthe inbound was synced from a node (kept as-is across further hops). Lets\nthe master attribute a deeply nested inbound to the real node instead of\nthe intermediate one it was fetched through.", + "type": "string" + }, + "port": { + "example": 443, + "maximum": 65535, + "minimum": 0, + "type": "integer" + }, + "protocol": { + "enum": [ + "vmess", + "vless", + "trojan", + "shadowsocks", + "wireguard", + "hysteria", + "http", + "mixed", + "tunnel", + "tun" + ], + "example": "vless", + "type": "string" + }, + "remark": { + "description": "Human-readable remark", + "example": "VLESS-443", + "type": "string" + }, + "settings": {}, + "sniffing": {}, + "streamSettings": {}, + "tag": { + "example": "in-443-tcp", + "type": "string" + }, + "total": { + "description": "Total traffic limit in bytes", + "type": "integer" + }, + "trafficReset": { + "description": "Traffic reset schedule", + "enum": [ + "never", + "hourly", + "daily", + "weekly", + "monthly" + ], + "type": "string" + }, + "up": { + "description": "Upload traffic in bytes", + "type": "integer" + } + }, + "required": [ + "clientStats", + "down", + "enable", + "expiryTime", + "id", + "lastTrafficResetTime", + "listen", + "port", + "protocol", + "remark", + "settings", + "sniffing", + "streamSettings", + "tag", + "total", + "trafficReset", + "up" + ], + "type": "object" + }, + "InboundClientIps": { + "description": "InboundClientIps stores IP addresses associated with inbound clients for access control.", + "properties": { + "clientEmail": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "ips": {} + }, + "required": [ + "clientEmail", + "id", + "ips" + ], + "type": "object" + }, + "InboundFallback": { + "properties": { + "alpn": { + "type": "string" + }, + "childId": { + "type": "integer" + }, + "dest": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "masterId": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "sortOrder": { + "type": "integer" + }, + "xver": { + "type": "integer" + } + }, + "required": [ + "alpn", + "childId", + "dest", + "id", + "masterId", + "name", + "path", + "sortOrder", + "xver" + ], + "type": "object" + }, + "InboundOption": { + "properties": { + "id": { + "example": 1, + "type": "integer" + }, + "port": { + "example": 443, + "type": "integer" + }, + "protocol": { + "example": "vless", + "type": "string" + }, + "remark": { + "example": "VLESS-443", + "type": "string" + }, + "ssMethod": { + "type": "string" + }, + "tag": { + "example": "in-443-tcp", + "type": "string" + }, + "tlsFlowCapable": { + "example": true, + "type": "boolean" + } + }, + "required": [ + "id", + "port", + "protocol", + "remark", + "ssMethod", + "tag", + "tlsFlowCapable" + ], + "type": "object" + }, + "Msg": { + "description": "Msg represents a standard API response message with success status, message text, and optional data object.", + "properties": { + "msg": { + "description": "Response message text", + "type": "string" + }, + "obj": { + "description": "Optional data object" + }, + "success": { + "description": "Indicates if the operation was successful", + "type": "boolean" + } + }, + "required": [ + "msg", + "obj", + "success" + ], + "type": "object" + }, + "Node": { + "description": "Node represents a remote 3x-ui panel registered with the central panel.\nThe central panel polls each node's existing /panel/api/server/status\nendpoint over HTTP using the per-node ApiToken to populate the runtime\nstatus fields below.", + "properties": { + "address": { + "example": "node1.example.com", + "type": "string" + }, + "allowPrivateAddress": { + "type": "boolean" + }, + "apiToken": { + "example": "abcdef0123456789", + "type": "string" + }, + "basePath": { + "example": "/", + "type": "string" + }, + "clientCount": { + "example": 27, + "type": "integer" + }, + "configDirty": { + "type": "boolean" + }, + "configDirtyAt": { + "type": "integer" + }, + "cpuPct": { + "example": 23.5, + "type": "number" + }, + "createdAt": { + "example": 1700000000, + "type": "integer" + }, + "depletedCount": { + "example": 1, + "type": "integer" + }, + "enable": { + "example": true, + "type": "boolean" + }, + "guid": { + "description": "Guid is the remote panel's stable self-identifier (its panelGuid),\nlearned from each heartbeat. It is the globally stable node identity used\nto attribute online clients/inbounds to the physical node across a chain\nof nodes (#4983); panel-local autoincrement ids don't survive a hop.\nObserved-state only — never user-edited.", + "type": "string" + }, + "id": { + "example": 1, + "type": "integer" + }, + "inboundCount": { + "example": 5, + "type": "integer" + }, + "lastError": { + "type": "string" + }, + "lastHeartbeat": { + "description": "unix seconds, 0 = never", + "example": 1700000000, + "type": "integer" + }, + "latencyMs": { + "example": 42, + "type": "integer" + }, + "memPct": { + "example": 45.1, + "type": "number" + }, + "name": { + "example": "de-fra-1", + "type": "string" + }, + "onlineCount": { + "example": 3, + "type": "integer" + }, + "panelVersion": { + "example": "v3.x.x", + "type": "string" + }, + "parentGuid": { + "description": "ParentGuid + Transitive are set only when a node is surfaced as part of a\nnode tree (#4983): direct nodes carry the master panel's own GUID, a\ntransitive sub-node carries its parent node's GUID. Transitive nodes are\nread-only projections (Id == 0, not persisted) — never edited or deployed.", + "type": "string" + }, + "pinnedCertSha256": { + "type": "string" + }, + "port": { + "example": 2053, + "maximum": 65535, + "minimum": 1, + "type": "integer" + }, + "remark": { + "type": "string" + }, + "scheme": { + "enum": [ + "http", + "https" + ], + "example": "https", + "type": "string" + }, + "status": { + "description": "Heartbeat-updated fields. UpdatedAt advances on every probe even when\nthe row is otherwise unchanged so the UI's \"last seen\" tooltip is\ntruthful without us having to read LastHeartbeat separately.\nonline|offline|unknown", + "example": "online", + "type": "string" + }, + "tlsVerifyMode": { + "enum": [ + "verify", + "skip", + "pin" + ], + "type": "string" + }, + "transitive": { + "type": "boolean" + }, + "updatedAt": { + "example": 1700000000, + "type": "integer" + }, + "uptimeSecs": { + "example": 86400, + "type": "integer" + }, + "xrayVersion": { + "example": "25.10.31", + "type": "string" + } + }, + "required": [ + "address", + "allowPrivateAddress", + "apiToken", + "basePath", + "clientCount", + "configDirty", + "configDirtyAt", + "cpuPct", + "createdAt", + "depletedCount", + "enable", + "guid", + "id", + "inboundCount", + "lastError", + "lastHeartbeat", + "latencyMs", + "memPct", + "name", + "onlineCount", + "panelVersion", + "pinnedCertSha256", + "port", + "remark", + "scheme", + "status", + "tlsVerifyMode", + "updatedAt", + "uptimeSecs", + "xrayVersion" + ], + "type": "object" + }, + "OutboundTraffics": { + "description": "OutboundTraffics tracks traffic statistics for Xray outbound connections.", + "properties": { + "down": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "tag": { + "type": "string" + }, + "total": { + "type": "integer" + }, + "up": { + "type": "integer" + } + }, + "required": [ + "down", + "id", + "tag", + "total", + "up" + ], + "type": "object" + }, + "ProbeResultUI": { + "properties": { + "cpuPct": { + "example": 12.5, + "type": "number" + }, + "error": { + "type": "string" + }, + "latencyMs": { + "example": 42, + "type": "integer" + }, + "memPct": { + "example": 45.2, + "type": "number" + }, + "panelVersion": { + "example": "v3.x.x", + "type": "string" + }, + "status": { + "example": "online", + "type": "string" + }, + "uptimeSecs": { + "example": 86400, + "type": "integer" + }, + "xrayVersion": { + "example": "25.10.31", + "type": "string" + } + }, + "required": [ + "cpuPct", + "error", + "latencyMs", + "memPct", + "panelVersion", + "status", + "uptimeSecs", + "xrayVersion" + ], + "type": "object" + }, + "Setting": { + "description": "Setting stores key-value configuration settings for the 3x-ui panel.", + "properties": { + "id": { + "type": "integer" + }, + "key": { + "type": "string" + }, + "value": { + "type": "string" + } + }, + "required": [ + "id", + "key", + "value" + ], + "type": "object" + }, + "User": { + "description": "User represents a user account in the 3x-ui panel.", + "properties": { + "id": { + "type": "integer" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "required": [ + "id", + "password", + "username" + ], + "type": "object" + } } }, "security": [ @@ -65,15 +1849,15 @@ }, { "name": "Settings", - "description": "Panel configuration and user credentials. All endpoints live under /panel/setting and require a logged-in session or Bearer token." + "description": "Panel configuration and user credentials. All endpoints live under /panel/api/setting and require a logged-in session or Bearer token." }, { "name": "API Tokens", - "description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request." + "description": "Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request — the token is a full-admin credential." }, { "name": "Xray Settings", - "description": "Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/xray." + "description": "Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/api/xray." }, { "name": "Subscription Server", @@ -297,7 +2081,12 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Inbound" + } + } } }, "example": { @@ -418,7 +2207,12 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "type": "array", + "items": { + "$ref": "#/components/schemas/InboundOption" + } + } } }, "example": { @@ -3912,7 +5706,9 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "$ref": "#/components/schemas/ClientTraffic" + } } }, "example": { @@ -4054,7 +5850,12 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Node" + } + } } }, "example": { @@ -4437,7 +6238,9 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "$ref": "#/components/schemas/ProbeResultUI" + } } }, "example": { @@ -4960,13 +6763,13 @@ } } }, - "/panel/setting/all": { + "/panel/api/setting/all": { "post": { "tags": [ "Settings" ], "summary": "Return every panel setting: web server, Telegram bot, subscription, security, LDAP. The full JSON blob that the Settings page edits.", - "operationId": "post_panel_setting_all", + "operationId": "post_panel_api_setting_all", "responses": { "200": { "description": "Successful response", @@ -4990,13 +6793,13 @@ } } }, - "/panel/setting/defaultSettings": { + "/panel/api/setting/defaultSettings": { "post": { "tags": [ "Settings" ], "summary": "Return the computed default settings based on the request host. Useful to preview what a fresh install would use.", - "operationId": "post_panel_setting_defaultSettings", + "operationId": "post_panel_api_setting_defaultSettings", "responses": { "200": { "description": "Successful response", @@ -5020,13 +6823,13 @@ } } }, - "/panel/setting/update": { + "/panel/api/setting/update": { "post": { "tags": [ "Settings" ], "summary": "Persist every setting at once. The body mirrors the shape returned by /all. Invalid values (bad ports, missing cert pairs, etc.) are rejected before write.", - "operationId": "post_panel_setting_update", + "operationId": "post_panel_api_setting_update", "requestBody": { "required": true, "content": { @@ -5060,13 +6863,13 @@ } } }, - "/panel/setting/updateUser": { + "/panel/api/setting/updateUser": { "post": { "tags": [ "Settings" ], "summary": "Change the panel admin username and password. Requires the current credentials for verification. The session is refreshed with the new values on success.", - "operationId": "post_panel_setting_updateUser", + "operationId": "post_panel_api_setting_updateUser", "requestBody": { "required": true, "content": { @@ -5130,13 +6933,13 @@ } } }, - "/panel/setting/restartPanel": { + "/panel/api/setting/restartPanel": { "post": { "tags": [ "Settings" ], "summary": "Restart the entire 3x-ui process after a 3-second grace period. The connection drops immediately; the panel comes back online ~5-10 seconds later.", - "operationId": "post_panel_setting_restartPanel", + "operationId": "post_panel_api_setting_restartPanel", "responses": { "200": { "description": "Successful response", @@ -5160,13 +6963,13 @@ } } }, - "/panel/setting/getDefaultJsonConfig": { + "/panel/api/setting/getDefaultJsonConfig": { "get": { "tags": [ "Settings" ], "summary": "Return the built-in default Xray JSON config template that ships with this panel version.", - "operationId": "get_panel_setting_getDefaultJsonConfig", + "operationId": "get_panel_api_setting_getDefaultJsonConfig", "responses": { "200": { "description": "Successful response", @@ -5190,13 +6993,13 @@ } } }, - "/panel/setting/apiTokens": { + "/panel/api/setting/apiTokens": { "get": { "tags": [ "API Tokens" ], "summary": "List every API token, enabled or not. The token value is never returned — only metadata.", - "operationId": "get_panel_setting_apiTokens", + "operationId": "get_panel_api_setting_apiTokens", "responses": { "200": { "description": "Successful response", @@ -5231,13 +7034,13 @@ } } }, - "/panel/setting/apiTokens/create": { + "/panel/api/setting/apiTokens/create": { "post": { "tags": [ "API Tokens" ], "summary": "Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.", - "operationId": "post_panel_setting_apiTokens_create", + "operationId": "post_panel_api_setting_apiTokens_create", "requestBody": { "required": true, "content": { @@ -5274,7 +7077,9 @@ "msg": { "type": "string" }, - "obj": {} + "obj": { + "$ref": "#/components/schemas/ApiTokenView" + } } }, "example": { @@ -5315,13 +7120,13 @@ } } }, - "/panel/setting/apiTokens/delete/{id}": { + "/panel/api/setting/apiTokens/delete/{id}": { "post": { "tags": [ "API Tokens" ], "summary": "Permanently delete a token. Any caller using it stops authenticating immediately.", - "operationId": "post_panel_setting_apiTokens_delete_id", + "operationId": "post_panel_api_setting_apiTokens_delete_id", "parameters": [ { "name": "id", @@ -5359,13 +7164,13 @@ } } }, - "/panel/setting/apiTokens/setEnabled/{id}": { + "/panel/api/setting/apiTokens/setEnabled/{id}": { "post": { "tags": [ "API Tokens" ], "summary": "Toggle a token enabled/disabled without deleting it. Disabled tokens are rejected by checkAPIAuth on the next request.", - "operationId": "post_panel_setting_apiTokens_setEnabled_id", + "operationId": "post_panel_api_setting_apiTokens_setEnabled_id", "parameters": [ { "name": "id", @@ -5425,13 +7230,13 @@ } } }, - "/panel/xray/": { + "/panel/api/xray/": { "post": { "tags": [ "Xray Settings" ], "summary": "Return the Xray config template (JSON string), available inbound tags, client reverse tags, and the configured outbound test URL in one response.", - "operationId": "post_panel_xray", + "operationId": "post_panel_api_xray", "responses": { "200": { "description": "Successful response", @@ -5464,13 +7269,13 @@ } } }, - "/panel/xray/getDefaultJsonConfig": { + "/panel/api/xray/getDefaultJsonConfig": { "get": { "tags": [ "Xray Settings" ], - "summary": "Return the built-in default Xray config shipped with the panel (identical to /panel/setting/getDefaultJsonConfig).", - "operationId": "get_panel_xray_getDefaultJsonConfig", + "summary": "Return the built-in default Xray config shipped with the panel (identical to /panel/api/setting/getDefaultJsonConfig).", + "operationId": "get_panel_api_xray_getDefaultJsonConfig", "responses": { "200": { "description": "Successful response", @@ -5494,13 +7299,13 @@ } } }, - "/panel/xray/getOutboundsTraffic": { + "/panel/api/xray/getOutboundsTraffic": { "get": { "tags": [ "Xray Settings" ], "summary": "Return traffic statistics for every outbound. Each outbound shows up/down/total counters.", - "operationId": "get_panel_xray_getOutboundsTraffic", + "operationId": "get_panel_api_xray_getOutboundsTraffic", "responses": { "200": { "description": "Successful response", @@ -5524,13 +7329,13 @@ } } }, - "/panel/xray/getXrayResult": { + "/panel/api/xray/getXrayResult": { "get": { "tags": [ "Xray Settings" ], "summary": "Return the most recent Xray process stdout/stderr output. Useful to check for startup errors or runtime warnings.", - "operationId": "get_panel_xray_getXrayResult", + "operationId": "get_panel_api_xray_getXrayResult", "responses": { "200": { "description": "Successful response", @@ -5554,13 +7359,13 @@ } } }, - "/panel/xray/update": { + "/panel/api/xray/update": { "post": { "tags": [ "Xray Settings" ], "summary": "Save the Xray JSON config template and optionally the outbound test URL. Both are sent as form fields.", - "operationId": "post_panel_xray_update", + "operationId": "post_panel_api_xray_update", "responses": { "200": { "description": "Successful response", @@ -5584,13 +7389,13 @@ } } }, - "/panel/xray/warp/{action}": { + "/panel/api/xray/warp/{action}": { "post": { "tags": [ "Xray Settings" ], "summary": "Manage Cloudflare Warp integration. The action parameter selects the operation.", - "operationId": "post_panel_xray_warp_action", + "operationId": "post_panel_api_xray_warp_action", "parameters": [ { "name": "action", @@ -5625,13 +7430,13 @@ } } }, - "/panel/xray/nord/{action}": { + "/panel/api/xray/nord/{action}": { "post": { "tags": [ "Xray Settings" ], "summary": "Manage NordVPN integration. The action parameter selects the operation.", - "operationId": "post_panel_xray_nord_action", + "operationId": "post_panel_api_xray_nord_action", "parameters": [ { "name": "action", @@ -5666,13 +7471,13 @@ } } }, - "/panel/xray/resetOutboundsTraffic": { + "/panel/api/xray/resetOutboundsTraffic": { "post": { "tags": [ "Xray Settings" ], "summary": "Reset traffic counters for a specific outbound by tag.", - "operationId": "post_panel_xray_resetOutboundsTraffic", + "operationId": "post_panel_api_xray_resetOutboundsTraffic", "requestBody": { "required": true, "content": { @@ -5706,13 +7511,13 @@ } } }, - "/panel/xray/testOutbound": { + "/panel/api/xray/testOutbound": { "post": { "tags": [ "Xray Settings" ], "summary": "Test an outbound configuration. Sends the outbound JSON (required), optionally all outbounds (to resolve sockopt.dialerProxy dependencies), and a mode flag.", - "operationId": "post_panel_xray_testOutbound", + "operationId": "post_panel_api_xray_testOutbound", "requestBody": { "required": true, "content": { diff --git a/frontend/src/api/queries/useAllSettings.ts b/frontend/src/api/queries/useAllSettings.ts index 593853c9..dafee8aa 100644 --- a/frontend/src/api/queries/useAllSettings.ts +++ b/frontend/src/api/queries/useAllSettings.ts @@ -8,7 +8,7 @@ import { AllSettingSchema, type AllSettingInput } from '@/schemas/setting'; import { keys } from '@/api/queryKeys'; async function fetchAllSetting(): Promise { - const msg = await HttpUtil.post('/panel/setting/all', undefined, { silent: true }); + const msg = await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true }); if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch settings'); const validated = parseMsg(msg, AllSettingSchema, 'setting/all'); return validated.obj; @@ -47,7 +47,7 @@ export function useAllSettings() { if (!body.success) { console.warn('[zod] setting/update body failed validation', body.error.issues); } - return HttpUtil.post('/panel/setting/update', body.success ? body.data : next); + return HttpUtil.post('/panel/api/setting/update', body.success ? body.data : next); }, onSuccess: (msg) => { if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.settings.all() }); diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 85cdb7c4..c619cbd0 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -142,7 +142,7 @@ async function fetchInboundOptions(): Promise { } async function fetchDefaults(): Promise> { - const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true }); + const msg = await HttpUtil.post('/panel/api/setting/defaultSettings', undefined, { silent: true }); if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults'); const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings'); return validated.obj || {}; diff --git a/frontend/src/hooks/useDatepicker.ts b/frontend/src/hooks/useDatepicker.ts index 60e78363..035b6356 100644 --- a/frontend/src/hooks/useDatepicker.ts +++ b/frontend/src/hooks/useDatepicker.ts @@ -22,7 +22,7 @@ async function loadOnce(): Promise { } pending = (async () => { try { - const msg = await HttpUtil.post('/panel/setting/defaultSettings'); + const msg = await HttpUtil.post('/panel/api/setting/defaultSettings'); if (msg?.success) { const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings'); cachedValue = validated.obj?.datepicker || 'gregorian'; diff --git a/frontend/src/hooks/useXraySetting.ts b/frontend/src/hooks/useXraySetting.ts index 13119940..bd1992af 100644 --- a/frontend/src/hooks/useXraySetting.ts +++ b/frontend/src/hooks/useXraySetting.ts @@ -72,7 +72,7 @@ export interface UseXraySettingResult { type XrayConfigPayload = z.infer; async function fetchXrayConfig(): Promise { - const msg = await HttpUtil.post('/panel/xray/', undefined, { silent: true }); + const msg = await HttpUtil.post('/panel/api/xray/', undefined, { silent: true }); if (!msg?.success) throw new Error(msg?.msg || 'Failed to load xray config'); if (typeof msg.obj !== 'string') throw new Error('Malformed xray config response: expected string'); let parsed: unknown; @@ -91,7 +91,7 @@ async function fetchXrayConfig(): Promise { } async function fetchOutboundsTraffic(): Promise { - const msg = await HttpUtil.get('/panel/xray/getOutboundsTraffic', undefined, { silent: true }); + const msg = await HttpUtil.get('/panel/api/xray/getOutboundsTraffic', undefined, { silent: true }); if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch outbounds traffic'); const validated = parseMsg(msg, OutboundTrafficListSchema, 'xray/getOutboundsTraffic'); return Array.isArray(validated.obj) ? validated.obj : []; @@ -200,7 +200,7 @@ export function useXraySetting(): UseXraySettingResult { mutationFn: async () => { const sentXraySetting = xraySettingRef.current; const sentTestUrl = outboundTestUrlRef.current || DEFAULT_TEST_URL; - const msg = await HttpUtil.post('/panel/xray/update', { + const msg = await HttpUtil.post('/panel/api/xray/update', { xraySetting: sentXraySetting, outboundTestUrl: sentTestUrl, }); @@ -217,7 +217,7 @@ export function useXraySetting(): UseXraySettingResult { const resetTrafficMut = useMutation({ mutationFn: (tag: string) => - HttpUtil.post('/panel/xray/resetOutboundsTraffic', { tag }), + HttpUtil.post('/panel/api/xray/resetOutboundsTraffic', { tag }), onSuccess: (msg) => { if (msg?.success) queryClient.invalidateQueries({ queryKey: keys.xray.outboundsTraffic() }); }, @@ -228,7 +228,7 @@ export function useXraySetting(): UseXraySettingResult { const msg = await HttpUtil.post('/panel/api/server/restartXrayService'); if (!msg?.success) return msg; await PromiseUtil.sleep(500); - const r = await HttpUtil.get('/panel/xray/getXrayResult'); + const r = await HttpUtil.get('/panel/api/xray/getXrayResult'); const validated = parseMsg(r, z.string(), 'xray/getXrayResult'); if (validated?.success) setRestartResult(validated.obj || ''); return msg; @@ -237,7 +237,7 @@ export function useXraySetting(): UseXraySettingResult { const resetDefaultMut = useMutation({ mutationFn: async (): Promise> => { - const raw = await HttpUtil.get('/panel/setting/getDefaultJsonConfig'); + const raw = await HttpUtil.get('/panel/api/setting/getDefaultJsonConfig'); return parseMsg(raw, XraySettingsValueSchema, 'setting/getDefaultJsonConfig'); }, onSuccess: (msg) => { @@ -264,7 +264,7 @@ export function useXraySetting(): UseXraySettingResult { [index]: { testing: true, result: null, mode: effMode }, })); try { - const raw = await HttpUtil.post('/panel/xray/testOutbound', { + const raw = await HttpUtil.post('/panel/api/xray/testOutbound', { outbound: JSON.stringify(outbound), allOutbounds: JSON.stringify(templateSettingsRef.current?.outbounds || []), mode: effMode, diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index 06191f69..84da5cc4 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -917,28 +917,28 @@ export const sections: readonly Section[] = [ id: 'settings', title: 'Settings', description: - 'Panel configuration and user credentials. All endpoints live under /panel/setting and require a logged-in session or Bearer token.', + 'Panel configuration and user credentials. All endpoints live under /panel/api/setting and require a logged-in session or Bearer token.', endpoints: [ { method: 'POST', - path: '/panel/setting/all', + path: '/panel/api/setting/all', summary: 'Return every panel setting: web server, Telegram bot, subscription, security, LDAP. The full JSON blob that the Settings page edits.', response: '{\n "success": true,\n "obj": {\n "webPort": 2053,\n "webCertFile": "",\n "webKeyFile": "",\n "webBasePath": "/",\n "subPort": 10882,\n "subPath": "/sub/",\n "tgBotEnable": false,\n "tgBotToken": "",\n ...\n }\n}', }, { method: 'POST', - path: '/panel/setting/defaultSettings', + path: '/panel/api/setting/defaultSettings', summary: 'Return the computed default settings based on the request host. Useful to preview what a fresh install would use.', }, { method: 'POST', - path: '/panel/setting/update', + path: '/panel/api/setting/update', summary: 'Persist every setting at once. The body mirrors the shape returned by /all. Invalid values (bad ports, missing cert pairs, etc.) are rejected before write.', body: '{\n "webPort": 2053,\n "webBasePath": "/",\n "subPort": 10882,\n "subPath": "/sub/",\n "tgBotEnable": false,\n ...\n}', }, { method: 'POST', - path: '/panel/setting/updateUser', + path: '/panel/api/setting/updateUser', summary: 'Change the panel admin username and password. Requires the current credentials for verification. The session is refreshed with the new values on success.', params: [ { name: 'oldUsername', in: 'body', type: 'string', desc: 'Current admin username.' }, @@ -950,12 +950,12 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/setting/restartPanel', + path: '/panel/api/setting/restartPanel', summary: 'Restart the entire 3x-ui process after a 3-second grace period. The connection drops immediately; the panel comes back online ~5-10 seconds later.', }, { method: 'GET', - path: '/panel/setting/getDefaultJsonConfig', + path: '/panel/api/setting/getDefaultJsonConfig', summary: 'Return the built-in default Xray JSON config template that ships with this panel version.', }, ], @@ -965,17 +965,17 @@ export const sections: readonly Section[] = [ id: 'api-tokens', title: 'API Tokens', description: - 'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request.', + 'Manage Bearer tokens used for programmatic auth (bots, central panels acting on this node, CI). Each token has a unique name and an enabled flag — disable to revoke without deleting, delete to revoke permanently. Tokens are stored as SHA-256 hashes and the plaintext is returned only once, in the create response — it cannot be retrieved afterwards, so copy it then. Send one as Authorization: Bearer <token> on any /panel/api/* request — the token is a full-admin credential.', endpoints: [ { method: 'GET', - path: '/panel/setting/apiTokens', + path: '/panel/api/setting/apiTokens', summary: 'List every API token, enabled or not. The token value is never returned — only metadata.', response: '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "default",\n "enabled": true,\n "createdAt": 1736000000\n }\n ]\n}', }, { method: 'POST', - path: '/panel/setting/apiTokens/create', + path: '/panel/api/setting/apiTokens/create', summary: 'Mint a new API token. Name must be unique and 1-64 characters; the token string is server-generated and returned only in this response — it is stored hashed and cannot be retrieved later.', params: [ { name: 'name', in: 'body', type: 'string', desc: 'Human-readable label, e.g. "central-panel-a".' }, @@ -986,7 +986,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/setting/apiTokens/delete/:id', + path: '/panel/api/setting/apiTokens/delete/:id', summary: 'Permanently delete a token. Any caller using it stops authenticating immediately.', params: [ { name: 'id', in: 'path', type: 'number', desc: 'Token row ID.' }, @@ -995,7 +995,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/setting/apiTokens/setEnabled/:id', + path: '/panel/api/setting/apiTokens/setEnabled/:id', summary: 'Toggle a token enabled/disabled without deleting it. Disabled tokens are rejected by checkAPIAuth on the next request.', params: [ { name: 'id', in: 'path', type: 'number', desc: 'Token row ID.' }, @@ -1011,32 +1011,32 @@ export const sections: readonly Section[] = [ id: 'xray-settings', title: 'Xray Settings', description: - 'Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/xray.', + 'Xray configuration template, outbound management, Warp/Nord integration, and config testing. All endpoints under /panel/api/xray.', endpoints: [ { method: 'POST', - path: '/panel/xray/', + path: '/panel/api/xray/', summary: 'Return the Xray config template (JSON string), available inbound tags, client reverse tags, and the configured outbound test URL in one response.', response: '{\n "success": true,\n "obj": {\n "xraySetting": "{...raw xray config...}",\n "inboundTags": "[\\"in-443-tcp\\"]",\n "clientReverseTags": "[]",\n "outboundTestUrl": "https://www.google.com/generate_204"\n }\n}', }, { method: 'GET', - path: '/panel/xray/getDefaultJsonConfig', - summary: 'Return the built-in default Xray config shipped with the panel (identical to /panel/setting/getDefaultJsonConfig).', + path: '/panel/api/xray/getDefaultJsonConfig', + summary: 'Return the built-in default Xray config shipped with the panel (identical to /panel/api/setting/getDefaultJsonConfig).', }, { method: 'GET', - path: '/panel/xray/getOutboundsTraffic', + path: '/panel/api/xray/getOutboundsTraffic', summary: 'Return traffic statistics for every outbound. Each outbound shows up/down/total counters.', }, { method: 'GET', - path: '/panel/xray/getXrayResult', + path: '/panel/api/xray/getXrayResult', summary: 'Return the most recent Xray process stdout/stderr output. Useful to check for startup errors or runtime warnings.', }, { method: 'POST', - path: '/panel/xray/update', + path: '/panel/api/xray/update', summary: 'Save the Xray JSON config template and optionally the outbound test URL. Both are sent as form fields.', params: [ { name: 'xraySetting', in: 'body (form)', type: 'string', desc: 'Full Xray JSON config template.' }, @@ -1045,7 +1045,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/xray/warp/:action', + path: '/panel/api/xray/warp/:action', summary: 'Manage Cloudflare Warp integration. The action parameter selects the operation.', params: [ { name: 'action', in: 'path', type: 'string', desc: 'data — return Warp stats (quota, remaining). del — delete Warp data. config — return current Warp config. reg — register a new Warp endpoint (sends privateKey, publicKey). license — set a Warp+ license key (sends license).' }, @@ -1056,7 +1056,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/xray/nord/:action', + path: '/panel/api/xray/nord/:action', summary: 'Manage NordVPN integration. The action parameter selects the operation.', params: [ { name: 'action', in: 'path', type: 'string', desc: 'countries — list available countries. servers — list servers in a country (sends countryId). reg — get NordVPN credentials (sends token). setKey — store NordVPN API key (sends key). data — return current NordVPN connection data. del — delete NordVPN data.' }, @@ -1067,7 +1067,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/xray/resetOutboundsTraffic', + path: '/panel/api/xray/resetOutboundsTraffic', summary: 'Reset traffic counters for a specific outbound by tag.', params: [ { name: 'tag', in: 'body (form)', type: 'string', desc: 'Outbound tag to reset (e.g. "proxy", "direct").' }, @@ -1076,7 +1076,7 @@ export const sections: readonly Section[] = [ }, { method: 'POST', - path: '/panel/xray/testOutbound', + path: '/panel/api/xray/testOutbound', summary: 'Test an outbound configuration. Sends the outbound JSON (required), optionally all outbounds (to resolve sockopt.dialerProxy dependencies), and a mode flag.', params: [ { name: 'outbound', in: 'body (form)', type: 'string', desc: 'JSON-encoded single outbound to test (required).' }, diff --git a/frontend/src/pages/inbounds/form/useSecurityActions.ts b/frontend/src/pages/inbounds/form/useSecurityActions.ts index d9902511..0f1c71c4 100644 --- a/frontend/src/pages/inbounds/form/useSecurityActions.ts +++ b/frontend/src/pages/inbounds/form/useSecurityActions.ts @@ -120,7 +120,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS // node's own paths (fetched through the central panel), not this panel's. const msg = typeof nodeId === 'number' ? await HttpUtil.get(`/panel/api/nodes/webCert/${nodeId}`, undefined, { silent: true }) - : await HttpUtil.post('/panel/setting/all', undefined, { silent: true }); + : await HttpUtil.post('/panel/api/setting/all', undefined, { silent: true }); if (!msg?.success) { messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty')); return; diff --git a/frontend/src/pages/inbounds/useInbounds.ts b/frontend/src/pages/inbounds/useInbounds.ts index 4446d91f..9bf04389 100644 --- a/frontend/src/pages/inbounds/useInbounds.ts +++ b/frontend/src/pages/inbounds/useInbounds.ts @@ -97,7 +97,7 @@ async function fetchLastOnlineMap(): Promise> { } async function fetchDefaultSettings(): Promise { - const msg = await HttpUtil.post('/panel/setting/defaultSettings', undefined, { silent: true }); + const msg = await HttpUtil.post('/panel/api/setting/defaultSettings', undefined, { silent: true }); if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch defaults'); const validated = parseMsg(msg, DefaultsPayloadSchema, 'setting/defaultSettings'); return validated.obj ?? {}; diff --git a/frontend/src/pages/index/BackupModal.tsx b/frontend/src/pages/index/BackupModal.tsx index bf6eb294..dbeffdc0 100644 --- a/frontend/src/pages/index/BackupModal.tsx +++ b/frontend/src/pages/index/BackupModal.tsx @@ -52,7 +52,7 @@ export default function BackupModal({ open, basePath: _basePath, onClose, onBusy } onBusy({ busy: true, tip: `${t('pages.settings.restartPanel')}…` }); - const restart = await HttpUtil.post('/panel/setting/restartPanel'); + const restart = await HttpUtil.post('/panel/api/setting/restartPanel'); if (restart?.success) { await PromiseUtil.sleep(5000); window.location.reload(); diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index ada2a629..3d12e0de 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -87,7 +87,7 @@ export default function IndexPage() { const [loadingTip, setLoadingTip] = useState(t('loading')); useEffect(() => { - HttpUtil.post<{ ipLimitEnable?: boolean }>('/panel/setting/defaultSettings').then((msg) => { + HttpUtil.post<{ ipLimitEnable?: boolean }>('/panel/api/setting/defaultSettings').then((msg) => { if (msg?.success && msg.obj) setIpLimitEnable(!!msg.obj.ipLimitEnable); }); HttpUtil.get('/panel/api/server/getPanelUpdateInfo').then((msg) => { diff --git a/frontend/src/pages/settings/SecurityTab.tsx b/frontend/src/pages/settings/SecurityTab.tsx index f564a528..7e045268 100644 --- a/frontend/src/pages/settings/SecurityTab.tsx +++ b/frontend/src/pages/settings/SecurityTab.tsx @@ -96,7 +96,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr const sendUpdateUser = useCallback(async () => { setUpdating(true); try { - const msg = await HttpUtil.post('/panel/setting/updateUser', user) as ApiMsg; + const msg = await HttpUtil.post('/panel/api/setting/updateUser', user) as ApiMsg; if (msg?.success) { await HttpUtil.post('/logout'); const basePath = window.X_UI_BASE_PATH || '/'; @@ -124,7 +124,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr const loadApiTokens = useCallback(async () => { setApiTokensLoading(true); try { - const msg = await HttpUtil.get('/panel/setting/apiTokens') as ApiMsg; + const msg = await HttpUtil.get('/panel/api/setting/apiTokens') as ApiMsg; if (msg?.success) setApiTokens(Array.isArray(msg.obj) ? msg.obj : []); } finally { setApiTokensLoading(false); @@ -156,7 +156,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr } setCreating(true); try { - const msg = await HttpUtil.post('/panel/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>; + const msg = await HttpUtil.post('/panel/api/setting/apiTokens/create', { name }) as ApiMsg<{ token?: string }>; if (msg?.success) { setCreateOpen(false); await loadApiTokens(); @@ -178,7 +178,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr cancelText: t('cancel'), okType: 'danger', onOk: async () => { - const msg = await HttpUtil.post(`/panel/setting/apiTokens/delete/${row.id}`) as ApiMsg; + const msg = await HttpUtil.post(`/panel/api/setting/apiTokens/delete/${row.id}`) as ApiMsg; if (msg?.success) await loadApiTokens(); }, }); @@ -186,7 +186,7 @@ export default function SecurityTab({ allSetting, updateSetting }: SecurityTabPr async function toggleTokenEnabled(row: ApiTokenRow) { const target = !row.enabled; - const msg = await HttpUtil.post(`/panel/setting/apiTokens/setEnabled/${row.id}`, { enabled: target }) as ApiMsg; + const msg = await HttpUtil.post(`/panel/api/setting/apiTokens/setEnabled/${row.id}`, { enabled: target }) as ApiMsg; if (msg?.success) { setApiTokens((prev) => prev.map((r) => (r.id === row.id ? { ...r, enabled: target } : r))); } diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index 7e0b8403..207abaef 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -142,7 +142,7 @@ export default function SettingsPage() { onOk: async () => { setSpinning(true); try { - const msg = await HttpUtil.post('/panel/setting/restartPanel') as ApiMsg; + const msg = await HttpUtil.post('/panel/api/setting/restartPanel') as ApiMsg; if (!msg?.success) return; await PromiseUtil.sleep(5000); window.location.replace(rebuildUrlAfterRestart()); diff --git a/frontend/src/pages/xray/overrides/NordModal.tsx b/frontend/src/pages/xray/overrides/NordModal.tsx index 23f0af12..efde508e 100644 --- a/frontend/src/pages/xray/overrides/NordModal.tsx +++ b/frontend/src/pages/xray/overrides/NordModal.tsx @@ -88,14 +88,14 @@ export default function NordModal({ }, [filteredServers]); const fetchCountries = useCallback(async () => { - const msg = await HttpUtil.post('/panel/xray/nord/countries'); + const msg = await HttpUtil.post('/panel/api/xray/nord/countries'); if (msg?.success && msg.obj) setCountries(JSON.parse(msg.obj)); }, []); const fetchData = useCallback(async () => { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/nord/data'); + const msg = await HttpUtil.post('/panel/api/xray/nord/data'); if (msg?.success) { const next = msg.obj ? JSON.parse(msg.obj) : null; setNordData(next); @@ -113,7 +113,7 @@ export default function NordModal({ async function login() { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/nord/reg', { token }); + const msg = await HttpUtil.post('/panel/api/xray/nord/reg', { token }); if (msg?.success && msg.obj) { setNordData(JSON.parse(msg.obj)); await fetchCountries(); @@ -126,7 +126,7 @@ export default function NordModal({ async function saveKey() { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: manualKey }); + const msg = await HttpUtil.post('/panel/api/xray/nord/setKey', { key: manualKey }); if (msg?.success && msg.obj) { setNordData(JSON.parse(msg.obj)); await fetchCountries(); @@ -139,7 +139,7 @@ export default function NordModal({ async function logout() { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/nord/del'); + const msg = await HttpUtil.post('/panel/api/xray/nord/del'); if (msg?.success) { onRemoveOutbound(nordOutboundIndex); onRemoveRoutingRules({ prefix: 'nord-' }); @@ -166,7 +166,7 @@ export default function NordModal({ setServerId(null); setCityId(null); try { - const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: newCountryId }); + const msg = await HttpUtil.post('/panel/api/xray/nord/servers', { countryId: newCountryId }); if (!msg?.success || !msg.obj) return; const data = JSON.parse(msg.obj); const locations = data.locations || []; diff --git a/frontend/src/pages/xray/overrides/WarpModal.tsx b/frontend/src/pages/xray/overrides/WarpModal.tsx index 6ba217dd..246b031f 100644 --- a/frontend/src/pages/xray/overrides/WarpModal.tsx +++ b/frontend/src/pages/xray/overrides/WarpModal.tsx @@ -111,7 +111,7 @@ export default function WarpModal({ const fetchData = useCallback(async () => { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/warp/data'); + const msg = await HttpUtil.post('/panel/api/xray/warp/data'); if (msg?.success) { const raw = msg.obj; setWarpData(raw && raw.length > 0 ? JSON.parse(raw) : null); @@ -133,7 +133,7 @@ export default function WarpModal({ setLoading(true); try { const keys = Wireguard.generateKeypair(); - const msg = await HttpUtil.post('/panel/xray/warp/reg', keys); + const msg = await HttpUtil.post('/panel/api/xray/warp/reg', keys); if (msg?.success && msg.obj) { const resp = JSON.parse(msg.obj); setWarpData(resp.data); @@ -148,7 +148,7 @@ export default function WarpModal({ async function getConfig() { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/warp/config'); + const msg = await HttpUtil.post('/panel/api/xray/warp/config'); if (msg?.success && msg.obj) { const parsed = JSON.parse(msg.obj); setWarpConfig(parsed); @@ -164,7 +164,7 @@ export default function WarpModal({ setLoading(true); setLicenseError(''); try { - const msg = await HttpUtil.post('/panel/xray/warp/license', { license: warpPlus }); + const msg = await HttpUtil.post('/panel/api/xray/warp/license', { license: warpPlus }); if (msg?.success && msg.obj) { setWarpData(JSON.parse(msg.obj)); setWarpConfig(null); @@ -180,7 +180,7 @@ export default function WarpModal({ async function delConfig() { setLoading(true); try { - const msg = await HttpUtil.post('/panel/xray/warp/del'); + const msg = await HttpUtil.post('/panel/api/xray/warp/del'); if (msg?.success) { setWarpData(null); setWarpConfig(null); diff --git a/frontend/vite.config.js b/frontend/vite.config.js index e7f01534..e8e84355 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -22,7 +22,7 @@ function resolveDBPath() { return '/etc/x-ui/x-ui.db'; } -const PANEL_API_PREFIXES = ['panel/api/', 'panel/setting/', 'panel/xray/', 'panel/csrf-token']; +const PANEL_API_PREFIXES = ['panel/api/', 'panel/csrf-token']; let cachedBasePath = '/'; diff --git a/web/controller/api.go b/web/controller/api.go index 06d35441..fd1058b7 100644 --- a/web/controller/api.go +++ b/web/controller/api.go @@ -14,13 +14,15 @@ import ( // APIController handles the main API routes for the 3x-ui panel, including inbounds and server management. type APIController struct { BaseController - inboundController *InboundController - serverController *ServerController - nodeController *NodeController - settingService service.SettingService - userService service.UserService - apiTokenService service.ApiTokenService - Tgbot service.Tgbot + inboundController *InboundController + serverController *ServerController + nodeController *NodeController + settingController *SettingController + xraySettingController *XraySettingController + settingService service.SettingService + userService service.UserService + apiTokenService service.ApiTokenService + Tgbot service.Tgbot } // NewAPIController creates a new APIController instance and initializes its routes. @@ -79,6 +81,12 @@ func (a *APIController) initRouter(g *gin.RouterGroup, customGeo *service.Custom NewCustomGeoController(api.Group("/custom-geo"), customGeo) + // Settings + Xray config management live under the API surface too, so the + // same API token drives them. Paths are /panel/api/setting/* and + // /panel/api/xray/*. + a.settingController = NewSettingController(api) + a.xraySettingController = NewXraySettingController(api) + // Extra routes api.POST("/backuptotgbot", a.BackuptoTgbot) } diff --git a/web/controller/api_docs_test.go b/web/controller/api_docs_test.go index 583d00a7..53d1412d 100644 --- a/web/controller/api_docs_test.go +++ b/web/controller/api_docs_test.go @@ -96,9 +96,9 @@ func TestAPIRoutesDocumented(t *testing.T) { case "node.go": basePath = "/panel/api/nodes" case "setting.go": - basePath = "/panel/setting" + basePath = "/panel/api/setting" case "xray_setting.go": - basePath = "/panel/xray" + basePath = "/panel/api/xray" case "custom_geo.go": basePath = "/panel/api/custom-geo" case "websocket.go": diff --git a/web/controller/xui.go b/web/controller/xui.go index d7d68c7e..7a82ea1f 100644 --- a/web/controller/xui.go +++ b/web/controller/xui.go @@ -10,12 +10,9 @@ import ( "github.com/gin-gonic/gin" ) -// XUIController is the main controller for the X-UI panel, managing sub-controllers. +// XUIController is the main controller for the X-UI panel, serving the SPA shell. type XUIController struct { BaseController - - settingController *SettingController - xraySettingController *XraySettingController } // NewXUIController creates a new XUIController and initializes its routes. @@ -49,9 +46,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { // so they fetch the session token via this endpoint at startup and replay it // on subsequent unsafe requests through axios. g.GET("/csrf-token", a.csrfToken) - - a.settingController = NewSettingController(g) - a.xraySettingController = NewXraySettingController(g) } // panelSPA serves the React SPA shell. Every GET under /panel/ that isn't an