Plain App Router admin (aligned to habbo-next, no Refine): - rank surfaced on the NextAuth session; staff guard isStaff() [pure, unit-tested] + requireStaff() reading min_staff_rank, gating /admin. - /admin dashboard (counts), /admin/users (paginated + search), /admin/users/[id] detail. - src/actions/admin-users.ts: staff-gated server actions wiring the user editor to the existing services — giveCurrency (RCON or DB fallback), setMotto/setRank (DB + RCON), alertUser, disconnectUser. Verified: tsc exit 0, vitest 45/45, next build exit 0 (/admin routes).
34 lines
824 B
TypeScript
34 lines
824 B
TypeScript
import { prisma } from "@/lib/prisma";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
|
|
export default async function AdminDashboard() {
|
|
const [users, online, articles] = await Promise.all([
|
|
prisma.user.count(),
|
|
prisma.user.count({ where: { online: "1" } }),
|
|
prisma.websiteArticles.count(),
|
|
]);
|
|
|
|
const stats = [
|
|
{ label: "Users", value: users },
|
|
{ label: "Online now", value: online },
|
|
{ label: "Articles", value: articles },
|
|
];
|
|
|
|
return (
|
|
<main>
|
|
<h1>Dashboard</h1>
|
|
<div className="grid cols-2">
|
|
{stats.map((s) => (
|
|
<div key={s.label} className="card">
|
|
<h2 style={{ margin: 0 }}>{s.value}</h2>
|
|
<p className="muted" style={{ margin: 0 }}>
|
|
{s.label}
|
|
</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|