149 lines
5.8 KiB
TypeScript
149 lines
5.8 KiB
TypeScript
"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>
|
||
);
|
||
}
|