DocDoeAI / src /app /api /backend /[...path] /route.ts
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
66680b1
Raw History Blame Contribute Delete
4.69 kB
import { NextRequest, NextResponse } from "next/server";
import { getPublicApiBaseUrl } from "@/lib/env";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export const maxDuration = 300;
function matchesRequestOrigin(request: NextRequest, origin: string) {
const host = request.headers.get("host");
if (host === null) return origin === request.nextUrl.origin;
// Next can normalize loopback URLs to localhost. Compare with the actual
// HTTP authority, not that normalized hostname or an untrusted forwarded
// host. Keep protocol and port strict; do not widen the allowed origins.
if (!host || /[\\/@?#,\s]/.test(host)) return false;
try {
return origin === new URL(`${request.nextUrl.protocol}//${host}`).origin;
} catch {
return false;
}
}
async function proxy(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {
const { path } = await context.params;
if (path.some((part) => !part || part === "." || part === ".." || /[\\/]/.test(part))) {
return NextResponse.json({ error: "Invalid API path" }, { status: 400 });
}
const pathname = `/${path.map(encodeURIComponent).join("/")}`;
const origin = request.headers.get("origin");
if (!["GET", "HEAD"].includes(request.method) && origin && !matchesRequestOrigin(request, origin)) {
return NextResponse.json({ error: "Invalid request origin" }, { status: 403 });
}
const upstream = new URL(getPublicApiBaseUrl());
upstream.pathname = `${upstream.pathname.replace(/\/$/, "")}${pathname}`;
upstream.search = request.nextUrl.search;
const headers = new Headers();
// The student's timezone is a validated planning hint, not an identity header.
for (const name of ["authorization", "content-type", "accept", "range", "if-range", "idempotency-key", "x-request-id", "x-student-timezone"]) {
const value = request.headers.get(name);
if (value) headers.set(name, value);
}
const cookie = request.cookies.get("docdoe_media_token");
if (process.env.NODE_ENV !== "production" && process.env.LOG_LEVEL === "debug") {
console.debug("[FIX:student-calendar] Forwarding planning context", {
timezoneProvided: headers.has("x-student-timezone"),
});
}
if (cookie && (pathname.startsWith("/generated/") || pathname === "/auth/logout")) {
headers.set("cookie", `docdoe_media_token=${encodeURIComponent(cookie.value)}`);
}
try {
const response = await fetch(upstream, {
method: request.method,
headers,
body: ["GET", "HEAD"].includes(request.method) ? undefined : request.body,
// Node fetch requires duplex when forwarding streaming upload bodies.
...({ duplex: "half" } as Record<string, string>),
signal: AbortSignal.any([request.signal, AbortSignal.timeout(290_000)]),
redirect: "manual",
cache: "no-store",
});
const outgoing = new Headers({ "Cache-Control": "private, no-store", "Vary": "Authorization, Cookie" });
for (const name of ["content-type", "content-range", "accept-ranges", "content-disposition", "retry-after", "x-request-id"]) {
const value = response.headers.get(name);
if (value) outgoing.set(name, value);
}
const result = new NextResponse(request.method === "HEAD" ? null : response.body, { status: response.status, headers: outgoing });
// Only the authenticated profile endpoint can promote an existing bearer
// session to a media cookie. Public API requests cannot mint one.
let mediaToken = response.ok && request.method === "GET" && ["/users/me", "/auth/session"].includes(pathname)
? request.headers.get("authorization")?.replace(/^Bearer\s+/i, "")
: undefined;
for (const value of response.headers.getSetCookie()) {
const match = /^docdoe_media_token=([^;]*)/.exec(value);
if (match) mediaToken = match[1];
}
const clearSession = pathname === "/auth/logout" ||
(pathname === "/users/me" && request.method === "DELETE" && response.ok);
if (mediaToken !== undefined || clearSession) {
const token = clearSession ? "" : mediaToken ?? "";
result.cookies.set("docdoe_media_token", token, {
httpOnly: true, secure: request.nextUrl.protocol === "https:", sameSite: "lax",
path: "/api", maxAge: token ? 604800 : 0,
});
}
return result;
} catch {
console.error("[FIX:backend-proxy] Upstream request failed", { method: request.method });
return NextResponse.json({ error: { message: "The study service could not be reached. Please retry.", code: "UPSTREAM_UNAVAILABLE" } }, { status: 502 });
}
}
export { proxy as GET, proxy as HEAD, proxy as POST, proxy as PUT, proxy as PATCH, proxy as DELETE };