work-license-server 1.0.2 → 1.0.4
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/README.md +9 -2
- package/assets/admin.js +572 -0
- package/assets/landing.css +113 -1
- package/assets/landing.js +322 -290
- package/bin/server.mjs +5 -2
- package/package.json +1 -1
- package/src/admin.js +319 -0
- package/src/agent.js +152 -8
- package/src/bots.js +1 -1
- package/src/catalog.js +153 -17
- package/src/crypto.js +249 -17
- package/src/index.js +22 -1
- package/src/landing.js +113 -141
- package/src/server.js +229 -24
- package/src/store.js +696 -50
- package/src/yookassa.js +22 -8
package/src/server.js
CHANGED
|
@@ -19,8 +19,11 @@
|
|
|
19
19
|
import http from 'node:http';
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import crypto from 'node:crypto';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
22
23
|
import {
|
|
23
24
|
PAK_PRODUCT,
|
|
25
|
+
BASE_LICENSE_PRODUCT,
|
|
26
|
+
ADDITIONAL_PRODUCTS,
|
|
24
27
|
SLA_TIERS,
|
|
25
28
|
SEAT_VOLUME_DISCOUNTS,
|
|
26
29
|
calculateSeatVolumeDiscount,
|
|
@@ -32,12 +35,16 @@ import {
|
|
|
32
35
|
calculatePartnerDiscount,
|
|
33
36
|
calculatePartnerEconomics,
|
|
34
37
|
calculateRegionPartnerQuota,
|
|
38
|
+
PUBLIC_OFERTA,
|
|
35
39
|
} from './catalog.js';
|
|
36
40
|
import { LicenseStore } from './store.js';
|
|
37
41
|
import { YooKassaService } from './yookassa.js';
|
|
38
42
|
import { SupportBotsHub } from './bots.js';
|
|
39
43
|
import { LicenseServerAiAgent } from './agent.js';
|
|
40
44
|
import { renderLandingPage, renderSandboxPaymentPage } from './landing.js';
|
|
45
|
+
import { renderAdminPage } from './admin.js';
|
|
46
|
+
|
|
47
|
+
const DEFAULT_DATA_DIR = fileURLToPath(new URL('../data', import.meta.url));
|
|
41
48
|
|
|
42
49
|
async function readJsonBody(req, maxBytes = 1024 * 1024) {
|
|
43
50
|
const chunks = [];
|
|
@@ -72,16 +79,17 @@ const STATIC_ASSETS = Object.freeze({
|
|
|
72
79
|
'/assets/russia-regions.json': { file: 'russia-regions.json', type: 'application/json; charset=utf-8' },
|
|
73
80
|
'/assets/landing.css': { file: 'landing.css', type: 'text/css; charset=utf-8' },
|
|
74
81
|
'/assets/landing.js': { file: 'landing.js', type: 'text/javascript; charset=utf-8' },
|
|
82
|
+
'/assets/admin.js': { file: 'admin.js', type: 'text/javascript; charset=utf-8' },
|
|
75
83
|
});
|
|
76
84
|
const assetCache = new Map();
|
|
77
85
|
let assetVersionCache = '';
|
|
78
86
|
|
|
79
|
-
/** Версия статики для сброса кэша браузера: короткий хеш стилей и
|
|
87
|
+
/** Версия статики для сброса кэша браузера: короткий хеш стилей и скриптов витрины/админки. */
|
|
80
88
|
function assetVersion() {
|
|
81
89
|
if (assetVersionCache && !process.env.WORK_DEV)
|
|
82
90
|
return assetVersionCache;
|
|
83
91
|
const hash = crypto.createHash('sha1');
|
|
84
|
-
for (const file of ['landing.css', 'landing.js'])
|
|
92
|
+
for (const file of ['landing.css', 'landing.js', 'admin.js'])
|
|
85
93
|
hash.update(fs.readFileSync(new URL(file, ASSETS_DIR)));
|
|
86
94
|
assetVersionCache = hash.digest('hex').slice(0, 10);
|
|
87
95
|
return assetVersionCache;
|
|
@@ -109,7 +117,11 @@ function sendHtml(res, status, html) {
|
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
export function createLicenseServerApp(options = {}) {
|
|
112
|
-
const store = options.store || new LicenseStore({
|
|
120
|
+
const store = options.store || new LicenseStore({
|
|
121
|
+
dataDir: options.dataDir || DEFAULT_DATA_DIR,
|
|
122
|
+
adminLogin: options.adminLogin,
|
|
123
|
+
adminPassword: options.adminPassword,
|
|
124
|
+
});
|
|
113
125
|
let bots = null;
|
|
114
126
|
const yookassa = options.yookassa || new YooKassaService({
|
|
115
127
|
store,
|
|
@@ -137,13 +149,51 @@ export function createLicenseServerApp(options = {}) {
|
|
|
137
149
|
fetchImpl: options.fetchImpl,
|
|
138
150
|
});
|
|
139
151
|
|
|
152
|
+
function extractBearerToken(req, body = null, url = null) {
|
|
153
|
+
const authHeader = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
|
154
|
+
const xAdmin = String(req.headers['x-admin-token'] || req.headers['x-admin-key'] || '').trim();
|
|
155
|
+
return authHeader || xAdmin || body?.adminToken || body?.sessionKey || url?.searchParams?.get('sessionKey') || '';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function ensureCustomerOferta(req, body = {}) {
|
|
159
|
+
const tok = extractBearerToken(req, body);
|
|
160
|
+
let user = store.findUserBySessionKey(tok);
|
|
161
|
+
if (user && user.ofertaAccepted)
|
|
162
|
+
return user;
|
|
163
|
+
|
|
164
|
+
// Если в запросе переданы реквизиты и явное согласие с офертой (ofertaAccepted: true) — регистрируем и подписываем ПЭП
|
|
165
|
+
if (body.ofertaAccepted === true) {
|
|
166
|
+
const email = body.email || body.customer?.email || 'client@odant-work.ru';
|
|
167
|
+
const companyName = body.companyName || body.customer?.name || 'Организация ODANT-WORK';
|
|
168
|
+
const inn = body.inn || body.customer?.inn || '';
|
|
169
|
+
user = store.registerOrLoginUser({
|
|
170
|
+
email,
|
|
171
|
+
companyName,
|
|
172
|
+
inn,
|
|
173
|
+
name: body.signerName || body.contactPerson || companyName,
|
|
174
|
+
licenseId: body.licenseId || '',
|
|
175
|
+
ofertaAccepted: true,
|
|
176
|
+
});
|
|
177
|
+
return user;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Если это прямой M2M-запрос с привязанного ПАК WORK (переданы hwid и nodeFingerprint)
|
|
181
|
+
if (body.hwid && body.nodeFingerprint)
|
|
182
|
+
return { hwid: body.hwid, ofertaAccepted: true };
|
|
183
|
+
|
|
184
|
+
const err = new Error('Любые действия по заказам, оплате и резервированию франшизы доступны только после регистрации и подписания публичной оферты ODANT-WORK (ПЭП)');
|
|
185
|
+
err.statusCode = 403;
|
|
186
|
+
err.code = 'OFERTA_REQUIRED';
|
|
187
|
+
throw err;
|
|
188
|
+
}
|
|
189
|
+
|
|
140
190
|
async function handler(req, res) {
|
|
141
191
|
try {
|
|
142
192
|
if (req.method === 'OPTIONS') {
|
|
143
193
|
res.writeHead(204, {
|
|
144
194
|
'Access-Control-Allow-Origin': '*',
|
|
145
195
|
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
146
|
-
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
196
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Admin-Token, X-Admin-Key',
|
|
147
197
|
});
|
|
148
198
|
res.end();
|
|
149
199
|
return;
|
|
@@ -155,44 +205,77 @@ export function createLicenseServerApp(options = {}) {
|
|
|
155
205
|
const url = new URL(req.url || '/', reqOrigin);
|
|
156
206
|
const pathname = url.pathname;
|
|
157
207
|
|
|
158
|
-
// 1. Главная страница
|
|
159
|
-
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html'
|
|
208
|
+
// 1. Главная страница публичной витрины work.odant.org (без админки)
|
|
209
|
+
if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
|
|
160
210
|
return sendHtml(res, 200, renderLandingPage({
|
|
161
211
|
publicKey: store.authorityKeys.publicKey,
|
|
162
212
|
regions: store.listRegionsWithQuotas(),
|
|
163
213
|
assetVersion: assetVersion(),
|
|
164
|
-
initialStage: pathname === '/admin' ? 'admin' : '',
|
|
165
214
|
}));
|
|
166
215
|
}
|
|
167
216
|
|
|
168
|
-
// 1.
|
|
217
|
+
// 1.0 Отдельная страница защищённой админ-панели (/admin)
|
|
218
|
+
if (req.method === 'GET' && (pathname === '/admin' || pathname === '/admin/')) {
|
|
219
|
+
return sendHtml(res, 200, renderAdminPage({
|
|
220
|
+
publicKey: store.authorityKeys.publicKey,
|
|
221
|
+
assetVersion: assetVersion(),
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 1.1 Статика: логотип ODANT, стили, клиентские скрипты и контуры субъектов РФ
|
|
169
226
|
if (req.method === 'GET' && STATIC_ASSETS[pathname]) {
|
|
170
227
|
return sendStaticAsset(res, STATIC_ASSETS[pathname]);
|
|
171
228
|
}
|
|
172
229
|
|
|
173
|
-
// 2. Каталог ПАК, тарифов SLA
|
|
230
|
+
// 2. Каталог ПАК, отдельной базовой лицензии (4 млн ₽), тарифов SLA, прочих товаров, партнёрской франшизы и оферты
|
|
174
231
|
if (req.method === 'GET' && pathname === '/api/catalog') {
|
|
175
232
|
return sendJson(res, 200, {
|
|
176
233
|
product: PAK_PRODUCT,
|
|
234
|
+
baseLicenseProduct: BASE_LICENSE_PRODUCT,
|
|
235
|
+
additionalProducts: ADDITIONAL_PRODUCTS,
|
|
177
236
|
tiers: Object.values(SLA_TIERS),
|
|
178
237
|
volumeDiscounts: SEAT_VOLUME_DISCOUNTS,
|
|
179
238
|
franchiseProgram: FRANCHISE_PROGRAM,
|
|
180
239
|
regions: store.listRegionsWithQuotas(),
|
|
181
240
|
trainingTracks: PARTNER_TRAINING_TRACKS,
|
|
241
|
+
oferta: PUBLIC_OFERTA,
|
|
182
242
|
authorityPublicKey: store.authorityKeys.publicKey,
|
|
183
243
|
});
|
|
184
244
|
}
|
|
185
245
|
|
|
186
|
-
|
|
246
|
+
if (req.method === 'GET' && pathname === '/api/oferta') {
|
|
247
|
+
return sendJson(res, 200, PUBLIC_OFERTA);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2.0 Вход / регистрация пользователя и подписание публичной оферты ПЭП
|
|
187
251
|
if (req.method === 'POST' && (pathname === '/api/auth/login' || pathname === '/api/auth/register')) {
|
|
188
252
|
const body = await readJsonBody(req);
|
|
189
|
-
const user = store.registerOrLoginUser(
|
|
253
|
+
const user = store.registerOrLoginUser({
|
|
254
|
+
...body,
|
|
255
|
+
hubUrl: body.hubUrl || reqOrigin,
|
|
256
|
+
});
|
|
257
|
+
return sendJson(res, 200, user);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// 2.0.1 Генерация или перевыпуск токена владельца (с зашитым URL сервера поддержки) в личном кабинете
|
|
261
|
+
if (req.method === 'POST' && pathname === '/api/cabinet/token') {
|
|
262
|
+
const body = await readJsonBody(req);
|
|
263
|
+
const key = extractBearerToken(req, body, url) || body.email || body.accountId || '';
|
|
264
|
+
const result = store.generateUserHubToken(key, {
|
|
265
|
+
hubUrl: body.hubUrl || reqOrigin,
|
|
266
|
+
});
|
|
267
|
+
return sendJson(res, 200, result);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (req.method === 'POST' && pathname === '/api/auth/accept-oferta') {
|
|
271
|
+
const body = await readJsonBody(req);
|
|
272
|
+
const key = extractBearerToken(req, body, url) || body.email || '';
|
|
273
|
+
const user = store.acceptUserOferta(key, body);
|
|
190
274
|
return sendJson(res, 200, user);
|
|
191
275
|
}
|
|
192
276
|
|
|
193
277
|
if (req.method === 'GET' && pathname === '/api/auth/me') {
|
|
194
|
-
const
|
|
195
|
-
const key = url.searchParams.get('sessionKey') || authHeader;
|
|
278
|
+
const key = extractBearerToken(req, null, url);
|
|
196
279
|
const user = store.findUserBySessionKey(key);
|
|
197
280
|
if (!user)
|
|
198
281
|
return sendJson(res, 401, { authenticated: false });
|
|
@@ -238,10 +321,16 @@ export function createLicenseServerApp(options = {}) {
|
|
|
238
321
|
return sendJson(res, 200, result);
|
|
239
322
|
}
|
|
240
323
|
|
|
241
|
-
// 2.6 Резервирование региональной франшизы с оплатой невозвратного залога 100 000 ₽ через ЮKassa
|
|
324
|
+
// 2.6 Резервирование региональной франшизы с оплатой невозвратного залога 100 000 ₽ через ЮKassa (только после регистрации и подписания оферты)
|
|
242
325
|
if (req.method === 'POST' && pathname === '/api/partners/reserve') {
|
|
243
326
|
const body = await readJsonBody(req);
|
|
244
|
-
const
|
|
327
|
+
const user = ensureCustomerOferta(req, body);
|
|
328
|
+
const reserved = store.reserveFranchiseSlot({
|
|
329
|
+
...body,
|
|
330
|
+
companyName: body.companyName || user?.companyName || '',
|
|
331
|
+
inn: body.inn || user?.inn || '',
|
|
332
|
+
email: body.email || user?.email || '',
|
|
333
|
+
});
|
|
245
334
|
const payment = await yookassa.createPaymentForOrder(reserved.order, { originOverride: reqOrigin });
|
|
246
335
|
return sendJson(res, 200, {
|
|
247
336
|
...payment,
|
|
@@ -254,9 +343,10 @@ export function createLicenseServerApp(options = {}) {
|
|
|
254
343
|
// 2.7 Собственный ИИ-агент лицензионного сервера ODANT-WORK
|
|
255
344
|
if (req.method === 'POST' && pathname === '/api/agent/chat') {
|
|
256
345
|
const body = await readJsonBody(req);
|
|
257
|
-
const
|
|
258
|
-
const
|
|
259
|
-
const
|
|
346
|
+
const tok = extractBearerToken(req, body, url);
|
|
347
|
+
const isAdmin = store.verifyAdminToken(tok);
|
|
348
|
+
const user = store.findUserBySessionKey(body.sessionKey || tok);
|
|
349
|
+
const reply = await agent.chat({ message: body.message || body.text || '', history: body.history || [], user, isAdmin });
|
|
260
350
|
return sendJson(res, 200, reply);
|
|
261
351
|
}
|
|
262
352
|
|
|
@@ -266,16 +356,23 @@ export function createLicenseServerApp(options = {}) {
|
|
|
266
356
|
return sendJson(res, 200, calculateOrderQuote(body));
|
|
267
357
|
}
|
|
268
358
|
|
|
269
|
-
// 4. Выпуск базовой лицензии ПАК (
|
|
359
|
+
// 4. Выпуск базовой лицензии ПАК (10 вечных мест + 1 год поддержки Enterprise)
|
|
270
360
|
if (req.method === 'POST' && pathname === '/api/license/issue') {
|
|
271
361
|
const body = await readJsonBody(req);
|
|
272
362
|
const summary = store.issueOrUpdateLicense(body);
|
|
273
363
|
return sendJson(res, 200, summary);
|
|
274
364
|
}
|
|
275
365
|
|
|
276
|
-
// 5. Синхронизация лицензии с ПАК клиента
|
|
366
|
+
// 5. Синхронизация лицензии с ПАК клиента (или по токену владельца)
|
|
277
367
|
if (req.method === 'POST' && pathname === '/api/license/sync') {
|
|
278
368
|
const body = await readJsonBody(req);
|
|
369
|
+
if (body.token) {
|
|
370
|
+
const synced = store.syncByOwnerToken({
|
|
371
|
+
...body,
|
|
372
|
+
canonicalHubUrl: body.canonicalHubUrl || reqOrigin,
|
|
373
|
+
});
|
|
374
|
+
return sendJson(res, 200, synced);
|
|
375
|
+
}
|
|
279
376
|
const summary = store.issueOrUpdateLicense({
|
|
280
377
|
licenseId: body.licenseId,
|
|
281
378
|
hwid: body.hwid,
|
|
@@ -285,6 +382,18 @@ export function createLicenseServerApp(options = {}) {
|
|
|
285
382
|
return sendJson(res, 200, summary);
|
|
286
383
|
}
|
|
287
384
|
|
|
385
|
+
// 5.1 Синхронизация и скачивание файлов лицензий (.lic.json) по токену владельца
|
|
386
|
+
if (req.method === 'POST' && pathname === '/api/license/sync-by-token') {
|
|
387
|
+
const body = await readJsonBody(req);
|
|
388
|
+
const tok = body.token || extractBearerToken(req, body, url);
|
|
389
|
+
const result = store.syncByOwnerToken({
|
|
390
|
+
...body,
|
|
391
|
+
token: tok,
|
|
392
|
+
canonicalHubUrl: body.canonicalHubUrl || reqOrigin,
|
|
393
|
+
});
|
|
394
|
+
return sendJson(res, 200, result);
|
|
395
|
+
}
|
|
396
|
+
|
|
288
397
|
// 6. Получение лицензии по ID / HWID
|
|
289
398
|
if (req.method === 'GET' && pathname.startsWith('/api/license/')) {
|
|
290
399
|
const id = decodeURIComponent(pathname.slice('/api/license/'.length));
|
|
@@ -294,10 +403,37 @@ export function createLicenseServerApp(options = {}) {
|
|
|
294
403
|
return sendJson(res, 200, summary);
|
|
295
404
|
}
|
|
296
405
|
|
|
297
|
-
// 7. Оформление
|
|
406
|
+
// 7. Оформление заказа: через ЮKassa ИЛИ по договору/счёту для юрлиц (только после регистрации и подписания оферты)
|
|
298
407
|
if (req.method === 'POST' && pathname === '/api/orders/checkout') {
|
|
299
408
|
const body = await readJsonBody(req);
|
|
300
|
-
const
|
|
409
|
+
const user = ensureCustomerOferta(req, body);
|
|
410
|
+
const order = store.createOrder({
|
|
411
|
+
...body,
|
|
412
|
+
licenseId: body.licenseId || user?.licenseId || '',
|
|
413
|
+
ownerAccountId: user?.userId || body.ownerAccountId || '',
|
|
414
|
+
customer: {
|
|
415
|
+
name: body.customer?.name || user?.companyName || '',
|
|
416
|
+
inn: body.customer?.inn || user?.inn || '',
|
|
417
|
+
kpp: body.customer?.kpp || '',
|
|
418
|
+
legalAddress: body.customer?.legalAddress || '',
|
|
419
|
+
email: body.customer?.email || user?.email || '',
|
|
420
|
+
phone: body.customer?.phone || user?.phone || '',
|
|
421
|
+
},
|
|
422
|
+
});
|
|
423
|
+
if (order.paymentMethod === 'invoice') {
|
|
424
|
+
return sendJson(res, 200, {
|
|
425
|
+
orderId: order.orderId,
|
|
426
|
+
licenseId: order.licenseId,
|
|
427
|
+
paymentMethod: 'invoice',
|
|
428
|
+
status: order.status,
|
|
429
|
+
amountRub: order.amountRub,
|
|
430
|
+
invoiceNumber: order.invoiceNumber,
|
|
431
|
+
contractNumber: order.contractNumber,
|
|
432
|
+
invoiceUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
433
|
+
confirmationUrl: `${reqOrigin}${order.invoiceUrl}`,
|
|
434
|
+
order,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
301
437
|
const payment = await yookassa.createPaymentForOrder(order, { originOverride: reqOrigin });
|
|
302
438
|
return sendJson(res, 200, {
|
|
303
439
|
...payment,
|
|
@@ -305,6 +441,15 @@ export function createLicenseServerApp(options = {}) {
|
|
|
305
441
|
});
|
|
306
442
|
}
|
|
307
443
|
|
|
444
|
+
// 7.1 Печатная форма Счёта-договора на оплату для юридических лиц
|
|
445
|
+
if (req.method === 'GET' && pathname.startsWith('/api/orders/') && pathname.endsWith('/invoice')) {
|
|
446
|
+
const orderId = decodeURIComponent(pathname.slice('/api/orders/'.length, -'/invoice'.length));
|
|
447
|
+
const html = store.renderOrderInvoiceHtml(orderId);
|
|
448
|
+
if (!html)
|
|
449
|
+
return sendHtml(res, 404, '<h1>Счёт не найден</h1>');
|
|
450
|
+
return sendHtml(res, 200, html);
|
|
451
|
+
}
|
|
452
|
+
|
|
308
453
|
// 8. Песочница оплаты ЮKassa
|
|
309
454
|
if (req.method === 'GET' && pathname.startsWith('/pay/sandbox/')) {
|
|
310
455
|
const orderId = decodeURIComponent(pathname.slice('/pay/sandbox/'.length));
|
|
@@ -352,8 +497,35 @@ export function createLicenseServerApp(options = {}) {
|
|
|
352
497
|
return sendJson(res, 200, ticket);
|
|
353
498
|
}
|
|
354
499
|
|
|
355
|
-
// 12.
|
|
500
|
+
// 12. Авторизация и защищённое API панели администратора (/admin)
|
|
501
|
+
if (req.method === 'POST' && pathname === '/api/admin/login') {
|
|
502
|
+
const body = await readJsonBody(req);
|
|
503
|
+
try {
|
|
504
|
+
const session = store.loginAdmin(body);
|
|
505
|
+
return sendJson(res, 200, session);
|
|
506
|
+
}
|
|
507
|
+
catch (err) {
|
|
508
|
+
return sendJson(res, 401, { authenticated: false, error: err?.message || 'Ошибка авторизации администратора' });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (req.method === 'POST' && pathname === '/api/admin/logout') {
|
|
513
|
+
const body = await readJsonBody(req);
|
|
514
|
+
const tok = extractBearerToken(req, body, url);
|
|
515
|
+
return sendJson(res, 200, store.logoutAdmin(tok));
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (req.method === 'GET' && pathname === '/api/admin/me') {
|
|
519
|
+
const tok = extractBearerToken(req, null, url);
|
|
520
|
+
if (!store.verifyAdminToken(tok))
|
|
521
|
+
return sendJson(res, 401, { authenticated: false });
|
|
522
|
+
return sendJson(res, 200, { authenticated: true, login: store.adminLogin });
|
|
523
|
+
}
|
|
524
|
+
|
|
356
525
|
if (req.method === 'GET' && pathname === '/api/admin/overview') {
|
|
526
|
+
const tok = extractBearerToken(req, null, url);
|
|
527
|
+
if (!store.verifyAdminToken(tok))
|
|
528
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
357
529
|
return sendJson(res, 200, {
|
|
358
530
|
licenses: store.listLicenses(),
|
|
359
531
|
tickets: store.listTickets(),
|
|
@@ -361,13 +533,46 @@ export function createLicenseServerApp(options = {}) {
|
|
|
361
533
|
partners: store.listPartners(),
|
|
362
534
|
reservations: Object.values(store.state.reservations || {}),
|
|
363
535
|
regions: store.listRegionsWithQuotas(),
|
|
536
|
+
users: store.listUsers(),
|
|
364
537
|
});
|
|
365
538
|
}
|
|
366
539
|
|
|
540
|
+
if (req.method === 'POST' && pathname.startsWith('/api/admin/orders/') && pathname.endsWith('/cancel')) {
|
|
541
|
+
const body = await readJsonBody(req);
|
|
542
|
+
const tok = extractBearerToken(req, body, url);
|
|
543
|
+
if (!store.verifyAdminToken(tok))
|
|
544
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
545
|
+
const orderId = decodeURIComponent(pathname.slice('/api/admin/orders/'.length, -'/cancel'.length));
|
|
546
|
+
return sendJson(res, 200, store.cancelOrder(orderId));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (req.method === 'POST' && pathname.startsWith('/api/admin/orders/') && pathname.endsWith('/confirm-invoice')) {
|
|
550
|
+
const body = await readJsonBody(req);
|
|
551
|
+
const tok = extractBearerToken(req, body, url);
|
|
552
|
+
if (!store.verifyAdminToken(tok))
|
|
553
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
554
|
+
const orderId = decodeURIComponent(pathname.slice('/api/admin/orders/'.length, -'/confirm-invoice'.length));
|
|
555
|
+
const confirmed = store.confirmInvoicePayment(orderId, body);
|
|
556
|
+
if (confirmed?.license) {
|
|
557
|
+
await bots?.notifyLicenseOrderPaid(confirmed.order, confirmed.license);
|
|
558
|
+
}
|
|
559
|
+
return sendJson(res, 200, confirmed);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (req.method === 'POST' && pathname.startsWith('/api/admin/tickets/') && pathname.endsWith('/close')) {
|
|
563
|
+
const body = await readJsonBody(req);
|
|
564
|
+
const tok = extractBearerToken(req, body, url);
|
|
565
|
+
if (!store.verifyAdminToken(tok))
|
|
566
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора' });
|
|
567
|
+
const ticketId = decodeURIComponent(pathname.slice('/api/admin/tickets/'.length, -'/close'.length));
|
|
568
|
+
return sendJson(res, 200, store.closeTicket(ticketId));
|
|
569
|
+
}
|
|
570
|
+
|
|
367
571
|
return sendJson(res, 404, { error: 'Not Found', path: pathname });
|
|
368
572
|
}
|
|
369
573
|
catch (err) {
|
|
370
|
-
|
|
574
|
+
const status = Number(err?.statusCode) || 400;
|
|
575
|
+
return sendJson(res, status, { error: err?.message || String(err), ...(err?.code ? { code: err.code } : {}) });
|
|
371
576
|
}
|
|
372
577
|
}
|
|
373
578
|
|