import { revalidatePath } from "next/cache"; import { redirect } from "next/navigation"; import { auth } from "@/auth"; import { NotAuthorized } from "@/components/ShellEmpty"; import { formatDateTime, formatRelative, truncate } from "@/lib/format"; import type { SessionWithExtras } from "@/lib/session"; import { canSee } from "@/lib/session"; import { createAPIKey, fetchAPIKeys, fetchCatalog, fetchTenantBySlug, revokeAPIKey, type APIKey, } from "@/lib/tenant-registry"; export default async function APIKeysPage({ params, searchParams, }: { params: Promise<{ slug: string }>; searchParams: Promise<{ plaintext?: string; err?: string }>; }) { const { slug } = await params; const flash = await searchParams; const session = (await auth()) as SessionWithExtras | null; if (!canSee(session, "api-keys")) return ; const tenant = await fetchTenantBySlug(slug); if (!tenant) redirect(`/${slug}/dashboard`); const [keys, catalog] = await Promise.all([ fetchAPIKeys(tenant.id), fetchCatalog(), ]); const active = keys.filter((k) => !k.revoked_at); const revoked = keys.filter((k) => k.revoked_at); async function doCreate(formData: FormData) { "use server"; const name = String(formData.get("name") ?? "").trim(); const product = String(formData.get("product") ?? "").trim(); const tenantId = String(formData.get("tenant_id")); const slugV = String(formData.get("slug")); if (!name) redirect(`/${slugV}/settings/api-keys?err=missing_name`); const res = await createAPIKey({ tenant_id: tenantId, name, product: product || undefined, }); if (!res.ok) { redirect(`/${slugV}/settings/api-keys?err=${res.error}`); } revalidatePath(`/${slugV}/settings/api-keys`); redirect(`/${slugV}/settings/api-keys?plaintext=${encodeURIComponent(res.plaintext)}`); } async function doRevoke(formData: FormData) { "use server"; const id = String(formData.get("id")); const slugV = String(formData.get("slug")); const res = await revokeAPIKey(id); if (!res.ok) { redirect(`/${slugV}/settings/api-keys?err=${res.error}`); } revalidatePath(`/${slugV}/settings/api-keys`); redirect(`/${slugV}/settings/api-keys`); } return (

API keys

Per-tenant keys for headless product calls. Hashed with argon2id; the plaintext is shown once on creation.

{flash.plaintext && } {flash.err && }

Create a new key

Active keys ({active.length})

{active.length === 0 ? (

No active keys.

) : ( )} {revoked.length > 0 && ( <>

Revoked ({revoked.length})

)}
); } function PlaintextBanner({ plaintext }: { plaintext: string }) { return (
Key created

Store this value — it cannot be retrieved later.

{plaintext}
); } function ErrorBanner({ err }: { err: string }) { return (
{err === "name_taken" && "A key with that name already exists."} {err === "missing_name" && "Name is required."} {err === "invalid_input" && "Input failed validation."} {!["name_taken", "missing_name", "invalid_input"].includes(err) && `Error: ${err}`}
); } function KeyTable({ keys, doRevoke, slug, canRevoke, }: { keys: APIKey[]; doRevoke: (fd: FormData) => Promise; slug: string; canRevoke: boolean; }) { return (
{canRevoke && } {keys.map((k) => ( {canRevoke && ( )} ))}
Name Prefix Product Created Last used
{truncate(k.name, 30)} {k.prefix}… {k.product || all} {formatRelative(k.created_at)} {k.last_used_at ? formatRelative(k.last_used_at) : never}
); } const inputStyle: React.CSSProperties = { padding: "8px 10px", border: "1px solid #ddd", borderRadius: 6, fontSize: 14, }; const btnPrimary: React.CSSProperties = { marginTop: 4, padding: "8px 14px", background: "#0070f3", color: "white", border: "none", borderRadius: 6, fontSize: 14, cursor: "pointer", justifySelf: "start", }; const btnDanger: React.CSSProperties = { padding: "4px 8px", background: "white", color: "#a82626", border: "1px solid #e8a5a5", borderRadius: 4, fontSize: 12, cursor: "pointer", }; const th: React.CSSProperties = { padding: "8px 10px", color: "#666", fontWeight: 500 }; const td: React.CSSProperties = { padding: "8px 10px" };