NextAuth.js v4 authentication and RBAC patterns for MeritGrid. Covers JWT sessions, role-based access control (STUDENT/TALENT/ORG/ADMIN), Prisma adapter, and Google OAuth setup.
next-auth ^4.24.13@next-auth/prisma-adapter ^1.0.7lib/auth.tsSTUDENT, TALENT, ORG, ADMINThe authOptions object is defined in lib/auth.ts and exported for use everywhere:
// lib/auth.ts
import { NextAuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GoogleProvider from "next-auth/providers/google";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { prisma } from "@/lib/db/postgresql";
import bcrypt from "bcryptjs";
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma as any), // any needed due to Prisma 7 type mismatch
session: { strategy: "jwt" },
secret: process.env.SUPABASE_JWT_SECRET,
pages: { signIn: "/student/login" },
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
}),
CredentialsProvider({ /* ... email + bcrypt flow */ }),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = (user as any).role;
token.id = user.id as string;
}
return token;
},
async session({ session, token }) {
if (session.user) {
(session.user as { role: string }).role = token.role as string;
(session.user as { id: string }).id = token.id as string;
}
return session;
},
},
};
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
import { redirect } from "next/navigation";
export default async function StudentDashboard() {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "STUDENT") redirect("/student/login");
// ...
}
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function POST(req: Request) {
const session = await getServerSession(authOptions);
if (!session || session.user.role !== "TALENT") {
return Response.json({ success: false, error: { code: "FORBIDDEN", message: "Unauthorized" } }, { status: 403 });
}
// ...
}
"use server";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth";
export async function sensitiveAction() {
const session = await getServerSession(authOptions);
if (!session) throw new Error("UNAUTHORIZED");
if (session.user.role !== "ADMIN") throw new Error("FORBIDDEN");
// ...
}
| Feature | STUDENT | TALENT | ORG | ADMIN |
|---|---|---|---|---|
| View scholarships | ✅ | ✅ | ✅ | ✅ |
| Apply for scholarships | ✅ | ❌ | ❌ | ✅ |
| Post scholarships/jobs | ❌ | ✅ | ✅ | ✅ |
| View all applications | ❌ | ✅ | ✅ | ✅ |
| Admin panel access | ❌ | ❌ | ❌ | ✅ |
In NODE_ENV=development, role checks can be mocked:
const isDev = process.env.NODE_ENV === "development";
if (!isDev && session.user.role !== "ADMIN") throw new Error("FORBIDDEN");
Always write production check first, then wrap with dev bypass.
Extend NextAuth types to include role and id:
// types/next-auth.d.ts
import "next-auth";
declare module "next-auth" {
interface User {
role: string;
id: string;
}
interface Session {
user: User;
}
}
declare module "next-auth/jwt" {
interface JWT {
role: string;
id: string;
}
}
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=<generate with openssl rand -base64 32>
SUPABASE_JWT_SECRET=<from Supabase dashboard>
GOOGLE_CLIENT_ID=<from Google Cloud console>
GOOGLE_CLIENT_SECRET=<from Google Cloud console>