import React, { useState, useEffect, useCallback, useRef } from 'react'; import { getAuditLogs, getAuditStats, exportAuditLogs } from '../../services/auditLogs'; import './AuditLogs.css'; // ── helpers ──────────────────────────────────────────────────────────────────── const ACTION_LABELS = { CREATE: 'Erstellen', UPDATE: 'Bearbeiten', DELETE: 'Löschen', RESTORE: 'Wiederherstellen', LOGIN: 'Login', LOGOUT: 'Logout', LOGIN_FAILED: 'Login fehlgeschlagen', IMPORT: 'Import', EXPORT: 'Export', BULK_UPDATE: 'Massen-Update', BULK_DELETE: 'Massen-Löschung', PASSWORD_RESET: 'Passwort-Reset', PASSWORD_CHANGE: 'Passwort-Änderung' }; const ACTION_ICONS = { CREATE: '➕', UPDATE: '✏️', DELETE: '🗑️', RESTORE: '↩️', LOGIN: '🔓', LOGOUT: '🔒', LOGIN_FAILED: '🚫', IMPORT: '📥', EXPORT: '📤', BULK_UPDATE: '✏️', BULK_DELETE: '🗑️', PASSWORD_RESET: '🔑', PASSWORD_CHANGE: '🔑' }; const ACTION_BADGE = { CREATE: 'badge-create', UPDATE: 'badge-update', DELETE: 'badge-delete', RESTORE: 'badge-restore', LOGIN: 'badge-login', LOGOUT: 'badge-logout', LOGIN_FAILED: 'badge-login-failed', IMPORT: 'badge-import', EXPORT: 'badge-export', BULK_UPDATE: 'badge-bulk-update', BULK_DELETE: 'badge-bulk-delete', PASSWORD_RESET: 'badge-password', PASSWORD_CHANGE: 'badge-password' }; const formatDate = (ds) => new Date(ds).toLocaleString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const parseUA = (ua) => { if (!ua) return '—'; if (/iPhone|iPad/.test(ua)) return '📱 iOS'; if (/Android/.test(ua)) return '📱 Android'; if (/Firefox\/(\d+)/.test(ua)) return `🦊 Firefox ${ua.match(/Firefox\/(\d+)/)[1]}`; if (/Edg\/(\d+)/.test(ua)) return `🔷 Edge ${ua.match(/Edg\/(\d+)/)[1]}`; if (/Chrome\/(\d+)/.test(ua)) return `🌐 Chrome ${ua.match(/Chrome\/(\d+)/)[1]}`; if (/Safari\/(\d+)/.test(ua)) return `🧭 Safari`; return ua.slice(0, 60); }; const statusBadgeClass = (code) => { if (!code) return ''; if (code < 300) return 'status-ok'; if (code < 400) return 'status-redirect'; if (code < 500) return 'status-error'; return 'status-server-error'; }; // ── component ────────────────────────────────────────────────────────────────── function AuditLogs() { const [logs, setLogs] = useState([]); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [statsLoading, setStatsLoading] = useState(true); const [error, setError] = useState(null); const [expanded, setExpanded] = useState({}); const [showStats, setShowStats] = useState(true); const [autoRefresh, setAutoRefresh] = useState(false); const [exporting, setExporting] = useState(false); const [pagination, setPagination] = useState({}); const [statsDays, setStatsDays] = useState(30); const [filters, setFilters] = useState({ action: '', resource: '', adminUsername: '', success: '', startDate: '', endDate: '', page: 1, limit: 25 }); const autoRefreshRef = useRef(null); const loadLogs = useCallback(async () => { setLoading(true); setError(null); const params = {}; Object.entries(filters).forEach(([k, v]) => { if (v !== '') params[k] = v; }); const result = await getAuditLogs(params); if (result.success) { setLogs(result.data); setPagination(result.pagination); } else { setError(result.message); } setLoading(false); }, [filters]); const loadStats = useCallback(async () => { setStatsLoading(true); const result = await getAuditStats(statsDays); if (result.success) setStats(result.data); setStatsLoading(false); }, [statsDays]); useEffect(() => { loadLogs(); }, [loadLogs]); useEffect(() => { loadStats(); }, [loadStats]); // Auto-refresh useEffect(() => { if (autoRefresh) { autoRefreshRef.current = setInterval(() => loadLogs(), 30000); } else { clearInterval(autoRefreshRef.current); } return () => clearInterval(autoRefreshRef.current); }, [autoRefresh, loadLogs]); const setFilter = (key, value) => setFilters(f => ({ ...f, [key]: value, page: 1 })); const toggleExpand = (id) => setExpanded(e => ({ ...e, [id]: !e[id] })); const handleExport = async () => { setExporting(true); const params = {}; Object.entries(filters).forEach(([k, v]) => { if (v !== '' && k !== 'page' && k !== 'limit') params[k] = v; }); await exportAuditLogs(params); setExporting(false); }; const resetFilters = () => setFilters({ action: '', resource: '', adminUsername: '', success: '', startDate: '', endDate: '', page: 1, limit: 25 }); // ── stats cards ────────────────────────────────────────────────────────────── const renderStats = () => { if (statsLoading) return
Lade Statistiken…
; if (!stats) return null; const maxBar = Math.max(...(stats.dailyActivity?.map(d => d.count) || [1]), 1); return (
{stats.total}
Aktionen gesamt
{stats.failed}
Fehlgeschlagen
{stats.total - stats.failed}
Erfolgreich
{stats.days} Tage
Zeitraum

Nach Aktion

{stats.byAction.map(a => (
{ACTION_ICONS[a.action]} {ACTION_LABELS[a.action] || a.action} {a.count}
))}

Nach Admin

{stats.byAdmin.map(a => (
👤 {a.admin} {a.count}
))}

Aktivität (letzte {stats.days} Tage)

{stats.dailyActivity.map(d => (
{d.failed > 0 && (
)}
))}
); }; // ── diff renderer ──────────────────────────────────────────────────────────── const renderDiff = (changes) => { if (!changes || Object.keys(changes).length === 0) return null; return ( {Object.entries(changes).map(([field, { before, after }]) => ( ))}
FeldVorherNachher
{field} {JSON.stringify(before) ?? '—'} {JSON.stringify(after) ?? '—'}
); }; // ── log item ───────────────────────────────────────────────────────────────── const renderLogItem = (log) => { const isExpanded = expanded[log._id]; const hasDiff = log.changes && Object.keys(log.changes).length > 0; const hasMeta = log.metadata && Object.keys(log.metadata).length > 0; const hasDetail = hasDiff || hasMeta || log.errorMessage || log.userAgent || log.requestPath; return (
hasDetail && toggleExpand(log._id)} style={{ cursor: hasDetail ? 'pointer' : 'default' }}> {ACTION_ICONS[log.action]} {ACTION_LABELS[log.action] || log.action} {log.resource} {log.statusCode && ( {log.statusCode} )} {log.duration != null && ( {log.duration} ms )} {formatDate(log.createdAt)} {hasDetail && ( {isExpanded ? '▲' : '▼'} )}
👤 {log.adminUsername || '—'} {log.resourceName && 📌 {log.resourceName}} {log.ipAddress && 🌐 {log.ipAddress}} {!log.success && ✗ Fehlgeschlagen}
{isExpanded && (
{log.requestMethod && log.requestPath && (
Anfrage {log.requestMethod} {log.requestPath}
)} {log.userAgent && (
Browser {parseUA(log.userAgent)}
)} {log.errorMessage && (
Fehler {log.errorMessage}
)} {hasMeta && (
Details {Object.entries(log.metadata).map(([k, v]) => ( {k}: {String(v)} ))}
)} {hasDiff && (
Änderungen
{renderDiff(log.changes)}
)}
)}
); }; // ── main render ─────────────────────────────────────────────────────────────── return (

📋 Audit-Logs

Vollständiges Protokoll aller Admin-Aktionen

{showStats && ( )}
{showStats && renderStats()}
setFilter('adminUsername', e.target.value)} className="filter-input" /> setFilter('startDate', e.target.value)} className="filter-input filter-date" title="Von Datum" /> setFilter('endDate', e.target.value)} className="filter-input filter-date" title="Bis Datum" />
{error &&

{error}

}
{pagination.total != null && ( {pagination.total} Einträge gefunden )}
{loading && logs.length === 0 ?
Lade Logs…
: logs.length === 0 ?
Keine Logs gefunden
: logs.map(renderLogItem) }
{pagination.pages > 1 && (
Seite {filters.page} / {pagination.pages}
)}
); } export default AuditLogs;