@alizadefaig/pos-server 1.2.29 → 1.2.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/core/Context.js +4 -1
- package/app/core/auth/scope.js +15 -2
- package/app/core/index.js +16 -0
- package/app/core/models/Order.js +60 -4
- package/app/core/models/OrderProduct.js +5 -0
- package/app/core/repositories/ProductRelations.js +4 -4
- package/app/core/resources/OrderResource.js +1 -0
- package/app/core/resources/TeamResource.js +7 -1
- package/app/core/services/AuthService.js +60 -8
- package/app/core/services/InitService.js +23 -4
- package/app/core/services/OrderProductService.js +6 -0
- package/app/core/services/OrderService.js +8 -3
- package/app/core/services/PrintService.js +16 -4
- package/app/core/services/ReportService.js +87 -55
- package/app/core/support/teamSettings.js +10 -1
- package/app/core/support/terminalAccess.js +93 -0
- package/app/core/support/unlock.js +9 -1
- package/app/db/migrations/013_terminal_bindings.sql +5 -0
- package/app/netstats.js +98 -0
- package/app/sync/discovery.js +201 -38
- package/app/sync/index.js +13 -4
- package/package.json +1 -1
- package/routes/desktop/api/offline.js +1 -1
- package/routes/desktop/api/printers.js +33 -11
- package/routes/desktop/local.js +1 -1
- package/routes/server/api/platform.js +1 -1
- package/server.js +4 -0
package/app/sync/discovery.js
CHANGED
|
@@ -170,9 +170,15 @@ const buildAnswer = ({nonce, httpPort, identity}) => {
|
|
|
170
170
|
* Проверка ответа мастера (UDP и HTTP). address — откуда ответ пришёл на самом деле: он обязан
|
|
171
171
|
* быть в подписанных hosts. port — если известен (HTTP): мастер обязан назвать тот же.
|
|
172
172
|
*/
|
|
173
|
-
const
|
|
174
|
-
if (!message
|
|
175
|
-
return
|
|
173
|
+
const check = (message, {key, nonce, address, companyId = null, port = null}) => {
|
|
174
|
+
if (!message) {
|
|
175
|
+
return 'bad_packet';
|
|
176
|
+
}
|
|
177
|
+
if (message.t !== ANSWER) {
|
|
178
|
+
return 'not_answer';
|
|
179
|
+
}
|
|
180
|
+
if (message.nonce !== nonce) {
|
|
181
|
+
return 'stale_nonce';
|
|
176
182
|
}
|
|
177
183
|
const hosts = Array.isArray(message.hosts) ? message.hosts.map(String) : [];
|
|
178
184
|
const fields = {
|
|
@@ -183,16 +189,20 @@ const accept = (message, {key, nonce, address, companyId = null, port = null}) =
|
|
|
183
189
|
hosts,
|
|
184
190
|
};
|
|
185
191
|
if (!Number.isInteger(fields.port) || fields.port <= 0 || fields.port > 65535) {
|
|
186
|
-
return
|
|
192
|
+
return 'bad_port';
|
|
187
193
|
}
|
|
188
194
|
if (port !== null && fields.port !== port) {
|
|
189
|
-
return
|
|
195
|
+
return 'port_mismatch';
|
|
190
196
|
}
|
|
191
|
-
if (!hosts.includes(address)
|
|
192
|
-
return
|
|
197
|
+
if (!hosts.includes(address)) {
|
|
198
|
+
return 'address_not_in_hosts';
|
|
199
|
+
}
|
|
200
|
+
// Другой ключ зала: мастер и ведомый настроены разными ключами (смена ключа не доехала).
|
|
201
|
+
if (!proofMatches(key, fields, message.proof)) {
|
|
202
|
+
return 'bad_proof';
|
|
193
203
|
}
|
|
194
204
|
if (companyId && fields.company_id !== companyId) {
|
|
195
|
-
return
|
|
205
|
+
return 'other_company';
|
|
196
206
|
}
|
|
197
207
|
return {
|
|
198
208
|
url: `http://${address}:${fields.port}`,
|
|
@@ -203,8 +213,31 @@ const accept = (message, {key, nonce, address, companyId = null, port = null}) =
|
|
|
203
213
|
};
|
|
204
214
|
};
|
|
205
215
|
|
|
216
|
+
const accept = (message, options) => {
|
|
217
|
+
const result = check(message, options);
|
|
218
|
+
return typeof result === 'string' ? null : result;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const bump = (map, key) => {
|
|
222
|
+
map[key] = (map[key] || 0) + 1;
|
|
223
|
+
};
|
|
224
|
+
|
|
206
225
|
// ---------------------------------------------------------------- мастер: отвечает
|
|
207
226
|
|
|
227
|
+
/** Почему buildAnswer вернул null — для диагностики. */
|
|
228
|
+
const noAnswerReason = (identity) => {
|
|
229
|
+
if (!config.lanKey()) {
|
|
230
|
+
return 'no_lan_key';
|
|
231
|
+
}
|
|
232
|
+
if (!identity || !identity.company_id) {
|
|
233
|
+
return 'no_session';
|
|
234
|
+
}
|
|
235
|
+
if (!interfaces().length) {
|
|
236
|
+
return 'no_interfaces';
|
|
237
|
+
}
|
|
238
|
+
return 'bad_nonce';
|
|
239
|
+
};
|
|
240
|
+
|
|
208
241
|
class Responder {
|
|
209
242
|
/**
|
|
210
243
|
* @param {{httpPort: () => number, identity: () => ({company_id, terminal_id}|null)}} options
|
|
@@ -217,6 +250,13 @@ class Responder {
|
|
|
217
250
|
this.socket = null;
|
|
218
251
|
this.answered = 0;
|
|
219
252
|
this.limiter = new Limiter(MAX_ANSWERS_PER_SECOND);
|
|
253
|
+
// Диагностика (/local/status.discovery): доходят ли запросы ведомых до мастера.
|
|
254
|
+
this.received = 0;
|
|
255
|
+
this.rejected = {};
|
|
256
|
+
this.sendErrors = 0;
|
|
257
|
+
this.lastQuery = null;
|
|
258
|
+
this.bound = null;
|
|
259
|
+
this.error = null;
|
|
220
260
|
}
|
|
221
261
|
|
|
222
262
|
start() {
|
|
@@ -228,12 +268,20 @@ class Responder {
|
|
|
228
268
|
const socket = dgram.createSocket('udp4');
|
|
229
269
|
socket.on('error', (e) => {
|
|
230
270
|
console.warn('[discovery] responder error:', e.message);
|
|
271
|
+
this.error = `${e.code || ''} ${e.message}`.trim();
|
|
231
272
|
this.stop();
|
|
232
273
|
});
|
|
233
274
|
socket.on('message', (buffer, rinfo) => {
|
|
275
|
+
this.received += 1;
|
|
276
|
+
this.lastQuery = {from: rinfo.address, at: new Date().toISOString()};
|
|
234
277
|
this.answer(socket, parse(buffer), rinfo).catch(() => {});
|
|
235
278
|
});
|
|
236
|
-
socket.bind(port, () =>
|
|
279
|
+
socket.bind(port, () => {
|
|
280
|
+
const address = socket.address();
|
|
281
|
+
this.bound = `${address.address}:${address.port}`;
|
|
282
|
+
this.error = null;
|
|
283
|
+
log(`answering on udp/${port}`);
|
|
284
|
+
});
|
|
237
285
|
socket.unref();
|
|
238
286
|
this.socket = socket;
|
|
239
287
|
return this;
|
|
@@ -241,19 +289,40 @@ class Responder {
|
|
|
241
289
|
|
|
242
290
|
async answer(socket, message, rinfo) {
|
|
243
291
|
if (!message || message.t !== QUERY) {
|
|
292
|
+
bump(this.rejected, 'bad_packet');
|
|
244
293
|
return;
|
|
245
294
|
}
|
|
246
295
|
const identity = this.identity();
|
|
247
296
|
// Ведомый другого заведения в той же сети — не наш.
|
|
248
297
|
if (identity && message.company_id && message.company_id !== identity.company_id) {
|
|
298
|
+
bump(this.rejected, 'other_company');
|
|
249
299
|
return;
|
|
250
300
|
}
|
|
251
301
|
const answer = buildAnswer({nonce: message.nonce, httpPort: this.httpPort(), identity});
|
|
252
|
-
if (!answer
|
|
302
|
+
if (!answer) {
|
|
303
|
+
bump(this.rejected, noAnswerReason(identity));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (!this.limiter.take()) {
|
|
307
|
+
bump(this.rejected, 'rate_limited');
|
|
253
308
|
return;
|
|
254
309
|
}
|
|
255
310
|
this.answered += 1;
|
|
256
|
-
await send(socket, answer, rinfo.port, rinfo.address)
|
|
311
|
+
if (!await send(socket, answer, rinfo.port, rinfo.address)) {
|
|
312
|
+
this.sendErrors += 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
status() {
|
|
317
|
+
return {
|
|
318
|
+
bound: this.bound,
|
|
319
|
+
error: this.error,
|
|
320
|
+
received: this.received,
|
|
321
|
+
answered: this.answered,
|
|
322
|
+
rejected: this.rejected,
|
|
323
|
+
send_errors: this.sendErrors,
|
|
324
|
+
last_query: this.lastQuery,
|
|
325
|
+
};
|
|
257
326
|
}
|
|
258
327
|
|
|
259
328
|
stop() {
|
|
@@ -271,21 +340,37 @@ class Responder {
|
|
|
271
340
|
|
|
272
341
|
const httpLimiter = new Limiter(MAX_ANSWERS_PER_SECOND);
|
|
273
342
|
let httpAnswered = 0;
|
|
343
|
+
const httpStats = {requests: 0, rejected: {}, last: null};
|
|
274
344
|
|
|
275
345
|
/**
|
|
276
346
|
* GET /local/whoami?nonce= — тот же подписанный ответ по HTTP (ступени 1 и 3 поиска). Путь
|
|
277
347
|
* открыт без ключа зала (lan.js): ответ сам доказывает знание ключа и ничего не раскрывает.
|
|
278
348
|
* null — не отвечаем (не мастер, нет ключа или сессии, превышен лимит).
|
|
279
349
|
*/
|
|
280
|
-
const whoami = ({nonce, httpPort, identity}) => {
|
|
350
|
+
const whoami = ({nonce, httpPort, identity, from = null}) => {
|
|
351
|
+
httpStats.requests += 1;
|
|
352
|
+
httpStats.last = {from, at: new Date().toISOString()};
|
|
281
353
|
const answer = buildAnswer({nonce, httpPort, identity});
|
|
282
|
-
if (!answer
|
|
354
|
+
if (!answer) {
|
|
355
|
+
bump(httpStats.rejected, noAnswerReason(identity));
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
if (!httpLimiter.take()) {
|
|
359
|
+
bump(httpStats.rejected, 'rate_limited');
|
|
283
360
|
return null;
|
|
284
361
|
}
|
|
285
362
|
httpAnswered += 1;
|
|
286
363
|
return answer;
|
|
287
364
|
};
|
|
288
365
|
|
|
366
|
+
/** Счётчики /local/whoami мастера: доходят ли HTTP-проверки ведомых. */
|
|
367
|
+
const whoamiStatus = () => ({
|
|
368
|
+
requests: httpStats.requests,
|
|
369
|
+
answered: httpAnswered,
|
|
370
|
+
rejected: {...httpStats.rejected},
|
|
371
|
+
last: httpStats.last,
|
|
372
|
+
});
|
|
373
|
+
|
|
289
374
|
// ---------------------------------------------------------------- ведомый: ищет
|
|
290
375
|
|
|
291
376
|
/**
|
|
@@ -295,9 +380,12 @@ const whoami = ({nonce, httpPort, identity}) => {
|
|
|
295
380
|
* @param {{companyId?: string, targets?: string[]}} options
|
|
296
381
|
* targets — куда слать вместо широковещательных адресов (тесты, сети без broadcast).
|
|
297
382
|
*/
|
|
298
|
-
const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve) => {
|
|
383
|
+
const probe = ({companyId = null, targets = null, stats = null} = {}) => new Promise((resolve) => {
|
|
299
384
|
const key = config.lanKey();
|
|
300
385
|
if (!key) {
|
|
386
|
+
if (stats) {
|
|
387
|
+
stats.error = 'no_lan_key';
|
|
388
|
+
}
|
|
301
389
|
resolve([]);
|
|
302
390
|
return;
|
|
303
391
|
}
|
|
@@ -321,12 +409,24 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
|
|
|
321
409
|
};
|
|
322
410
|
const timer = setTimeout(finish, window);
|
|
323
411
|
|
|
324
|
-
socket.on('error',
|
|
412
|
+
socket.on('error', (e) => {
|
|
413
|
+
if (stats) {
|
|
414
|
+
stats.error = `${e.code || ''} ${e.message}`.trim();
|
|
415
|
+
}
|
|
416
|
+
finish();
|
|
417
|
+
});
|
|
325
418
|
socket.on('message', (buffer, rinfo) => {
|
|
326
|
-
const
|
|
327
|
-
if (
|
|
328
|
-
|
|
419
|
+
const result = check(parse(buffer), {key, nonce, address: rinfo.address, companyId});
|
|
420
|
+
if (stats) {
|
|
421
|
+
stats.answers += 1;
|
|
422
|
+
}
|
|
423
|
+
if (typeof result === 'string') {
|
|
424
|
+
if (stats && Object.keys(stats.rejected).length < 20) {
|
|
425
|
+
bump(stats.rejected, `${result}@${rinfo.address}`);
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
329
428
|
}
|
|
429
|
+
found.set(rinfo.address, result);
|
|
330
430
|
});
|
|
331
431
|
socket.bind(0, async () => {
|
|
332
432
|
socket.setBroadcast(true);
|
|
@@ -334,7 +434,12 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
|
|
|
334
434
|
const addresses = targets && targets.length
|
|
335
435
|
? targets
|
|
336
436
|
: [...new Set([...interfaces().map((item) => item.broadcast), '255.255.255.255'])];
|
|
337
|
-
await Promise.all(addresses.map((address) => send(socket, message, port, address)));
|
|
437
|
+
const results = await Promise.all(addresses.map((address) => send(socket, message, port, address)));
|
|
438
|
+
if (stats) {
|
|
439
|
+
stats.targets = addresses;
|
|
440
|
+
stats.sent = results.filter(Boolean).length;
|
|
441
|
+
stats.send_errors = results.length - stats.sent;
|
|
442
|
+
}
|
|
338
443
|
});
|
|
339
444
|
});
|
|
340
445
|
|
|
@@ -342,16 +447,34 @@ const probe = ({companyId = null, targets = null} = {}) => new Promise((resolve)
|
|
|
342
447
|
* Проверка адреса по HTTP: GET <url>/local/whoami со свежим nonce. Отдаёт кандидата или null.
|
|
343
448
|
* Только http и IPv4 — подпись мастера перечисляет его IPv4, по имени хоста её не сверить.
|
|
344
449
|
*/
|
|
345
|
-
const verify = async (url,
|
|
450
|
+
const verify = async (url, options = {}) => {
|
|
451
|
+
const result = await verifyWhy(url, options);
|
|
452
|
+
return typeof result === 'string' ? null : result;
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
/** Сетевая ошибка fetch → короткий код: timeout, ECONNREFUSED, EHOSTUNREACH, … */
|
|
456
|
+
const fetchError = (e) => {
|
|
457
|
+
if (e && e.name === 'AbortError') {
|
|
458
|
+
return 'timeout';
|
|
459
|
+
}
|
|
460
|
+
const cause = e && e.cause;
|
|
461
|
+
return (cause && (cause.code || cause.name)) || (e && (e.code || e.name)) || 'error';
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
/** Как verify, но вместо null — причина отказа строкой (для диагностики). */
|
|
465
|
+
const verifyWhy = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {}) => {
|
|
346
466
|
const key = config.lanKey();
|
|
347
467
|
let target;
|
|
348
468
|
try {
|
|
349
469
|
target = new URL(url);
|
|
350
470
|
} catch (e) {
|
|
351
|
-
return
|
|
471
|
+
return 'bad_url';
|
|
352
472
|
}
|
|
353
|
-
if (!key
|
|
354
|
-
return
|
|
473
|
+
if (!key) {
|
|
474
|
+
return 'no_lan_key';
|
|
475
|
+
}
|
|
476
|
+
if (target.protocol !== 'http:' || !IPV4.test(target.hostname)) {
|
|
477
|
+
return 'bad_url';
|
|
355
478
|
}
|
|
356
479
|
const port = Number(target.port || 80);
|
|
357
480
|
const nonce = randomBytes(16).toString('hex');
|
|
@@ -363,12 +486,12 @@ const verify = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {})
|
|
|
363
486
|
redirect: 'error',
|
|
364
487
|
});
|
|
365
488
|
if (!response.ok) {
|
|
366
|
-
return
|
|
489
|
+
return `http_${response.status}`;
|
|
367
490
|
}
|
|
368
491
|
const body = Buffer.from(await response.arrayBuffer());
|
|
369
|
-
return
|
|
492
|
+
return check(parse(body), {key, nonce, address: target.hostname, companyId, port});
|
|
370
493
|
} catch (e) {
|
|
371
|
-
return
|
|
494
|
+
return fetchError(e);
|
|
372
495
|
} finally {
|
|
373
496
|
clearTimeout(timer);
|
|
374
497
|
}
|
|
@@ -378,12 +501,30 @@ const verify = async (url, {companyId = null, timeout = VERIFY_TIMEOUT_MS} = {})
|
|
|
378
501
|
* Скан подсети по HTTP пачками по SCAN_BATCH. Останавливается на первой пачке, где кто-то
|
|
379
502
|
* ответил. hosts — вместо своей подсети (тесты).
|
|
380
503
|
*/
|
|
381
|
-
const scan = async ({companyId = null, hosts = null, port = settings().httpPort} = {}) => {
|
|
504
|
+
const scan = async ({companyId = null, hosts = null, port = settings().httpPort, stats = null} = {}) => {
|
|
382
505
|
const list = hosts || subnetHosts();
|
|
506
|
+
if (stats) {
|
|
507
|
+
stats.hosts = list.length;
|
|
508
|
+
stats.first = list[0] || null;
|
|
509
|
+
stats.last = list[list.length - 1] || null;
|
|
510
|
+
}
|
|
383
511
|
for (let i = 0; i < list.length; i += SCAN_BATCH) {
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
512
|
+
const batch = list.slice(i, i + SCAN_BATCH);
|
|
513
|
+
const answers = await Promise.all(batch
|
|
514
|
+
.map((host) => verifyWhy(`http://${host}:${port}`, {companyId, timeout: SCAN_TIMEOUT_MS})));
|
|
515
|
+
if (stats) {
|
|
516
|
+
answers.forEach((answer, index) => {
|
|
517
|
+
if (typeof answer !== 'string') {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
bump(stats.errors, answer);
|
|
521
|
+
// Кто-то ответил по HTTP, но не как наш мастер — адрес интересен: возможно, это он.
|
|
522
|
+
if (answer !== 'timeout' && answer !== 'ECONNREFUSED' && stats.responded.length < 10) {
|
|
523
|
+
stats.responded.push(`${batch[index]}:${answer}`);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
const found = answers.filter((answer) => typeof answer !== 'string');
|
|
387
528
|
if (found.length) {
|
|
388
529
|
return found;
|
|
389
530
|
}
|
|
@@ -420,6 +561,9 @@ class Finder {
|
|
|
420
561
|
this.lastFoundAt = null;
|
|
421
562
|
this.candidates = [];
|
|
422
563
|
this.conflict = false;
|
|
564
|
+
this.searches = 0;
|
|
565
|
+
this.lastSearch = null;
|
|
566
|
+
this.lastScan = null;
|
|
423
567
|
}
|
|
424
568
|
|
|
425
569
|
start() {
|
|
@@ -468,30 +612,44 @@ class Finder {
|
|
|
468
612
|
|
|
469
613
|
async search() {
|
|
470
614
|
this.lastProbeAt = new Date().toISOString();
|
|
615
|
+
this.searches += 1;
|
|
471
616
|
const companyId = this.companyId();
|
|
472
617
|
let candidates = [];
|
|
473
618
|
let via = null;
|
|
619
|
+
// Диагностика: что дала каждая ступень последнего поиска.
|
|
620
|
+
const report = {at: this.lastProbeAt, company_id: companyId, known: {}, udp: null};
|
|
474
621
|
// 1. известный адрес: текущий или сохранённый с прошлого запуска
|
|
475
622
|
const known = [...new Set([this.current, this.remembered()].filter(Boolean))];
|
|
476
623
|
for (const url of known) {
|
|
477
|
-
const candidate = await
|
|
478
|
-
if (candidate) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
break;
|
|
624
|
+
const candidate = await verifyWhy(url, {companyId});
|
|
625
|
+
if (typeof candidate === 'string') {
|
|
626
|
+
report.known[url] = candidate;
|
|
627
|
+
continue;
|
|
482
628
|
}
|
|
629
|
+
report.known[url] = 'ok';
|
|
630
|
+
candidates = [candidate];
|
|
631
|
+
via = 'saved';
|
|
632
|
+
break;
|
|
483
633
|
}
|
|
484
634
|
// 2. широковещательный запрос
|
|
485
635
|
if (!candidates.length && !this.stopped) {
|
|
486
|
-
|
|
636
|
+
const stats = {targets: [], sent: 0, send_errors: 0, answers: 0, rejected: {}, error: null};
|
|
637
|
+
candidates = await probe({companyId, targets: this.targets, stats});
|
|
638
|
+
report.udp = stats;
|
|
487
639
|
via = 'udp';
|
|
488
640
|
}
|
|
489
641
|
// 3. скан подсети — широковещание могло не дойти (фильтр точки доступа, брандмауэр)
|
|
490
642
|
if (!candidates.length && !this.stopped && this.scanDue()) {
|
|
491
643
|
this.lastScanAt = Date.now();
|
|
492
|
-
|
|
644
|
+
const stats = {at: new Date().toISOString(), hosts: 0, first: null, last: null,
|
|
645
|
+
errors: {}, responded: [], found: 0};
|
|
646
|
+
candidates = await scan({companyId, hosts: this.scanHosts, stats});
|
|
647
|
+
stats.found = candidates.length;
|
|
648
|
+
this.lastScan = stats;
|
|
493
649
|
via = 'scan';
|
|
494
650
|
}
|
|
651
|
+
report.found = candidates.map((c) => c.url);
|
|
652
|
+
this.lastSearch = report;
|
|
495
653
|
if (this.stopped) {
|
|
496
654
|
return;
|
|
497
655
|
}
|
|
@@ -532,6 +690,10 @@ class Finder {
|
|
|
532
690
|
conflict: this.conflict,
|
|
533
691
|
via: this.via,
|
|
534
692
|
last_scan_at: this.lastScanAt ? new Date(this.lastScanAt).toISOString() : null,
|
|
693
|
+
searches: this.searches,
|
|
694
|
+
interfaces: interfaces(),
|
|
695
|
+
last_search: this.lastSearch,
|
|
696
|
+
last_scan: this.lastScan,
|
|
535
697
|
};
|
|
536
698
|
}
|
|
537
699
|
|
|
@@ -543,4 +705,5 @@ class Finder {
|
|
|
543
705
|
|
|
544
706
|
const httpAnswers = () => httpAnswered;
|
|
545
707
|
|
|
546
|
-
module.exports = {Responder, Finder, probe, verify, scan, whoami, httpAnswers, sign,
|
|
708
|
+
module.exports = {Responder, Finder, probe, verify, verifyWhy, scan, whoami, whoamiStatus, httpAnswers, sign,
|
|
709
|
+
interfaces, settings};
|
package/app/sync/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const {CloudSignal} = require('./signal');
|
|
|
7
7
|
const {MasterBridge} = require('./bridge');
|
|
8
8
|
const {lanHeaders} = require('./lan');
|
|
9
9
|
const discovery = require('./discovery');
|
|
10
|
+
const netstats = require('../netstats');
|
|
10
11
|
const lanDevices = require('./lanDevices');
|
|
11
12
|
const lanFront = require('./lanFront');
|
|
12
13
|
const {getConfig} = require('../config');
|
|
@@ -1125,8 +1126,8 @@ const startDiscovery = () => {
|
|
|
1125
1126
|
};
|
|
1126
1127
|
|
|
1127
1128
|
/** Ответ мастера на GET /local/whoami (ступени поиска ведомого). null — 404. */
|
|
1128
|
-
const whoami = (nonce) => (config.role() === 'master' && state.started
|
|
1129
|
-
? discovery.whoami({nonce, httpPort: getConfig().port, identity: state.session})
|
|
1129
|
+
const whoami = (nonce, from = null) => (config.role() === 'master' && state.started
|
|
1130
|
+
? discovery.whoami({nonce, httpPort: getConfig().port, identity: state.session, from})
|
|
1130
1131
|
: null);
|
|
1131
1132
|
|
|
1132
1133
|
/**
|
|
@@ -1441,7 +1442,7 @@ const resume = async ({code} = {}) => {
|
|
|
1441
1442
|
const key = throttleKey(session.team_id, session.terminal_id);
|
|
1442
1443
|
unlockThrottle.assertAllowed(key);
|
|
1443
1444
|
const user = code
|
|
1444
|
-
? await findUnlockUser(state.store, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt)
|
|
1445
|
+
? await findUnlockUser(state.store, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt, blob.terminal)
|
|
1445
1446
|
: null;
|
|
1446
1447
|
if (!user) {
|
|
1447
1448
|
unlockThrottle.fail(key);
|
|
@@ -1525,6 +1526,10 @@ const status = () => {
|
|
|
1525
1526
|
|
|
1526
1527
|
const result = {
|
|
1527
1528
|
enabled,
|
|
1529
|
+
// Фронт деплоится раньше, чем pos-server доезжает до касс: по версии и списку событий
|
|
1530
|
+
// ядра он понимает, что этот Node умеет офлайн (лениво: ядро само требует app/sync).
|
|
1531
|
+
version: PACKAGE_VERSION,
|
|
1532
|
+
core_events: require('../core').offlineEvents(),
|
|
1528
1533
|
mode: enabled ? state.mode : 'cloud',
|
|
1529
1534
|
role: config.role(),
|
|
1530
1535
|
session: Boolean(state.session),
|
|
@@ -1576,12 +1581,16 @@ const status = () => {
|
|
|
1576
1581
|
ready: Boolean(state.signal && state.signal.relayReady),
|
|
1577
1582
|
};
|
|
1578
1583
|
result.discovery = state.responder
|
|
1579
|
-
? {port: discovery.settings().port, answering: Boolean(state.responder.socket),
|
|
1584
|
+
? {port: discovery.settings().port, answering: Boolean(state.responder.socket),
|
|
1585
|
+
answered: state.responder.answered, udp: state.responder.status(),
|
|
1586
|
+
whoami: discovery.whoamiStatus(), interfaces: discovery.interfaces()}
|
|
1580
1587
|
: null;
|
|
1581
1588
|
result.lan = config.isLanFrontEnabled()
|
|
1582
1589
|
? {urls: lanFront.urls(getConfig().port), front: lanFront.status(), devices: lanDevices.status()}
|
|
1583
1590
|
: null;
|
|
1584
1591
|
}
|
|
1592
|
+
// Слушает ли Node на 0.0.0.0 и доходят ли до него соединения из зала (app/netstats.js).
|
|
1593
|
+
result.net = netstats.snapshot();
|
|
1585
1594
|
return result;
|
|
1586
1595
|
};
|
|
1587
1596
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alizadefaig/pos-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.31",
|
|
4
4
|
"description": "Server in express js with Proxy to Main server if need and will work with Electron JS",
|
|
5
5
|
"homepage": "https://gitlab.com/faigalizade/pos-server#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -99,7 +99,7 @@ registerOfflineRoutes(router, '/api', {
|
|
|
99
99
|
const key = throttleKey(context.teamId, context.terminalId);
|
|
100
100
|
unlockThrottle.assertAllowed(key);
|
|
101
101
|
const blob = readInit();
|
|
102
|
-
const user = await findUnlockUser(db, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt);
|
|
102
|
+
const user = await findUnlockUser(db, Array.isArray(blob.users) ? blob.users : [], code, blob.pin_salt, blob.terminal);
|
|
103
103
|
if (!user) {
|
|
104
104
|
unlockThrottle.fail(key);
|
|
105
105
|
return null;
|
|
@@ -16,18 +16,40 @@ const appConfig = getConfig();
|
|
|
16
16
|
let PROXY_TARGET = appConfig.apiUrl || 'http://localhost:8000';
|
|
17
17
|
const getUsbPrinters = async () => {
|
|
18
18
|
let result = [];
|
|
19
|
-
|
|
19
|
+
let usbDevices = [];
|
|
20
|
+
try {
|
|
21
|
+
usbDevices = escpos.USB.findPrinter();
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.warn('[printers] usb scan failed:', e.message);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
20
26
|
for (const usbDevice of usbDevices) {
|
|
21
|
-
usbDevice.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
const {idVendor, idProduct, iProduct} = usbDevice.deviceDescriptor;
|
|
28
|
+
const printer = {type: 'USB', name: null, vendor_id: idVendor, product_id: idProduct};
|
|
29
|
+
// На Windows libusb открывает только устройство на драйвере WinUSB. Принтер на родном
|
|
30
|
+
// драйвере (usbprint.sys / драйвер производителя) даёт LIBUSB_ERROR_NOT_SUPPORTED —
|
|
31
|
+
// раньше это исключение роняло весь скан в 500, и не было видно даже LAN-принтеров.
|
|
32
|
+
let opened = false;
|
|
33
|
+
try {
|
|
34
|
+
usbDevice.open();
|
|
35
|
+
opened = true;
|
|
36
|
+
printer.name = await getStringDescriptor(usbDevice, iProduct);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
printer.error = e.message;
|
|
39
|
+
console.warn(`[printers] usb ${idVendor}:${idProduct} not opened:`, e.message);
|
|
40
|
+
} finally {
|
|
41
|
+
if (opened) {
|
|
42
|
+
try {
|
|
43
|
+
usbDevice.close();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!printer.name) {
|
|
49
|
+
const hex = (n) => Number(n).toString(16).padStart(4, '0');
|
|
50
|
+
printer.name = `USB ${hex(idVendor)}:${hex(idProduct)}`;
|
|
51
|
+
}
|
|
52
|
+
result.push(printer);
|
|
31
53
|
}
|
|
32
54
|
|
|
33
55
|
return result;
|
package/routes/desktop/local.js
CHANGED
|
@@ -61,7 +61,7 @@ router.get('/local/status', (req, res) => {
|
|
|
61
61
|
|
|
62
62
|
// Ведомый ищет мастера (app/sync/discovery.js): подписанный ответ на nonce, без ключа зала.
|
|
63
63
|
router.get('/local/whoami', (req, res) => {
|
|
64
|
-
const answer = isSyncEnabled() ? sync.whoami(req.query.nonce) : null;
|
|
64
|
+
const answer = isSyncEnabled() ? sync.whoami(req.query.nonce, String(req.socket.remoteAddress || '').replace(/^::ffff:/, '')) : null;
|
|
65
65
|
if (!answer) {
|
|
66
66
|
return res.status(404).json({success: false});
|
|
67
67
|
}
|
|
@@ -17,7 +17,7 @@ const init = (services) => new InitService(services);
|
|
|
17
17
|
|
|
18
18
|
registerOfflineRoutes(router, '/api', {
|
|
19
19
|
/** AuthController::init. `user` в ответ не входит: токен кладётся в модель и не выводится. */
|
|
20
|
-
'app.init': [CLOUD_READ, async (services) => init(services).init()],
|
|
20
|
+
'app.init': [CLOUD_READ, async (services, params, extra) => init(services).init(extra)],
|
|
21
21
|
|
|
22
22
|
/** ProductController::get / OrderProductController::get — оба ProductService::allProducts. */
|
|
23
23
|
'products.get': [CLOUD_READ, async (services) => init(services).products(services.context.teamId)],
|
package/server.js
CHANGED
|
@@ -13,6 +13,7 @@ const {internalGuard, originGuard} = require("./app/security");
|
|
|
13
13
|
const {isSyncEnabled} = require("./app/sync/config");
|
|
14
14
|
const multer = require('multer');
|
|
15
15
|
const {getLogger} = require("./app/logger/Log");
|
|
16
|
+
const netstats = require("./app/netstats");
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
|
|
@@ -69,6 +70,8 @@ const bodyParsers = [
|
|
|
69
70
|
express.urlencoded({extended: true, limit: BODY_LIMIT}),
|
|
70
71
|
upload.any(),
|
|
71
72
|
];
|
|
73
|
+
// Запросы из зала (не loopback) и их исход — для диагностики кассы (/local/status.net).
|
|
74
|
+
app.use(netstats.middleware);
|
|
72
75
|
// Мастер раздаёт фронт телефонам зала (LAN_FRONT): страница и её файлы открыты без ключа и
|
|
73
76
|
// токена — данных в них нет, а API за ними проверяется ниже. No-op без флага.
|
|
74
77
|
app.use(require('./app/sync/lanFront').middleware());
|
|
@@ -178,6 +181,7 @@ const runExpress = (conf = null) => {
|
|
|
178
181
|
|
|
179
182
|
// ==== Start Listen ====
|
|
180
183
|
const hostname = appConfig.host || 'localhost';
|
|
184
|
+
netstats.attach(server, {host: hostname, port});
|
|
181
185
|
server.listen(port, hostname, async () => {
|
|
182
186
|
if (hostname === 'localhost') {
|
|
183
187
|
console.log(`Server started on ${hostname} http://${hostname}:${port}`);
|