complete new audit system

This commit is contained in:
2026-03-21 18:23:54 +01:00
parent 38b3b7da73
commit fd55742c57
159 changed files with 2841 additions and 736 deletions
+131 -59
View File
@@ -1,5 +1,5 @@
import { useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { auditLogApi, AuditLogSearchParams } from '../../services/api';
import type { AuditLog, AuditAction, AuditSensitivity } from '../../types';
@@ -7,7 +7,7 @@ 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 { ArrowLeft, Download, Eye, Shield, ShieldCheck, ShieldAlert, RefreshCw, ChevronLeft, ChevronRight, X } from 'lucide-react';
import { ArrowLeft, Download, Eye, Shield, ShieldAlert, RefreshCw, ChevronLeft, ChevronRight, X } from 'lucide-react';
const ACTION_OPTIONS = [
{ value: '', label: 'Alle Aktionen' },
@@ -82,6 +82,82 @@ function getSensitivityIcon(sensitivity: AuditSensitivity) {
}
}
const RESOURCE_TYPE_LABELS: Record<string, string> = {
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<string, string> = {
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<string, unknown>, after: Record<string, unknown>) {
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 <p className="text-sm text-gray-500 italic">Keine sichtbaren Änderungen</p>;
}
return changes.map(({ field, from, to }) => (
<div key={field} className="flex items-start gap-3 p-2 bg-gray-50 rounded text-sm">
<span className="font-medium text-gray-700 min-w-[150px]">
{FIELD_LABELS[field] || field}
</span>
<span className="text-red-600 line-through">{formatValue(from)}</span>
<span className="text-gray-400"></span>
<span className="text-green-700 font-medium">{formatValue(to)}</span>
</div>
));
}
interface DetailModalProps {
log: AuditLog;
onClose: () => void;
@@ -126,7 +202,7 @@ function DetailModal({ log, onClose }: DetailModalProps) {
</div>
<div>
<dt className="text-sm text-gray-500">Ressource</dt>
<dd className="font-medium">{log.resourceType} {log.resourceId && `#${log.resourceId}`}</dd>
<dd className="font-medium">{RESOURCE_TYPE_LABELS[log.resourceType] || log.resourceType} {log.resourceId && `#${log.resourceId}`}</dd>
</div>
<div>
<dt className="text-sm text-gray-500">Endpoint</dt>
@@ -161,36 +237,33 @@ function DetailModal({ log, onClose }: DetailModalProps) {
{(before || after) && !log.changesEncrypted && (
<div className="border-t pt-4">
<h3 className="font-medium mb-3">Änderungen</h3>
<div className="grid grid-cols-2 gap-4">
{before && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2">Vorher</h4>
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
{JSON.stringify(before, null, 2)}
</pre>
</div>
)}
{after && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2">Nachher</h4>
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
{JSON.stringify(after, null, 2)}
</pre>
</div>
)}
</div>
{before && after ? (
<div className="space-y-2">
{renderChangeDiff(before, after)}
</div>
) : (
<div className="grid grid-cols-2 gap-4">
{before && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2">Vorher</h4>
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
{JSON.stringify(before, null, 2)}
</pre>
</div>
)}
{after && (
<div>
<h4 className="text-sm font-medium text-gray-500 mb-2">Daten</h4>
<pre className="bg-gray-50 p-3 rounded text-xs overflow-auto max-h-64">
{JSON.stringify(after, null, 2)}
</pre>
</div>
)}
</div>
)}
</div>
)}
{log.hash && (
<div className="border-t pt-4 mt-4">
<h3 className="font-medium mb-2">Integrität</h3>
<div className="text-xs font-mono bg-gray-50 p-2 rounded break-all">
<div><span className="text-gray-500">Hash:</span> {log.hash}</div>
{log.previousHash && <div className="mt-1"><span className="text-gray-500">Vorheriger:</span> {log.previousHash}</div>}
</div>
</div>
)}
<div className="flex justify-end mt-6 pt-4 border-t">
<Button variant="secondary" onClick={onClose}>Schließen</Button>
@@ -215,19 +288,6 @@ export default function AuditLogs() {
queryFn: () => auditLogApi.search({ ...filters, page }),
});
const verifyMutation = useMutation({
mutationFn: () => auditLogApi.verifyIntegrity(),
onSuccess: (result) => {
if (result.data?.valid) {
alert('Hash-Kette ist intakt. Keine Manipulationen festgestellt.');
} else {
alert(`Integritätsfehler gefunden:\n${result.data?.errors?.join('\n')}`);
}
},
onError: (error) => {
alert(`Fehler bei der Integritätsprüfung: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
},
});
const logs = logsData?.data || [];
const pagination = logsData?.pagination;
@@ -239,18 +299,30 @@ export default function AuditLogs() {
const handleExport = async (format: 'json' | 'csv') => {
try {
const result = await auditLogApi.export({ ...filters, format });
const data = result.data;
const blob = new Blob(
[format === 'json' ? JSON.stringify(data, null, 2) : ''],
{ type: format === 'json' ? 'application/json' : 'text/csv' }
);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`;
a.click();
URL.revokeObjectURL(url);
if (format === 'csv') {
// CSV direkt als Download
const token = localStorage.getItem('token');
const params = new URLSearchParams();
params.set('format', 'csv');
if (filters.action) params.set('action', filters.action);
if (filters.sensitivity) params.set('sensitivity', filters.sensitivity);
if (filters.resourceType) params.set('resourceType', filters.resourceType);
if (filters.startDate) params.set('startDate', filters.startDate);
if (filters.endDate) params.set('endDate', filters.endDate);
window.open(`/api/audit-logs/export?${params}&token=${token}`, '_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'}`);
}
@@ -312,11 +384,11 @@ export default function AuditLogs() {
</Button>
<Button variant="secondary" onClick={() => handleExport('json')}>
<Download className="w-4 h-4 mr-2" />
Export
JSON
</Button>
<Button variant="secondary" onClick={() => verifyMutation.mutate()} disabled={verifyMutation.isPending}>
<ShieldCheck className="w-4 h-4 mr-2" />
{verifyMutation.isPending ? 'Prüfe...' : 'Integrität'}
<Button variant="secondary" onClick={() => handleExport('csv')}>
<Download className="w-4 h-4 mr-2" />
CSV
</Button>
</div>
</div>