127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, ShieldAlert } from "lucide-react";
|
|
import { AdminLayout } from "@/components/admin/admin-layout";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useLanguage } from "@/components/language-provider";
|
|
|
|
interface Event {
|
|
id: string;
|
|
event_type: string;
|
|
severity: string;
|
|
ip_address: string;
|
|
user_agent: string;
|
|
user?: { email: string };
|
|
metadata: Record<string, any>;
|
|
status: string;
|
|
created_at: string;
|
|
}
|
|
|
|
const SEVERITY_COLORS: Record<string, string> = {
|
|
low: "bg-slate-500/15 text-slate-300",
|
|
medium: "bg-amber-500/15 text-amber-300",
|
|
high: "bg-rose-500/15 text-rose-300",
|
|
critical: "bg-rose-600/20 text-rose-200",
|
|
};
|
|
|
|
const SEVERITY_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
low: ShieldAlert,
|
|
medium: AlertTriangle,
|
|
high: AlertTriangle,
|
|
critical: XCircle,
|
|
};
|
|
|
|
export default function AdminEventsPage() {
|
|
const { lang } = useLanguage();
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("access_token");
|
|
if (!token) {
|
|
window.location.href = "/login?redirect=/admin/events";
|
|
return;
|
|
}
|
|
|
|
const fetchData = async () => {
|
|
const baseURL = process.env.NEXT_PUBLIC_API_URL;
|
|
try {
|
|
const res = await fetch(`${baseURL}/security/events/`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
const data = await res.json();
|
|
setEvents(data.results || []);
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchData();
|
|
}, []);
|
|
|
|
const t = (fa: string, en: string) => (lang === "fa" ? fa : en);
|
|
|
|
if (loading) {
|
|
return (
|
|
<AdminLayout>
|
|
<div className="flex items-center justify-center h-64">
|
|
<RefreshCw className="h-8 w-8 animate-spin text-brand-400" />
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<AdminLayout>
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-bold text-white">{t("رویدادهای امنیتی", "Security Events")}</h2>
|
|
|
|
<div className="overflow-x-auto rounded-xl border border-white/10">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-white/10 bg-white/[0.02]">
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("زمان", "Time")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("رویداد", "Event")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("کاربر", "User")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("آی\u200cپی", "IP")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("شدت", "Severity")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{events.map((e) => {
|
|
const Icon = SEVERITY_ICONS[e.severity] || ShieldAlert;
|
|
return (
|
|
<tr key={e.id} className="border-b border-white/5">
|
|
<td className="px-4 py-3 text-slate-400">
|
|
{new Date(e.created_at).toLocaleString(lang === "fa" ? "fa-IR" : "en-US")}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<code className="text-xs text-slate-300">{e.event_type}</code>
|
|
</td>
|
|
<td className="px-4 py-3 text-slate-300">{e.user?.email || "—"}</td>
|
|
<td className="px-4 py-3 text-slate-400 font-mono">{e.ip_address}</td>
|
|
<td className="px-4 py-3">
|
|
<Badge className={`text-xs ${SEVERITY_COLORS[e.severity] || "bg-slate-500/15 text-slate-300"}`}>
|
|
<Icon className="h-3 w-3 mr-1" />
|
|
{e.severity}
|
|
</Badge>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
{events.length === 0 && !loading && (
|
|
<div className="py-12 text-center text-slate-500">
|
|
{t("رویدادی یافت نشد", "No events found")}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|