Some checks failed
Build & Deploy to Staging / Build & Deploy to Staging (push) Has been cancelled
- Create src/swagger.ts config module for swagger-jsdoc
- Add GET /openapi.json dynamic route (generated from @openapi annotations)
- Delete static public/openapi.json (was drifting from code)
- Add @openapi annotation for deprecated /v1/signup/free in index.ts
- Import swaggerSpec into index.ts
- All 12 endpoints now code-driven: demo/html, demo/markdown, convert/html,
convert/markdown, convert/url, templates, templates/{id}/render,
recover, recover/verify, billing/checkout, signup/free, health
165 lines
5.6 KiB
JavaScript
165 lines
5.6 KiB
JavaScript
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 });
|
|
}
|
|
});
|