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:
@@ -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'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user