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

149 lines
5.8 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 { Search, Shield, RefreshCw, Copy, Eye, EyeOff } 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 App {
id: string;
name: string;
client_id: string;
client_type: string;
redirect_uris: string[];
grant_types: string[];
scopes: string[];
is_active: boolean;
created_at: string;
}
export default function AdminApplicationsPage() {
const { lang, dir } = useLanguage();
const [apps, setApps] = useState<App[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [showSecret, setShowSecret] = useState<string | null>(null);
useEffect(() => {
const token = localStorage.getItem("access_token");
if (!token) {
window.location.href = "/login?redirect=/admin/applications";
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}/applications/?${qs.toString()}`, {
headers: { Authorization: `Bearer ${token}` },
});
const data = await res.json();
setApps(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);
const copyToClipboard = (text: string) => navigator.clipboard.writeText(text);
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("اپلیکیشن\u200cها", "Applications")}</h2>
<Button size="sm">{t("ثبت اپلیکیشن", "Register Application")}</Button>
</div>
<div className="relative flex-1 max-w-md">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-500" />
<input
type="text"
placeholder={t("جستجو در اپلیکیشن‌ها...", "Search applications...")}
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 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("نام", "Name")}</th>
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("Client ID", "Client ID")}</th>
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("نوع", "Type")}</th>
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("اسکوپ\u200cها", "Scopes")}</th>
<th className="px-4 py-3 text-start text-xs font-medium text-slate-400">{t("وضعیت", "Status")}</th>
</tr>
</thead>
<tbody>
{apps.map((app) => (
<tr key={app.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-lg bg-brand-gradient/10">
<Shield className="h-5 w-5 text-brand-300" />
</span>
<span className="font-medium text-white">{app.name}</span>
</div>
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-2 font-mono text-xs">
<code className="text-slate-400">{app.client_id.slice(0, 16)}...</code>
<button
onClick={() => copyToClipboard(app.client_id)}
className="opacity-50 hover:opacity-100"
title="Copy"
>
<Copy className="h-3 w-3 text-slate-500" />
</button>
</div>
</td>
<td className="px-4 py-3 text-slate-300 capitalize">{app.client_type}</td>
<td className="px-4 py-3">
<div className="flex flex-wrap gap-1">
{app.scopes?.slice(0, 4).map((s) => (
<Badge key={s} variant="secondary" className="text-xs">
{s}
</Badge>
))}
</div>
</td>
<td className="px-4 py-3">
<Badge variant={app.is_active ? "default" : "secondary"} className="text-xs">
{app.is_active ? t("فعال", "Active") : t("غیرفعال", "Inactive")}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</AdminLayout>
);
}