Files
jtlPosSync/server.js
2026-07-27 02:16:26 +02:00

168 lines
4.7 KiB
JavaScript

import fs from 'node:fs';
import https from 'node:https';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import 'dotenv/config';
import { connectDb, closeDb } from './src/db.js';
import { isDemoMode } from './src/demo/mode.js';
import { loadDemoCatalog } from './src/demo/store.js';
import { readCertMetadata } from './src/cert-meta.js';
import { createJtlPosServer } from './src/jtl-server.js';
import { createPairingStore } from './src/pairing.js';
import { closeOrderLog } from './src/order-log.js';
import { closeRequestLog, logRequest } from './src/request-log.js';
import { logger } from './src/logger.js';
import { fetchActiveShop } from './src/shop.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PORT = Number(process.env.PORT) || 4443;
const AUTH_TOKEN = process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241';
const PAIRING_CODE = process.env.PAIRING_CODE || '307018';
const certsDir = path.join(__dirname, 'certs');
const keyPath = path.join(certsDir, 'key.pem');
const certPath = path.join(certsDir, 'cert.pem');
if (!fs.existsSync(keyPath) || !fs.existsSync(certPath)) {
logger.error('Missing TLS certificate. Run: npm run cert');
process.exit(1);
}
const CONSOLE_URL_MAX_LENGTH = 100;
function truncateUrl(url) {
if (url.length <= CONSOLE_URL_MAX_LENGTH) {
return url;
}
return `${url.slice(0, CONSOLE_URL_MAX_LENGTH)}...`;
}
let lastLoggedInitUrl = null;
let suppressedInitCount = 0;
function flushSuppressedInitLogs() {
if (suppressedInitCount > 0) {
logger.info(`Suppressed ${suppressedInitCount} duplicate init log(s)`);
suppressedInitCount = 0;
}
}
function formatBody(buffer) {
if (!buffer.length) {
return '(empty)';
}
const text = buffer.toString('utf8');
if (/^[\x09\x0A\x0D\x20-\x7E\u0080-\uFFFF]*$/.test(text)) {
return text;
}
return `[binary ${buffer.length} bytes]`;
}
const certPem = fs.readFileSync(certPath);
const keyPem = fs.readFileSync(keyPath);
const certMeta = readCertMetadata(certPem);
const pairingStore = createPairingStore();
pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS');
pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS');
const jtlHandler = createJtlPosServer(pairingStore, {
authToken: AUTH_TOKEN,
...certMeta,
});
const loggedJtlHandler = async (req, res) => {
const started = Date.now();
let responseBuffer = Buffer.alloc(0);
const originalEnd = res.end.bind(res);
res.end = (chunk, ...args) => {
if (chunk) {
responseBuffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
}
return originalEnd(chunk, ...args);
};
await jtlHandler(req, res);
const responseBody = formatBody(responseBuffer);
const durationMs = Date.now() - started;
const isInit = req.url.startsWith('/api/v1/init');
if (isInit) {
if (lastLoggedInitUrl === req.url) {
suppressedInitCount++;
return;
}
lastLoggedInitUrl = req.url;
} else {
flushSuppressedInitLogs();
lastLoggedInitUrl = null;
}
logger.info(`${req.socket.remoteAddress} ${req.method} ${truncateUrl(req.url)} ${res.statusCode} ${durationMs}ms`);
logRequest({
remoteAddress: req.socket.remoteAddress,
method: req.method,
url: req.url,
statusCode: res.statusCode,
durationMs,
response: responseBody,
});
};
const httpsServer = https.createServer(
{
key: keyPem,
cert: certPem,
},
loggedJtlHandler
);
async function start() {
if (isDemoMode()) {
const stats = await loadDemoCatalog();
logger.success(
`DEMO_MODE: loaded catalog (${stats.products} products, ${stats.categories} categories, ${stats.customerGroups} customer groups, ${stats.composites} composite links)`
);
logger.info('MSSQL is skipped while DEMO_MODE=true');
} else {
try {
const pool = await connectDb();
logger.success(`MSSQL connected: ${process.env.MSSQL_SERVER}/${process.env.MSSQL_DATABASE}`);
const activeShop = await fetchActiveShop(pool);
logger.info(`Active shop ID: ${activeShop}`);
} catch (err) {
logger.warn(`MSSQL connection skipped: ${err.message}`);
logger.warn('POS handshake will still work; sync from database is not available yet.');
}
}
httpsServer.listen(PORT, '0.0.0.0', () => {
logger.success(`HTTPS POS server listening on https://0.0.0.0:${PORT}`);
logger.info(`Certificate: ${certPath}`);
logger.info(`Pairing code: ${PAIRING_CODE}`);
logger.info(`Auth token: ${AUTH_TOKEN}`);
});
}
async function shutdown() {
logger.info('Shutting down...');
httpsServer.close();
await closeDb();
closeRequestLog();
closeOrderLog();
}
process.on('SIGINT', () => {
shutdown().finally(() => process.exit(0));
});
process.on('SIGTERM', () => {
shutdown().finally(() => process.exit(0));
});
start();