import { NextResponse } from 'next/server' type RouteContext = { params: Promise<{ path?: string[] }> | { path?: string[] } } const HOP_BY_HOP_HEADERS = new Set([ 'connection', 'content-encoding', 'content-length', 'host', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'set-cookie', 'te', 'trailer', 'transfer-encoding', 'upgrade', ]) function normalizeApiOrigin(value: string): string { return value.replace(/\/+$/, '').replace(/\/api(?:\/v1)?$/, '') } function resolveApiOrigins(): string[] { return Array.from(new Set([ process.env.API_INTERNAL_URL, process.env.API_URL, process.env.NEXT_PUBLIC_API_URL, 'http://localhost:4000', ] .filter((value): value is string => Boolean(value)) .map(normalizeApiOrigin))) } function rewriteCookieForDashboard(cookie: string): string { return cookie .replace(/;\s*Domain=[^;]*/gi, '') .replace(/;\s*SameSite=None/gi, '; SameSite=Lax') } function getSetCookieHeaders(headers: Headers): string[] { const withGetter = headers as Headers & { getSetCookie?: () => string[] } if (typeof withGetter.getSetCookie === 'function') return withGetter.getSetCookie() const cookie = headers.get('set-cookie') return cookie ? [cookie] : [] } function copyRequestHeaders(request: Request): Headers { const headers = new Headers() request.headers.forEach((value, key) => { if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value) }) return headers } function copyResponseHeaders(upstream: Response): Headers { const headers = new Headers() upstream.headers.forEach((value, key) => { if (!HOP_BY_HOP_HEADERS.has(key.toLowerCase())) headers.set(key, value) }) return headers } async function proxyDashboardApi(request: Request, context: RouteContext) { const params = await Promise.resolve(context.params) const path = (params.path ?? []).map(encodeURIComponent).join('/') const requestUrl = new URL(request.url) const method = request.method.toUpperCase() const hasBody = method !== 'GET' && method !== 'HEAD' const body = hasBody ? await request.arrayBuffer() : undefined const headers = copyRequestHeaders(request) let lastError: unknown = null let upstream: Response | null = null for (const origin of resolveApiOrigins()) { const upstreamUrl = new URL(`/api/v1/${path}${requestUrl.search}`, origin) try { upstream = await fetch(upstreamUrl, { method, headers, body, cache: 'no-store', }) break } catch (error) { lastError = error } } if (!upstream) { const message = lastError instanceof Error ? lastError.message : 'API proxy request failed' return NextResponse.json({ error: 'api_proxy_unavailable', message }, { status: 502 }) } const response = new NextResponse(await upstream.arrayBuffer(), { status: upstream.status, statusText: upstream.statusText, headers: copyResponseHeaders(upstream), }) for (const cookie of getSetCookieHeaders(upstream.headers)) { response.headers.append('set-cookie', rewriteCookieForDashboard(cookie)) } return response } export const runtime = 'nodejs' export const dynamic = 'force-dynamic' export async function GET(request: Request, context: RouteContext) { return proxyDashboardApi(request, context) } export async function POST(request: Request, context: RouteContext) { return proxyDashboardApi(request, context) } export async function PUT(request: Request, context: RouteContext) { return proxyDashboardApi(request, context) } export async function PATCH(request: Request, context: RouteContext) { return proxyDashboardApi(request, context) } export async function DELETE(request: Request, context: RouteContext) { return proxyDashboardApi(request, context) }