feat: implement stateless authentication using NextAuth with demo users#4
feat: implement stateless authentication using NextAuth with demo users#4Monish-Parmar10 wants to merge 4 commits intoIPS-TECH-CLUB:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds stateless authentication to the Next.js App Router using NextAuth with JWT sessions, including demo users/roles and middleware-based route protection.
Changes:
- Added NextAuth configuration (credentials + OAuth providers), JWT/session role propagation, and server-side auth utilities.
- Introduced middleware-based access control for
/dashboardand/admin-style routes. - Updated project/tooling config (TS path baseUrl, dependencies, lint-staged rules) and added initial docs.
Reviewed changes
Copilot reviewed 10 out of 13 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds baseUrl to support path alias resolution. |
| src/types/next-auth.d.ts | Augments NextAuth Session/JWT types to include id, role, provider. |
| src/middleware.ts | Adds auth/admin route protection and redirects via withAuth. |
| src/lib/auth/auth.utils.ts | Adds server-side helpers for session/user/role checks. |
| src/lib/auth/auth.config.ts | Adds NextAuth options with credentials + Google/GitHub providers and JWT/session callbacks. |
| src/lib/auth/AuthProvider.tsx | Adds a client SessionProvider wrapper for App Router usage. |
| src/app/layout.tsx | Wraps the app with AuthProvider to provide session context. |
| src/app/api/auth/[...nextauth]/route.ts | Adds the NextAuth route handler for App Router. |
| src/app/api/Auth/route.ts | Removes an unused/empty API route. |
| package.json | Adds NextAuth (and other deps) required for auth flow/tooling. |
| lint-staged.config.js | Changes lint-staged to run ESLint + Prettier for JS/TS files. |
| docs/overview.mdx | Adds initial project overview documentation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| ); | ||
|
|
||
| export const config = { | ||
| matcher: ['/((?!api/auth|api/health|_next/static|_next/image|favicon.ico|public).*)'], |
There was a problem hiding this comment.
config.matcher does not exclude requests for files served from /public (e.g. /logo.webp, /next.svg), because those assets are requested from the root path rather than /public/.... This causes the auth middleware to run on static asset requests unnecessarily. Consider excluding common static file extensions (e.g. .*\.(png|jpg|jpeg|webp|svg|ico)$) or explicitly excluding known asset paths.
| matcher: ['/((?!api/auth|api/health|_next/static|_next/image|favicon.ico|public).*)'], | |
| matcher: [ | |
| '/((?!api/auth|api/health|_next/static|_next/image|favicon.ico|.*\\.(?:png|jpg|jpeg|webp|svg|ico)$).*)', | |
| ], |
| if (isAuthPage && isAuth) { | ||
| return NextResponse.redirect(new URL('/dashboard', req.url)); | ||
| } |
There was a problem hiding this comment.
This redirect sends authenticated users on /auth/* to /dashboard, but this repo currently has no /dashboard route under src/app. As-is, signed-in users hitting auth pages will be redirected to a 404. Either add a /dashboard route or redirect to an existing page.
| @@ -0,0 +1,56 @@ | |||
| import { getServerSession } from 'next-auth/next'; | |||
There was a problem hiding this comment.
getServerSession is imported from next-auth/next, which is primarily for the Pages Router. Since this repo is using the App Router (src/app/...), consider importing getServerSession from next-auth to match the recommended App Router integration and reduce the risk of future incompatibilities.
| import { getServerSession } from 'next-auth/next'; | |
| import { getServerSession } from 'next-auth'; |
| '*.{js,ts,tsx}': ['eslint --fix', 'prettier --write'], | ||
| }; | ||
|
|
||
| export default config; |
There was a problem hiding this comment.
lint-staged.config.js uses ESM export default, but package.json does not declare "type": "module". Unless lint-staged is explicitly configured to load ESM here, Node will treat this file as CommonJS and fail to parse it. Consider renaming to lint-staged.config.mjs or switching to module.exports = config.
| export default config; | |
| module.exports = config; |
| const users = [ | ||
| { | ||
| id: '1', | ||
| email: process.env.DEMO_EMAIL, | ||
| password: process.env.DEMO_PASSWORD, | ||
| name: 'Demo User', | ||
| role: 'user', | ||
| }, | ||
| { | ||
| id: '2', | ||
| email: process.env.ADMIN_EMAIL, | ||
| password: process.env.ADMIN_PASSWORD, |
There was a problem hiding this comment.
Demo users read email/password from env vars, which can be undefined. If these aren’t set, credentials login will always fail (and can produce misleading errors). Consider providing the demo defaults in code for local testing or validating required env vars at startup with a clear error.
| const users = [ | |
| { | |
| id: '1', | |
| email: process.env.DEMO_EMAIL, | |
| password: process.env.DEMO_PASSWORD, | |
| name: 'Demo User', | |
| role: 'user', | |
| }, | |
| { | |
| id: '2', | |
| email: process.env.ADMIN_EMAIL, | |
| password: process.env.ADMIN_PASSWORD, | |
| const DEMO_EMAIL = process.env.DEMO_EMAIL ?? 'demo@example.com'; | |
| const DEMO_PASSWORD = process.env.DEMO_PASSWORD ?? 'password123'; | |
| const ADMIN_EMAIL = process.env.ADMIN_EMAIL ?? 'admin@example.com'; | |
| const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD ?? 'admin123'; | |
| const users = [ | |
| { | |
| id: '1', | |
| email: DEMO_EMAIL, | |
| password: DEMO_PASSWORD, | |
| name: 'Demo User', | |
| role: 'user', | |
| }, | |
| { | |
| id: '2', | |
| email: ADMIN_EMAIL, | |
| password: ADMIN_PASSWORD, |
| GoogleProvider({ | ||
| clientId: process.env.GOOGLE_CLIENT_ID || '', | ||
| clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', | ||
| authorization: { | ||
| params: { | ||
| prompt: 'consent', | ||
| access_type: 'offline', | ||
| response_type: 'code', | ||
| }, | ||
| }, | ||
| }), |
There was a problem hiding this comment.
Google provider is always registered even when env vars are missing, falling back to empty strings. This can lead to runtime errors or broken sign-in options. Consider conditionally including the provider only when GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are set.
| GoogleProvider({ | |
| clientId: process.env.GOOGLE_CLIENT_ID || '', | |
| clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', | |
| authorization: { | |
| params: { | |
| prompt: 'consent', | |
| access_type: 'offline', | |
| response_type: 'code', | |
| }, | |
| }, | |
| }), | |
| ...(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET | |
| ? [ | |
| GoogleProvider({ | |
| clientId: process.env.GOOGLE_CLIENT_ID, | |
| clientSecret: process.env.GOOGLE_CLIENT_SECRET, | |
| authorization: { | |
| params: { | |
| prompt: 'consent', | |
| access_type: 'offline', | |
| response_type: 'code', | |
| }, | |
| }, | |
| }), | |
| ] | |
| : []), |
| export const authOptions: NextAuthOptions = { | ||
| session: { | ||
| strategy: 'jwt', | ||
| maxAge: 30 * 24 * 60 * 60, //30 days | ||
| updateAge: 24 * 60 * 60, //24 hours | ||
| }, | ||
|
|
||
| providers: [ | ||
| CredentialsProvider({ | ||
| name: 'Credentials', | ||
| credentials: { | ||
| email: { label: 'Email', type: 'text', placeholder: 'demo@example.com' }, | ||
| password: { label: 'Password', type: 'password' }, | ||
| }, | ||
| async authorize(credentials) { | ||
| if (!credentials?.email || !credentials?.password) { | ||
| throw new Error('Email and password are required'); | ||
| } | ||
|
|
||
| const user = users.find((u) => u.email === credentials.email); | ||
|
|
||
| if (!user) { | ||
| throw new Error('No user found with this email'); | ||
| } | ||
|
|
||
| const isValid = credentials.password === user.password; | ||
|
|
||
| if (!isValid) { | ||
| throw new Error('Invalid password'); | ||
| } | ||
|
|
||
| return { | ||
| id: user.id, | ||
| email: user.email, | ||
| name: user.name, | ||
| role: user.role, | ||
| }; | ||
| }, | ||
| }), | ||
|
|
||
| //google OAuth Provider | ||
| GoogleProvider({ | ||
| clientId: process.env.GOOGLE_CLIENT_ID || '', | ||
| clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', | ||
| authorization: { | ||
| params: { | ||
| prompt: 'consent', | ||
| access_type: 'offline', | ||
| response_type: 'code', | ||
| }, | ||
| }, | ||
| }), | ||
|
|
||
| //gitHub OAuth Provider | ||
| GitHubProvider({ | ||
| clientId: process.env.GITHUB_CLIENT_ID || '', | ||
| clientSecret: process.env.GITHUB_CLIENT_SECRET || '', | ||
| }), | ||
| ], | ||
|
|
There was a problem hiding this comment.
GitHub provider is always registered even when env vars are missing, falling back to empty strings. This can lead to runtime errors or broken sign-in options. Consider conditionally including the provider only when GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are set.
| export const authOptions: NextAuthOptions = { | |
| session: { | |
| strategy: 'jwt', | |
| maxAge: 30 * 24 * 60 * 60, //30 days | |
| updateAge: 24 * 60 * 60, //24 hours | |
| }, | |
| providers: [ | |
| CredentialsProvider({ | |
| name: 'Credentials', | |
| credentials: { | |
| email: { label: 'Email', type: 'text', placeholder: 'demo@example.com' }, | |
| password: { label: 'Password', type: 'password' }, | |
| }, | |
| async authorize(credentials) { | |
| if (!credentials?.email || !credentials?.password) { | |
| throw new Error('Email and password are required'); | |
| } | |
| const user = users.find((u) => u.email === credentials.email); | |
| if (!user) { | |
| throw new Error('No user found with this email'); | |
| } | |
| const isValid = credentials.password === user.password; | |
| if (!isValid) { | |
| throw new Error('Invalid password'); | |
| } | |
| return { | |
| id: user.id, | |
| email: user.email, | |
| name: user.name, | |
| role: user.role, | |
| }; | |
| }, | |
| }), | |
| //google OAuth Provider | |
| GoogleProvider({ | |
| clientId: process.env.GOOGLE_CLIENT_ID || '', | |
| clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', | |
| authorization: { | |
| params: { | |
| prompt: 'consent', | |
| access_type: 'offline', | |
| response_type: 'code', | |
| }, | |
| }, | |
| }), | |
| //gitHub OAuth Provider | |
| GitHubProvider({ | |
| clientId: process.env.GITHUB_CLIENT_ID || '', | |
| clientSecret: process.env.GITHUB_CLIENT_SECRET || '', | |
| }), | |
| ], | |
| const providers = [ | |
| CredentialsProvider({ | |
| name: 'Credentials', | |
| credentials: { | |
| email: { label: 'Email', type: 'text', placeholder: 'demo@example.com' }, | |
| password: { label: 'Password', type: 'password' }, | |
| }, | |
| async authorize(credentials) { | |
| if (!credentials?.email || !credentials?.password) { | |
| throw new Error('Email and password are required'); | |
| } | |
| const user = users.find((u) => u.email === credentials.email); | |
| if (!user) { | |
| throw new Error('No user found with this email'); | |
| } | |
| const isValid = credentials.password === user.password; | |
| if (!isValid) { | |
| throw new Error('Invalid password'); | |
| } | |
| return { | |
| id: user.id, | |
| email: user.email, | |
| name: user.name, | |
| role: user.role, | |
| }; | |
| }, | |
| }), | |
| //google OAuth Provider | |
| GoogleProvider({ | |
| clientId: process.env.GOOGLE_CLIENT_ID || '', | |
| clientSecret: process.env.GOOGLE_CLIENT_SECRET || '', | |
| authorization: { | |
| params: { | |
| prompt: 'consent', | |
| access_type: 'offline', | |
| response_type: 'code', | |
| }, | |
| }, | |
| }), | |
| ]; | |
| if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) { | |
| providers.push( | |
| //gitHub OAuth Provider | |
| GitHubProvider({ | |
| clientId: process.env.GITHUB_CLIENT_ID, | |
| clientSecret: process.env.GITHUB_CLIENT_SECRET, | |
| }) | |
| ); | |
| } | |
| export const authOptions: NextAuthOptions = { | |
| session: { | |
| strategy: 'jwt', | |
| maxAge: 30 * 24 * 60 * 60, //30 days | |
| updateAge: 24 * 60 * 60, //24 hours | |
| }, | |
| providers, |
| import { NextAuthOptions } from 'next-auth'; | ||
| import CredentialsProvider from 'next-auth/providers/credentials'; | ||
| import GoogleProvider from 'next-auth/providers/google'; | ||
| import GitHubProvider from 'next-auth/providers/github'; | ||
| //import { compare } from "bcryptjs"; |
There was a problem hiding this comment.
bcryptjs and @types/bcryptjs are added, but password verification in authorize is still a plain string comparison and the bcrypt import is commented out. Either wire up bcrypt verification (and store hashed demo passwords), or remove these deps to avoid unused packages and confusion.
| "dependencies": { | ||
| "bcryptjs": "^3.0.3", | ||
| "clsx": "^2.1.1", | ||
| "next": "16.1.5", | ||
| "next-auth": "^4.24.13", |
There was a problem hiding this comment.
bcryptjs is added as a dependency but does not appear to be used anywhere (only a commented-out import exists). If bcrypt isn’t being used in this PR, consider removing it to avoid unused dependencies and unnecessary install size.
| } | ||
|
|
||
| return NextResponse.redirect( | ||
| new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url), |
There was a problem hiding this comment.
Unauthenticated users are redirected to /auth/signin, but this repo currently has no /auth/* routes under src/app, and NextAuth’s default sign-in page is /api/auth/signin. Either implement /auth/signin (and set pages.signIn in authOptions) or redirect to the built-in endpoint to avoid 404 redirects.
| new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url), | |
| new URL(`/api/auth/signin?callbackUrl=${encodeURIComponent(from)}`, req.url), |
This PR introduces stateless authentication using NextAuth with JWT sessions.
Features
Demo Credentials
email: demo@example.com
password: demo123
Admin:
email: admin@example.com
password: admin123
Notes
This implementation currently uses in-memory demo users for testing.
Database integration can be added later.
Checklist