Add admin foundation + Users resource (Filament replacement, slice 1)
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).
This commit is contained in:
1 parent
d9a8ce532f
commit
9f81096f05
10 files changed
+334
-3
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { isStaff } from "@/lib/admin/is-staff";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { siteSettings } from "@/lib/services/site-settings";
|
||||
|
||||
export { isStaff };
|
||||
|
||||
export async function getMinStaffRank(): Promise<number> {
|
||||
const n = Number(await siteSettings.get("min_staff_rank", "7"));
|
||||
return Number.isFinite(n) ? n : 7;
|
||||
}
|
||||
|
||||
export interface StaffUser {
|
||||
id: number;
|
||||
rank: number;
|
||||
username: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gate for admin pages and actions: redirects to /login when unauthenticated
|
||||
* and to / when authenticated but not staff. Returns the staff user otherwise.
|
||||
*/
|
||||
export async function requireStaff(): Promise<StaffUser> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/login");
|
||||
const minRank = await getMinStaffRank();
|
||||
if (!isStaff(session.user.rank, minRank)) redirect("/");
|
||||
return {
|
||||
id: Number(session.user.id),
|
||||
rank: session.user.rank,
|
||||
username: session.user.name ?? "",
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isStaff } from "./is-staff";
|
||||
|
||||
describe("isStaff", () => {
|
||||
it("is true at or above the min staff rank", () => {
|
||||
expect(isStaff(7, 7)).toBe(true);
|
||||
expect(isStaff(10, 7)).toBe(true);
|
||||
});
|
||||
|
||||
it("is false below the min staff rank", () => {
|
||||
expect(isStaff(6, 7)).toBe(false);
|
||||
expect(isStaff(1, 7)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
/** AtomCMS housekeeping gate: staff are users with rank >= min_staff_rank. */
|
||||
export function isStaff(rank: number, minStaffRank: number): boolean {
|
||||
return rank >= minStaffRank;
|
||||
}
|
||||
+7
-2
@@ -35,14 +35,19 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
});
|
||||
}
|
||||
|
||||
return { id: String(user.id), name: user.username };
|
||||
return { id: String(user.id), name: user.username, rank: user.rank };
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
// NextAuth stores the user id in token.sub automatically; surface it on the session.
|
||||
jwt({ token, user }) {
|
||||
if (user) token.rank = (user as { rank?: number }).rank;
|
||||
return token;
|
||||
},
|
||||
// NextAuth stores the user id in token.sub automatically; surface id + rank.
|
||||
session({ session, token }) {
|
||||
if (token.sub && session.user) session.user.id = token.sub;
|
||||
if (typeof token.rank === "number" && session.user) session.user.rank = token.rank;
|
||||
return session;
|
||||
},
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user