WellNuo/web/middleware.ts
Sergei 3f0fe56e02 Add protected route middleware and auth store for web app
- Implement Next.js middleware for route protection
- Create Zustand auth store for web (similar to mobile)
- Add comprehensive tests for middleware and auth store
- Protect authenticated routes (/dashboard, /profile)
- Redirect unauthenticated users to /login
- Redirect authenticated users from auth routes to /dashboard
- Handle session expiration with 401 callback
- Set access token cookie for middleware
- All tests passing (105 tests total)
2026-01-31 17:49:21 -08:00

100 lines
2.9 KiB
TypeScript

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Protected Route Middleware for WellNuo Web
*
* This middleware protects authenticated routes and manages authentication flows.
*
* Route Groups:
* - (auth): Public routes - login, verify-otp, etc.
* - (main): Protected routes - dashboard, profile, etc.
*
* Flow:
* 1. Check if user has accessToken in cookies
* 2. If no token and accessing protected route → redirect to /login
* 3. If has token and accessing auth route → redirect to /dashboard
*/
// Public routes that don't require authentication
const publicRoutes = [
'/login',
'/verify-otp',
'/signup',
'/forgot-password',
'/reset-password',
];
// API routes (always allowed)
const apiRoutes = ['/api'];
// Static assets and Next.js internals (always allowed)
const publicPaths = ['/_next', '/favicon.ico', '/images', '/fonts'];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Allow API routes, static assets, and Next.js internals
if (
apiRoutes.some(route => pathname.startsWith(route)) ||
publicPaths.some(path => pathname.startsWith(path))
) {
return NextResponse.next();
}
// Check if user is authenticated
const accessToken = request.cookies.get('accessToken')?.value;
const isAuthenticated = !!accessToken;
// Check if current route is public
const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route));
// Root path handling
if (pathname === '/') {
if (isAuthenticated) {
// Redirect to dashboard if authenticated
return NextResponse.redirect(new URL('/dashboard', request.url));
} else {
// Redirect to login if not authenticated
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Redirect logic
if (!isAuthenticated && !isPublicRoute) {
// Not authenticated and trying to access protected route → redirect to login
const loginUrl = new URL('/login', request.url);
// Save the original URL to redirect back after login
loginUrl.searchParams.set('redirect', pathname);
return NextResponse.redirect(loginUrl);
}
if (isAuthenticated && isPublicRoute) {
// Authenticated and trying to access auth routes → redirect to dashboard
return NextResponse.redirect(new URL('/dashboard', request.url));
}
// Allow the request to proceed
return NextResponse.next();
}
/**
* Matcher Configuration
*
* Only run middleware on specific routes.
* Excludes: API routes, static files, Next.js internals
*/
export const config = {
matcher: [
/*
* Match all request paths except:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder files
*/
'/((?!api|_next/static|_next/image|favicon.ico|images|fonts).*)',
],
};