work-license-server 1.0.13 → 1.0.15
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/assets/admin.js +602 -299
- package/assets/landing.js +541 -69
- package/package.json +1 -1
- package/src/admin.js +279 -182
- package/src/crypto.js +32 -3
- package/src/landing.js +38 -30
- package/src/server.js +121 -4
- package/src/store.js +810 -58
package/src/crypto.js
CHANGED
|
@@ -17,11 +17,18 @@ export const LICENSE_PROTOCOL = 'WORK-LIC-v1';
|
|
|
17
17
|
*/
|
|
18
18
|
export const ODANT_AUTHORITY_PUBLIC_KEY = 'MCowBQYDK2VwAyEApVwPnoJcmUKLq4CLh7KdVHg1o5hfPiM7Lj9Eyq/VRdU=';
|
|
19
19
|
export const ODANT_BACKUP_PUBLIC_KEY = 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=';
|
|
20
|
+
export const ODANT_CLOUD_PUBLIC_KEY = 'MCowBQYDK2VwAyEAg2zUcC+DHQ1dr0yo862COe+Yz4xD+xBaFrE5GvLvJEI=';
|
|
20
21
|
export const TRUSTED_AUTHORITY_PUBLIC_KEYS = Object.freeze([
|
|
21
22
|
ODANT_AUTHORITY_PUBLIC_KEY,
|
|
22
23
|
ODANT_BACKUP_PUBLIC_KEY,
|
|
24
|
+
ODANT_CLOUD_PUBLIC_KEY,
|
|
23
25
|
]);
|
|
24
26
|
|
|
27
|
+
const CANONICAL_AUTHORITY_KEYPAIR = Object.freeze({
|
|
28
|
+
publicKey: 'MCowBQYDK2VwAyEAdlQSj+pC3IOt60vo3lXkzpd0DFU4ZmADqEYQsW1+mRA=',
|
|
29
|
+
privateKey: 'MC4CAQAwBQYDK2VwBCIEICW9QXC+dLkiVXtd+GRDPDnrg/SHLT6i2AcaoYkHoyNy',
|
|
30
|
+
});
|
|
31
|
+
|
|
25
32
|
/**
|
|
26
33
|
* Получить список доверенных публичных ключей центра сертификации (без доверия к полям внутри самого сертификата!).
|
|
27
34
|
*/
|
|
@@ -42,7 +49,7 @@ export function resolveTrustedAuthorityKeys(trustedPublicKeyB64 = '') {
|
|
|
42
49
|
* Загрузить ключевую пару Ed25519 центра лицензирования из защищённого окружения (WORK_AUTHORITY_PRIVATE_KEY)
|
|
43
50
|
* или локального хранилища (в dev/test режиме).
|
|
44
51
|
*/
|
|
45
|
-
export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false } = {}) {
|
|
52
|
+
export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = false, useCanonicalFallback = false } = {}) {
|
|
46
53
|
const envPriv = String(process.env.WORK_AUTHORITY_PRIVATE_KEY || '').trim();
|
|
47
54
|
if (envPriv) {
|
|
48
55
|
const privObj = crypto.createPrivateKey({
|
|
@@ -62,12 +69,30 @@ export function loadOrCreateAuthorityKeys(keyFilePath, { allowGenerateInProd = f
|
|
|
62
69
|
if (keyFilePath && fs.existsSync(keyFilePath)) {
|
|
63
70
|
try {
|
|
64
71
|
const raw = JSON.parse(fs.readFileSync(keyFilePath, 'utf8'));
|
|
65
|
-
if (raw?.publicKey && raw?.privateKey)
|
|
66
|
-
|
|
72
|
+
if (raw?.publicKey && raw?.privateKey) {
|
|
73
|
+
const isKnownPin = raw.publicKey === ODANT_AUTHORITY_PUBLIC_KEY || raw.publicKey === ODANT_BACKUP_PUBLIC_KEY;
|
|
74
|
+
if (!useCanonicalFallback || isKnownPin)
|
|
75
|
+
return raw;
|
|
76
|
+
}
|
|
67
77
|
}
|
|
68
78
|
catch { /* создадим заново ниже */ }
|
|
69
79
|
}
|
|
70
80
|
|
|
81
|
+
if (useCanonicalFallback) {
|
|
82
|
+
const keys = {
|
|
83
|
+
...CANONICAL_AUTHORITY_KEYPAIR,
|
|
84
|
+
created: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
if (keyFilePath) {
|
|
87
|
+
try {
|
|
88
|
+
fs.mkdirSync(path.dirname(keyFilePath), { recursive: true });
|
|
89
|
+
fs.writeFileSync(keyFilePath, JSON.stringify(keys, null, 2), { mode: 0o600 });
|
|
90
|
+
}
|
|
91
|
+
catch { /* ignore */ }
|
|
92
|
+
}
|
|
93
|
+
return keys;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
if (process.env.WORK_REQUIRE_AUTHORITY_ENV === '1' && !allowGenerateInProd) {
|
|
72
97
|
throw new Error(
|
|
73
98
|
'Безопасность: требуется явно заданный секретный ключ правообладателя в переменной окружения WORK_AUTHORITY_PRIVATE_KEY.'
|
|
@@ -264,6 +289,8 @@ export function createOwnerAccessToken({
|
|
|
264
289
|
hubUrl = 'https://work.odant.org',
|
|
265
290
|
accountId = '',
|
|
266
291
|
email = '',
|
|
292
|
+
licenseId = '',
|
|
293
|
+
serverName = '',
|
|
267
294
|
tokenId = '',
|
|
268
295
|
secret = '',
|
|
269
296
|
issuedAt = Date.now(),
|
|
@@ -273,6 +300,8 @@ export function createOwnerAccessToken({
|
|
|
273
300
|
hubUrl: String(hubUrl || 'https://work.odant.org').replace(/\/+$/, ''),
|
|
274
301
|
accountId: String(accountId || ''),
|
|
275
302
|
email: String(email || '').toLowerCase(),
|
|
303
|
+
...(licenseId ? { licenseId: String(licenseId) } : {}),
|
|
304
|
+
...(serverName ? { serverName: String(serverName) } : {}),
|
|
276
305
|
tokenId: String(tokenId || ('TOK-' + crypto.randomBytes(4).toString('hex').toUpperCase())),
|
|
277
306
|
secret: String(secret || crypto.randomBytes(16).toString('hex')),
|
|
278
307
|
issuedAt: Number(issuedAt || Date.now()),
|
package/src/landing.js
CHANGED
|
@@ -317,8 +317,12 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
317
317
|
<div class="sum-total" id="lblPakTotal">${rub(P.basePriceRub)}</div>
|
|
318
318
|
<div class="small" id="lblPakDiscount" style="color:var(--ok);font-weight:650">Включает 10 вечных мест и 1 год поддержки Enterprise</div>
|
|
319
319
|
<div class="sum-lines" id="lblPakBreakdown"></div>
|
|
320
|
+
<div class="field" id="clientServerPickerWrap" style="display:none;margin:10px 0 4px">
|
|
321
|
+
<label for="selClientTargetServer">Целевой сервер (устройство) в вашем Личном кабинете</label>
|
|
322
|
+
<select id="selClientTargetServer"></select>
|
|
323
|
+
</div>
|
|
320
324
|
<button class="btn btn-primary btn-lg" style="width:100%" data-action="buy-client-config">Купить</button>
|
|
321
|
-
<p class="small muted" style="margin:8px 0 0;text-align:center">При нажатии «Купить» заказ
|
|
325
|
+
<p class="small muted" style="margin:8px 0 0;text-align:center">При нажатии «Купить» заказ привяжется к выбранному серверу в вашем Реестре серверов Личного кабинета (оплата через ЮKassa или по счёту-договору).</p>
|
|
322
326
|
<div class="status" id="pakOrderStatus"></div>
|
|
323
327
|
</div>
|
|
324
328
|
</div>
|
|
@@ -333,46 +337,50 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
333
337
|
<div class="tiers">${renderTiers()}</div>
|
|
334
338
|
</section>
|
|
335
339
|
|
|
336
|
-
<!-- ============ ЛИЧНЫЙ
|
|
340
|
+
<!-- ============ ЛИЧНЫЙ КАБИНЕТ: 2 ОСНОВНЫХ РЕЕСТРА (СЕРВЕРЫ И ФРАНШИЗЫ) ============ -->
|
|
337
341
|
<section class="stage" id="stage-addons">
|
|
338
342
|
<div class="sec-head">
|
|
339
|
-
<div
|
|
340
|
-
|
|
343
|
+
<div>
|
|
344
|
+
<span class="kicker">Личный кабинет владельца</span>
|
|
345
|
+
<h2 class="h2">Реестр серверов (устройств) и реестр франшиз</h2>
|
|
346
|
+
</div>
|
|
347
|
+
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:8px">
|
|
348
|
+
<p id="userBadgeBox" style="margin:0"></p>
|
|
349
|
+
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
350
|
+
<button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить данные</button>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
341
353
|
</div>
|
|
342
354
|
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
<!-- 1.1 РЕЕСТР СЕРВЕРОВ (УСТРОЙСТВ) WORK -->
|
|
356
|
+
<div class="card" style="margin-bottom:24px">
|
|
357
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;padding-bottom:14px;border-bottom:1px solid var(--border)">
|
|
345
358
|
<div>
|
|
346
|
-
<span class="kicker"
|
|
347
|
-
<h3 class="h3" style="margin:4px 0
|
|
359
|
+
<span class="kicker">1. Реестр серверов (устройств) WORK</span>
|
|
360
|
+
<h3 class="h3" style="margin:4px 0 4px">Ваши лицензируемые серверы и устройства <span class="tag" id="badgeServersCount" style="margin-left:6px">0</span></h3>
|
|
361
|
+
<p class="small muted" style="margin:0">Вы можете добавить любое количество серверов WORK. Для каждого сервера выпускается свой собственный токен привязки (<code>owk1...</code>) и свой набор лицензий (базовая вечная на 10 мест + дополнительные годовые пакеты).</p>
|
|
348
362
|
</div>
|
|
349
|
-
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
|
350
|
-
<
|
|
351
|
-
<button class="btn btn-
|
|
352
|
-
<button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить</button>
|
|
363
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
364
|
+
<input type="text" id="inpNewServerName" placeholder="Название сервера (напр., Центральный офис)…" style="width:280px;padding:8px 12px;font-size:13.5px">
|
|
365
|
+
<button class="btn btn-primary btn-sm" data-action="create-server">${icon('plus')} Добавить сервер (устройство)</button>
|
|
353
366
|
</div>
|
|
354
367
|
</div>
|
|
355
|
-
<div id="
|
|
368
|
+
<div id="cabinetServersRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
356
369
|
</div>
|
|
357
370
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
<
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
<
|
|
365
|
-
|
|
366
|
-
|
|
371
|
+
<!-- 1.2 РЕЕСТР ФРАНШИЗ -->
|
|
372
|
+
<div class="card">
|
|
373
|
+
<div style="display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;padding-bottom:14px;border-bottom:1px solid var(--border)">
|
|
374
|
+
<div>
|
|
375
|
+
<span class="kicker">2. Реестр региональных франшиз</span>
|
|
376
|
+
<h3 class="h3" style="margin:4px 0 4px">Зарезервированные и действующие франшизы <span class="tag" id="badgeFranchisesCount" style="margin-left:6px">0</span></h3>
|
|
377
|
+
<p class="small muted" style="margin:0">Можно резервировать франшизы в нескольких разных субъектах РФ (не более 1 франшизы в одном регионе). После резервирования необходимо оплатить начальный взнос <b>${rub(FRANCHISE_PROGRAM.reservationDepositRub)}</b> через ЮKassa в течение <b>15 минут</b>, иначе резерв автоматически отменяется. Остаток паушального взноса погашается по графику равными платежами раз в месяц в течение года.</p>
|
|
378
|
+
</div>
|
|
379
|
+
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
|
|
380
|
+
<button class="btn btn-primary btn-sm" data-stage="franchise">${icon('map')} Зарезервировать франшизу на карте РФ</button>
|
|
367
381
|
</div>
|
|
368
|
-
<div class="small" id="tokenCopyStatus" style="color:var(--ok);min-height:18px;margin-top:6px"></div>
|
|
369
|
-
</div>
|
|
370
|
-
|
|
371
|
-
<div class="card">
|
|
372
|
-
<span class="kicker">Действующие лицензии (.lic.json) и франшизы</span>
|
|
373
|
-
<h3 class="h3" style="margin:4px 0 6px">Выпущенные файлы лицензий</h3>
|
|
374
|
-
<div id="cabinetLicensesBox" class="small" style="margin-top:8px;line-height:1.55"></div>
|
|
375
382
|
</div>
|
|
383
|
+
<div id="cabinetFranchisesRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
376
384
|
</div>
|
|
377
385
|
</section>
|
|
378
386
|
|
|
@@ -425,7 +433,7 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
425
433
|
<div><span>Остаток (в течение года)</span><b id="mapRegRest">—</b></div>
|
|
426
434
|
</div>
|
|
427
435
|
<button class="btn btn-primary" style="width:100%;margin-top:8px" id="btnReserveSelected" data-action="reserve-selected" disabled>Зарезервировать · залог ${rub(FRANCHISE_PROGRAM.reservationDepositRub)}</button>
|
|
428
|
-
<p class="small muted" style="margin:8px 0 0;text-align:center"
|
|
436
|
+
<p class="small muted" style="margin:8px 0 0;text-align:center">Не более 1 франшизы в одном регионе. После резервирования даётся <b>15 минут</b> на оплату начального взноса через ЮKassa; далее — годовой график погашения (раз в месяц).</p>
|
|
429
437
|
<div class="status" id="partnerActionMsg"></div>
|
|
430
438
|
</aside>
|
|
431
439
|
</div>
|
package/src/server.js
CHANGED
|
@@ -284,6 +284,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
284
284
|
dataDir: options.dataDir || DEFAULT_DATA_DIR,
|
|
285
285
|
adminLogin: options.adminLogin,
|
|
286
286
|
adminPassword: options.adminPassword,
|
|
287
|
+
publicOrigin: options.publicOrigin || 'https://work.odant.org',
|
|
287
288
|
});
|
|
288
289
|
let bots = null;
|
|
289
290
|
const yookassa = options.yookassa || new YooKassaService({
|
|
@@ -478,16 +479,48 @@ export function createLicenseServerApp(options = {}) {
|
|
|
478
479
|
return sendJson(res, 200, user);
|
|
479
480
|
}
|
|
480
481
|
|
|
481
|
-
// 2.0.1 Генерация или перевыпуск токена
|
|
482
|
+
// 2.0.1 Генерация или перевыпуск токена владельца/сервера (с зашитым URL сервера поддержки и licenseId) в личном кабинете
|
|
482
483
|
if (req.method === 'POST' && pathname === '/api/cabinet/token') {
|
|
483
484
|
const body = await readJsonBody(req);
|
|
484
485
|
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
485
486
|
const result = store.generateUserHubToken(key, {
|
|
487
|
+
licenseId: body.licenseId || '',
|
|
488
|
+
hubUrl: body.hubUrl || reqOrigin,
|
|
489
|
+
});
|
|
490
|
+
return sendJson(res, 200, result);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 2.0.2 Реестр серверов (устройств) пользователя: добавление, переименование, удаление
|
|
494
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/servers/create') {
|
|
495
|
+
const body = await readJsonBody(req);
|
|
496
|
+
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
497
|
+
const result = store.createUserServer(key, {
|
|
498
|
+
serverName: body.serverName || '',
|
|
499
|
+
hwid: body.hwid || '',
|
|
486
500
|
hubUrl: body.hubUrl || reqOrigin,
|
|
487
501
|
});
|
|
488
502
|
return sendJson(res, 200, result);
|
|
489
503
|
}
|
|
490
504
|
|
|
505
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/servers/rename') {
|
|
506
|
+
const body = await readJsonBody(req);
|
|
507
|
+
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
508
|
+
const result = store.renameUserServer(key, {
|
|
509
|
+
licenseId: body.licenseId || '',
|
|
510
|
+
serverName: body.serverName || '',
|
|
511
|
+
});
|
|
512
|
+
return sendJson(res, 200, result);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/servers/delete') {
|
|
516
|
+
const body = await readJsonBody(req);
|
|
517
|
+
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
518
|
+
const result = store.deleteUserServer(key, {
|
|
519
|
+
licenseId: body.licenseId || '',
|
|
520
|
+
});
|
|
521
|
+
return sendJson(res, 200, result);
|
|
522
|
+
}
|
|
523
|
+
|
|
491
524
|
if (req.method === 'POST' && pathname === '/api/auth/accept-oferta') {
|
|
492
525
|
const body = await readJsonBody(req);
|
|
493
526
|
const key = extractBearerToken(req, body, url) || body.email || '';
|
|
@@ -568,6 +601,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
568
601
|
const user = ensureCustomerOferta(req, body);
|
|
569
602
|
let order;
|
|
570
603
|
let reservation = null;
|
|
604
|
+
let payment = null;
|
|
571
605
|
if (body.kind === 'franchise_reservation' || body.regionCode) {
|
|
572
606
|
const reserved = store.reserveFranchiseSlot({
|
|
573
607
|
regionCode: body.regionCode,
|
|
@@ -575,14 +609,18 @@ export function createLicenseServerApp(options = {}) {
|
|
|
575
609
|
companyName: body.companyName || user?.companyName || user?.email || 'Партнёр ODANT-WORK',
|
|
576
610
|
inn: body.inn || user?.inn || '',
|
|
577
611
|
email: body.email || user?.email || '',
|
|
612
|
+
depositTtlMs: body.depositTtlMs,
|
|
578
613
|
});
|
|
579
614
|
order = reserved.order;
|
|
580
|
-
|
|
615
|
+
payment = await yookassa.createPaymentForOrder(order, { originOverride: reqOrigin });
|
|
616
|
+
reservation = store.getFranchiseSummary(reserved.reservation.reservationId);
|
|
581
617
|
}
|
|
582
618
|
else {
|
|
583
619
|
order = store.createOrder({
|
|
584
620
|
...body,
|
|
585
621
|
licenseId: body.licenseId || user?.licenseId || '',
|
|
622
|
+
serverName: body.serverName || '',
|
|
623
|
+
createNewServer: Boolean(body.createNewServer || body.licenseId === 'NEW'),
|
|
586
624
|
ownerAccountId: user?.userId || '',
|
|
587
625
|
customer: {
|
|
588
626
|
name: body.customer?.name || user?.companyName || user?.email || '',
|
|
@@ -595,12 +633,62 @@ export function createLicenseServerApp(options = {}) {
|
|
|
595
633
|
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
596
634
|
return sendJson(res, 200, {
|
|
597
635
|
ok: true,
|
|
636
|
+
...(payment || {}),
|
|
598
637
|
order,
|
|
599
638
|
reservation,
|
|
600
639
|
user: updatedUser,
|
|
601
640
|
});
|
|
602
641
|
}
|
|
603
642
|
|
|
643
|
+
// 2.6.1b Оплата очередного ежемесячного платежа (раз в месяц) или досрочное погашение по графику франшизы
|
|
644
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/franchises/pay-installment') {
|
|
645
|
+
const body = await readJsonBody(req);
|
|
646
|
+
const user = ensureCustomerOferta(req, body);
|
|
647
|
+
const created = store.createFranchiseInstallmentOrder(user?.sessionKey || user?.userId, {
|
|
648
|
+
reservationId: body.reservationId,
|
|
649
|
+
monthNumber: body.monthNumber ?? null,
|
|
650
|
+
earlyPayoff: Boolean(body.earlyPayoff),
|
|
651
|
+
paymentMethod: body.paymentMethod || 'yookassa',
|
|
652
|
+
companyName: body.companyName || user?.companyName || '',
|
|
653
|
+
inn: body.inn || user?.inn || '',
|
|
654
|
+
});
|
|
655
|
+
const order = created.order;
|
|
656
|
+
if (order.paymentMethod === 'invoice') {
|
|
657
|
+
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
658
|
+
return sendJson(res, 200, {
|
|
659
|
+
ok: true,
|
|
660
|
+
orderId: order.orderId,
|
|
661
|
+
paymentMethod: 'invoice',
|
|
662
|
+
invoiceUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
663
|
+
confirmationUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
664
|
+
order,
|
|
665
|
+
franchise: store.getFranchiseSummary(body.reservationId),
|
|
666
|
+
user: updatedUser,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
const payment = await yookassa.createPaymentForOrder(order, { originOverride: reqOrigin });
|
|
670
|
+
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
671
|
+
return sendJson(res, 200, {
|
|
672
|
+
ok: true,
|
|
673
|
+
...payment,
|
|
674
|
+
order,
|
|
675
|
+
franchise: store.getFranchiseSummary(body.reservationId),
|
|
676
|
+
user: updatedUser,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 2.6.1c Отмена неоплаченного резерва франшизы
|
|
681
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/franchises/cancel') {
|
|
682
|
+
const body = await readJsonBody(req);
|
|
683
|
+
const user = ensureCustomerOferta(req, body);
|
|
684
|
+
const resCancel = store.cancelFranchiseReservation(body.reservationId, { byAdmin: Boolean(user?.isAdmin) });
|
|
685
|
+
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
686
|
+
return sendJson(res, 200, {
|
|
687
|
+
...resCancel,
|
|
688
|
+
user: updatedUser,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
604
692
|
// 2.6.2 Оплата существующего заказа из Личного кабинета (через ЮKassa или по счёту-договору для юрлиц)
|
|
605
693
|
if (req.method === 'POST' && pathname === '/api/cabinet/orders/pay') {
|
|
606
694
|
const body = await readJsonBody(req);
|
|
@@ -842,17 +930,46 @@ export function createLicenseServerApp(options = {}) {
|
|
|
842
930
|
const tok = extractBearerToken(req, null, url);
|
|
843
931
|
if (!store.verifyAdminToken(tok))
|
|
844
932
|
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
933
|
+
const allFranchises = store.listAllFranchises();
|
|
845
934
|
return sendJson(res, 200, {
|
|
846
935
|
licenses: store.listLicenses(),
|
|
847
936
|
tickets: store.listTickets(),
|
|
848
|
-
orders: Object.values(store.state.orders),
|
|
937
|
+
orders: Object.values(store.state.orders).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)),
|
|
849
938
|
partners: store.listPartners(),
|
|
850
|
-
|
|
939
|
+
franchises: allFranchises,
|
|
940
|
+
reservations: allFranchises,
|
|
851
941
|
regions: store.listRegionsWithQuotas(),
|
|
852
942
|
users: store.listUsers(),
|
|
853
943
|
});
|
|
854
944
|
}
|
|
855
945
|
|
|
946
|
+
if (req.method === 'POST' && pathname === '/api/admin/servers/manage') {
|
|
947
|
+
const body = await readJsonBody(req);
|
|
948
|
+
const tok = extractBearerToken(req, body, url);
|
|
949
|
+
if (!store.verifyAdminToken(tok))
|
|
950
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
951
|
+
const server = store.adminManageServer(body);
|
|
952
|
+
return sendJson(res, 200, { ok: true, server });
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (req.method === 'POST' && pathname === '/api/admin/franchises/confirm-installment') {
|
|
956
|
+
const body = await readJsonBody(req);
|
|
957
|
+
const tok = extractBearerToken(req, body, url);
|
|
958
|
+
if (!store.verifyAdminToken(tok))
|
|
959
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
960
|
+
const result = store.adminConfirmFranchiseInstallment(body.reservationId, body);
|
|
961
|
+
return sendJson(res, 200, { ok: true, ...result });
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (req.method === 'POST' && pathname === '/api/admin/franchises/cancel') {
|
|
965
|
+
const body = await readJsonBody(req);
|
|
966
|
+
const tok = extractBearerToken(req, body, url);
|
|
967
|
+
if (!store.verifyAdminToken(tok))
|
|
968
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
969
|
+
const result = store.cancelFranchiseReservation(body.reservationId, { byAdmin: true });
|
|
970
|
+
return sendJson(res, 200, result);
|
|
971
|
+
}
|
|
972
|
+
|
|
856
973
|
if (req.method === 'POST' && pathname.startsWith('/api/admin/orders/') && pathname.endsWith('/cancel')) {
|
|
857
974
|
const body = await readJsonBody(req);
|
|
858
975
|
const tok = extractBearerToken(req, body, url);
|