From b92e2deb4150e20e8a668072b646ee461cf2fee6 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 29 Sep 2026 13:44:14 +0200 Subject: [PATCH] u --- docs/api/openapi.json | 16 ++++++++++++++-- docs/api/routes/article/add-image.md | 5 ++++- docs/api/routes/category/add-image.md | 5 ++++- src/api/routes/article.js | 12 +++++++++--- src/api/routes/brand.js | 5 +++-- src/api/routes/category.js | 12 +++++++++--- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/docs/api/openapi.json b/docs/api/openapi.json index cedfccc..ec79638 100644 --- a/docs/api/openapi.json +++ b/docs/api/openapi.json @@ -8949,7 +8949,7 @@ "Category" ], "summary": "Add category image", - "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n", + "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).\nWebP images are automatically converted to AVIF on the server.\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`.\nAuth like GET /category/tree — user must be authorized for the category's company.\n", "security": [ { "cookieAuth": [] @@ -8978,6 +8978,12 @@ "type": "string", "format": "binary" } + }, + "image/webp": { + "schema": { + "type": "string", + "format": "binary" + } } } }, @@ -10687,7 +10693,7 @@ "Article" ], "summary": "Add article image", - "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`).\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n", + "description": "Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`).\nWebP images are automatically converted to AVIF on the server.\nFull: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000.\nThumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`.\nAuth like GET by SKU.\n", "security": [ { "cookieAuth": [] @@ -10716,6 +10722,12 @@ "type": "string", "format": "binary" } + }, + "image/webp": { + "schema": { + "type": "string", + "format": "binary" + } } } }, diff --git a/docs/api/routes/article/add-image.md b/docs/api/routes/article/add-image.md index 98ec30d..555dc92 100644 --- a/docs/api/routes/article/add-image.md +++ b/docs/api/routes/article/add-image.md @@ -4,7 +4,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Add article image -Accepts a full-size AVIF body (`Content-Type: image/avif`). +Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`). +WebP images are automatically converted to AVIF on the server. Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`. Auth like GET by SKU. @@ -20,6 +21,8 @@ Auth like GET by SKU. Raw binary (`image/avif`). +Raw binary (`image/webp`). + ## Signals On success, emits SSE signal: `article-` (see [`GET /events`](../events.md)). diff --git a/docs/api/routes/category/add-image.md b/docs/api/routes/category/add-image.md index 07bde77..00547dd 100644 --- a/docs/api/routes/category/add-image.md +++ b/docs/api/routes/category/add-image.md @@ -4,7 +4,8 @@ Protected. Requires a valid session cookie (`credentials: 'include'`). Add category image -Accepts a full-size AVIF body (`Content-Type: image/avif`). +Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`). +WebP images are automatically converted to AVIF on the server. Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`. Auth like GET /category/tree — user must be authorized for the category's company. @@ -20,6 +21,8 @@ Auth like GET /category/tree — user must be authorized for the category's comp Raw binary (`image/avif`). +Raw binary (`image/webp`). + ## Signals On success, emits SSE signal: `category` (see [`GET /events`](../events.md)). diff --git a/src/api/routes/article.js b/src/api/routes/article.js index 1d7ccf7..5d63def 100644 --- a/src/api/routes/article.js +++ b/src/api/routes/article.js @@ -51,6 +51,7 @@ function parseFullHash(value) { function articleErrorStatus(error) { if (error.message === 'No company selected') return 400; + if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Parent not found') return 404; if (error.message === 'Article not found') return 404; @@ -738,7 +739,8 @@ export function register(app, basePath = '') { * tags: [Article] * summary: Add article image * description: | - * Accepts a full-size AVIF body (`Content-Type: image/avif`). + * Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`). + * WebP images are automatically converted to AVIF on the server. * Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. * Thumb always scaled to max 200×200. Stores hash/thumbhash, links `articlepic`. * Auth like GET by SKU. @@ -760,6 +762,10 @@ export function register(app, basePath = '') { * schema: * type: string * format: binary + * image/webp: + * schema: + * type: string + * format: binary * responses: * 201: * description: Image stored and linked @@ -797,7 +803,7 @@ export function register(app, basePath = '') { */ app.post( `${basePath}/article/:sku/image`, - express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), + express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), async (req, res) => { const sku = typeof req.params.sku === 'string' ? req.params.sku.trim() : ''; if (!sku) { @@ -806,7 +812,7 @@ export function register(app, basePath = '') { const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; if (!imageBuffer?.length) { - return res.status(400).json({ error: 'AVIF image body is required' }); + return res.status(400).json({ error: 'AVIF or WebP image body is required' }); } try { diff --git a/src/api/routes/brand.js b/src/api/routes/brand.js index f8d5f02..0846628 100644 --- a/src/api/routes/brand.js +++ b/src/api/routes/brand.js @@ -13,6 +13,7 @@ function requireSelectedCompany(req) { function brandErrorStatus(error) { if (error.message === 'No company selected') return 400; + if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Brand not found') return 404; @@ -244,7 +245,7 @@ export function register(app, basePath = '') { app.post( `${basePath}/brand/:id/image`, - express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), + express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), async (req, res) => { const selected = requireSelectedCompany(req); if (selected.error) { @@ -258,7 +259,7 @@ export function register(app, basePath = '') { const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; if (!imageBuffer?.length) { - return res.status(400).json({ error: 'AVIF image body is required' }); + return res.status(400).json({ error: 'AVIF or WebP image body is required' }); } try { diff --git a/src/api/routes/category.js b/src/api/routes/category.js index dd1e392..5b6e07b 100644 --- a/src/api/routes/category.js +++ b/src/api/routes/category.js @@ -24,6 +24,7 @@ function parsePositiveInt(value, field) { function categoryErrorStatus(error) { if (error.message === 'No company selected') return 400; + if (error.message === 'Unsupported image format' || error.message?.includes('unsupported image format')) return 400; if (error.message === 'Not authorized') return 403; if (error.message === 'Company not found') return 404; if (error.message === 'Parent not found') return 404; @@ -647,7 +648,8 @@ export function register(app, basePath = '') { * tags: [Category] * summary: Add category image * description: | - * Accepts a full-size AVIF body (`Content-Type: image/avif`). + * Accepts a full-size AVIF body (`Content-Type: image/avif`) or WebP body (`Content-Type: image/webp`). + * WebP images are automatically converted to AVIF on the server. * Full: stored as-is when already AVIF and ≤1000×1000; otherwise scaled to max 1000×1000. * Thumb always scaled to max 200×200. Stores hash/thumbhash, links `categorypic`. * Auth like GET /category/tree — user must be authorized for the category's company. @@ -669,6 +671,10 @@ export function register(app, basePath = '') { * schema: * type: string * format: binary + * image/webp: + * schema: + * type: string + * format: binary * responses: * 201: * description: Image stored and linked @@ -772,7 +778,7 @@ export function register(app, basePath = '') { */ app.post( `${basePath}/category/:id/image`, - express.raw({ type: ['image/avif', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), + express.raw({ type: ['image/avif', 'image/webp', 'application/octet-stream'], limit: AVIF_BODY_LIMIT }), async (req, res) => { const parsed = parsePositiveInt(req.params.id, 'id'); if (parsed.error) { @@ -781,7 +787,7 @@ export function register(app, basePath = '') { const imageBuffer = Buffer.isBuffer(req.body) ? req.body : null; if (!imageBuffer?.length) { - return res.status(400).json({ error: 'AVIF image body is required' }); + return res.status(400).json({ error: 'AVIF or WebP image body is required' }); } try {