"use client"; import * as React from "react"; import { useRouter } from "next/navigation"; import { Plus } from "lucide-react"; import { API_KEY_SCOPES } from "@fetcha/core/client"; import { createApiKey, type CreatedKey } from "@/actions/api-keys"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Field, Hint, Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Alert } from "@/components/ui/alert"; import { Badge } from "@/components/ui/badge"; import { DEFAULT_SCOPES, SCOPE_INFO } from "@/lib/account-snippets"; import { cn } from "@/lib/utils"; import { KeyReveal } from "./key-reveal"; export interface ProjectOption { id: string; name: string; environment: string; } const EXPIRATIONS = [ { value: "0", label: "Never expires" }, { value: "30", label: "30 days" }, { value: "90", label: "90 days" }, { value: "365", label: "1 year" }, ]; export function CreateKeyDialog({ projects, defaultProjectId, trigger, defaultOpen, onCreated, }: { projects: ProjectOption[]; defaultProjectId: string; trigger?: React.ReactNode; defaultOpen?: boolean; onCreated?: (key: CreatedKey) => void; }) { const router = useRouter(); const [open, setOpen] = React.useState(Boolean(defaultOpen)); const [created, setCreated] = React.useState(null); function handleOpenChange(next: boolean) { // While the plaintext is on screen, closing must go through the explicit "Done" button. if (!next && created) return; setOpen(next); if (!next) setCreated(null); } return ( {trigger ?? ( )} created && e.preventDefault()} onPointerDownOutside={(e) => created && e.preventDefault()} onInteractOutside={(e) => created && e.preventDefault()}> {created ? ( <> API key created Your new key for {created.name} is ready. { setCreated(null); setOpen(false); router.refresh(); }} /> ) : ( <> Create API key Keys authenticate requests to the Fetcha API. The secret is shown once, then stored hashed. { setCreated(k); onCreated?.(k); }} /> )} ); } /** The form alone (also used inline by onboarding). */ export function CreateKeyForm({ projects, defaultProjectId, onCreated, compact, defaultName = "", submitLabel = "Create key", }: { projects: ProjectOption[]; defaultProjectId: string; onCreated: (key: CreatedKey) => void; compact?: boolean; defaultName?: string; submitLabel?: string; }) { const [pending, startTransition] = React.useTransition(); const [name, setName] = React.useState(defaultName); const [projectId, setProjectId] = React.useState(defaultProjectId); const [mode, setMode] = React.useState<"live" | "test">("live"); const [scopes, setScopes] = React.useState(DEFAULT_SCOPES); const [expires, setExpires] = React.useState("0"); const [error, setError] = React.useState(null); function toggleScope(s: string) { setScopes((cur) => (cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s])); } function submit(e: React.FormEvent) { e.preventDefault(); setError(null); startTransition(async () => { const res = await createApiKey({ name, projectId, mode, scopes, expiresInDays: Number(expires) || undefined }); if (!res.ok) { setError(res.error); return; } if (res.data) onCreated(res.data); }); } return (
{error ? {error} : null}
setName(e.target.value)} placeholder="e.g. Production backend" maxLength={64} required autoFocus /> Only for you — appears in the key list and audit log. Requests, usage and limits are tracked per project.
Mode
{( [ { v: "live", title: "Live", desc: "fch_live_… — real traffic through the network, billed against your plan." }, { v: "test", title: "Test", desc: "fch_test_… — for integration tests and CI. Same API, clearly separated in logs and usage." }, ] as const ).map((o) => ( ))}
Scopes
{API_KEY_SCOPES.map((s) => { const info = SCOPE_INFO[s]; const disabled = Boolean(info?.comingSoon); return ( ); })}
Expired keys are rejected with INVALID_API_KEY. Short-lived keys are safer for CI and contractors.
); }