255 lines
9.2 KiB
TypeScript
255 lines
9.2 KiB
TypeScript
import { routes as templatebuilder, workerPath } from "./packages/templatebuilder/index";
|
|
import { routes as extractor } from "./packages/extractor/index";
|
|
import { routes as summarizer } from "./packages/summarizer/index";
|
|
import { join } from "node:path";
|
|
import { rm } from "node:fs/promises";
|
|
import { dataPath } from "./deployPaths";
|
|
import {
|
|
addSecurityHeaders,
|
|
getCsrfCookieName,
|
|
isAuthenticated,
|
|
isSafeStem,
|
|
login,
|
|
loginPage,
|
|
logout,
|
|
requireAuth,
|
|
securityHeaders,
|
|
withAuth,
|
|
} from "./security";
|
|
|
|
const SUMMARIZER_OUTPUTS_DIR =
|
|
process.env.BMP_SUMMARIZER_OUTPUTS_DIR ??
|
|
dataPath(join(import.meta.dir, "packages/summarizer/outputs"), "summarizer", "outputs");
|
|
const EXTRACTOR_OUTPUTS_DIR =
|
|
process.env.BMP_EXTRACTOR_OUTPUTS_DIR ??
|
|
dataPath(join(import.meta.dir, "packages/extractor/outputs"), "extractor", "outputs");
|
|
const ROOT_PAGE_PATH = join(import.meta.dir, "index.html");
|
|
const BMP_LOGO_PATH = join(import.meta.dir, "assets/bmp-logo.png");
|
|
const PORT = Number(process.env.PORT ?? 3000);
|
|
|
|
function contentTypeFor(path: string): string {
|
|
if (path.endsWith(".json")) return "application/json; charset=utf-8";
|
|
if (path.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
if (path.endsWith(".pdf")) return "application/pdf";
|
|
if (path.endsWith(".html")) return "text/html; charset=utf-8";
|
|
return "application/octet-stream";
|
|
}
|
|
|
|
async function serveSummaryDownload(req: Request, stem: string, file: string): Promise<Response> {
|
|
const blocked = await requireAuth(req);
|
|
if (blocked) return blocked;
|
|
|
|
if (
|
|
!isSafeStem(stem) ||
|
|
file !== `${stem}.json` &&
|
|
file !== `${stem}.xlsx` &&
|
|
file !== `${stem}.pdf` &&
|
|
file !== `${stem}.report.html` &&
|
|
file !== `${stem}.questions.pdf` &&
|
|
file !== `${stem}.questions.html`
|
|
) {
|
|
return new Response("Not found", { status: 404 });
|
|
}
|
|
|
|
const path = join(SUMMARIZER_OUTPUTS_DIR, stem, file);
|
|
const output = Bun.file(path);
|
|
if (!(await output.exists())) {
|
|
return new Response("Not found", { status: 404 });
|
|
}
|
|
|
|
return new Response(output, {
|
|
headers: {
|
|
...securityHeaders,
|
|
"Content-Type": contentTypeFor(file),
|
|
"Cache-Control": "private, no-store",
|
|
"Content-Disposition": `attachment; filename="${file.replaceAll('"', "")}"`,
|
|
},
|
|
});
|
|
}
|
|
|
|
Bun.serve({
|
|
port: PORT,
|
|
routes: {
|
|
"/login": {
|
|
GET: loginPage,
|
|
POST: login,
|
|
},
|
|
"/healthz": {
|
|
GET: () => Response.json({ ok: true }),
|
|
},
|
|
"/logout": {
|
|
POST: withAuth(async () => logout(), { csrf: true }),
|
|
},
|
|
"/api/auth/session": {
|
|
GET: async (req: Request) => addSecurityHeaders(Response.json({
|
|
authenticated: await isAuthenticated(req),
|
|
csrfCookieName: getCsrfCookieName(),
|
|
})),
|
|
},
|
|
"/": {
|
|
GET: withAuth(async () => new Response(Bun.file(ROOT_PAGE_PATH), {
|
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
})),
|
|
},
|
|
"/assets/bmp-logo.png": {
|
|
GET: withAuth(async () => new Response(Bun.file(BMP_LOGO_PATH), {
|
|
headers: { "Content-Type": "image/png", "Cache-Control": "private, max-age=86400" },
|
|
})),
|
|
},
|
|
"/api/results/:stem": {
|
|
DELETE: withAuth(async (req: Request) => {
|
|
const stem = (req as Request & { params: Record<string, string | undefined> }).params.stem ?? "";
|
|
if (!isSafeStem(stem)) {
|
|
return Response.json({ error: "Invalid result id" }, { status: 400 });
|
|
}
|
|
|
|
const targets = [
|
|
join(EXTRACTOR_OUTPUTS_DIR, stem),
|
|
join(SUMMARIZER_OUTPUTS_DIR, stem),
|
|
];
|
|
await Promise.all(targets.map((target) => rm(target, { recursive: true, force: true })));
|
|
return Response.json({ ok: true, stem, deleted: targets.length });
|
|
}, {
|
|
csrf: true,
|
|
limit: { key: "result-delete", max: 20, windowMs: 60_000 },
|
|
}),
|
|
},
|
|
"/api/pipeline/result/:stem": {
|
|
GET: withAuth(async (req: Request) => {
|
|
const stem = (req as Request & { params: Record<string, string | undefined> }).params.stem ?? "";
|
|
if (!isSafeStem(stem)) {
|
|
return Response.json({ error: "Invalid result id" }, { status: 400 });
|
|
}
|
|
|
|
const jsonPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.json`);
|
|
const xlsxPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.xlsx`);
|
|
const pdfPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.pdf`);
|
|
const reportHtmlPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.report.html`);
|
|
const questionsPdfPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.questions.pdf`);
|
|
const questionsHtmlPath = join(SUMMARIZER_OUTPUTS_DIR, stem, `${stem}.questions.html`);
|
|
const jsonFile = Bun.file(jsonPath);
|
|
if (!(await jsonFile.exists())) {
|
|
return Response.json({ error: "Summary result not found" }, { status: 404 });
|
|
}
|
|
|
|
const downloads = [
|
|
{
|
|
label: "JSON herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.json`)}`,
|
|
},
|
|
];
|
|
|
|
if (await Bun.file(xlsxPath).exists()) {
|
|
downloads.push({
|
|
label: "XLSX herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.xlsx`)}`,
|
|
});
|
|
}
|
|
if (await Bun.file(pdfPath).exists()) {
|
|
downloads.push({
|
|
label: "PDF herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.pdf`)}`,
|
|
});
|
|
}
|
|
if (await Bun.file(reportHtmlPath).exists()) {
|
|
downloads.push({
|
|
label: "Report-HTML herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.report.html`)}`,
|
|
});
|
|
}
|
|
if (await Bun.file(questionsPdfPath).exists()) {
|
|
downloads.push({
|
|
label: "Fragen-PDF herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.questions.pdf`)}`,
|
|
});
|
|
}
|
|
if (await Bun.file(questionsHtmlPath).exists()) {
|
|
downloads.push({
|
|
label: "Fragen-HTML herunterladen",
|
|
url: `/downloads/summarizer/${encodeURIComponent(stem)}/${encodeURIComponent(`${stem}.questions.html`)}`,
|
|
});
|
|
}
|
|
|
|
return Response.json({
|
|
ok: true,
|
|
stem,
|
|
summary: await jsonFile.json(),
|
|
downloads,
|
|
});
|
|
}),
|
|
},
|
|
"/downloads/summarizer/:stem/:file": {
|
|
GET: async (req: Request) => {
|
|
const params = (req as Request & { params: Record<string, string | undefined> }).params;
|
|
const stem = params.stem ?? "";
|
|
const file = params.file ?? "";
|
|
return serveSummaryDownload(req, stem, file);
|
|
},
|
|
},
|
|
...templatebuilder,
|
|
...extractor,
|
|
...summarizer,
|
|
},
|
|
async fetch(req) {
|
|
const { pathname } = new URL(req.url);
|
|
if (pathname === "/pdf.worker.mjs") {
|
|
const blocked = await requireAuth(req);
|
|
if (blocked) return blocked;
|
|
return addSecurityHeaders(new Response(Bun.file(workerPath), {
|
|
headers: { "Content-Type": "application/javascript", "Cache-Control": "private, max-age=3600" },
|
|
}));
|
|
}
|
|
if (pathname === "/login" && req.method === "POST") {
|
|
return login(req);
|
|
}
|
|
if (pathname === "/logout" && req.method === "POST") {
|
|
const blocked = await requireAuth(req, { csrf: true });
|
|
if (blocked) return blocked;
|
|
return logout();
|
|
}
|
|
if (pathname === "/login" && req.method === "GET") {
|
|
return loginPage(req);
|
|
}
|
|
if (pathname === "/healthz" && req.method === "GET") {
|
|
return Response.json({ ok: true });
|
|
}
|
|
if (pathname === "/") {
|
|
const blocked = await requireAuth(req);
|
|
if (blocked) return blocked;
|
|
return addSecurityHeaders(new Response(Bun.file(ROOT_PAGE_PATH), {
|
|
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
}));
|
|
}
|
|
if (pathname === "/assets/bmp-logo.png") {
|
|
const blocked = await requireAuth(req);
|
|
if (blocked) return blocked;
|
|
return addSecurityHeaders(new Response(Bun.file(BMP_LOGO_PATH), {
|
|
headers: { "Content-Type": "image/png", "Cache-Control": "private, max-age=86400" },
|
|
}));
|
|
}
|
|
if (pathname.startsWith("/_bun/")) {
|
|
const blocked = await requireAuth(req);
|
|
if (blocked) return blocked;
|
|
return new Response("Not found", {
|
|
status: 404,
|
|
headers: securityHeaders,
|
|
});
|
|
}
|
|
if (pathname.startsWith("/downloads/summarizer/")) {
|
|
const [stem = "", file = ""] = pathname
|
|
.slice("/downloads/summarizer/".length)
|
|
.split("/")
|
|
.map((part) => decodeURIComponent(part));
|
|
return serveSummaryDownload(req, stem, file);
|
|
}
|
|
return new Response("Not found", { status: 404, headers: securityHeaders });
|
|
},
|
|
development: { hmr: true, console: true },
|
|
});
|
|
|
|
console.log("bmp-rewrite");
|
|
console.log(` pipeline → http://localhost:${PORT}/`);
|
|
console.log(` templatebuilder → http://localhost:${PORT}/builder`);
|
|
console.log(` extractor → http://localhost:${PORT}/extractor`);
|
|
console.log(` summarizer → http://localhost:${PORT}/summarizer`);
|