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 (
| Name |
Prefix |
Product |
Created |
Last used |
{canRevoke && | }
{keys.map((k) => (
| {truncate(k.name, 30)} |
{k.prefix}… |
{k.product || all} |
{formatRelative(k.created_at)}
|
{k.last_used_at ? formatRelative(k.last_used_at) : never}
|
{canRevoke && (
|
)}
))}
);
}
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" };