MBO-Tech-IT-Webseite/modules/02-admin-auth/files/app/admin/audit-logs/page.tsx

205 lines
7.8 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}