feat(02-02): auth infrastructure -- route groups, middleware, session, auth-actions, auth store
- 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>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
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$).*)'],
|
||||
};
|
||||
Reference in New Issue
Block a user