89 lines
2.9 KiB
TypeScript
89 lines
2.9 KiB
TypeScript
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
import { verifyAdminToken, ADMIN_COOKIE_NAME } from '@/lib/session';
|
|
import { getTabForPathname, hasTabPermission } from '@/lib/permissions';
|
|
|
|
// Public endpoints and static resources
|
|
const PUBLIC_PREFIXES = [
|
|
'/login',
|
|
'/api/health',
|
|
'/api/cron',
|
|
'/api/external',
|
|
'/_next',
|
|
'/favicon.ico',
|
|
];
|
|
|
|
export async function middleware(request: NextRequest) {
|
|
const { pathname, search } = request.nextUrl;
|
|
const isServerAction = request.headers.has('next-action');
|
|
|
|
// 1. Bypass static files and Next.js internal assets (only for GET/HEAD, never for Server Action POSTs)
|
|
if (
|
|
!isServerAction &&
|
|
(request.method === 'GET' || request.method === 'HEAD') &&
|
|
(pathname.startsWith('/_next') || pathname.includes('.'))
|
|
) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// 2. Read session cookie
|
|
const token = request.cookies.get(ADMIN_COOKIE_NAME)?.value;
|
|
const session = await verifyAdminToken(token);
|
|
|
|
const isLoginPage = pathname === '/login';
|
|
|
|
// 3. If accessing /login
|
|
if (isLoginPage) {
|
|
// If already logged in and not invoking a login server action, redirect directly to dashboard
|
|
if (session && !isServerAction) {
|
|
return NextResponse.redirect(new URL('/dashboard', request.url));
|
|
}
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// 4. If accessing other public APIs (like cron or external integrations), disallow Server Action invocations
|
|
const isPublicPrefix = PUBLIC_PREFIXES.some(prefix => pathname.startsWith(prefix));
|
|
if (isPublicPrefix && !isServerAction) {
|
|
return NextResponse.next();
|
|
}
|
|
|
|
// 5. Protected route: redirect to /login if unauthenticated (or 401 for Server Actions)
|
|
if (!session) {
|
|
if (isServerAction) {
|
|
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
|
}
|
|
const loginUrl = new URL('/login', request.url);
|
|
const returnUrl = pathname + (search || '');
|
|
if (pathname !== '/' && pathname !== '') {
|
|
loginUrl.searchParams.set('from', returnUrl);
|
|
}
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
// 6. Check granular tab permission
|
|
const tabKey = getTabForPathname(pathname);
|
|
if (tabKey && !hasTabPermission(session.adminRole, session.permissions, tabKey)) {
|
|
if (isServerAction) {
|
|
return NextResponse.json({ error: 'Permission refusée' }, { status: 403 });
|
|
}
|
|
// If user lacks permission for this tab, redirect back to /dashboard
|
|
const dashboardUrl = new URL('/dashboard', request.url);
|
|
dashboardUrl.searchParams.set('unauthorized', tabKey);
|
|
return NextResponse.redirect(dashboardUrl);
|
|
}
|
|
|
|
return NextResponse.next();
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
/*
|
|
* Match all request paths except:
|
|
* - _next/static (static files)
|
|
* - _next/image (image optimization files)
|
|
* - favicon.ico (favicon file)
|
|
*/
|
|
'/((?!_next/static|_next/image|favicon.ico).*)',
|
|
],
|
|
};
|