164 lines
6.4 KiB
TypeScript
164 lines
6.4 KiB
TypeScript
import { Suspense } from "react";
|
||
import Link from "next/link";
|
||
import { verifySessionToken } from "@/lib/admin-auth";
|
||
import { getAuditLogs } from "@/lib/audit-log";
|
||
import { cookies } from "next/headers";
|
||
import { AdminNav } from "@/components/admin/AdminNav";
|
||
|
||
async function AuditLogsContent() {
|
||
const cookieStore = await cookies();
|
||
const token = cookieStore.get("admin_session")?.value;
|
||
|
||
if (!token) {
|
||
return (
|
||
<div className="text-center py-12">
|
||
<p className="text-red-400 font-semibold">Nicht authentifiziert</p>
|
||
<Link href="/admin/login" className="text-orange-400 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-400 font-semibold">Session abgelaufen</p>
|
||
<Link href="/admin/login" className="text-orange-400 underline mt-4 inline-block">
|
||
Erneut anmelden
|
||
</Link>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const logs = await getAuditLogs({ limit: 100, offset: 0 });
|
||
|
||
const successCount = logs.filter((l) => l.success).length;
|
||
const failedCount = logs.filter((l) => !l.success).length;
|
||
const uniqueIps = new Set(logs.map((l) => l.ip_addr)).size;
|
||
|
||
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">
|
||
<div>
|
||
<h1 className="text-2xl font-bold text-white tracking-tight">Audit-Logs</h1>
|
||
<p className="text-sm text-slate-500 mt-0.5">Login-Aktivitäten und Sicherheitsereignisse</p>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
{[
|
||
{ label: "Erfolgreich (Gesamt)", wert: successCount, color: "text-green-400" },
|
||
{ label: "Fehlgeschlagen (Gesamt)", wert: failedCount, color: "text-red-400" },
|
||
{ label: "Fehlgeschlagen (1h)", wert: recentFailed, color: recentFailed >= 5 ? "text-red-400" : "text-green-400" },
|
||
{ label: "Eindeutige IPs", wert: uniqueIps, color: "text-white" },
|
||
].map((k) => (
|
||
<div key={k.label} className="bg-[#18212f] border border-gray-800 p-4 rounded-lg">
|
||
<div className="text-xs text-slate-500 mb-1">{k.label}</div>
|
||
<div className={`text-2xl font-bold ${k.color}`}>{k.wert}</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{recentFailed >= 5 && (
|
||
<div className="bg-red-500/10 border border-red-500/30 p-4 rounded-lg">
|
||
<p className="text-sm font-semibold text-red-400">⚠️ Verdächtige Aktivität erkannt</p>
|
||
<p className="text-sm text-red-400/70 mt-1">
|
||
{recentFailed} fehlgeschlagene Login-Versuche in der letzten Stunde
|
||
</p>
|
||
</div>
|
||
)}
|
||
|
||
<div className="bg-[#18212f] border border-gray-800 rounded-lg overflow-hidden">
|
||
<div className="overflow-x-auto">
|
||
<table className="w-full text-sm">
|
||
<thead className="border-b border-gray-800">
|
||
<tr className="text-xs text-slate-500 uppercase tracking-wide">
|
||
<th className="px-4 py-3 text-left font-medium">Zeitstempel</th>
|
||
<th className="px-4 py-3 text-left font-medium">E-Mail</th>
|
||
<th className="px-4 py-3 text-left font-medium">IP-Adresse</th>
|
||
<th className="px-4 py-3 text-left font-medium">Status</th>
|
||
<th className="px-4 py-3 text-left font-medium">Grund</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody className="divide-y divide-gray-800/50">
|
||
{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-[#111925] transition-colors">
|
||
<td className="px-4 py-3 whitespace-nowrap text-slate-400">
|
||
{new Date(log.timestamp).toLocaleString("de-DE")}
|
||
</td>
|
||
<td className="px-4 py-3 font-mono text-slate-300 truncate">{log.email}</td>
|
||
<td className="px-4 py-3 font-mono text-slate-400 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-500/10 text-green-400 border border-green-500/20"
|
||
: "bg-red-500/10 text-red-400 border border-red-500/20"
|
||
}`}>
|
||
{log.success ? "✓ Erfolgreich" : "✗ Fehlgeschlagen"}
|
||
</span>
|
||
</td>
|
||
<td className="px-4 py-3 text-slate-500 text-xs">
|
||
{log.reason ? formatReason(log.reason) : "–"}
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-blue-500/10 border border-blue-500/20 p-4 rounded-lg">
|
||
<p className="text-sm text-blue-400">
|
||
<strong>Hinweis:</strong> Detaillierte Geräte-Informationen sind in der Supabase-Tabelle{" "}
|
||
<code className="bg-[#111925] px-2 py-0.5 rounded border border-blue-500/20 font-mono text-xs">
|
||
admin_audit_logs
|
||
</code>{" "}
|
||
gespeichert.
|
||
</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="min-h-screen bg-[#111925]">
|
||
<AdminNav />
|
||
<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>
|
||
</div>
|
||
);
|
||
}
|