import { useEffect, useMemo, useState } from "react"; import { KeyRound, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react"; import type { ApiSettings, CredentialEnvelopeSummary, MailProfileScope } from "../types"; import { createCredentialEnvelope, deleteCredentialEnvelope, listCredentialEnvelopes, updateCredentialEnvelope } from "../api/credentials"; import Button from "./Button"; import Card from "./Card"; import ConfirmDialog from "./ConfirmDialog"; import ConnectionTree, { type ConnectionTreeColumn } from "./ConnectionTree"; import Dialog from "./Dialog"; import DismissibleAlert from "./DismissibleAlert"; import FormField from "./FormField"; import LoadingFrame from "./LoadingFrame"; import PasswordField from "./PasswordField"; import StatusBadge from "./StatusBadge"; import TableActionGroup from "./table/TableActionGroup"; import ToggleSwitch from "./ToggleSwitch"; import { useUnsavedDraftGuard } from "./UnsavedChangesGuard"; export type CredentialEnvelopeTargetOption = { id: string; label: string; secondary?: string | null; }; export type CredentialEnvelopeManagerProps = { settings: ApiSettings; scopeType: Extract; scopeId?: string | null; targetOptions?: CredentialEnvelopeTargetOption[]; targetLabel?: string; title?: string; canWrite: boolean; }; type CredentialKind = "username_password" | "token" | "api_key"; type CredentialDraft = { name: string; description: string; credentialKind: CredentialKind; username: string; secret: string; allowedModules: string; allowedServerRefs: string; inheritToLowerScopes: boolean; isActive: boolean; clearSecret: boolean; retainedPublicData: Record; }; const EMPTY_DRAFT: CredentialDraft = { name: "", description: "", credentialKind: "username_password", username: "", secret: "", allowedModules: "", allowedServerRefs: "", inheritToLowerScopes: false, isActive: true, clearSecret: false, retainedPublicData: {} }; export default function CredentialEnvelopeManager({ settings, scopeType, scopeId, targetOptions = [], targetLabel = "Target", title = "Credential envelopes", canWrite }: CredentialEnvelopeManagerProps) { const [selectedTargetId, setSelectedTargetId] = useState(scopeId ?? targetOptions[0]?.id ?? ""); const [credentials, setCredentials] = useState([]); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [editing, setEditing] = useState(null); const [draft, setDraft] = useState(EMPTY_DRAFT); const [savedDraftKey, setSavedDraftKey] = useState(""); const [deleting, setDeleting] = useState(null); const activeScopeId = scopeType === "system" || scopeType === "tenant" ? scopeId ?? null : selectedTargetId || null; const scopeReady = scopeType === "system" || scopeType === "tenant" || Boolean(activeScopeId); const draftDirty = Boolean(editing) && credentialDraftKey(draft) !== savedDraftKey; useUnsavedDraftGuard({ dirty: draftDirty, onSave: saveDraft, onDiscard: closeEditor }); useEffect(() => { if (scopeId) { setSelectedTargetId(scopeId); return; } if ( targetOptions.length > 0 && !targetOptions.some((option) => option.id === selectedTargetId) ) { setSelectedTargetId(targetOptions[0].id); } }, [scopeId, selectedTargetId, targetOptions]); useEffect(() => { void loadCredentials(); }, [ activeScopeId, scopeReady, scopeType, settings.accessToken, settings.apiBaseUrl, settings.apiKey ]); async function loadCredentials() { if (!scopeReady) { setCredentials([]); return; } setLoading(true); setError(""); try { setCredentials( await listCredentialEnvelopes(settings, { scope_type: scopeType, scope_id: activeScopeId, include_inactive: true }) ); } catch (err) { setCredentials([]); setError(errorMessage(err)); } finally { setLoading(false); } } function openCreate() { const next = { ...EMPTY_DRAFT, retainedPublicData: {} }; setEditing("new"); setDraft(next); setSavedDraftKey(credentialDraftKey(next)); setError(""); setNotice(""); } function openEdit(credential: CredentialEnvelopeSummary) { const next = credentialDraft(credential); setEditing(credential); setDraft(next); setSavedDraftKey(credentialDraftKey(next)); setError(""); setNotice(""); } function closeEditor() { if (saving) return; setEditing(null); setDraft(EMPTY_DRAFT); setSavedDraftKey(""); } async function saveDraft(): Promise { if (!editing || !scopeReady || !draft.name.trim() || !canWrite) return false; if (editing === "new" && !draft.secret.trim()) { setError("Enter a secret before creating the credential."); return false; } const kindChanged = editing !== "new" && draft.credentialKind !== editing.credential_kind; if (kindChanged && !draft.secret.trim()) { setError("Enter a replacement secret when changing the credential type."); return false; } setSaving(true); setError(""); try { const publicData = { ...draft.retainedPublicData, username: draft.username.trim() || undefined }; if (!draft.username.trim()) delete publicData.username; const shared = { name: draft.name.trim(), description: draft.description.trim() || null, credential_kind: draft.credentialKind, public_data: publicData, allowed_modules: splitValues(draft.allowedModules), allowed_server_refs: splitValues(draft.allowedServerRefs), inherit_to_lower_scopes: draft.inheritToLowerScopes, is_active: draft.isActive }; if (editing === "new") { await createCredentialEnvelope(settings, { scope_type: scopeType, scope_id: activeScopeId, ...shared, secret_data: secretPayload(draft) }); } else { await updateCredentialEnvelope(settings, editing.id, { ...shared, ...(draft.secret.trim() ? { secret_data: secretPayload(draft) } : {}), clear_secret: draft.clearSecret }); } setNotice(editing === "new" ? "Credential created." : "Credential saved."); closeEditor(); await loadCredentials(); return true; } catch (err) { setError(errorMessage(err)); return false; } finally { setSaving(false); } } async function confirmDelete() { if (!deleting) return; setSaving(true); setError(""); try { await deleteCredentialEnvelope(settings, deleting.id); setNotice("Credential deleted. Connections using it will remain configured but cannot authenticate."); setDeleting(null); await loadCredentials(); } catch (err) { setError(errorMessage(err)); } finally { setSaving(false); } } const columns = useMemo[]>( () => [ { id: "credential", header: "Credential", width: "minmax(240px, 1.4fr)", render: (credential) => (
{credential.name}
{credential.description || credential.public_data.username ? String(credential.description || credential.public_data.username) : credential.id}
) }, { id: "kind", header: "Type", width: "minmax(150px, 0.7fr)", render: (credential) => credentialKindLabel(credential.credential_kind) }, { id: "availability", header: "Availability", width: "minmax(220px, 1fr)", render: (credential) => availabilityLabel(credential) }, { id: "status", header: "Status", width: "120px", render: (credential) => ( ) } ], [] ); const kindChanged = editing !== null && editing !== "new" && draft.credentialKind !== editing.credential_kind; const saveDisabled = saving || !canWrite || !draft.name.trim() || (editing === "new" && !draft.secret.trim()) || (kindChanged && !draft.secret.trim()); return (
{targetOptions.length > 0 && ( )} {error && {error}} {notice && !error && {notice}}
} > credential.id} emptyText="No credentials are configured for this scope." renderActions={(credential) => ( , onClick: () => openEdit(credential), disabled: !canWrite || saving }, { id: "delete", label: `Delete ${credential.name}`, icon: , onClick: () => setDeleting(credential), disabled: !canWrite || saving, variant: "danger" } ]} /> )} /> } >

