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/admin.js
CHANGED
|
@@ -1,15 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Админ-панель центра лицензирования и франшиз ODANT-WORK (/admin).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Разделы:
|
|
8
|
-
* 1. Реестр серверов (устройств) и лицензий клиентов
|
|
9
|
-
* 2. Реестр региональных франшиз и 12-месячных графиков погашения
|
|
10
|
-
* 3. Заказы ЮKassa и безналичные счета-договоры
|
|
11
|
-
* 4. Зарегистрированные клиенты, оферты (ПЭП) и роли BOSS
|
|
12
|
-
* 5. Тикеты техподдержки SLA (MAX · Telegram · VK)
|
|
4
|
+
* Единая шапка с основным сайтом (О платформе · Клиентам · Франшиза · Личный кабинет · Админ-панель)
|
|
5
|
+
* и двухколоночная навигация со счётчиками в скобках (по эталону Скрина 1).
|
|
13
6
|
*/
|
|
14
7
|
import {
|
|
15
8
|
PAK_PRODUCT,
|
|
@@ -44,7 +37,7 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
44
37
|
<head>
|
|
45
38
|
<meta charset="utf-8">
|
|
46
39
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
|
47
|
-
<title>ODANT-WORK ·
|
|
40
|
+
<title>ODANT-WORK · Админ-панель</title>
|
|
48
41
|
<meta name="robots" content="noindex, nofollow">
|
|
49
42
|
<meta name="theme-color" content="#04060b">
|
|
50
43
|
<link rel="icon" type="image/png" href="/favicon.ico">
|
|
@@ -97,210 +90,196 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
97
90
|
<div style="margin-top:8px;text-align:center">
|
|
98
91
|
<a href="/" class="small muted">← Вернуться на главную страницу work.odant.org</a>
|
|
99
92
|
</div>
|
|
100
|
-
<div class="small muted" style="margin-top:14px;padding-top:12px;border-top:1px solid var(--line)">
|
|
101
|
-
Ключ ЦС Ed25519: <code>${esc(publicKey.slice(0, 26))}…</code>
|
|
102
|
-
</div>
|
|
103
93
|
</div>
|
|
104
94
|
</div>
|
|
105
95
|
|
|
106
|
-
<!-- Основная
|
|
96
|
+
<!-- Основная оболочка с единой шапкой сайта и двухколоночным меню разделов -->
|
|
107
97
|
<div class="shell" id="adminAppShell" style="display:none">
|
|
108
98
|
<header class="topbar">
|
|
109
|
-
<a href="/" class="brand" style="text-decoration:none">
|
|
99
|
+
<a href="/" class="brand" style="text-decoration:none" aria-label="ODANT-WORK — на главную">
|
|
110
100
|
<img src="/assets/odant-logo.png" alt="ODANT">
|
|
111
101
|
<span>
|
|
112
|
-
<span class="brand-name">ODANT<b>·</b>WORK
|
|
113
|
-
<span class="brand-sub"
|
|
102
|
+
<span class="brand-name">ODANT<b>·</b>WORK</span><br>
|
|
103
|
+
<span class="brand-sub">учёт · документооборот · процессы · приватный ИИ</span>
|
|
114
104
|
</span>
|
|
115
105
|
</a>
|
|
116
106
|
|
|
117
|
-
<nav class="tabs" id="
|
|
118
|
-
<
|
|
119
|
-
<
|
|
120
|
-
<
|
|
121
|
-
<
|
|
122
|
-
<
|
|
107
|
+
<nav class="tabs" id="tabs" aria-label="Разделы">
|
|
108
|
+
<span class="tab-ind" id="tabInd"></span>
|
|
109
|
+
<a class="tab" href="/#product" style="text-decoration:none">О платформе</a>
|
|
110
|
+
<a class="tab" href="/#clients" style="text-decoration:none">Клиентам</a>
|
|
111
|
+
<a class="tab" href="/#franchise" style="text-decoration:none">Франшиза</a>
|
|
112
|
+
<a class="tab" href="/#addons" style="text-decoration:none">Личный кабинет</a>
|
|
113
|
+
<a class="tab active" href="/admin" id="tileAdmin" style="text-decoration:none">Админ-панель</a>
|
|
123
114
|
</nav>
|
|
124
115
|
|
|
125
116
|
<div class="top-actions">
|
|
126
|
-
<a href="/#addons" class="btn btn-ghost btn-sm" style="text-decoration:none">← Личный кабинет</a>
|
|
127
117
|
<button class="icon-btn" id="btnTheme" title="Светлая / тёмная тема" aria-label="Переключить тему">${icon('sun')}</button>
|
|
128
|
-
<button class="btn btn-
|
|
129
|
-
<button class="btn btn-ghost btn-sm" data-action="admin-logout" title="Завершить сессию">${icon('lock')}<span id="admUserLabel">Выйти</span></button>
|
|
118
|
+
<button class="btn btn-primary btn-sm" data-action="open-adm-profile" title="Аккаунт администратора">${icon('user')}<span id="admUserLabel">BOSS</span></button>
|
|
130
119
|
</div>
|
|
131
120
|
</header>
|
|
132
121
|
|
|
133
|
-
<section class="stage active" style="padding-top:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
122
|
+
<section class="stage active" style="padding-top:24px">
|
|
123
|
+
<div class="sec-layout">
|
|
124
|
+
<!-- Левое вертикальное меню разделов со счётчиками в скобках (как на Скрине 1) -->
|
|
125
|
+
<aside class="sec-nav" aria-label="Разделы администрирования">
|
|
126
|
+
<button type="button" class="sec-nav-item active" data-adm-tab="servers" id="navAdmServers">Серверы и лицензии (0)</button>
|
|
127
|
+
<button type="button" class="sec-nav-item" data-adm-tab="franchises" id="navAdmFranchises">Франшизы (0)</button>
|
|
128
|
+
<div class="sec-nav-sep"></div>
|
|
129
|
+
<button type="button" class="sec-nav-item" data-adm-tab="orders" id="navAdmOrders">Заказы и счета (0)</button>
|
|
130
|
+
<button type="button" class="sec-nav-item" data-adm-tab="users" id="navAdmUsers">Клиенты и роли (0)</button>
|
|
131
|
+
<button type="button" class="sec-nav-item" data-adm-tab="tickets" id="navAdmTickets">Тикеты поддержки (0)</button>
|
|
132
|
+
</aside>
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<div>
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
134
|
+
<!-- Правая область выбранного раздела -->
|
|
135
|
+
<div class="sec-main">
|
|
136
|
+
<!-- 1. СЕРВЕРЫ И ЛИЦЕНЗИИ -->
|
|
137
|
+
<div data-adm-sec="servers">
|
|
138
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
139
|
+
<h2 class="sec-title">Серверы (устройства) WORK и лицензии клиентов</h2>
|
|
140
|
+
<div class="sec-controls">
|
|
141
|
+
<input type="search" id="inpFilterServers" placeholder="Поиск по клиенту, Email, HWID…">
|
|
142
|
+
<select id="selFilterServers">
|
|
143
|
+
<option value="all">Все серверы</option>
|
|
144
|
+
<option value="base_active">Базовая активна</option>
|
|
145
|
+
<option value="base_inactive">Без базовой лицензии</option>
|
|
146
|
+
<option value="hwid_bound">HWID привязан</option>
|
|
147
|
+
</select>
|
|
148
|
+
<button type="button" class="btn btn-primary btn-sm" data-action="open-create-server-modal">+ Добавить сервер</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
<div class="sec-box">
|
|
152
|
+
<table class="sec-table">
|
|
153
|
+
<thead><tr>
|
|
154
|
+
<th>Сервер / ID</th>
|
|
155
|
+
<th>Владелец / Организация</th>
|
|
156
|
+
<th>Железо (HWID)</th>
|
|
157
|
+
<th>Базовая лицензия</th>
|
|
158
|
+
<th>Доп. места и SLA</th>
|
|
159
|
+
<th style="text-align:right">Файлы</th>
|
|
160
|
+
</tr></thead>
|
|
161
|
+
<tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
162
|
+
</table>
|
|
163
|
+
</div>
|
|
160
164
|
</div>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="table-wrap">
|
|
163
|
-
<table class="t">
|
|
164
|
-
<thead><tr>
|
|
165
|
-
<th>Сервер / ID</th>
|
|
166
|
-
<th>Владелец / Организация</th>
|
|
167
|
-
<th>Привязка железа (HWID)</th>
|
|
168
|
-
<th>Базовая лицензия (10 вечн.)</th>
|
|
169
|
-
<th>Доп. места и SLA</th>
|
|
170
|
-
<th>Управление</th>
|
|
171
|
-
</tr></thead>
|
|
172
|
-
<tbody id="admLicensesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
173
|
-
</table>
|
|
174
|
-
</div>
|
|
175
|
-
</div>
|
|
176
165
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
166
|
+
<!-- 2. ФРАНШИЗЫ И ГРАФИКИ ПОГАШЕНИЯ (12 МЕС.) -->
|
|
167
|
+
<div data-adm-sec="franchises" style="display:none">
|
|
168
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
169
|
+
<h2 class="sec-title">Региональные франшизы и графики погашения (12 мес.)</h2>
|
|
170
|
+
<div class="sec-controls">
|
|
171
|
+
<input type="search" id="inpFilterFranchises" placeholder="Поиск по региону, компании, ИНН…">
|
|
172
|
+
<select id="selFilterFranchises">
|
|
173
|
+
<option value="all">Все активные</option>
|
|
174
|
+
<option value="pending_deposit">Ожидают залога (15 мин)</option>
|
|
175
|
+
<option value="deposit_paid">Погашение по графику</option>
|
|
176
|
+
<option value="overdue">С просрочкой</option>
|
|
177
|
+
<option value="paid_in_full">Полностью погашены</option>
|
|
178
|
+
<option value="closed">Отменённые / Истёкшие</option>
|
|
179
|
+
</select>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="sec-box">
|
|
183
|
+
<table class="sec-table">
|
|
184
|
+
<thead><tr>
|
|
185
|
+
<th>Субъект РФ / ID</th>
|
|
186
|
+
<th>Франчайзи</th>
|
|
187
|
+
<th>Залог (100 тыс. ₽)</th>
|
|
188
|
+
<th>График (12 мес.)</th>
|
|
189
|
+
<th>Оплачено / Остаток</th>
|
|
190
|
+
<th style="text-align:right">Управление</th>
|
|
191
|
+
</tr></thead>
|
|
192
|
+
<tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
193
|
+
</table>
|
|
194
|
+
</div>
|
|
194
195
|
</div>
|
|
195
|
-
</div>
|
|
196
|
-
<div class="table-wrap">
|
|
197
|
-
<table class="t">
|
|
198
|
-
<thead><tr>
|
|
199
|
-
<th>Субъект РФ / ID</th>
|
|
200
|
-
<th>Франчайзи / Контакты</th>
|
|
201
|
-
<th>Начальный взнос (залог 100 тыс. ₽)</th>
|
|
202
|
-
<th>График погашения (12 мес.)</th>
|
|
203
|
-
<th>Финансовая сводка</th>
|
|
204
|
-
<th>Управление</th>
|
|
205
|
-
</tr></thead>
|
|
206
|
-
<tbody id="admFranchisesBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
207
|
-
</table>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
196
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
197
|
+
<!-- 3. ЗАКАЗЫ И СЧЕТА -->
|
|
198
|
+
<div data-adm-sec="orders" style="display:none">
|
|
199
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
200
|
+
<h2 class="sec-title">Заказы ЮKassa и безналичные счета-договоры</h2>
|
|
201
|
+
<div class="sec-controls">
|
|
202
|
+
<input type="search" id="inpFilterOrders" placeholder="Поиск по заказу, счёту, ИНН…">
|
|
203
|
+
<select id="selFilterOrders">
|
|
204
|
+
<option value="all">Все заказы</option>
|
|
205
|
+
<option value="pending">Ожидают оплаты</option>
|
|
206
|
+
<option value="paid">Оплачены</option>
|
|
207
|
+
<option value="canceled">Отменены</option>
|
|
208
|
+
</select>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
<div class="sec-box">
|
|
212
|
+
<table class="sec-table">
|
|
213
|
+
<thead><tr>
|
|
214
|
+
<th>Заказ / Счёт</th>
|
|
215
|
+
<th>Назначение</th>
|
|
216
|
+
<th>Плательщик</th>
|
|
217
|
+
<th>Сумма</th>
|
|
218
|
+
<th>Статус</th>
|
|
219
|
+
<th style="text-align:right">Действия</th>
|
|
220
|
+
</tr></thead>
|
|
221
|
+
<tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
222
|
+
</table>
|
|
223
|
+
</div>
|
|
226
224
|
</div>
|
|
227
|
-
</div>
|
|
228
|
-
<div class="table-wrap">
|
|
229
|
-
<table class="t">
|
|
230
|
-
<thead><tr>
|
|
231
|
-
<th>Заказ / Счёт / Дата</th>
|
|
232
|
-
<th>Назначение и объект</th>
|
|
233
|
-
<th>Плательщик / ИНН</th>
|
|
234
|
-
<th class="num">Сумма</th>
|
|
235
|
-
<th>Статус</th>
|
|
236
|
-
<th>Действия</th>
|
|
237
|
-
</tr></thead>
|
|
238
|
-
<tbody id="admOrdersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
239
|
-
</table>
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
225
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
<
|
|
226
|
+
<!-- 4. КЛИЕНТЫ И РОЛИ BOSS -->
|
|
227
|
+
<div data-adm-sec="users" style="display:none">
|
|
228
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
229
|
+
<h2 class="sec-title">Зарегистрированные клиенты и права администраторов (BOSS)</h2>
|
|
230
|
+
<div class="sec-controls">
|
|
231
|
+
<input type="search" id="inpFilterUsers" placeholder="Поиск по Email, организации, ИНН…">
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<div class="sec-box">
|
|
235
|
+
<table class="sec-table">
|
|
236
|
+
<thead><tr>
|
|
237
|
+
<th>Email / ID</th>
|
|
238
|
+
<th>Роль</th>
|
|
239
|
+
<th>Организация / ИНН</th>
|
|
240
|
+
<th>Реестры клиента</th>
|
|
241
|
+
<th>Оферта (ПЭП)</th>
|
|
242
|
+
<th style="text-align:right">Вход</th>
|
|
243
|
+
</tr></thead>
|
|
244
|
+
<tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
245
|
+
</table>
|
|
246
|
+
</div>
|
|
252
247
|
</div>
|
|
253
|
-
</div>
|
|
254
|
-
<div class="table-wrap">
|
|
255
|
-
<table class="t">
|
|
256
|
-
<thead><tr>
|
|
257
|
-
<th>Email / ID</th>
|
|
258
|
-
<th>Роль на сервере</th>
|
|
259
|
-
<th>Организация / ИНН</th>
|
|
260
|
-
<th>Серверов / Франшиз</th>
|
|
261
|
-
<th>Договор-оферта (ПЭП)</th>
|
|
262
|
-
<th>Регистрация / Вход</th>
|
|
263
|
-
</tr></thead>
|
|
264
|
-
<tbody id="admUsersBody"><tr><td colspan="6" class="muted">Загрузка…</td></tr></tbody>
|
|
265
|
-
</table>
|
|
266
|
-
</div>
|
|
267
|
-
</div>
|
|
268
248
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
249
|
+
<!-- 5. ТИКЕТЫ ПОДДЕРЖКИ SLA -->
|
|
250
|
+
<div data-adm-sec="tickets" style="display:none">
|
|
251
|
+
<div class="sec-bar" style="margin-bottom:12px">
|
|
252
|
+
<h2 class="sec-title">Тикеты техподдержки SLA (MAX · Telegram · VK)</h2>
|
|
253
|
+
<div class="sec-controls">
|
|
254
|
+
<select id="selFilterTickets">
|
|
255
|
+
<option value="all">Все обращения</option>
|
|
256
|
+
<option value="open">Открытые</option>
|
|
257
|
+
<option value="answered">Отвеченные</option>
|
|
258
|
+
<option value="closed">Закрытые</option>
|
|
259
|
+
</select>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="sec-box">
|
|
263
|
+
<table class="sec-table">
|
|
264
|
+
<thead><tr>
|
|
265
|
+
<th>Тикет / Канал</th>
|
|
266
|
+
<th>Клиент / Сервер</th>
|
|
267
|
+
<th>SLA</th>
|
|
268
|
+
<th>Переписка</th>
|
|
269
|
+
<th style="text-align:right">Ответ инженера</th>
|
|
270
|
+
</tr></thead>
|
|
271
|
+
<tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
|
|
272
|
+
</table>
|
|
273
|
+
</div>
|
|
283
274
|
</div>
|
|
284
275
|
</div>
|
|
285
|
-
<div class="table-wrap">
|
|
286
|
-
<table class="t">
|
|
287
|
-
<thead><tr>
|
|
288
|
-
<th>Тикет / Канал</th>
|
|
289
|
-
<th>Клиент / Сервер</th>
|
|
290
|
-
<th>Тариф SLA</th>
|
|
291
|
-
<th>Переписка</th>
|
|
292
|
-
<th>Ответ инженера</th>
|
|
293
|
-
</tr></thead>
|
|
294
|
-
<tbody id="admTicketsBody"><tr><td colspan="5" class="muted">Загрузка…</td></tr></tbody>
|
|
295
|
-
</table>
|
|
296
|
-
</div>
|
|
297
276
|
</div>
|
|
298
277
|
</section>
|
|
299
278
|
|
|
300
279
|
<footer>
|
|
301
280
|
<div class="footer-bottom">
|
|
302
|
-
<span>© ODANT ·
|
|
303
|
-
<span>Ключ центра сертификации Ed25519: <code>${esc(publicKey)}
|
|
281
|
+
<span>© ODANT · ${esc(P.domain)}</span>
|
|
282
|
+
<span>Ключ центра сертификации Ed25519: <code>${esc(publicKey.slice(0, 28))}…</code></span>
|
|
304
283
|
</div>
|
|
305
284
|
</footer>
|
|
306
285
|
</div>
|
|
@@ -386,14 +365,14 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
386
365
|
<button class="icon-btn" data-action="close-sched-modal" aria-label="Закрыть">${icon('close')}</button>
|
|
387
366
|
</div>
|
|
388
367
|
<div id="admSchedSummary" style="margin-bottom:12px"></div>
|
|
389
|
-
<div
|
|
390
|
-
<table class="
|
|
368
|
+
<div style="max-height:420px;overflow-y:auto">
|
|
369
|
+
<table class="sec-table">
|
|
391
370
|
<thead><tr>
|
|
392
371
|
<th>Месяц</th>
|
|
393
372
|
<th>Дата платежа</th>
|
|
394
|
-
<th
|
|
373
|
+
<th>Сумма</th>
|
|
395
374
|
<th>Статус</th>
|
|
396
|
-
<th style="text-align:right"
|
|
375
|
+
<th style="text-align:right">Действие</th>
|
|
397
376
|
</tr></thead>
|
|
398
377
|
<tbody id="admSchedBody"></tbody>
|
|
399
378
|
</table>
|
|
@@ -423,6 +402,30 @@ export function renderAdminPage({ publicKey = '', assetVersion = '' } = {}) {
|
|
|
423
402
|
</div>
|
|
424
403
|
</div>
|
|
425
404
|
|
|
405
|
+
<!-- Модальное окно: Аккаунт администратора (защита от случайного выхода) -->
|
|
406
|
+
<div class="modal" id="admProfileModal" role="dialog" aria-modal="true">
|
|
407
|
+
<div class="card" style="max-width:440px">
|
|
408
|
+
<div class="modal-head">
|
|
409
|
+
<img src="/assets/odant-logo.png" alt="">
|
|
410
|
+
<div>
|
|
411
|
+
<h3 class="h3" style="margin:0">Аккаунт администратора</h3>
|
|
412
|
+
<div class="small muted">Сессия активна и сохранена в браузере</div>
|
|
413
|
+
</div>
|
|
414
|
+
<button class="icon-btn" data-action="close-adm-profile" aria-label="Закрыть">${icon('close')}</button>
|
|
415
|
+
</div>
|
|
416
|
+
<div style="padding:14px 16px;border-radius:12px;border:1px solid var(--line-strong);background:var(--field);margin-bottom:14px">
|
|
417
|
+
<div class="small muted">Вы вошли как</div>
|
|
418
|
+
<div style="font-size:16px;font-weight:700;margin-top:2px" id="admProfileEmail">—</div>
|
|
419
|
+
<div style="margin-top:6px"><span class="tag ok">BOSS · Главный администратор</span></div>
|
|
420
|
+
</div>
|
|
421
|
+
<div style="display:flex;flex-direction:column;gap:8px">
|
|
422
|
+
<a class="btn btn-primary" href="/#addons" style="width:100%;text-decoration:none">Перейти в Личный кабинет</a>
|
|
423
|
+
<button type="button" class="btn btn-ghost" style="width:100%;color:var(--danger)" data-action="admin-logout">Выйти из аккаунта</button>
|
|
424
|
+
<button type="button" class="btn btn-ghost btn-sm" style="width:100%" data-action="close-adm-profile">Закрыть</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
</div>
|
|
428
|
+
|
|
426
429
|
<script>window.__ODANT_ADMIN__ = ${jsonForScript(adminData)};</script>
|
|
427
430
|
<script src="/assets/admin.js${v}" defer></script>
|
|
428
431
|
</body>
|
package/src/crypto.js
CHANGED
|
@@ -381,3 +381,75 @@ export function verifyOwnerAccessToken(tokenStr = '', trustedPublicKeyB64 = '')
|
|
|
381
381
|
return false;
|
|
382
382
|
}
|
|
383
383
|
}
|
|
384
|
+
|
|
385
|
+
export const SESSION_TOKEN_PREFIX = 'wks_1';
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Сгенерировать бессрочный подписанный токен сессии браузера (wks_1.<payload_b64url>.<sig_b64url>),
|
|
389
|
+
* который не сбрасывается при перезапуске/обновлении контейнера сервера, пока пользователь сам не нажал «Выйти».
|
|
390
|
+
*/
|
|
391
|
+
export function createSessionToken({
|
|
392
|
+
userId = '',
|
|
393
|
+
email = '',
|
|
394
|
+
name = '',
|
|
395
|
+
companyName = '',
|
|
396
|
+
inn = '',
|
|
397
|
+
isAdmin = false,
|
|
398
|
+
issuedAt = Date.now(),
|
|
399
|
+
} = {}, authorityKeys) {
|
|
400
|
+
const payloadObj = {
|
|
401
|
+
v: 1,
|
|
402
|
+
userId: String(userId || ''),
|
|
403
|
+
email: String(email || '').toLowerCase(),
|
|
404
|
+
...(name ? { name: String(name) } : {}),
|
|
405
|
+
...(companyName ? { companyName: String(companyName) } : {}),
|
|
406
|
+
...(inn ? { inn: String(inn) } : {}),
|
|
407
|
+
isAdmin: Boolean(isAdmin),
|
|
408
|
+
issuedAt: Number(issuedAt || Date.now()),
|
|
409
|
+
};
|
|
410
|
+
const payloadB64 = Buffer.from(JSON.stringify(payloadObj), 'utf8').toString('base64url');
|
|
411
|
+
let sigB64 = '';
|
|
412
|
+
if (authorityKeys?.privateKey) {
|
|
413
|
+
const priv = crypto.createPrivateKey({
|
|
414
|
+
key: Buffer.from(String(authorityKeys.privateKey), 'base64'),
|
|
415
|
+
format: 'der',
|
|
416
|
+
type: 'pkcs8',
|
|
417
|
+
});
|
|
418
|
+
sigB64 = crypto.sign(null, Buffer.from(`${SESSION_TOKEN_PREFIX}.${payloadB64}`, 'utf8'), priv).toString('base64url');
|
|
419
|
+
}
|
|
420
|
+
return `${SESSION_TOKEN_PREFIX}.${payloadB64}.${sigB64}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Проверить подпись Ed25519 сессионного токена (wks_1.<payload>.<sig>) и вернуть payload.
|
|
425
|
+
*/
|
|
426
|
+
export function verifySessionToken(tokenStr = '', trustedPublicKeyB64 = '') {
|
|
427
|
+
try {
|
|
428
|
+
const raw = String(tokenStr || '').trim();
|
|
429
|
+
const parts = raw.split('.');
|
|
430
|
+
if (parts.length !== 3 || parts[0] !== SESSION_TOKEN_PREFIX || !parts[2])
|
|
431
|
+
return null;
|
|
432
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
433
|
+
if (!payload?.email)
|
|
434
|
+
return null;
|
|
435
|
+
const trustedKeys = resolveTrustedAuthorityKeys(trustedPublicKeyB64);
|
|
436
|
+
const signedData = Buffer.from(`${SESSION_TOKEN_PREFIX}.${parts[1]}`, 'utf8');
|
|
437
|
+
const sigBuf = Buffer.from(parts[2], 'base64url');
|
|
438
|
+
for (const keyB64 of trustedKeys) {
|
|
439
|
+
try {
|
|
440
|
+
const pub = crypto.createPublicKey({
|
|
441
|
+
key: Buffer.from(String(keyB64), 'base64'),
|
|
442
|
+
format: 'der',
|
|
443
|
+
type: 'spki',
|
|
444
|
+
});
|
|
445
|
+
if (crypto.verify(null, signedData, pub, sigBuf))
|
|
446
|
+
return payload;
|
|
447
|
+
}
|
|
448
|
+
catch { /* try next */ }
|
|
449
|
+
}
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|