diff --git a/src/api/routes/labeltemplate.js b/src/api/routes/labeltemplate.js index 5dfc3b1..1864ea0 100644 --- a/src/api/routes/labeltemplate.js +++ b/src/api/routes/labeltemplate.js @@ -1,3 +1,4 @@ +import { validateObjects } from 'react-canvas-label-editor'; import * as db from '../../db/labeltemplate.js'; import { renderThumb } from '../../services/labelThumb.js'; import { emit } from '../sse.js'; @@ -29,6 +30,7 @@ function labeltemplateErrorStatus(error) { if (error.message?.startsWith('Invalid dpi')) return 400; if (error.message?.startsWith('Invalid type')) return 400; if (error.message === 'Content required') return 400; + if (error.message?.startsWith('Invalid label objects')) return 400; return 500; } @@ -209,8 +211,13 @@ export function register(app, basePath = '') { const { name, width, height, dpi, type, content } = req.body ?? {}; + const validation = validateLabelContent(content); + if (!validation.valid) { + return res.status(400).json({ error: validation.error, ...(validation.errors ? { errors: validation.errors } : {}) }); + } + try { - const thumb = await buildThumb({ width, height, content }); + const thumb = await buildThumb({ width, height, content, objects: validation.objects }); const template = await db.create(req.user.id, { name, width, @@ -242,9 +249,18 @@ export function register(app, basePath = '') { const { name, width, height, dpi, type, content } = req.body ?? {}; + let validatedObjects; + if (content !== undefined) { + const validation = validateLabelContent(content); + if (!validation.valid) { + return res.status(400).json({ error: validation.error, ...(validation.errors ? { errors: validation.errors } : {}) }); + } + validatedObjects = validation.objects; + } + try { // Re-render thumb whenever content/dimensions are provided - const thumb = await buildThumb({ width, height, content }); + const thumb = await buildThumb({ width, height, content, objects: validatedObjects }); const template = await db.update(req.user.id, parsedId.id, { name, width, @@ -445,6 +461,45 @@ function serializeTemplate(template) { }; } +/** + * Validates the label template content and its objects. + * Expects content to be an object with an `objects` array (or an array of objects). + * + * @param {any} content + * @returns {{ valid: true, objects: Array } | { valid: false, error: string, errors?: string[] }} + */ +function validateLabelContent(content) { + if (content == null) { + return { valid: false, error: 'Content required' }; + } + + let parsed = content; + if (typeof content === 'string') { + try { + parsed = JSON.parse(content); + } catch { + return { valid: false, error: 'Invalid JSON in content' }; + } + } + + const objects = Array.isArray(parsed?.objects) + ? parsed.objects + : Array.isArray(parsed) + ? parsed + : null; + + if (!objects) { + return { valid: false, error: 'content.objects must be an array' }; + } + + const { valid, errors } = validateObjects(objects); + if (!valid) { + return { valid: false, error: `Invalid label objects: ${errors.join(', ')}`, errors }; + } + + return { valid: true, objects }; +} + /** * Attempts to render a thumbnail from content + dimensions. * Returns null silently if content or dimensions are missing/invalid — @@ -452,18 +507,20 @@ function serializeTemplate(template) { * * @returns {Promise} */ -async function buildThumb({ width, height, content }) { +async function buildThumb({ width, height, content, objects: passedObjects }) { try { - if (content == null) return null; + if (content == null && passedObjects == null) return null; const w = Number(width); const h = Number(height); if (!Number.isInteger(w) || w <= 0 || !Number.isInteger(h) || h <= 0) return null; - const objects = Array.isArray(content?.objects) - ? content.objects - : Array.isArray(content) - ? content - : []; + const objects = passedObjects ?? ( + Array.isArray(content?.objects) + ? content.objects + : Array.isArray(content) + ? content + : [] + ); return await renderThumb({ width: w, height: h, objects }); } catch { @@ -471,3 +528,4 @@ async function buildThumb({ width, height, content }) { return null; } } +