import { Router } from "express"; import { renderPdf } from "../services/browser.js"; import logger from "../services/logger.js"; import { templates, renderTemplate } from "../services/templates.js"; function sanitizeFilename(name) { return name.replace(/["\r\n\x00-\x1f]/g, "_").substring(0, 200); } export const templatesRouter = Router(); /** * @openapi * /v1/templates: * get: * tags: [Templates] * summary: List available templates * description: Returns a list of all built-in document templates with their required fields. * security: * - BearerAuth: [] * - ApiKeyHeader: [] * responses: * 200: * description: List of templates * content: * application/json: * schema: * type: object * properties: * templates: * type: array * items: * type: object * properties: * id: * type: string * example: invoice * name: * type: string * example: Invoice * description: * type: string * fields: * type: array * items: * type: object * properties: * name: * type: string * required: * type: boolean * description: * type: string * 401: * description: Missing API key * 403: * description: Invalid API key */ templatesRouter.get("/", (_req, res) => { const list = Object.entries(templates).map(([id, t]) => ({ id, name: t.name, description: t.description, fields: t.fields, })); res.json({ templates: list }); }); /** * @openapi * /v1/templates/{id}/render: * post: * tags: [Templates] * summary: Render a template to PDF * description: | * Renders a built-in template with the provided data and returns a PDF. * Use GET /v1/templates to see available templates and their required fields. * Special fields: `_format` (page size), `_margin` (page margins), `_filename` (output filename). * security: * - BearerAuth: [] * - ApiKeyHeader: [] * parameters: * - in: path * name: id * required: true * schema: * type: string * description: Template ID (e.g. "invoice", "receipt") * requestBody: * required: true * content: * application/json: * schema: * type: object * properties: * data: * type: object * description: Template data (fields depend on template). Can also be passed at root level. * _format: * type: string * enum: [A4, Letter, Legal, A3, A5, Tabloid] * default: A4 * description: Page size override * _margin: * type: object * properties: * top: { type: string } * right: { type: string } * bottom: { type: string } * left: { type: string } * description: Page margin override * _filename: * type: string * description: Custom output filename * responses: * 200: * description: PDF document * content: * application/pdf: * schema: * type: string * format: binary * 400: * description: Missing required template fields * 401: * description: Missing API key * 403: * description: Invalid API key * 404: * description: Template not found * 500: * description: Template rendering failed */ templatesRouter.post("/:id/render", async (req, res) => { try { const id = req.params.id; const template = templates[id]; if (!template) { res.status(404).json({ error: `Template '${id}' not found` }); return; } const data = req.body.data || req.body; // Validate required fields const missingFields = template.fields .filter((f) => f.required && (data[f.name] === undefined || data[f.name] === null || data[f.name] === "")) .map((f) => f.name); if (missingFields.length > 0) { res.status(400).json({ error: "Missing required fields", missing: missingFields, hint: `Required fields for '${id}': ${template.fields.filter((f) => f.required).map((f) => f.name).join(", ")}`, }); return; } const html = renderTemplate(id, data); const pdf = await renderPdf(html, { format: data._format || "A4", margin: data._margin, }); const filename = sanitizeFilename(data._filename || `${id}.pdf`); res.setHeader("Content-Type", "application/pdf"); res.setHeader("Content-Disposition", `inline; filename="${filename}"`); res.send(pdf); } catch (err) { logger.error({ err }, "Template render error"); res.status(500).json({ error: "Template rendering failed", detail: err.message }); } });