import { useState, useEffect, useRef } from "react"; import { Plus, Copy, Trash2, Loader2, AlertCircle, CheckCircle, MoreHorizontal, Edit2, Check } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { api, OrganizationApiKey } from "@/lib/api"; import { useToast } from "@/hooks/use-toast"; import { useOrg } from "@/contexts/OrgContext"; import { formatDate } from "@/lib/date"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; interface NewApiKeyState { key: string; name: string; description?: string; createdAt: string; } interface EditingKey { id: string; name: string; description: string | null; } function useCopyButton() { const [copied, setCopied] = useState(false); const copy = (text: string) => { navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(() => setCopied(false), 2000); }); }; return { copied, copy }; } export default function ApiKeysPage() { const { toast } = useToast(); const { selectedOrgId: orgId } = useOrg(); const queryClient = useQueryClient(); const { copy, copied } = useCopyButton(); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [newSecret, setNewSecret] = useState(null); const [editingKey, setEditingKey] = useState(null); const [showKey, setShowKey] = useState(false); const [isCreating, setIsCreating] = useState(false); const nameRef = useRef(null); const descriptionRef = useRef(null); const editNameRef = useRef(null); const editDescriptionRef = useRef(null); // Fetch API keys const { data: apiKeysData, isLoading } = useQuery({ queryKey: ['api-keys', orgId], queryFn: () => orgId ? api.organizations.getApiKeys(orgId) : null, enabled: !!orgId, }); // Create API key mutation const { mutate: createKey, isPending: isCreatingKey } = useMutation({ mutationFn: () => { if (!orgId) throw new Error('Organization ID not set'); const name = nameRef.current?.value; const description = descriptionRef.current?.value; if (!name) throw new Error('Name is required'); return api.organizations.createApiKey(orgId, name, description); }, onSuccess: (data) => { const apiKey = data.api_key; setNewSecret({ key: apiKey.key || '', name: apiKey.name, description: apiKey.description || undefined, createdAt: apiKey.created_at, }); setIsCreateDialogOpen(false); if (nameRef.current) nameRef.current.value = ''; if (descriptionRef.current) descriptionRef.current.value = ''; queryClient.invalidateQueries({ queryKey: ['api-keys', orgId] }); toast({ title: 'API Key Created', description: 'Store the key value securely - you won\'t be able to see it again.', }); }, onError: () => { toast({ title: 'Failed to create API key', description: 'Please try again.', variant: 'destructive', }); }, }); // Update API key mutation const { mutate: updateKey, isPending: isUpdatingKey } = useMutation({ mutationFn: () => { if (!orgId || !editingKey) throw new Error('Required data missing'); return api.organizations.updateApiKey(orgId, editingKey.id, { name: editNameRef.current?.value, description: editDescriptionRef.current?.value, }); }, onSuccess: () => { setIsEditDialogOpen(false); queryClient.invalidateQueries({ queryKey: ['api-keys', orgId] }); toast({ title: 'API Key Updated', description: 'Changes saved successfully.', }); }, onError: () => { toast({ title: 'Failed to update API key', description: 'Please try again.', variant: 'destructive', }); }, }); // Delete API key mutation const { mutate: deleteKey, isPending: isDeletingKey } = useMutation({ mutationFn: (keyId: string) => { if (!orgId) throw new Error('Organization ID not set'); return api.organizations.deleteApiKey(orgId, keyId); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['api-keys', orgId] }); toast({ title: 'API Key Deleted', description: 'The API key has been permanently removed.', }); }, onError: () => { toast({ title: 'Failed to delete API key', description: 'Please try again.', variant: 'destructive', }); }, }); const handleCreateKey = () => { setIsCreating(true); createKey(); setIsCreating(false); }; const handleEditKey = (key: OrganizationApiKey) => { setEditingKey({ id: key.id, name: key.name, description: key.description, }); setIsEditDialogOpen(true); }; const handleUpdateKey = () => { updateKey(); }; const handleDeleteKey = (keyId: string) => { if (confirm('Are you sure you want to delete this API key? This action cannot be undone.')) { deleteKey(keyId); } }; const apiKeys = apiKeysData?.api_keys || []; const activeKeys = apiKeys.filter(k => !k.is_revoked); const revokedKeys = apiKeys.filter(k => k.is_revoked); if (isLoading) { return (
); } return (

API Keys

Manage API keys for programmatic access to your organization.

{/* New key reveal banner */} {newSecret && (
API key created — copy it now, you won't see it again.
{newSecret.key}
)} {/* Key list */} {isLoading ? (
Loading...
) : apiKeys.length === 0 ? (

No API keys yet

Create one to enable external integrations.

) : (
{activeKeys.map((key) => (
{key.name} {key.last_used_at && ( Last used {formatDate(key.last_used_at)} )}
{key.description && (

{key.description}

)}

Created {formatDate(key.created_at)}

handleEditKey(key)} className="cursor-pointer"> Edit handleDeleteKey(key.id)} className="text-destructive cursor-pointer" disabled={isDeletingKey} > Delete
))} {revokedKeys.length > 0 && ( <>
Revoked
{revokedKeys.map((key) => (

{key.name}

Revoked {formatDate(key.revoked_at || '')} {key.revoke_reason && ` — ${key.revoke_reason}`}

))} )}
)}
{/* Create Dialog */} Create API Key Create a new API key for external integrations. The key will be displayed only once.