u
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
66
src/v1/middleware/posAuth.js
Normal file
66
src/v1/middleware/posAuth.js
Normal 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' });
|
||||
}
|
||||
}
|
||||
@@ -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
165
src/v1/routes/init.js
Normal 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' });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user