- 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)
100 lines
2.9 KiB
TypeScript
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).*)',
|
|
],
|
|
};
|