import { NextResponse } from "next/server"; import type { NextRequest } from "next/server"; import { corsHeaders, forbidden, isAllowedOrigin } from "@/lib/api-route"; import { LOCALE_COOKIE, matchLocale } from "@/lib/i18n/locales"; function withCorsHeaders(response: NextResponse, origin: string | null): NextResponse { const headers = corsHeaders(origin); for (const [key, value] of Object.entries(headers)) { response.headers.set(key, value); } return response; } const RATE_LIMIT_WINDOW_MS = 60_000; const RATE_LIMIT_MAX_REQUESTS = 30; const rateLimitHits = new Map(); export function _resetRateLimit(): void { rateLimitHits.clear(); } export function clientIp(request: NextRequest): string { const cloudflareIp = request.headers.get("cf-connecting-ip")?.trim(); if (cloudflareIp) { return cloudflareIp; } const forwardedFor = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim(); if (forwardedFor) { return forwardedFor; } return "direct"; } function isRateLimited(ip: string, now: number = Date.now()): boolean { const cutoff = now - RATE_LIMIT_WINDOW_MS; const hits = (rateLimitHits.get(ip) ?? []).filter((timestamp) => timestamp > cutoff); if (hits.length >= RATE_LIMIT_MAX_REQUESTS) { rateLimitHits.set(ip, hits); return true; } hits.push(now); rateLimitHits.set(ip, hits); return false; } function rateLimited(origin: string | null): NextResponse { return NextResponse.json( { error: "Too many requests" }, { status: 429, headers: { "Cache-Control": "no-store", "Retry-After": String(Math.ceil(RATE_LIMIT_WINDOW_MS / 1000)), ...corsHeaders(origin), }, }, ); } function handleApi(request: NextRequest): NextResponse { const origin = request.headers.get("origin"); if (isRateLimited(clientIp(request))) { return rateLimited(origin); } if (!isAllowedOrigin(request)) { return forbidden(origin); } if (request.method === "OPTIONS") { const response = new NextResponse(null, { status: 204 }); withCorsHeaders(response, origin); response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); response.headers.set("Access-Control-Max-Age", "86400"); return response; } return withCorsHeaders(NextResponse.next(), origin); } export function proxy(request: NextRequest): NextResponse { if (request.nextUrl.pathname.startsWith("/api/")) { return handleApi(request); } if (request.cookies.has(LOCALE_COOKIE)) { return NextResponse.next(); } const locale = matchLocale(request.headers.get("accept-language")); const response = NextResponse.next(); response.cookies.set(LOCALE_COOKIE, locale, { path: "/", maxAge: 60 * 60 * 24 * 365, sameSite: "lax", }); return response; } export const config = { matcher: [ "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp|ico)).*)", ], };