mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-20 01:50:58 +00:00
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:
@@ -905,7 +905,7 @@ export const sections: readonly Section[] = [
|
||||
method: 'GET',
|
||||
path: '/panel/api/nodes/list',
|
||||
summary: 'List every configured node with its connection details, health, and last heartbeat patch.',
|
||||
responseSchema: 'Node',
|
||||
responseSchema: 'NodeView',
|
||||
responseSchemaArray: true,
|
||||
},
|
||||
{
|
||||
@@ -927,6 +927,7 @@ export const sections: readonly Section[] = [
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Node ID.' },
|
||||
],
|
||||
responseSchema: 'NodeView',
|
||||
},
|
||||
{
|
||||
method: 'GET',
|
||||
@@ -940,18 +941,19 @@ export const sections: readonly Section[] = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/nodes/add',
|
||||
summary: 'Register a new remote node. Provide its URL, apiToken, and optional remark / allowPrivateAddress flag.',
|
||||
summary: 'Register a new remote node. Provide its URL, write-only apiToken, and optional remark / allowPrivateAddress flag. Responses expose hasApiToken only.',
|
||||
body:
|
||||
'{\n "name": "de-fra-1",\n "remark": "",\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef...",\n "enable": true,\n "allowPrivateAddress": false\n}',
|
||||
'{\n "name": "de-fra-1",\n "remark": "",\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef...",\n "clearApiToken": false,\n "enable": true,\n "allowPrivateAddress": false\n}',
|
||||
responseSchema: 'NodeView',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/panel/api/nodes/update/:id',
|
||||
summary: 'Replace a node\u2019s connection details. Same body shape as /add.',
|
||||
summary: 'Replace a node\u2019s connection details. apiToken is write-only: omit it or send an empty string to keep the stored token; set clearApiToken=true to clear it.',
|
||||
params: [
|
||||
{ name: 'id', in: 'path', type: 'number', desc: 'Node ID.' },
|
||||
],
|
||||
body: '{\n "name": "de-fra-1",\n "remark": "",\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "abcdef...",\n "enable": true,\n "allowPrivateAddress": false\n}',
|
||||
body: '{\n "name": "de-fra-1",\n "remark": "",\n "scheme": "https",\n "address": "node1.example.com",\n "port": 2053,\n "basePath": "/",\n "apiToken": "",\n "clearApiToken": false,\n "enable": true,\n "allowPrivateAddress": false\n}',
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
|
||||
@@ -45,6 +45,7 @@ function defaultValues(): NodeFormValues {
|
||||
port: 2053,
|
||||
basePath: '/',
|
||||
apiToken: '',
|
||||
hasStoredToken: false,
|
||||
enable: true,
|
||||
allowPrivateAddress: false,
|
||||
tlsVerifyMode: 'verify',
|
||||
@@ -107,6 +108,8 @@ export default function NodeFormModal({
|
||||
scheme: (node.scheme as 'http' | 'https') || base.scheme,
|
||||
inboundSyncMode: (node.inboundSyncMode as 'all' | 'selected') || base.inboundSyncMode,
|
||||
inboundTags: node.inboundTags ?? [],
|
||||
apiToken: '',
|
||||
hasStoredToken: node.hasApiToken ?? false,
|
||||
}
|
||||
: base;
|
||||
if (next.scheme === 'http') next.tlsVerifyMode = 'skip';
|
||||
@@ -120,8 +123,11 @@ export default function NodeFormModal({
|
||||
[mode, t],
|
||||
);
|
||||
|
||||
const editingWithToken = mode === 'edit' && Boolean(node?.hasApiToken);
|
||||
|
||||
function buildPayload(values: NodeFormValues): Partial<NodeRecord> {
|
||||
return {
|
||||
const token = values.apiToken.trim();
|
||||
const payload: Partial<NodeRecord> = {
|
||||
id: values.id || 0,
|
||||
name: values.name.trim(),
|
||||
remark: values.remark?.trim() || '',
|
||||
@@ -129,7 +135,6 @@ export default function NodeFormModal({
|
||||
address: values.address.trim(),
|
||||
port: values.port,
|
||||
basePath: values.basePath.trim() || '/',
|
||||
apiToken: values.apiToken.trim(),
|
||||
enable: values.enable,
|
||||
allowPrivateAddress: values.allowPrivateAddress,
|
||||
tlsVerifyMode: values.tlsVerifyMode,
|
||||
@@ -138,10 +143,12 @@ export default function NodeFormModal({
|
||||
inboundTags: values.inboundSyncMode === 'selected' ? values.inboundTags : [],
|
||||
outboundTag: values.outboundTag || '',
|
||||
};
|
||||
if (token) payload.apiToken = token;
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function onTest() {
|
||||
if (!(await methods.trigger(['address', 'port']))) return;
|
||||
if (!(await methods.trigger(['name', 'address', 'port']))) return;
|
||||
setTesting(true);
|
||||
setTestResult(null);
|
||||
try {
|
||||
@@ -158,7 +165,7 @@ export default function NodeFormModal({
|
||||
}
|
||||
|
||||
async function onFetchPin() {
|
||||
if (!(await methods.trigger(['address', 'port']))) return;
|
||||
if (!(await methods.trigger(['name', 'address', 'port']))) return;
|
||||
setFetchingPin(true);
|
||||
try {
|
||||
const payload = buildPayload(methods.getValues());
|
||||
@@ -369,8 +376,11 @@ export default function NodeFormModal({
|
||||
name="apiToken"
|
||||
rules={{ validate: rhfZodValidate(NodeFormSchema.shape.apiToken) }}
|
||||
tooltip={t('pages.nodes.apiTokenHint')}
|
||||
extra={editingWithToken ? t('pages.nodes.apiTokenKeepHint') : undefined}
|
||||
>
|
||||
<Input.Password placeholder={t('pages.nodes.apiTokenPlaceholder')} />
|
||||
<Input.Password
|
||||
placeholder={editingWithToken ? t('pages.nodes.apiTokenKeepHint') : t('pages.nodes.apiTokenPlaceholder')}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
|
||||
Reference in New Issue
Block a user