TypeScript 97.5%
SQL 1.4%
Python 0.8%
1import Link from "next/link";2import { ShieldCheck } from "lucide-react";3import { getWorkspace } from "@/lib/session";4import { listApiKeys } from "@/lib/queries/account";5import { PageHeader } from "@/components/ui/page-header";6import { Alert } from "@/components/ui/alert";7import { cn } from "@/lib/utils";8import { KeysTable } from "@/components/dashboard/api-keys/keys-table";9import { CreateKeyDialog } from "@/components/dashboard/api-keys/create-key-dialog";1011export const dynamic = "force-dynamic";1213export const metadata = { title: "API keys · Fetcha" };1415export default async function ApiKeysPage({ searchParams }: { searchParams: Promise<{ project?: string; new?: string }> }) {16 const sp = await searchParams;17 const ws = await getWorkspace();1819 // ?project=all → every project of the org; ?project=<id> → that project; default → current project.20 const allProjects = sp.project === "all";21 const requested = !allProjects && sp.project ? ws.projects.find((p) => p.id === sp.project) : null;22 const scopeProject = allProjects ? null : requested ?? ws.project;2324 const keys = await listApiKeys(ws.organization.id, scopeProject?.id ?? null);25 const projectOptions = ws.projects.map((p) => ({ id: p.id, name: p.name, environment: p.environment }));26 const active = keys.filter((k) => k.status === "active").length;2728 return (29 <div className="grid gap-6">30 <PageHeader31 title="API keys"32 description="Authenticate requests with a Bearer token. Keys belong to a project and inherit its limits and defaults."33 actions={<CreateKeyDialog projects={projectOptions} defaultProjectId={scopeProject?.id ?? ws.project.id} defaultOpen={sp.new === "1"} />}34 />3536 <div className="flex flex-wrap items-center justify-between gap-3">37 <nav className="inline-flex h-9 items-center gap-0.5 rounded-md bg-bg-muted p-1 text-[13px] font-medium text-fg-muted" aria-label="Project filter">38 <FilterLink href={`/dashboard/api-keys?project=${ws.project.id}`} active={!allProjects && scopeProject?.id === ws.project.id}>39 {ws.project.name}40 <span className="ml-1 text-[11px] font-normal text-fg-subtle">current</span>41 </FilterLink>42 {requested && requested.id !== ws.project.id ? (43 <FilterLink href={`/dashboard/api-keys?project=${requested.id}`} active>44 {requested.name}45 </FilterLink>46 ) : null}47 <FilterLink href="/dashboard/api-keys?project=all" active={allProjects}>48 All projects49 </FilterLink>50 </nav>51 <p className="text-[12.5px] text-fg-subtle tabular">52 {active} active · {keys.length - active} inactive53 </p>54 </div>5556 <KeysTable keys={keys} projects={projectOptions} defaultProjectId={scopeProject?.id ?? ws.project.id} showProject={allProjects} />5758 <Alert59 variant="info"60 title="Keep your keys secret"61 action={62 <Link href="/docs/authentication" className="text-[13px] font-medium text-accent underline-offset-4 hover:underline">63 Auth docs64 </Link>65 }66 >67 <ul className="mt-1 grid gap-1 text-[13px]">68 <li className="flex gap-2">69 <ShieldCheck className="mt-0.5 size-3.5 shrink-0 text-info" aria-hidden />70 Keys are hashed at rest — we cannot recover a lost secret, only rotate it.71 </li>72 <li className="flex gap-2">73 <ShieldCheck className="mt-0.5 size-3.5 shrink-0 text-info" aria-hidden />74 Rotate regularly and give every service, environment and contractor its own key with the minimum scopes.75 </li>76 <li className="flex gap-2">77 <ShieldCheck className="mt-0.5 size-3.5 shrink-0 text-info" aria-hidden />78 Never commit keys to source control or ship them in browser code. Load them from environment variables on your server.79 </li>80 </ul>81 </Alert>82 </div>83 );84}8586function FilterLink({ href, active, children }: { href: string; active: boolean; children: React.ReactNode }) {87 return (88 <Link href={href} aria-current={active ? "page" : undefined} className={cn("inline-flex h-7 items-center rounded-[5px] px-3 transition-colors", active ? "bg-bg text-fg shadow-xs" : "hover:text-fg")}>89 {children}90 </Link>91 );92}93