work-license-server 1.0.14 → 1.0.16

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/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">При нажатии «Купить» заказ добавится в ваш Личный кабинет, где вы сможете оплатить его через ЮKassa или выставить счёт-договор для юр. лица.</p>
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,45 +337,37 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
333
337
  <div class="tiers">${renderTiers()}</div>
334
338
  </section>
335
339
 
336
- <!-- ============ ЛИЧНЫЙ КАБИНЕТ (открывается после входа / регистрации) ============ -->
340
+ <!-- ============ ЛИЧНЫЙ КАБИНЕТ: ЛЕВОЕ МЕНЮ РАЗДЕЛОВ + ЧИСТЫЕ РЕЕСТРЫ ============ -->
337
341
  <section class="stage" id="stage-addons">
338
- <div class="sec-head">
339
- <div><span class="kicker">Личный кабинет владельца</span><h2 class="h2">Заказы, токен привязки и лицензии</h2></div>
340
- <p id="userBadgeBox"></p>
341
- </div>
342
+ <div class="sec-layout">
343
+ <aside class="sec-nav" aria-label="Разделы Личного кабинета">
344
+ <button type="button" class="sec-nav-item active" data-cab-tab="servers" id="navCabServers">Серверы и устройства (0)</button>
345
+ <button type="button" class="sec-nav-item" data-cab-tab="franchises" id="navCabFranchises">Региональные франшизы (0)</button>
346
+ </aside>
342
347
 
343
- <div class="card" style="margin-bottom:20px">
344
- <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap">
345
- <div>
346
- <span class="kicker">Заказы и оплата</span>
347
- <h3 class="h3" style="margin:4px 0 0">Ваши заказы в Личном кабинете</h3>
348
- </div>
349
- <div style="display:flex;gap:8px;flex-wrap:wrap">
350
- <button class="btn btn-ghost btn-sm" data-stage="clients">+ Добавить лицензию / ПАК</button>
351
- <button class="btn btn-ghost btn-sm" data-stage="franchise">+ Зарезервировать франшизу</button>
352
- <button class="btn btn-ghost btn-sm" data-action="refresh-cabinet">Обновить</button>
348
+ <div class="sec-main">
349
+ <!-- 1.1 РЕЕСТР СЕРВЕРОВ (УСТРОЙСТВ) WORK -->
350
+ <div id="cabSecServers">
351
+ <div class="sec-bar" style="margin-bottom:12px">
352
+ <h2 class="sec-title">Серверы и устройства WORK</h2>
353
+ <div class="sec-controls">
354
+ <input type="text" id="inpNewServerName" placeholder="Название нового сервера…">
355
+ <button type="button" class="btn btn-primary btn-sm" data-action="create-server">+ Добавить сервер</button>
356
+ </div>
357
+ </div>
358
+ <div class="sec-box" id="cabinetServersRegistry"></div>
353
359
  </div>
354
- </div>
355
- <div id="cabinetOrdersBox" style="margin-top:14px"></div>
356
- </div>
357
360
 
358
- <div class="split">
359
- <div class="card">
360
- <span class="kicker">Привязка локального сервера WORK</span>
361
- <h3 class="h3" style="margin:4px 0 8px">Токен владельца (содержит URL сервера поддержки)</h3>
362
- <p class="small" style="margin:0 0 12px">Скопируйте этот токен и вставьте его в разделе <b>WORK → Лицензия</b> вашей локальной системы. По этому токену система сама скачивает и обновляет оплаченные файлы лицензий в папку <code>#system</code>.</p>
363
- <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center">
364
- <input type="text" id="inpCabinetHubToken" readonly placeholder="owk1..." style="flex:1;min-width:220px;font-family:monospace">
365
- <button class="btn btn-primary btn-sm" data-action="copy-hub-token">Скопировать токен</button>
366
- <button class="btn btn-ghost btn-sm" data-action="gen-hub-token">Перевыпустить</button>
361
+ <!-- 1.2 РЕЕСТР РЕГИОНАЛЬНЫХ ФРАНШИЗ -->
362
+ <div id="cabSecFranchises" style="display:none">
363
+ <div class="sec-bar" style="margin-bottom:12px">
364
+ <h2 class="sec-title">Региональные франшизы</h2>
365
+ <div class="sec-controls">
366
+ <button type="button" class="btn btn-primary btn-sm" data-stage="franchise">+ Выбрать регион на карте РФ</button>
367
+ </div>
368
+ </div>
369
+ <div class="sec-box" id="cabinetFranchisesRegistry"></div>
367
370
  </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
371
  </div>
376
372
  </div>
377
373
  </section>
@@ -425,7 +421,7 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
425
421
  <div><span>Остаток (в течение года)</span><b id="mapRegRest">—</b></div>
426
422
  </div>
427
423
  <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">При нажатии «Зарезервировать» заявка на регион добавится как заказ в ваш Личный кабинет.</p>
424
+ <p class="small muted" style="margin:8px 0 0;text-align:center">Не более 1 франшизы в одном регионе. После резервирования даётся <b>15 минут</b> на оплату начального взноса через ЮKassa; далее — годовой график погашения (раз в месяц).</p>
429
425
  <div class="status" id="partnerActionMsg"></div>
430
426
  </aside>
431
427
  </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 Генерация или перевыпуск токена владельца (с зашитым URL сервера поддержки) в личном кабинете
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
- reservation = reserved.reservation;
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
- reservations: Object.values(store.state.reservations || {}),
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);