Identity

The name and type are visible; secret values are never returned by the API.

setDraft({ ...draft, name: event.target.value })} autoFocus /> setDraft({ ...draft, description: event.target.value })} /> setDraft({ ...draft, username: event.target.value })} /> setDraft({ ...draft, secret, clearSecret: false })} disabled={saving} autoComplete="new-password" /> {editing !== "new" && ( setDraft({ ...draft, clearSecret })} label="Remove configured secret" /> )}

Availability

Empty module or server lists mean every module or server allowed by scope.

setDraft({ ...draft, allowedModules: event.target.value })} /> setDraft({ ...draft, allowedServerRefs: event.target.value })} /> setDraft({ ...draft, inheritToLowerScopes })} label="Visible to lower scopes" /> setDraft({ ...draft, isActive })} label="Active" />
setDeleting(null)} onConfirm={() => void confirmDelete()} /> ); } function credentialDraft(credential: CredentialEnvelopeSummary): CredentialDraft { return { ...EMPTY_DRAFT, name: credential.name, description: credential.description ?? "", credentialKind: supportedCredentialKind(credential.credential_kind), username: String(credential.public_data.username ?? ""), allowedModules: credential.allowed_modules.join(", "), allowedServerRefs: credential.allowed_server_refs.join(", "), inheritToLowerScopes: credential.inherit_to_lower_scopes, isActive: credential.is_active, retainedPublicData: { ...credential.public_data } }; } function credentialDraftKey(draft: CredentialDraft): string { return JSON.stringify(draft); } function splitValues(value: string): string[] { return [...new Set(value.split(",").map((item) => item.trim()).filter(Boolean))]; } function secretPayload(draft: CredentialDraft): Record { const secret = draft.secret.trim(); if (!secret) return {}; if (draft.credentialKind === "token") return { token: secret }; if (draft.credentialKind === "api_key") return { api_key: secret }; return { password: secret }; } function supportedCredentialKind(value: string): CredentialKind { return value === "token" || value === "api_key" ? value : "username_password"; } function secretFieldLabel(kind: CredentialKind): string { if (kind === "token") return "Access token"; if (kind === "api_key") return "API key"; return "Password"; } function credentialKindLabel(kind: string): string { if (kind === "username_password") return "Username and password"; if (kind === "api_key") return "API key"; if (kind === "token") return "Access token"; return kind.split("_").join(" "); } function availabilityLabel(credential: CredentialEnvelopeSummary): string { const modules = credential.allowed_modules.length ? credential.allowed_modules.join(", ") : "All modules"; const servers = credential.allowed_server_refs.length ? `${credential.allowed_server_refs.length} server restriction${credential.allowed_server_refs.length === 1 ? "" : "s"}` : "all servers"; return `${modules} · ${servers}${credential.inherit_to_lower_scopes ? " · inherited" : ""}`; } function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); }