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 => (
|
{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 (
|
{fmtDateTime(row.created_at)}
|
{meta.label}
|
{label}
|
{actor}
|
{target}
|
);
})}
)}
load(p)} />
);
}