ClauseGuard / web /app /admin /page.tsx
gaurv007's picture
Admin panel: dashboard (stats), user management (search/plan change/ban), scan viewer (delete), team manager, API key viewer (revoke), activity logs — admin: ankygaur9972@gmail.com
8ab19de verified
"use client";
import { useState, useEffect } from "react";
import Link from "next/link";
import {
LayoutDashboard, Users, ScanText, Key, Shield, ScrollText, Activity,
Search, Ban, CircleCheck, ChevronDown, ChevronUp, Trash2, Crown,
ArrowLeft, RefreshCw, TriangleAlert, UserCheck, Eye, EyeOff
} from "lucide-react";
type Tab = "overview" | "users" | "scans" | "teams" | "api-keys" | "logs";
export default function AdminPage() {
const [tab, setTab] = useState<Tab>("overview");
const [stats, setStats] = useState<any>(null);
const [users, setUsers] = useState<any[]>([]);
const [scans, setScans] = useState<any[]>([]);
const [teams, setTeams] = useState<any[]>([]);
const [apiKeys, setApiKeys] = useState<any[]>([]);
const [logs, setLogs] = useState<any[]>([]);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [userTotal, setUserTotal] = useState(0);
const [scanTotal, setScanTotal] = useState(0);
async function fetchData(action: string, params?: Record<string, string>) {
const qs = new URLSearchParams({ action, ...params });
const res = await fetch(`/api/admin?${qs}`);
if (!res.ok) { window.location.href = "/dashboard-pages/dashboard"; return null; }
return res.json();
}
async function adminAction(body: any) {
setLoading(true);
await fetch("/api/admin", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
setLoading(false);
loadTab(tab);
}
async function loadTab(t: Tab) {
setLoading(true);
switch (t) {
case "overview": { const d = await fetchData("stats"); if (d) setStats(d); break; }
case "users": { const d = await fetchData("users", search ? { search } : {}); if (d) { setUsers(d.users); setUserTotal(d.total); } break; }
case "scans": { const d = await fetchData("scans"); if (d) { setScans(d.scans); setScanTotal(d.total); } break; }
case "teams": { const d = await fetchData("teams"); if (d) setTeams(d.teams); break; }
case "api-keys": { const d = await fetchData("api-keys"); if (d) setApiKeys(d.keys); break; }
case "logs": { const d = await fetchData("logs"); if (d) setLogs(d.logs); break; }
}
setLoading(false);
}
useEffect(() => { loadTab(tab); }, [tab]);
const TABS: { key: Tab; label: string; icon: any }[] = [
{ key: "overview", label: "Overview", icon: LayoutDashboard },
{ key: "users", label: "Users", icon: Users },
{ key: "scans", label: "Scans", icon: ScanText },
{ key: "teams", label: "Teams", icon: Shield },
{ key: "api-keys", label: "API Keys", icon: Key },
{ key: "logs", label: "Activity", icon: Activity },
];
const planBadge = (plan: string) => {
const c: Record<string, string> = { free: "bg-zinc-100 text-zinc-600", pro: "bg-blue-50 text-blue-700", team: "bg-purple-50 text-purple-700" };
return <span className={`text-[11px] font-medium px-2 py-0.5 rounded ${c[plan] || c.free}`}>{plan}</span>;
};
return (
<div className="min-h-screen bg-white">
<div className="max-w-6xl mx-auto px-5 py-8">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<Link href="/dashboard-pages/dashboard" className="inline-flex items-center gap-1.5 text-sm text-zinc-400 hover:text-zinc-600 mb-2">
<ArrowLeft className="w-3.5 h-3.5" /> Dashboard
</Link>
<h1 className="text-2xl font-semibold tracking-tight flex items-center gap-2">
<Crown className="w-6 h-6 text-amber-500" />
Admin Panel
</h1>
</div>
<button onClick={() => loadTab(tab)} disabled={loading}
className="p-2 rounded-lg hover:bg-zinc-100 text-zinc-400 disabled:opacity-40">
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
</button>
</div>
{/* Tabs */}
<div className="flex gap-1 mb-6 overflow-x-auto pb-1">
{TABS.map((t) => (
<button key={t.key} onClick={() => setTab(t.key)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
tab === t.key ? "bg-zinc-900 text-white" : "text-zinc-500 hover:bg-zinc-100"
}`}>
<t.icon className="w-4 h-4" />
{t.label}
</button>
))}
</div>
{/* Overview */}
{tab === "overview" && stats && (
<div className="space-y-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{[
{ label: "Total Users", value: stats.total_users, sub: `${stats.banned_users} banned` },
{ label: "Pro Users", value: stats.pro_users },
{ label: "Team Users", value: stats.team_users },
{ label: "Free Users", value: stats.free_users },
{ label: "Total Scans", value: stats.total_scans },
{ label: "Scans Today", value: stats.scans_today },
{ label: "Teams", value: stats.total_teams },
{ label: "Active API Keys", value: stats.total_api_keys },
].map((s, i) => (
<div key={i} className="border border-zinc-200 rounded-xl p-4">
<p className="text-xs text-zinc-400">{s.label}</p>
<p className="text-2xl font-semibold mt-1">{s.value}</p>
{s.sub && <p className="text-xs text-zinc-400 mt-0.5">{s.sub}</p>}
</div>
))}
</div>
</div>
)}
{/* Users */}
{tab === "users" && (
<div>
<div className="flex gap-2 mb-4">
<div className="flex-1 relative">
<Search className="w-4 h-4 absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
<input value={search} onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && loadTab("users")}
placeholder="Search by email or name..."
className="w-full pl-10 pr-4 py-2.5 border border-zinc-200 rounded-lg text-sm focus:outline-none focus:border-zinc-400" />
</div>
<button onClick={() => loadTab("users")} className="px-4 bg-zinc-900 text-white rounded-lg text-sm font-medium hover:bg-zinc-800">Search</button>
</div>
<p className="text-xs text-zinc-400 mb-3">{userTotal} users total</p>
<div className="border border-zinc-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-50 border-b border-zinc-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-zinc-500">User</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Plan</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Scans</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Joined</th>
<th className="text-right px-4 py-3 font-medium text-zinc-500">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{users.map((u) => (
<tr key={u.id} className={`${u.is_banned ? "opacity-50 bg-red-50/30" : "hover:bg-zinc-50"}`}>
<td className="px-4 py-3">
<p className="font-medium flex items-center gap-1.5">
{u.full_name || u.email}
{u.role === "admin" && <Crown className="w-3.5 h-3.5 text-amber-500" />}
{u.is_banned && <Ban className="w-3.5 h-3.5 text-red-500" />}
</p>
<p className="text-xs text-zinc-400">{u.email}</p>
</td>
<td className="px-4 py-3">{planBadge(u.plan)}</td>
<td className="px-4 py-3 text-zinc-500">{u.analyses_this_month}/mo</td>
<td className="px-4 py-3 text-xs text-zinc-400">{new Date(u.created_at).toLocaleDateString()}</td>
<td className="px-4 py-3 text-right">
<div className="flex gap-1 justify-end">
<select defaultValue={u.plan} onChange={(e) => adminAction({ action: "update_plan", userId: u.id, plan: e.target.value })}
className="text-xs border border-zinc-200 rounded px-2 py-1 bg-white">
<option value="free">Free</option>
<option value="pro">Pro</option>
<option value="team">Team</option>
</select>
<button onClick={() => adminAction({ action: "ban_user", userId: u.id, banned: !u.is_banned })}
className={`p-1.5 rounded hover:bg-zinc-100 ${u.is_banned ? "text-emerald-500" : "text-red-400"}`}
title={u.is_banned ? "Unban" : "Ban"}>
{u.is_banned ? <CircleCheck className="w-3.5 h-3.5" /> : <Ban className="w-3.5 h-3.5" />}
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Scans */}
{tab === "scans" && (
<div>
<p className="text-xs text-zinc-400 mb-3">{scanTotal} scans total</p>
<div className="border border-zinc-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-50 border-b border-zinc-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Source</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Grade</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Risk</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Clauses</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Date</th>
<th className="text-right px-4 py-3 font-medium text-zinc-500">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{scans.map((s) => (
<tr key={s.id} className="hover:bg-zinc-50">
<td className="px-4 py-3 max-w-xs truncate text-zinc-700">{s.source_url || "Manual scan"}</td>
<td className="px-4 py-3">
<span className={`text-xs font-semibold px-2 py-0.5 rounded ${
s.grade === "F" || s.grade === "D" ? "bg-red-50 text-red-700" :
s.grade === "C" ? "bg-amber-50 text-amber-700" : "bg-emerald-50 text-emerald-700"
}`}>{s.grade}</span>
</td>
<td className="px-4 py-3 text-zinc-500">{s.risk_score}/100</td>
<td className="px-4 py-3 text-zinc-500">{s.flagged_count}/{s.total_clauses}</td>
<td className="px-4 py-3 text-xs text-zinc-400">{new Date(s.created_at).toLocaleDateString()}</td>
<td className="px-4 py-3 text-right">
<button onClick={() => adminAction({ action: "delete_scan", scanId: s.id })}
className="p-1.5 rounded hover:bg-red-50 text-red-400" title="Delete">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Teams */}
{tab === "teams" && (
<div className="border border-zinc-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-50 border-b border-zinc-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Team</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Owner</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Seats</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Created</th>
<th className="text-right px-4 py-3 font-medium text-zinc-500">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{teams.map((t) => (
<tr key={t.id} className="hover:bg-zinc-50">
<td className="px-4 py-3 font-medium">{t.name}</td>
<td className="px-4 py-3 text-xs text-zinc-400 font-mono">{t.owner_id.slice(0, 8)}...</td>
<td className="px-4 py-3 text-zinc-500">{t.max_seats}</td>
<td className="px-4 py-3 text-xs text-zinc-400">{new Date(t.created_at).toLocaleDateString()}</td>
<td className="px-4 py-3 text-right">
<button onClick={() => adminAction({ action: "delete_team", teamId: t.id })}
className="p-1.5 rounded hover:bg-red-50 text-red-400" title="Delete team">
<Trash2 className="w-3.5 h-3.5" />
</button>
</td>
</tr>
))}
{teams.length === 0 && <tr><td colSpan={5} className="px-4 py-8 text-center text-zinc-400">No teams yet.</td></tr>}
</tbody>
</table>
</div>
)}
{/* API Keys */}
{tab === "api-keys" && (
<div className="border border-zinc-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-50 border-b border-zinc-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Name</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Key</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Usage</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Status</th>
<th className="text-right px-4 py-3 font-medium text-zinc-500">Actions</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{apiKeys.map((k) => (
<tr key={k.id} className="hover:bg-zinc-50">
<td className="px-4 py-3 font-medium">{k.name}</td>
<td className="px-4 py-3 text-xs text-zinc-400 font-mono">{k.key_prefix}</td>
<td className="px-4 py-3 text-zinc-500">{k.calls_this_month}/{k.calls_limit}</td>
<td className="px-4 py-3">
<span className={`text-xs font-medium px-2 py-0.5 rounded ${k.is_active ? "bg-emerald-50 text-emerald-700" : "bg-red-50 text-red-700"}`}>
{k.is_active ? "Active" : "Revoked"}
</span>
</td>
<td className="px-4 py-3 text-right">
{k.is_active && (
<button onClick={() => adminAction({ action: "revoke_api_key", keyId: k.id })}
className="p-1.5 rounded hover:bg-red-50 text-red-400" title="Revoke">
<Ban className="w-3.5 h-3.5" />
</button>
)}
</td>
</tr>
))}
{apiKeys.length === 0 && <tr><td colSpan={5} className="px-4 py-8 text-center text-zinc-400">No API keys.</td></tr>}
</tbody>
</table>
</div>
)}
{/* Activity Logs */}
{tab === "logs" && (
<div className="border border-zinc-200 rounded-xl overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-zinc-50 border-b border-zinc-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Action</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Target</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Details</th>
<th className="text-left px-4 py-3 font-medium text-zinc-500">Time</th>
</tr>
</thead>
<tbody className="divide-y divide-zinc-100">
{logs.map((l) => (
<tr key={l.id} className="hover:bg-zinc-50">
<td className="px-4 py-3 font-medium">{l.action}</td>
<td className="px-4 py-3 text-xs text-zinc-400">{l.target_type}: {l.target_id?.slice(0, 8)}...</td>
<td className="px-4 py-3 text-xs text-zinc-400 font-mono">{l.details ? JSON.stringify(l.details) : "—"}</td>
<td className="px-4 py-3 text-xs text-zinc-400">{new Date(l.created_at).toLocaleString()}</td>
</tr>
))}
{logs.length === 0 && <tr><td colSpan={4} className="px-4 py-8 text-center text-zinc-400">No admin actions yet.</td></tr>}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}