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.
This commit is contained in:
rcourtman
2026-04-29 09:23:17 +01:00
parent 5d496a5d2a
commit 68007fd1ec
7 changed files with 36 additions and 8 deletions
@@ -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
@@ -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
@@ -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<SecurityStatusInfo | null>;
@@ -202,9 +203,7 @@ export const BackupTransferDialogs: Component<BackupTransferDialogsProps> = (pro
</div>
<div class="text-xs text-muted rounded p-2">
<p class="font-semibold mb-1">
Create or rotate API tokens in Settings Security API tokens.
</p>
<p class="font-semibold mb-1">{getAPITokenManagementLocationMessage()}</p>
<p>
Tokens are managed in the UI and stored in <code>api_tokens.json</code>.
</p>
@@ -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(
@@ -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.',
@@ -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'),
@@ -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;