This commit is contained in:
seb
2026-09-30 09:47:59 +02:00
parent 1de802fbaa
commit 82cc138ba6
15 changed files with 624 additions and 11 deletions

View File

@@ -1,9 +1,15 @@
import { Router } from 'express';
import { posAuthMiddleware } from './middleware/posAuth.js';
import { handleClient } from './routes/client.js';
import { handleInit } from './routes/init.js';
const router = Router();
// POS discovery and pairing
router.get('/client', handleClient);
// POS sync initialization counts
router.get('/init', posAuthMiddleware, handleInit);
export default router;

View File

@@ -0,0 +1,66 @@
import chalk from 'chalk';
import * as db from '../../db/posterminal.js';
export function extractPosToken(req) {
const authHeader = req.headers.authorization;
if (typeof authHeader === 'string' && authHeader.trim()) {
const trimmed = authHeader.trim();
if (/^Bearer\s+/i.test(trimmed)) {
return trimmed.replace(/^Bearer\s+/i, '').trim();
}
if (/^Basic\s+/i.test(trimmed)) {
const b64 = trimmed.replace(/^Basic\s+/i, '').trim();
try {
const decoded = Buffer.from(b64, 'base64').toString('utf8');
const parts = decoded.split(':');
return (parts[1] || parts[0] || '').trim();
} catch {
return b64;
}
}
return trimmed;
}
for (const key of ['x-auth-token', 'authtoken', 'auth-token', 'token', 'x-token']) {
const val = req.headers[key];
if (typeof val === 'string' && val.trim()) {
return val.trim();
}
}
const queryToken = req.query?.authToken || req.query?.token;
if (typeof queryToken === 'string' && queryToken.trim()) {
return queryToken.trim();
}
return '';
}
/**
* Verifies the POS terminal token from the Authorization header and attaches
* `req.posterminal`.
* @param {import('express').Request} req
* @param {import('express').Response} res
* @param {import('express').NextFunction} next
*/
export async function posAuthMiddleware(req, res, next) {
const token = extractPosToken(req);
if (!token) {
console.log(chalk.yellow(' [posAuth] No token found in headers:'), req.headers);
return res.status(401).json({ Message: 'Authorization has been denied for this request.' });
}
try {
const terminal = await db.findByToken(token);
if (!terminal) {
console.log(chalk.yellow(` [posAuth] Unknown token "${token}", headers:`), req.headers);
return res.status(401).json({ Message: 'Authorization has been denied for this request.' });
}
req.posterminal = terminal;
next();
} catch (err) {
console.error('POS auth error:', err);
return res.status(500).json({ Message: 'Internal server error' });
}
}

View File

@@ -2,6 +2,7 @@ import crypto from 'node:crypto';
import * as db from '../../db/posterminal.js';
import { getCompanyById } from '../../db/company.js';
import { getCertMetadata } from '../../util/cert.js';
import { emit } from '../../api/sse.js';
export function serverTimestamp() {
const d = new Date();
@@ -166,6 +167,10 @@ export async function handleClient(req, res) {
pairedAt,
});
if (terminal.companyId) {
emit('posterminal', { companyId: terminal.companyId });
}
let mandantName = process.env.MANDANT_NAME || 'eB-Standard';
let mandantDatabase = process.env.MANDANT_DATABASE || 'eazybusiness';
let mandantId = String(terminal.companyId || process.env.MANDANT_ID || '1');

165
src/v1/routes/init.js Normal file
View File

@@ -0,0 +1,165 @@
import * as db from '../../db/posterminal.js';
/**
* @openapi
* /v1/init:
* get:
* tags: [POS]
* summary: POS sync initialization counts
* description: |
* Returns entity change counts since the provided `lastChanged*` unix timestamp cursors
* for the authenticated POS terminal (`Authorization: Bearer <authToken>`).
* x-markdown: pos/init.md
* parameters:
* - in: header
* name: Authorization
* required: true
* schema:
* type: string
* description: POS device authToken obtained during pairing (`Bearer <token>` or `<token>`)
* example: "Bearer c30099b66ec1fcb9ccd05795423c94e8"
* - in: query
* name: lastChangedProduct
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced product update
* example: 0
* - in: query
* name: lastChangedCategory
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced category update
* example: 0
* - in: query
* name: lastChangedCustomer
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced customer update
* example: 0
* - in: query
* name: lastChangedCustomerGroup
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced customer group update
* example: 0
* - in: query
* name: lastChangedCompositeProduct
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced composite product update
* example: 0
* - in: query
* name: lastChangedDeletedEntity
* required: false
* schema:
* type: integer
* default: 0
* description: Unix timestamp of last synced deleted entity
* example: 0
* responses:
* 200:
* description: Sync initialization counts
* content:
* application/json:
* schema:
* type: object
* properties:
* version:
* type: string
* example: "1.10.12.0"
* product_count:
* type: string
* example: "19"
* category_count:
* type: string
* example: "4"
* customer_count:
* type: string
* example: "0"
* customerGroup_count:
* type: string
* example: "1"
* compositeProduct_count:
* type: string
* example: "0"
* configurationGroup_count:
* type: string
* example: "0"
* configurationItem_count:
* type: string
* example: "0"
* deletedEntity_count:
* type: string
* example: "0"
* max_orderId_count:
* type: string
* example: "0"
* 401:
* description: Missing or invalid POS Authorization token
* content:
* application/json:
* schema:
* type: object
* properties:
* Message:
* type: string
* example: "Authorization has been denied for this request."
* @param {import('express').Request} req
* @param {import('express').Response} res
*/
export async function handleInit(req, res) {
const posterminalId = req.posterminal.id;
const productCursor = Number(req.query.lastChangedProduct) || 0;
const categoryCursor = Number(req.query.lastChangedCategory) || 0;
const customerCursor = Number(req.query.lastChangedCustomer) || 0;
const customerGroupCursor = Number(req.query.lastChangedCustomerGroup) || 0;
const compositeProductCursor = Number(req.query.lastChangedCompositeProduct) || 0;
const deletedEntityCursor = Number(req.query.lastChangedDeletedEntity) || 0;
try {
const [
productCount,
categoryCount,
customerCount,
customerGroupCount,
compositeProductCount,
deletedEntityCount,
maxOrderIdCount,
] = await Promise.all([
db.getProductCount({ posterminalId, cursor: productCursor }),
db.getCategoryCount({ posterminalId, cursor: categoryCursor }),
db.getCustomerCount({ posterminalId, cursor: customerCursor }),
db.getCustomerGroupCount({ posterminalId, cursor: customerGroupCursor }),
db.getCompositeProductCount({ posterminalId, cursor: compositeProductCursor }),
db.getDeletedEntityCount({ posterminalId, cursor: deletedEntityCursor }),
db.getMaxOrderIdCount({ posterminalId }),
]);
return res.status(200).json({
version: '1.10.12.0',
product_count: String(productCount),
category_count: String(categoryCount),
customer_count: String(customerCount),
customerGroup_count: String(customerGroupCount),
compositeProduct_count: String(compositeProductCount),
configurationGroup_count: '0',
configurationItem_count: '0',
deletedEntity_count: String(deletedEntityCount),
max_orderId_count: String(maxOrderIdCount),
});
} catch (err) {
console.error('POS /v1/init error:', err);
return res.status(500).json({ Message: 'Internal server error' });
}
}