import { useState } from 'react' import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { Key, Plus, Copy, Trash2, AlertTriangle, Check } from 'lucide-react' import { EmptyState } from '@/components/ui/empty-state' import { apiKeysApi, type ApiKeyResponse, type ApiKeyCreateResponse, } from '@/lib/api' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' const AVAILABLE_SCOPES = [ { id: 'devices:read', label: 'Devices: Read' }, { id: 'devices:write', label: 'Devices: Write' }, { id: 'config:read', label: 'Config: Read' }, { id: 'config:write', label: 'Config: Write' }, { id: 'alerts:read', label: 'Alerts: Read' }, { id: 'firmware:write', label: 'Firmware: Write' }, ] as const function formatRelativeTime(iso: string | null): string { if (!iso) return 'Never' const date = new Date(iso) const now = new Date() const diffMs = now.getTime() - date.getTime() const diffMins = Math.floor(diffMs / 60000) if (diffMins < 1) return 'Just now' if (diffMins < 60) return `${diffMins}m ago` const diffHours = Math.floor(diffMins / 60) if (diffHours < 24) return `${diffHours}h ago` const diffDays = Math.floor(diffHours / 24) if (diffDays < 30) return `${diffDays}d ago` return date.toLocaleDateString() } function formatDate(iso: string | null): string { if (!iso) return 'Never' return new Date(iso).toLocaleDateString() } function getKeyStatus(key: ApiKeyResponse): { label: string className: string } { if (key.revoked_at) { return { label: 'Revoked', className: 'border-border bg-elevated/50 text-text-muted' } } if (key.expires_at && new Date(key.expires_at) <= new Date()) { return { label: 'Expired', className: 'border-error/30 bg-error/10 text-error' } } return { label: 'Active', className: 'border-success/30 bg-success/10 text-success' } } interface ApiKeysPageProps { tenantId: string } export function ApiKeysPage({ tenantId }: ApiKeysPageProps) { const queryClient = useQueryClient() const [showCreateDialog, setShowCreateDialog] = useState(false) const [showKeyDialog, setShowKeyDialog] = useState(false) const [showRevokeDialog, setShowRevokeDialog] = useState(false) const [revokeTarget, setRevokeTarget] = useState(null) const [newKey, setNewKey] = useState(null) const [copied, setCopied] = useState(false) // Create form state const [name, setName] = useState('') const [selectedScopes, setSelectedScopes] = useState([]) const [expiresAt, setExpiresAt] = useState('') const keysQuery = useQuery({ queryKey: ['api-keys', tenantId], queryFn: () => apiKeysApi.list(tenantId), enabled: !!tenantId, }) const createMutation = useMutation({ mutationFn: (data: { name: string; scopes: string[]; expires_at?: string }) => apiKeysApi.create(tenantId, data), onSuccess: (data) => { setNewKey(data) setShowCreateDialog(false) setShowKeyDialog(true) resetForm() queryClient.invalidateQueries({ queryKey: ['api-keys', tenantId] }) }, }) const revokeMutation = useMutation({ mutationFn: (keyId: string) => apiKeysApi.revoke(tenantId, keyId), onSuccess: () => { setShowRevokeDialog(false) setRevokeTarget(null) queryClient.invalidateQueries({ queryKey: ['api-keys', tenantId] }) }, }) function resetForm() { setName('') setSelectedScopes([]) setExpiresAt('') } function handleCreate() { const data: { name: string; scopes: string[]; expires_at?: string } = { name, scopes: selectedScopes, } if (expiresAt) { data.expires_at = new Date(expiresAt).toISOString() } createMutation.mutate(data) } function toggleScope(scope: string) { setSelectedScopes((prev) => prev.includes(scope) ? prev.filter((s) => s !== scope) : [...prev, scope], ) } async function copyKey() { if (!newKey) return await navigator.clipboard.writeText(newKey.key) setCopied(true) setTimeout(() => setCopied(false), 2000) } const keys = keysQuery.data ?? [] return (
{/* Header */}

API Keys

Create and manage API keys for programmatic access

{/* Key list */} {keys.length === 0 && !keysQuery.isLoading ? ( setShowCreateDialog(true) }} /> ) : (
{keys.map((key) => { const status = getKeyStatus(key) const isInactive = !!key.revoked_at return ( ) })}
Name Key Scopes Last Used Expires Status Actions
{key.name} {key.key_prefix}...
{key.scopes.map((scope) => ( {scope} ))}
{formatRelativeTime(key.last_used_at)} {formatDate(key.expires_at)} {status.label} {!key.revoked_at && ( )} {key.revoked_at && ( Revoked {formatDate(key.revoked_at)} )}
)} {/* Create API Key Dialog */} Create API Key Create a new API key for programmatic access. Choose which scopes the key should have access to.
{/* Name */}
setName(e.target.value)} />
{/* Scopes */}
{AVAILABLE_SCOPES.map((scope) => ( ))}
{/* Expiry */}
setExpiresAt(e.target.value)} min={new Date().toISOString().split('T')[0]} />
{/* One-Time Key Display Dialog */} { if (!open) { setShowKeyDialog(false) setNewKey(null) setCopied(false) } }} > API Key Created Copy your API key now. You will not be able to see it again.

This key will not be shown again. Store it securely.

                {newKey?.key}
              
{/* Revoke Confirmation Dialog */} Revoke API Key Are you sure you want to revoke{' '} {revokeTarget?.name}? This action cannot be undone and will immediately prevent any requests using this key.
) }