import { useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { useAuth } from '../../context/AuthContext'; import { auditLogApi, AuditLogSearchParams, authApi } from '../../services/api'; import type { AuditLog, AuditAction, AuditSensitivity } from '../../types'; import Card from '../../components/ui/Card'; import Button from '../../components/ui/Button'; import Input from '../../components/ui/Input'; import Select from '../../components/ui/Select'; import AuditIntegrityCard from './AuditIntegrityCard'; import { ArrowLeft, Download, Eye, Shield, ShieldAlert, RefreshCw, ChevronLeft, ChevronRight, X } from 'lucide-react'; const ACTION_OPTIONS = [ { value: '', label: 'Alle Aktionen' }, { value: 'CREATE', label: 'Erstellt' }, { value: 'READ', label: 'Gelesen' }, { value: 'UPDATE', label: 'Aktualisiert' }, { value: 'DELETE', label: 'Gelöscht' }, { value: 'EXPORT', label: 'Exportiert' }, { value: 'ANONYMIZE', label: 'Anonymisiert' }, { value: 'LOGIN', label: 'Login' }, { value: 'LOGOUT', label: 'Logout' }, { value: 'LOGIN_FAILED', label: 'Login fehlgeschlagen' }, { value: 'TOKEN_REFRESH', label: 'Sitzung verlängert' }, ]; const SENSITIVITY_OPTIONS = [ { value: '', label: 'Alle Stufen' }, { value: 'LOW', label: 'Niedrig' }, { value: 'MEDIUM', label: 'Mittel' }, { value: 'HIGH', label: 'Hoch' }, { value: 'CRITICAL', label: 'Kritisch' }, ]; const RESOURCE_OPTIONS = [ { value: '', label: 'Alle Ressourcen' }, { value: 'Customer', label: 'Kunden' }, { value: 'Contract', label: 'Verträge' }, { value: 'User', label: 'Benutzer' }, { value: 'BankCard', label: 'Bankdaten' }, { value: 'IdentityDocument', label: 'Ausweise' }, { value: 'Address', label: 'Adressen' }, { value: 'Meter', label: 'Zähler' }, { value: 'ContractTask', label: 'Aufgaben' }, { value: 'Authentication', label: 'Anmeldung' }, { value: 'CustomerConsent', label: 'Einwilligungen' }, { value: 'GDPR', label: 'DSGVO / Vollmacht' }, { value: 'EmailProviderConfig', label: 'E-Mail-Provider' }, { value: 'AppSetting', label: 'Einstellungen' }, ]; function formatDate(date: string): string { return new Date(date).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function getActionColor(action: AuditAction): string { switch (action) { case 'CREATE': return 'bg-green-100 text-green-800'; case 'READ': return 'bg-blue-100 text-blue-800'; case 'UPDATE': return 'bg-yellow-100 text-yellow-800'; case 'DELETE': return 'bg-red-100 text-red-800'; case 'EXPORT': return 'bg-purple-100 text-purple-800'; case 'ANONYMIZE': return 'bg-orange-100 text-orange-800'; case 'LOGIN': return 'bg-teal-100 text-teal-800'; case 'LOGOUT': return 'bg-gray-100 text-gray-800'; case 'LOGIN_FAILED': return 'bg-red-200 text-red-900'; case 'TOKEN_REFRESH': return 'bg-slate-50 text-slate-500'; default: return 'bg-gray-100 text-gray-800'; } } function getSensitivityIcon(sensitivity: AuditSensitivity) { switch (sensitivity) { case 'LOW': return ; case 'MEDIUM': return ; case 'HIGH': return ; case 'CRITICAL': return ; default: return ; } } const RESOURCE_TYPE_LABELS: Record = { Customer: 'Kunde', Contract: 'Vertrag', BankCard: 'Bankverbindung', IdentityDocument: 'Ausweis', Address: 'Adresse', Meter: 'Zähler', MeterReading: 'Zählerstand', User: 'Benutzer', CustomerConsent: 'Einwilligung', RepresentativeAuthorization: 'Vollmacht', ContractTask: 'Aufgabe', EnergyContractDetails: 'Strom/Gas-Details', EmailProviderConfig: 'E-Mail-Provider', AppSetting: 'Einstellung', GDPR: 'Datenschutz', Authentication: 'Anmeldung', }; const FIELD_LABELS: Record = { firstName: 'Vorname', lastName: 'Nachname', email: 'E-Mail', phone: 'Telefon', mobile: 'Mobil', salutation: 'Anrede', companyName: 'Firma', type: 'Typ', notes: 'Notizen', street: 'Straße', houseNumber: 'Hausnummer', postalCode: 'PLZ', city: 'Stadt', country: 'Land', meterNumber: 'Zählernummer', location: 'Standort', tariffModel: 'Tarifmodell', isActive: 'Aktiv', isDefault: 'Standard', status: 'Status', source: 'Quelle', contractNumber: 'Vertragsnummer', startDate: 'Vertragsbeginn', endDate: 'Vertragsende', portalUsername: 'Portal-Benutzername', portalEnabled: 'Portal aktiv', basePrice: 'Grundpreis', unitPrice: 'Arbeitspreis', unitPriceNt: 'NT-Arbeitspreis', bonus: 'Bonus', name: 'Name', domain: 'Domain', apiUrl: 'API-URL', documentNumber: 'Dokumentennummer', expiryDate: 'Ablaufdatum', iban: 'IBAN', bic: 'BIC', bankName: 'Bank', accountHolder: 'Kontoinhaber', isGranted: 'Vollmacht erteilt', documentPath: 'Dokument-Pfad', value: 'Zählerstand', valueNt: 'NT-Zählerstand', readingDate: 'Ablesedatum', privacyPolicyPath: 'Datenschutz-PDF', }; function formatValue(value: unknown): string { if (value === null || value === undefined) return '-'; if (typeof value === 'boolean') return value ? 'Ja' : 'Nein'; if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value)) { try { return new Date(value).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }); } catch { return value; } } return String(value); } function renderChangeDiff(before: Record, after: Record) { const allKeys = new Set([...Object.keys(before), ...Object.keys(after)]); const changes: { field: string; from: unknown; to: unknown }[] = []; for (const key of allKeys) { // Technische Felder überspringen if (['id', 'createdAt', 'updatedAt', 'passwordEncrypted', 'portalPasswordEncrypted', 'portalPasswordHash', 'systemEmailPasswordEncrypted'].includes(key)) continue; const fromVal = before[key]; const toVal = after[key]; if (JSON.stringify(fromVal) !== JSON.stringify(toVal)) { changes.push({ field: key, from: fromVal, to: toVal }); } } if (changes.length === 0) { return

Keine sichtbaren Änderungen

; } return changes.map(({ field, from, to }) => (
{FIELD_LABELS[field] || field} {formatValue(from)} {formatValue(to)}
)); } interface DetailModalProps { log: AuditLog; onClose: () => void; } function DetailModal({ log, onClose }: DetailModalProps) { const parseChanges = (changes: string | undefined): Record | null => { if (!changes) return null; try { return JSON.parse(changes); } catch { return null; } }; const before = parseChanges(log.changesBefore); const after = parseChanges(log.changesAfter); return (

Audit-Log Details

Zeitpunkt
{formatDate(log.createdAt)}
Benutzer
{log.userEmail}
Aktion
{log.action}
Ressource
{RESOURCE_TYPE_LABELS[log.resourceType] || log.resourceType} {log.resourceId && `#${log.resourceId}`}
Endpoint
{log.httpMethod} {log.endpoint}
IP-Adresse
{log.ipAddress}
Sensitivität
{getSensitivityIcon(log.sensitivity)} {log.sensitivity}
Dauer
{log.durationMs ? `${log.durationMs}ms` : '-'}
{log.resourceLabel && (
Ressource-Bezeichnung
{log.resourceLabel}
)}
{log.changesEncrypted && (
Die Änderungsdaten sind verschlüsselt und können nur mit dem Encryption-Key eingesehen werden.
)} {(before || after) && !log.changesEncrypted && (

Änderungen

{before && after ? (
{renderChangeDiff(before, after)}
) : (
{before && (

Vorher

                        {JSON.stringify(before, null, 2)}
                      
)} {after && (

Daten

                        {JSON.stringify(after, null, 2)}
                      
)}
)}
)}
); } export default function AuditLogs() { const navigate = useNavigate(); // Export haengt an `audit:export`, Lesen an `audit:read` – ein Konto darf // blaettern duerfen, ohne das gesamte Protokoll herausziehen zu koennen. // Ohne diese Abfrage stuenden hier Knoepfe, die zuverlaessig 403 liefern. const { hasPermission } = useAuth(); const [page, setPage] = useState(1); const [filters, setFilters] = useState({ page: 1, limit: 50, }); const [selectedLog, setSelectedLog] = useState(null); const { data: logsData, isLoading, refetch } = useQuery({ queryKey: ['audit-logs', { ...filters, page }], queryFn: () => auditLogApi.search({ ...filters, page }), }); const logs = logsData?.data || []; const pagination = logsData?.pagination; const handleFilterChange = (key: keyof AuditLogSearchParams, value: string) => { setFilters(prev => ({ ...prev, [key]: value || undefined })); setPage(1); }; const handleExport = async (format: 'json' | 'csv') => { try { if (format === 'csv') { // CSV direkt als Download – mit kurzlebigem Download-Token (60s), // damit das langlebige Access-JWT nicht in Logs/Referer/History landet. const downloadToken = await authApi.getDownloadToken(); const params = new URLSearchParams(); params.set('format', 'csv'); // ALLE aktiven Filter mitgeben (Pentest R186-01). Vorher standen hier // nur fuenf – wer im Suchfeld eingrenzte und dann CSV klickte, bekam // stillschweigend das gesamte Protokoll statt seiner Auswahl. // `page`/`limit` gehoeren nicht dazu: der Export ist bewusst // vollstaendig ueber die gefilterte Menge. for (const [schluessel, wert] of Object.entries(filters)) { if (schluessel === 'page' || schluessel === 'limit') continue; if (wert === undefined || wert === null || wert === '') continue; params.set(schluessel, String(wert)); } window.open(`/api/audit-logs/export?${params}&token=${downloadToken ?? ''}`, '_blank'); } else { const result = await auditLogApi.export({ ...filters, format }); const blob = new Blob( [JSON.stringify(result.data, null, 2)], { type: 'application/json' } ); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } } catch (error) { alert(`Export fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`); } }; return (

Audit-Protokoll

{/* Filter */}
handleFilterChange('search', e.target.value)} className="w-full" /> handleFilterChange('resourceType', e.target.value)} options={RESOURCE_OPTIONS} /> handleFilterChange('startDate', e.target.value)} /> handleFilterChange('endDate', e.target.value)} />
{hasPermission('audit:export') && ( )} {hasPermission('audit:export') && ( )}
{/* Tabelle */} {isLoading ? (
Laden...
) : logs.length === 0 ? (
Keine Audit-Logs gefunden.
) : ( <>
{logs.map((log) => ( ))}
Zeitpunkt Benutzer Aktion Ressource IP
{formatDate(log.createdAt)}
{log.userEmail}
{log.userRole &&
{log.userRole}
}
{log.action} {log.resourceLabel ? (
{log.resourceLabel}
) : (
{log.resourceType}
)}
{log.ipAddress} {getSensitivityIcon(log.sensitivity)}
{/* Pagination */} {pagination && pagination.totalPages > 1 && (
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Einträge)
)} )}
{/* Detail Modal */} {selectedLog && ( setSelectedLog(null)} /> )}
); }