import { useEffect, useMemo, useRef, useState } from "react"; import { Pencil, Plus, Search, Trash2 } from "lucide-react"; import type { ApiSettings, AuthInfo } from "@govoplan/core-webui"; import { createApiKey, fetchApiKeysDelta, fetchPermissionCatalog, fetchUsersDelta, revokeApiKey, type ApiKeyAdminItem, type PermissionItem, type UserAdminItem } from "../../api/admin"; import { Button } from "@govoplan/core-webui"; import { DataGrid, type DataGridColumn } from "@govoplan/core-webui"; import { Dialog } from "@govoplan/core-webui"; import { FormField } from "@govoplan/core-webui"; import { DateTimeField } from "@govoplan/core-webui"; import { StatusBadge } from "@govoplan/core-webui"; import { ConfirmDialog } from "@govoplan/core-webui"; import { AdminIconButton, AdminPageLayout, AdminSelectionList, adminErrorMessage, formatAdminDateTime as formatDateTime, useDeltaWatermarks } from "@govoplan/core-webui"; import { scopeGrants, i18nMessage, useUnsavedDraftGuard } from "@govoplan/core-webui"; import { loadDeltaRows } from "./utils/deltaRows"; function defaultDraft(userId: string) { return { name: "", userId, scopes: ["campaign:read"], expiresAt: "" }; } export default function ApiKeysPanel({ settings, auth, canCreate, canRevoke }: {settings: ApiSettings;auth: AuthInfo;canCreate: boolean;canRevoke: boolean;}) { const [keys, setKeys] = useState([]); const [users, setUsers] = useState([]); const [permissions, setPermissions] = useState([]); const keysRef = useRef([]); const usersRef = useRef([]); const { getDeltaWatermark, setDeltaWatermark, resetDeltaWatermark } = useDeltaWatermarks(); const [showRevoked, setShowRevoked] = useState(false); const [creating, setCreating] = useState(false); const [viewing, setViewing] = useState(null); const [draft, setDraft] = useState(() => defaultDraft(auth.user.id)); const [savedDraftKey, setSavedDraftKey] = useState(() => draftKey(defaultDraft(auth.user.id))); const [secret, setSecret] = useState<{name: string;value: string;} | null>(null); const [revoking, setRevoking] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [success, setSuccess] = useState(""); const dirty = creating && draftKey(draft) !== savedDraftKey; useUnsavedDraftGuard({ dirty, onSave: save, onDiscard: closeCreate }); async function load() { setLoading(true); setError(""); try { const keyScope = showRevoked ? "all" : "active"; const [nextKeys, nextUsers, nextPermissions] = await Promise.all([ loadDeltaRows( keysRef.current, `access:api-keys:${keyScope}`, getDeltaWatermark, setDeltaWatermark, (since) => fetchApiKeysDelta(settings, showRevoked, { since }), (response) => response.api_keys, (key) => key.id, "access_api_key", sortApiKeys ), loadDeltaRows( usersRef.current, "access:api-keys:users", getDeltaWatermark, setDeltaWatermark, (since) => fetchUsersDelta(settings, { since }), (response) => response.users, (user) => user.id, "access_user", sortUsers ), fetchPermissionCatalog(settings) ]); keysRef.current = nextKeys; usersRef.current = nextUsers; setKeys(nextKeys); setUsers(nextUsers.filter((user) => user.is_active && user.account_is_active)); setPermissions(nextPermissions.filter((permission) => permission.level === "tenant")); } catch (err) {setError(adminErrorMessage(err));} finally {setLoading(false);} } useEffect(() => { keysRef.current = []; usersRef.current = []; resetDeltaWatermark(); void load(); }, [settings.accessToken, settings.apiBaseUrl, (auth.active_tenant ?? auth.tenant).id, showRevoked, resetDeltaWatermark]); const selectedUser = users.find((user) => user.id === draft.userId); const allowedPermissions = permissions.filter((permission) => selectedUser?.effective_scopes.some((scope) => scopeGrants(scope, permission.scope))); const columns = useMemo[]>(() => [ { id: "name", header: "i18n:govoplan-access.name.709a2322", width: "minmax(190px, 1fr)", minWidth: 170, resizable: true, sticky: "start", sortable: true, filterable: true, value: (row) => row.name, render: (row) =>
{row.name}
{row.prefix}…
}, { id: "owner", header: "i18n:govoplan-access.owner.89ff3122", width: 250, minWidth: 180, maxWidth: 480, resizable: true, fill: true, sortable: true, filterable: true, value: (row) => row.user_email }, { id: "scopes", header: "i18n:govoplan-access.scopes.c23540e5", width: 120, resizable: false, sortable: true, filterable: true, filterType: "integer", value: (row) => row.scopes.length, render: (row) => String(row.scopes.length) }, { id: "status", header: "i18n:govoplan-access.status.bae7d5be", width: 120, resizable: false, sortable: true, filterable: true, value: (row) => row.revoked_at ? "revoked" : "active", render: (row) => }, { id: "last_used", header: "i18n:govoplan-access.last_used.f1109d3d", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.last_used_at || "", render: (row) => formatDateTime(row.last_used_at) }, { id: "expires", header: "i18n:govoplan-access.expires.a99be3da", width: 180, minWidth: 150, resizable: true, sortable: true, value: (row) => row.expires_at || "", render: (row) => row.expires_at ? formatDateTime(row.expires_at) : "i18n:govoplan-access.no_expiry.39d436aa" }, { id: "actions", header: "i18n:govoplan-access.actions.c3cd636a", width: 150, sticky: "end", resizable: false, align: "right", render: (row) =>
} onClick={() => setViewing(row)} /> } disabled /> } variant="danger" onClick={() => setRevoking(row)} disabled={!canRevoke || Boolean(row.revoked_at)} />
}], [canRevoke]); function openCreate() { const defaultUser = users.find((user) => user.id === auth.user.id) ?? users[0]; const nextDraft = defaultDraft(defaultUser?.id || ""); setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); setCreating(true); setError(""); } function closeCreate() { setCreating(false); const nextDraft = defaultDraft(auth.user.id); setDraft(nextDraft); setSavedDraftKey(draftKey(nextDraft)); } async function save(): Promise { setBusy(true); setError(""); try { const created = await createApiKey(settings, { name: draft.name, user_id: draft.userId, scopes: draft.scopes, expires_at: draft.expiresAt ? new Date(draft.expiresAt).toISOString() : null }); setSecret({ name: created.name, value: created.secret }); setCreating(false); setSuccess(i18nMessage("i18n:govoplan-access.api_key_value_created.0ccdfbb2", { value0: created.name })); await load(); return true; } catch (err) {setError(adminErrorMessage(err));return false;} finally {setBusy(false);} } async function revoke() { if (!revoking) return; setBusy(true); setError(""); try { await revokeApiKey(settings, revoking.id); setSuccess(i18nMessage("i18n:govoplan-access.api_key_value_revoked.b4431f0e", { value0: revoking.name })); setRevoking(null); await load(); } catch (err) {setError(adminErrorMessage(err));} finally {setBusy(false);} } return ( <> } variant="primary" onClick={openCreate} disabled={!canCreate || !users.length} />}>
row.id} emptyText="i18n:govoplan-access.no_api_keys_found.1f377128" />
!busy && setCreating(false)} className="admin-dialog admin-dialog-wide" footer={<>}>
setDraft({ ...draft, name: event.target.value })} /> setDraft({ ...draft, expiresAt: value })} />
i18n:govoplan-access.allowed_scopes.d94515ff ({ id: permission.scope, label: permission.label, description: i18nMessage("i18n:govoplan-access.value_value.0e2772ed", { value0: permission.scope, value1: permission.description }) }))} selected={draft.scopes} onChange={(scopes) => setDraft({ ...draft, scopes })} emptyText="i18n:govoplan-access.the_selected_user_has_no_tenant_permissions_avai.96985ec7" />
setViewing(null)} className="admin-dialog admin-dialog-wide" footer={}> {viewing && <>
i18n:govoplan-access.name.709a2322
{viewing.name}
i18n:govoplan-access.prefix.90eceb01
{viewing.prefix}…
i18n:govoplan-access.owner.89ff3122
{viewing.user_email}
i18n:govoplan-access.status.bae7d5be
{viewing.revoked_at ? "i18n:govoplan-access.revoked.85f17ac0" : "i18n:govoplan-access.active.a733b809"}
i18n:govoplan-access.created.accf40c8
{formatDateTime(viewing.created_at)}
i18n:govoplan-access.last_used.f1109d3d
{formatDateTime(viewing.last_used_at)}
i18n:govoplan-access.expires.a99be3da
{viewing.expires_at ? formatDateTime(viewing.expires_at) : "i18n:govoplan-access.no_expiry.39d436aa"}
i18n:govoplan-access.revoked.85f17ac0
{formatDateTime(viewing.revoked_at)}

i18n:govoplan-access.scopes.c23540e5

{viewing.scopes.map((scope) => {scope})}
}
setSecret(null)} className="admin-dialog" footer={}> {secret && <>

i18n:govoplan-access.the_secret_for.c60737ef {secret.name} i18n:govoplan-access.is_shown_once.af2b1235

{secret.value}

i18n:govoplan-access.store_it_in_a_secret_manager_only_its_prefix_and.796ac588

}
setRevoking(null)} onConfirm={() => void revoke()} /> ); } function draftKey(draft: ReturnType): string { return JSON.stringify(draft); } function sortApiKeys(left: ApiKeyAdminItem, right: ApiKeyAdminItem): number { return left.name.localeCompare(right.name) || left.prefix.localeCompare(right.prefix); } function sortUsers(left: UserAdminItem, right: UserAdminItem): number { return left.email.localeCompare(right.email); }