payment-backoffice/middleware.ts
2025-11-25 10:50:27 +01:00

137 lines
4.1 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<string, string[]> = {
// 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*",
],
};