complete new audit system
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user