gdpr audit implemented, email log, vollmachten, pdf delete cancel data privacy and vollmachten, removed message no id card in engergy car, and other contracts that are not telecom contracts, added insert counter for engery
This commit is contained in:
@@ -0,0 +1,413 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { auditLogApi, AuditLogSearchParams } 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 { ArrowLeft, Download, Eye, Shield, ShieldCheck, 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' },
|
||||
];
|
||||
|
||||
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: 'Ausweisdokumente' },
|
||||
{ value: 'Authentication', label: 'Authentifizierung' },
|
||||
{ value: 'CustomerConsent', label: 'Einwilligungen' },
|
||||
{ value: 'GDPR', label: 'DSGVO' },
|
||||
];
|
||||
|
||||
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';
|
||||
default: return 'bg-gray-100 text-gray-800';
|
||||
}
|
||||
}
|
||||
|
||||
function getSensitivityIcon(sensitivity: AuditSensitivity) {
|
||||
switch (sensitivity) {
|
||||
case 'LOW': return <Shield className="w-4 h-4 text-gray-400" />;
|
||||
case 'MEDIUM': return <Shield className="w-4 h-4 text-blue-500" />;
|
||||
case 'HIGH': return <ShieldAlert className="w-4 h-4 text-orange-500" />;
|
||||
case 'CRITICAL': return <ShieldAlert className="w-4 h-4 text-red-500" />;
|
||||
default: return <Shield className="w-4 h-4 text-gray-400" />;
|
||||
}
|
||||
}
|
||||
|
||||
interface DetailModalProps {
|
||||
log: AuditLog;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
function DetailModal({ log, onClose }: DetailModalProps) {
|
||||
const parseChanges = (changes: string | undefined): Record<string, unknown> | null => {
|
||||
if (!changes) return null;
|
||||
try {
|
||||
return JSON.parse(changes);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const before = parseChanges(log.changesBefore);
|
||||
const after = parseChanges(log.changesAfter);
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-semibold">Audit-Log Details</h2>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-6">
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Zeitpunkt</dt>
|
||||
<dd className="font-medium">{formatDate(log.createdAt)}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Benutzer</dt>
|
||||
<dd className="font-medium">{log.userEmail}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Aktion</dt>
|
||||
<dd><span className={`px-2 py-1 rounded text-xs font-medium ${getActionColor(log.action)}`}>{log.action}</span></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Ressource</dt>
|
||||
<dd className="font-medium">{log.resourceType} {log.resourceId && `#${log.resourceId}`}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Endpoint</dt>
|
||||
<dd className="font-mono text-sm">{log.httpMethod} {log.endpoint}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">IP-Adresse</dt>
|
||||
<dd className="font-mono text-sm">{log.ipAddress}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Sensitivität</dt>
|
||||
<dd className="flex items-center gap-2">{getSensitivityIcon(log.sensitivity)} {log.sensitivity}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="text-sm text-gray-500">Dauer</dt>
|
||||
<dd>{log.durationMs ? `${log.durationMs}ms` : '-'}</dd>
|
||||
</div>
|
||||
{log.resourceLabel && (
|
||||
<div className="col-span-2">
|
||||
<dt className="text-sm text-gray-500">Ressource-Bezeichnung</dt>
|
||||
<dd className="font-medium">{log.resourceLabel}</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{log.changesEncrypted && (
|
||||
<div className="mb-4 p-3 bg-yellow-50 text-yellow-800 rounded-lg text-sm">
|
||||
Die Änderungsdaten sind verschlüsselt und können nur mit dem Encryption-Key eingesehen werden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AuditLogs() {
|
||||
const navigate = useNavigate();
|
||||
const [page, setPage] = useState(1);
|
||||
const [filters, setFilters] = useState<AuditLogSearchParams>({
|
||||
page: 1,
|
||||
limit: 50,
|
||||
});
|
||||
const [selectedLog, setSelectedLog] = useState<AuditLog | null>(null);
|
||||
|
||||
const { data: logsData, isLoading, refetch } = useQuery({
|
||||
queryKey: ['audit-logs', { ...filters, page }],
|
||||
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;
|
||||
|
||||
const handleFilterChange = (key: keyof AuditLogSearchParams, value: string) => {
|
||||
setFilters(prev => ({ ...prev, [key]: value || undefined }));
|
||||
setPage(1);
|
||||
};
|
||||
|
||||
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);
|
||||
} catch (error) {
|
||||
alert(`Export fehlgeschlagen: ${error instanceof Error ? error.message : 'Unbekannter Fehler'}`);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<Button variant="ghost" onClick={() => navigate('/settings')}>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Zurück
|
||||
</Button>
|
||||
<h1 className="text-2xl font-bold">Audit-Protokoll</h1>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<Card className="mb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<Input
|
||||
placeholder="Suche..."
|
||||
value={filters.search || ''}
|
||||
onChange={(e) => handleFilterChange('search', e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
<Select
|
||||
value={filters.action || ''}
|
||||
onChange={(e) => handleFilterChange('action', e.target.value)}
|
||||
options={ACTION_OPTIONS}
|
||||
/>
|
||||
<Select
|
||||
value={filters.resourceType || ''}
|
||||
onChange={(e) => handleFilterChange('resourceType', e.target.value)}
|
||||
options={RESOURCE_OPTIONS}
|
||||
/>
|
||||
<Select
|
||||
value={filters.sensitivity || ''}
|
||||
onChange={(e) => handleFilterChange('sensitivity', e.target.value as AuditSensitivity)}
|
||||
options={SENSITIVITY_OPTIONS}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||
<Input
|
||||
type="date"
|
||||
label="Von"
|
||||
value={filters.startDate || ''}
|
||||
onChange={(e) => handleFilterChange('startDate', e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
type="date"
|
||||
label="Bis"
|
||||
value={filters.endDate || ''}
|
||||
onChange={(e) => handleFilterChange('endDate', e.target.value)}
|
||||
/>
|
||||
<div></div>
|
||||
<div className="flex gap-2 justify-end items-end">
|
||||
<Button variant="secondary" onClick={() => refetch()}>
|
||||
<RefreshCw className="w-4 h-4 mr-2" />
|
||||
Aktualisieren
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={() => handleExport('json')}>
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
Export
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Tabelle */}
|
||||
<Card>
|
||||
{isLoading ? (
|
||||
<div className="text-center py-8">Laden...</div>
|
||||
) : logs.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-500">Keine Audit-Logs gefunden.</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="text-left py-3 px-4">Zeitpunkt</th>
|
||||
<th className="text-left py-3 px-4">Benutzer</th>
|
||||
<th className="text-left py-3 px-4">Aktion</th>
|
||||
<th className="text-left py-3 px-4">Ressource</th>
|
||||
<th className="text-left py-3 px-4">IP</th>
|
||||
<th className="text-center py-3 px-4"></th>
|
||||
<th className="text-center py-3 px-4"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{logs.map((log) => (
|
||||
<tr key={log.id} className="border-b hover:bg-gray-50">
|
||||
<td className="py-3 px-4 whitespace-nowrap">{formatDate(log.createdAt)}</td>
|
||||
<td className="py-3 px-4">
|
||||
<div className="truncate max-w-[200px]" title={log.userEmail}>
|
||||
{log.userEmail}
|
||||
</div>
|
||||
{log.userRole && <div className="text-xs text-gray-500">{log.userRole}</div>}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getActionColor(log.action)}`}>
|
||||
{log.action}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>{log.resourceType}</div>
|
||||
{log.resourceLabel && (
|
||||
<div className="text-xs text-gray-500 truncate max-w-[200px]" title={log.resourceLabel}>
|
||||
{log.resourceLabel}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-3 px-4 font-mono text-xs">{log.ipAddress}</td>
|
||||
<td className="py-3 px-4 text-center">{getSensitivityIcon(log.sensitivity)}</td>
|
||||
<td className="py-3 px-4 text-center">
|
||||
<Button variant="ghost" size="sm" onClick={() => setSelectedLog(log)}>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{pagination && pagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-4 pt-4 border-t">
|
||||
<div className="text-sm text-gray-500">
|
||||
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Einträge)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => Math.max(1, p - 1))}
|
||||
disabled={page === 1}
|
||||
>
|
||||
<ChevronLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => setPage(p => Math.min(pagination.totalPages, p + 1))}
|
||||
disabled={page === pagination.totalPages}
|
||||
>
|
||||
<ChevronRight className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Detail Modal */}
|
||||
{selectedLog && (
|
||||
<DetailModal log={selectedLog} onClose={() => setSelectedLog(null)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user