feat: Manage API tokens from settings

A card in the Access group beside Members and single sign-on rather than a
new nav entry — /settings/instance was folded back in for exactly this
reason. The plaintext is shown once in a well block and never again.

Tokens outside a newly tightened lifetime policy are flagged rather than
broken, because the policy governs issuance, not existing credentials.
This commit is contained in:
2026-08-12 14:58:57 +00:00
parent 3b4c87a292
commit 182752d9ab
3 changed files with 442 additions and 1 deletions
+39 -1
View File
@@ -197,8 +197,22 @@ export interface Settings {
secrets: SecretsSettings;
workflow_log_retention_days?: number | null;
local_login_enabled?: boolean;
api_token_max_days?: number | null;
}
export type ApiToken = {
token_id: string;
name: string;
hint: string;
role: Role;
scopes: string[];
expires_at?: string | null;
created_at: string;
last_used_at?: string | null;
user_id: string;
user_email?: string;
};
export interface SecretGroupSummary {
group: string;
key_count: number;
@@ -683,7 +697,12 @@ export const api = {
return request<Settings>("/settings");
},
saveSettings(settings: { alerts: AlertSettings; workflow_log_retention_days?: number | null; local_login_enabled?: boolean }): Promise<{ saved: boolean }> {
saveSettings(settings: {
alerts: AlertSettings;
workflow_log_retention_days?: number | null;
local_login_enabled?: boolean;
api_token_max_days?: number | null;
}): Promise<{ saved: boolean }> {
return request<{ saved: boolean }>("/settings", {
method: "PUT",
body: JSON.stringify(settings),
@@ -694,6 +713,25 @@ export const api = {
return request<{ token: string }>("/settings/secrets-token", { method: "POST" });
},
listApiTokens(all = false): Promise<{ tokens: ApiToken[]; all: boolean }> {
return request<{ tokens: ApiToken[]; all: boolean }>(`/tokens${all ? "?all=true" : ""}`);
},
listTokenScopes(): Promise<{ scopes: string[] }> {
return request<{ scopes: string[] }>("/tokens/scopes");
},
createApiToken(body: { name: string; role: Role; scopes: string[]; expires_in_days?: number | null }): Promise<{ token: string; record: ApiToken }> {
return request<{ token: string; record: ApiToken }>("/tokens", {
method: "POST",
body: JSON.stringify(body),
});
},
revokeApiToken(tokenId: string): Promise<{ revoked: boolean }> {
return request<{ revoked: boolean }>(`/tokens/${tokenId}`, { method: "DELETE" });
},
listSecretGroups(): Promise<SecretGroupSummary[]> {
return request<SecretGroupSummary[]>("/secrets");
},