fix(nodes): make node API tokens write-only (#5613)

* fix(nodes): make node API tokens write-only

* fix(nodes): keep token optional on edit for write-only API tokens

NodeView no longer returns apiToken, so the edit form must consume hasApiToken and not require re-entering the token. Relaxes the form validation on edit, adds a keep-current placeholder, and adds the i18n key to all 13 locales.
This commit is contained in:
n0ctal
2026-07-23 18:35:11 +05:00
committed by GitHub
parent 892c06c8bc
commit c77608bc47
29 changed files with 1716 additions and 88 deletions
+65 -1
View File
@@ -494,7 +494,6 @@ export const EXAMPLES: Record<string, unknown> = {
"activeCount": 23,
"address": "node1.example.com",
"allowPrivateAddress": false,
"apiToken": "abcdef0123456789",
"basePath": "/",
"clientCount": 27,
"configDirty": false,
@@ -535,6 +534,71 @@ export const EXAMPLES: Record<string, unknown> = {
"xrayState": "",
"xrayVersion": "25.10.31"
},
"NodeMutationRequest": {
"address": "",
"allowPrivateAddress": false,
"apiToken": null,
"basePath": "",
"clearApiToken": false,
"enable": false,
"id": 0,
"inboundSyncMode": "all",
"inboundTags": [
""
],
"name": "",
"outboundTag": "",
"pinnedCertSha256": "",
"port": 1,
"remark": "",
"scheme": "http",
"tlsVerifyMode": "verify"
},
"NodeView": {
"activeCount": 20,
"address": "node.example.com",
"allowPrivateAddress": false,
"basePath": "/",
"clientCount": 25,
"configDirty": false,
"configDirtyAt": 0,
"cpuPct": 12.5,
"createdAt": 1700000000,
"depletedCount": 1,
"disabledCount": 2,
"enable": true,
"guid": "node-guid",
"hasApiToken": true,
"id": 1,
"inboundCount": 3,
"inboundSyncMode": "all",
"inboundTags": [
"in-443-tcp"
],
"lastError": "",
"lastHeartbeat": 1700000000,
"latencyMs": 42,
"memPct": 45.2,
"name": "edge-1",
"netDown": 1048576,
"netUp": 2097152,
"onlineCount": 5,
"outboundTag": "direct",
"panelVersion": "v3.x.x",
"parentGuid": "",
"pinnedCertSha256": "",
"port": 2053,
"remark": "Primary edge",
"scheme": "https",
"status": "online",
"tlsVerifyMode": "verify",
"transitive": false,
"updatedAt": 1700003600,
"uptimeSecs": 86400,
"xrayError": "",
"xrayState": "running",
"xrayVersion": "25.10.31"
},
"OutboundTraffics": {
"down": 0,
"id": 0,
+302 -5
View File
@@ -2032,10 +2032,6 @@ export const SCHEMAS: Record<string, unknown> = {
"allowPrivateAddress": {
"type": "boolean"
},
"apiToken": {
"example": "abcdef0123456789",
"type": "string"
},
"basePath": {
"example": "/",
"type": "string"
@@ -2206,7 +2202,6 @@ export const SCHEMAS: Record<string, unknown> = {
"activeCount",
"address",
"allowPrivateAddress",
"apiToken",
"basePath",
"clientCount",
"configDirty",
@@ -2245,6 +2240,308 @@ export const SCHEMAS: Record<string, unknown> = {
],
"type": "object"
},
"NodeMutationRequest": {
"description": "NodeMutationRequest is the node write/probe contract. ApiToken is accepted\nonly as input. On update, nil means keep the stored token; replacement and\nclearing are explicit and mutually exclusive.",
"properties": {
"address": {
"type": "string"
},
"allowPrivateAddress": {
"type": "boolean"
},
"apiToken": {
"nullable": true,
"type": "string"
},
"basePath": {
"type": "string"
},
"clearApiToken": {
"type": "boolean"
},
"enable": {
"type": "boolean"
},
"id": {
"type": "integer"
},
"inboundSyncMode": {
"enum": [
"all",
"selected"
],
"type": "string"
},
"inboundTags": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"outboundTag": {
"type": "string"
},
"pinnedCertSha256": {
"type": "string"
},
"port": {
"maximum": 65535,
"minimum": 1,
"type": "integer"
},
"remark": {
"type": "string"
},
"scheme": {
"enum": [
"http",
"https"
],
"type": "string"
},
"tlsVerifyMode": {
"enum": [
"verify",
"skip",
"pin",
"mtls"
],
"type": "string"
}
},
"required": [
"address",
"allowPrivateAddress",
"basePath",
"enable",
"id",
"inboundSyncMode",
"inboundTags",
"name",
"outboundTag",
"pinnedCertSha256",
"port",
"remark",
"scheme",
"tlsVerifyMode"
],
"type": "object"
},
"NodeView": {
"description": "NodeView is the browser/API read contract for nodes. Credentials are\nwrite-only: responses expose only whether a node has a token configured.",
"properties": {
"activeCount": {
"example": 20,
"type": "integer"
},
"address": {
"example": "node.example.com",
"type": "string"
},
"allowPrivateAddress": {
"example": false,
"type": "boolean"
},
"basePath": {
"example": "/",
"type": "string"
},
"clientCount": {
"example": 25,
"type": "integer"
},
"configDirty": {
"example": false,
"type": "boolean"
},
"configDirtyAt": {
"example": 0,
"type": "integer"
},
"cpuPct": {
"example": 12.5,
"type": "number"
},
"createdAt": {
"example": 1700000000,
"type": "integer"
},
"depletedCount": {
"example": 1,
"type": "integer"
},
"disabledCount": {
"example": 2,
"type": "integer"
},
"enable": {
"example": true,
"type": "boolean"
},
"guid": {
"example": "node-guid",
"type": "string"
},
"hasApiToken": {
"example": true,
"type": "boolean"
},
"id": {
"example": 1,
"type": "integer"
},
"inboundCount": {
"example": 3,
"type": "integer"
},
"inboundSyncMode": {
"example": "all",
"type": "string"
},
"inboundTags": {
"example": [
"in-443-tcp"
],
"items": {
"type": "string"
},
"type": "array"
},
"lastError": {
"type": "string"
},
"lastHeartbeat": {
"example": 1700000000,
"type": "integer"
},
"latencyMs": {
"example": 42,
"type": "integer"
},
"memPct": {
"example": 45.2,
"type": "number"
},
"name": {
"example": "edge-1",
"type": "string"
},
"netDown": {
"example": 1048576,
"type": "integer"
},
"netUp": {
"example": 2097152,
"type": "integer"
},
"onlineCount": {
"example": 5,
"type": "integer"
},
"outboundTag": {
"example": "direct",
"type": "string"
},
"panelVersion": {
"example": "v3.x.x",
"type": "string"
},
"parentGuid": {
"type": "string"
},
"pinnedCertSha256": {
"type": "string"
},
"port": {
"example": 2053,
"type": "integer"
},
"remark": {
"example": "Primary edge",
"type": "string"
},
"scheme": {
"example": "https",
"type": "string"
},
"status": {
"example": "online",
"type": "string"
},
"tlsVerifyMode": {
"example": "verify",
"type": "string"
},
"transitive": {
"example": false,
"type": "boolean"
},
"updatedAt": {
"example": 1700003600,
"type": "integer"
},
"uptimeSecs": {
"example": 86400,
"type": "integer"
},
"xrayError": {
"type": "string"
},
"xrayState": {
"example": "running",
"type": "string"
},
"xrayVersion": {
"example": "25.10.31",
"type": "string"
}
},
"required": [
"activeCount",
"address",
"allowPrivateAddress",
"basePath",
"clientCount",
"configDirty",
"configDirtyAt",
"cpuPct",
"createdAt",
"depletedCount",
"disabledCount",
"enable",
"guid",
"hasApiToken",
"id",
"inboundCount",
"inboundSyncMode",
"inboundTags",
"lastError",
"lastHeartbeat",
"latencyMs",
"memPct",
"name",
"netDown",
"netUp",
"onlineCount",
"outboundTag",
"panelVersion",
"pinnedCertSha256",
"port",
"remark",
"scheme",
"status",
"tlsVerifyMode",
"updatedAt",
"uptimeSecs",
"xrayError",
"xrayState",
"xrayVersion"
],
"type": "object"
},
"OutboundTraffics": {
"description": "OutboundTraffics tracks traffic statistics for Xray outbound connections.",
"properties": {
+63 -1
View File
@@ -477,7 +477,6 @@ export interface Node {
activeCount: number;
address: string;
allowPrivateAddress: boolean;
apiToken: string;
basePath: string;
clientCount: number;
configDirty: boolean;
@@ -517,6 +516,69 @@ export interface Node {
xrayVersion: string;
}
export interface NodeMutationRequest {
address: string;
allowPrivateAddress: boolean;
apiToken?: string | null;
basePath: string;
clearApiToken?: boolean;
enable: boolean;
id: number;
inboundSyncMode: string;
inboundTags: string[];
name: string;
outboundTag: string;
pinnedCertSha256: string;
port: number;
remark: string;
scheme: string;
tlsVerifyMode: string;
}
export interface NodeView {
activeCount: number;
address: string;
allowPrivateAddress: boolean;
basePath: string;
clientCount: number;
configDirty: boolean;
configDirtyAt: number;
cpuPct: number;
createdAt: number;
depletedCount: number;
disabledCount: number;
enable: boolean;
guid: string;
hasApiToken: boolean;
id: number;
inboundCount: number;
inboundSyncMode: string;
inboundTags: string[];
lastError: string;
lastHeartbeat: number;
latencyMs: number;
memPct: number;
name: string;
netDown: number;
netUp: number;
onlineCount: number;
outboundTag: string;
panelVersion: string;
parentGuid?: string;
pinnedCertSha256: string;
port: number;
remark: string;
scheme: string;
status: string;
tlsVerifyMode: string;
transitive?: boolean;
updatedAt: number;
uptimeSecs: number;
xrayError: string;
xrayState: string;
xrayVersion: string;
}
export interface OutboundTraffics {
down: number;
id: number;
+65 -1
View File
@@ -507,7 +507,6 @@ export const NodeSchema = z.object({
activeCount: z.number().int(),
address: z.string(),
allowPrivateAddress: z.boolean(),
apiToken: z.string(),
basePath: z.string(),
clientCount: z.number().int(),
configDirty: z.boolean(),
@@ -548,6 +547,71 @@ export const NodeSchema = z.object({
});
export type Node = z.infer<typeof NodeSchema>;
export const NodeMutationRequestSchema = z.object({
address: z.string(),
allowPrivateAddress: z.boolean(),
apiToken: z.string().nullable().optional(),
basePath: z.string(),
clearApiToken: z.boolean().optional(),
enable: z.boolean(),
id: z.number().int(),
inboundSyncMode: z.enum(['all', 'selected']),
inboundTags: z.array(z.string()),
name: z.string(),
outboundTag: z.string(),
pinnedCertSha256: z.string(),
port: z.number().int().min(1).max(65535),
remark: z.string(),
scheme: z.enum(['http', 'https']),
tlsVerifyMode: z.enum(['verify', 'skip', 'pin', 'mtls']),
});
export type NodeMutationRequest = z.infer<typeof NodeMutationRequestSchema>;
export const NodeViewSchema = z.object({
activeCount: z.number().int(),
address: z.string(),
allowPrivateAddress: z.boolean(),
basePath: z.string(),
clientCount: z.number().int(),
configDirty: z.boolean(),
configDirtyAt: z.number().int(),
cpuPct: z.number(),
createdAt: z.number().int(),
depletedCount: z.number().int(),
disabledCount: z.number().int(),
enable: z.boolean(),
guid: z.string(),
hasApiToken: z.boolean(),
id: z.number().int(),
inboundCount: z.number().int(),
inboundSyncMode: z.string(),
inboundTags: z.array(z.string()),
lastError: z.string(),
lastHeartbeat: z.number().int(),
latencyMs: z.number().int(),
memPct: z.number(),
name: z.string(),
netDown: z.number().int(),
netUp: z.number().int(),
onlineCount: z.number().int(),
outboundTag: z.string(),
panelVersion: z.string(),
parentGuid: z.string().optional(),
pinnedCertSha256: z.string(),
port: z.number().int(),
remark: z.string(),
scheme: z.string(),
status: z.string(),
tlsVerifyMode: z.string(),
transitive: z.boolean().optional(),
updatedAt: z.number().int(),
uptimeSecs: z.number().int(),
xrayError: z.string(),
xrayState: z.string(),
xrayVersion: z.string(),
});
export type NodeView = z.infer<typeof NodeViewSchema>;
export const OutboundTrafficsSchema = z.object({
down: z.number().int(),
id: z.number().int(),