diff --git a/docs/architecture.md b/docs/architecture.md index 59e1cfec8..39ecafe02 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -340,6 +340,22 @@ code_ works whether the target is the local Xray or a remote node. - **Dispatch:** `manager.go` → `Manager.RuntimeFor(nodeID *int)`; `nil` nodeID → `Local`, otherwise a cached/lazy-loaded `Remote`. `InvalidateNode(id)` drops a cached remote client. +Bearer tokens use the narrowest scope that matches the caller's duties: + +| Scope | Access | +| ------------ | ---------------------------------------------------------------------- | +| `monitor` | Read-only status, version, and metric endpoints. | +| `node-sync` | Node configuration, inbound/client synchronization, traffic, and Xray. | +| `node-admin` | All `node-sync` access plus `POST /panel/api/server/updatePanel`. | +| `admin` | Every panel API endpoint; intended for fully trusted automation. | + +`node-admin` remains a node synchronization identity when inbound rules are applied. This +preserves the node-specific validation path while keeping software-update authority separate +from ordinary `node-sync` credentials. On an mTLS connection, only a valid `node-admin` bearer +elevates the request; other bearer scopes retain the historical mTLS `node-sync` behavior. +Existing tokens retain their stored scope, and legacy tokens without one continue to migrate to +`admin`. + **Node identity & attribution (the hard part).** Inbounds carry a `NodeID` _and_ an `OriginNodeGuid`. Because inbounds can be pushed across hops, the panel attributes traffic and online clients back to the originating panel using **stable GUIDs** rather than local IDs. diff --git a/docs/content/docs/en/reference/api/api-tokens.mdx b/docs/content/docs/en/reference/api/api-tokens.mdx index f47ca9f2d..963eaf3db 100644 --- a/docs/content/docs/en/reference/api/api-tokens.mdx +++ b/docs/content/docs/en/reference/api/api-tokens.mdx @@ -1,8 +1,10 @@ --- title: API Tokens -description: Manage scoped Bearer tokens for programmatic auth. Tokens grant - admin, monitor, or node-sync access, may expire, and are stored as SHA-256 - hashes. The plaintext is returned only once at creation. +description: Manage scoped Bearer tokens for programmatic auth. admin grants the + full panel API; monitor is read-only for status and metrics; node-sync manages + node configuration without panel updates; node-admin adds panel self-update to + node-sync. Tokens may expire and are stored as SHA-256 hashes. The plaintext + is returned only once at creation. full: true _openapi: preload: @@ -44,13 +46,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/authentication.mdx b/docs/content/docs/en/reference/api/authentication.mdx index 8ba39be38..16e2d7b49 100644 --- a/docs/content/docs/en/reference/api/authentication.mdx +++ b/docs/content/docs/en/reference/api/authentication.mdx @@ -59,13 +59,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/backup.mdx b/docs/content/docs/en/reference/api/backup.mdx index de5b9a272..f29de1513 100644 --- a/docs/content/docs/en/reference/api/backup.mdx +++ b/docs/content/docs/en/reference/api/backup.mdx @@ -21,13 +21,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/clients.mdx b/docs/content/docs/en/reference/api/clients.mdx index 4031fbb49..d120a8e3e 100644 --- a/docs/content/docs/en/reference/api/clients.mdx +++ b/docs/content/docs/en/reference/api/clients.mdx @@ -663,13 +663,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/hosts.mdx b/docs/content/docs/en/reference/api/hosts.mdx index 92dc7ac0e..0ff41553f 100644 --- a/docs/content/docs/en/reference/api/hosts.mdx +++ b/docs/content/docs/en/reference/api/hosts.mdx @@ -78,13 +78,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/inbounds.mdx b/docs/content/docs/en/reference/api/inbounds.mdx index a27dc3e73..3eaac929a 100644 --- a/docs/content/docs/en/reference/api/inbounds.mdx +++ b/docs/content/docs/en/reference/api/inbounds.mdx @@ -197,13 +197,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/nodes.mdx b/docs/content/docs/en/reference/api/nodes.mdx index 6cf408989..c81d856e9 100644 --- a/docs/content/docs/en/reference/api/nodes.mdx +++ b/docs/content/docs/en/reference/api/nodes.mdx @@ -145,13 +145,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/server.mdx b/docs/content/docs/en/reference/api/server.mdx index d817ee18c..58cd3dbdc 100644 --- a/docs/content/docs/en/reference/api/server.mdx +++ b/docs/content/docs/en/reference/api/server.mdx @@ -114,8 +114,8 @@ _openapi: url: '#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release' - depth: 2 title: Self-update the panel to the latest version. The server restarts on - success. - url: '#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success' + success. Bearer-token callers need admin or node-admin scope. + url: '#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success-bearer-token-callers-need-admin-or-node-admin-scope' - depth: 2 title: Toggle the panel update channel between stable and the rolling per-commit dev release. Only effective on dev builds. @@ -263,8 +263,8 @@ _openapi: newest release. id: download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release - content: Self-update the panel to the latest version. The server restarts on - success. - id: self-update-the-panel-to-the-latest-version-the-server-restarts-on-success + success. Bearer-token callers need admin or node-admin scope. + id: self-update-the-panel-to-the-latest-version-the-server-restarts-on-success-bearer-token-callers-need-admin-or-node-admin-scope - content: Toggle the panel update channel between stable and the rolling per-commit dev release. Only effective on dev builds. id: toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds @@ -321,13 +321,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/settings.mdx b/docs/content/docs/en/reference/api/settings.mdx index 442de9f1a..df46620ec 100644 --- a/docs/content/docs/en/reference/api/settings.mdx +++ b/docs/content/docs/en/reference/api/settings.mdx @@ -102,13 +102,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/subscription-balancers.mdx b/docs/content/docs/en/reference/api/subscription-balancers.mdx index 6acdc9912..1efa91574 100644 --- a/docs/content/docs/en/reference/api/subscription-balancers.mdx +++ b/docs/content/docs/en/reference/api/subscription-balancers.mdx @@ -50,13 +50,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/subscription-server.mdx b/docs/content/docs/en/reference/api/subscription-server.mdx index 7fd6bc674..9781ff90a 100644 --- a/docs/content/docs/en/reference/api/subscription-server.mdx +++ b/docs/content/docs/en/reference/api/subscription-server.mdx @@ -98,13 +98,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/websocket.mdx b/docs/content/docs/en/reference/api/websocket.mdx index bd842d540..baec3c25d 100644 --- a/docs/content/docs/en/reference/api/websocket.mdx +++ b/docs/content/docs/en/reference/api/websocket.mdx @@ -29,13 +29,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/content/docs/en/reference/api/xray-settings.mdx b/docs/content/docs/en/reference/api/xray-settings.mdx index 4576a0687..531d3f058 100644 --- a/docs/content/docs/en/reference/api/xray-settings.mdx +++ b/docs/content/docs/en/reference/api/xray-settings.mdx @@ -213,13 +213,11 @@ _openapi: {/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */} export default function Layout(props) { - const { APIPage, OpenAPIPage } = props.components ?? {}; - // "APIPage" is the old name from v10, this allows both for backward compatibility - const Comp = OpenAPIPage ?? APIPage; + const { OpenAPIPage } = props.components ?? {}; return ( <> {props.children} - + ); } \ No newline at end of file diff --git a/docs/public/openapi.json b/docs/public/openapi.json index b50cb368f..d36661bd5 100644 --- a/docs/public/openapi.json +++ b/docs/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 — an API token is a full-admin credential, so treat it like the panel password." + "description": "Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with a scoped API token from Settings → Security → API Token (Bearer). Token access is limited by its admin, monitor, node-sync, or node-admin scope; treat admin tokens like the panel password." }, "servers": [ { @@ -16,7 +16,7 @@ "bearerAuth": { "type": "http", "scheme": "bearer", - "description": "API token from Settings → Security → API Token. Send as `Authorization: Bearer `." + "description": "Scoped API token from Settings → Security → API Token. Send as `Authorization: Bearer `." }, "cookieAuth": { "type": "apiKey", @@ -4855,7 +4855,7 @@ }, { "name": "API Tokens", - "description": "Manage scoped Bearer tokens for programmatic auth. Tokens grant admin, monitor, or node-sync access, may expire, and are stored as SHA-256 hashes. The plaintext is returned only once at creation." + "description": "Manage scoped Bearer tokens for programmatic auth. admin grants the full panel API; monitor is read-only for status and metrics; node-sync manages node configuration without panel updates; node-admin adds panel self-update to node-sync. Tokens may expire and are stored as SHA-256 hashes. The plaintext is returned only once at creation." }, { "name": "Xray Settings", @@ -7184,7 +7184,7 @@ "tags": [ "Server" ], - "summary": "Self-update the panel to the latest version. The server restarts on success.", + "summary": "Self-update the panel to the latest version. The server restarts on success. Bearer-token callers need admin or node-admin scope.", "operationId": "post_panel_api_server_updatePanel", "requestBody": { "required": false, @@ -13208,9 +13208,11 @@ "obj": [ { "id": 1, - "name": "default", + "name": "central-panel-a", "enabled": true, - "createdAt": 1736000000 + "createdAt": 1736000000, + "scope": "node-admin", + "expiresAt": 0 } ] } @@ -13240,7 +13242,13 @@ }, "scope": { "type": "string", - "description": "admin (default), monitor, or node-sync." + "enum": [ + "admin", + "monitor", + "node-sync", + "node-admin" + ], + "description": "admin (default), monitor, node-sync, or node-admin. node-admin inherits node-sync and may also start a panel update." }, "expiresAt": { "type": "integer", @@ -13253,7 +13261,7 @@ }, "example": { "name": "central-panel-a", - "scope": "node-sync", + "scope": "node-admin", "expiresAt": 1798761600000 } } diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index b50cb368f..d36661bd5 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 — an API token is a full-admin credential, so treat it like the panel password." + "description": "Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with a scoped API token from Settings → Security → API Token (Bearer). Token access is limited by its admin, monitor, node-sync, or node-admin scope; treat admin tokens like the panel password." }, "servers": [ { @@ -16,7 +16,7 @@ "bearerAuth": { "type": "http", "scheme": "bearer", - "description": "API token from Settings → Security → API Token. Send as `Authorization: Bearer `." + "description": "Scoped API token from Settings → Security → API Token. Send as `Authorization: Bearer `." }, "cookieAuth": { "type": "apiKey", @@ -4855,7 +4855,7 @@ }, { "name": "API Tokens", - "description": "Manage scoped Bearer tokens for programmatic auth. Tokens grant admin, monitor, or node-sync access, may expire, and are stored as SHA-256 hashes. The plaintext is returned only once at creation." + "description": "Manage scoped Bearer tokens for programmatic auth. admin grants the full panel API; monitor is read-only for status and metrics; node-sync manages node configuration without panel updates; node-admin adds panel self-update to node-sync. Tokens may expire and are stored as SHA-256 hashes. The plaintext is returned only once at creation." }, { "name": "Xray Settings", @@ -7184,7 +7184,7 @@ "tags": [ "Server" ], - "summary": "Self-update the panel to the latest version. The server restarts on success.", + "summary": "Self-update the panel to the latest version. The server restarts on success. Bearer-token callers need admin or node-admin scope.", "operationId": "post_panel_api_server_updatePanel", "requestBody": { "required": false, @@ -13208,9 +13208,11 @@ "obj": [ { "id": 1, - "name": "default", + "name": "central-panel-a", "enabled": true, - "createdAt": 1736000000 + "createdAt": 1736000000, + "scope": "node-admin", + "expiresAt": 0 } ] } @@ -13240,7 +13242,13 @@ }, "scope": { "type": "string", - "description": "admin (default), monitor, or node-sync." + "enum": [ + "admin", + "monitor", + "node-sync", + "node-admin" + ], + "description": "admin (default), monitor, node-sync, or node-admin. node-admin inherits node-sync and may also start a panel update." }, "expiresAt": { "type": "integer", @@ -13253,7 +13261,7 @@ }, "example": { "name": "central-panel-a", - "scope": "node-sync", + "scope": "node-admin", "expiresAt": 1798761600000 } } diff --git a/frontend/scripts/build-openapi.mjs b/frontend/scripts/build-openapi.mjs index 8cd3ff844..e2b4e5d2e 100644 --- a/frontend/scripts/build-openapi.mjs +++ b/frontend/scripts/build-openapi.mjs @@ -23,7 +23,7 @@ const SECURITY_SCHEMES = { type: 'http', scheme: 'bearer', description: - 'API token from Settings → Security → API Token. Send as `Authorization: Bearer `.', + 'Scoped API token from Settings → Security → API Token. Send as `Authorization: Bearer `.', }, cookieAuth: { type: 'apiKey', @@ -342,7 +342,7 @@ export function buildSpec() { title: '3X-UI Panel API', version: PANEL_VERSION, 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.', + 'Programmatic interface to a 3X-UI panel. Authenticate either by logging in (cookie) or with a scoped API token from Settings → Security → API Token (Bearer). Token access is limited by its admin, monitor, node-sync, or node-admin scope; treat admin tokens like the panel password.', }, servers: [{ url: '/', description: 'Current panel (basePath aware)' }], components: { diff --git a/frontend/src/pages/api-docs/endpoints.ts b/frontend/src/pages/api-docs/endpoints.ts index a0b343a5b..5d6bddd2d 100644 --- a/frontend/src/pages/api-docs/endpoints.ts +++ b/frontend/src/pages/api-docs/endpoints.ts @@ -663,7 +663,8 @@ export const sections: readonly Section[] = [ { method: 'POST', path: '/panel/api/server/updatePanel', - summary: 'Self-update the panel to the latest version. The server restarts on success.', + summary: + 'Self-update the panel to the latest version. The server restarts on success. Bearer-token callers need admin or node-admin scope.', params: [ { name: 'dev', @@ -1996,7 +1997,7 @@ export const sections: readonly Section[] = [ id: 'api-tokens', title: 'API Tokens', description: - 'Manage scoped Bearer tokens for programmatic auth. Tokens grant admin, monitor, or node-sync access, may expire, and are stored as SHA-256 hashes. The plaintext is returned only once at creation.', + 'Manage scoped Bearer tokens for programmatic auth. admin grants the full panel API; monitor is read-only for status and metrics; node-sync manages node configuration without panel updates; node-admin adds panel self-update to node-sync. Tokens may expire and are stored as SHA-256 hashes. The plaintext is returned only once at creation.', endpoints: [ { method: 'GET', @@ -2004,7 +2005,7 @@ export const sections: readonly Section[] = [ 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}', + '{\n "success": true,\n "obj": [\n {\n "id": 1,\n "name": "central-panel-a",\n "enabled": true,\n "createdAt": 1736000000,\n "scope": "node-admin",\n "expiresAt": 0\n }\n ]\n}', }, { method: 'POST', @@ -2022,7 +2023,8 @@ export const sections: readonly Section[] = [ name: 'scope', in: 'body', type: 'string', - desc: 'admin (default), monitor, or node-sync.', + desc: 'admin (default), monitor, node-sync, or node-admin. node-admin inherits node-sync and may also start a panel update.', + enum: ['admin', 'monitor', 'node-sync', 'node-admin'], optional: true, }, { @@ -2033,7 +2035,7 @@ export const sections: readonly Section[] = [ optional: true, }, ], - body: '{\n "name": "central-panel-a",\n "scope": "node-sync",\n "expiresAt": 1798761600000\n}', + body: '{\n "name": "central-panel-a",\n "scope": "node-admin",\n "expiresAt": 1798761600000\n}', responseSchema: 'ApiTokenView', errorResponse: '{\n "success": false,\n "msg": "a token with that name already exists"\n}', diff --git a/frontend/src/pages/settings/SecurityTab.css b/frontend/src/pages/settings/SecurityTab.css index 87a6d5f49..2b2237d03 100644 --- a/frontend/src/pages/settings/SecurityTab.css +++ b/frontend/src/pages/settings/SecurityTab.css @@ -49,6 +49,17 @@ gap: 2px; } +.api-token-name-line { + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; +} + +.api-token-name-line .ant-tag { + margin-inline-end: 0; +} + .api-token-name { font-weight: 600; font-size: 13.5px; diff --git a/frontend/src/pages/settings/SecurityTab.tsx b/frontend/src/pages/settings/SecurityTab.tsx index eb25b51dd..570cfef76 100644 --- a/frontend/src/pages/settings/SecurityTab.tsx +++ b/frontend/src/pages/settings/SecurityTab.tsx @@ -1,6 +1,19 @@ import { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { Button, Empty, Form, Input, Modal, Space, Spin, Switch, Tabs, message } from 'antd'; +import { + Button, + Empty, + Form, + Input, + Modal, + Select, + Space, + Spin, + Switch, + Tabs, + Tag, + message, +} from 'antd'; import { ApiOutlined, SafetyOutlined, UserOutlined } from '@ant-design/icons'; import { ClipboardManager, HttpUtil, IntlUtil, RandomUtil } from '@/utils'; import type { AllSetting } from '@/models/setting'; @@ -16,12 +29,14 @@ interface ApiMsg { obj?: T; } +type ApiTokenScope = 'admin' | 'monitor' | 'node-sync' | 'node-admin'; + interface ApiTokenRow { id: number; name: string; enabled: boolean; createdAt: number; - scope: 'admin' | 'monitor' | 'node-sync'; + scope: ApiTokenScope; expiresAt: number; } @@ -76,6 +91,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: const [apiTokensLoading, setApiTokensLoading] = useState(true); const [createOpen, setCreateOpen] = useState(false); const [createName, setCreateName] = useState(''); + const [createScope, setCreateScope] = useState('admin'); const [creating, setCreating] = useState(false); const [createdToken, setCreatedToken] = useState<{ name: string; token: string } | null>(null); @@ -157,6 +173,7 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: function openCreateModal() { setCreateName(''); + setCreateScope('admin'); setCreateOpen(true); } @@ -168,9 +185,10 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: } setCreating(true); try { - const msg = (await HttpUtil.post('/panel/api/setting/apiTokens/create', { name })) as ApiMsg<{ - token?: string; - }>; + const msg = (await HttpUtil.post('/panel/api/setting/apiTokens/create', { + name, + scope: createScope, + })) as ApiMsg<{ token?: string }>; if (msg?.success) { setCreateOpen(false); await loadApiTokens(); @@ -217,6 +235,29 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: return IntlUtil.formatDate(apiTokenCreatedAtMilliseconds(ts)); } + const scopeMeta: Record = { + admin: { + label: t('pages.settings.security.apiTokenScopeAdmin'), + description: t('pages.settings.security.apiTokenScopeAdminDesc'), + color: 'red', + }, + monitor: { + label: t('pages.settings.security.apiTokenScopeMonitor'), + description: t('pages.settings.security.apiTokenScopeMonitorDesc'), + color: 'blue', + }, + 'node-sync': { + label: t('pages.settings.security.apiTokenScopeNodeSync'), + description: t('pages.settings.security.apiTokenScopeNodeSyncDesc'), + color: 'gold', + }, + 'node-admin': { + label: t('pages.settings.security.apiTokenScopeNodeAdmin'), + description: t('pages.settings.security.apiTokenScopeNodeAdminDesc'), + color: 'purple', + }, + }; + function toggleTwoFactor() { if (!allSetting.twoFactorEnable) { const newToken = RandomUtil.randomBase32String(); @@ -345,7 +386,12 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }:
- {row.name} +
+ {row.name} + + {scopeMeta[row.scope].label} + +
{formatTokenDate(row.createdAt)} @@ -396,6 +442,21 @@ export default function SecurityTab({ allSetting, updateSetting, saveSetting }: onPressEnter={confirmCreateToken} /> + +