172 lines
7.1 KiB
TypeScript
172 lines
7.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { Search, Users, RefreshCw, Trash2 } from "lucide-react";
|
|
import { AdminLayout } from "@/components/admin/admin-layout";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { useLanguage } from "@/components/language-provider";
|
|
|
|
interface User {
|
|
user_id: string;
|
|
email: string;
|
|
full_name: string;
|
|
status: string;
|
|
is_active: boolean;
|
|
is_staff: boolean;
|
|
mfa_enabled: boolean;
|
|
trust_state?: string;
|
|
trust_score?: number;
|
|
last_login_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export default function AdminUsersPage() {
|
|
const { lang, dir } = useLanguage();
|
|
const [users, setUsers] = useState<User[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [search, setSearch] = useState("");
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem("access_token");
|
|
if (!token) {
|
|
window.location.href = "/login?redirect=/admin/users";
|
|
return;
|
|
}
|
|
|
|
const fetchData = async () => {
|
|
const baseURL = process.env.NEXT_PUBLIC_API_URL;
|
|
const qs = new URLSearchParams();
|
|
if (search) qs.set("search", search);
|
|
|
|
try {
|
|
const res = await fetch(`${baseURL}/users/?${qs.toString()}`, {
|
|
headers: { Authorization: `Bearer ${token}` },
|
|
});
|
|
const data = await res.json();
|
|
setUsers(data.results || []);
|
|
} catch (e) {
|
|
console.error(e);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const timer = setTimeout(fetchData, search ? 500 : 0);
|
|
return () => clearTimeout(timer);
|
|
}, [search]);
|
|
|
|
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">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xl font-bold text-white">{t("کاربران", "Users")}</h2>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div className="relative flex-1">
|
|
<Search className={`absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-500 ${dir === "rtl" ? "left-3 right-auto" : ""}`} />
|
|
<input
|
|
type="text"
|
|
placeholder={t("جستجو در کاربران...", "Search users...")}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full pl-12 pr-4 py-2.5 rounded-lg border border-white/10 bg-white/5 text-white focus:outline-none focus:ring-2 focus:ring-brand-500/50"
|
|
dir={dir}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<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("کاربر", "User")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("وضعیت", "Status")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("اعتماد", "Trust")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("امنیت", "Security")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("آخرین ورود", "Last Login")}</th>
|
|
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("اقدامات", "Actions")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{users.map((u) => (
|
|
<tr key={u.user_id} className="border-b border-white/5">
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className="flex h-9 w-9 items-center justify-center rounded-full bg-brand-gradient text-xs font-bold text-white">
|
|
{u.full_name?.charAt(0) || u.email?.charAt(0) || "?"}
|
|
</span>
|
|
<div>
|
|
<div className="font-medium text-white">{u.full_name || u.email}</div>
|
|
<div className="text-xs text-slate-500">{u.email}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<Badge variant={u.is_active ? "default" : "secondary"} className="text-xs">
|
|
{u.status}
|
|
</Badge>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
{u.trust_score !== undefined && (
|
|
<span className="text-xs font-medium text-white bg-slate-800/50 rounded px-1.5 py-0.5">
|
|
{u.trust_score}/100
|
|
</span>
|
|
)}
|
|
{u.trust_state && (
|
|
<span className={`text-xs px-1.5 py-0.5 rounded ${
|
|
u.trust_state === "strong" ? "bg-emerald-500/15 text-emerald-400" :
|
|
u.trust_state === "verified" ? "bg-amber-500/15 text-amber-400" :
|
|
"bg-slate-500/15 text-slate-400"
|
|
}`}>
|
|
{lang === "fa"
|
|
? (u.trust_state === "basic" ? "پایه" : u.trust_state === "verified" ? "تأیید شده" : "قوی")
|
|
: (u.trust_state.charAt(0).toUpperCase() + u.trust_state.slice(1))
|
|
}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-xs text-slate-400">MFA:</span>
|
|
<Badge variant={u.mfa_enabled ? "default" : "secondary"} className="text-xs">
|
|
{u.mfa_enabled ? t("فعال", "On") : t("خاموش", "Off")}
|
|
</Badge>
|
|
{u.is_staff && (
|
|
<Badge className="text-xs bg-brand-500/15 text-brand-200">Admin</Badge>
|
|
)}
|
|
</div>
|
|
</td>
|
|
<td className="px-4 py-3 text-slate-400">
|
|
{u.last_login_at ? new Date(u.last_login_at).toLocaleString(lang === "fa" ? "fa-IR" : "en-US") : t("هرگز", "Never")}
|
|
</td>
|
|
<td className="px-4 py-3">
|
|
<Button variant="ghost" size="sm">
|
|
<Trash2 className="h-4 w-4 text-rose-400" />
|
|
</Button>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</AdminLayout>
|
|
);
|
|
}
|