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;