1"use client";
2
3
16import { useState } from "react";
17import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
18import { Plus } from "lucide-react";
19import { orgWorkspaceApi } from "@/lib/org-admin";
20import { API_BASE } from "@/lib/constants";
21import { toast } from "@/stores/ui-store";
22import { Section } from "@/components/settings/settings-shell";
23import { ApiKeyTable } from "@/components/settings/api-keys/ApiKeyTable";
24import { ApiKeyCreateDialog } from "@/components/settings/api-keys/ApiKeyCreateDialog";
25import { ApiKeyRevealDialog } from "@/components/settings/api-keys/ApiKeyRevealDialog";
26
27export function AdminApiKeysTab({ workspaceId }: { workspaceId: string | null }) {
28 const qc = useQueryClient();
29 const [createOpen, setCreateOpen] = useState(false);
30 const [reveal, setReveal] = useState<{ rawKey: string; name: string } | null>(null);
31 const [revokingId, setRevokingId] = useState<string | null>(null);
32
33 const keys = useQuery({
34 queryKey: ["workspaces", workspaceId, "api-keys"],
35 queryFn: () => orgWorkspaceApi.listApiKeys(workspaceId!),
36 enabled: Boolean(workspaceId),
37 retry: false,
38 });
39
40 const create = useMutation({
41 mutationFn: (input: { name: string; scopes: string[]; rateLimit?: number; expiresAt?: string }) =>
42 orgWorkspaceApi.createApiKey(workspaceId!, input),
43 onSuccess: () => qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "api-keys"] }),
44 });
45
46 const revoke = useMutation({
47 mutationFn: (id: string) => orgWorkspaceApi.deleteApiKey(workspaceId!, id),
48 onMutate: (id) => setRevokingId(id),
49 onSettled: () => setRevokingId(null),
50 onSuccess: () => qc.invalidateQueries({ queryKey: ["workspaces", workspaceId, "api-keys"] }),
51 onError: (e) => toast("error", "Could not revoke the key", (e as Error).message),
52 });
53
54 if (!workspaceId) {
55 return (
56 <Section title="API keys" description="Pick a workspace above to manage its API keys.">
57 <p className="text-sm text-muted-foreground">No workspace selected.</p>
58 </Section>
59 );
60 }
61
62 return (
63 <Section title="API keys" description="Authenticate server-to-server calls. The raw key is shown once, at creation.">
64 <div className="mb-3 flex justify-end">
65 <button
66 type="button"
67 onClick={() => setCreateOpen(true)}
68 className="inline-flex items-center gap-1 rounded-md bg-primary px-2.5 py-1.5 text-sm font-medium text-primary-foreground hover:opacity-90"
69 data-testid="org-admin-apikey-create"
70 >
71 <Plus className="size-3.5" aria-hidden />
72 Create API key
73 </button>
74 </div>
75
76 <ApiKeyTable
77 rows={keys.data?.data ?? []}
78 isLoading={keys.isLoading}
79 baseApiUrl={API_BASE}
80 workspaceId={workspaceId}
81 onRevoke={(row) => revoke.mutate(row.id)}
82 revokingId={revokingId}
83 onCreate={() => setCreateOpen(true)}
84 />
85
86 <ApiKeyCreateDialog
87 open={createOpen}
88 onClose={() => setCreateOpen(false)}
89 onCreated={(k) => {
90 setCreateOpen(false);
91 setReveal(k);
92 }}
93 create={create.mutateAsync}
94 isPending={create.isPending}
95 />
96
97 {reveal ? (
98 <ApiKeyRevealDialog open rawKey={reveal.rawKey} keyName={reveal.name} onConfirmed={() => setReveal(null)} />
99 ) : null}
100 </Section>
101 );
102}
103