168 lines
4.7 KiB
JavaScript
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();
|