import index from "./index.html"; import { dirname, join } from "node:path"; import { mkdir } from "node:fs/promises"; import { dataPath } from "../../deployPaths"; import { isSafeStem, jsonError, withAuth } from "../../security"; export const workerPath = new URL( import.meta.resolve("pdfjs-dist/build/pdf.worker.mjs"), ).pathname; const TEMPLATE_PATH = process.env.BMP_TEMPLATE_PATH ?? dataPath(join(import.meta.dir, "template.json"), "templatebuilder", "template.json"); await mkdir(dirname(TEMPLATE_PATH), { recursive: true }); async function loadTemplate(): Promise> { const file = Bun.file(TEMPLATE_PATH); if (!(await file.exists())) return {}; return file.json(); } export const routes = { "/builder": { GET: withAuth(async () => index), }, "/api/save": { POST: withAuth(async (req: Request) => { let data; try { data = await req.json(); } catch { return jsonError("Expected JSON body", 400); } if (!isSafeTemplate(data)) { return jsonError("Template contains unsafe keys or too many fields", 400); } await Bun.write(TEMPLATE_PATH, JSON.stringify(data, null, 2)); return Response.json({ ok: true }); }, { csrf: true, limit: { key: "template-save", max: 20, windowMs: 60_000 }, }), }, "/api/load": { GET: withAuth(async () => { return Response.json(await loadTemplate()); }), }, } as const; function isSafeTemplate(value: unknown): value is Record { let fieldCount = 0; function visit(node: unknown, depth: number): boolean { if (depth > 8 || typeof node !== "object" || node === null || Array.isArray(node)) { return false; } for (const [key, child] of Object.entries(node as Record)) { if (!/^[a-zA-Z0-9_-]{1,80}$/.test(key)) return false; if (typeof child === "string") { fieldCount += 1; if (child.length > 200 || fieldCount > 250) return false; continue; } if (typeof child === "object" && child !== null && "fieldName" in child) { fieldCount += 1; if (fieldCount > 250 || !isSafeFieldObject(child)) return false; continue; } if (!visit(child, depth + 1)) return false; } return true; } return visit(value, 0); } function isSafeFieldObject(value: object): boolean { const field = value as Record; if (typeof field.fieldName !== "string" || field.fieldName.length > 200) return false; if (field.label != null && (typeof field.label !== "string" || field.label.length > 300)) return false; if (field.antwortFormat != null && field.antwortFormat !== "einzelfrage" && field.antwortFormat !== "mehrfachfrage_ein_antwortfeld") { return false; } if (field.fragen != null) { if (!Array.isArray(field.fragen) || field.fragen.length > 25) return false; for (const frage of field.fragen) { if (typeof frage !== "object" || frage === null) return false; const item = frage as Record; if (typeof item.text !== "string" || item.text.length > 1000) return false; if (item.id != null && (typeof item.id !== "string" || !isSafeStem(item.id))) return false; } } return true; }