This commit is contained in:
seb
2026-06-21 09:09:06 +02:00
commit 965e581151
30 changed files with 6914 additions and 0 deletions

141
lib/src/jtl-server.js Normal file
View File

@@ -0,0 +1,141 @@
'use strict';
const { serverTimestamp } = require('./seed');
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', (chunk) => chunks.push(chunk));
req.on('end', () => resolve(Buffer.concat(chunks)));
req.on('error', reject);
});
}
function sendJson(res, statusCode, body) {
const responseBody = JSON.stringify(body);
res.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'Content-Length': Buffer.byteLength(responseBody),
});
res.end(responseBody);
}
function createJtlPosServer(store, config = {}) {
const { authToken = process.env.AUTH_TOKEN || '9a2e3036ed9c47e389741d9dbb7590e9' } = config;
const {
certificateFingerprint = 'BC2114CF407A42724BEEF417960F76DCBF9DE879',
certificateSerialNumber = '00BFC8BEACDB981B165210EF111CB9D3',
serverFingerprint = '39-6D-BD-DE-F3-5C-5A-EA-C2-19-CF-EB-A7-A9-58-2F-20-3F-20-F7-3D-E6-CA-8E-AE-FD-28-30-37-A6-45-AE',
mandantId = '1',
mandantName = 'eB-Standard',
mandantDatabase = 'eazybusiness',
} = config;
function buildClientStep1() {
return {
authCode: null,
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
mandantName: null,
mandantDatabase: null,
serverFingerprint,
name: null,
serverTimestamp: serverTimestamp(),
};
}
function buildClientStep2(authCode) {
return {
authCode,
authToken,
certificateFingerprint,
certificateSerialNumber,
mandantId,
mandantName,
mandantDatabase,
serverFingerprint: null,
name: null,
serverTimestamp: serverTimestamp(),
};
}
async function handle(req, res) {
const url = new URL(req.url, 'https://localhost');
const route = (method, pathname) => method === req.method && url.pathname === pathname;
if (route('GET', '/api/v1/client')) {
const authCode = url.searchParams.get('authCode') || '';
const name = url.searchParams.get('name') || 'JTL-POS';
const state = store.getState();
// Step 1 — the first 4 digits of a pre-registered 6-digit pairing code.
if (authCode.length <= 4 && authCode.length > 0) {
return sendJson(res, 200, buildClientStep1());
}
// Step 2 — full 6-digit code must exist in the pending authCodes map.
if (authCode.length === 6) {
if (state.authCodes[authCode]) {
store.dispatch({ type: 'REVOKE_PAIRING_CODE', payload: { code: authCode } });
store.dispatch({ type: 'REGISTER_DEVICE', payload: { token: authToken, name } });
return sendJson(res, 200, buildClientStep2(authCode));
}
return sendJson(res, 400, { Message: 'Der Authentifizierungscode ist falsch.' });
}
return sendJson(res, 400, { Message: 'Keinen passenden Authentifizierungscode gefunden.' });
}
if (route('GET', '/api/v1/init')) {
const params = {
lastChangedCategory: url.searchParams.get('lastChangedCategory') || '0',
lastChangedCustomer: url.searchParams.get('lastChangedCustomer') || '0',
lastChangedCustomerGroup: url.searchParams.get('lastChangedCustomerGroup') || '0',
lastChangedProduct: url.searchParams.get('lastChangedProduct') || '0',
lastChangedConfigurationGroup: url.searchParams.get('lastChangedConfigurationGroup') || '0',
lastChangedConfigurationItem: url.searchParams.get('lastChangedConfigurationItem') || '0',
lastChangedCompositeProduct: url.searchParams.get('lastChangedCompositeProduct') || '0',
lastChangedDeletedEntity: url.searchParams.get('lastChangedDeletedEntity') || '0',
};
return sendJson(res, 200, store.getInitCounts(params));
}
const entityEndpoints = {
'/api/v1/customergroup': { collection: 'customerGroups', param: 'lastChangedCustomerGroup', limit: '200' },
'/api/v1/deletedentity': { collection: 'deletedEntities', param: 'lastChangedDeletedEntity', limit: '200' },
'/api/v1/category': { collection: 'categories', param: 'lastChangedCategory', limit: '20' },
'/api/v1/customer': { collection: 'customers', param: 'lastChangedCustomer', limit: '20' },
'/api/v1/product': { collection: 'products', param: 'lastChangedProduct', limit: '20' },
'/api/v1/productcomposite': { collection: 'productComposites', param: 'lastChangedCompositeProduct', limit: '20' },
};
for (const [pathname, { collection, param, limit: defaultLimit }] of Object.entries(entityEndpoints)) {
if (route('GET', pathname)) {
const cursor = url.searchParams.get(param) || '0';
const limit = url.searchParams.get('limit') || defaultLimit;
const response = store.selectFilteredListFromCollection(collection, cursor, limit);
return sendJson(res, 200, response);
}
}
return sendJson(res, 404, {
Message: `No HTTP resource was found that matches the request URI '${url}'.`,
});
}
return async function requestListener(req, res) {
const body = await readBody(req);
// Attach raw body for optional logging by the wrapper
req.rawBody = body;
try {
await handle(req, res);
} catch (err) {
sendJson(res, 500, { Message: err.message });
}
};
}
module.exports = { createJtlPosServer };