import { useState, useEffect, useCallback } from 'react'; import { api } from '../../api.js'; // ── Libellés et couleurs par catégorie ───────────────────────────────────── const CATEGORY_META = { auth: { label: 'Connexion', color: '#3b82f6' }, account: { label: 'Compte', color: '#8b5cf6' }, role: { label: 'Rôle', color: '#f59e0b' }, status: { label: 'Statut', color: '#ef4444' }, '2fa': { label: '2FA', color: '#10b981' }, invitation: { label: 'Invitation', color: '#6366f1' }, }; // ── Statut succès / avertissement / échec par action ─────────────────────── // IMPORTANT : garder synchronisé avec STATUS_ACTIONS dans backend/src/routes/auditLogs.js const ACTION_STATUS = { login_success: 'success', login_2fa_success: 'success', user_registered: 'success', user_created: 'success', email_verified_admin: 'success', invitation_accepted: 'success', invitation_sent: 'success', '2fa_enabled': 'success', role_changed: 'warning', status_changed: 'warning', '2fa_disabled': 'warning', user_deleted: 'warning', account_self_deleted: 'warning', login_failed: 'failure', }; const STATUS_META = { success: { label: 'Succès', color: '#16a34a' }, warning: { label: 'Avertissement', color: '#f59e0b' }, failure: { label: 'Échec', color: '#dc2626' }, }; function StatusIcon({ status, color }) { const meta = STATUS_META[status]; if (!meta) return null; const common = { width: 14, height: 14, viewBox: '0 0 24 24', fill: 'none', stroke: color || meta.color, strokeWidth: 2, strokeLinecap: 'round', strokeLinejoin: 'round' }; return ( {status === 'success' && ( )} {status === 'warning' && ( )} {status === 'failure' && ( )} ); } function StatusBadge({ status, count, active, onClick }) { const meta = STATUS_META[status]; return ( ); } const ACTION_LABELS = { login_success: 'Connexion réussie', login_failed: 'Échec de connexion', login_2fa_success: 'Connexion 2FA réussie', user_registered: 'Auto-inscription', user_created: 'Compte créé (admin)', user_deleted: 'Compte supprimé', account_self_deleted:'Compte supprimé (par l’utilisateur)', email_verified_admin:'Email vérifié (admin)', role_changed: 'Rôle modifié', status_changed: 'Statut modifié', '2fa_enabled': '2FA activé', '2fa_disabled': '2FA désactivé', invitation_sent: 'Invitation envoyée', invitation_accepted: 'Invitation acceptée', }; // ── Helpers ───────────────────────────────────────────────────────────────── function fmtPerson(name, email) { if (name && email) return `${name} (${email})`; if (email) return email; if (name) return name; return '—'; } function fmtDateTime(str) { if (!str) return '—'; const d = new Date(str.replace(' ', 'T') + (str.includes('+') ? '' : 'Z')); return d.toLocaleString('fr-FR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function DetailTooltip({ details }) { if (!details) return null; const entries = typeof details === 'object' ? Object.entries(details).filter(([, v]) => v !== null && v !== undefined) : []; if (entries.length === 0) return null; return ( `${k}: ${v}`).join('\n')} >i ); } // ── Pagination ─────────────────────────────────────────────────────────────── function Pagination({ page, total, limit, onPage }) { const pages = Math.max(1, Math.ceil(total / limit)); if (pages <= 1) return null; const getPages = () => { const arr = []; for (let i = Math.max(1, page - 2); i <= Math.min(pages, page + 2); i++) arr.push(i); return arr; }; return (
{page > 3 && <>} {getPages().map(p => ( ))} {page < pages - 2 && <>}
); } // ── Composant principal ────────────────────────────────────────────────────── export default function AuditLogsSection() { const [rows, setRows] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [categories, setCats] = useState([]); const [statusCounts, setStatusCounts] = useState({ success: 0, warning: 0, failure: 0 }); // Filtres const [filterCat, setFilterCat] = useState(''); const [filterStatus, setFilterStatus] = useState(''); const [filterSearch, setFilterSearch] = useState(''); const [filterFrom, setFilterFrom] = useState(''); const [filterTo, setFilterTo] = useState(''); const LIMIT = 50; const load = useCallback(async (p = 1) => { setLoading(true); try { const params = { page: p, limit: LIMIT }; if (filterCat) params.category = filterCat; if (filterStatus) params.status = filterStatus; if (filterSearch) params.search = filterSearch; if (filterFrom) params.dateFrom = filterFrom; if (filterTo) params.dateTo = filterTo; const [data, cats] = await Promise.all([ api.get('/admin/audit-logs', params), categories.length ? Promise.resolve(categories) : api.get('/admin/audit-logs/categories'), ]); setRows(data.rows); setTotal(data.total); setStatusCounts(data.statusCounts || { success: 0, warning: 0, failure: 0 }); setPage(p); if (!categories.length) setCats(cats); } catch (e) { console.error(e); } finally { setLoading(false); } }, [filterCat, filterStatus, filterSearch, filterFrom, filterTo]); // eslint-disable-line useEffect(() => { load(1); }, [filterCat, filterStatus, filterFrom, filterTo]); // eslint-disable-line // Recherche texte : debounce 350ms useEffect(() => { const t = setTimeout(() => load(1), 350); return () => clearTimeout(t); }, [filterSearch]); // eslint-disable-line const toggleStatus = (st) => setFilterStatus(prev => prev === st ? '' : st); const allCats = Object.keys(CATEGORY_META); const allStatuses = Object.keys(STATUS_META); return (

Audit — Activité des comptes

Historique sur 30 jours · {total} événement{total !== 1 ? 's' : ''}
{/* Filtres : catégorie (liste déroulante) + statut (chips avec compteurs) */}
{allStatuses.map(st => ( ))}
{/* Barre de recherche + dates */}
setFilterSearch(e.target.value)} style={{ background: 'transparent', border: 'none', outline: 'none', flex: 1, fontSize: 13, color: 'var(--text)' }} /> {filterSearch && ( )}
setFilterFrom(e.target.value)} style={{ padding: '0 8px', height: 32, borderRadius: 8, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, }} /> setFilterTo(e.target.value)} style={{ padding: '0 8px', height: 32, borderRadius: 8, border: '1px solid var(--border)', background: 'var(--surface)', color: 'var(--text)', fontSize: 13, }} /> {(filterFrom || filterTo) && ( )}
{/* Tableau */} {loading ? (

Chargement…

) : rows.length === 0 ? (
Aucun événement trouvé pour ces critères.
) : (
{['Date / Heure', 'Catégorie', 'Événement', 'Acteur', 'Concerné'].map(h => ( ))} {rows.map((row, i) => { const meta = CATEGORY_META[row.category] || { label: row.category, color: '#6b7280' }; const label = ACTION_LABELS[row.action] || row.action; const actor = fmtPerson(row.actor_name, row.actor_email); const target = row.target_email && row.target_id !== row.actor_id ? fmtPerson(row.target_name, row.target_email) : '—'; return ( ); })}
{h}
{fmtDateTime(row.created_at)} {meta.label} {label} {actor} {target}
)} load(p)} />
); }