mc-dashboard/lib/auth.ts

93 lines
2.6 KiB
TypeScript
Raw Permalink Normal View History

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>
2026-04-13 06:52:05 -06:00
import NextAuth, { type Session } from "next-auth";
import Credentials from "next-auth/providers/credentials";
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>
2026-04-13 06:52:05 -06:00
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({
name: "Admin Login",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
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>
2026-04-13 06:52:05 -06:00
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;
},
}),
],
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>
2026-04-13 06:52:05 -06:00
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",
},
session: {
strategy: "jwt",
maxAge: 24 * 60 * 60,
},
});
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>
2026-04-13 06:52:05 -06:00
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;
}