work-license-server 1.0.15 → 1.0.17
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 +57 -41
- package/assets/landing.css +150 -0
- package/assets/landing.js +195 -170
- package/package.json +1 -1
- package/src/admin.js +189 -186
- package/src/crypto.js +72 -0
- package/src/landing.js +40 -39
- package/src/server.js +20 -1
- package/src/store.js +56 -10
package/assets/admin.js
CHANGED
|
@@ -16,28 +16,44 @@
|
|
|
16
16
|
let activeTab = 'servers';
|
|
17
17
|
let admCountdownTimer = null;
|
|
18
18
|
|
|
19
|
+
function readCookieSession() {
|
|
20
|
+
try {
|
|
21
|
+
const m = String(document.cookie || '').match(/(?:^|;\s*)odant_work_session=([^;]+)/);
|
|
22
|
+
return m?.[1] ? decodeURIComponent(m[1].trim()) : '';
|
|
23
|
+
}
|
|
24
|
+
catch { return ''; }
|
|
25
|
+
}
|
|
26
|
+
|
|
19
27
|
function getStoredToken() {
|
|
20
28
|
try {
|
|
21
|
-
return
|
|
29
|
+
return localStorage.getItem('odant_work_session')
|
|
22
30
|
|| localStorage.getItem(TOKEN_KEY)
|
|
23
|
-
||
|
|
31
|
+
|| sessionStorage.getItem(TOKEN_KEY)
|
|
32
|
+
|| sessionStorage.getItem('odant_work_session')
|
|
33
|
+
|| readCookieSession()
|
|
24
34
|
|| '';
|
|
25
35
|
}
|
|
26
|
-
catch { return
|
|
36
|
+
catch { return readCookieSession(); }
|
|
27
37
|
}
|
|
28
|
-
function setStoredToken(tok) {
|
|
38
|
+
function setStoredToken(tok, email = '') {
|
|
29
39
|
adminToken = tok || '';
|
|
30
40
|
try {
|
|
31
41
|
if (tok) {
|
|
32
42
|
sessionStorage.setItem(TOKEN_KEY, tok);
|
|
33
43
|
localStorage.setItem(TOKEN_KEY, tok);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
44
|
+
localStorage.setItem('odant_work_session', tok);
|
|
45
|
+
sessionStorage.setItem('odant_work_session', tok);
|
|
46
|
+
document.cookie = `odant_work_session=${encodeURIComponent(tok)}; path=/; max-age=31536000; SameSite=Lax`;
|
|
37
47
|
}
|
|
38
48
|
else {
|
|
39
49
|
sessionStorage.removeItem(TOKEN_KEY);
|
|
40
50
|
localStorage.removeItem(TOKEN_KEY);
|
|
51
|
+
localStorage.removeItem('odant_work_session');
|
|
52
|
+
sessionStorage.removeItem('odant_work_session');
|
|
53
|
+
document.cookie = 'odant_work_session=; path=/; max-age=0; SameSite=Lax';
|
|
54
|
+
}
|
|
55
|
+
if (email) {
|
|
56
|
+
localStorage.setItem('odant_work_email', email);
|
|
41
57
|
}
|
|
42
58
|
}
|
|
43
59
|
catch { /* приватный режим */ }
|
|
@@ -78,6 +94,13 @@
|
|
|
78
94
|
$('#adminLoginScreen').style.display = '';
|
|
79
95
|
$('#adminAppShell').style.display = 'none';
|
|
80
96
|
if ($('#admLoginError')) $('#admLoginError').textContent = errMsg;
|
|
97
|
+
try {
|
|
98
|
+
const savedEmail = localStorage.getItem('odant_work_email') || '';
|
|
99
|
+
if (savedEmail && $('#admLoginEmail') && !$('#admLoginEmail').value) {
|
|
100
|
+
$('#admLoginEmail').value = savedEmail;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch { /* ignore */ }
|
|
81
104
|
setTimeout(() => $('#admLoginEmail')?.focus(), 50);
|
|
82
105
|
}
|
|
83
106
|
|
|
@@ -89,10 +112,21 @@
|
|
|
89
112
|
setTimeout(() => $('#admLoginEmail')?.focus(), 50);
|
|
90
113
|
}
|
|
91
114
|
|
|
115
|
+
function moveIndicator() {
|
|
116
|
+
const active = $('#tabs .tab.active');
|
|
117
|
+
const ind = $('#tabInd');
|
|
118
|
+
if (!ind) return;
|
|
119
|
+
if (!active) { ind.style.width = '0px'; return; }
|
|
120
|
+
ind.style.left = active.offsetLeft + 'px';
|
|
121
|
+
ind.style.width = active.offsetWidth + 'px';
|
|
122
|
+
}
|
|
123
|
+
|
|
92
124
|
function showAdminApp(login = 'BOSS') {
|
|
93
125
|
$('#adminLoginScreen').style.display = 'none';
|
|
94
126
|
$('#adminAppShell').style.display = '';
|
|
95
|
-
if ($('#admUserLabel')) $('#admUserLabel').textContent =
|
|
127
|
+
if ($('#admUserLabel')) $('#admUserLabel').textContent = login;
|
|
128
|
+
if ($('#admProfileEmail')) $('#admProfileEmail').textContent = login;
|
|
129
|
+
requestAnimationFrame(moveIndicator);
|
|
96
130
|
}
|
|
97
131
|
|
|
98
132
|
async function sendAdminOtpCode() {
|
|
@@ -155,7 +189,7 @@
|
|
|
155
189
|
if (!user.isAdmin) {
|
|
156
190
|
throw new Error(`Аккаунт ${user.email} не обладает правами администратора (BOSS) сервера лицензирования.`);
|
|
157
191
|
}
|
|
158
|
-
setStoredToken(user.sessionKey);
|
|
192
|
+
setStoredToken(user.sessionKey, user.email);
|
|
159
193
|
resetAdmStep();
|
|
160
194
|
showAdminApp(user.email);
|
|
161
195
|
await refreshRegistry();
|
|
@@ -169,9 +203,9 @@
|
|
|
169
203
|
}
|
|
170
204
|
|
|
171
205
|
async function logoutAdmin() {
|
|
206
|
+
$('#admProfileModal')?.classList.remove('open');
|
|
172
207
|
try { await api('/api/admin/logout', {}); } catch { /* ignore */ }
|
|
173
208
|
setStoredToken('');
|
|
174
|
-
try { localStorage.removeItem('odant_work_session'); } catch { /* ignore */ }
|
|
175
209
|
resetAdmStep();
|
|
176
210
|
showLoginGate('');
|
|
177
211
|
}
|
|
@@ -186,6 +220,7 @@
|
|
|
186
220
|
try {
|
|
187
221
|
const me = await api('/api/admin/me');
|
|
188
222
|
if (me.authenticated) {
|
|
223
|
+
setStoredToken(saved, me.email || me.login || '');
|
|
189
224
|
showAdminApp(me.login || 'admin');
|
|
190
225
|
await refreshRegistry();
|
|
191
226
|
}
|
|
@@ -201,7 +236,7 @@
|
|
|
201
236
|
/* ================= Вкладки и форматирование ================= */
|
|
202
237
|
function setTab(tab) {
|
|
203
238
|
activeTab = tab || 'servers';
|
|
204
|
-
$$('
|
|
239
|
+
$$('[data-adm-tab]').forEach(b => b.classList.toggle('active', b.dataset.admTab === activeTab));
|
|
205
240
|
$$('[data-adm-sec]').forEach(sec => {
|
|
206
241
|
sec.style.display = sec.dataset.admSec === activeTab ? '' : 'none';
|
|
207
242
|
});
|
|
@@ -265,34 +300,13 @@
|
|
|
265
300
|
const users = data.users || [];
|
|
266
301
|
|
|
267
302
|
const activeFranchises = franchises.filter(f => f && f.status !== 'canceled' && f.status !== 'expired');
|
|
268
|
-
const totalSeats = licenses.reduce((s, l) => s + (l.totalSeats || 0), 0);
|
|
269
|
-
const paidOrders = orders.filter(o => o.status === 'paid');
|
|
270
|
-
const pendingOrders = orders.filter(o => o.status === 'pending' || o.status === 'awaiting_invoice');
|
|
271
|
-
const paidRub = paidOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
272
|
-
const pendingRub = pendingOrders.reduce((s, o) => s + (Number(o.amountRub) || 0), 0);
|
|
273
303
|
const openTickets = tickets.filter(t => t.status !== 'answered' && t.status !== 'closed');
|
|
274
304
|
|
|
275
|
-
if ($('#
|
|
276
|
-
if ($('#
|
|
277
|
-
if ($('#
|
|
278
|
-
if ($('#
|
|
279
|
-
if ($('#
|
|
280
|
-
|
|
281
|
-
if ($('#admStatLic')) {
|
|
282
|
-
$('#admStatLic').textContent = String(licenses.length);
|
|
283
|
-
$('#admStatLicSub').textContent = `серверов · ${totalSeats} мест`;
|
|
284
|
-
$('#admStatFranchises').textContent = String(activeFranchises.length);
|
|
285
|
-
const overdueCnt = activeFranchises.filter(f => (f.overdueMonthsCount || 0) > 0).length;
|
|
286
|
-
$('#admStatFranchisesSub').textContent = overdueCnt > 0 ? `просрочек: ${overdueCnt}` : 'активных франшиз';
|
|
287
|
-
$('#admStatPaid').textContent = rub(paidRub);
|
|
288
|
-
$('#admStatPaidSub').textContent = `оплачено заказов: ${paidOrders.length}`;
|
|
289
|
-
$('#admStatPending').textContent = rub(pendingRub);
|
|
290
|
-
$('#admStatPendingSub').textContent = `ожидают оплаты: ${pendingOrders.length}`;
|
|
291
|
-
$('#admStatUsers').textContent = String(users.length);
|
|
292
|
-
$('#admStatUsersSub').textContent = `админов BOSS: ${users.filter(u => u.isAdmin).length}`;
|
|
293
|
-
$('#admStatTickets').textContent = String(openTickets.length);
|
|
294
|
-
$('#admStatTicketsSub').textContent = `открытых из ${tickets.length}`;
|
|
295
|
-
}
|
|
305
|
+
if ($('#navAdmServers')) $('#navAdmServers').textContent = `Серверы и лицензии (${licenses.length})`;
|
|
306
|
+
if ($('#navAdmFranchises')) $('#navAdmFranchises').textContent = `Франшизы (${activeFranchises.length})`;
|
|
307
|
+
if ($('#navAdmOrders')) $('#navAdmOrders').textContent = `Заказы и счета (${orders.length})`;
|
|
308
|
+
if ($('#navAdmUsers')) $('#navAdmUsers').textContent = `Клиенты и роли (${users.length})`;
|
|
309
|
+
if ($('#navAdmTickets')) $('#navAdmTickets').textContent = `Тикеты поддержки (${openTickets.length})`;
|
|
296
310
|
|
|
297
311
|
renderServersTable(licenses);
|
|
298
312
|
renderFranchisesTable(franchises);
|
|
@@ -364,10 +378,8 @@
|
|
|
364
378
|
<button class="btn btn-ghost btn-sm" style="padding:3px 8px;font-size:11.5px" data-adm-open-grant="${esc(lid)}" data-srv-name="${esc(srvName)}">+ Начислить доп. места</button>
|
|
365
379
|
</div>
|
|
366
380
|
</td>
|
|
367
|
-
<td>
|
|
368
|
-
<
|
|
369
|
-
<button class="btn btn-primary btn-sm" data-cert="${esc(lid)}">Файлы .lic.json (${(l.licenseFiles || []).length})</button>
|
|
370
|
-
</div>
|
|
381
|
+
<td style="text-align:right">
|
|
382
|
+
<button class="btn btn-primary btn-sm" style="padding:6px 10px;font-size:12px" data-cert="${esc(lid)}">.lic.json (${(l.licenseFiles || []).length})</button>
|
|
371
383
|
</td>
|
|
372
384
|
</tr>`;
|
|
373
385
|
}).join('') || '<tr><td colspan="6" class="muted">Серверов по заданному фильтру не найдено</td></tr>';
|
|
@@ -913,6 +925,8 @@
|
|
|
913
925
|
case 'adm-send-code': sendAdminOtpCode(); break;
|
|
914
926
|
case 'adm-verify-code': verifyAdminOtpCode(); break;
|
|
915
927
|
case 'adm-reset-step': resetAdmStep(); break;
|
|
928
|
+
case 'open-adm-profile': $('#admProfileModal')?.classList.add('open'); break;
|
|
929
|
+
case 'close-adm-profile': $('#admProfileModal')?.classList.remove('open'); break;
|
|
916
930
|
case 'admin-logout': logoutAdmin(); break;
|
|
917
931
|
case 'registry': refreshRegistry(); break;
|
|
918
932
|
case 'open-create-server-modal': $('#admCreateServerModal')?.classList.add('open'); break;
|
|
@@ -949,12 +963,14 @@
|
|
|
949
963
|
$$('.modal.open').forEach(m => m.classList.remove('open'));
|
|
950
964
|
}
|
|
951
965
|
});
|
|
966
|
+
window.addEventListener('resize', moveIndicator);
|
|
952
967
|
}
|
|
953
968
|
|
|
954
969
|
function start() {
|
|
955
970
|
applyTheme(document.documentElement.getAttribute('data-theme') || 'dark');
|
|
956
971
|
bind();
|
|
957
972
|
setTab('servers');
|
|
973
|
+
if (document.fonts?.ready) document.fonts.ready.then(moveIndicator);
|
|
958
974
|
checkAdminSession();
|
|
959
975
|
}
|
|
960
976
|
|
package/assets/landing.css
CHANGED
|
@@ -614,6 +614,156 @@ body[data-stage="admin"] .ai-fab { display: none; }
|
|
|
614
614
|
.admin-left { position: static; }
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
+
/* ---------- Двухколоночная навигация по разделам (эталон Скрина 1: Личный кабинет и Админ-панель) ---------- */
|
|
618
|
+
.sec-layout {
|
|
619
|
+
display: grid;
|
|
620
|
+
grid-template-columns: 230px minmax(0, 1fr);
|
|
621
|
+
gap: 28px;
|
|
622
|
+
align-items: start;
|
|
623
|
+
margin-top: 4px;
|
|
624
|
+
}
|
|
625
|
+
@media (max-width: 920px) {
|
|
626
|
+
.sec-layout { grid-template-columns: 1fr; gap: 16px; }
|
|
627
|
+
}
|
|
628
|
+
.sec-nav {
|
|
629
|
+
display: flex;
|
|
630
|
+
flex-direction: column;
|
|
631
|
+
gap: 4px;
|
|
632
|
+
position: sticky;
|
|
633
|
+
top: 84px;
|
|
634
|
+
}
|
|
635
|
+
.sec-nav-item {
|
|
636
|
+
display: flex;
|
|
637
|
+
align-items: center;
|
|
638
|
+
justify-content: space-between;
|
|
639
|
+
width: 100%;
|
|
640
|
+
padding: 9px 14px;
|
|
641
|
+
border-radius: 8px;
|
|
642
|
+
border: 1.5px solid transparent;
|
|
643
|
+
background: transparent;
|
|
644
|
+
color: var(--text-2);
|
|
645
|
+
font-size: 14px;
|
|
646
|
+
font-weight: 500;
|
|
647
|
+
text-align: left;
|
|
648
|
+
cursor: pointer;
|
|
649
|
+
transition: background .15s ease, border-color .15s ease, color .15s ease;
|
|
650
|
+
}
|
|
651
|
+
.sec-nav-item:hover {
|
|
652
|
+
background: color-mix(in srgb, var(--brand) 6%, transparent);
|
|
653
|
+
color: var(--text);
|
|
654
|
+
}
|
|
655
|
+
.sec-nav-item.active {
|
|
656
|
+
border-color: #3b82f6;
|
|
657
|
+
background: rgba(59, 130, 246, 0.08);
|
|
658
|
+
color: #1d4ed8;
|
|
659
|
+
font-weight: 600;
|
|
660
|
+
}
|
|
661
|
+
html[data-theme="dark"] .sec-nav-item.active {
|
|
662
|
+
border-color: #3b82f6;
|
|
663
|
+
background: rgba(59, 130, 246, 0.16);
|
|
664
|
+
color: #60a5fa;
|
|
665
|
+
}
|
|
666
|
+
.sec-nav-sep {
|
|
667
|
+
height: 1px;
|
|
668
|
+
background: var(--line);
|
|
669
|
+
margin: 6px 4px;
|
|
670
|
+
}
|
|
671
|
+
.sec-main {
|
|
672
|
+
min-width: 0;
|
|
673
|
+
display: flex;
|
|
674
|
+
flex-direction: column;
|
|
675
|
+
gap: 12px;
|
|
676
|
+
}
|
|
677
|
+
.sec-bar {
|
|
678
|
+
display: flex;
|
|
679
|
+
align-items: center;
|
|
680
|
+
justify-content: space-between;
|
|
681
|
+
gap: 12px;
|
|
682
|
+
flex-wrap: wrap;
|
|
683
|
+
min-height: 36px;
|
|
684
|
+
}
|
|
685
|
+
.sec-title {
|
|
686
|
+
margin: 0;
|
|
687
|
+
font-size: 16px;
|
|
688
|
+
font-weight: 700;
|
|
689
|
+
color: var(--text);
|
|
690
|
+
}
|
|
691
|
+
.sec-controls {
|
|
692
|
+
display: flex;
|
|
693
|
+
align-items: center;
|
|
694
|
+
gap: 8px;
|
|
695
|
+
flex-wrap: wrap;
|
|
696
|
+
}
|
|
697
|
+
.sec-controls input[type="text"],
|
|
698
|
+
.sec-controls input[type="search"],
|
|
699
|
+
.sec-controls select {
|
|
700
|
+
width: auto !important;
|
|
701
|
+
min-width: 0;
|
|
702
|
+
height: 34px;
|
|
703
|
+
padding: 6px 12px;
|
|
704
|
+
font-size: 13px;
|
|
705
|
+
border-radius: 8px;
|
|
706
|
+
border: 1px solid var(--line-strong);
|
|
707
|
+
background: var(--panel-solid);
|
|
708
|
+
color: var(--text);
|
|
709
|
+
}
|
|
710
|
+
.sec-controls input[type="search"],
|
|
711
|
+
.sec-controls input[type="text"] {
|
|
712
|
+
width: 230px !important;
|
|
713
|
+
}
|
|
714
|
+
.sec-controls select {
|
|
715
|
+
width: 185px !important;
|
|
716
|
+
}
|
|
717
|
+
.sec-controls .btn {
|
|
718
|
+
height: 34px;
|
|
719
|
+
padding: 0 13px;
|
|
720
|
+
font-size: 13px;
|
|
721
|
+
border-radius: 8px;
|
|
722
|
+
}
|
|
723
|
+
.sec-box {
|
|
724
|
+
background: var(--panel-solid);
|
|
725
|
+
border: 1px solid var(--line-strong);
|
|
726
|
+
border-radius: 12px;
|
|
727
|
+
padding: 16px 20px;
|
|
728
|
+
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.04);
|
|
729
|
+
overflow: hidden;
|
|
730
|
+
}
|
|
731
|
+
.sec-table {
|
|
732
|
+
width: 100%;
|
|
733
|
+
border-collapse: collapse;
|
|
734
|
+
font-size: 13.5px;
|
|
735
|
+
}
|
|
736
|
+
.sec-table th,
|
|
737
|
+
.sec-table td {
|
|
738
|
+
padding: 10px 10px;
|
|
739
|
+
text-align: left;
|
|
740
|
+
border-bottom: 1px solid var(--line);
|
|
741
|
+
vertical-align: middle;
|
|
742
|
+
white-space: normal;
|
|
743
|
+
word-break: break-word;
|
|
744
|
+
}
|
|
745
|
+
.sec-table th {
|
|
746
|
+
color: var(--muted);
|
|
747
|
+
font-size: 11px;
|
|
748
|
+
font-weight: 700;
|
|
749
|
+
letter-spacing: .05em;
|
|
750
|
+
text-transform: uppercase;
|
|
751
|
+
padding-top: 4px;
|
|
752
|
+
padding-bottom: 8px;
|
|
753
|
+
}
|
|
754
|
+
.sec-table tbody tr:last-child td {
|
|
755
|
+
border-bottom: 0;
|
|
756
|
+
}
|
|
757
|
+
.sec-table tbody tr:hover {
|
|
758
|
+
background: color-mix(in srgb, var(--brand) 4%, transparent);
|
|
759
|
+
}
|
|
760
|
+
.sec-item {
|
|
761
|
+
padding: 14px 0;
|
|
762
|
+
border-bottom: 1px solid var(--line);
|
|
763
|
+
}
|
|
764
|
+
.sec-item:first-child { padding-top: 2px; }
|
|
765
|
+
.sec-item:last-child { border-bottom: 0; padding-bottom: 2px; }
|
|
766
|
+
|
|
617
767
|
/* ---------- Оферта и вход администратора ---------- */
|
|
618
768
|
.oferta-banner {
|
|
619
769
|
padding: 16px 22px;
|