cdf4d60038
- Create (auth) route group with standalone layout (no sidebar/header, D-04) - Create (portal) route group wrapping children with AppShell - Move dashboard page into (portal) route group - Add Next.js middleware for JWT-based route protection using jose - Create session.ts with verifySession/getSessionFromCookies helpers - Create auth-actions.ts server actions: login, logout, fetchCurrentUser - Create Zustand auth-store for client-side user state - Install jose and zod dependencies Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
74 lines
2.1 KiB
TypeScript
74 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { jwtVerify } from 'jose';
|
|
|
|
/**
|
|
* Next.js middleware for frontend route protection (Pattern 4).
|
|
*
|
|
* Validates JWT session cookie on every request. Redirects to /login
|
|
* if missing or invalid. This is an optimistic check -- the API still
|
|
* validates the JWT independently on every request.
|
|
*/
|
|
|
|
const publicRoutes = ['/login', '/reset-password'];
|
|
|
|
function getSecret() {
|
|
const secret = process.env.JWT_SECRET || process.env.SESSION_SECRET;
|
|
if (!secret) {
|
|
// In development, allow a fallback to prevent startup crashes
|
|
// when env vars are not yet configured
|
|
return new TextEncoder().encode('development-secret-change-me');
|
|
}
|
|
return new TextEncoder().encode(secret);
|
|
}
|
|
|
|
export async function middleware(req: NextRequest) {
|
|
const path = req.nextUrl.pathname;
|
|
|
|
// Allow public routes without authentication
|
|
if (publicRoutes.some((route) => path.startsWith(route))) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// Skip static assets and API routes (handled by NestJS)
|
|
if (
|
|
path.startsWith('/_next/static') ||
|
|
path.startsWith('/_next/image') ||
|
|
path.startsWith('/favicon.ico') ||
|
|
path.startsWith('/api')
|
|
) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// Read session cookie
|
|
const session = req.cookies.get('session')?.value;
|
|
|
|
if (!session) {
|
|
return NextResponse.redirect(new URL('/login', req.nextUrl));
|
|
}
|
|
|
|
try {
|
|
const { payload } = await jwtVerify(session, getSecret(), {
|
|
algorithms: ['HS256'],
|
|
});
|
|
|
|
// D-06: Force password change redirect
|
|
if (
|
|
payload.mustChangePassword === true &&
|
|
!path.startsWith('/change-password')
|
|
) {
|
|
return NextResponse.redirect(new URL('/change-password', req.nextUrl));
|
|
}
|
|
|
|
return NextResponse.next();
|
|
} catch {
|
|
// JWT verification failed -- clear stale cookie and redirect to login
|
|
const response = NextResponse.redirect(new URL('/login', req.nextUrl));
|
|
response.cookies.delete('session');
|
|
return response;
|
|
}
|
|
}
|
|
|
|
export const config = {
|
|
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
|
|
};
|