"use client"; import * as React from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { MoreHorizontal, RefreshCw, Ban, KeyRound } from "lucide-react"; import { revokeApiKey, rotateApiKey, type CreatedKey } from "@/actions/api-keys"; import type { ApiKeyRow } from "@/lib/queries/account"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Badge, StatusBadge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Alert } from "@/components/ui/alert"; import { EmptyState } from "@/components/ui/empty-state"; import { SimpleTooltip } from "@/components/ui/tooltip"; import { formatDateOnly, timeAgo, formatDate } from "@/lib/format"; import { maskedKey } from "@/lib/account-snippets"; import { KeyReveal } from "./key-reveal"; import { CreateKeyDialog, type ProjectOption } from "./create-key-dialog"; type PendingAction = { kind: "rotate" | "revoke"; key: ApiKeyRow } | null; export function KeysTable({ keys, projects, defaultProjectId, showProject }: { keys: ApiKeyRow[]; projects: ProjectOption[]; defaultProjectId: string; showProject: boolean }) { const router = useRouter(); const [pendingAction, setPendingAction] = React.useState(null); const [rotated, setRotated] = React.useState(null); const [error, setError] = React.useState(null); const [busy, startTransition] = React.useTransition(); function confirm() { if (!pendingAction) return; const { kind, key } = pendingAction; setError(null); startTransition(async () => { if (kind === "revoke") { const res = await revokeApiKey(key.id); if (!res.ok) { setError(res.error); return; } setPendingAction(null); router.refresh(); } else { const res = await rotateApiKey(key.id); if (!res.ok) { setError(res.error); return; } setPendingAction(null); if (res.data) setRotated(res.data); } }); } if (keys.length === 0) { return ( } /> ); } return ( <> {error && !pendingAction ? {error} : null}
Name Key {showProject ? Project : null} Mode Scopes Created Last used Expires Status Actions {keys.map((k) => { const inactive = k.status !== "active"; return ( {k.name} {maskedKey(k.keyPrefix, k.last4)} {showProject ? ( {k.projectName} ) : null} {k.mode}
{k.scopes.map((s) => ( {s} ))}
{formatDateOnly(k.createdAt)} {timeAgo(k.lastUsedAt)} {k.expiresAt ? formatDateOnly(k.expiresAt) : "Never"} setPendingAction({ kind: "rotate", key: k })}> Rotate key setPendingAction({ kind: "revoke", key: k })}> Revoke
); })}
{/* Confirm rotate / revoke */} { if (!o && !busy) { setPendingAction(null); setError(null); } }} > {pendingAction ? ( <> {pendingAction.kind === "rotate" ? "Rotate this key?" : "Revoke this key?"} {pendingAction.kind === "rotate" ? ( <> A new secret is generated for {pendingAction.key.name} with the same project, mode, scopes and expiration. The current key{" "} {maskedKey(pendingAction.key.keyPrefix, pendingAction.key.last4)} stops working immediately. ) : ( <> {pendingAction.key.name} ({maskedKey(pendingAction.key.keyPrefix, pendingAction.key.last4)}) will be rejected on the next request. This cannot be undone. )} {error ? {error} : null} ) : null} {/* Show the rotated key once */} { /* closing only through the Done button */ }} > e.preventDefault()} onPointerDownOutside={(e) => e.preventDefault()} onInteractOutside={(e) => e.preventDefault()}> {rotated ? ( <> Key rotated Update your integrations with the new secret. The previous key has been revoked. { setRotated(null); router.refresh(); }} /> ) : null} ); }