Role-based access: admin vs superadmin
- Credentials provider now resolves two distinct accounts from env:
SUPERADMIN_USERNAME / SUPERADMIN_PASSWORD → role "superadmin"
ADMIN_USERNAME / ADMIN_PASSWORD → role "admin"
The role is carried through the JWT and Session callbacks so the UI
and API can gate on it. Types extended via types/next-auth.d.ts.
- lib/auth.ts exports requireRole(minRole) and sessionRole(session).
- /api/players POST rejects "op" and "deop" with 403 unless the caller
is superadmin. All other player actions (whitelist add/remove, ban,
pardon) remain available to both roles.
- lib/use-role.ts (client hook) exposes role / isSuperadmin / authed
for UI gating without duplicating session typing.
- PlayerManager: Add-OP button and per-row Deop action are hidden or
disabled for non-superadmin; when a regular admin is viewing the
Ops tab, a banner explains the read-only state.
- PlayerDrawer: Make Op / Deop button disabled with tooltip for
non-superadmin; whitelist, ban, pardon unchanged.
- Navbar: subtle role pill next to the user name ("Super" for
superadmin amber-tinted, "Admin" for admin).
- Migration note: the existing ADMIN_* credentials now log in as the
restricted admin role. Set SUPERADMIN_USERNAME + SUPERADMIN_PASSWORD
in .env.local to retain operator-management ability. A placeholder
superadmin account was generated in .env.local; the password is in
the commit terminal output only, not the repo.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a69dc9243
commit
1ac5513d2c
7 changed files with 151 additions and 18 deletions
74
lib/auth.ts
74
lib/auth.ts
|
|
@ -1,6 +1,31 @@
|
|||
import NextAuth from "next-auth";
|
||||
import NextAuth, { type Session } from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
|
||||
export type Role = "admin" | "superadmin";
|
||||
|
||||
type AccountEntry = { username: string; password: string; role: Role; id: string };
|
||||
|
||||
function loadAccounts(): AccountEntry[] {
|
||||
const accounts: AccountEntry[] = [];
|
||||
if (process.env.SUPERADMIN_USERNAME && process.env.SUPERADMIN_PASSWORD) {
|
||||
accounts.push({
|
||||
id: "superadmin",
|
||||
username: process.env.SUPERADMIN_USERNAME,
|
||||
password: process.env.SUPERADMIN_PASSWORD,
|
||||
role: "superadmin",
|
||||
});
|
||||
}
|
||||
if (process.env.ADMIN_USERNAME && process.env.ADMIN_PASSWORD) {
|
||||
accounts.push({
|
||||
id: "admin",
|
||||
username: process.env.ADMIN_USERNAME,
|
||||
password: process.env.ADMIN_PASSWORD,
|
||||
role: "admin",
|
||||
});
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [
|
||||
Credentials({
|
||||
|
|
@ -10,16 +35,39 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
if (
|
||||
credentials?.username === process.env.ADMIN_USERNAME &&
|
||||
credentials?.password === process.env.ADMIN_PASSWORD
|
||||
) {
|
||||
return { id: "1", name: "Admin" };
|
||||
const username = credentials?.username;
|
||||
const password = credentials?.password;
|
||||
if (typeof username !== "string" || typeof password !== "string") {
|
||||
return null;
|
||||
}
|
||||
for (const acc of loadAccounts()) {
|
||||
if (acc.username === username && acc.password === password) {
|
||||
return {
|
||||
id: acc.id,
|
||||
name: acc.username,
|
||||
role: acc.role,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
async jwt({ token, user }) {
|
||||
if (user && "role" in user) {
|
||||
token.role = (user as { role?: Role }).role;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (session.user && token.role) {
|
||||
(session.user as Session["user"] & { role?: Role }).role =
|
||||
token.role as Role;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
},
|
||||
|
|
@ -28,3 +76,17 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|||
maxAge: 24 * 60 * 60,
|
||||
},
|
||||
});
|
||||
|
||||
export async function requireRole(minRole: Role): Promise<Session | null> {
|
||||
const session = await auth();
|
||||
if (!session) return null;
|
||||
const role = (session.user as { role?: Role } | undefined)?.role;
|
||||
if (!role) return null;
|
||||
if (minRole === "superadmin" && role !== "superadmin") return null;
|
||||
return session;
|
||||
}
|
||||
|
||||
export function sessionRole(session: Session | null | undefined): Role | null {
|
||||
if (!session?.user) return null;
|
||||
return ((session.user as { role?: Role }).role as Role) || null;
|
||||
}
|
||||
|
|
|
|||
16
lib/use-role.ts
Normal file
16
lib/use-role.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
export type Role = "admin" | "superadmin";
|
||||
|
||||
export function useRole(): { role: Role | null; isSuperadmin: boolean; authed: boolean } {
|
||||
const { data: session, status } = useSession();
|
||||
const role =
|
||||
(session?.user as { role?: Role } | undefined)?.role ?? null;
|
||||
return {
|
||||
role,
|
||||
isSuperadmin: role === "superadmin",
|
||||
authed: status === "authenticated" && !!session,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue