334 lines
14 KiB
TypeScript
334 lines
14 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import {
|
||
Users,
|
||
Building2,
|
||
Shield,
|
||
Activity,
|
||
RefreshCw,
|
||
Lock,
|
||
Calendar,
|
||
Mail,
|
||
Phone,
|
||
} from "lucide-react";
|
||
import { AdminLayout } from "@/components/admin/admin-layout";
|
||
import { useLanguage } from "@/components/language-provider";
|
||
import { apiFetch } from "@/lib/api";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Button } from "@/components/ui/button";
|
||
|
||
interface ApiResult {
|
||
count: number;
|
||
results: any[];
|
||
}
|
||
|
||
interface NavItem {
|
||
id: string;
|
||
label: string;
|
||
icon: React.ComponentType<{ className?: string }>;
|
||
href: string;
|
||
}
|
||
|
||
export default function AdminDashboardPage() {
|
||
const { t, lang } = useLanguage();
|
||
const [activeSessions, setActiveSessions] = useState<number>(0);
|
||
const [stats, setStats] = useState<ApiResult | null>(null);
|
||
const [sessions, setSessions] = useState<any[]>([]);
|
||
const [events, setEvents] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const token = localStorage.getItem("access_token");
|
||
if (!token) {
|
||
window.location.href = "/login?redirect=/admin";
|
||
return;
|
||
}
|
||
|
||
const fetchData = async () => {
|
||
try {
|
||
const [usersRes, orgsRes, appsRes, sessionsRes, eventsRes] =
|
||
await Promise.all([
|
||
apiFetch("/users/"),
|
||
apiFetch("/organizations/"),
|
||
apiFetch("/applications/"),
|
||
apiFetch("/sessions/"),
|
||
apiFetch("/security/events/"),
|
||
]);
|
||
|
||
const usersData = await usersRes.json();
|
||
const orgsData = await orgsRes.json();
|
||
const appsData = await appsRes.json();
|
||
const sessionsData = await sessionsRes.json();
|
||
const eventsData = await eventsRes.json();
|
||
|
||
setStats({
|
||
count: 4,
|
||
results: [
|
||
{ name: "users", count: usersData.count || 0 },
|
||
{ name: "organizations", count: orgsData.count || 0 },
|
||
{ name: "applications", count: appsData.count || 0 },
|
||
{ name: "sessions", count: sessionsData.count || 0 },
|
||
],
|
||
});
|
||
|
||
const sessionsList = sessionsData.results || [];
|
||
setSessions(sessionsList);
|
||
const active = sessionsList.filter(
|
||
(s: any) => s.status === "active"
|
||
).length;
|
||
setActiveSessions(active);
|
||
setEvents(eventsData.results || []);
|
||
} catch (e) {
|
||
console.error("Admin fetch error:", e);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
fetchData();
|
||
}, []);
|
||
|
||
const navItems: NavItem[] = [
|
||
{ id: "home", label: t.admin.dashboard, icon: Users, href: "/admin" },
|
||
{ id: "users", label: t.admin.users, icon: Users, href: "/admin/users" },
|
||
{ id: "organizations", label: t.admin.orgs as string, icon: Building2, href: "/admin/orgs" },
|
||
{ id: "applications", label: t.admin.apps as string, icon: Shield, href: "/admin/apps" },
|
||
{ id: "security", label: t.admin.security as string, icon: Lock, href: "/admin/security" },
|
||
{ id: "audit", label: t.admin.auditLog, icon: Calendar, href: "/admin/audit" },
|
||
];
|
||
|
||
const cards = [
|
||
{ name: "users", labelFa: "کاربران", labelEn: "Users", icon: Users, color: "text-blue-400", bg: "bg-blue-500/10" },
|
||
{ name: "organizations", labelFa: "سازمان\u200cها", labelEn: "Organizations", icon: Building2, color: "text-emerald-400", bg: "bg-emerald-500/10" },
|
||
{ name: "applications", labelFa: "اپلیکیشن\u200cها", labelEn: "Applications", icon: Shield, color: "text-purple-400", bg: "bg-purple-500/10" },
|
||
{ name: "sessions", labelFa: "نشست\u200cها", labelEn: "Sessions", icon: Activity, color: "text-amber-400", bg: "bg-amber-500/10" },
|
||
];
|
||
|
||
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 className="min-h-screen">
|
||
{/* Left Sidebar */}
|
||
<aside className="w-64 bg-ink/95 border-r border-white/10 flex flex-col h-screen">
|
||
<div className="p-6 border-b border-white/10">
|
||
<h2 className="text-sm font-semibold text-slate-400 tracking-wider">
|
||
{t.admin.welcomeBack}
|
||
</h2>
|
||
</div>
|
||
<nav className="flex-1 pt-2">
|
||
<ul className="space-y-1 px-2">
|
||
{navItems.map((item) => (
|
||
<li key={item.id}>
|
||
<Button
|
||
asChild
|
||
variant="ghost"
|
||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-slate-300 hover:text-white hover:bg-white/5 transition hover:-translate-x-1"
|
||
>
|
||
<item.icon className="h-4 w-4" />
|
||
<span className="line-clamp-1">{item.label}</span>
|
||
</Button>
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</nav>
|
||
<div className="p-6 border-t border-white/10 mt-auto">
|
||
<div className="w-8 h-8 rounded-full bg-brand-gradient flex items-center justify-center text-white text-sm font-bold">
|
||
fa
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
{/* Main Content */}
|
||
<main className="flex-1 p-6 sm:p-8 overflow-y-auto">
|
||
{/* Header / Quick Stats */}
|
||
<div className="bg-ink/80 rounded-2xl border border-white/10 p-6 mb-8 sm:mb-12 max-w-7xl">
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{cards.map((card) => {
|
||
const value = stats?.results.find((r) => r.name === card.name)?.count || 0;
|
||
return (
|
||
<div
|
||
key={card.name}
|
||
className="rounded-2xl border border-white/10 bg-white/5 p-4 flex flex-col items-start rounded-xl hover:bg-white/10 transition hover:-translate-y-0.5"
|
||
>
|
||
<div className="text-3xl font-bold text-white mb-1">{value}</div>
|
||
<div className="text-xs text-slate-400">{card.labelEn}</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Quick Action Cards */}
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-8 sm:mb-12">
|
||
<div
|
||
className="rounded-2xl border border-white/10 bg-white/5 p-6 flex flex-col items-start rounded-xl hover:bg-white/10 transition"
|
||
>
|
||
<div className="w-12 h-12 rounded-xl bg-blue-500/15 flex items-center justify-center mb-4">
|
||
<Users className="h-6 w-6 text-blue-300" />
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-white mb-2">Manage Users</h3>
|
||
<p className="text-slate-400 text-sm">{t.admin.manageUsers}</p>
|
||
<Button size="sm" variant="ghost">
|
||
{t.admin.viewAll}
|
||
</Button>
|
||
</div>
|
||
|
||
<div
|
||
className="rounded-2xl border border-white/10 bg-white/5 p-6 flex flex-col items-start rounded-xl hover:bg-white/10 transition"
|
||
>
|
||
<div className="w-12 h-12 rounded-emerald-500/15 flex items-center justify-center mb-4">
|
||
<Building2 className="h-6 w-6 text-emerald-300" />
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-white mb-2">Organizations</h3>
|
||
<p className="text-slate-400 text-sm">{t.admin.manageOrgs}</p>
|
||
<Button size="sm" variant="ghost">
|
||
{t.admin.viewAll}
|
||
</Button>
|
||
</div>
|
||
|
||
<div
|
||
className="rounded-2xl border border-white/10 bg-white/5 p-6 flex flex-col items-start rounded-xl hover:bg-white/10 transition"
|
||
>
|
||
<div className="w-12 h-12 rounded-purple-500/15 flex items-center justify-center mb-4">
|
||
<Shield className="h-6 w-6 text-purple-300" />
|
||
</div>
|
||
<h3 className="text-lg font-semibold text-white mb-2">Security</h3>
|
||
<p className="text-slate-400 text-sm">{t.admin.securitySettings}</p>
|
||
<Button size="sm" variant="ghost">
|
||
{t.admin.viewAll}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Active Sessions Section */}
|
||
<section className="bg-ink/80 rounded-2xl border border-white/10 p-6 sm:p-8 max-w-2xl">
|
||
<div className="flex items-between justify-between mb-4">
|
||
<div>
|
||
<h2 className="text-xl font-semibold text-white">
|
||
{t.dashboard.activeSessions!}
|
||
</h2>
|
||
<p className="text-slate-400 text-sm">{activeSessions} {t.dashboard.of as string} {stats?.results.find((r) => r.name === "sessions")?.count || 0} {t.dashboard.totalSessions as string}</p>
|
||
</div>
|
||
<Button size="sm" variant="ghost">
|
||
{t.dashboard.manageSessions}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="overflow-x-auto">
|
||
<table className="min-w-full rounded-lg border border-white/10">
|
||
<thead>
|
||
<tr className="border-b border-white/10">
|
||
<th className="text-left text-xs font-medium text-slate-400 p-4">User</th>
|
||
<th className="text-left text-xs font-medium text-slate-400 p-4">App</th>
|
||
<th className="text-left text-xs font-medium text-slate-400 p-4">Location</th>
|
||
<th className="text-left text-xs font-medium text-slate-400 p-4">Status</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{sessions.slice(0, 5).length === 0 ? (
|
||
<tr>
|
||
<td colSpan={4} className="p-4 text-center text-sm text-slate-500">
|
||
{lang === "fa" ? "نشست فعالی یافت نشد" : "No active sessions found"}
|
||
</td>
|
||
</tr>
|
||
) : (
|
||
sessions.slice(0, 5).map((r: any, i: number) => (
|
||
<tr key={r.id || i} className="border-b border-white/10">
|
||
<td className="p-4 font-medium text-white">
|
||
<div className="flex items-center gap-3">
|
||
<div className="h-7 w-7 rounded-full bg-brand-gradient text-[10px] font-bold text-white">
|
||
{(r.user_email || "?").charAt(0).toUpperCase()}
|
||
</div>
|
||
<div className="text-slate-200">
|
||
{r.user_email || (lang === "fa" ? "کاربر ناشناس" : "Unknown user")}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td className="p-4 text-slate-400">{r.application_name || "—"}</td>
|
||
<td className="p-4 text-slate-400">{r.ip_address || "—"}</td>
|
||
<td className="p-4">
|
||
<span
|
||
className={
|
||
"rounded-full px-2 py-0.5 text-[10px] " +
|
||
(r.status === "active"
|
||
? "bg-emerald-500/15 text-emerald-300"
|
||
: "bg-white/10 text-slate-400")
|
||
}
|
||
>
|
||
{r.status === "active"
|
||
? lang === "fa"
|
||
? "فعال"
|
||
: "Active"
|
||
: r.status === "revoked"
|
||
? lang === "fa"
|
||
? "لغو شده"
|
||
: "Revoked"
|
||
: lang === "fa"
|
||
? "منقضی"
|
||
: "Expired"}
|
||
</span>
|
||
</td>
|
||
</tr>
|
||
))
|
||
)}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
{/* Recent Security Events */}
|
||
<section className="bg-ink/80 rounded-2xl border border-white/10 p-6 sm:p-8 max-w-2xl">
|
||
<div className="flex items-between justify-between mb-4">
|
||
<h2 className="text-xl font-semibold text-white">
|
||
{t.dashboard.recentEvents}
|
||
</h2>
|
||
<Button size="sm" variant="ghost">
|
||
{t.dashboard.viewAll}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
{events.length === 0 ? (
|
||
<p className="py-4 text-center text-sm text-slate-500">
|
||
{lang === "fa" ? "رخداد امنیتی یافت نشد" : "No security events"}
|
||
</p>
|
||
) : (
|
||
events.slice(0, 6).map((r: any, i: number) => (
|
||
<div key={r.id || i} className="px-3 py-2 rounded-lg bg-ink/60 text-sm">
|
||
<div className="flex items-center gap-3 mb-1">
|
||
<span
|
||
className="flex h-7 w-7 items-center justify-center rounded-full bg-brand-gradient text-[10px] font-bold text-white"
|
||
>
|
||
{(r.user_email || "?").charAt(0).toUpperCase()}
|
||
</span>
|
||
<div>
|
||
<div className="text-slate-200">{r.event_type}</div>
|
||
<div className="text-[10px] text-slate-500">
|
||
{r.occurred_at
|
||
? new Date(r.occurred_at).toLocaleString(lang === "fa" ? "fa-IR" : "en-US")
|
||
: ""}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div className="flex items-center justify-between text-slate-500">
|
||
<span>{r.user_email || (lang === "fa" ? "ناشناس" : "anonymous")}</span>
|
||
<span>{r.severity || r.status || ""}</span>
|
||
</div>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
</section>
|
||
</main>
|
||
</AdminLayout>
|
||
);
|
||
} |