04
Middleware + Dashboard Layout
Edge-level auth protection — replaces PrivateRoute.jsx entirely
Week 32–3 daysEstimated: 14h totalSecurity Critical
Why this phase matters
This is your security layer. Without middleware, any user can access any dashboard by typing the URL. middleware.js runs at Vercel's edge network before any page loads — unauthenticated users are redirected before the page even starts rendering.
middleware.js — the most powerful file in Next.js
Runs at Vercel's edge network before any page loads. Unauthenticated users are redirected before the page even starts rendering.
Dashboard Layout
Create app/dashboard/layout.jsx — add 'use client', paste DashboardLayout
Delete PrivateRoute.jsx — middleware.js does this job better now
Replace <NavLink> — use usePathname() for active route detection
Middleware Implementation
Create middleware.js with role-based routing — read JWT from cookie, decode role, redirect to correct dashboard
Migrate JWT from localStorage → httpOnly cookie — update Express login endpoint to set cookie instead of returning token in body
Add role-based redirect logic — 5 user types → 5 different dashboards (user, doctor, hospital, admin, founder)
Test protection in incognito — open /dashboard in incognito → must redirect to /user/login
Configure cookie SameSite + Secure flags — Lax for same-domain, None+Secure for cross-domain (production)
Add loading state during auth redirects — show skeleton or spinner while middleware checks auth
middleware.js — edge-level auth
import { NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
const PUBLIC_PATHS = ['/', '/about', '/pricing', '/team', '/user/login', '/user/signup', '/doctor/login', '/doctor/signup']
// Role → Dashboard mapping
const ROLE_DASHBOARDS = {
user: '/dashboard',
doctor: '/doctor/dashboard',
hospital: '/hospital/dashboard',
admin: '/admin/dashboard',
founder: '/founder/dashboard',
}
export async function middleware(request) {
const { pathname } = request.nextUrl
// Allow public paths
if (PUBLIC_PATHS.some(p => pathname.startsWith(p))) {
return NextResponse.next()
}
// Read JWT from httpOnly cookie
const token = request.cookies.get('auth-token')?.value
if (!token) {
return NextResponse.redirect(new URL('/user/login', request.url))
}
try {
const secret = new TextEncoder().encode(process.env.JWT_SECRET)
const { payload } = await jwtVerify(token, secret)
// Optional: role-based access control
const role = payload.role
const allowedPath = ROLE_DASHBOARDS[role]
if (allowedPath && !pathname.startsWith(allowedPath)) {
return NextResponse.redirect(new URL(allowedPath, request.url))
}
return NextResponse.next()
} catch {
return NextResponse.redirect(new URL('/user/login', request.url))
}
}
export const config = {
matcher: ['/dashboard/:path*', '/doctor/:path*', '/hospital/:path*', '/admin/:path*', '/founder/:path*'],
}Express — set httpOnly cookie on login
// In your Express login route (e.g., routes/auth.js)
router.post('/api/auth/login', async (req, res) => {
// ... validate credentials ...
const token = jwt.sign(
{ id: user._id, role: user.role, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
)
// SET AS COOKIE instead of returning in body
res.cookie('auth-token', token, {
httpOnly: true, // JS can't read it (XSS protection)
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/',
})
res.json({ success: true, user: { id: user._id, role: user.role } })
})
// Logout — clear the cookie
router.post('/api/auth/logout', (req, res) => {
res.clearCookie('auth-token')
res.json({ success: true })
})Gotcha: Middleware reads cookies, not localStorage. If your auth stores JWT in localStorage, middleware can't read it. Move to cookies.
Gotcha: Cookie SameSite must be 'Lax' for same-domain or 'None' + Secure for cross-domain. Wrong config = silent auth failures in production.