From 68007fd1ec71af77dc58ef47fcaf7dbe02f0ed0e Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 29 Apr 2026 09:23:17 +0100 Subject: [PATCH] Canonicalize API token settings copy Route API token reveal and rotation guidance through the shared API token presentation helper so settings copy points to API Access instead of legacy Security token wording. --- .../v6/internal/subsystems/api-contracts.md | 4 ++++ .../v6/internal/subsystems/security-privacy.md | 4 ++++ .../Settings/BackupTransferDialogs.tsx | 5 ++--- .../Settings/__tests__/APITokenManager.test.tsx | 1 + .../Settings/useAPITokenManagerState.ts | 3 ++- .../utils/__tests__/apiTokenPresentation.test.ts | 11 +++++++++++ .../src/utils/apiTokenPresentation.ts | 16 ++++++++++++---- 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index dc38eea05..a35a65217 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -723,6 +723,10 @@ entry. The API layer already uses contract tests in many places, but every major live contract should continue moving toward canonical-only runtime shapes. +The shared API-token presentation helper also owns API token management-location +copy for Settings surfaces. Token reveal and rotation guidance must point +operators to `Settings → API Access` and must not revive legacy +`Security → API tokens` wording. That same shared `internal/api/` boundary now also keeps ephemeral auth flow state and request correlation fail-closed. OIDC authorization state storage must cap abandoned entries and evict the earliest-expiring state before diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index c83325e56..82d24bb80 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -204,6 +204,10 @@ That shared token-management boundary now also includes `frontend-modern/src/utils/apiTokenPresentation.ts`, so API-token load, generate, and revoke errors stay on one governed customer-facing wording path instead of drifting back into hook-local notification strings. +That same API-token presentation helper also owns API token management-location +copy for Settings surfaces. Token reveal and rotation guidance must point +operators to `Settings → API Access` and must not revive legacy +`Security → API tokens` wording. That same token-management boundary must also treat top-level TrueNAS appliances as canonical agent-scope resources through the shared agent-facet helper. Security surfaces may consume compatibility-normalized diff --git a/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx b/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx index beae8fb6e..68618ee5b 100644 --- a/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx +++ b/frontend-modern/src/components/Settings/BackupTransferDialogs.tsx @@ -4,6 +4,7 @@ import { Dialog } from '@/components/shared/Dialog'; import { SectionHeader } from '@/components/shared/SectionHeader'; import { controlClass, formField, formHelpText, labelClass } from '@/components/shared/Form'; import type { SecurityStatus as SecurityStatusInfo } from '@/types/config'; +import { getAPITokenManagementLocationMessage } from '@/utils/apiTokenPresentation'; interface BackupTransferDialogsProps { securityStatus: Accessor; @@ -202,9 +203,7 @@ export const BackupTransferDialogs: Component = (pro
-

- Create or rotate API tokens in Settings → Security → API tokens. -

+

{getAPITokenManagementLocationMessage()}

Tokens are managed in the UI and stored in api_tokens.json.

diff --git a/frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx b/frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx index be6111410..bc5ae0066 100644 --- a/frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx @@ -157,6 +157,7 @@ describe('APITokenManager', () => { name: 'Container automation', scopes: [DOCKER_MANAGE_SCOPE, DOCKER_REPORT_SCOPE], }), + note: 'Copy this token now. You can reopen this dialog from Settings → API Access while this page stays open.', }), ); expect(notificationSuccessMock).toHaveBeenCalledWith( diff --git a/frontend-modern/src/components/Settings/useAPITokenManagerState.ts b/frontend-modern/src/components/Settings/useAPITokenManagerState.ts index 00664a37a..38366539d 100644 --- a/frontend-modern/src/components/Settings/useAPITokenManagerState.ts +++ b/frontend-modern/src/components/Settings/useAPITokenManagerState.ts @@ -16,6 +16,7 @@ import type { Resource } from '@/types/resource'; import { formatRelativeTime } from '@/utils/format'; import { getAPITokenGenerateErrorMessage, + getAPITokenRevealSettingsNote, getAPITokensLoadErrorMessage, getAPITokenRevokeErrorMessage, } from '@/utils/apiTokenPresentation'; @@ -196,7 +197,7 @@ export const useAPITokenManagerState = (props: APITokenManagerProps) => { token, record, source: 'security', - note: 'Copy this token now. You can reopen this dialog from Security → API tokens while this page stays open.', + note: getAPITokenRevealSettingsNote(), }); notificationStore.success( 'New API token generated. Copy it below while it is still visible.', diff --git a/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts b/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts index efda0fb3d..0516bb890 100644 --- a/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { getAPITokenGenerateErrorMessage, + getAPITokenManagementLocationMessage, + getAPITokenRevealSettingsNote, getAPITokensLoadErrorMessage, getAPITokenRevokeErrorMessage, } from '@/utils/apiTokenPresentation'; @@ -12,6 +14,15 @@ describe('apiTokenPresentation', () => { expect(getAPITokenRevokeErrorMessage()).toBe('Unable to revoke the API token.'); }); + it('returns canonical API token settings location copy', () => { + expect(getAPITokenManagementLocationMessage()).toBe( + 'Create or rotate API tokens in Settings → API Access.', + ); + expect(getAPITokenRevealSettingsNote()).toBe( + 'Copy this token now. You can reopen this dialog from Settings → API Access while this page stays open.', + ); + }); + it('surfaces token scope denial copy for generate failures', () => { const error = Object.assign( new Error('Cannot grant scope "monitoring:read": your token does not have this scope'), diff --git a/frontend-modern/src/utils/apiTokenPresentation.ts b/frontend-modern/src/utils/apiTokenPresentation.ts index a0d923d75..7e567a64f 100644 --- a/frontend-modern/src/utils/apiTokenPresentation.ts +++ b/frontend-modern/src/utils/apiTokenPresentation.ts @@ -1,7 +1,3 @@ -export function getAPITokensLoadErrorMessage(): string { - return 'Unable to load API tokens.'; -} - import { API_SCOPE_LABELS } from '@/constants/apiScopes'; type APITokenErrorShape = { @@ -10,6 +6,18 @@ type APITokenErrorShape = { message?: string; }; +export function getAPITokensLoadErrorMessage(): string { + return 'Unable to load API tokens.'; +} + +export function getAPITokenManagementLocationMessage(): string { + return 'Create or rotate API tokens in Settings → API Access.'; +} + +export function getAPITokenRevealSettingsNote(): string { + return 'Copy this token now. You can reopen this dialog from Settings → API Access while this page stays open.'; +} + export function getAPITokenGenerateErrorMessage(error?: unknown): string { if (error && typeof error === 'object') { const typedError = error as APITokenErrorShape;