feat(api-tokens): UI fuer Bearer-Tokens — Modul 3/16 des UI-Backlogs

Backend existierte vollstaendig (POST create mit Einmal-Plaintext-Anzeige,
GET list ohne Hashes, DELETE revoke; mcp:read/mcp:write via mcp_server-
Plugin registriert), Frontend hatte 0% Abdeckung.

- api/apiTokens.ts: TanStack-Hooks (useApiTokens, create mit
  ApiTokenCreated-Response inkl. Einmal-Token, revoke)
- pages/ApiTokens.tsx: Token-Karten (Name, Scope-Badges, Ablauf, zuletzt
  genutzt, Abgelaufen-Badge), Create-Dialog (Name, Scopes als
  Komma-Liste, optionale Gueltigkeit in Tagen), EINMALIGE
  Plaintext-Anzeige mit Copy-Button und Warnung, Revoke mit Confirm —
  Aktionen hinter mcp:write gegated
- Platzierung: Settings-Subpage /settings/api-tokens (statisch, Core-Route)
  + Settings-Nav-Item (true-core-settings-Muster)
- i18n apiTokens.* de/en

Verifikation: Vitest 8/8 (Rendering, Scopes, Ablauf-Badge,
Permission-Gating, Create-Flow mit Reveal-Dialog, Revoke) · tsc exit 0 ·
production build exit 0.
This commit is contained in:
Agent Zero
2026-09-13 09:23:01 +02:00
parent f9ff92bec9
commit 4bdc6c66c7
7 changed files with 583 additions and 0 deletions
+66
View File
@@ -0,0 +1,66 @@
/**
* API Tokens client — Bearer tokens for programmatic access.
*
* Backend: /api/v1/tokens (create, list, revoke).
* The plaintext token is returned ONCE at creation — never stored.
* Permissions: mcp:read (list) / mcp:write (create, revoke).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost, apiDelete } from '@/api/client';
export interface ApiTokenInfo {
id: string;
name: string;
scopes: string[];
expires_at: string | null;
last_used_at: string | null;
created_at: string | null;
}
export interface ApiTokenListResponse {
items: ApiTokenInfo[];
total: number;
}
/** Response of the create endpoint — includes the plaintext token ONCE. */
export interface ApiTokenCreated extends ApiTokenInfo {
token: string;
}
export interface ApiTokenCreatePayload {
name: string;
scopes?: string[];
expires_in_days?: number | null;
}
// ─── Query hooks ─────────────────────────────────────────────
export function useApiTokens() {
return useQuery<ApiTokenListResponse>({
queryKey: ['api-tokens'],
queryFn: () => apiGet<ApiTokenListResponse>('/tokens'),
});
}
// ─── Mutation hooks ──────────────────────────────────────────
export function useCreateApiToken() {
const qc = useQueryClient();
return useMutation<ApiTokenCreated, Error, ApiTokenCreatePayload>({
mutationFn: (data) => apiPost<ApiTokenCreated>('/tokens', data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['api-tokens'] });
},
});
}
export function useRevokeApiToken() {
const qc = useQueryClient();
return useMutation<void, Error, string>({
mutationFn: (tokenId) => apiDelete(`/tokens/${tokenId}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['api-tokens'] });
},
});
}