/** * Server-side API proxy for the backend. ⭐ THE SYSTEMIC AUTH FIX ⭐ * * Do NOT replace this with a static `rewrites()` proxy in next.config.mjs. A * static rewrite forwards requests as-is and CANNOT inject an Authorization * header — that was the root cause of the "no token reaches the backend" bug * (every browser fetch hit the backend unauthenticated → 401/500). This cost * ~9 days on CannaManage; the template ships the fix so it never recurs. * * This Route Handler runs on the server, reads the NextAuth session via * `auth()` (so the JWT never leaves the server), and forwards the request to * `${BACKEND_URL}/api/v1/` with `Authorization: Bearer `. * * Method- and content-agnostic: query string preserved; raw request body * streamed unparsed (JSON + multipart uploads + binary all work); upstream * response body streamed back verbatim (byte-exact downloads). * * Adjust the upstream path prefix (`/api/v1/`) if your backend differs. */ import { NextResponse } from "next/server" import type { NextRequest } from "next/server" import { auth } from "@/lib/auth" // Always run dynamically — this proxy depends on per-request auth + body. export const dynamic = "force-dynamic" const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:8080" // Hop-by-hop and host-specific headers that must not be forwarded upstream. const STRIPPED_REQUEST_HEADERS = new Set([ "host", "connection", "content-length", "transfer-encoding", "accept-encoding", ]) // Headers that must not be copied from the upstream response back to the client. const STRIPPED_RESPONSE_HEADERS = new Set([ "connection", "transfer-encoding", "content-encoding", "content-length", ]) async function proxy(req: NextRequest, path: string[]): Promise { const session = await auth() const accessToken = session?.accessToken // Build the upstream URL: /api/backend/ → BACKEND_URL/api/v1/ const search = req.nextUrl.search // includes leading "?" or "" const upstreamUrl = `${BACKEND_URL}/api/v1/${path.join("/")}${search}` // Clone the incoming headers, stripping hop-by-hop/host ones, then inject auth. const headers = new Headers() req.headers.forEach((value, key) => { if (!STRIPPED_REQUEST_HEADERS.has(key.toLowerCase())) { headers.set(key, value) } }) if (accessToken) { headers.set("Authorization", `Bearer ${accessToken}`) } const method = req.method.toUpperCase() const hasBody = method !== "GET" && method !== "HEAD" try { const upstream = await fetch(upstreamUrl, { method, headers, // Stream the raw body through unparsed (works for JSON + multipart + binary). body: hasBody ? req.body : undefined, // Required by undici/Node when sending a streaming request body. ...(hasBody ? { duplex: "half" } : {}), redirect: "manual", cache: "no-store", } as RequestInit) // Copy upstream response headers, dropping ones that break a re-emitted body. const responseHeaders = new Headers() upstream.headers.forEach((value, key) => { if (!STRIPPED_RESPONSE_HEADERS.has(key.toLowerCase())) { responseHeaders.set(key, value) } }) // Stream the body straight back — byte-exact for downloads. return new NextResponse(upstream.body, { status: upstream.status, statusText: upstream.statusText, headers: responseHeaders, }) } catch { return NextResponse.json( { code: "BACKEND_UNREACHABLE", message: "Unable to reach the API." }, { status: 502 } ) } } // Next.js 15: the second arg's `params` is a Promise. type Ctx = { params: Promise<{ path: string[] }> } export async function GET(req: NextRequest, ctx: Ctx) { return proxy(req, (await ctx.params).path) } export async function POST(req: NextRequest, ctx: Ctx) { return proxy(req, (await ctx.params).path) } export async function PUT(req: NextRequest, ctx: Ctx) { return proxy(req, (await ctx.params).path) } export async function PATCH(req: NextRequest, ctx: Ctx) { return proxy(req, (await ctx.params).path) } export async function DELETE(req: NextRequest, ctx: Ctx) { return proxy(req, (await ctx.params).path) }