"use client"; import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { Button } from '@nextsparkjs/core/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@nextsparkjs/core/components/ui/card'; import { Badge } from '@nextsparkjs/core/components/ui/badge'; import { Alert, AlertDescription } from '@nextsparkjs/core/components/ui/alert'; import { Skeleton } from '@nextsparkjs/core/components/ui/skeleton'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@nextsparkjs/core/components/ui/dialog'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@nextsparkjs/core/components/ui/dropdown-menu'; import { AlertTriangle, Copy, Eye, EyeOff, Key, MoreVertical, Plus, Trash2, Activity } from 'lucide-react'; import { CreateApiKeyDialog } from '@nextsparkjs/core/components/api/keys/CreateApiKeyDialog'; import { ApiKeyDisplay } from '@nextsparkjs/core/components/api/keys/ApiKeyDisplay'; import { toast } from 'sonner'; import { getTemplateOrDefaultClient } from '@nextsparkjs/registries/template-registry.client' import { sel } from '@nextsparkjs/core/selectors' interface ApiKey { id: string; keyPrefix: string; name: string; scopes: string[]; status: 'active' | 'inactive' | 'expired'; lastUsedAt: string | null; expiresAt: string | null; createdAt: string; usage_stats: { total_requests: number; last_24h: number; avg_response_time: number | null; }; } interface NewApiKeyResponse { id: string; name: string; key: string; scopes: string[]; warning: string; } function ApiKeysPage() { const [showCreateDialog, setShowCreateDialog] = useState(false); const [newApiKey, setNewApiKey] = useState(null); const [selectedKey, setSelectedKey] = useState(null); const queryClient = useQueryClient(); // Fetch API keys const { data: apiKeys, isLoading, error } = useQuery({ queryKey: ['api-keys'], queryFn: async () => { const response = await fetch('/api/v1/api-keys'); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch API keys'); } const result = await response.json(); return result.data; } }); // Revoke API key mutation const revokeApiKey = useMutation({ mutationFn: async (keyId: string) => { const response = await fetch(`/api/v1/api-keys/${keyId}`, { method: 'DELETE' }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to revoke API key'); } return response.json(); }, onSuccess: (data) => { toast.success(`API key "${data.data.name}" has been revoked`); queryClient.invalidateQueries({ queryKey: ['api-keys'] }); }, onError: (error) => { toast.error(`Failed to revoke API key: ${error.message}`); } }); // Toggle API key status const toggleApiKey = useMutation({ mutationFn: async ({ keyId, status }: { keyId: string; status: 'active' | 'inactive' }) => { const response = await fetch(`/api/v1/api-keys/${keyId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to update API key'); } return response.json(); }, onSuccess: (data) => { const statusText = data.data.status === 'active' ? 'activated' : 'deactivated'; toast.success(`API key "${data.data.name}" has been ${statusText}`); queryClient.invalidateQueries({ queryKey: ['api-keys'] }); }, onError: (error) => { toast.error(`Failed to update API key: ${error.message}`); } }); const handleCreateSuccess = (apiKeyData: NewApiKeyResponse) => { setNewApiKey(apiKeyData); setShowCreateDialog(false); queryClient.invalidateQueries({ queryKey: ['api-keys'] }); }; const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text); toast.success('Copied to clipboard'); }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('es-ES', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const getScopeColor = (scope: string) => { if (scope === '*') return 'destructive'; if (scope.includes('admin')) return 'secondary'; if (scope.includes('write') || scope.includes('delete')) return 'default'; return 'outline'; }; if (error) { const isPermissionError = error.message.includes('Insufficient permissions'); return (

API Keys

Gestiona las API keys para integración externa

{isPermissionError ? (
Acceso restringido

Solo los administradores pueden gestionar API keys. Contacta a un administrador si necesitas acceso a esta funcionalidad.

) : ( `Error loading API keys: ${error.message}` )}
); } return (

API Keys

Gestiona las API keys para integración externa

{/* New API Key Display */} {newApiKey && ( setNewApiKey(null)} /> )} {/* API Keys List */}
{isLoading ? ( // Loading skeletons Array.from({ length: 3 }).map((_, i) => (
)) ) : apiKeys?.length === 0 ? (

No API Keys

No tienes API keys creadas. Crea una para empezar a usar la API externa.

) : ( apiKeys?.map((apiKey) => (
{apiKey.name} {apiKey.status !== 'active' && ( {apiKey.status === 'inactive' ? 'Inactiva' : 'Expirada'} )} {apiKey.keyPrefix}••••••••••••••••••••••••••••••••••••••••••••••••••••
{apiKey.status === 'active' ? 'Activa' : apiKey.status === 'inactive' ? 'Inactiva' : 'Expirada'} setSelectedKey(apiKey.id)} data-cy={sel('settings.apiKeys.row.menu.viewDetails', { id: apiKey.id })} > Ver detalles toggleApiKey.mutate({ keyId: apiKey.id, status: apiKey.status === 'active' ? 'inactive' : 'active' })} disabled={toggleApiKey.isPending} data-cy={sel('settings.apiKeys.row.menu.toggle', { id: apiKey.id })} > {apiKey.status === 'active' ? ( <> Desactivar ) : ( <> Activar )} revokeApiKey.mutate(apiKey.id)} disabled={revokeApiKey.isPending} className="text-destructive" data-cy={sel('settings.apiKeys.row.menu.revoke', { id: apiKey.id })} > Revocar
{/* Scopes */}
Permisos:
{apiKey.scopes.map((scope) => ( {scope} ))}
{/* Usage Stats */}
Total requests
{apiKey.usage_stats.total_requests.toLocaleString()}
Últimas 24h
{apiKey.usage_stats.last_24h.toLocaleString()}
Tiempo promedio
{apiKey.usage_stats.avg_response_time ? `${Math.round(apiKey.usage_stats.avg_response_time)}ms` : 'N/A' }
{/* Metadata */}
Creada: {formatDate(apiKey.createdAt)}
{apiKey.lastUsedAt && (
Ultimo uso: {formatDate(apiKey.lastUsedAt)}
)} {apiKey.expiresAt && (
Expira: {formatDate(apiKey.expiresAt)}
)}
)) )}
{/* Create API Key Dialog */} setShowCreateDialog(false)} onSuccess={handleCreateSuccess} /> {/* API Key Details Dialog */} {selectedKey && ( setSelectedKey(null)} /> )}
); } // Componente para mostrar detalles de API key function ApiKeyDetailsDialog({ keyId, open, onClose }: { keyId: string; open: boolean; onClose: () => void; }) { const { data: keyDetails, isLoading } = useQuery({ queryKey: ['api-key-details', keyId], queryFn: async () => { const response = await fetch(`/api/v1/api-keys/${keyId}`); if (!response.ok) throw new Error('Failed to fetch API key details'); const result = await response.json(); return result.data; }, enabled: open }); return ( Detalles de API Key Estadísticas de uso y configuración {isLoading ? (
) : keyDetails ? (
{/* Basic Info */}

Información básica

Nombre:
{keyDetails.name}
Estado:
{keyDetails.status === 'active' ? 'Activa' : keyDetails.status === 'inactive' ? 'Inactiva' : 'Expirada'}
{/* Usage Statistics */}

Estadísticas de uso

Total de requests:
{keyDetails.usage_stats.total_requests.toLocaleString()}
Ultimas 24 horas:
{keyDetails.usage_stats.last_24h.toLocaleString()}
Ultimos 7 dias:
{keyDetails.usage_stats.last_7d.toLocaleString()}
Ultimos 30 dias:
{keyDetails.usage_stats.last_30d.toLocaleString()}
Tiempo promedio:
{keyDetails.usage_stats.avg_response_time ? `${Math.round(keyDetails.usage_stats.avg_response_time)}ms` : 'N/A' }
Tasa de exito:
{keyDetails.usage_stats.success_rate ? `${Math.round(keyDetails.usage_stats.success_rate)}%` : 'N/A' }
) : null}
); } export default getTemplateOrDefaultClient('app/dashboard/settings/api-keys/page.tsx', ApiKeysPage)