83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
/**
|
|
* Middleware
|
|
*
|
|
* Composes:
|
|
* 1. next-intl locale detection & prefix enforcement (/fr/..., /en/...)
|
|
* 2. Auth protection — redirects unauthenticated users to /{locale}/login
|
|
*
|
|
* All URLs are locale-prefixed (localePrefix: 'always' in routing.ts).
|
|
*/
|
|
|
|
import createMiddleware from 'next-intl/middleware';
|
|
import { NextResponse } from 'next/server';
|
|
import type { NextRequest } from 'next/server';
|
|
import { routing } from './i18n/routing';
|
|
|
|
const intlMiddleware = createMiddleware(routing);
|
|
|
|
// Paths that do not require authentication (matched AFTER locale stripping)
|
|
const exactPublicPaths = ['/'];
|
|
const prefixPublicPaths = [
|
|
'/login',
|
|
'/register',
|
|
'/forgot-password',
|
|
'/reset-password',
|
|
'/verify-email',
|
|
'/about',
|
|
'/careers',
|
|
'/blog',
|
|
'/press',
|
|
'/contact',
|
|
'/carrier',
|
|
'/pricing',
|
|
'/docs',
|
|
];
|
|
|
|
function stripLocale(pathname: string): { locale: string | null; pathWithoutLocale: string } {
|
|
for (const locale of routing.locales) {
|
|
if (pathname === `/${locale}`) return { locale, pathWithoutLocale: '/' };
|
|
if (pathname.startsWith(`/${locale}/`)) {
|
|
return { locale, pathWithoutLocale: pathname.slice(`/${locale}`.length) };
|
|
}
|
|
}
|
|
return { locale: null, pathWithoutLocale: pathname };
|
|
}
|
|
|
|
export default function middleware(request: NextRequest) {
|
|
const { pathname } = request.nextUrl;
|
|
|
|
// Step 1: let next-intl handle locale redirection (e.g. `/dashboard` → `/fr/dashboard`)
|
|
const intlResponse = intlMiddleware(request);
|
|
|
|
// If next-intl already issued a redirect, honor it (user will land on the
|
|
// locale-prefixed URL and the middleware will run again with auth intact).
|
|
if (intlResponse.status >= 300 && intlResponse.status < 400) {
|
|
return intlResponse;
|
|
}
|
|
|
|
// Step 2: auth protection
|
|
const { locale, pathWithoutLocale } = stripLocale(pathname);
|
|
const resolvedLocale = locale || routing.defaultLocale;
|
|
|
|
const isPublicPath =
|
|
exactPublicPaths.includes(pathWithoutLocale) ||
|
|
prefixPublicPaths.some(p => pathWithoutLocale === p || pathWithoutLocale.startsWith(p + '/'));
|
|
|
|
const token = request.cookies.get('accessToken')?.value;
|
|
|
|
if (!isPublicPath && !token) {
|
|
const loginUrl = new URL(`/${resolvedLocale}/login`, request.url);
|
|
loginUrl.searchParams.set('redirect', pathname);
|
|
return NextResponse.redirect(loginUrl);
|
|
}
|
|
|
|
return intlResponse;
|
|
}
|
|
|
|
export const config = {
|
|
// Exclude Next.js internals, API routes, and static assets
|
|
matcher: [
|
|
'/((?!_next/static|_next/image|api|assets|favicon\\.ico|manifest\\.json|.*\\.(?:png|jpg|jpeg|gif|webp|svg|ico|mp4|mp3|pdf|txt|xml|csv|json)$).*)',
|
|
],
|
|
};
|