refactor: migrate pages and API routes to App Router

This commit is contained in:
2026-11-05 23:20:34 +08:00
parent 982fa188e2
commit 6c4d6100b7
18 changed files with 287 additions and 174 deletions
+122
View File
@@ -0,0 +1,122 @@
import { NextRequest, NextResponse } from "next/server";
const allowed =
/^(profile|auth\/(login|register|verify-email|exchange|me|logout)|workspaces(?:\/[a-zA-Z0-9_-]+(?:\/(?:materials|search)(?:\/[a-zA-Z0-9_-]+)?)?)?)$/;
const bodyLimit = 1024 * 1024;
// Route Handlers do not have the Pages Router bodyParser size limit.
async function readBody(req: NextRequest) {
const reader = req.body?.getReader();
if (!reader) return {};
const chunks: Uint8Array[] = [];
let size = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
size += value.byteLength;
if (size > bodyLimit) {
await reader.cancel();
throw new Error("body_too_large");
}
chunks.push(value);
}
} finally {
reader.releaseLock();
}
if (!size) return {};
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
}
async function handler(
req: NextRequest,
{ params }: { params: Promise<{ path: string[] }> },
) {
const responseHeaders = new Headers({ "Cache-Control": "no-store" });
const json = (body: unknown, status: number) =>
NextResponse.json(body, { status, headers: responseHeaders });
const requestOrigin = req.headers.get("origin");
if (req.method !== "GET" && requestOrigin) {
try {
if (new URL(requestOrigin).host !== req.headers.get("host"))
return json({ error: "invalid_origin" }, 403);
} catch {
return json({ error: "invalid_origin" }, 403);
}
}
const path = (await params).path.join("/");
if (!allowed.test(path)) return json({ error: "not_found" }, 404);
let requestBody: unknown;
if (req.method !== "GET") {
try {
requestBody = await readBody(req);
} catch (error) {
const tooLarge = error instanceof Error && error.message === "body_too_large";
return json({ error: tooLarge ? "body_too_large" : "invalid_json" }, tooLarge ? 413 : 400);
}
}
const origin = process.env.FOUNDRY_API_ORIGIN || "https://api.foundry.entroia.com";
const project = process.env.NEXT_PUBLIC_FOUNDRY_PROJECT || "endoleap";
const headers: Record<string, string> = { "Content-Type": "application/json" };
const session = req.cookies.get("foundry_session")?.value;
if (session) headers.Authorization = "Bearer " + session;
for (const h of ["x-foundry-code", "x-foundry-verifier", "if-match"]) {
const value = req.headers.get(h);
if (value !== null) headers[h] = value;
}
const query = new URLSearchParams();
for (const key of ["kind", "sort", "q", "limit", "offset"]) {
const values = req.nextUrl.searchParams.getAll(key);
if (values.length === 1 && values[0].length < 300) query.set(key, values[0]);
}
const started = performance.now();
try {
const upstream = await fetch(
origin + "/projects/" + encodeURIComponent(project) + "/" + path +
(query.size ? "?" + query.toString() : ""),
{
method: req.method,
headers,
body: req.method === "GET" ? undefined : JSON.stringify(requestBody || {}),
signal: AbortSignal.any([AbortSignal.timeout(15000), req.signal]),
redirect: "manual",
cache: "no-store",
},
);
const headersReceived = performance.now();
const body = upstream.status === 204 ? null : await upstream.json();
responseHeaders.set(
"Server-Timing",
`upstream;dur=${(headersReceived - started).toFixed(1)}, total;dur=${(performance.now() - started).toFixed(1)}`,
);
if (body?.access_token) {
responseHeaders.set(
"Set-Cookie",
`foundry_session=${encodeURIComponent(body.access_token)}; HttpOnly; SameSite=Lax; Path=/; Max-Age=${Math.min(body.expires_in || 28800, 28800)}${process.env.NODE_ENV === "production" && req.headers.get("x-forwarded-proto") === "https" ? "; Secure" : ""}`,
);
delete body.access_token;
}
if (path === "auth/logout" && upstream.ok)
responseHeaders.set(
"Set-Cookie",
"foundry_session=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0",
);
if (upstream.status === 204)
return new NextResponse(null, { status: 204, headers: responseHeaders });
return json(body || { error: "upstream_unavailable" }, upstream.status);
} catch (error) {
responseHeaders.set("Server-Timing", `total;dur=${(performance.now() - started).toFixed(1)}`);
const timeout = error instanceof Error && error.name === "TimeoutError";
return json({ error: timeout ? "upstream_timeout" : "upstream_unavailable" }, timeout ? 504 : 502);
}
}
function methodNotAllowed() {
return NextResponse.json({ error: "method_not_allowed" }, {
status: 405,
headers: { "Cache-Control": "no-store", Allow: "GET, POST, PATCH, DELETE" },
});
}
export { handler as GET, handler as POST, handler as PATCH, handler as DELETE };
export { methodNotAllowed as PUT, methodNotAllowed as HEAD, methodNotAllowed as OPTIONS };