gh_UserManager/apps/web/app/admin/page.tsx
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

334 lines
14 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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.

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