work-license-server 1.0.14 → 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 +4 -0
- package/src/landing.js +38 -30
- package/src/server.js +120 -4
- package/src/store.js +771 -57
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
|
@@ -479,16 +479,48 @@ export function createLicenseServerApp(options = {}) {
|
|
|
479
479
|
return sendJson(res, 200, user);
|
|
480
480
|
}
|
|
481
481
|
|
|
482
|
-
// 2.0.1 Генерация или перевыпуск токена
|
|
482
|
+
// 2.0.1 Генерация или перевыпуск токена владельца/сервера (с зашитым URL сервера поддержки и licenseId) в личном кабинете
|
|
483
483
|
if (req.method === 'POST' && pathname === '/api/cabinet/token') {
|
|
484
484
|
const body = await readJsonBody(req);
|
|
485
485
|
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
486
486
|
const result = store.generateUserHubToken(key, {
|
|
487
|
+
licenseId: body.licenseId || '',
|
|
487
488
|
hubUrl: body.hubUrl || reqOrigin,
|
|
488
489
|
});
|
|
489
490
|
return sendJson(res, 200, result);
|
|
490
491
|
}
|
|
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 || '',
|
|
500
|
+
hubUrl: body.hubUrl || reqOrigin,
|
|
501
|
+
});
|
|
502
|
+
return sendJson(res, 200, result);
|
|
503
|
+
}
|
|
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
|
+
|
|
492
524
|
if (req.method === 'POST' && pathname === '/api/auth/accept-oferta') {
|
|
493
525
|
const body = await readJsonBody(req);
|
|
494
526
|
const key = extractBearerToken(req, body, url) || body.email || '';
|
|
@@ -569,6 +601,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
569
601
|
const user = ensureCustomerOferta(req, body);
|
|
570
602
|
let order;
|
|
571
603
|
let reservation = null;
|
|
604
|
+
let payment = null;
|
|
572
605
|
if (body.kind === 'franchise_reservation' || body.regionCode) {
|
|
573
606
|
const reserved = store.reserveFranchiseSlot({
|
|
574
607
|
regionCode: body.regionCode,
|
|
@@ -576,14 +609,18 @@ export function createLicenseServerApp(options = {}) {
|
|
|
576
609
|
companyName: body.companyName || user?.companyName || user?.email || 'Партнёр ODANT-WORK',
|
|
577
610
|
inn: body.inn || user?.inn || '',
|
|
578
611
|
email: body.email || user?.email || '',
|
|
612
|
+
depositTtlMs: body.depositTtlMs,
|
|
579
613
|
});
|
|
580
614
|
order = reserved.order;
|
|
581
|
-
|
|
615
|
+
payment = await yookassa.createPaymentForOrder(order, { originOverride: reqOrigin });
|
|
616
|
+
reservation = store.getFranchiseSummary(reserved.reservation.reservationId);
|
|
582
617
|
}
|
|
583
618
|
else {
|
|
584
619
|
order = store.createOrder({
|
|
585
620
|
...body,
|
|
586
621
|
licenseId: body.licenseId || user?.licenseId || '',
|
|
622
|
+
serverName: body.serverName || '',
|
|
623
|
+
createNewServer: Boolean(body.createNewServer || body.licenseId === 'NEW'),
|
|
587
624
|
ownerAccountId: user?.userId || '',
|
|
588
625
|
customer: {
|
|
589
626
|
name: body.customer?.name || user?.companyName || user?.email || '',
|
|
@@ -596,12 +633,62 @@ export function createLicenseServerApp(options = {}) {
|
|
|
596
633
|
const updatedUser = store.findUserBySessionKey(user?.sessionKey);
|
|
597
634
|
return sendJson(res, 200, {
|
|
598
635
|
ok: true,
|
|
636
|
+
...(payment || {}),
|
|
599
637
|
order,
|
|
600
638
|
reservation,
|
|
601
639
|
user: updatedUser,
|
|
602
640
|
});
|
|
603
641
|
}
|
|
604
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
|
+
|
|
605
692
|
// 2.6.2 Оплата существующего заказа из Личного кабинета (через ЮKassa или по счёту-договору для юрлиц)
|
|
606
693
|
if (req.method === 'POST' && pathname === '/api/cabinet/orders/pay') {
|
|
607
694
|
const body = await readJsonBody(req);
|
|
@@ -843,17 +930,46 @@ export function createLicenseServerApp(options = {}) {
|
|
|
843
930
|
const tok = extractBearerToken(req, null, url);
|
|
844
931
|
if (!store.verifyAdminToken(tok))
|
|
845
932
|
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
933
|
+
const allFranchises = store.listAllFranchises();
|
|
846
934
|
return sendJson(res, 200, {
|
|
847
935
|
licenses: store.listLicenses(),
|
|
848
936
|
tickets: store.listTickets(),
|
|
849
|
-
orders: Object.values(store.state.orders),
|
|
937
|
+
orders: Object.values(store.state.orders).sort((a, b) => (b.createdAt || 0) - (a.createdAt || 0)),
|
|
850
938
|
partners: store.listPartners(),
|
|
851
|
-
|
|
939
|
+
franchises: allFranchises,
|
|
940
|
+
reservations: allFranchises,
|
|
852
941
|
regions: store.listRegionsWithQuotas(),
|
|
853
942
|
users: store.listUsers(),
|
|
854
943
|
});
|
|
855
944
|
}
|
|
856
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
|
+
|
|
857
973
|
if (req.method === 'POST' && pathname.startsWith('/api/admin/orders/') && pathname.endsWith('/cancel')) {
|
|
858
974
|
const body = await readJsonBody(req);
|
|
859
975
|
const tok = extractBearerToken(req, body, url);
|