import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import toast from 'react-hot-toast'; import { monitoringApi, type SecurityEventType, type SecuritySeverity } from '../../services/api'; 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 Modal from '../../components/ui/Modal'; import { ArrowLeft, Send, RefreshCw, Mail, ShieldAlert, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, Trash2 } from 'lucide-react'; /** * Liefert die anzuzeigenden Seitenzahlen für die Pagination. * Bis zu 10 Seitenzahlen, current möglichst mittig. */ function paginationWindow(current: number, total: number, size = 10): number[] { if (total <= size) return Array.from({ length: total }, (_, i) => i + 1); let start = Math.max(1, current - Math.floor(size / 2)); let end = start + size - 1; if (end > total) { end = total; start = end - size + 1; } return Array.from({ length: end - start + 1 }, (_, i) => start + i); } const TYPE_OPTIONS: { value: SecurityEventType | ''; label: string }[] = [ { value: '', label: 'Alle Typen' }, { value: 'LOGIN_FAILED', label: 'Login fehlgeschlagen' }, { value: 'LOGIN_SUCCESS', label: 'Login erfolgreich' }, { value: 'RATE_LIMIT_HIT', label: 'Rate-Limit greift' }, { value: 'ACCESS_DENIED', label: 'Zugriff verweigert (IDOR)' }, { value: 'SSRF_BLOCKED', label: 'SSRF blockiert' }, { value: 'PASSWORD_RESET_REQUEST', label: 'Passwort-Reset angefordert' }, { value: 'PASSWORD_RESET_CONFIRM', label: 'Passwort-Reset bestätigt' }, { value: 'LOGOUT', label: 'Logout' }, { value: 'TOKEN_REJECTED', label: 'Token abgelehnt' }, { value: 'PERMISSION_CHANGED', label: 'Berechtigung geändert' }, { value: 'AUDIT_SEAL_CHANGED', label: 'Bestandssiegel gesetzt/ersetzt' }, { value: 'SUSPICIOUS', label: 'Verdächtig (Threshold)' }, ]; const SEVERITY_OPTIONS: { value: SecuritySeverity | ''; label: string }[] = [ { value: '', label: 'Alle Stufen' }, { value: 'INFO', label: 'Info' }, { value: 'LOW', label: 'Niedrig' }, { value: 'MEDIUM', label: 'Mittel' }, { value: 'HIGH', label: 'Hoch' }, { value: 'CRITICAL', label: 'Kritisch' }, ]; function severityClass(s: SecuritySeverity): string { switch (s) { case 'CRITICAL': return 'bg-red-100 text-red-800 border border-red-300'; case 'HIGH': return 'bg-orange-100 text-orange-800 border border-orange-300'; case 'MEDIUM': return 'bg-yellow-100 text-yellow-800 border border-yellow-300'; case 'LOW': return 'bg-blue-100 text-blue-800 border border-blue-300'; default: return 'bg-gray-100 text-gray-700 border border-gray-300'; } } function severityIcon(s: SecuritySeverity): string { switch (s) { case 'CRITICAL': return '🚨'; case 'HIGH': return '⚠️'; case 'MEDIUM': return '🟡'; case 'LOW': return '🟢'; default: return 'ℹ️'; } } export default function Monitoring() { const navigate = useNavigate(); const queryClient = useQueryClient(); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(50); const [filters, setFilters] = useState({ type: '' as SecurityEventType | '', severity: '' as SecuritySeverity | '', search: '', ip: '', }); const [showClearConfirm, setShowClearConfirm] = useState(false); const [clearOlderThanDays, setClearOlderThanDays] = useState(''); const [alertEmail, setAlertEmail] = useState(''); const [digestEnabled, setDigestEnabled] = useState(false); // Settings laden const { data: settingsData } = useQuery({ queryKey: ['monitoring-settings'], queryFn: monitoringApi.getSettings, }); // States nach Laden synchronisieren (nur initial) if (settingsData?.data && alertEmail === '' && settingsData.data.alertEmail !== '') { setAlertEmail(settingsData.data.alertEmail); setDigestEnabled(settingsData.data.digestEnabled); } // Events laden const { data: eventsData, isLoading: eventsLoading } = useQuery({ queryKey: ['monitoring-events', page, pageSize, filters], queryFn: () => monitoringApi.getEvents({ page, limit: pageSize, ...filters }), refetchInterval: 30_000, // alle 30s neu laden }); const clearEvents = useMutation({ mutationFn: (olderThanDays?: number) => monitoringApi.clearEvents(olderThanDays), onSuccess: (res) => { toast.success(res.message || 'Events gelöscht'); setShowClearConfirm(false); setClearOlderThanDays(''); queryClient.invalidateQueries({ queryKey: ['monitoring-events'] }); }, onError: (e: Error) => toast.error(e.message || 'Löschen fehlgeschlagen'), }); const saveSettings = useMutation({ mutationFn: () => monitoringApi.updateSettings({ alertEmail, digestEnabled }), onSuccess: () => { toast.success('Einstellungen gespeichert'); queryClient.invalidateQueries({ queryKey: ['monitoring-settings'] }); }, onError: (e: Error) => toast.error(e.message || 'Speichern fehlgeschlagen'), }); const testAlert = useMutation({ mutationFn: () => monitoringApi.testAlert(), onSuccess: (res) => toast.success(res.message || 'Test-Alert versendet'), onError: (e: Error) => toast.error(e.message || 'Test fehlgeschlagen'), }); const runDigest = useMutation({ mutationFn: () => monitoringApi.runDigest(), onSuccess: (res) => { const r = res.data; if (r?.sent) toast.success(`Digest mit ${r.eventCount} Events versendet`); else toast(r?.reason || 'Kein Digest versendet', { icon: 'ℹ️' }); }, onError: (e: Error) => toast.error(e.message || 'Digest fehlgeschlagen'), }); const events = eventsData?.data || []; const stats = eventsData?.stats; const pagination = eventsData?.pagination; return (

Sicherheits-Monitoring

Sicherheitsrelevante Ereignisse + Alert-Einstellungen.

{/* Settings */}

Alert-Empfänger

setAlertEmail(e.target.value)} placeholder="security@deine-firma.de" />

