u
This commit is contained in:
@@ -275,6 +275,7 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
||||
| [`routes/unit/list.md`](routes/unit/list.md) | `GET /unit` |
|
||||
| [`routes/unit/upsert.md`](routes/unit/upsert.md) | `PUT /unit` |
|
||||
| [`routes/v1/category.md`](routes/v1/category.md) | `GET /v1/category` |
|
||||
| [`routes/pos/cimage.md`](routes/pos/cimage.md) | `GET /v1/cimage` |
|
||||
| [`routes/pos/client.md`](routes/pos/client.md) | `GET /v1/client` |
|
||||
| [`routes/v1/customer.md`](routes/v1/customer.md) | `GET /v1/customer` |
|
||||
| [`routes/v1/customergroup.md`](routes/v1/customergroup.md) | `GET /v1/customergroup` |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Dash API Routes Overview (All)
|
||||
|
||||
Overview of all 268 API endpoints across all modules.
|
||||
Overview of all 269 API endpoints across all modules.
|
||||
|
||||
### Notation
|
||||
|
||||
@@ -924,6 +924,9 @@ _none_
|
||||
#### JTL-POS category synchronization — `GET /v1/category`
|
||||
- Params: `query: lastChangedCategory?: str, limit?: int`
|
||||
- Returns: `200 {}[]`
|
||||
#### POS category image delivery — `GET /v1/cimage`
|
||||
- Params: `query: path: str, w?: int, h?: int, format?: str`
|
||||
- Returns: `200 jpeg`
|
||||
#### POS discovery and pairing — `GET /v1/client`
|
||||
- Params: `query: authCode: str, name?: str`
|
||||
- Returns: `200 { authCode: str|null, authToken: str|null, certificateFingerprint: str, certificateSerialNumber: str, mandantId: str, mandantName: str|null, mandantDatabase: str|null, serverFingerprint: str|null, name: str|null, serverTimestamp: str }`
|
||||
|
||||
33
docs/api/routes/pos/cimage.md
Normal file
33
docs/api/routes/pos/cimage.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# `GET /v1/cimage`
|
||||
|
||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
||||
|
||||
POS category image delivery
|
||||
|
||||
Returns category image by hash (`path` query param) scaled to preview size with 3:4 portrait ratio.
|
||||
|
||||
|
||||
## Query parameters
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `path` | yes | string | SHA-256 hash of the category image |
|
||||
| `w` | no | integer | Max width |
|
||||
| `h` | no | integer | Max height |
|
||||
| `format` | no | string | Optional target format (defaults to jpeg/png or client Accept preference) |
|
||||
|
||||
## Response `200`
|
||||
|
||||
Resized image binary
|
||||
|
||||
## Errors
|
||||
|
||||
| Status | Description |
|
||||
|--------|-------------|
|
||||
| 304 | Image not modified |
|
||||
| 400 | Missing required query parameter 'path' |
|
||||
| 404 | Image not found |
|
||||
|
||||
---
|
||||
|
||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
||||
@@ -41,6 +41,7 @@ const OMITTED_ROUTES = new Set([
|
||||
'GET /shipment/{id}/file/{fileId}',
|
||||
'GET /thumb/{fullHash}',
|
||||
'GET /v1/category',
|
||||
'GET /v1/cimage',
|
||||
'GET /v1/client',
|
||||
'GET /v1/customer',
|
||||
'GET /v1/customergroup',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { handleCustomerGroup } from './routes/customergroup.js';
|
||||
import { handleCustomer } from './routes/customer.js';
|
||||
import { handleProduct } from './routes/product.js';
|
||||
import { handleDeletedEntity } from './routes/deletedentity.js';
|
||||
import { handlePimage } from './routes/pimage.js';
|
||||
import { handlePimage, handleCimage } from './routes/pimage.js';
|
||||
import { handleOrder } from './routes/order.js';
|
||||
|
||||
const router = Router();
|
||||
@@ -30,7 +30,7 @@ router.post('/order', posAuthMiddleware, handleOrder);
|
||||
|
||||
// POS image delivery
|
||||
router.get('/pimage', handlePimage);
|
||||
router.get('/cimage', handlePimage);
|
||||
router.get('/cimage', handleCimage);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import sharp from 'sharp';
|
||||
import * as db from '../../db/posterminal.js';
|
||||
|
||||
const MAX_DIMENSION = 200;
|
||||
const PIMAGE_DEFAULT_W = 200;
|
||||
const PIMAGE_DEFAULT_H = 200;
|
||||
const CIMAGE_DEFAULT_W = 150;
|
||||
const CIMAGE_DEFAULT_H = 200;
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
@@ -85,16 +88,128 @@ const MAX_DIMENSION = 200;
|
||||
* example: "No image was found for path 'abc'."
|
||||
*/
|
||||
export async function handlePimage(req, res) {
|
||||
const isCimage = req.path?.includes('cimage') || req.originalUrl?.includes('/cimage');
|
||||
return servePosImage(req, res, { isCimage });
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /v1/cimage:
|
||||
* get:
|
||||
* tags: [POS]
|
||||
* summary: POS category image delivery
|
||||
* description: |
|
||||
* Returns category image by hash (`path` query param) scaled to preview size with 3:4 portrait ratio.
|
||||
* x-markdown: pos/cimage.md
|
||||
* parameters:
|
||||
* - in: query
|
||||
* name: path
|
||||
* required: true
|
||||
* schema:
|
||||
* type: string
|
||||
* description: SHA-256 hash of the category image
|
||||
* example: "71de4085ce2b1a53317832421009d844eec0c36a3ec071a31661cc2a212e15e6"
|
||||
* - in: query
|
||||
* name: w
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 150
|
||||
* description: Max width
|
||||
* - in: query
|
||||
* name: h
|
||||
* required: false
|
||||
* schema:
|
||||
* type: integer
|
||||
* default: 200
|
||||
* description: Max height
|
||||
* - in: query
|
||||
* name: format
|
||||
* required: false
|
||||
* schema:
|
||||
* type: string
|
||||
* enum: [jpeg, png, webp, avif]
|
||||
* description: Optional target format (defaults to jpeg/png or client Accept preference)
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Resized image binary
|
||||
* content:
|
||||
* image/jpeg:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* image/png:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* image/webp:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* image/avif:
|
||||
* schema:
|
||||
* type: string
|
||||
* format: binary
|
||||
* 304:
|
||||
* description: Image not modified
|
||||
* 400:
|
||||
* description: Missing required query parameter 'path'
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* Message:
|
||||
* type: string
|
||||
* example: "Missing required query parameter 'path'."
|
||||
* 404:
|
||||
* description: Image not found
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* properties:
|
||||
* Message:
|
||||
* type: string
|
||||
* example: "No image was found for path 'abc'."
|
||||
*/
|
||||
export async function handleCimage(req, res) {
|
||||
return servePosImage(req, res, { isCimage: true });
|
||||
}
|
||||
|
||||
async function servePosImage(req, res, { isCimage = false } = {}) {
|
||||
const rawPath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
||||
|
||||
if (!rawPath) {
|
||||
return res.status(400).json({ Message: "Missing required query parameter 'path'." });
|
||||
}
|
||||
|
||||
const widthParam = req.query.w !== undefined ? Number(req.query.w) : MAX_DIMENSION;
|
||||
const heightParam = req.query.h !== undefined ? Number(req.query.h) : MAX_DIMENSION;
|
||||
const targetW = Number.isFinite(widthParam) && widthParam > 0 ? Math.min(Math.round(widthParam), 2000) : MAX_DIMENSION;
|
||||
const targetH = Number.isFinite(heightParam) && heightParam > 0 ? Math.min(Math.round(heightParam), 2000) : MAX_DIMENSION;
|
||||
const hasW = req.query.w !== undefined && Number.isFinite(Number(req.query.w)) && Number(req.query.w) > 0;
|
||||
const hasH = req.query.h !== undefined && Number.isFinite(Number(req.query.h)) && Number(req.query.h) > 0;
|
||||
|
||||
let targetW;
|
||||
let targetH;
|
||||
|
||||
if (isCimage) {
|
||||
if (hasW && !hasH) {
|
||||
targetW = Math.min(Math.round(Number(req.query.w)), 2000);
|
||||
targetH = Math.min(Math.round((targetW * 4) / 3), 2000);
|
||||
} else if (hasH && !hasW) {
|
||||
targetH = Math.min(Math.round(Number(req.query.h)), 2000);
|
||||
targetW = Math.min(Math.round((targetH * 3) / 4), 2000);
|
||||
} else if (hasW && hasH) {
|
||||
targetW = Math.min(Math.round(Number(req.query.w)), 2000);
|
||||
targetH = Math.min(Math.round(Number(req.query.h)), 2000);
|
||||
} else {
|
||||
targetW = CIMAGE_DEFAULT_W;
|
||||
targetH = CIMAGE_DEFAULT_H;
|
||||
}
|
||||
} else {
|
||||
const widthParam = hasW ? Number(req.query.w) : PIMAGE_DEFAULT_W;
|
||||
const heightParam = hasH ? Number(req.query.h) : PIMAGE_DEFAULT_H;
|
||||
targetW = Math.min(Math.round(widthParam), 2000);
|
||||
targetH = Math.min(Math.round(heightParam), 2000);
|
||||
}
|
||||
|
||||
try {
|
||||
const image = await db.getPosImageByHash(rawPath, { width: targetW, height: targetH });
|
||||
@@ -102,7 +217,7 @@ export async function handlePimage(req, res) {
|
||||
return res.status(404).json({ Message: `No image was found for path '${rawPath}'.` });
|
||||
}
|
||||
|
||||
const etag = `"${image.fullHash}-${targetW}x${targetH}"`;
|
||||
const etag = `"${image.fullHash}-${targetW}x${targetH}${isCimage ? '-c' : ''}"`;
|
||||
const ifNoneMatch = req.headers['if-none-match'];
|
||||
if (ifNoneMatch === etag || ifNoneMatch === image.fullHash || ifNoneMatch === `"${image.fullHash}"`) {
|
||||
res.setHeader('ETag', etag);
|
||||
@@ -113,11 +228,19 @@ export async function handlePimage(req, res) {
|
||||
const imageSharp = sharp(image.buffer);
|
||||
const meta = await imageSharp.metadata();
|
||||
|
||||
let pipeline = imageSharp
|
||||
.resize(targetW, targetH, {
|
||||
const resizeOptions = isCimage
|
||||
? {
|
||||
fit: 'contain',
|
||||
background: '#ffffff',
|
||||
withoutEnlargement: true,
|
||||
}
|
||||
: {
|
||||
fit: 'inside',
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
};
|
||||
|
||||
let pipeline = imageSharp
|
||||
.resize(targetW, targetH, resizeOptions)
|
||||
.flatten({ background: '#ffffff' });
|
||||
|
||||
const accept = String(req.headers.accept || '');
|
||||
@@ -146,7 +269,7 @@ export async function handlePimage(req, res) {
|
||||
res.setHeader('ETag', etag);
|
||||
return res.status(200).send(buffer);
|
||||
} catch (err) {
|
||||
console.error('POS /v1/pimage error:', err);
|
||||
console.error(`POS /v1/${isCimage ? 'cimage' : 'pimage'} error:`, err);
|
||||
return res.status(500).json({ Message: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user