Compare commits
4 Commits
94e44e8e61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6312eaec48 | ||
|
|
3e8ea1ef76 | ||
|
|
e9f29dc569 | ||
|
|
10c4269da0 |
@@ -8,11 +8,6 @@ PAIRING_CODE=307018
|
|||||||
LOG_FILE=logs/requests.log
|
LOG_FILE=logs/requests.log
|
||||||
ORDER_LOG_FILE=logs/orders.log
|
ORDER_LOG_FILE=logs/orders.log
|
||||||
|
|
||||||
# TLS certificate metadata returned during pairing
|
|
||||||
CERTIFICATE_FINGERPRINT=BC2114CF407A42724BEEF417960F76DCBF9DE879
|
|
||||||
CERTIFICATE_SERIAL_NUMBER=00BFC8BEACDB981B165210EF111CB9D3
|
|
||||||
SERVER_FINGERPRINT=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
|
|
||||||
|
|
||||||
# Mandant
|
# Mandant
|
||||||
MANDANT_ID=1
|
MANDANT_ID=1
|
||||||
MANDANT_NAME=eB-Standard
|
MANDANT_NAME=eB-Standard
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
@@ -12,10 +13,50 @@ const certPath = path.join(certsDir, 'cert.pem');
|
|||||||
|
|
||||||
fs.mkdirSync(certsDir, { recursive: true });
|
fs.mkdirSync(certsDir, { recursive: true });
|
||||||
|
|
||||||
// ECDSA P-256 keeps pairing QR codes much smaller than RSA-2048
|
function isIp(value) {
|
||||||
const subject = '/CN=localhost/O=JTL POS Sync/C=DE';
|
return /^(?:\d{1,3}\.){3}\d{1,3}$/.test(value) || value.includes(':');
|
||||||
const san = 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0';
|
}
|
||||||
|
|
||||||
|
function localIpv4s() {
|
||||||
|
const ips = [];
|
||||||
|
for (const entries of Object.values(os.networkInterfaces())) {
|
||||||
|
for (const entry of entries || []) {
|
||||||
|
if (entry.family !== 'IPv4' || entry.internal) continue;
|
||||||
|
// Skip link-local / docker / libvirt bridge noise by default — keep LAN + extras via args
|
||||||
|
if (entry.address.startsWith('169.254.')) continue;
|
||||||
|
if (entry.address.startsWith('172.17.')) continue;
|
||||||
|
if (entry.address.startsWith('192.168.122.')) continue;
|
||||||
|
ips.push(entry.address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ips;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dnsNames = new Set(['localhost']);
|
||||||
|
const ipAddrs = new Set(['127.0.0.1', '0.0.0.0']);
|
||||||
|
|
||||||
|
for (const ip of localIpv4s()) {
|
||||||
|
ipAddrs.add(ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
const extras = [
|
||||||
|
...(process.env.CERT_SAN || '').split(/[,\s]+/).filter(Boolean),
|
||||||
|
...process.argv.slice(2),
|
||||||
|
];
|
||||||
|
for (const value of extras) {
|
||||||
|
if (isIp(value)) ipAddrs.add(value);
|
||||||
|
else dnsNames.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanParts = [
|
||||||
|
...[...dnsNames].map((name) => `DNS:${name}`),
|
||||||
|
...[...ipAddrs].map((ip) => `IP:${ip}`),
|
||||||
|
];
|
||||||
|
const san = `subjectAltName=${sanParts.join(',')}`;
|
||||||
|
const cn = [...dnsNames][0] || 'localhost';
|
||||||
|
const subject = `/CN=${cn}/O=JTL POS Sync/C=DE`;
|
||||||
|
|
||||||
|
// ECDSA P-256 keeps pairing QR codes much smaller than RSA-2048
|
||||||
execSync(
|
execSync(
|
||||||
`openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout "${keyPath}" -out "${certPath}" -days 3650 -subj "${subject}" -addext "${san}"`,
|
`openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -keyout "${keyPath}" -out "${certPath}" -days 3650 -subj "${subject}" -addext "${san}"`,
|
||||||
{ stdio: 'inherit' }
|
{ stdio: 'inherit' }
|
||||||
@@ -34,6 +75,6 @@ const serial = execSync(`openssl x509 -in "${certPath}" -noout -serial`, {
|
|||||||
|
|
||||||
logger.success(`Wrote ${keyPath}`);
|
logger.success(`Wrote ${keyPath}`);
|
||||||
logger.success(`Wrote ${certPath}`);
|
logger.success(`Wrote ${certPath}`);
|
||||||
logger.info(`CERTIFICATE_FINGERPRINT=${sha1.replace(/:/g, '')}`);
|
logger.info(`SAN: ${sanParts.join(', ')}`);
|
||||||
logger.info(`CERTIFICATE_SERIAL_NUMBER=${serial}`);
|
logger.info(`Fingerprint: ${sha1.replace(/:/g, '')}`);
|
||||||
logger.info(`SERVER_FINGERPRINT=${sha1.replace(/:/g, '-')}`);
|
logger.info(`Serial: ${serial}`);
|
||||||
|
|||||||
@@ -79,17 +79,25 @@ The server starts without MSSQL if `MSSQL_USER` is unset or the connection fails
|
|||||||
|
|
||||||
## TLS certificates
|
## TLS certificates
|
||||||
|
|
||||||
Place a certificate and key at `certs/cert.pem` and `certs/key.pem` (relative to the working directory when you run the binary).
|
From the repo root (preferred — picks up LAN IPs automatically):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run cert
|
||||||
|
# optional extras:
|
||||||
|
npm run cert -- 192.168.188.22 sync.quixpos.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Or manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
mkdir -p certs
|
mkdir -p certs
|
||||||
openssl req -x509 -newkey rsa:2048 -nodes \
|
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
|
||||||
-keyout certs/key.pem -out certs/cert.pem -days 3650 \
|
-keyout certs/key.pem -out certs/cert.pem -days 3650 \
|
||||||
-subj '/CN=localhost/O=JTL POS Sync/C=DE' \
|
-subj '/CN=localhost/O=JTL POS Sync/C=DE' \
|
||||||
-addext 'subjectAltName=DNS:localhost,IP:127.0.0.1,IP:0.0.0.0'
|
-addext 'subjectAltName=DNS:localhost,DNS:sync.quixpos.com,IP:127.0.0.1,IP:0.0.0.0,IP:192.168.188.22'
|
||||||
```
|
```
|
||||||
|
|
||||||
On startup the server prints the pairing code and whether MSSQL connected.
|
Place `certs/cert.pem` and `certs/key.pem` relative to the working directory when you run the binary. The browser hostname check requires the address you open (`192.168.x.x` or a DNS name) to appear in the certificate SAN — trusting a CA alone is not enough.
|
||||||
|
|
||||||
## API endpoints
|
## API endpoints
|
||||||
|
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ int64_t HttpRequest::get_query_int64(const std::string& key, int64_t def) const
|
|||||||
// HttpResponse
|
// HttpResponse
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static const char* CORS_HEADERS =
|
||||||
|
"Access-Control-Allow-Origin: *\r\n"
|
||||||
|
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
|
||||||
|
"Access-Control-Allow-Headers: Content-Type, Authorization\r\n"
|
||||||
|
"Access-Control-Max-Age: 86400\r\n";
|
||||||
|
|
||||||
void HttpResponse::send_json(int code, const json& body) {
|
void HttpResponse::send_json(int code, const json& body) {
|
||||||
if (headers_sent) return;
|
if (headers_sent) return;
|
||||||
status_code = code;
|
status_code = code;
|
||||||
@@ -80,6 +86,7 @@ void HttpResponse::send_json(int code, const json& body) {
|
|||||||
"Content-Type: application/json; charset=utf-8\r\n"
|
"Content-Type: application/json; charset=utf-8\r\n"
|
||||||
"Content-Length: " + std::to_string(body_str.size()) + "\r\n"
|
"Content-Length: " + std::to_string(body_str.size()) + "\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: keep-alive\r\n"
|
||||||
|
+ std::string(CORS_HEADERS) +
|
||||||
"\r\n"
|
"\r\n"
|
||||||
+ body_str;
|
+ body_str;
|
||||||
|
|
||||||
@@ -95,6 +102,7 @@ void HttpResponse::send_binary(int code, const std::vector<uint8_t>& data, const
|
|||||||
"Content-Type: " + content_type + "\r\n"
|
"Content-Type: " + content_type + "\r\n"
|
||||||
"Content-Length: " + std::to_string(data.size()) + "\r\n"
|
"Content-Length: " + std::to_string(data.size()) + "\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: keep-alive\r\n"
|
||||||
|
+ std::string(CORS_HEADERS) +
|
||||||
"\r\n";
|
"\r\n";
|
||||||
|
|
||||||
session_write_binary(session, header, data);
|
session_write_binary(session, header, data);
|
||||||
@@ -108,6 +116,7 @@ void HttpResponse::send_empty(int code) {
|
|||||||
std::string resp = "HTTP/1.1 " + std::to_string(code) + " " + reason_phrase(code) + "\r\n"
|
std::string resp = "HTTP/1.1 " + std::to_string(code) + " " + reason_phrase(code) + "\r\n"
|
||||||
"Content-Length: 0\r\n"
|
"Content-Length: 0\r\n"
|
||||||
"Connection: keep-alive\r\n"
|
"Connection: keep-alive\r\n"
|
||||||
|
+ std::string(CORS_HEADERS) +
|
||||||
"\r\n";
|
"\r\n";
|
||||||
|
|
||||||
session_write(session, resp);
|
session_write(session, resp);
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
#include <uv.h>
|
#include <uv.h>
|
||||||
|
#include <openssl/pem.h>
|
||||||
|
#include <openssl/x509.h>
|
||||||
|
#include <openssl/evp.h>
|
||||||
|
#include <openssl/bn.h>
|
||||||
|
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "log.hpp"
|
#include "log.hpp"
|
||||||
@@ -29,12 +34,60 @@ static Router router;
|
|||||||
static PairingStore pairing_store;
|
static PairingStore pairing_store;
|
||||||
static RequestLog request_log;
|
static RequestLog request_log;
|
||||||
|
|
||||||
static json build_config() {
|
static bool read_cert_metadata(const char* cert_path,
|
||||||
|
std::string& fingerprint,
|
||||||
|
std::string& serial,
|
||||||
|
std::string& server_fingerprint) {
|
||||||
|
FILE* fp = std::fopen(cert_path, "r");
|
||||||
|
if (!fp) return false;
|
||||||
|
X509* cert = PEM_read_X509(fp, nullptr, nullptr, nullptr);
|
||||||
|
std::fclose(fp);
|
||||||
|
if (!cert) return false;
|
||||||
|
|
||||||
|
unsigned char md[EVP_MAX_MD_SIZE];
|
||||||
|
unsigned int md_len = 0;
|
||||||
|
if (X509_digest(cert, EVP_sha1(), md, &md_len) != 1) {
|
||||||
|
X509_free(cert);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const char* hex = "0123456789ABCDEF";
|
||||||
|
fingerprint.clear();
|
||||||
|
fingerprint.reserve(md_len * 2);
|
||||||
|
server_fingerprint.clear();
|
||||||
|
server_fingerprint.reserve(md_len * 3 - 1);
|
||||||
|
for (unsigned int i = 0; i < md_len; ++i) {
|
||||||
|
fingerprint.push_back(hex[(md[i] >> 4) & 0xF]);
|
||||||
|
fingerprint.push_back(hex[md[i] & 0xF]);
|
||||||
|
if (i) server_fingerprint.push_back('-');
|
||||||
|
server_fingerprint.push_back(hex[(md[i] >> 4) & 0xF]);
|
||||||
|
server_fingerprint.push_back(hex[md[i] & 0xF]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ASN1_INTEGER* asn1_serial = X509_get0_serialNumber(cert);
|
||||||
|
BIGNUM* bn = ASN1_INTEGER_to_BN(asn1_serial, nullptr);
|
||||||
|
char* hex_serial = bn ? BN_bn2hex(bn) : nullptr;
|
||||||
|
bool ok = hex_serial != nullptr;
|
||||||
|
if (ok) {
|
||||||
|
serial = hex_serial;
|
||||||
|
for (char& c : serial) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
|
||||||
|
OPENSSL_free(hex_serial);
|
||||||
|
}
|
||||||
|
BN_free(bn);
|
||||||
|
X509_free(cert);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
static json build_config(const char* cert_path) {
|
||||||
|
std::string fingerprint, serial, server_fingerprint;
|
||||||
|
if (!read_cert_metadata(cert_path, fingerprint, serial, server_fingerprint)) {
|
||||||
|
logc::error("failed to read certificate metadata from %s", cert_path);
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
{"authToken", config::get("AUTH_TOKEN", "df40ad2067954646abb0499548a52241")},
|
{"authToken", config::get("AUTH_TOKEN", "df40ad2067954646abb0499548a52241")},
|
||||||
{"certificateFingerprint", config::get("CERTIFICATE_FINGERPRINT", "BC2114CF407A42724BEEF417960F76DCBF9DE879")},
|
{"certificateFingerprint", fingerprint},
|
||||||
{"certificateSerialNumber", config::get("CERTIFICATE_SERIAL_NUMBER", "00BFC8BEACDB981B165210EF111CB9D3")},
|
{"certificateSerialNumber", serial},
|
||||||
{"serverFingerprint", config::get("SERVER_FINGERPRINT", "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")},
|
{"serverFingerprint", server_fingerprint},
|
||||||
{"mandantId", config::get("MANDANT_ID", "1")},
|
{"mandantId", config::get("MANDANT_ID", "1")},
|
||||||
{"mandantName", config::get("MANDANT_NAME", "eB-Standard")},
|
{"mandantName", config::get("MANDANT_NAME", "eB-Standard")},
|
||||||
{"mandantDatabase", config::get("MANDANT_DATABASE", "eazybusiness")},
|
{"mandantDatabase", config::get("MANDANT_DATABASE", "eazybusiness")},
|
||||||
@@ -125,7 +178,7 @@ int main(int /*argc*/, char* argv[]) {
|
|||||||
|
|
||||||
loop = uv_default_loop();
|
loop = uv_default_loop();
|
||||||
|
|
||||||
server_config = build_config();
|
server_config = build_config(cert_path.c_str());
|
||||||
|
|
||||||
// Register routes
|
// Register routes
|
||||||
router.add_route("GET", "/v1/client", handle_client);
|
router.add_route("GET", "/v1/client", handle_client);
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ void Router::dispatch(tls_session* sess, PairingStore& pairing, const json& conf
|
|||||||
full_url += "?" + req.query_string;
|
full_url += "?" + req.query_string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method == "OPTIONS") {
|
||||||
|
resp.send_empty(204);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::string route_key = req.method + " " + req.path;
|
std::string route_key = req.method + " " + req.path;
|
||||||
auto it = routes_.find(route_key);
|
auto it = routes_.find(route_key);
|
||||||
if (it != routes_.end()) {
|
if (it != routes_.end()) {
|
||||||
|
|||||||
14
server.js
14
server.js
@@ -7,6 +7,7 @@ import 'dotenv/config';
|
|||||||
import { connectDb, closeDb } from './src/db.js';
|
import { connectDb, closeDb } from './src/db.js';
|
||||||
import { isDemoMode } from './src/demo/mode.js';
|
import { isDemoMode } from './src/demo/mode.js';
|
||||||
import { loadDemoCatalog } from './src/demo/store.js';
|
import { loadDemoCatalog } from './src/demo/store.js';
|
||||||
|
import { readCertMetadata } from './src/cert-meta.js';
|
||||||
import { createJtlPosServer } from './src/jtl-server.js';
|
import { createJtlPosServer } from './src/jtl-server.js';
|
||||||
import { createPairingStore } from './src/pairing.js';
|
import { createPairingStore } from './src/pairing.js';
|
||||||
import { closeOrderLog } from './src/order-log.js';
|
import { closeOrderLog } from './src/order-log.js';
|
||||||
@@ -59,11 +60,18 @@ function formatBody(buffer) {
|
|||||||
return `[binary ${buffer.length} bytes]`;
|
return `[binary ${buffer.length} bytes]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certPem = fs.readFileSync(certPath);
|
||||||
|
const keyPem = fs.readFileSync(keyPath);
|
||||||
|
const certMeta = readCertMetadata(certPem);
|
||||||
|
|
||||||
const pairingStore = createPairingStore();
|
const pairingStore = createPairingStore();
|
||||||
pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS');
|
pairingStore.setPairingCode(PAIRING_CODE, 'JTL-POS');
|
||||||
pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS');
|
pairingStore.registerDevice(AUTH_TOKEN, 'JTL-POS');
|
||||||
|
|
||||||
const jtlHandler = createJtlPosServer(pairingStore, { authToken: AUTH_TOKEN });
|
const jtlHandler = createJtlPosServer(pairingStore, {
|
||||||
|
authToken: AUTH_TOKEN,
|
||||||
|
...certMeta,
|
||||||
|
});
|
||||||
|
|
||||||
const loggedJtlHandler = async (req, res) => {
|
const loggedJtlHandler = async (req, res) => {
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
@@ -108,8 +116,8 @@ const loggedJtlHandler = async (req, res) => {
|
|||||||
|
|
||||||
const httpsServer = https.createServer(
|
const httpsServer = https.createServer(
|
||||||
{
|
{
|
||||||
key: fs.readFileSync(keyPath),
|
key: keyPem,
|
||||||
cert: fs.readFileSync(certPath),
|
cert: certPem,
|
||||||
},
|
},
|
||||||
loggedJtlHandler
|
loggedJtlHandler
|
||||||
);
|
);
|
||||||
|
|||||||
12
src/cert-meta.js
Normal file
12
src/cert-meta.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { X509Certificate } from 'node:crypto';
|
||||||
|
|
||||||
|
/** Derive pairing metadata from a PEM-encoded TLS certificate. */
|
||||||
|
export function readCertMetadata(certPem) {
|
||||||
|
const x509 = new X509Certificate(certPem);
|
||||||
|
const sha1 = x509.fingerprint; // colon-separated uppercase hex
|
||||||
|
return {
|
||||||
|
certificateFingerprint: sha1.replace(/:/g, ''),
|
||||||
|
certificateSerialNumber: x509.serialNumber,
|
||||||
|
serverFingerprint: sha1.replace(/:/g, '-'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -60,7 +60,6 @@ export function handle(req, res, { url, pairingStore, config }) {
|
|||||||
|
|
||||||
if (authCode.length === 6) {
|
if (authCode.length === 6) {
|
||||||
if (pairingStore.hasPairingCode(authCode)) {
|
if (pairingStore.hasPairingCode(authCode)) {
|
||||||
pairingStore.revokePairingCode(authCode);
|
|
||||||
pairingStore.registerDevice(config.authToken, name);
|
pairingStore.registerDevice(config.authToken, name);
|
||||||
return sendJson(res, 200, buildClientStep2(authCode, config));
|
return sendJson(res, 200, buildClientStep2(authCode, config));
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/http.js
17
src/http.js
@@ -13,11 +13,19 @@ export function readBody(req) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CORS_HEADERS = {
|
||||||
|
'Access-Control-Allow-Origin': '*',
|
||||||
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
||||||
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
|
'Access-Control-Max-Age': '86400',
|
||||||
|
};
|
||||||
|
|
||||||
export function sendJson(res, statusCode, body) {
|
export function sendJson(res, statusCode, body) {
|
||||||
const responseBody = JSON.stringify(body);
|
const responseBody = JSON.stringify(body);
|
||||||
res.writeHead(statusCode, {
|
res.writeHead(statusCode, {
|
||||||
'Content-Type': 'application/json; charset=utf-8',
|
'Content-Type': 'application/json; charset=utf-8',
|
||||||
'Content-Length': Buffer.byteLength(responseBody),
|
'Content-Length': Buffer.byteLength(responseBody),
|
||||||
|
...CORS_HEADERS,
|
||||||
});
|
});
|
||||||
res.end(responseBody);
|
res.end(responseBody);
|
||||||
}
|
}
|
||||||
@@ -26,10 +34,19 @@ export function sendBinary(res, statusCode, buffer, contentType) {
|
|||||||
res.writeHead(statusCode, {
|
res.writeHead(statusCode, {
|
||||||
'Content-Type': contentType,
|
'Content-Type': contentType,
|
||||||
'Content-Length': buffer.length,
|
'Content-Length': buffer.length,
|
||||||
|
...CORS_HEADERS,
|
||||||
});
|
});
|
||||||
res.end(buffer);
|
res.end(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sendCorsPreflight(res) {
|
||||||
|
res.writeHead(204, {
|
||||||
|
'Content-Length': 0,
|
||||||
|
...CORS_HEADERS,
|
||||||
|
});
|
||||||
|
res.end();
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizePath(pathname) {
|
export function normalizePath(pathname) {
|
||||||
return pathname.replace(/^\/api(?=\/v1\/)/, '');
|
return pathname.replace(/^\/api(?=\/v1\/)/, '');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
import { endpoints } from './endpoints/index.js';
|
import { endpoints } from './endpoints/index.js';
|
||||||
import { normalizePath, readBody, sendJson } from './http.js';
|
import { normalizePath, readBody, sendCorsPreflight, sendJson } from './http.js';
|
||||||
|
|
||||||
function buildConfig(config = {}) {
|
function buildConfig(config = {}) {
|
||||||
return {
|
return {
|
||||||
authToken: config.authToken || process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241',
|
authToken: config.authToken || process.env.AUTH_TOKEN || 'df40ad2067954646abb0499548a52241',
|
||||||
certificateFingerprint:
|
certificateFingerprint: config.certificateFingerprint || '',
|
||||||
config.certificateFingerprint ||
|
certificateSerialNumber: config.certificateSerialNumber || '',
|
||||||
process.env.CERTIFICATE_FINGERPRINT ||
|
serverFingerprint: config.serverFingerprint || '',
|
||||||
'BC2114CF407A42724BEEF417960F76DCBF9DE879',
|
|
||||||
certificateSerialNumber:
|
|
||||||
config.certificateSerialNumber ||
|
|
||||||
process.env.CERTIFICATE_SERIAL_NUMBER ||
|
|
||||||
'00BFC8BEACDB981B165210EF111CB9D3',
|
|
||||||
serverFingerprint:
|
|
||||||
config.serverFingerprint ||
|
|
||||||
process.env.SERVER_FINGERPRINT ||
|
|
||||||
'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: config.mandantId || process.env.MANDANT_ID || '1',
|
mandantId: config.mandantId || process.env.MANDANT_ID || '1',
|
||||||
mandantName: config.mandantName || process.env.MANDANT_NAME || 'eB-Standard',
|
mandantName: config.mandantName || process.env.MANDANT_NAME || 'eB-Standard',
|
||||||
mandantDatabase: config.mandantDatabase || process.env.MANDANT_DATABASE || 'eazybusiness',
|
mandantDatabase: config.mandantDatabase || process.env.MANDANT_DATABASE || 'eazybusiness',
|
||||||
@@ -27,6 +18,10 @@ export function createJtlPosServer(pairingStore, config = {}) {
|
|||||||
const routes = new Map(endpoints.map((endpoint) => [`${endpoint.method} ${endpoint.path}`, endpoint]));
|
const routes = new Map(endpoints.map((endpoint) => [`${endpoint.method} ${endpoint.path}`, endpoint]));
|
||||||
|
|
||||||
async function handle(req, res) {
|
async function handle(req, res) {
|
||||||
|
if (req.method === 'OPTIONS') {
|
||||||
|
return sendCorsPreflight(res);
|
||||||
|
}
|
||||||
|
|
||||||
const url = new URL(req.url, 'https://localhost');
|
const url = new URL(req.url, 'https://localhost');
|
||||||
const pathname = normalizePath(url.pathname);
|
const pathname = normalizePath(url.pathname);
|
||||||
const routeKey = `${req.method} ${pathname}`;
|
const routeKey = `${req.method} ${pathname}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user