"use client"; import { useEffect, useMemo, useState } from "react"; import { api } from "@/lib/api"; import { useAuth } from "@/hooks/useAuth"; import { formatDate } from "@/lib/utils"; import { Copy, Check, KeyRound, Plus, Trash2, ShieldAlert } from "lucide-react"; interface PermissionDef { key: string; label: string; group: string; } interface ApiKey { id: string; name: string; prefix: string; permissions: string[]; createdByPubkey: string; lastUsedAt: string | null; revokedAt: string | null; createdAt: string; } export default function ApiKeysPage() { const { can } = useAuth(); const allowed = can("api_keys.manage"); const [permissions, setPermissions] = useState([]); const [keys, setKeys] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [name, setName] = useState(""); const [selected, setSelected] = useState>({}); const [creating, setCreating] = useState(false); const [revealedKey, setRevealedKey] = useState<{ name: string; key: string } | null>(null); const [copied, setCopied] = useState(false); const load = async () => { setLoading(true); try { const [registry, list] = await Promise.all([ api.getPermissionRegistry(), api.getApiKeys(), ]); setPermissions(registry.permissions); setKeys(list); } catch (err: any) { setError(err.message); } finally { setLoading(false); } }; useEffect(() => { if (allowed) load(); else setLoading(false); }, [allowed]); const groups = useMemo(() => { const order: string[] = []; const byGroup: Record = {}; for (const perm of permissions) { if (!byGroup[perm.group]) { byGroup[perm.group] = []; order.push(perm.group); } byGroup[perm.group].push(perm); } return order.map((group) => ({ group, perms: byGroup[group] })); }, [permissions]); const selectedKeys = useMemo( () => Object.entries(selected).filter(([, v]) => v).map(([k]) => k), [selected] ); const toggle = (key: string) => { setSelected((prev) => ({ ...prev, [key]: !prev[key] })); }; const handleCreate = async () => { if (!name.trim() || selectedKeys.length === 0) return; setCreating(true); setError(""); try { const created = await api.createApiKey({ name: name.trim(), permissions: selectedKeys }); setRevealedKey({ name: created.name, key: created.key }); setCopied(false); setName(""); setSelected({}); await load(); } catch (err: any) { setError(err.message); } finally { setCreating(false); } }; const handleRevoke = async (id: string) => { setError(""); try { await api.revokeApiKey(id); await load(); } catch (err: any) { setError(err.message); } }; const handleCopy = async () => { if (!revealedKey) return; try { await navigator.clipboard.writeText(revealedKey.key); setCopied(true); window.setTimeout(() => setCopied(false), 2000); } catch { // ignore } }; if (!allowed) { return (
You do not have permission to manage API keys.
); } if (loading) { return (
Loading API keys...
); } const labelFor = (key: string) => permissions.find((p) => p.key === key)?.label ?? key; return (

API Keys

Create scoped API keys for programmatic access. A key is shown in full only once, right after you create it, so copy it immediately.

{error &&

{error}

} {revealedKey && (
Copy your new key now — it won't be shown again

Key for {revealedKey.name}:

{revealedKey.key}
)} {/* Create form */}

Create API Key

setName(e.target.value)} placeholder="e.g. Blog publishing bot" maxLength={100} className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 text-sm focus:outline-none focus:ring-1 focus:ring-primary/40" />

Permissions

{groups.map(({ group, perms }) => (

{group}

{perms.map((perm) => ( ))}
))}
{/* Existing keys */}

Existing Keys

{keys.length === 0 ? (

No API keys yet.

) : ( keys.map((key) => (

{key.name}

{key.revokedAt && ( Revoked )}

{key.prefix}…

Created {formatDate(key.createdAt)} {key.lastUsedAt ? ` · Last used ${formatDate(key.lastUsedAt)}` : " · Never used"}

{key.permissions.map((p) => ( {labelFor(p)} ))}
{!key.revokedAt && ( )}
)) )}
); }