Améliroation de la piste d'audit
This commit is contained in:
@@ -12,39 +12,59 @@ const CATEGORY_META = {
|
||||
invitation: { label: 'Invitation', color: '#6366f1' },
|
||||
};
|
||||
|
||||
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é',
|
||||
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',
|
||||
// ── 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',
|
||||
};
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────────────
|
||||
const STATUS_META = {
|
||||
success: { label: 'Succès', color: '#16a34a' },
|
||||
warning: { label: 'Avertissement', color: '#f59e0b' },
|
||||
failure: { label: 'Échec', color: '#dc2626' },
|
||||
};
|
||||
|
||||
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 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 (
|
||||
<span style={{ display: 'inline-flex', marginRight: 6, verticalAlign: 'middle' }} title={meta.label}>
|
||||
{status === 'success' && (
|
||||
<svg {...common}><circle cx="12" cy="12" r="10"/><polyline points="16 9 11 14 8 11"/></svg>
|
||||
)}
|
||||
{status === 'warning' && (
|
||||
<svg {...common}><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
|
||||
)}
|
||||
{status === 'failure' && (
|
||||
<svg {...common}><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryBadge({ cat, active, onClick }) {
|
||||
const meta = CATEGORY_META[cat] || { label: cat, color: '#6b7280' };
|
||||
function StatusBadge({ status, count, active, onClick }) {
|
||||
const meta = STATUS_META[status];
|
||||
return (
|
||||
<button
|
||||
onClick={() => onClick(cat)}
|
||||
onClick={() => onClick(status)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6,
|
||||
padding: '3px 10px',
|
||||
borderRadius: 20,
|
||||
border: `1px solid ${meta.color}`,
|
||||
@@ -57,11 +77,56 @@ function CategoryBadge({ cat, active, onClick }) {
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<StatusIcon status={status} color={active ? '#fff' : meta.color} />
|
||||
{meta.label}
|
||||
<span style={{
|
||||
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
||||
minWidth: 18, height: 18, borderRadius: 9, padding: '0 5px',
|
||||
fontSize: 11, fontWeight: 700,
|
||||
background: active ? 'rgba(255,255,255,.25)' : `${meta.color}22`,
|
||||
color: active ? '#fff' : meta.color,
|
||||
}}>
|
||||
{count}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
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'
|
||||
@@ -119,9 +184,11 @@ export default function AuditLogsSection() {
|
||||
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('');
|
||||
@@ -133,6 +200,7 @@ export default function AuditLogsSection() {
|
||||
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;
|
||||
@@ -143,6 +211,7 @@ export default function AuditLogsSection() {
|
||||
]);
|
||||
setRows(data.rows);
|
||||
setTotal(data.total);
|
||||
setStatusCounts(data.statusCounts || { success: 0, warning: 0, failure: 0 });
|
||||
setPage(p);
|
||||
if (!categories.length) setCats(cats);
|
||||
} catch (e) {
|
||||
@@ -150,9 +219,9 @@ export default function AuditLogsSection() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [filterCat, filterSearch, filterFrom, filterTo]); // eslint-disable-line
|
||||
}, [filterCat, filterStatus, filterSearch, filterFrom, filterTo]); // eslint-disable-line
|
||||
|
||||
useEffect(() => { load(1); }, [filterCat, filterFrom, filterTo]); // eslint-disable-line
|
||||
useEffect(() => { load(1); }, [filterCat, filterStatus, filterFrom, filterTo]); // eslint-disable-line
|
||||
|
||||
// Recherche texte : debounce 350ms
|
||||
useEffect(() => {
|
||||
@@ -160,9 +229,10 @@ export default function AuditLogsSection() {
|
||||
return () => clearTimeout(t);
|
||||
}, [filterSearch]); // eslint-disable-line
|
||||
|
||||
const toggleCat = (cat) => setFilterCat(prev => prev === cat ? '' : cat);
|
||||
const toggleStatus = (st) => setFilterStatus(prev => prev === st ? '' : st);
|
||||
|
||||
const allCats = Object.keys(CATEGORY_META);
|
||||
const allStatuses = Object.keys(STATUS_META);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -173,10 +243,25 @@ export default function AuditLogsSection() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Filtres catégories */}
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12 }}>
|
||||
{allCats.map(cat => (
|
||||
<CategoryBadge key={cat} cat={cat} active={filterCat === cat} onClick={toggleCat} />
|
||||
{/* Filtres : catégorie (liste déroulante) + statut (chips avec compteurs) */}
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap', marginBottom: 16 }}>
|
||||
<select
|
||||
value={filterCat}
|
||||
onChange={e => setFilterCat(e.target.value)}
|
||||
style={{
|
||||
width: 'auto', minWidth: 180, flexShrink: 0,
|
||||
height: 32, padding: '0 8px', borderRadius: 8, border: '1px solid var(--border)',
|
||||
background: 'var(--surface)', color: 'var(--text)', fontSize: 13,
|
||||
}}
|
||||
>
|
||||
<option value="">Toutes les catégories</option>
|
||||
{allCats.map(cat => (
|
||||
<option key={cat} value={cat}>{CATEGORY_META[cat]?.label || cat}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{allStatuses.map(st => (
|
||||
<StatusBadge key={st} status={st} count={statusCounts[st] ?? 0} active={filterStatus === st} onClick={toggleStatus} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -249,9 +334,9 @@ export default function AuditLogsSection() {
|
||||
{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 = row.actor_name || row.actor_email || '—';
|
||||
const actor = fmtPerson(row.actor_name, row.actor_email);
|
||||
const target = row.target_email && row.target_id !== row.actor_id
|
||||
? (row.target_name || row.target_email)
|
||||
? fmtPerson(row.target_name, row.target_email)
|
||||
: '—';
|
||||
|
||||
return (
|
||||
@@ -275,6 +360,7 @@ export default function AuditLogsSection() {
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '7px 10px' }}>
|
||||
<StatusIcon status={ACTION_STATUS[row.action]} />
|
||||
<span style={{ color: 'var(--text)' }}>{label}</span>
|
||||
<DetailTooltip details={row.details} />
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user