feat(secrets): scoped shared secrets rule-management UI (Phase 2)

Completes scoped shared secrets end-to-end: /shared-secrets list/new/edit
routes (mirroring metric-alert-rules) with an env-key name, a WRITE-ONLY
value (password input; never pre-filled — the API returns only has_value;
omitted on PATCH to keep the stored secret, provided to rotate; cleared
after save), an encrypted toggle (flipping it requires re-entering the
value, matching the server's 400 guard), a global|app scope with an
App-grouping picker (listApps), description, and enabled. 409 conflicts
surface a friendly message. New "System" nav entry (IconKey) + api.ts
client + full sharedsecrets.* i18n (en/ru parity).

Reviewed: typescript APPROVE (0 CRITICAL/HIGH).
This commit is contained in:
2026-05-29 16:11:46 +03:00
parent fa6d5bd3ba
commit 15e5b186cd
7 changed files with 1990 additions and 1 deletions
+51
View File
@@ -1428,3 +1428,54 @@ export function deleteMetricAlertRule(id: number): Promise<void> {
return del<void>(`/api/metric-alert-rules/${id}`);
}
// ── Shared secrets ──────────────────────────────────────────────────
// A shared secret is a named env KEY whose value the reconciler injects
// into matching workloads. Scope model: scope="global" applies to every
// app; scope="app" + app_id narrows it to one app grouping.
//
// The value is WRITE-ONLY: the API never returns it, only a `has_value`
// presence flag. On update, omit `value` to keep the stored secret;
// provide it to set/rotate. Flipping `encrypted` requires resubmitting
// `value` (the server 400s otherwise). IDs are uuid strings.
export interface SharedSecret {
id: string;
name: string; // the env KEY
has_value: boolean; // true if a value is stored (value itself is write-only, never returned)
encrypted: boolean;
scope: 'global' | 'app';
app_id: string; // set when scope === 'app'
description: string;
enabled: boolean;
created_at: string;
updated_at: string;
}
export interface SharedSecretInput {
name?: string;
value?: string; // omit to keep the existing value on update; provide to set/rotate
encrypted?: boolean;
scope?: 'global' | 'app';
app_id?: string;
description?: string;
enabled?: boolean;
}
export function listSharedSecrets(opts?: {
appID?: string;
signal?: AbortSignal;
}): Promise<SharedSecret[]> {
const params = opts?.appID ? `?app_id=${encodeURIComponent(opts.appID)}` : '';
return get<SharedSecret[]>(`/api/shared-secrets${params}`, opts?.signal);
}
export function getSharedSecret(id: string, signal?: AbortSignal): Promise<SharedSecret> {
return get<SharedSecret>(`/api/shared-secrets/${id}`, signal);
}
export function createSharedSecret(data: SharedSecretInput): Promise<SharedSecret> {
return post<SharedSecret>('/api/shared-secrets', data);
}
export function updateSharedSecret(id: string, data: SharedSecretInput): Promise<SharedSecret> {
return patch<SharedSecret>(`/api/shared-secrets/${id}`, data);
}
export function deleteSharedSecret(id: string): Promise<void> {
return del<void>(`/api/shared-secrets/${id}`);
}