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/src/landing.js
CHANGED
|
@@ -337,50 +337,38 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
337
337
|
<div class="tiers">${renderTiers()}</div>
|
|
338
338
|
</section>
|
|
339
339
|
|
|
340
|
-
<!-- ============ ЛИЧНЫЙ КАБИНЕТ:
|
|
340
|
+
<!-- ============ ЛИЧНЫЙ КАБИНЕТ: ЛЕВОЕ МЕНЮ РАЗДЕЛОВ + ЧИСТЫЕ РЕЕСТРЫ ============ -->
|
|
341
341
|
<section class="stage" id="stage-addons">
|
|
342
|
-
<div class="sec-
|
|
343
|
-
<
|
|
344
|
-
<
|
|
345
|
-
<
|
|
346
|
-
</
|
|
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>
|
|
353
|
-
</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>
|
|
354
347
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
<
|
|
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>
|
|
366
359
|
</div>
|
|
367
|
-
</div>
|
|
368
|
-
<div id="cabinetServersRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
369
|
-
</div>
|
|
370
360
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
<button class="btn btn-primary btn-sm" data-stage="franchise">${icon('map')} Зарезервировать франшизу на карте РФ</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>
|
|
381
370
|
</div>
|
|
382
371
|
</div>
|
|
383
|
-
<div id="cabinetFranchisesRegistry" style="margin-top:16px;display:flex;flex-direction:column;gap:16px"></div>
|
|
384
372
|
</div>
|
|
385
373
|
</section>
|
|
386
374
|
|
|
@@ -510,6 +498,20 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
510
498
|
|
|
511
499
|
<div id="authIntentNotice" class="small" style="display:none;padding:10px 12px;border-radius:10px;background:rgba(0,163,224,.12);border:1px solid rgba(0,163,224,.35);margin-bottom:4px"></div>
|
|
512
500
|
|
|
501
|
+
<!-- Карточка активной сессии (показывается, когда пользователь уже вошёл) -->
|
|
502
|
+
<div id="authStepLoggedIn" style="display:none">
|
|
503
|
+
<div style="padding:14px 16px;border-radius:12px;border:1px solid var(--line-strong);background:var(--field);margin-bottom:14px">
|
|
504
|
+
<div class="small muted">Активная сессия в браузере</div>
|
|
505
|
+
<div style="font-size:16px;font-weight:700;margin-top:2px" id="authLoggedEmail">—</div>
|
|
506
|
+
<div style="margin-top:6px" id="authLoggedRoleBadge"></div>
|
|
507
|
+
</div>
|
|
508
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
509
|
+
<button type="button" class="btn btn-primary" style="width:100%" data-action="go-cabinet">Перейти в Личный кабинет</button>
|
|
510
|
+
<a class="btn btn-ghost" id="btnGoAdminModal" href="/admin" style="width:100%;display:none;text-decoration:none">Открыть Админ-панель</a>
|
|
511
|
+
<button type="button" class="btn btn-ghost" style="width:100%;color:var(--danger)" data-action="logout">Выйти из аккаунта</button>
|
|
512
|
+
</div>
|
|
513
|
+
</div>
|
|
514
|
+
|
|
513
515
|
<div id="authStepEmail">
|
|
514
516
|
<div class="field">
|
|
515
517
|
<label for="authEmail">Ваш Email *</label>
|
|
@@ -543,7 +545,6 @@ export function renderLandingPage({ publicKey = '', regions = null, assetVersion
|
|
|
543
545
|
|
|
544
546
|
<div class="status" id="authErrorMsg" style="color:var(--danger);min-height:18px;margin-top:6px"></div>
|
|
545
547
|
<div style="display:flex;gap:10px;margin-top:6px;justify-content:flex-end;flex-wrap:wrap">
|
|
546
|
-
<button class="btn btn-ghost btn-sm" id="btnLogoutModal" data-action="logout" style="display:none">Выйти из аккаунта</button>
|
|
547
548
|
<button class="btn btn-ghost btn-sm" data-action="auth-close">Закрыть</button>
|
|
548
549
|
</div>
|
|
549
550
|
</div>
|
package/src/server.js
CHANGED
|
@@ -316,7 +316,15 @@ export function createLicenseServerApp(options = {}) {
|
|
|
316
316
|
function extractBearerToken(req, body = null, url = null) {
|
|
317
317
|
const authHeader = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
318
318
|
const xAdmin = String(req.headers['x-admin-token'] || req.headers['x-admin-key'] || '').trim();
|
|
319
|
-
|
|
319
|
+
let cookieTok = '';
|
|
320
|
+
const cookieHdr = String(req.headers.cookie || '');
|
|
321
|
+
if (cookieHdr) {
|
|
322
|
+
const m = cookieHdr.match(/(?:^|;\s*)odant_work_session=([^;]+)/);
|
|
323
|
+
if (m?.[1]) {
|
|
324
|
+
try { cookieTok = decodeURIComponent(m[1].trim()); } catch { cookieTok = m[1].trim(); }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return authHeader || xAdmin || body?.adminToken || body?.sessionKey || url?.searchParams?.get('sessionKey') || cookieTok || '';
|
|
320
328
|
}
|
|
321
329
|
|
|
322
330
|
function ensureCustomerOferta(req, body = {}) {
|
|
@@ -466,6 +474,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
466
474
|
ofertaAccepted: true,
|
|
467
475
|
hubUrl: body.hubUrl || reqOrigin,
|
|
468
476
|
});
|
|
477
|
+
res.setHeader('Set-Cookie', `odant_work_session=${encodeURIComponent(user.sessionKey)}; Path=/; Max-Age=31536000; SameSite=Lax`);
|
|
469
478
|
return sendJson(res, 200, user);
|
|
470
479
|
}
|
|
471
480
|
|
|
@@ -476,9 +485,18 @@ export function createLicenseServerApp(options = {}) {
|
|
|
476
485
|
...body,
|
|
477
486
|
hubUrl: body.hubUrl || reqOrigin,
|
|
478
487
|
});
|
|
488
|
+
res.setHeader('Set-Cookie', `odant_work_session=${encodeURIComponent(user.sessionKey)}; Path=/; Max-Age=31536000; SameSite=Lax`);
|
|
479
489
|
return sendJson(res, 200, user);
|
|
480
490
|
}
|
|
481
491
|
|
|
492
|
+
if (req.method === 'POST' && pathname === '/api/auth/logout') {
|
|
493
|
+
const body = await readJsonBody(req);
|
|
494
|
+
const tok = extractBearerToken(req, body, url);
|
|
495
|
+
store.logoutAdmin(tok);
|
|
496
|
+
res.setHeader('Set-Cookie', 'odant_work_session=; Path=/; Max-Age=0; SameSite=Lax');
|
|
497
|
+
return sendJson(res, 200, { ok: true });
|
|
498
|
+
}
|
|
499
|
+
|
|
482
500
|
// 2.0.1 Генерация или перевыпуск токена владельца/сервера (с зашитым URL сервера поддержки и licenseId) в личном кабинете
|
|
483
501
|
if (req.method === 'POST' && pathname === '/api/cabinet/token') {
|
|
484
502
|
const body = await readJsonBody(req);
|
|
@@ -906,6 +924,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
906
924
|
if (req.method === 'POST' && pathname === '/api/admin/logout') {
|
|
907
925
|
const body = await readJsonBody(req);
|
|
908
926
|
const tok = extractBearerToken(req, body, url);
|
|
927
|
+
res.setHeader('Set-Cookie', 'odant_work_session=; Path=/; Max-Age=0; SameSite=Lax');
|
|
909
928
|
return sendJson(res, 200, store.logoutAdmin(tok));
|
|
910
929
|
}
|
|
911
930
|
|
package/src/store.js
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
createOwnerAccessToken,
|
|
30
30
|
parseOwnerAccessToken,
|
|
31
31
|
verifyOwnerAccessToken,
|
|
32
|
+
createSessionToken,
|
|
33
|
+
verifySessionToken,
|
|
32
34
|
} from './crypto.js';
|
|
33
35
|
|
|
34
36
|
const ONE_YEAR_MS = 365 * 24 * 3600_000;
|
|
@@ -82,6 +84,7 @@ export class LicenseStore {
|
|
|
82
84
|
this.adminLogin = String(adminLogin || process.env.WORK_ADMIN_LOGIN || 'admin').trim();
|
|
83
85
|
this.adminPassword = String(adminPassword || process.env.WORK_ADMIN_PASSWORD || process.env.WORK_ADMIN_KEY || 'odant-admin');
|
|
84
86
|
this.adminSessions = new Map();
|
|
87
|
+
this.revokedSessions = new Set();
|
|
85
88
|
|
|
86
89
|
this.state = {
|
|
87
90
|
licenses: {},
|
|
@@ -124,7 +127,10 @@ export class LicenseStore {
|
|
|
124
127
|
|
|
125
128
|
logoutAdmin(token = '') {
|
|
126
129
|
const clean = String(token || '').trim();
|
|
127
|
-
if (clean)
|
|
130
|
+
if (clean) {
|
|
131
|
+
this.adminSessions.delete(clean);
|
|
132
|
+
this.revokedSessions.add(clean);
|
|
133
|
+
}
|
|
128
134
|
return { ok: true };
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -134,15 +140,15 @@ export class LicenseStore {
|
|
|
134
140
|
|
|
135
141
|
getAdminIdentity(token = '') {
|
|
136
142
|
const clean = String(token || '').trim();
|
|
137
|
-
if (!clean)
|
|
143
|
+
if (!clean || this.revokedSessions.has(clean))
|
|
138
144
|
return null;
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
145
|
+
const userFromTok = this._findUser(clean);
|
|
146
|
+
if (userFromTok && userFromTok.isAdmin) {
|
|
141
147
|
return {
|
|
142
148
|
authenticated: true,
|
|
143
|
-
login:
|
|
144
|
-
email:
|
|
145
|
-
userId:
|
|
149
|
+
login: userFromTok.email,
|
|
150
|
+
email: userFromTok.email,
|
|
151
|
+
userId: userFromTok.userId,
|
|
146
152
|
role: 'BOSS',
|
|
147
153
|
};
|
|
148
154
|
}
|
|
@@ -1197,11 +1203,40 @@ export class LicenseStore {
|
|
|
1197
1203
|
|
|
1198
1204
|
_findUser(sessionKeyOrEmail = '') {
|
|
1199
1205
|
const key = String(sessionKeyOrEmail || '').trim();
|
|
1200
|
-
if (!key)
|
|
1206
|
+
if (!key || this.revokedSessions.has(key))
|
|
1201
1207
|
return null;
|
|
1202
|
-
|
|
1208
|
+
let found = Object.values(this.state.users || {}).find(
|
|
1203
1209
|
u => u.sessionKey === key || u.email === key.toLowerCase() || u.userId === key
|
|
1204
1210
|
) || null;
|
|
1211
|
+
if (found)
|
|
1212
|
+
return found;
|
|
1213
|
+
|
|
1214
|
+
// Если контейнер перезапускался, проверяем криптографическую подпись бессрочного токена сессии wks_1...
|
|
1215
|
+
const verifiedPayload = verifySessionToken(key, this.authorityKeys.publicKey);
|
|
1216
|
+
if (verifiedPayload?.email) {
|
|
1217
|
+
const cleanEmail = String(verifiedPayload.email).toLowerCase();
|
|
1218
|
+
let user = this.state.users[cleanEmail];
|
|
1219
|
+
if (!user) {
|
|
1220
|
+
this.registerOrLoginUser({
|
|
1221
|
+
email: cleanEmail,
|
|
1222
|
+
name: verifiedPayload.name || '',
|
|
1223
|
+
companyName: verifiedPayload.companyName || '',
|
|
1224
|
+
inn: verifiedPayload.inn || '',
|
|
1225
|
+
ofertaAccepted: true,
|
|
1226
|
+
});
|
|
1227
|
+
user = this.state.users[cleanEmail];
|
|
1228
|
+
}
|
|
1229
|
+
if (user) {
|
|
1230
|
+
if (verifiedPayload.isAdmin) {
|
|
1231
|
+
user.isAdmin = true;
|
|
1232
|
+
user.role = 'BOSS';
|
|
1233
|
+
}
|
|
1234
|
+
user.sessionKey = key;
|
|
1235
|
+
try { this._save(); } catch { /* ignore */ }
|
|
1236
|
+
return user;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1205
1240
|
}
|
|
1206
1241
|
|
|
1207
1242
|
/**
|
|
@@ -1558,6 +1593,17 @@ export class LicenseStore {
|
|
|
1558
1593
|
user.role = 'BOSS';
|
|
1559
1594
|
}
|
|
1560
1595
|
|
|
1596
|
+
// Выпускаем криптографически подписанный бессрочный токен сессии (wks_1...), устойчивый к перезапускам сервера
|
|
1597
|
+
user.sessionKey = createSessionToken({
|
|
1598
|
+
userId: user.userId,
|
|
1599
|
+
email: user.email,
|
|
1600
|
+
name: user.name,
|
|
1601
|
+
companyName: user.companyName,
|
|
1602
|
+
inn: user.inn,
|
|
1603
|
+
isAdmin: Boolean(user.isAdmin),
|
|
1604
|
+
issuedAt: now,
|
|
1605
|
+
}, this.authorityKeys);
|
|
1606
|
+
|
|
1561
1607
|
if (ofertaAccepted !== false && !user.ofertaAccepted) {
|
|
1562
1608
|
user.ofertaAccepted = true;
|
|
1563
1609
|
user.ofertaAcceptedAt = now;
|
|
@@ -1784,7 +1830,7 @@ export class LicenseStore {
|
|
|
1784
1830
|
const key = String(sessionKey || '').trim();
|
|
1785
1831
|
if (!key)
|
|
1786
1832
|
return null;
|
|
1787
|
-
const user =
|
|
1833
|
+
const user = this._findUser(key);
|
|
1788
1834
|
if (!user)
|
|
1789
1835
|
return null;
|
|
1790
1836
|
return this._formatUserResponse(user);
|