Files
carmanagement/apps/homepage/src/proxy.ts
T
root ab03ee3a4d
Build & Deploy / Build & Push Docker Image (push) Failing after 48s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / API Unit Tests (push) Failing after 5m2s
Test / Marketplace Unit Tests (push) Failing after 4m56s
Test / Admin Unit Tests (push) Successful in 9m36s
Test / Dashboard Unit Tests (push) Successful in 9m37s
Test / API Integration Tests (push) Successful in 9m54s
fix login
2026-06-27 00:55:55 -04:00

101 lines
3.8 KiB
TypeScript

import {
defaultLocale,
isLocale,
localeCookie,
localeCookieMaxAgeSeconds,
localeFromAcceptLanguage,
} from '@/lib/localization/config';
import { buildContentSecurityPolicy, createNonce } from '@/lib/security/csp';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
function applySecurityHeaders(response: NextResponse, contentSecurityPolicy: string): NextResponse {
response.headers.set('Content-Security-Policy', contentSecurityPolicy);
response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('Cross-Origin-Opener-Policy', 'same-origin');
response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
return response;
}
function resolveApiOrigin(): string | undefined {
const origins = new Set<string>();
const urls = [process.env.API_INTERNAL_URL, process.env.NEXT_PUBLIC_API_URL];
for (const url of urls) {
if (!url) continue;
try {
origins.add(new URL(url).origin);
} catch {
// ignore malformed URLs
}
}
return origins.size > 0 ? Array.from(origins).join(' ') : undefined;
}
export function proxy(request: NextRequest): NextResponse {
const nonce = createNonce();
const isDev = process.env.NODE_ENV !== 'production';
const apiOrigin = resolveApiOrigin();
const csp = buildContentSecurityPolicy(nonce, isDev, apiOrigin);
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-nonce', nonce);
requestHeaders.set('Content-Security-Policy', csp);
if (request.nextUrl.pathname === '/') {
const cookieLocale = request.cookies.get(localeCookie)?.value;
const locale = isLocale(cookieLocale)
? cookieLocale
: localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
const destination = request.nextUrl.clone();
destination.pathname = `/${locale}`;
const response = NextResponse.redirect(destination);
response.cookies.set(localeCookie, locale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
return applySecurityHeaders(response, csp);
}
// Redirect locale-less paths (e.g. /sign-in → /en/sign-in), but not /dashboard, /admin, or files
const firstSegment = request.nextUrl.pathname.split('/')[1];
const isFile = /\.\w+$/.test(request.nextUrl.pathname);
if (firstSegment && !isFile && !isLocale(firstSegment) && firstSegment !== 'dashboard' && firstSegment !== 'admin') {
const cookieLocale = request.cookies.get(localeCookie)?.value;
const locale = isLocale(cookieLocale)
? cookieLocale
: localeFromAcceptLanguage(request.headers.get('accept-language')) || defaultLocale;
const destination = request.nextUrl.clone();
destination.pathname = `/${locale}${request.nextUrl.pathname}`;
return applySecurityHeaders(NextResponse.redirect(destination), csp);
}
const response = NextResponse.next({ request: { headers: requestHeaders } });
const explicitLocale = request.nextUrl.pathname.split('/')[1];
if (isLocale(explicitLocale)) {
response.cookies.set(localeCookie, explicitLocale, {
path: '/',
sameSite: 'lax',
maxAge: localeCookieMaxAgeSeconds,
secure: process.env.NODE_ENV === 'production',
httpOnly: false,
});
}
return applySecurityHeaders(response, csp);
}
export const config = {
matcher: [
{
source: '/((?!api|_next/static|_next/image|assets|favicon.ico|robots.txt|sitemap.xml).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
],
};