import "./SecretsView.css"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Check, Copy, Eye, EyeOff, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; type ToastKind = "info" | "success" | "error"; type SecretScope = "project" | "global"; type SecretPolicy = "auto" | "prompt" | "deny"; interface SecretRecord { id: string; scope: SecretScope; key: string; description: string | null; accessPolicy: SecretPolicy; envExportable: boolean; envExportKey: string | null; lastReadAt: string | null; } interface SecretsViewProps { addToast?: (msg: string, kind?: ToastKind) => void; } interface SecretFormState { key: string; value: string; description: string; scope: SecretScope; accessPolicy: SecretPolicy; envExportable: boolean; envExportKey: string; } const EMPTY_FORM: SecretFormState = { key: "", value: "", description: "", scope: "project", accessPolicy: "prompt", envExportable: false, envExportKey: "", }; export const SecretsView = ({ addToast }: SecretsViewProps) => { const [secrets, setSecrets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [formError, setFormError] = useState(null); const [editing, setEditing] = useState(null); const [showModal, setShowModal] = useState(false); const [showDeleteId, setShowDeleteId] = useState(null); const [form, setForm] = useState(EMPTY_FORM); const [showValue, setShowValue] = useState(false); const [revealedValues, setRevealedValues] = useState>({}); const [copiedId, setCopiedId] = useState(null); const revealTimersRef = useRef>>(new Map()); const copyTimersRef = useRef>>(new Map()); const request = useCallback(async (url: string, init?: RequestInit): Promise => { const response = await fetch(url, { ...init, headers: { "Content-Type": "application/json", ...(init?.headers ?? {}), }, }); if (!response.ok) { const payload = await response.json().catch(() => ({ error: "Request failed" })); throw new Error(String(payload?.error ?? "Request failed")); } if (response.status === 204) return undefined as T; return response.json() as Promise; }, []); const loadSecrets = useCallback(async () => { setLoading(true); setError(null); try { const data = await request<{ secrets: SecretRecord[] }>("/api/secrets"); setSecrets(data.secrets); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } }, [request]); useEffect(() => { void loadSecrets(); return () => { revealTimersRef.current.forEach((timer) => clearTimeout(timer)); copyTimersRef.current.forEach((timer) => clearTimeout(timer)); }; }, [loadSecrets]); const openCreate = () => { setEditing(null); setForm(EMPTY_FORM); setShowModal(true); setShowValue(false); setFormError(null); }; const openEdit = (secret: SecretRecord) => { setEditing(secret); setForm({ key: secret.key, value: "", description: secret.description ?? "", scope: secret.scope, accessPolicy: secret.accessPolicy, envExportable: secret.envExportable, envExportKey: secret.envExportKey ?? "", }); setShowModal(true); setShowValue(false); setFormError(null); }; const submit = async () => { setFormError(null); try { if (editing) { const body: Record = { key: form.key, description: form.description || null, accessPolicy: form.accessPolicy, envExportable: form.envExportable, envExportKey: form.envExportable ? (form.envExportKey || null) : null, }; if (form.value) body.value = form.value; await request(`/api/secrets/${editing.scope}/${editing.id}`, { method: "PATCH", body: JSON.stringify(body), }); } else { await request("/api/secrets", { method: "POST", body: JSON.stringify({ scope: form.scope, key: form.key, value: form.value, description: form.description || null, accessPolicy: form.accessPolicy, envExportable: form.envExportable, envExportKey: form.envExportable ? (form.envExportKey || null) : null, }), }); } setShowModal(false); setForm(EMPTY_FORM); await loadSecrets(); } catch (err) { setFormError(err instanceof Error ? err.message : String(err)); } }; const revealSecret = async (secret: SecretRecord) => { const data = await request<{ key: string; value: string }>(`/api/secrets/${secret.scope}/${secret.id}/reveal`, { method: "POST" }); setRevealedValues((current) => ({ ...current, [secret.id]: data.value })); addToast?.("Revealed", "success"); const timer = setTimeout(() => { setRevealedValues((current) => ({ ...current, [secret.id]: null })); }, 30000); const existing = revealTimersRef.current.get(secret.id); if (existing) clearTimeout(existing); revealTimersRef.current.set(secret.id, timer); }; const copySecret = async (secret: SecretRecord) => { const revealed = revealedValues[secret.id]; if (!revealed) return; await navigator.clipboard.writeText(revealed); setCopiedId(secret.id); addToast?.("Copied", "success"); const timer = setTimeout(() => { setCopiedId(null); setRevealedValues((current) => ({ ...current, [secret.id]: null })); }, 1500); const existing = copyTimersRef.current.get(secret.id); if (existing) clearTimeout(existing); copyTimersRef.current.set(secret.id, timer); }; const deleteSecret = async (secret: SecretRecord) => { await request(`/api/secrets/${secret.scope}/${secret.id}`, { method: "DELETE" }); setShowDeleteId(null); await loadSecrets(); }; const sortedSecrets = useMemo(() => [...secrets].sort((a, b) => a.key.localeCompare(b.key)), [secrets]); return (

Secrets

{error ?
{error}
: null} {loading ?
Loading…
: null} {!loading && sortedSecrets.length === 0 ?
No secrets found.
: null}
{sortedSecrets.map((secret) => { const revealed = revealedValues[secret.id]; return (
{secret.key}
{secret.scope} {secret.accessPolicy} {secret.envExportable ? env_exportable : null}
{revealed ?
{revealed}
: null}
{secret.lastReadAt ? new Date(secret.lastReadAt).toLocaleString() : "Never read"}
{showDeleteId === secret.id ? (
) : null}
); })}
{showModal ? (

{editing ? "Edit secret" : "Add secret"}

setForm((c) => ({ ...c, key: e.target.value }))} />
setForm((c) => ({ ...c, value: e.target.value }))} />