MBO-Tech-IT-Webseite/app/admin/audit-logs/page.tsx

164 lines
6.4 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.

This file contains Unicode characters that might be confused with other characters. 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 { 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>
);
}