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