205 lines
7.8 KiB
TypeScript
205 lines
7.8 KiB
TypeScript
import { Suspense } from "react";
|
||
import Link from "next/link";
|
||
import { ArrowLeft } from "lucide-react";
|
||
import { verifySessionToken } from "@/lib/admin-auth";
|
||
import { getAuditLogs } from "@/lib/audit-log";
|
||
import { cookies } from "next/headers";
|
||
|
||
interface AuditLogEntry {
|
||
id: string;
|
||
email: string;
|
||
ip_addr: string;
|
||
user_agent: string;
|
||
success: boolean;
|
||
timestamp: string;
|
||
reason?: string;
|
||
}
|
||
|
||
async function AuditLogsContent() {
|
||
// ──── Auth Check ────
|
||
const cookieStore = await cookies();
|
||
const token = cookieStore.get("admin_session")?.value;
|
||
|
||
if (!token) {
|
||
return (
|
||
<div className="text-center py-12">
|
||
<p className="text-red-500 font-semibold">Unauthorized</p>
|
||
<Link href="/admin/login" className="text-blue-500 underline mt-4 inline-block">
|
||
Zurück zum Login
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const session = await verifySessionToken(token);
|
||
if (!session) {
|
||
return (
|
||
<div className="text-center py-12">
|
||
<p className="text-red-500 font-semibold">Session abgelaufen</p>
|
||
<Link href="/admin/login" className="text-blue-500 underline mt-4 inline-block">
|
||
Erneut anmelden
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ──── Audit-Logs abrufen (letzte 100) ────
|
||
const logs = await getAuditLogs({ limit: 100, offset: 0 });
|
||
|
||
// ──── Statistiken ────
|
||
const successCount = logs.filter((l) => l.success).length;
|
||
const failedCount = logs.filter((l) => !l.success).length;
|
||
const uniqueEmails = new Set(logs.map((l) => l.email)).size;
|
||
const uniqueIps = new Set(logs.map((l) => l.ip_addr)).size;
|
||
|
||
// ──── Fehlerrate in der letzten Stunde ────
|
||
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
||
const recentLogs = logs.filter((l) => l.timestamp > oneHourAgo);
|
||
const recentFailed = recentLogs.filter((l) => !l.success).length;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Header */}
|
||
<div className="bg-gradient-to-r from-[#1c1917] to-[#2d2b29] text-white py-8 px-6 rounded-lg">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold tracking-tight">Audit-Logs</h1>
|
||
<p className="text-white/70 text-sm mt-1">Login-Aktivitäten und Sicherheitsereignisse</p>
|
||
</div>
|
||
<Link
|
||
href="/admin"
|
||
className="inline-flex items-center gap-2 text-white/60 hover:text-white transition-colors"
|
||
>
|
||
<ArrowLeft className="w-4 h-4" />
|
||
Zurück
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Statistiken */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="bg-white border border-slate-200 p-4 rounded-lg">
|
||
<div className="text-sm text-slate-500">Erfolgreich (Gesamt)</div>
|
||
<div className="text-2xl font-bold text-green-600">{successCount}</div>
|
||
</div>
|
||
<div className="bg-white border border-slate-200 p-4 rounded-lg">
|
||
<div className="text-sm text-slate-500">Fehlgeschlagen (Gesamt)</div>
|
||
<div className="text-2xl font-bold text-red-600">{failedCount}</div>
|
||
</div>
|
||
<div className="bg-white border border-slate-200 p-4 rounded-lg">
|
||
<div className="text-sm text-slate-500">Fehlgeschlagen (1h)</div>
|
||
<div className={`text-2xl font-bold ${recentFailed >= 5 ? "text-red-600" : "text-green-600"}`}>
|
||
{recentFailed}
|
||
</div>
|
||
</div>
|
||
<div className="bg-white border border-slate-200 p-4 rounded-lg">
|
||
<div className="text-sm text-slate-500">Eindeutige IPs</div>
|
||
<div className="text-2xl font-bold text-slate-900">{uniqueIps}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Warnung bei verdächtiger Aktivität */}
|
||
{recentFailed >= 5 && (
|
||
<div className="bg-red-50 border-l-4 border-red-500 p-4 rounded">
|
||
<div className="flex">
|
||
<div>
|
||
<p className="text-sm font-semibold text-red-800">⚠️ Verdächtige Aktivität erkannt</p>
|
||
<p className="text-sm text-red-700 mt-1">
|
||
{recentFailed} fehlgeschlagene Login-Versuche in der letzten Stunde
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Logs Tabelle */}
|
||
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead className="bg-slate-50 border-b border-slate-200">
|
||
<tr>
|
||
<th className="px-4 py-3 text-left font-semibold text-slate-700">Zeitstempel</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-slate-700">E-Mail</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-slate-700">IP-Adresse</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-slate-700">Status</th>
|
||
<th className="px-4 py-3 text-left font-semibold text-slate-700">Grund</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-slate-200">
|
||
{logs.length === 0 ? (
|
||
<tr>
|
||
<td colSpan={5} className="px-4 py-8 text-center text-slate-500">
|
||
Keine Audit-Logs vorhanden
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
logs.map((log) => (
|
||
<tr key={log.id} className="hover:bg-slate-50 transition-colors">
|
||
<td className="px-4 py-3 whitespace-nowrap text-slate-600">
|
||
{new Date(log.timestamp).toLocaleString("de-DE")}
|
||
</td>
|
||
<td className="px-4 py-3 font-mono text-slate-700 truncate">{log.email}</td>
|
||
<td className="px-4 py-3 font-mono text-slate-700 text-xs">{log.ip_addr}</td>
|
||
<td className="px-4 py-3">
|
||
<span
|
||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||
log.success
|
||
? "bg-green-100 text-green-800"
|
||
: "bg-red-100 text-red-800"
|
||
}`}
|
||
>
|
||
{log.success ? "✓ Erfolgreich" : "✗ Fehlgeschlagen"}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-600 text-xs">
|
||
{log.reason ? formatReason(log.reason) : "-"}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
{/* User-Agent Info */}
|
||
<div className="bg-blue-50 border border-blue-200 p-4 rounded-lg">
|
||
<p className="text-sm text-blue-800">
|
||
<strong>Hinweis:</strong> User-Agent und detaillierte Geräte-Informationen sind in der Datenbank
|
||
gespeichert. Für erweiterte Analysen siehe die Supabase-Tabelle{" "}
|
||
<code className="bg-white px-2 py-1 rounded border border-blue-200 font-mono text-xs">
|
||
admin_audit_logs
|
||
</code>
|
||
</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function formatReason(reason: string): string {
|
||
const reasons: Record<string, string> = {
|
||
invalid_password: "Falsches Passwort",
|
||
user_not_found_or_inactive: "User nicht gefunden / inaktiv",
|
||
missing_credentials: "Fehlende Anmeldedaten",
|
||
invalid_token: "Ungültiger Token",
|
||
token_expired: "Token abgelaufen",
|
||
};
|
||
return reasons[reason] || reason;
|
||
}
|
||
|
||
export default function AuditLogsPage() {
|
||
return (
|
||
<div className="max-w-6xl mx-auto px-4 py-8">
|
||
<Suspense
|
||
fallback={
|
||
<div className="text-center py-12">
|
||
<p className="text-slate-500">Lädt Audit-Logs...</p>
|
||
</div>
|
||
}
|
||
>
|
||
<AuditLogsContent />
|
||
</Suspense>
|
||
</div>
|
||
);
|
||
}
|