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 { 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)}...`; } const lastInitState = { url: '', result: '', timestamp: 0 }; let suppressedCount = 0; setInterval(() => { if (suppressedCount > 0) { logger.info(`Suppressed ${suppressedCount} init log(s) in the last hour`); suppressedCount = 0; } }, 60 * 60 * 1000); function shouldLogInit(url, statusCode, durationMs, responseBody) { if (statusCode !== 200) return true; if (durationMs > 400) return true; const now = Date.now(); const timeSinceLast = now - lastInitState.timestamp; if (timeSinceLast < 30_000 && url === lastInitState.url && responseBody === lastInitState.result) { lastInitState.timestamp = now; suppressedCount++; return false; } lastInitState.url = url; lastInitState.result = responseBody; lastInitState.timestamp = now; return true; } 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 pairingStore = createPairingStore(); pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS'); pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS'); const jtlHandler = createJtlPosServer(pairingStore, { authToken: AUTH_TOKEN }); 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 || shouldLogInit(req.url, res.statusCode, durationMs, responseBody)) { 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: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath), }, loggedJtlHandler ); async function start() { 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();