From e642f7324e54c08e95577292830304e133a63c80 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 11 May 2026 13:57:42 +0200 Subject: [PATCH] feat(panel): in-panel API documentation page New /panel/api-docs route with a one-page reference covering every /panel/api/* endpoint (Auth, Inbounds, Server, Nodes, Custom Geo, Backup) plus a Bearer-token primer that reads the current token and exposes Show/Copy/Regenerate inline. Sidebar gets an API Docs entry right after Xray; the menu label is shared via menu.apiDocs across all 13 locales. --- frontend/api-docs.html | 13 + frontend/src/components/AppSidebar.vue | 93 +--- frontend/src/entries/api-docs.js | 17 + frontend/src/pages/api-docs/ApiDocsPage.vue | 339 +++++++++++ frontend/src/pages/api-docs/EndpointRow.vue | 128 +++++ .../src/pages/api-docs/EndpointSection.vue | 65 +++ frontend/src/pages/api-docs/endpoints.js | 525 ++++++++++++++++++ frontend/vite.config.js | 3 + web/controller/xui.go | 6 + web/translation/ar-EG.json | 1 + web/translation/en-US.json | 1 + web/translation/es-ES.json | 1 + web/translation/fa-IR.json | 1 + web/translation/id-ID.json | 1 + web/translation/ja-JP.json | 1 + web/translation/pt-BR.json | 1 + web/translation/ru-RU.json | 1 + web/translation/tr-TR.json | 1 + web/translation/uk-UA.json | 1 + web/translation/vi-VN.json | 1 + web/translation/zh-CN.json | 1 + web/translation/zh-TW.json | 1 + 22 files changed, 1113 insertions(+), 89 deletions(-) create mode 100644 frontend/api-docs.html create mode 100644 frontend/src/entries/api-docs.js create mode 100644 frontend/src/pages/api-docs/ApiDocsPage.vue create mode 100644 frontend/src/pages/api-docs/EndpointRow.vue create mode 100644 frontend/src/pages/api-docs/EndpointSection.vue create mode 100644 frontend/src/pages/api-docs/endpoints.js diff --git a/frontend/api-docs.html b/frontend/api-docs.html new file mode 100644 index 00000000..9f35080a --- /dev/null +++ b/frontend/api-docs.html @@ -0,0 +1,13 @@ + + + + + + 3x-ui · API Docs + + +
+
+ + + diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index 8152a242..7763b715 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -10,6 +10,7 @@ import { LogoutOutlined, CloseOutlined, MenuOutlined, + ApiOutlined, } from '@ant-design/icons-vue'; import { theme, currentTheme, toggleTheme, toggleUltra, pauseAnimationsUntilLeave } from '@/composables/useTheme.js'; @@ -19,17 +20,12 @@ const { t } = useI18n(); const SIDEBAR_COLLAPSED_KEY = 'isSidebarCollapsed'; const props = defineProps({ - // Path prefix (e.g. /custom-base/) the panel is served under. Defaults - // to '' which means tab keys end up as '/panel/...'. Pages pass the - // value the Go backend gave them (in production via a meta tag). basePath: { type: String, default: '' }, // Current request URI so the matching menu item highlights. requestUri: { type: String, default: '' }, }); -// AD-Vue 4 dropped in favor of explicit icon -// imports — keep a small name-to-component map so tab definitions stay -// declarative. + const iconByName = { dashboard: DashboardOutlined, user: UserOutlined, @@ -37,41 +33,26 @@ const iconByName = { tool: ToolOutlined, cluster: ClusterOutlined, logout: LogoutOutlined, + apidocs: ApiOutlined, }; -// basePath comes from Go (`/` by default, `/myprefix/` when configured) so -// these concatenations land on absolute paths. In dev we synthesize the prop -// from a window global which can be empty — force a leading slash so the -// browser doesn't resolve the link relative to the current pathname (which -// would turn /panel/settings + 'panel/...' into /panel/panel/...). const prefix = props.basePath?.startsWith('/') ? props.basePath : `/${props.basePath || ''}`; -// Labels are i18n-driven so the sidebar matches the locale picked -// in panel settings without a page reload of the sidebar component. const tabs = computed(() => [ { key: `${prefix}panel/`, icon: 'dashboard', title: t('menu.dashboard') }, { key: `${prefix}panel/inbounds`, icon: 'user', title: t('menu.inbounds') }, { key: `${prefix}panel/nodes`, icon: 'cluster', title: t('menu.nodes') }, { key: `${prefix}panel/settings`, icon: 'setting', title: t('menu.settings') }, { key: `${prefix}panel/xray`, icon: 'tool', title: t('menu.xray') }, + { key: `${prefix}panel/api-docs`, icon: 'apidocs', title: t('menu.apiDocs') }, { key: `${prefix}logout`, icon: 'logout', title: t('logout') }, ]); -// Logout sits in its own pinned-to-bottom block on the drawer; the -// remaining items are the navigation proper. The full-height sider on -// desktop still uses `tabs` as-is so the desktop look is unchanged. const navTabs = computed(() => tabs.value.filter((tab) => tab.icon !== 'logout')); const utilTabs = computed(() => tabs.value.filter((tab) => tab.icon === 'logout')); - const activeTab = ref([props.requestUri]); - const drawerOpen = ref(false); const collapsed = ref(JSON.parse(localStorage.getItem(SIDEBAR_COLLAPSED_KEY) || 'false')); - -// Drawer width is capped against the viewport — AD-Vue's default 378px -// overflows on narrow phones (e.g. 360px portrait), leaving the page -// hidden behind the mask. `min()` keeps it sane on both phones and -// tablets while never exceeding 320px on larger displays. const drawerWidth = 'min(82vw, 320px)'; function openLink(key) { @@ -98,12 +79,6 @@ function closeDrawer() { drawerOpen.value = false; } -/* 3-state theme cycle driven by the brand-row icon button. - * Light → Dark (turn dark on, ensure ultra off) - * Dark → Ultra (turn ultra on) - * Ultra → Light (turn ultra off, turn dark off) - * Using a single button keeps the sider header clean — the old - * ThemeSwitch a-sub-menu plus its expandable items lived here. */ function cycleTheme() { pauseAnimationsUntilLeave('theme-cycle'); if (!theme.isDark) { @@ -212,13 +187,6 @@ function cycleTheme() { + + diff --git a/frontend/src/pages/api-docs/EndpointRow.vue b/frontend/src/pages/api-docs/EndpointRow.vue new file mode 100644 index 00000000..0b7fb300 --- /dev/null +++ b/frontend/src/pages/api-docs/EndpointRow.vue @@ -0,0 +1,128 @@ + + + + + + + diff --git a/frontend/src/pages/api-docs/EndpointSection.vue b/frontend/src/pages/api-docs/EndpointSection.vue new file mode 100644 index 00000000..a015795e --- /dev/null +++ b/frontend/src/pages/api-docs/EndpointSection.vue @@ -0,0 +1,65 @@ + + + + + + + diff --git a/frontend/src/pages/api-docs/endpoints.js b/frontend/src/pages/api-docs/endpoints.js new file mode 100644 index 00000000..47929738 --- /dev/null +++ b/frontend/src/pages/api-docs/endpoints.js @@ -0,0 +1,525 @@ +export const sections = [ + { + id: 'auth', + title: 'Authentication', + description: + 'Two authentication modes are supported. UI sessions use a cookie set by the login endpoint. Programmatic clients (bots, scripts, remote panels) authenticate with a Bearer token taken from Settings → Security → API Token. Both work for every endpoint under /panel/api/*.', + endpoints: [ + { + method: 'POST', + path: '/login', + summary: 'Authenticate with username + password and receive a session cookie. Required before any cookie-based API call.', + params: [ + { name: 'username', in: 'body', type: 'string', desc: 'Panel admin username.' }, + { name: 'password', in: 'body', type: 'string', desc: 'Panel admin password.' }, + { name: 'twoFactorCode', in: 'body', type: 'string', desc: 'OTP code when 2FA is enabled. Omit otherwise.' }, + ], + body: '{\n "username": "admin",\n "password": "admin",\n "twoFactorCode": "123456"\n}', + response: + '{\n "success": true,\n "msg": "Logged in successfully"\n}', + }, + { + method: 'GET', + path: '/logout', + summary: 'Clear the session cookie. Redirects back to the login page; not useful from non-browser clients.', + }, + { + method: 'GET', + path: '/csrf-token', + summary: 'Mint a CSRF token for the current session. The SPA replays it in the X-CSRF-Token header on unsafe requests. Bearer-token callers can skip this — the middleware short-circuits CSRF for authenticated API requests.', + response: + '{\n "success": true,\n "obj": "csrf-token-string"\n}', + }, + { + method: 'POST', + path: '/getTwoFactorEnable', + summary: 'Returns whether 2FA is enabled on the panel — used by the login page to decide whether to show the OTP field.', + response: '{\n "success": true,\n "obj": false\n}', + }, + ], + }, + + { + id: 'inbounds', + title: 'Inbounds API', + description: + 'Manage inbound configurations and their clients. All endpoints live under /panel/api/inbounds and require a logged-in session or Bearer token.', + endpoints: [ + { + method: 'GET', + path: '/panel/api/inbounds/list', + summary: 'List every inbound owned by the authenticated user, including each inbound’s clientStats traffic counters.', + response: + '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "userId": 1,\n "up": 0,\n "down": 0,\n "total": 0,\n "remark": "VLESS-443",\n "enable": true,\n "expiryTime": 0,\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "settings": "{\\"clients\\":[...]}",\n "streamSettings": "{...}",\n "tag": "inbound-443",\n "sniffing": "{...}",\n "clientStats": [...]\n }\n ]\n}', + }, + { + method: 'GET', + path: '/panel/api/inbounds/get/:id', + summary: 'Fetch a single inbound by numeric ID.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + ], + }, + { + method: 'GET', + path: '/panel/api/inbounds/getClientTraffics/:email', + summary: 'Traffic counters for a client identified by email.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email (unique across the panel).' }, + ], + }, + { + method: 'GET', + path: '/panel/api/inbounds/getClientTrafficsById/:id', + summary: 'Traffic counters for a client identified by its UUID/password.', + params: [ + { name: 'id', in: 'path', type: 'string', desc: 'Client subId / UUID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/add', + summary: 'Create a new inbound. Send the full inbound payload (protocol, port, settings JSON, streamSettings JSON, sniffing JSON, remark, expiryTime, total, enable).', + body: + '{\n "enable": true,\n "remark": "VLESS-443",\n "listen": "",\n "port": 443,\n "protocol": "vless",\n "expiryTime": 0,\n "total": 0,\n "settings": "{\\"clients\\":[{\\"id\\":\\"...\\",\\"email\\":\\"user1\\"}],\\"decryption\\":\\"none\\",\\"fallbacks\\":[]}",\n "streamSettings": "{\\"network\\":\\"tcp\\",\\"security\\":\\"reality\\",\\"realitySettings\\":{...}}",\n "sniffing": "{\\"enabled\\":true,\\"destOverride\\":[\\"http\\",\\"tls\\"]}"\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/del/:id', + summary: 'Delete an inbound by ID. Also removes its associated client stats rows.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/update/:id', + summary: 'Replace an inbound’s configuration. Body shape mirrors /add. Heavy on inbounds with thousands of clients — prefer /setEnable for enable-only flips.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/setEnable/:id', + summary: 'Toggle only the enable flag without serialising the whole settings JSON. Recommended for UI switches on large inbounds.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + ], + body: '{\n "enable": false\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/clientIps/:email', + summary: 'List source IPs that have connected with the given client’s credentials. Returns an array of "ip (timestamp)" strings.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/clearClientIps/:email', + summary: 'Reset the recorded IP list for a client.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/addClient', + summary: 'Add one or more clients to an existing inbound. The settings field is the JSON-encoded settings.clients array of the target inbound.', + body: + '{\n "id": 1,\n "settings": "{\\"clients\\":[{\\"id\\":\\"uuid-here\\",\\"email\\":\\"newuser\\",\\"limitIp\\":0,\\"totalGB\\":0,\\"expiryTime\\":0,\\"enable\\":true,\\"flow\\":\\"\\"}]}"\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/:id/copyClients', + summary: 'Copy selected clients from one inbound into another. Useful for duplicating user lists across protocols.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Target inbound ID.' }, + { name: 'sourceInboundId', in: 'body', type: 'number', desc: 'Inbound ID to read clients from.' }, + { name: 'clientEmails', in: 'body', type: 'string[]', desc: 'Emails of clients to copy. Empty means all clients.' }, + { name: 'flow', in: 'body', type: 'string', desc: 'Override the flow field on copied clients (e.g. "xtls-rprx-vision"). Empty to keep source flow.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/:id/delClient/:clientId', + summary: 'Delete a client by its UUID/password from a specific inbound.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + { name: 'clientId', in: 'path', type: 'string', desc: 'Client UUID / password.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/updateClient/:clientId', + summary: 'Update a single client without rewriting the whole settings JSON. Send the target inbound payload with the new client values.', + params: [ + { name: 'clientId', in: 'path', type: 'string', desc: 'Client UUID / password.' }, + ], + body: + '{\n "id": 1,\n "settings": "{\\"clients\\":[{\\"id\\":\\"uuid-here\\",\\"email\\":\\"user1\\",\\"limitIp\\":2,\\"totalGB\\":10737418240,\\"expiryTime\\":1735689600000,\\"enable\\":true}]}"\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/:id/resetClientTraffic/:email', + summary: 'Zero out upload + download counters for one client.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/resetAllTraffics', + summary: 'Reset upload + download counters on every inbound. Destructive — accounting history is lost.', + }, + { + method: 'POST', + path: '/panel/api/inbounds/resetAllClientTraffics/:id', + summary: 'Reset traffic for every client in one inbound.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/delDepletedClients/:id', + summary: 'Delete clients in this inbound whose traffic cap or expiry has elapsed. Pass id=-1 to sweep every inbound.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID, or -1 for all inbounds.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/import', + summary: 'Bulk-import an inbound from a JSON blob (e.g. one exported via the UI). The body uses form encoding with a single "data" field.', + params: [ + { name: 'data', in: 'body (form)', type: 'string', desc: 'JSON-encoded inbound payload.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/inbounds/onlines', + summary: 'List the emails of currently connected clients (last seen within the heartbeat window).', + response: '{\n "success": true,\n "obj": ["user1", "user2"]\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/lastOnline', + summary: 'Map of client email → last-seen unix timestamp.', + }, + { + method: 'POST', + path: '/panel/api/inbounds/updateClientTraffic/:email', + summary: 'Manually adjust a client’s upload + download counters. Useful for migrations from external accounting systems.', + params: [ + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + body: '{\n "upload": 1073741824,\n "download": 5368709120\n}', + }, + { + method: 'POST', + path: '/panel/api/inbounds/:id/delClientByEmail/:email', + summary: 'Delete a client identified by email rather than UUID.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Inbound ID.' }, + { name: 'email', in: 'path', type: 'string', desc: 'Client email.' }, + ], + }, + ], + }, + + { + id: 'server', + title: 'Server API', + description: + 'System status, log retrieval, certificate generators, Xray binary management, and backup/restore. All under /panel/api/server.', + endpoints: [ + { + method: 'GET', + path: '/panel/api/server/status', + summary: 'Real-time machine snapshot: CPU, memory, swap, disk, network IO, load averages, open connections, Xray state. Cached and refreshed every 2 seconds in the background.', + }, + { + method: 'GET', + path: '/panel/api/server/cpuHistory/:bucket', + summary: 'Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same data with a uniform {t, v} shape.', + params: [ + { name: 'bucket', in: 'path', type: 'number', desc: 'Bucket size in seconds. Allowed: 2, 30, 60, 120, 180, 300.' }, + ], + }, + { + method: 'GET', + path: '/panel/api/server/history/:metric/:bucket', + summary: 'Aggregated time-series for one metric. Returns an array of {t, v} samples covering the last ~6 hours.', + params: [ + { name: 'metric', in: 'path', type: 'string', desc: 'cpu | mem | swap | netIn | netOut | tcpCount | udpCount | load1 | online.' }, + { name: 'bucket', in: 'path', type: 'number', desc: 'Bucket size in seconds. Allowed: 2, 30, 60, 120, 180, 300.' }, + ], + }, + { + method: 'GET', + path: '/panel/api/server/getXrayVersion', + summary: 'List Xray binary versions available for install on this host.', + }, + { + method: 'GET', + path: '/panel/api/server/getPanelUpdateInfo', + summary: 'Check whether a newer 3x-ui release is available on GitHub.', + }, + { + method: 'GET', + path: '/panel/api/server/getConfigJson', + summary: 'Return the assembled Xray config that’s currently running on this host.', + }, + { + method: 'GET', + path: '/panel/api/server/getDb', + summary: 'Stream the SQLite database file as an attachment. Use as a manual backup.', + }, + { + method: 'GET', + path: '/panel/api/server/getNewUUID', + summary: 'Generate a fresh UUID v4. Convenience helper for client IDs.', + }, + { + method: 'GET', + path: '/panel/api/server/getNewX25519Cert', + summary: 'Generate a new X25519 keypair for Reality.', + }, + { + method: 'GET', + path: '/panel/api/server/getNewmldsa65', + summary: 'Generate a new ML-DSA-65 keypair (post-quantum signature). Returns {privateKey, publicKey, seed}.', + }, + { + method: 'GET', + path: '/panel/api/server/getNewmlkem768', + summary: 'Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns {clientKey, serverKey}.', + }, + { + method: 'GET', + path: '/panel/api/server/getNewVlessEnc', + summary: 'Generate a new VLESS encryption keypair.', + }, + { + method: 'POST', + path: '/panel/api/server/stopXrayService', + summary: 'Stop the Xray binary. All proxies go offline immediately.', + }, + { + method: 'POST', + path: '/panel/api/server/restartXrayService', + summary: 'Reload Xray with the current config. Typically required after structural inbound or routing changes.', + }, + { + method: 'POST', + path: '/panel/api/server/installXray/:version', + summary: 'Download and install the specified Xray version. Pass "latest" for the newest release.', + params: [ + { name: 'version', in: 'path', type: 'string', desc: 'Xray tag (e.g. v25.10.31) or "latest".' }, + ], + }, + { + method: 'POST', + path: '/panel/api/server/updatePanel', + summary: 'Self-update the panel to the latest version. The server restarts on success.', + }, + { + method: 'POST', + path: '/panel/api/server/updateGeofile', + summary: 'Refresh the default GeoIP / GeoSite data files. Body can include a fileName, or use the /:fileName variant.', + }, + { + method: 'POST', + path: '/panel/api/server/updateGeofile/:fileName', + summary: 'Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).', + params: [ + { name: 'fileName', in: 'path', type: 'string', desc: 'Filename of the data file to refresh.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/server/logs/:count', + summary: 'Return the last N lines of the panel’s own log.', + params: [ + { name: 'count', in: 'path', type: 'number', desc: 'Number of trailing log lines.' }, + ], + body: '{\n "level": "info",\n "syslog": false\n}', + }, + { + method: 'POST', + path: '/panel/api/server/xraylogs/:count', + summary: 'Return the last N lines of the Xray process log.', + params: [ + { name: 'count', in: 'path', type: 'number', desc: 'Number of trailing log lines.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/server/importDB', + summary: 'Restore the panel DB from an uploaded SQLite file (multipart form, field name "db"). The panel restarts after restore. Destructive.', + }, + { + method: 'POST', + path: '/panel/api/server/getNewEchCert', + summary: 'Generate a new ECH (Encrypted Client Hello) keypair. Body picks the algorithm.', + }, + ], + }, + + { + id: 'nodes', + title: 'Nodes API', + description: + 'Manage remote 3x-ui panels acting as nodes for a central panel. All endpoints under /panel/api/nodes.', + endpoints: [ + { + method: 'GET', + path: '/panel/api/nodes/list', + summary: 'List every configured node with its connection details, health, and last heartbeat patch.', + }, + { + method: 'GET', + path: '/panel/api/nodes/get/:id', + summary: 'Fetch a single node by ID.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/nodes/add', + summary: 'Register a new remote node. Provide its URL, apiToken, and optional label/notes.', + body: + '{\n "name": "de-fra-1",\n "scheme": "https",\n "host": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef..."\n}', + }, + { + method: 'POST', + path: '/panel/api/nodes/update/:id', + summary: 'Replace a node’s connection details. Same body shape as /add.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/nodes/del/:id', + summary: 'Delete a node. Inbounds bound to it are not auto-migrated.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/nodes/setEnable/:id', + summary: 'Pause or resume traffic sync with this node.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + ], + body: '{\n "enable": true\n}', + }, + { + method: 'POST', + path: '/panel/api/nodes/test', + summary: 'Probe a node without saving it. Uses the body as connection details and returns whether the handshake succeeds.', + }, + { + method: 'POST', + path: '/panel/api/nodes/probe/:id', + summary: 'Probe an existing node, updating its cached health state.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + ], + }, + { + method: 'GET', + path: '/panel/api/nodes/history/:id/:metric/:bucket', + summary: 'Aggregated metric history for a node — same shape as /server/history, scoped to one node.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Node ID.' }, + { name: 'metric', in: 'path', type: 'string', desc: 'Metric key (cpu, mem, netIn, …).' }, + { name: 'bucket', in: 'path', type: 'number', desc: 'Bucket size in seconds.' }, + ], + }, + ], + }, + + { + id: 'customGeo', + title: 'Custom Geo API', + description: + 'Manage user-supplied GeoIP / GeoSite source files. All endpoints under /panel/api/custom-geo.', + endpoints: [ + { + method: 'GET', + path: '/panel/api/custom-geo/list', + summary: 'List configured custom geo sources with their type, alias, URL, status, and last-download timestamp.', + }, + { + method: 'GET', + path: '/panel/api/custom-geo/aliases', + summary: 'List geo aliases currently usable in routing rules — both built-in defaults and the user-configured ones.', + }, + { + method: 'POST', + path: '/panel/api/custom-geo/add', + summary: 'Register a custom geo source. Alias is auto-normalised; URL must point to a .dat / .json blob.', + body: + '{\n "type": "geoip",\n "alias": "myips",\n "url": "https://example.com/geo/my.dat"\n}', + }, + { + method: 'POST', + path: '/panel/api/custom-geo/update/:id', + summary: 'Replace a custom geo source. Same body shape as /add.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Custom geo source ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/custom-geo/delete/:id', + summary: 'Remove a custom geo source and its cached file.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Custom geo source ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/custom-geo/download/:id', + summary: 'Re-download one custom geo source on demand.', + params: [ + { name: 'id', in: 'path', type: 'number', desc: 'Custom geo source ID.' }, + ], + }, + { + method: 'POST', + path: '/panel/api/custom-geo/update-all', + summary: 'Re-download every configured custom geo source. Errors are reported per-source in the response.', + }, + ], + }, + + { + id: 'backup', + title: 'Backup', + description: 'Operations that interact with the configured Telegram bot.', + endpoints: [ + { + method: 'GET', + path: '/panel/api/backuptotgbot', + summary: 'Send a fresh DB backup to every Telegram chat configured as an admin recipient. No body, no params.', + }, + ], + }, +]; + +export const methodColors = { + GET: 'blue', + POST: 'green', + PUT: 'orange', + PATCH: 'orange', + DELETE: 'red', +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index da0a1ba2..91d42c19 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -26,6 +26,8 @@ const BASE_MIGRATED_ROUTES = { 'panel/xray/': '/xray.html', 'panel/nodes': '/nodes.html', 'panel/nodes/': '/nodes.html', + 'panel/api-docs': '/api-docs.html', + 'panel/api-docs/': '/api-docs.html', }; let cachedBasePath = '/'; @@ -150,6 +152,7 @@ export default defineConfig({ inbounds: path.resolve(__dirname, 'inbounds.html'), xray: path.resolve(__dirname, 'xray.html'), nodes: path.resolve(__dirname, 'nodes.html'), + apiDocs: path.resolve(__dirname, 'api-docs.html'), subpage: path.resolve(__dirname, 'subpage.html'), }, output: { diff --git a/web/controller/xui.go b/web/controller/xui.go index d385cd50..2fcf346b 100644 --- a/web/controller/xui.go +++ b/web/controller/xui.go @@ -36,6 +36,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) { g.GET("/nodes", a.nodes) g.GET("/settings", a.settings) g.GET("/xray", a.xraySettings) + g.GET("/api-docs", a.apiDocs) // SPA pages built by Vite don't have a server-rendered , // so they fetch the session token via this endpoint at startup and replay it @@ -76,6 +77,11 @@ func (a *XUIController) xraySettings(c *gin.Context) { serveDistPage(c, "xray.html") } +// apiDocs renders the in-panel API documentation page. +func (a *XUIController) apiDocs(c *gin.Context) { + serveDistPage(c, "api-docs.html") +} + // csrfToken returns the session CSRF token to authenticated SPA clients. // The endpoint is GET (a safe method) so it bypasses CSRFMiddleware itself, // but checkLogin still gates the response — anonymous callers get 401/redirect. diff --git a/web/translation/ar-EG.json b/web/translation/ar-EG.json index c6e381e7..2cdc33a6 100644 --- a/web/translation/ar-EG.json +++ b/web/translation/ar-EG.json @@ -97,6 +97,7 @@ "nodes": "النودز", "settings": "إعدادات البانل", "xray": "إعدادات Xray", + "apiDocs": "API Docs", "logout": "تسجيل خروج", "link": "إدارة" }, diff --git a/web/translation/en-US.json b/web/translation/en-US.json index 04a32ba7..bd1f882f 100644 --- a/web/translation/en-US.json +++ b/web/translation/en-US.json @@ -97,6 +97,7 @@ "nodes": "Nodes", "settings": "Panel Settings", "xray": "Xray Configs", + "apiDocs": "API Docs", "logout": "Log Out", "link": "Manage" }, diff --git a/web/translation/es-ES.json b/web/translation/es-ES.json index ce9d5c67..a351d192 100644 --- a/web/translation/es-ES.json +++ b/web/translation/es-ES.json @@ -97,6 +97,7 @@ "nodes": "Nodos", "settings": "Configuraciones", "xray": "Ajustes Xray", + "apiDocs": "API Docs", "logout": "Cerrar Sesión", "link": "Gestionar" }, diff --git a/web/translation/fa-IR.json b/web/translation/fa-IR.json index eedb3826..c512ed29 100644 --- a/web/translation/fa-IR.json +++ b/web/translation/fa-IR.json @@ -97,6 +97,7 @@ "nodes": "نودها", "settings": "تنظیمات پنل", "xray": "پیکربندی ایکس‌ری", + "apiDocs": "API Docs", "logout": "خروج", "link": "مدیریت" }, diff --git a/web/translation/id-ID.json b/web/translation/id-ID.json index 0373b3a1..ce83d006 100644 --- a/web/translation/id-ID.json +++ b/web/translation/id-ID.json @@ -97,6 +97,7 @@ "nodes": "Node", "settings": "Pengaturan Panel", "xray": "Konfigurasi Xray", + "apiDocs": "API Docs", "logout": "Keluar", "link": "Kelola" }, diff --git a/web/translation/ja-JP.json b/web/translation/ja-JP.json index 57c7af8f..073072a8 100644 --- a/web/translation/ja-JP.json +++ b/web/translation/ja-JP.json @@ -97,6 +97,7 @@ "nodes": "ノード", "settings": "パネル設定", "xray": "Xray設定", + "apiDocs": "API Docs", "logout": "ログアウト", "link": "リンク管理" }, diff --git a/web/translation/pt-BR.json b/web/translation/pt-BR.json index f93215d7..d19506d0 100644 --- a/web/translation/pt-BR.json +++ b/web/translation/pt-BR.json @@ -97,6 +97,7 @@ "nodes": "Nós", "settings": "Panel Settings", "xray": "Xray Configs", + "apiDocs": "API Docs", "logout": "Sair", "link": "Gerenciar" }, diff --git a/web/translation/ru-RU.json b/web/translation/ru-RU.json index 6d32f596..9bf9ddeb 100644 --- a/web/translation/ru-RU.json +++ b/web/translation/ru-RU.json @@ -97,6 +97,7 @@ "nodes": "Узлы", "settings": "Настройки", "xray": "Настройки Xray", + "apiDocs": "API Docs", "logout": "Выход", "link": "Управление" }, diff --git a/web/translation/tr-TR.json b/web/translation/tr-TR.json index b9681101..fc1b8bfe 100644 --- a/web/translation/tr-TR.json +++ b/web/translation/tr-TR.json @@ -97,6 +97,7 @@ "nodes": "Düğümler", "settings": "Panel Ayarları", "xray": "Xray Yapılandırmaları", + "apiDocs": "API Docs", "logout": "Çıkış Yap", "link": "Yönet" }, diff --git a/web/translation/uk-UA.json b/web/translation/uk-UA.json index 10fe2bd9..23df613b 100644 --- a/web/translation/uk-UA.json +++ b/web/translation/uk-UA.json @@ -97,6 +97,7 @@ "nodes": "Вузли", "settings": "Параметри панелі", "xray": "Конфігурації Xray", + "apiDocs": "API Docs", "logout": "Вийти", "link": "Керувати" }, diff --git a/web/translation/vi-VN.json b/web/translation/vi-VN.json index 2dc5081e..56251525 100644 --- a/web/translation/vi-VN.json +++ b/web/translation/vi-VN.json @@ -98,6 +98,7 @@ "settings": "Cài đặt bảng điều khiển", "logout": "Đăng xuất", "xray": "Cài đặt Xray", + "apiDocs": "API Docs", "link": "Quản lý" }, "pages": { diff --git a/web/translation/zh-CN.json b/web/translation/zh-CN.json index d9f7b562..07dbeff2 100644 --- a/web/translation/zh-CN.json +++ b/web/translation/zh-CN.json @@ -97,6 +97,7 @@ "nodes": "节点", "settings": "面板设置", "xray": "Xray 设置", + "apiDocs": "API Docs", "logout": "退出登录", "link": "管理" }, diff --git a/web/translation/zh-TW.json b/web/translation/zh-TW.json index 84f2f053..09c34438 100644 --- a/web/translation/zh-TW.json +++ b/web/translation/zh-TW.json @@ -97,6 +97,7 @@ "nodes": "節點", "settings": "面板設定", "xray": "Xray 設定", + "apiDocs": "API Docs", "logout": "退出登入", "link": "管理" },