// AGENTFORGE-IA — Log de actividad de usuarios const AuditPanel = () => { const getToken = () => sessionStorage.getItem('af_token') || ''; const [logs, setLogs] = React.useState([]); const [total, setTotal] = React.useState(0); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); // Filtros const [search, setSearch] = React.useState(''); const [category, setCategory] = React.useState(''); const [actorEmail, setActorEmail] = React.useState(''); const [dateFrom, setDateFrom] = React.useState(''); const [dateTo, setDateTo] = React.useState(''); const [successFilter, setSuccessFilter] = React.useState(''); const [activeTab, setActiveTab] = React.useState('all'); // 'all' | 'access' // Paginación const [page, setPage] = React.useState(0); const PAGE_SIZE = 50; // Edición de notas const [editingNoteId, setEditingNoteId] = React.useState(null); const [noteValue, setNoteValue] = React.useState(''); const [savingNote, setSavingNote] = React.useState(false); const CATEGORIES = [ { value: 'auth', label: 'Acceso', color: '#60a5fa' }, { value: 'user_mgmt', label: 'Gestión de usuarios', color: '#a78bfa' }, { value: 'mfa', label: 'MFA / 2FA', color: '#f59e0b' }, { value: 'roles', label: 'Roles y permisos', color: '#34d399' }, { value: 'subscription', label: 'Suscripciones', color: '#fb923c' }, { value: 'integration', label: 'Integraciones', color: '#f472b6' }, { value: 'billing', label: 'Facturación', color: '#fbbf24' }, { value: 'configuracion', label: 'Configuración', color: '#94a3b8' }, { value: 'acciones', label: 'Acciones', color: '#ff5b1f' }, ]; const catMap = Object.fromEntries(CATEGORIES.map(c => [c.value, c])); const fetchLogs = React.useCallback(async (currentPage = 0) => { setLoading(true); setError(''); try { const params = new URLSearchParams(); const effectiveCategory = activeTab === 'access' ? 'auth' : category; if (search.trim()) params.set('search', search.trim()); if (effectiveCategory) params.set('category', effectiveCategory); if (actorEmail.trim()) params.set('actor_email', actorEmail.trim()); if (dateFrom) params.set('date_from', dateFrom + 'T00:00:00'); if (dateTo) params.set('date_to', dateTo + 'T23:59:59'); if (successFilter === 'ok') params.set('success', 'true'); if (successFilter === 'error') params.set('success', 'false'); params.set('limit', String(PAGE_SIZE)); params.set('offset', String(currentPage * PAGE_SIZE)); const res = await fetch('/api/v1/audit/logs?' + params.toString(), { headers: { Authorization: 'Bearer ' + getToken() }, }); if (!res.ok) { setError('Error cargando actividad'); return; } const data = await res.json(); setLogs(data.items || []); setTotal(data.total || 0); setPage(currentPage); } catch (_) { setError('Error de red'); } finally { setLoading(false); } }, [search, category, actorEmail, dateFrom, dateTo, successFilter, activeTab]); React.useEffect(() => { fetchLogs(0); }, [fetchLogs]); const handleExport = async () => { const params = new URLSearchParams(); const effectiveCategory = activeTab === 'access' ? 'auth' : category; if (search.trim()) params.set('search', search.trim()); if (effectiveCategory) params.set('category', effectiveCategory); if (actorEmail.trim()) params.set('actor_email', actorEmail.trim()); if (dateFrom) params.set('date_from', dateFrom + 'T00:00:00'); if (dateTo) params.set('date_to', dateTo + 'T23:59:59'); if (successFilter === 'ok') params.set('success', 'true'); if (successFilter === 'error') params.set('success', 'false'); const res = await fetch('/api/v1/audit/logs/export?' + params.toString(), { headers: { Authorization: 'Bearer ' + getToken() }, }); if (!res.ok) { window.afNotify && window.afNotify('Error al exportar', 'error'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const cd = res.headers.get('Content-Disposition') || ''; const m = cd.match(/filename="(.+?)"/); a.download = m ? m[1] : 'actividad_usuarios.csv'; a.click(); URL.revokeObjectURL(url); }; const startEditNote = (log) => { setEditingNoteId(log.id); setNoteValue(log.notes || ''); }; const saveNote = async (logId) => { setSavingNote(true); try { const res = await fetch(`/api/v1/audit/logs/${logId}/notes`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() }, body: JSON.stringify({ notes: noteValue.trim() || null }), }); if (!res.ok) { window.afNotify && window.afNotify('Error guardando nota', 'error'); return; } const updated = await res.json(); setLogs(prev => prev.map(l => l.id === logId ? { ...l, ...updated } : l)); setEditingNoteId(null); window.afNotify && window.afNotify('Nota guardada'); } catch (_) { window.afNotify && window.afNotify('Error de red', 'error'); } finally { setSavingNote(false); } }; const formatDT = (iso) => { if (!iso) return '—'; const d = new Date(iso); return d.toLocaleDateString('es-ES') + ' ' + d.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit', second: '2-digit' }); }; const totalPages = Math.ceil(total / PAGE_SIZE); // ── Estilos ────────────────────────────────────────────────────────────────── const wrap = { padding: '24px 28px', color: 'var(--text)', fontFamily: '"Inter Tight","Inter",sans-serif', minHeight: '100vh' }; const hdr = { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', marginBottom: 20 }; const inp = { background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', fontSize: 12.5, color: 'var(--text)', fontFamily: 'inherit', outline: 'none' }; const sel = { ...inp, cursor: 'pointer' }; const btn = { padding: '6px 14px', borderRadius: 6, border: '1px solid var(--border)', cursor: 'pointer', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', background: 'var(--bg-2)', color: 'var(--text-2)' }; const btnP = { ...btn, background: 'var(--brand)', color: '#fff', border: 'none' }; const tabBtn = (a) => ({ padding: '6px 14px', border: 'none', borderRadius: 6, cursor: 'pointer', fontSize: 12.5, fontWeight: 600, fontFamily: 'inherit', background: a ? 'var(--brand)' : 'transparent', color: a ? '#fff' : 'var(--text-2)', transition: 'all 0.15s' }); const thS = { padding: '9px 12px', textAlign: 'left', color: 'var(--text-2)', fontWeight: 600, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.06em', borderBottom: '1px solid var(--border)', whiteSpace: 'nowrap' }; const tdS = (fail) => ({ padding: '10px 12px', borderBottom: '1px solid var(--border)', verticalAlign: 'top', background: fail ? 'rgba(248,113,113,0.04)' : 'transparent' }); const badge = (color, bg) => ({ display: 'inline-flex', alignItems: 'center', padding: '2px 7px', borderRadius: 4, fontSize: 11, fontWeight: 600, color, background: bg }); const mono = { fontFamily: '"JetBrains Mono",ui-monospace,monospace', fontSize: 11.5 }; const noteInp = { ...inp, border: '1px solid var(--brand)', width: 180, resize: 'none', fontSize: 12 }; const catBadge = (cat) => { const c = catMap[cat] || { label: cat, color: '#888' }; return {c.label}; }; const okBadge = (ok) => ok ? ✓ OK : ✗ Error; const accessTypeBadge = (action) => { if (action === 'auth.login') return → Inicio de sesión; if (action === 'auth.logout') return ← Cierre de sesión; if (action === 'auth.login_failed') return ⊘ Acceso fallido; return null; }; const colCount = activeTab === 'access' ? 6 : 7; return (
{/* Header */}
Actividad de usuarios
Registro exhaustivo de accesos y acciones de todos los usuarios · Retención: 60 días · {total.toLocaleString('es-ES')} entradas
{/* Tabs */}
{/* Filtros */}
setSearch(e.target.value)} onKeyDown={e => e.key === 'Enter' && fetchLogs(0)} /> {activeTab === 'all' && ( )} setActorEmail(e.target.value)} /> setDateFrom(e.target.value)} title="Desde" /> setDateTo(e.target.value)} title="Hasta" />
{error &&
{error}
} {/* Tabla */}
{activeTab === 'all' && } {activeTab === 'access' && } {activeTab === 'all' && } {loading && ( )} {!loading && logs.length === 0 && ( )} {!loading && logs.map(log => ( { e.currentTarget.style.background = 'rgba(255,255,255,0.025)'; }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }} > {/* Fecha/hora */} {/* Usuario */} {/* Tipo de acción (solo en "todas") */} {activeTab === 'all' && ( )} {/* Qué hizo */} {/* Sobre qué recurso */} {/* Estado */} {/* Notas editables */}
Fecha y hora UsuarioTipoQué hizo Sobre quéResultadoEstadoNotas (editables)
Cargando actividad…
Sin registros para los filtros seleccionados
{formatDT(log.created_at)}
{log.actor_full_name || log.actor_email}
{log.actor_email}
{log.actor_is_admin && Admin}
{catBadge(log.category)} {activeTab === 'access' ? accessTypeBadge(log.action) : ( <>
{log.action_label}
{log.action}
{log.detail && Object.keys(log.detail).length > 0 && (
Ver detalle
                              {JSON.stringify(log.detail, null, 2)}
                            
)} ) }
{log.resource_type && {log.resource_type}} {log.resource_label &&
{log.resource_label}
}
{okBadge(log.success)} {editingNoteId === log.id ? (