import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { jwtVerify } from "jose"; const COOKIE_NAME = "auth_token"; const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET!); // Define route-to-role mappings // Routes can be protected by specific roles/groups or left open to all authenticated users // Users can have multiple groups, and access is granted if ANY of their groups match const ROUTE_ROLES: Record = { // Admin routes - only accessible by Super Admin or admin groups "/dashboard/admin": ["Super Admin", "Admin"], "/admin": ["Super Admin", "Admin"], // Add more route guards here as needed // Example: "/dashboard/settings": ["Super Admin", "admin", "manager"], // Example: "/dashboard/transactions": ["Super Admin", "admin", "operator", "viewer"], }; /** * Check if a user's groups have access to a specific route * Returns true if ANY of the user's groups match ANY of the required roles */ function hasRouteAccess( userGroups: string[] | undefined, pathname: string ): boolean { // If no role is required for this route, allow access const requiredRoles = Object.entries(ROUTE_ROLES).find(([route]) => pathname.startsWith(route) )?.[1]; // If no role requirement found, allow access (route is open to all authenticated users) if (!requiredRoles) { return true; } // If user has no groups, deny access if (!userGroups || userGroups.length === 0) { return false; } // Check if ANY of the user's groups match ANY of the required roles return userGroups.some(group => requiredRoles.includes(group)); } function isExpired(exp?: number) { return exp ? exp * 1000 <= Date.now() : false; } async function validateToken(token: string) { const raw = token.startsWith("Bearer ") ? token.slice(7) : token; try { const { payload } = await jwtVerify(raw, JWT_SECRET, { algorithms: ["HS256"], }); console.log("[middleware] payload", payload); return payload as { exp?: number; MustChangePassword?: boolean; Groups?: string[]; [key: string]: unknown; }; } catch (err) { console.error("Token validation error:", err); return null; } } export async function middleware(request: NextRequest) { const token = request.cookies.get(COOKIE_NAME)?.value; const loginUrl = new URL("/login", request.url); const currentPath = request.nextUrl.pathname; // 1️⃣ No token if (!token) { loginUrl.searchParams.set("reason", "no-token"); loginUrl.searchParams.set("redirect", currentPath); return NextResponse.redirect(loginUrl); } // 2️⃣ Validate + decode const payload = await validateToken(token); if (!payload) { const res = NextResponse.redirect(loginUrl); res.cookies.delete(COOKIE_NAME); loginUrl.searchParams.set("reason", "invalid-token"); loginUrl.searchParams.set("redirect", currentPath); return res; } // 3️⃣ Expiry check if (isExpired(payload.exp)) { const res = NextResponse.redirect(loginUrl); res.cookies.delete(COOKIE_NAME); loginUrl.searchParams.set("reason", "expired-token"); loginUrl.searchParams.set("redirect", currentPath); return res; } // 4️⃣ Must change password check if (payload.MustChangePassword) { loginUrl.searchParams.set("reason", "change-password"); loginUrl.searchParams.set("redirect", currentPath); return NextResponse.redirect(loginUrl); } // 5️⃣ Role-based route guard (checking Groups array) const userGroups = (payload.Groups as string[] | undefined) || []; if (!hasRouteAccess(userGroups, currentPath)) { // Redirect to dashboard home or unauthorized page const unauthorizedUrl = new URL("/dashboard", request.url); unauthorizedUrl.searchParams.set("reason", "unauthorized"); unauthorizedUrl.searchParams.set( "message", "You don't have permission to access this page" ); return NextResponse.redirect(unauthorizedUrl); } // ✅ All good return NextResponse.next(); } export const config = { matcher: [ "/dashboard/:path*", "/settings/:path*", "/admin/:path*", "/change-password/:path*", ], };