141 lines
3.8 KiB
TypeScript
141 lines
3.8 KiB
TypeScript
import { NextResponse } from "next/server";
|
|
import { auth } from "@/app/(auth)/auth";
|
|
import { getKittyCadProviderConfig } from "@/lib/kittycad/server";
|
|
|
|
export const dynamic = "force-dynamic";
|
|
export const maxDuration = 60;
|
|
export const runtime = "nodejs";
|
|
|
|
const HOP_BY_HOP_HEADERS = new Set([
|
|
"connection",
|
|
"content-length",
|
|
"host",
|
|
"keep-alive",
|
|
"proxy-authenticate",
|
|
"proxy-authorization",
|
|
"te",
|
|
"trailer",
|
|
"transfer-encoding",
|
|
"upgrade",
|
|
]);
|
|
|
|
const BLOCKED_PATH_PREFIXES = [
|
|
"/admin",
|
|
"/internal",
|
|
"/logout",
|
|
"/oauth2",
|
|
"/org/api-tokens",
|
|
"/user/api-tokens",
|
|
"/users",
|
|
];
|
|
|
|
type RouteContext = {
|
|
params: Promise<{ path: string[] }>;
|
|
};
|
|
|
|
function isBlockedPath(pathname: string) {
|
|
return BLOCKED_PATH_PREFIXES.some(
|
|
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`)
|
|
);
|
|
}
|
|
|
|
function copyRequestHeaders(request: Request, apiKey: string) {
|
|
const headers = new Headers();
|
|
|
|
for (const [name, value] of request.headers.entries()) {
|
|
const normalizedName = name.toLowerCase();
|
|
if (
|
|
HOP_BY_HOP_HEADERS.has(normalizedName) ||
|
|
normalizedName === "authorization" ||
|
|
normalizedName === "cookie" ||
|
|
normalizedName === "origin" ||
|
|
normalizedName === "referer" ||
|
|
normalizedName.startsWith("x-forwarded-") ||
|
|
normalizedName === "x-hicad-ws-proxy-secret"
|
|
) {
|
|
continue;
|
|
}
|
|
headers.set(name, value);
|
|
}
|
|
|
|
headers.set("Authorization", `Bearer ${apiKey}`);
|
|
headers.set("Accept", request.headers.get("accept") || "application/json");
|
|
return headers;
|
|
}
|
|
|
|
function copyResponseHeaders(upstream: Response) {
|
|
const headers = new Headers();
|
|
|
|
for (const [name, value] of upstream.headers.entries()) {
|
|
const normalizedName = name.toLowerCase();
|
|
if (
|
|
HOP_BY_HOP_HEADERS.has(normalizedName) ||
|
|
normalizedName === "content-encoding" ||
|
|
normalizedName === "set-cookie"
|
|
) {
|
|
continue;
|
|
}
|
|
headers.set(name, value);
|
|
}
|
|
|
|
headers.set("Cache-Control", "private, no-store");
|
|
return headers;
|
|
}
|
|
|
|
async function proxyZooRequest(request: Request, context: RouteContext) {
|
|
const session = await auth();
|
|
if (!session?.user?.id) {
|
|
return NextResponse.json({ error: "未登录" }, { status: 401 });
|
|
}
|
|
|
|
const { path } = await context.params;
|
|
const pathname = `/${path.map(encodeURIComponent).join("/")}`;
|
|
if (isBlockedPath(pathname)) {
|
|
return NextResponse.json(
|
|
{ error: "该 Zoo API 不允许通过 HiCAD 代理访问" },
|
|
{ status: 403 }
|
|
);
|
|
}
|
|
|
|
try {
|
|
const config = await getKittyCadProviderConfig();
|
|
const incomingUrl = new URL(request.url);
|
|
const baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
const upstreamUrl = `${baseUrl}${pathname}${incomingUrl.search}`;
|
|
const method = request.method.toUpperCase();
|
|
const hasBody = method !== "GET" && method !== "HEAD";
|
|
const init: RequestInit & { duplex?: "half" } = {
|
|
body: hasBody ? request.body : undefined,
|
|
cache: "no-store",
|
|
duplex: hasBody ? "half" : undefined,
|
|
headers: copyRequestHeaders(request, config.apiKey),
|
|
method,
|
|
redirect: "manual",
|
|
signal: request.signal,
|
|
};
|
|
const upstream = await fetch(upstreamUrl, init);
|
|
|
|
return new Response(upstream.body, {
|
|
headers: copyResponseHeaders(upstream),
|
|
status: upstream.status,
|
|
statusText: upstream.statusText,
|
|
});
|
|
} catch (error) {
|
|
return NextResponse.json(
|
|
{
|
|
error:
|
|
error instanceof Error ? error.message : "Zoo API 代理请求失败",
|
|
},
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
}
|
|
|
|
export const DELETE = proxyZooRequest;
|
|
export const GET = proxyZooRequest;
|
|
export const HEAD = proxyZooRequest;
|
|
export const OPTIONS = proxyZooRequest;
|
|
export const PATCH = proxyZooRequest;
|
|
export const POST = proxyZooRequest;
|
|
export const PUT = proxyZooRequest;
|