Files
tessera-ctl/apps/web/src/middleware.ts
T
schalli cdf4d60038 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>
2026-06-18 13:32:53 +02:00

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$).*)'],
};