Skip to content

feat: implement stateless authentication using NextAuth with demo users#4

Open
Monish-Parmar10 wants to merge 4 commits intoIPS-TECH-CLUB:mainfrom
Monish-Parmar10:feature/authentication
Open

feat: implement stateless authentication using NextAuth with demo users#4
Monish-Parmar10 wants to merge 4 commits intoIPS-TECH-CLUB:mainfrom
Monish-Parmar10:feature/authentication

Conversation

@Monish-Parmar10
Copy link
Copy Markdown

This PR introduces stateless authentication using NextAuth with JWT sessions.

Features

  • Credentials-based authentication
  • Demo users (user + admin roles)
  • Role support in session and JWT
  • NextAuth configuration and utilities
  • Middleware protection for routes

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

  • Code formatted with Prettier
  • Linted with ESLint
  • Tests executed with pnpm test

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 /dashboard and /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).*)'],
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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)$).*)',
],

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +14
if (isAuthPage && isAuth) {
return NextResponse.redirect(new URL('/dashboard', req.url));
}
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,56 @@
import { getServerSession } from 'next-auth/next';
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
import { getServerSession } from 'next-auth/next';
import { getServerSession } from 'next-auth';

Copilot uses AI. Check for mistakes.
'*.{js,ts,tsx}': ['eslint --fix', 'prettier --write'],
};

export default config;
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
export default config;
module.exports = config;

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +18
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,
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +75
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || '',
clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
authorization: {
params: {
prompt: 'consent',
access_type: 'offline',
response_type: 'code',
},
},
}),
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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',
},
},
}),
]
: []),

Copilot uses AI. Check for mistakes.
Comment on lines +24 to +83
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 || '',
}),
],

Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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,

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
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";
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines 15 to +19
"dependencies": {
"bcryptjs": "^3.0.3",
"clsx": "^2.1.1",
"next": "16.1.5",
"next-auth": "^4.24.13",
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
}

return NextResponse.redirect(
new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url),
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
new URL(`/auth/signin?from=${encodeURIComponent(from)}`, req.url),
new URL(`/api/auth/signin?callbackUrl=${encodeURIComponent(from)}`, req.url),

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants