Download src/app/api/backend/[...path]/route.ts from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 4.69 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/app/api/backend/%5B...path%5D/route.ts
- Command line
-
hf download 'hf://spaces/asnannp/DocDoeAI/src/app/api/backend/[...path]/route.ts'
-
curl -L -o route.ts https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/app/api/backend/%5B...path%5D/route.ts
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 }; | |