Files
opencrm/frontend/src/pages/settings/EmailLogs.tsx
T
duffyduck 8fc050a282 docs: TESTING.md mit Check-Listen für Security + Email-Log-System
Strukturierter Test-Katalog für manuelle Abnahmetests vor einem Release.

Security-System (10 Abschnitte):
- Login + Rate-Limiting
- Passwort-Reset-Flow (Mitarbeiter + Portal)
- Rate-Limiting Passwort-Reset
- Berechtigungen (RBAC)
- Portal-Isolation (DSGVO-kritisch)
- Session-Invalidation
- Audit-Log
- DSGVO-Features (Export, Löschanfragen, Einwilligungen)
- Verschlüsselte Credentials
- DSGVO-Einwilligung sperrt Tabs für Mitarbeiter

Email-Log-System (5 Abschnitte):
- Email-Log-Seite (UI, Filter, Suche, Pagination)
- Alle 6 Kontexte durchspielen:
  consent-link, authorization-request, customer-email,
  birthday-greeting, birthday-greeting-auto, password-reset
- Fehlgeschlagener Versand wird geloggt (Test mit falschem SMTP-Passwort)
- Details-Modal mit SMTP-Details
- Automatisches Logging (Kontext, triggeredBy)

Außerdem: Neue Kontexte in EmailLogs.tsx CONTEXT_LABELS ergänzt
(birthday-greeting, birthday-greeting-auto, password-reset).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 17:21:34 +02:00

