initial commit
This commit is contained in:
96
packages/templatebuilder/index.ts
Normal file
96
packages/templatebuilder/index.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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<Record<string, unknown>> {
|
||||
const file = Bun.file(TEMPLATE_PATH);
|
||||
if (!(await file.exists())) return {};
|
||||
return file.json();
|
||||
}
|
||||
|
||||
export const routes = {
|
||||
"/builder": 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<string, unknown> {
|
||||
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<string, unknown>)) {
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user