import { useMutation, useQuery } from "@tanstack/react-query"; import { KeyRound, Plus, Trash2, X } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { createApiKeyMutation, deleteApiKeyMutation, getApiKeysOptions, } from "~/client/api-client/@tanstack/react-query.gen"; import type { GetApiKeysResponse } from "~/client/api-client/types.gen"; import { Alert, AlertDescription, AlertTitle } from "~/client/components/ui/alert"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { Button } from "~/client/components/ui/button"; import { CardContent, CardDescription, CardTitle } from "~/client/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/client/components/ui/dialog"; import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; import { useTimeFormat } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; type Props = { passwordAuthSupported: boolean; hasPassword: boolean; }; type ApiKey = GetApiKeysResponse["apiKeys"][number]; const EXPIRATION_OPTIONS = [ { value: "30", label: "30 days" }, { value: "90", label: "90 days" }, { value: "365", label: "1 year" }, { value: "never", label: "No expiration" }, ] as const; type ExpirationValue = (typeof EXPIRATION_OPTIONS)[number]["value"]; export function ApiKeysSection({ passwordAuthSupported, hasPassword }: Props) { const { formatDateTime } = useTimeFormat(); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [newKeyName, setNewKeyName] = useState(""); const [newKeyExpiration, setNewKeyExpiration] = useState("365"); const [password, setPassword] = useState(""); const [createdKey, setCreatedKey] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const { data, isPending } = useQuery(getApiKeysOptions()); const apiKeys = data?.apiKeys ?? []; const limit = data?.limit ?? 50; const canCreateApiKey = !passwordAuthSupported || hasPassword; const createKey = useMutation({ ...createApiKeyMutation(), onSuccess: (apiKey) => { toast.success("API key created"); setCreatedKey(apiKey.key); setNewKeyName(""); setPassword(""); }, onError: (error) => { toast.error("Failed to create API key", { description: error.message }); }, }); const deleteKey = useMutation({ ...deleteApiKeyMutation(), onSuccess: () => { toast.success("API key revoked"); setDeleteTarget(null); }, onError: (error) => { toast.error("Failed to revoke API key", { description: error.message }); }, }); const closeCreateDialog = () => { setCreateDialogOpen(false); setNewKeyName(""); setNewKeyExpiration("365"); setPassword(""); setCreatedKey(null); }; const handleCreate = (event: React.SubmitEvent) => { event.preventDefault(); const name = newKeyName.trim(); if (!name) { toast.error("Name is required"); return; } if (passwordAuthSupported && !password) { toast.error("Password is required"); return; } const expiresIn = newKeyExpiration === "never" ? null : Number(newKeyExpiration) * 24 * 60 * 60; createKey.mutate({ body: { name, password: passwordAuthSupported ? password : "", expiresIn } }); }; return ( <>
API Keys Create keys for API access to the active organization.

{apiKeys.length} active keys for this organization

Limit {limit} active keys per user.

Local password required A local password is required before API keys can be created.

Loading API keys...

Name Created Expires Last used Actions 0 })}> No API keys yet. {apiKeys.map((apiKey) => ( {apiKey.name ?? "Unnamed key"} {formatDateTime(new Date(apiKey.createdAt))} {apiKey.expiresAt ? formatDateTime(new Date(apiKey.expiresAt)) : "Never"} {apiKey.lastRequestAt ? formatDateTime(new Date(apiKey.lastRequestAt)) : "Never"} ))}
{ if (open) setCreateDialogOpen(true); else closeCreateDialog(); }} >
{createdKey ? "API key created" : "Create API key"} {createdKey ? "Save this key now. For security reasons it will not be shown again." : "The key is shown once after creation and cannot be revealed later."}
setNewKeyName(event.target.value)} maxLength={32} required />
setPassword(event.target.value)} required={passwordAuthSupported} />
e.currentTarget.select()} />
!open && setDeleteTarget(null)}> Revoke API key? This will revoke "{deleteTarget?.name ?? "this key"}". Future requests using it will fail. Cancel { event.preventDefault(); if (deleteTarget) { deleteKey.mutate({ path: { keyId: deleteTarget.id } }); } }} disabled={deleteKey.isPending} > Revoke ); }