import { match } from '@formatjs/intl-localematcher' import Negotiator from 'negotiator' import type { UserSession } from '@heygermany/functions/src/application-types.d.js' import type { DB, UserRole, UserType } from '@heygermany/sdk' import { UserTypes } from '@heygermany/sdk' import type { SupportedLocale } from '@heygermany/sdk/config' import { LOCALES, DEFAULT_LOCALE } from '@/config' type RequestSnapshot = { url: string pathname: string search: string headers: Headers cookies: Map } type RequestRoutingResult = | { type: 'redirect' location: string } | { type: 'continue' locale: string } const LOCALE_EXEMPT_PREFIXES = ['/pilot'] const LOCALE_EXEMPT_LOCALES: Record = { '/pilot': 'de', } const getApiBase = (request: RequestSnapshot) => { return process.env.VITE_API_BASE_URL || process.env.API_URL || new URL(request.url).origin } function shouldBypassRouting(pathname: string): boolean { return ( pathname.startsWith('/api/') || pathname === '/api' || pathname.startsWith('/_build/') || pathname === '/_build' || pathname.startsWith('/_serverFn/') || pathname === '/_serverFn' || pathname.startsWith('/@') || pathname.startsWith('/src/') || pathname.startsWith('/node_modules/') || /\.[a-z0-9]+$/i.test(pathname) ) } const getCookieHeader = (cookies: Map) => { return Array.from(cookies.entries()) .map(([name, value]) => `${name}=${value}`) .join('; ') } const getBetterAuthSession = async ( request: RequestSnapshot ): Promise => { let response: Response try { response = await fetch(`${getApiBase(request)}/api/auth/get-session`, { method: 'GET', headers: { cookie: getCookieHeader(request.cookies), }, }) } catch (error) { // The session lookup must never crash SSR — a transient backend outage or // unreachable API should degrade to "logged out", not 500 the whole page. console.warn('[request-routing] get-session fetch failed, treating as no session:', error) return null } if (!response.ok) { return null } const data = (await response.json()) as { user?: { id: string name: string role: UserRole type: UserType } | null } | null if (!data?.user) { return null } return { userId: data.user.id, name: data.user.name, role: data.user.role, type: data.user.type, } } function getLocale(request: RequestSnapshot): string { try { const cookieLocale = request.cookies.get('NEXT_LOCALE') if (cookieLocale && LOCALES.includes(cookieLocale as SupportedLocale)) { return cookieLocale } const headers = Object.fromEntries(request.headers.entries()) as Negotiator.Headers const languages = new Negotiator({ headers }).languages() const validLanguages = languages.filter((lang) => { try { Intl.getCanonicalLocales(lang) return true } catch { return false } }) if (validLanguages.length === 0) { return DEFAULT_LOCALE } return match(validLanguages, LOCALES, DEFAULT_LOCALE) } catch (error) { console.error('[Middleware] Error in locale detection:', error) return DEFAULT_LOCALE } } function hasLocale(pathname: string): boolean { return LOCALES.some((locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) } function isLocaleExemptPath(pathname: string): boolean { return LOCALE_EXEMPT_PREFIXES.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)) } function getLocaleExemptPathLocale(pathname: string): SupportedLocale | null { for (const [prefix, locale] of Object.entries(LOCALE_EXEMPT_LOCALES)) { if (pathname === prefix || pathname.startsWith(`${prefix}/`)) { return locale } } return null } function getLocaleFromPath(pathname: string): string | null { for (const locale of LOCALES) { if (pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`) { return locale } } return null } function removeLocalePrefix(pathname: string): string { const locale = getLocaleFromPath(pathname) if (locale) { return pathname.slice(`/${locale}`.length) || '/' } return pathname } function requiresAuth(pathname: string): { required: boolean; type?: DB.UserType } { const pathWithoutLocale = removeLocalePrefix(pathname) if (pathWithoutLocale.includes('/auth/login')) { return { required: false } } if (pathWithoutLocale.startsWith('/backoffice')) { return { required: true, type: UserTypes.BACKOFFICE } } if (pathWithoutLocale.startsWith('/company')) { return { required: true, type: UserTypes.COMPANY } } if (pathWithoutLocale.startsWith('/jobs/application')) { return { required: true, type: UserTypes.CANDIDATE } } return { required: false } } const toAbsoluteUrl = (request: RequestSnapshot, location: string) => { return new URL(location, request.url).toString() } export const getResolvedLocale = (pathname: string) => { return getLocaleFromPath(pathname) || getLocaleExemptPathLocale(pathname) || DEFAULT_LOCALE } export async function resolveRequestRouting( request: RequestSnapshot ): Promise { if (shouldBypassRouting(request.pathname)) { return { type: 'continue', locale: getResolvedLocale(request.pathname), } } const fullUrl = request.pathname + request.search if (!hasLocale(request.pathname) && !isLocaleExemptPath(request.pathname)) { const locale = getLocale(request) return { type: 'redirect', location: toAbsoluteUrl(request, `/${locale}${request.pathname}${request.search}`), } } const userSession = await getBetterAuthSession(request) const authCheck = requiresAuth(request.pathname) if (authCheck.required) { const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE if (userSession?.type !== UserTypes.BACKOFFICE) { if (!userSession || userSession.type !== authCheck.type) { let loginPath = '' if (authCheck.type === UserTypes.BACKOFFICE) { loginPath = `/${currentLocale}/backoffice/auth/login` } else if (authCheck.type === UserTypes.COMPANY) { loginPath = `/${currentLocale}/company/auth/login` } else if (authCheck.type === UserTypes.CANDIDATE) { loginPath = `/${currentLocale}/jobs/apply` } const returnUrl = encodeURIComponent(fullUrl) return { type: 'redirect', location: toAbsoluteUrl(request, `${loginPath}?return=${returnUrl}`), } } } } if (userSession) { const pathWithoutLocale = removeLocalePrefix(request.pathname) const currentLocale = getLocaleFromPath(request.pathname) || DEFAULT_LOCALE if (pathWithoutLocale.includes('/auth/login')) { let dashboardPath = '' if (userSession.type === UserTypes.BACKOFFICE) { dashboardPath = `/${currentLocale}/backoffice/candidates` } else if (userSession.type === UserTypes.COMPANY) { dashboardPath = `/${currentLocale}/company/candidates` } else if (userSession.type === UserTypes.CANDIDATE) { dashboardPath = `/${currentLocale}/jobs/application` } if (dashboardPath) { return { type: 'redirect', location: toAbsoluteUrl(request, dashboardPath), } } } } return { type: 'continue', locale: getResolvedLocale(request.pathname), } }