ShipDesk
Task Manager
ShipDesk
Progress
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.jsxadd 'use client', paste DashboardLayout
4 hrs
Delete PrivateRoute.jsxmiddleware.js does this job better now
1 min
Replace <NavLink>use usePathname() for active route detection
1 hr
Middleware Implementation
Create middleware.js with role-based routingread JWT from cookie, decode role, redirect to correct dashboard
3 hrs
Migrate JWT from localStorage → httpOnly cookieupdate Express login endpoint to set cookie instead of returning token in body
2 hrs
Add role-based redirect logic5 user types → 5 different dashboards (user, doctor, hospital, admin, founder)
2 hrs
Test protection in incognitoopen /dashboard in incognito → must redirect to /user/login
30 min
Configure cookie SameSite + Secure flagsLax for same-domain, None+Secure for cross-domain (production)
30 min
Add loading state during auth redirectsshow skeleton or spinner while middleware checks auth
1 hr
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.
Previous
Phase 03Authentication Pages
Next
Phase 05Dashboard Pages Migration