import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import type { MenuListResponse } from '@/api/menu/types';
import type { UserResponse } from '@/api/auth/types';
import { getCachedMenuPaths, setCachedMenuPaths } from '@/lib/menuCache';
import { buildMenuFullPaths } from '@/utils/menu';
import {
AUTH_ONLY_PATHS,
PERMISSION_REQUIRED_PATHS,
PUBLIC_PATHS,
} from '@/constants/authMiddleware';
export const isPublicPath = (path: string) =>
PUBLIC_PATHS.has(path) ||
path.includes('.') ||
path.startsWith('/.well-known');
export const normalizePath = (path: string): string =>
path.replace(/\/+$/, '') || '/';
const fetchMenuItems = async (
token: string
): Promise<MenuListResponse | null> => {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
if (!baseUrl) return null;
try {
const response = await fetch(`${baseUrl}/api/menus`, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
});
const json = await response.json();
return json.data as MenuListResponse;
} catch (error) {
console.error('Failed to fetch menus:', error);
return null;
}
};
const fetchUserInfo = async (
token: string
): Promise<UserResponse | null> => {
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL;
if (!baseUrl) return null;
try {
const response = await fetch(`${baseUrl}/api/users/me`, {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${token}`,
},
cache: 'no-store',
});
const json = await response.json();
return json.data as UserResponse;
} catch (error) {
console.error('Failed to fetch user info:', error);
return null;
}
};
/**
* Redirect authenticated users away from login/signup pages
*/
const handleAuthenticatedAuthPageRedirect = (
path: string,
token: string | undefined,
request: NextRequest
) => {
if ((path === '/login' || path === '/signup') && token) {
return NextResponse.redirect(new URL('/', request.url));
}
return null;
};
/**
* Redirect unauthenticated users to login
*/
const handleMissingTokenRedirect = (
token: string | undefined,
request: NextRequest
) => {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return null;
};
/**
* Check permission-required paths
*/
const checkPermissionRequiredPath = async (
path: string,
token: string
): Promise<NextResponse | null> => {
const requiredPermission = PERMISSION_REQUIRED_PATHS.get(path);
if (!requiredPermission) {
return null;
}
const userInfo = await fetchUserInfo(token);
if (!userInfo || !userInfo.roles.includes(requiredPermission)) {
return NextResponse.redirect(new URL('/404', undefined));
}
return null;
};
/**
* Get menu paths from cache or fetch from API
*/
const getOrFetchMenuPaths = async (
token: string,
request: NextRequest
): Promise<Set<string> | NextResponse> => {
let menuPaths = getCachedMenuPaths(token);
if (menuPaths) {
return menuPaths;
}
const menuResponse = await fetchMenuItems(token);
if (!menuResponse) {
return NextResponse.redirect(new URL('/login', request.url));
}
menuPaths = buildMenuFullPaths(menuResponse.items);
setCachedMenuPaths(token, menuPaths);
return menuPaths;
};
/**
* Check if the user has access to the requested menu path
*/
const checkMenuPathAccess = (
normalizedPath: string,
menuPaths: Set<string>,
request: NextRequest
): NextResponse | null => {
if (!menuPaths.has(normalizedPath) && !AUTH_ONLY_PATHS.has(normalizedPath)) {
return NextResponse.redirect(new URL('/404', request.url));
}
return null;
};
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
const token = request.cookies.get('session_token')?.value;
// 1. Redirect authenticated users away from auth pages
const authPageRedirect = handleAuthenticatedAuthPageRedirect(
path,
token,
request
);
if (authPageRedirect) return authPageRedirect;
// 2. Allow public paths
if (isPublicPath(path)) {
return NextResponse.next();
}
try {
// 3. Redirect to login if no token
const missingTokenRedirect = handleMissingTokenRedirect(token, request);
if (missingTokenRedirect) return missingTokenRedirect;
const normalizedPath = normalizePath(path);
// 4. Check permission-required paths
const permissionError = await checkPermissionRequiredPath(
normalizedPath,
token!
);
if (permissionError) return permissionError;
// 5. Fetch menu paths
const menuPathsOrRedirect = await getOrFetchMenuPaths(token!, request);
if (menuPathsOrRedirect instanceof NextResponse) {
return menuPathsOrRedirect;
}
const menuPaths = menuPathsOrRedirect;
// 6. Check menu path access
const menuAccessError = checkMenuPathAccess(
normalizedPath,
menuPaths,
request
);
if (menuAccessError) return menuAccessError;
} catch (error) {
console.error('Auth check failed:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!api|_next/static|_next/image|favicon.ico|assets|.*\\..*|$).*)',
],
};