Leer lassen, um Alerts zu deaktivieren.

{settingsData?.data?.lastDigestAt && ( Letzter Digest: {new Date(settingsData.data.lastDigestAt).toLocaleString('de-DE')} )}
Sofort-Alert: CRITICAL-Events (z.B. Brute-Force-Verdacht) werden binnen 1 Minute per E-Mail versendet.
Digest: HIGH+MEDIUM-Events werden zur vollen Stunde gesammelt verschickt (wenn aktiviert).
{/* Stats-Cards */} {stats && (
{(['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO'] as SecuritySeverity[]).map((sev) => (
c.startsWith('text-'))[0]}`}> {severityIcon(sev)} {sev}
{stats.bySeverity[sev] || 0}
))}
)} {/* Filter */}
{ setFilters((f) => ({ ...f, severity: e.target.value as any })); setPage(1); }} options={SEVERITY_OPTIONS} /> { setFilters((f) => ({ ...f, search: e.target.value })); setPage(1); }} placeholder="z.B. admin@admin.com" /> { setFilters((f) => ({ ...f, ip: e.target.value })); setPage(1); }} placeholder="z.B. 1.2.3.4" />
{/* Tabelle */}

Events

{eventsLoading ? (
Lade…
) : events.length === 0 ? (
Keine Events für diese Filter.
) : (
{events.map((e) => ( ))}
Zeit Severity Typ Nachricht Wer IP Endpoint Alert
{new Date(e.createdAt).toLocaleString('de-DE', { dateStyle: 'short', timeStyle: 'medium' })} {severityIcon(e.severity)} {e.severity} {e.type} {e.message} {e.userEmail || (e.userId ? `User #${e.userId}` : e.customerId ? `Kunde #${e.customerId}` : '–')} {e.ipAddress || '–'} {e.endpoint || '–'} {e.alerted ? '✉️ ja' : '–'}
)} {pagination && pagination.totalPages > 1 && (
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Einträge)
{paginationWindow(page, pagination.totalPages, 10).map((p) => ( ))}
)}
{/* Clear-Confirm-Modal */} setShowClearConfirm(false)} title="Security-Log leeren" size="sm" >

Sicher? Alle Events werden aus der Datenbank entfernt. Ein Audit-Log-Eintrag mit deinem Namen bleibt erhalten.

setClearOlderThanDays(e.target.value === '' ? '' : parseInt(e.target.value))} placeholder="leer = alle löschen" className="block w-full max-w-[200px] px-3 py-2 border border-gray-300 rounded text-sm" />

Beispiel: 30 = nur Events älter als 30 Tage löschen.

); }