309 lines
13 KiB
TypeScript

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { useNavigate } from 'react-router-dom';
import { emailLogApi, EmailLog } from '../../services/api';
import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import Badge from '../../components/ui/Badge';
import Modal from '../../components/ui/Modal';
import { ArrowLeft, CheckCircle2, XCircle, Mail, Server, ChevronLeft, ChevronRight } from 'lucide-react';
const CONTEXT_LABELS: Record<string, string> = {
'consent-link': 'Datenschutz-Link',
'authorization-request': 'Vollmacht-Anfrage',
'customer-email': 'Kunden-E-Mail',
'birthday-greeting': 'Geburtstagsgruß (manuell)',
'birthday-greeting-auto': 'Geburtstagsgruß (automatisch)',
'password-reset': 'Passwort-Reset',
};
export default function EmailLogs() {
const navigate = useNavigate();
const [page, setPage] = useState(1);
const [search, setSearch] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('');
const [contextFilter, setContextFilter] = useState<string>('');
const [selectedLog, setSelectedLog] = useState<EmailLog | null>(null);
const { data: statsData } = useQuery({
queryKey: ['email-log-stats'],
queryFn: () => emailLogApi.getStats(),
});
const { data: logsData, isLoading } = useQuery({
queryKey: ['email-logs', page, search, statusFilter, contextFilter],
queryFn: () => emailLogApi.getLogs({
page,
limit: 30,
success: statusFilter || undefined,
search: search || undefined,
context: contextFilter || undefined,
}),
});
const stats = statsData?.data;
const logs = logsData?.data || [];
const pagination = logsData?.pagination;
const formatDate = (date: string) =>
new Date(date).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit', second: '2-digit',
});
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">E-Mail-Versandlog</h1>
</div>
{/* Statistiken */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<Card>
<div className="text-center">
<p className="text-2xl font-bold text-gray-900">{stats.total}</p>
<p className="text-sm text-gray-500">Gesamt</p>
</div>
</Card>
<Card>
<div className="text-center">
<p className="text-2xl font-bold text-green-600">{stats.success}</p>
<p className="text-sm text-gray-500">Erfolgreich</p>
</div>
</Card>
<Card>
<div className="text-center">
<p className="text-2xl font-bold text-red-600">{stats.failed}</p>
<p className="text-sm text-gray-500">Fehlgeschlagen</p>
</div>
</Card>
<Card>
<div className="text-center">
<p className="text-2xl font-bold text-blue-600">{stats.last24h}</p>
<p className="text-sm text-gray-500">Letzte 24h</p>
</div>
</Card>
</div>
)}
{/* Filter */}
<Card className="mb-6">
<div className="flex gap-4 flex-wrap">
<div className="flex-1 min-w-[200px]">
<Input
placeholder="Suche (Absender, Empfänger, Betreff...)"
value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }}
/>
</div>
<select
value={statusFilter}
onChange={(e) => { setStatusFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Alle Status</option>
<option value="true">Erfolgreich</option>
<option value="false">Fehlgeschlagen</option>
</select>
<select
value={contextFilter}
onChange={(e) => { setContextFilter(e.target.value); setPage(1); }}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Alle Typen</option>
<option value="consent-link">Datenschutz-Link</option>
<option value="authorization-request">Vollmacht-Anfrage</option>
<option value="customer-email">Kunden-E-Mail</option>
</select>
</div>
</Card>
{/* Log-Tabelle */}
<Card>
{isLoading ? (
<div className="text-center py-8 text-gray-500">Laden...</div>
) : logs.length === 0 ? (
<div className="text-center py-8 text-gray-500">Keine E-Mail-Logs vorhanden.</div>
) : (
<>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b">
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Status</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Zeitpunkt</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Typ</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Von</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">An</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">Betreff</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm">SMTP</th>
<th className="text-left py-3 px-3 font-medium text-gray-600 text-sm"></th>
</tr>
</thead>
<tbody>
{logs.map((log) => (
<tr key={log.id} className="border-b hover:bg-gray-50">
<td className="py-2 px-3">
{log.success ? (
<CheckCircle2 className="w-4 h-4 text-green-500" />
) : (
<XCircle className="w-4 h-4 text-red-500" />
)}
</td>
<td className="py-2 px-3 text-xs text-gray-500 whitespace-nowrap">
{formatDate(log.sentAt)}
</td>
<td className="py-2 px-3">
<span className="text-xs px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">
{CONTEXT_LABELS[log.context] || log.context}
</span>
</td>
<td className="py-2 px-3 text-sm truncate max-w-[150px]">{log.fromAddress}</td>
<td className="py-2 px-3 text-sm truncate max-w-[150px]">{log.toAddress}</td>
<td className="py-2 px-3 text-sm truncate max-w-[200px]">{log.subject}</td>
<td className="py-2 px-3 text-xs text-gray-500 whitespace-nowrap">
{log.smtpServer}:{log.smtpPort}
</td>
<td className="py-2 px-3">
<button
onClick={() => setSelectedLog(log)}
className="text-blue-600 hover:underline text-xs"
>
Details
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
{pagination && pagination.totalPages > 1 && (
<div className="mt-4 flex items-center justify-between">
<p className="text-sm text-gray-500">
Seite {pagination.page} von {pagination.totalPages} ({pagination.total} Einträge)
</p>
<div className="flex gap-2">
<Button
variant="secondary"
size="sm"
disabled={page <= 1}
onClick={() => setPage(page - 1)}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<Button
variant="secondary"
size="sm"
disabled={page >= pagination.totalPages}
onClick={() => setPage(page + 1)}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</>
)}
</Card>
{/* Detail-Modal */}
{selectedLog && (
<Modal
isOpen={true}
onClose={() => setSelectedLog(null)}
title="E-Mail-Log Details"
>
<div className="space-y-4">
{/* Status */}
<div className="flex items-center gap-2">
{selectedLog.success ? (
<Badge variant="success">Erfolgreich</Badge>
) : (
<Badge variant="danger">Fehlgeschlagen</Badge>
)}
<span className="text-sm text-gray-500">{formatDate(selectedLog.sentAt)}</span>
</div>
{/* E-Mail-Details */}
<div className="border rounded-lg p-4 space-y-2">
<div className="flex items-start gap-2">
<Mail className="w-4 h-4 text-gray-400 mt-0.5" />
<div className="flex-1">
<div className="grid grid-cols-[80px_1fr] gap-1 text-sm">
<span className="text-gray-500">Von:</span>
<span>{selectedLog.fromAddress}</span>
<span className="text-gray-500">An:</span>
<span>{selectedLog.toAddress}</span>
<span className="text-gray-500">Betreff:</span>
<span>{selectedLog.subject}</span>
<span className="text-gray-500">Typ:</span>
<span>{CONTEXT_LABELS[selectedLog.context] || selectedLog.context}</span>
{selectedLog.triggeredBy && (
<>
<span className="text-gray-500">Ausgelöst von:</span>
<span>{selectedLog.triggeredBy}</span>
</>
)}
{selectedLog.messageId && (
<>
<span className="text-gray-500">Message-ID:</span>
<span className="font-mono text-xs break-all">{selectedLog.messageId}</span>
</>
)}
</div>
</div>
</div>
</div>
{/* SMTP-Details */}
<div className="border rounded-lg p-4 space-y-2">
<div className="flex items-start gap-2">
<Server className="w-4 h-4 text-gray-400 mt-0.5" />
<div className="flex-1">
<h4 className="text-sm font-medium mb-2">SMTP-Verbindung</h4>
<div className="grid grid-cols-[100px_1fr] gap-1 text-sm">
<span className="text-gray-500">Server:</span>
<span className="font-mono text-xs">{selectedLog.smtpServer}:{selectedLog.smtpPort}</span>
<span className="text-gray-500">Verschlüsselung:</span>
<span>{selectedLog.smtpEncryption}</span>
<span className="text-gray-500">Benutzer:</span>
<span className="font-mono text-xs">{selectedLog.smtpUser}</span>
</div>
</div>
</div>
</div>
{/* SMTP-Antwort */}
{selectedLog.smtpResponse && (
<div className="border rounded-lg p-4">
<h4 className="text-sm font-medium mb-2">SMTP-Antwort</h4>
<pre className="text-xs bg-gray-50 p-3 rounded overflow-x-auto whitespace-pre-wrap font-mono text-gray-700">
{selectedLog.smtpResponse}
</pre>
</div>
)}
{/* Fehlermeldung */}
{selectedLog.errorMessage && (
<div className="border border-red-200 rounded-lg p-4 bg-red-50">
<h4 className="text-sm font-medium text-red-800 mb-2">Fehlermeldung</h4>
<pre className="text-xs whitespace-pre-wrap font-mono text-red-700">
{selectedLog.errorMessage}
</pre>
</div>
)}
</div>
</Modal>
)}
</div>
);
}