work-license-server 1.0.22 → 1.0.23
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 +639 -180
- package/assets/landing.css +158 -0
- package/package.json +1 -1
- package/src/admin.js +219 -73
- package/src/server.js +159 -17
- package/src/store.js +246 -9
- package/src/yookassa.js +9 -4
package/src/server.js
CHANGED
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
calculateRegionPartnerQuota,
|
|
40
40
|
PUBLIC_OFERTA,
|
|
41
41
|
} from './catalog.js';
|
|
42
|
-
import { LicenseStore, resolvePersistentDataDir } from './store.js';
|
|
42
|
+
import { LicenseStore, resolvePersistentDataDir, isMaskedSecret } from './store.js';
|
|
43
43
|
import { YooKassaService } from './yookassa.js';
|
|
44
44
|
import { SupportBotsHub } from './bots.js';
|
|
45
45
|
import { LicenseServerAiAgent } from './agent.js';
|
|
@@ -119,13 +119,21 @@ function sendHtml(res, status, html) {
|
|
|
119
119
|
res.end(html);
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
function loadSmtpConfig() {
|
|
122
|
+
function loadSmtpConfig(store = null) {
|
|
123
|
+
if (store && typeof store.getSmtpConfig === 'function') {
|
|
124
|
+
const sc = store.getSmtpConfig();
|
|
125
|
+
if (sc?.host && sc?.user && sc?.pass)
|
|
126
|
+
return sc;
|
|
127
|
+
}
|
|
123
128
|
if (process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS) {
|
|
124
129
|
return {
|
|
125
130
|
host: process.env.SMTP_HOST,
|
|
126
131
|
port: Number(process.env.SMTP_PORT || 465),
|
|
132
|
+
secure: Number(process.env.SMTP_PORT || 465) === 465,
|
|
127
133
|
user: process.env.SMTP_USER,
|
|
128
134
|
pass: process.env.SMTP_PASS,
|
|
135
|
+
fromEmail: process.env.SMTP_FROM || process.env.SMTP_USER,
|
|
136
|
+
senderName: process.env.SMTP_SENDER_NAME || 'ODANT-WORK',
|
|
129
137
|
};
|
|
130
138
|
}
|
|
131
139
|
const candidates = [
|
|
@@ -141,8 +149,11 @@ function loadSmtpConfig() {
|
|
|
141
149
|
return {
|
|
142
150
|
host: j.host,
|
|
143
151
|
port: Number(j.port || 465),
|
|
152
|
+
secure: j.secure !== undefined ? Boolean(j.secure) : Number(j.port || 465) === 465,
|
|
144
153
|
user: j.auth.user,
|
|
145
154
|
pass: j.auth.pass,
|
|
155
|
+
fromEmail: j.fromEmail || j.auth.user,
|
|
156
|
+
senderName: j.senderName || 'ODANT-WORK',
|
|
146
157
|
};
|
|
147
158
|
}
|
|
148
159
|
}
|
|
@@ -152,8 +163,11 @@ function loadSmtpConfig() {
|
|
|
152
163
|
return {
|
|
153
164
|
host: 'smtp.yandex.ru',
|
|
154
165
|
port: 465,
|
|
166
|
+
secure: true,
|
|
155
167
|
user: 'proman1968.perepelkin@yandex.ru',
|
|
156
168
|
pass: 'zifeomdqijldyece',
|
|
169
|
+
fromEmail: 'proman1968.perepelkin@yandex.ru',
|
|
170
|
+
senderName: 'ODANT-WORK',
|
|
157
171
|
};
|
|
158
172
|
}
|
|
159
173
|
|
|
@@ -162,11 +176,17 @@ function loadSmtpConfig() {
|
|
|
162
176
|
* с полноценными RFC 5322 заголовками Date, Message-ID и text+html, чтобы Gmail доставлял письмо мгновенно).
|
|
163
177
|
*/
|
|
164
178
|
let _cachedTransporter = null;
|
|
165
|
-
async function sendOtpEmail({ to, code }) {
|
|
166
|
-
const cfg = loadSmtpConfig();
|
|
167
|
-
const
|
|
168
|
-
const
|
|
169
|
-
const
|
|
179
|
+
async function sendOtpEmail({ to, code, store = null, isTest = false }) {
|
|
180
|
+
const cfg = loadSmtpConfig(store);
|
|
181
|
+
const senderName = cfg.senderName || 'ODANT-WORK';
|
|
182
|
+
const fromAddr = cfg.fromEmail || cfg.user;
|
|
183
|
+
const subject = isTest ? `Тестовое письмо SMTP · ${senderName}` : `Код подтверждения ${senderName}`;
|
|
184
|
+
const textBody = isTest
|
|
185
|
+
? `Почтовый сервер SMTP (${cfg.host}:${cfg.port}) успешно настроен и отправляет письма от ${senderName} <${fromAddr}>.`
|
|
186
|
+
: `Ваш одноразовый код: ${code}\n\nДействует 10 минут.`;
|
|
187
|
+
const htmlBody = isTest
|
|
188
|
+
? `<h2>Проверка SMTP прошла успешно</h2><p>Почтовый сервер <b>${cfg.host}:${cfg.port}</b> подключён и доставляет письма от <b>${senderName} <${fromAddr}></b>.</p>`
|
|
189
|
+
: `
|
|
170
190
|
<h2>Код подтверждения</h2>
|
|
171
191
|
<p>Ваш код для входа/регистрации на work.odant.org:</p>
|
|
172
192
|
<h1 style="font-size: 48px; letter-spacing: 10px; font-weight: bold; text-align: center;">
|
|
@@ -174,7 +194,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
174
194
|
</h1>
|
|
175
195
|
<p>Код действителен <strong>10 минут</strong>. Ни с кем не делитесь.</p>
|
|
176
196
|
<p>Если это не вы — просто проигнорируйте письмо.</p>
|
|
177
|
-
<p>С уважением,<br
|
|
197
|
+
<p>С уважением,<br>${senderName}</p>
|
|
178
198
|
`;
|
|
179
199
|
|
|
180
200
|
try {
|
|
@@ -190,7 +210,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
190
210
|
_cachedTransporter = nodemailer.createTransport({
|
|
191
211
|
host: cfg.host,
|
|
192
212
|
port: cfg.port,
|
|
193
|
-
secure: cfg.port === 465,
|
|
213
|
+
secure: cfg.secure !== undefined ? Boolean(cfg.secure) : cfg.port === 465,
|
|
194
214
|
auth: {
|
|
195
215
|
user: cfg.user,
|
|
196
216
|
pass: cfg.pass,
|
|
@@ -198,7 +218,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
198
218
|
});
|
|
199
219
|
}
|
|
200
220
|
const info = await _cachedTransporter.sendMail({
|
|
201
|
-
from: `"
|
|
221
|
+
from: `"${senderName}" <${fromAddr}>`,
|
|
202
222
|
to,
|
|
203
223
|
subject,
|
|
204
224
|
text: textBody,
|
|
@@ -215,7 +235,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
215
235
|
}
|
|
216
236
|
|
|
217
237
|
// Резервный нативный SMTPS-клиент с полными RFC 5322 заголовками (Date + Message-ID + multipart/alternative text+html)
|
|
218
|
-
const domain = (
|
|
238
|
+
const domain = (fromAddr.split('@')[1] || 'yandex.ru').trim();
|
|
219
239
|
const msgId = `<${crypto.randomUUID()}@${domain}>`;
|
|
220
240
|
const dateHdr = new Date().toUTCString();
|
|
221
241
|
const boundary = `----=_Part_${crypto.randomBytes(8).toString('hex')}`;
|
|
@@ -225,7 +245,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
225
245
|
const mimeMsg = [
|
|
226
246
|
`Date: ${dateHdr}`,
|
|
227
247
|
`Message-ID: ${msgId}`,
|
|
228
|
-
`From: "
|
|
248
|
+
`From: "${senderName}" <${fromAddr}>`,
|
|
229
249
|
`To: <${to}>`,
|
|
230
250
|
`Subject: =?UTF-8?B?${subjectB64}?=`,
|
|
231
251
|
'MIME-Version: 1.0',
|
|
@@ -289,11 +309,13 @@ export function createLicenseServerApp(options = {}) {
|
|
|
289
309
|
autoRecover: options.autoRecover,
|
|
290
310
|
extraSearchRoots: options.extraSearchRoots,
|
|
291
311
|
});
|
|
312
|
+
const initSettings = store._ensureSettingsObject();
|
|
292
313
|
let bots = null;
|
|
293
314
|
const yookassa = options.yookassa || new YooKassaService({
|
|
294
315
|
store,
|
|
295
|
-
shopId: options.yookassaShopId,
|
|
296
|
-
secretKey: options.yookassaSecretKey,
|
|
316
|
+
shopId: options.yookassaShopId ?? initSettings.yookassa.shopId,
|
|
317
|
+
secretKey: options.yookassaSecretKey ?? initSettings.yookassa.secretKey,
|
|
318
|
+
sendReceipt: initSettings.yookassa.sendReceipt,
|
|
297
319
|
publicOrigin: options.publicOrigin || 'https://work.odant.org',
|
|
298
320
|
fetchImpl: options.fetchImpl,
|
|
299
321
|
onOrderPaid: async (order, license) => {
|
|
@@ -303,8 +325,8 @@ export function createLicenseServerApp(options = {}) {
|
|
|
303
325
|
bots = options.bots || new SupportBotsHub({
|
|
304
326
|
store,
|
|
305
327
|
yookassa,
|
|
306
|
-
maxBotSecret: options.maxBotSecret,
|
|
307
|
-
telegramBotSecret: options.telegramBotSecret,
|
|
328
|
+
maxBotSecret: options.maxBotSecret ?? initSettings.bots.maxToken,
|
|
329
|
+
telegramBotSecret: options.telegramBotSecret ?? initSettings.bots.telegramToken,
|
|
308
330
|
vkBotSecret: options.vkBotSecret,
|
|
309
331
|
vkConfirmationCode: options.vkConfirmationCode,
|
|
310
332
|
publicOrigin: options.publicOrigin || 'https://work.odant.org',
|
|
@@ -443,7 +465,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
443
465
|
});
|
|
444
466
|
console.log('[work.odant.org] Registration/Login code for', cleanEmail, ':', code);
|
|
445
467
|
if (!options.skipEmailSend) {
|
|
446
|
-
await sendOtpEmail({ to: cleanEmail, code });
|
|
468
|
+
await sendOtpEmail({ to: cleanEmail, code, store });
|
|
447
469
|
}
|
|
448
470
|
return sendJson(res, 200, {
|
|
449
471
|
ok: true,
|
|
@@ -996,9 +1018,129 @@ export function createLicenseServerApp(options = {}) {
|
|
|
996
1018
|
reservations: allFranchises,
|
|
997
1019
|
regions: store.listRegionsWithQuotas(),
|
|
998
1020
|
users: store.listUsers(),
|
|
1021
|
+
settings: store.getAdminSettingsSummary(),
|
|
999
1022
|
});
|
|
1000
1023
|
}
|
|
1001
1024
|
|
|
1025
|
+
if (req.method === 'POST' && pathname === '/api/admin/settings/smtp') {
|
|
1026
|
+
const body = await readJsonBody(req);
|
|
1027
|
+
const tok = extractBearerToken(req, body, url);
|
|
1028
|
+
if (!store.verifyAdminToken(tok))
|
|
1029
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
1030
|
+
_cachedTransporter = null;
|
|
1031
|
+
const settings = store.updateSmtpSettings(body);
|
|
1032
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (req.method === 'POST' && pathname === '/api/admin/settings/smtp/test') {
|
|
1036
|
+
const body = await readJsonBody(req);
|
|
1037
|
+
const tok = extractBearerToken(req, body, url);
|
|
1038
|
+
const admSession = store.verifyAdminToken(tok);
|
|
1039
|
+
if (!admSession)
|
|
1040
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
1041
|
+
const smtpCfg = store.getSmtpConfig();
|
|
1042
|
+
const targetEmail = String(body.to || admSession.email || smtpCfg.fromEmail || smtpCfg.user || '').trim();
|
|
1043
|
+
if (!targetEmail || !targetEmail.includes('@'))
|
|
1044
|
+
return sendJson(res, 400, { error: 'Укажите Email для отправки тестового письма' });
|
|
1045
|
+
if (!options.skipEmailSend) {
|
|
1046
|
+
_cachedTransporter = null;
|
|
1047
|
+
await sendOtpEmail({ to: targetEmail, code: '000000', store, isTest: true });
|
|
1048
|
+
}
|
|
1049
|
+
return sendJson(res, 200, {
|
|
1050
|
+
ok: true,
|
|
1051
|
+
to: targetEmail,
|
|
1052
|
+
message: `Тестовое письмо отправлено на ${targetEmail}`,
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (req.method === 'POST' && pathname === '/api/admin/settings/yookassa') {
|
|
1057
|
+
const body = await readJsonBody(req);
|
|
1058
|
+
const tok = extractBearerToken(req, body, url);
|
|
1059
|
+
if (!store.verifyAdminToken(tok))
|
|
1060
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
1061
|
+
const settings = store.updateYooKassaSettings(body);
|
|
1062
|
+
const rawYk = store._ensureSettingsObject().yookassa;
|
|
1063
|
+
yookassa.shopId = rawYk.shopId;
|
|
1064
|
+
yookassa.secretKey = rawYk.secretKey;
|
|
1065
|
+
yookassa.sendReceipt = Boolean(rawYk.sendReceipt);
|
|
1066
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (req.method === 'POST' && pathname === '/api/admin/settings/bots') {
|
|
1070
|
+
const body = await readJsonBody(req);
|
|
1071
|
+
const tok = extractBearerToken(req, body, url);
|
|
1072
|
+
if (!store.verifyAdminToken(tok))
|
|
1073
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
1074
|
+
|
|
1075
|
+
const rawBots = store._ensureSettingsObject().bots;
|
|
1076
|
+
const nextTgToken = (body.telegramToken !== undefined && !isMaskedSecret(body.telegramToken))
|
|
1077
|
+
? String(body.telegramToken || '').trim()
|
|
1078
|
+
: rawBots.telegramToken;
|
|
1079
|
+
const nextMaxToken = (body.maxToken !== undefined && !isMaskedSecret(body.maxToken))
|
|
1080
|
+
? String(body.maxToken || '').trim()
|
|
1081
|
+
: rawBots.maxToken;
|
|
1082
|
+
|
|
1083
|
+
let tgUsername = body.telegramUsername !== undefined
|
|
1084
|
+
? String(body.telegramUsername || '').trim().replace(/^@+/, '')
|
|
1085
|
+
: rawBots.telegramUsername;
|
|
1086
|
+
let maxUsername = body.maxUsername !== undefined
|
|
1087
|
+
? String(body.maxUsername || '').trim().replace(/^@+/, '')
|
|
1088
|
+
: rawBots.maxUsername;
|
|
1089
|
+
|
|
1090
|
+
const fetchFn = options.fetchImpl || globalThis.fetch;
|
|
1091
|
+
if (nextTgToken && typeof fetchFn === 'function') {
|
|
1092
|
+
try {
|
|
1093
|
+
const r = await fetchFn(`https://api.telegram.org/bot${encodeURIComponent(nextTgToken)}/getMe`, {
|
|
1094
|
+
signal: AbortSignal.timeout(5000),
|
|
1095
|
+
});
|
|
1096
|
+
if (r.ok) {
|
|
1097
|
+
const j = await r.json().catch(() => ({}));
|
|
1098
|
+
if (j?.result?.username) {
|
|
1099
|
+
tgUsername = String(j.result.username).replace(/^@+/, '');
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
catch { /* офлайн или тестовый токен */ }
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
if (nextMaxToken && typeof fetchFn === 'function') {
|
|
1107
|
+
try {
|
|
1108
|
+
const r = await fetchFn(`https://botapi.max.ru/me?access_token=${encodeURIComponent(nextMaxToken)}`, {
|
|
1109
|
+
headers: { 'Authorization': nextMaxToken },
|
|
1110
|
+
signal: AbortSignal.timeout(5000),
|
|
1111
|
+
});
|
|
1112
|
+
if (r.ok) {
|
|
1113
|
+
const j = await r.json().catch(() => ({}));
|
|
1114
|
+
const uname = j?.username || j?.user?.username || (j?.user_id ? `id${j.user_id}_bot` : '');
|
|
1115
|
+
if (uname) {
|
|
1116
|
+
maxUsername = String(uname).replace(/^@+/, '');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
catch { /* офлайн или тестовый токен */ }
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const settings = store.updateBotsSettings({
|
|
1124
|
+
telegramToken: body.telegramToken,
|
|
1125
|
+
telegramUsername: tgUsername,
|
|
1126
|
+
maxToken: body.maxToken,
|
|
1127
|
+
maxUsername,
|
|
1128
|
+
});
|
|
1129
|
+
const updatedRawBots = store._ensureSettingsObject().bots;
|
|
1130
|
+
bots.telegramBotSecret = updatedRawBots.telegramToken;
|
|
1131
|
+
bots.maxBotSecret = updatedRawBots.maxToken;
|
|
1132
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
if (req.method === 'POST' && pathname === '/api/admin/settings/legal') {
|
|
1136
|
+
const body = await readJsonBody(req);
|
|
1137
|
+
const tok = extractBearerToken(req, body, url);
|
|
1138
|
+
if (!store.verifyAdminToken(tok))
|
|
1139
|
+
return sendJson(res, 401, { error: 'Требуется авторизация администратора (/admin)' });
|
|
1140
|
+
const settings = store.updateLegalSettings(body);
|
|
1141
|
+
return sendJson(res, 200, { ok: true, settings });
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1002
1144
|
if (req.method === 'GET' && pathname === '/api/admin/backup/export') {
|
|
1003
1145
|
const tok = extractBearerToken(req, null, url);
|
|
1004
1146
|
if (!store.verifyAdminToken(tok))
|
package/src/store.js
CHANGED
|
@@ -254,12 +254,62 @@ function isAutoPlaceholderLicense(lic, allOrders = {}) {
|
|
|
254
254
|
return !hasOrders;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
export function maskSecret(value = '') {
|
|
258
|
+
const s = String(value || '').trim();
|
|
259
|
+
if (!s) return '';
|
|
260
|
+
if (s.length >= 10)
|
|
261
|
+
return `${s.slice(0, 6)}•••••••${s.slice(-4)}`;
|
|
262
|
+
return '•••••••';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function isMaskedSecret(value = '') {
|
|
266
|
+
return String(value || '').includes('•••');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createDefaultPortalSettings() {
|
|
270
|
+
return {
|
|
271
|
+
smtp: {
|
|
272
|
+
host: process.env.SMTP_HOST || 'smtp.yandex.ru',
|
|
273
|
+
port: Number(process.env.SMTP_PORT || 465),
|
|
274
|
+
secure: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) === 465 : true,
|
|
275
|
+
user: process.env.SMTP_USER || 'proman1968.perepelkin@yandex.ru',
|
|
276
|
+
pass: process.env.SMTP_PASS || 'zifeomdqijldyece',
|
|
277
|
+
fromEmail: process.env.SMTP_FROM || process.env.SMTP_USER || 'proman1968.perepelkin@yandex.ru',
|
|
278
|
+
senderName: process.env.SMTP_SENDER_NAME || 'ODANT-WORK',
|
|
279
|
+
},
|
|
280
|
+
yookassa: {
|
|
281
|
+
shopId: process.env.YOOKASSA_SHOP_ID || '',
|
|
282
|
+
secretKey: process.env.YOOKASSA_SECRET_KEY || '',
|
|
283
|
+
sendReceipt: true,
|
|
284
|
+
},
|
|
285
|
+
bots: {
|
|
286
|
+
telegramToken: process.env.TELEGRAM_BOT_TOKEN || '',
|
|
287
|
+
telegramUsername: process.env.TELEGRAM_BOT_USERNAME || '',
|
|
288
|
+
maxToken: process.env.MAX_BOT_TOKEN || '',
|
|
289
|
+
maxUsername: process.env.MAX_BOT_USERNAME || '',
|
|
290
|
+
},
|
|
291
|
+
legal: {
|
|
292
|
+
companyName: 'ООО «БИС» («Бизнес Интер Софт»)',
|
|
293
|
+
authorName: 'Перепелкин Роман Александрович',
|
|
294
|
+
legalStatus: 'Правообладатель и Оператор платформы · Проприетарное ПО (Все права защищены)',
|
|
295
|
+
inn: '6234049193',
|
|
296
|
+
ogrn: '',
|
|
297
|
+
supportEmail: 'support@odant.org',
|
|
298
|
+
phone: '+7 (4912) 77-70-21',
|
|
299
|
+
address: VENDOR_CONTACTS.address || '390000, г. Рязань, ул. Ленина, 47',
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
257
304
|
/**
|
|
258
305
|
* Безопасное объединение (merge без потери записей) двух состояний store.json.
|
|
259
306
|
* Сохраняет всех пользователей, их реквизиты, токены привязки (owk1...), серверы/лицензии,
|
|
260
307
|
* заказы, франшизы, партнёров, тикеты и привязки ботов.
|
|
261
308
|
*/
|
|
262
309
|
export function mergeStoreStates(baseState = {}, incomingState = {}) {
|
|
310
|
+
const defSettings = createDefaultPortalSettings();
|
|
311
|
+
const baseSet = baseState.settings || {};
|
|
312
|
+
const incSet = incomingState.settings || {};
|
|
263
313
|
const out = {
|
|
264
314
|
licenses: { ...(baseState.licenses || {}) },
|
|
265
315
|
orders: { ...(baseState.orders || {}) },
|
|
@@ -269,6 +319,12 @@ export function mergeStoreStates(baseState = {}, incomingState = {}) {
|
|
|
269
319
|
reservations: { ...(baseState.reservations || {}) },
|
|
270
320
|
certificates: { ...(baseState.certificates || {}) },
|
|
271
321
|
users: { ...(baseState.users || {}) },
|
|
322
|
+
settings: {
|
|
323
|
+
smtp: { ...defSettings.smtp, ...(baseSet.smtp || {}), ...(incSet.smtp || {}) },
|
|
324
|
+
yookassa: { ...defSettings.yookassa, ...(baseSet.yookassa || {}), ...(incSet.yookassa || {}) },
|
|
325
|
+
bots: { ...defSettings.bots, ...(baseSet.bots || {}), ...(incSet.bots || {}) },
|
|
326
|
+
legal: { ...defSettings.legal, ...(baseSet.legal || {}), ...(incSet.legal || {}) },
|
|
327
|
+
},
|
|
272
328
|
};
|
|
273
329
|
|
|
274
330
|
// 1. Заказы (предпочитаем оплаченные или более свежие)
|
|
@@ -531,6 +587,7 @@ export class LicenseStore {
|
|
|
531
587
|
this.stateFile = path.join(this.dataDir, 'store.json');
|
|
532
588
|
this.prevStateFile = path.join(this.dataDir, 'store.prev.json');
|
|
533
589
|
this.keyFile = path.join(this.dataDir, 'authority-key.json');
|
|
590
|
+
this.mailFile = path.join(this.dataDir, 'mail.json');
|
|
534
591
|
|
|
535
592
|
// Если в новой постоянной папке ещё нет authority-key.json, восстанавливаем его из прежних папок / кэша npx
|
|
536
593
|
if (this.autoRecover && !fs.existsSync(this.keyFile)) {
|
|
@@ -557,8 +614,10 @@ export class LicenseStore {
|
|
|
557
614
|
reservations: {},
|
|
558
615
|
certificates: {},
|
|
559
616
|
users: {},
|
|
617
|
+
settings: createDefaultPortalSettings(),
|
|
560
618
|
};
|
|
561
619
|
this._load();
|
|
620
|
+
this._loadOrSyncMailFile();
|
|
562
621
|
}
|
|
563
622
|
|
|
564
623
|
/**
|
|
@@ -660,9 +719,13 @@ export class LicenseStore {
|
|
|
660
719
|
*/
|
|
661
720
|
setUserAdminRole(emailOrUserId = '', isAdmin = true) {
|
|
662
721
|
const key = String(emailOrUserId || '').trim();
|
|
663
|
-
|
|
722
|
+
let user = Object.values(this.state.users || {}).find(
|
|
664
723
|
u => u.userId === key || String(u.email || '').toLowerCase() === key.toLowerCase()
|
|
665
724
|
);
|
|
725
|
+
if (!user && key.includes('@')) {
|
|
726
|
+
this.registerOrLoginUser({ email: key.toLowerCase() });
|
|
727
|
+
user = this.state.users[key.toLowerCase()] || null;
|
|
728
|
+
}
|
|
666
729
|
if (!user)
|
|
667
730
|
throw new Error('Пользователь не найден');
|
|
668
731
|
const nextAdmin = Boolean(isAdmin);
|
|
@@ -678,6 +741,172 @@ export class LicenseStore {
|
|
|
678
741
|
return this._formatUserResponse(user);
|
|
679
742
|
}
|
|
680
743
|
|
|
744
|
+
_ensureSettingsObject() {
|
|
745
|
+
const def = createDefaultPortalSettings();
|
|
746
|
+
if (!this.state.settings || typeof this.state.settings !== 'object') {
|
|
747
|
+
this.state.settings = def;
|
|
748
|
+
}
|
|
749
|
+
this.state.settings.smtp = { ...def.smtp, ...(this.state.settings.smtp || {}) };
|
|
750
|
+
this.state.settings.yookassa = { ...def.yookassa, ...(this.state.settings.yookassa || {}) };
|
|
751
|
+
this.state.settings.bots = { ...def.bots, ...(this.state.settings.bots || {}) };
|
|
752
|
+
this.state.settings.legal = { ...def.legal, ...(this.state.settings.legal || {}) };
|
|
753
|
+
return this.state.settings;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
_loadOrSyncMailFile() {
|
|
757
|
+
const s = this._ensureSettingsObject();
|
|
758
|
+
try {
|
|
759
|
+
if (fs.existsSync(this.mailFile)) {
|
|
760
|
+
const j = JSON.parse(fs.readFileSync(this.mailFile, 'utf8'));
|
|
761
|
+
if (j?.host) s.smtp.host = String(j.host).trim();
|
|
762
|
+
if (j?.port) s.smtp.port = Number(j.port);
|
|
763
|
+
if (typeof j?.secure === 'boolean') s.smtp.secure = j.secure;
|
|
764
|
+
if (j?.auth?.user || j?.user) s.smtp.user = String(j.auth?.user || j.user).trim();
|
|
765
|
+
if (j?.auth?.pass || j?.pass) s.smtp.pass = String(j.auth?.pass || j.pass);
|
|
766
|
+
if (j?.fromEmail) s.smtp.fromEmail = String(j.fromEmail).trim();
|
|
767
|
+
if (j?.senderName) s.smtp.senderName = String(j.senderName).trim();
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
this._writeMailFile();
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
catch { /* ignore */ }
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
_writeMailFile() {
|
|
777
|
+
try {
|
|
778
|
+
const smtp = this._ensureSettingsObject().smtp;
|
|
779
|
+
const payload = {
|
|
780
|
+
host: smtp.host,
|
|
781
|
+
port: Number(smtp.port || 465),
|
|
782
|
+
secure: Boolean(smtp.secure),
|
|
783
|
+
fromEmail: smtp.fromEmail || smtp.user,
|
|
784
|
+
senderName: smtp.senderName || 'ODANT-WORK',
|
|
785
|
+
auth: {
|
|
786
|
+
user: smtp.user,
|
|
787
|
+
pass: smtp.pass,
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
fs.writeFileSync(this.mailFile, JSON.stringify(payload, null, 2), 'utf8');
|
|
791
|
+
}
|
|
792
|
+
catch { /* ignore */ }
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
getSmtpConfig() {
|
|
796
|
+
const smtp = this._ensureSettingsObject().smtp;
|
|
797
|
+
return {
|
|
798
|
+
host: String(smtp.host || 'smtp.yandex.ru').trim(),
|
|
799
|
+
port: Number(smtp.port || 465),
|
|
800
|
+
secure: smtp.secure !== undefined ? Boolean(smtp.secure) : Number(smtp.port || 465) === 465,
|
|
801
|
+
user: String(smtp.user || '').trim(),
|
|
802
|
+
pass: String(smtp.pass || ''),
|
|
803
|
+
fromEmail: String(smtp.fromEmail || smtp.user || '').trim(),
|
|
804
|
+
senderName: String(smtp.senderName || 'ODANT-WORK').trim(),
|
|
805
|
+
mailFile: this.mailFile,
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
getLegalSettings() {
|
|
810
|
+
return { ...this._ensureSettingsObject().legal };
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
getAdminSettingsSummary() {
|
|
814
|
+
const s = this._ensureSettingsObject();
|
|
815
|
+
const smtpConnected = Boolean(s.smtp.host && s.smtp.user && s.smtp.pass);
|
|
816
|
+
const yookassaLive = Boolean(s.yookassa.shopId && s.yookassa.secretKey);
|
|
817
|
+
return {
|
|
818
|
+
smtp: {
|
|
819
|
+
host: s.smtp.host || '',
|
|
820
|
+
port: Number(s.smtp.port || 465),
|
|
821
|
+
secure: Boolean(s.smtp.secure),
|
|
822
|
+
user: s.smtp.user || '',
|
|
823
|
+
pass: maskSecret(s.smtp.pass),
|
|
824
|
+
fromEmail: s.smtp.fromEmail || s.smtp.user || '',
|
|
825
|
+
senderName: s.smtp.senderName || 'ODANT-WORK',
|
|
826
|
+
connected: smtpConnected,
|
|
827
|
+
mailFile: this.mailFile,
|
|
828
|
+
},
|
|
829
|
+
yookassa: {
|
|
830
|
+
shopId: s.yookassa.shopId || '',
|
|
831
|
+
secretKey: maskSecret(s.yookassa.secretKey),
|
|
832
|
+
sendReceipt: Boolean(s.yookassa.sendReceipt),
|
|
833
|
+
live: yookassaLive,
|
|
834
|
+
},
|
|
835
|
+
bots: {
|
|
836
|
+
telegramToken: maskSecret(s.bots.telegramToken),
|
|
837
|
+
telegramUsername: String(s.bots.telegramUsername || '').replace(/^@+/, ''),
|
|
838
|
+
maxToken: maskSecret(s.bots.maxToken),
|
|
839
|
+
maxUsername: String(s.bots.maxUsername || '').replace(/^@+/, ''),
|
|
840
|
+
},
|
|
841
|
+
legal: {
|
|
842
|
+
companyName: s.legal.companyName || '',
|
|
843
|
+
authorName: s.legal.authorName || '',
|
|
844
|
+
legalStatus: s.legal.legalStatus || '',
|
|
845
|
+
inn: s.legal.inn || '',
|
|
846
|
+
ogrn: s.legal.ogrn || '',
|
|
847
|
+
supportEmail: s.legal.supportEmail || '',
|
|
848
|
+
phone: s.legal.phone || '',
|
|
849
|
+
address: s.legal.address || '',
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
updateSmtpSettings(patch = {}) {
|
|
855
|
+
const s = this._ensureSettingsObject().smtp;
|
|
856
|
+
if (patch.host !== undefined) s.host = String(patch.host || '').trim();
|
|
857
|
+
if (patch.port !== undefined) s.port = Number(patch.port || 465);
|
|
858
|
+
if (patch.secure !== undefined) s.secure = Boolean(patch.secure);
|
|
859
|
+
if (patch.user !== undefined) s.user = String(patch.user || '').trim();
|
|
860
|
+
if (patch.pass !== undefined && !isMaskedSecret(patch.pass)) {
|
|
861
|
+
s.pass = String(patch.pass || '');
|
|
862
|
+
}
|
|
863
|
+
if (patch.fromEmail !== undefined) s.fromEmail = String(patch.fromEmail || '').trim();
|
|
864
|
+
if (patch.senderName !== undefined) s.senderName = String(patch.senderName || '').trim();
|
|
865
|
+
this._writeMailFile();
|
|
866
|
+
this._save();
|
|
867
|
+
return this.getAdminSettingsSummary();
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
updateYooKassaSettings(patch = {}) {
|
|
871
|
+
const y = this._ensureSettingsObject().yookassa;
|
|
872
|
+
if (patch.shopId !== undefined) y.shopId = String(patch.shopId || '').trim();
|
|
873
|
+
if (patch.secretKey !== undefined && !isMaskedSecret(patch.secretKey)) {
|
|
874
|
+
y.secretKey = String(patch.secretKey || '').trim();
|
|
875
|
+
}
|
|
876
|
+
if (patch.sendReceipt !== undefined) y.sendReceipt = Boolean(patch.sendReceipt);
|
|
877
|
+
this._save();
|
|
878
|
+
return this.getAdminSettingsSummary();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
updateBotsSettings(patch = {}) {
|
|
882
|
+
const b = this._ensureSettingsObject().bots;
|
|
883
|
+
if (patch.telegramToken !== undefined && !isMaskedSecret(patch.telegramToken)) {
|
|
884
|
+
b.telegramToken = String(patch.telegramToken || '').trim();
|
|
885
|
+
}
|
|
886
|
+
if (patch.telegramUsername !== undefined) {
|
|
887
|
+
b.telegramUsername = String(patch.telegramUsername || '').trim().replace(/^@+/, '');
|
|
888
|
+
}
|
|
889
|
+
if (patch.maxToken !== undefined && !isMaskedSecret(patch.maxToken)) {
|
|
890
|
+
b.maxToken = String(patch.maxToken || '').trim();
|
|
891
|
+
}
|
|
892
|
+
if (patch.maxUsername !== undefined) {
|
|
893
|
+
b.maxUsername = String(patch.maxUsername || '').trim().replace(/^@+/, '');
|
|
894
|
+
}
|
|
895
|
+
this._save();
|
|
896
|
+
return this.getAdminSettingsSummary();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
updateLegalSettings(patch = {}) {
|
|
900
|
+
const l = this._ensureSettingsObject().legal;
|
|
901
|
+
for (const k of ['companyName', 'authorName', 'legalStatus', 'inn', 'ogrn', 'supportEmail', 'phone', 'address']) {
|
|
902
|
+
if (patch[k] !== undefined) {
|
|
903
|
+
l[k] = String(patch[k] || '').trim();
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
this._save();
|
|
907
|
+
return this.getAdminSettingsSummary();
|
|
908
|
+
}
|
|
909
|
+
|
|
681
910
|
_recoverAuthorityKeyFromCandidates() {
|
|
682
911
|
const allRoots = [...this.extraSearchRoots];
|
|
683
912
|
const dirs = findCandidateRecoveryDirs({
|
|
@@ -730,6 +959,7 @@ export class LicenseStore {
|
|
|
730
959
|
reservations: stateObj.reservations || {},
|
|
731
960
|
certificates: stateObj.certificates || {},
|
|
732
961
|
users: stateObj.users || {},
|
|
962
|
+
settings: stateObj.settings || {},
|
|
733
963
|
};
|
|
734
964
|
}
|
|
735
965
|
catch {
|
|
@@ -1083,8 +1313,11 @@ export class LicenseStore {
|
|
|
1083
1313
|
? Boolean(baseActivated)
|
|
1084
1314
|
: (existing?.baseActivated !== undefined ? Boolean(existing.baseActivated) : true);
|
|
1085
1315
|
|
|
1086
|
-
const resolvedOwnerAccountId = String(ownerAccountId || existing?.ownerAccountId || '');
|
|
1087
1316
|
const resolvedOwnerEmail = String(ownerEmail || customer.email || existing?.ownerEmail || existing?.customer?.email || '').toLowerCase();
|
|
1317
|
+
const matchedUser = resolvedOwnerEmail
|
|
1318
|
+
? (this.state.users?.[resolvedOwnerEmail] || Object.values(this.state.users || {}).find(u => String(u.email || '').toLowerCase() === resolvedOwnerEmail))
|
|
1319
|
+
: null;
|
|
1320
|
+
const resolvedOwnerAccountId = String(ownerAccountId || existing?.ownerAccountId || matchedUser?.userId || '');
|
|
1088
1321
|
const resolvedServerName = String(serverName || existing?.serverName || 'Сервер WORK #1').trim();
|
|
1089
1322
|
const resolvedHubUrl = String(hubUrl || existing?.hubUrl || 'https://work.odant.org').replace(/\/+$/, '');
|
|
1090
1323
|
|
|
@@ -1122,10 +1355,10 @@ export class LicenseStore {
|
|
|
1122
1355
|
baseActivated: isBaseActivated,
|
|
1123
1356
|
baseSku: String(baseSku || existing?.baseSku || PAK_PRODUCT.sku),
|
|
1124
1357
|
customer: {
|
|
1125
|
-
name: customer.name || existing?.customer?.name || '
|
|
1126
|
-
inn: customer.inn || existing?.customer?.inn || '',
|
|
1127
|
-
email: customer.email || existing?.customer?.email || '',
|
|
1128
|
-
phone: customer.phone || existing?.customer?.phone || '',
|
|
1358
|
+
name: customer.name || existing?.customer?.name || matchedUser?.companyName || matchedUser?.name || resolvedOwnerEmail || 'Клиент WORK',
|
|
1359
|
+
inn: customer.inn || existing?.customer?.inn || matchedUser?.inn || '',
|
|
1360
|
+
email: customer.email || existing?.customer?.email || resolvedOwnerEmail || '',
|
|
1361
|
+
phone: customer.phone || existing?.customer?.phone || matchedUser?.phone || '',
|
|
1129
1362
|
},
|
|
1130
1363
|
hardware: {
|
|
1131
1364
|
hwid: hwid || existing?.hardware?.hwid || ('PAK-' + crypto.randomBytes(6).toString('hex').toUpperCase()),
|
|
@@ -1729,6 +1962,10 @@ export class LicenseStore {
|
|
|
1729
1962
|
return null;
|
|
1730
1963
|
const dateStr = new Date(ord.createdAt || Date.now()).toLocaleDateString('ru-RU');
|
|
1731
1964
|
const c = ord.customer || {};
|
|
1965
|
+
const legal = this.getLegalSettings();
|
|
1966
|
+
const vendorTitle = legal.companyName || `ООО «${VENDOR_CONTACTS.company}»`;
|
|
1967
|
+
const vendorAddr = legal.address || VENDOR_CONTACTS.address;
|
|
1968
|
+
const vendorPhone = legal.phone || VENDOR_CONTACTS.phones.join(', ');
|
|
1732
1969
|
const custName = (c.name && !String(c.name).includes('@')) ? c.name : 'Организация (не указана)';
|
|
1733
1970
|
const rowsHtml = (ord.items || []).map((it, idx) => `
|
|
1734
1971
|
<tr>
|
|
@@ -1777,7 +2014,7 @@ export class LicenseStore {
|
|
|
1777
2014
|
<span class="status">${ord.status === 'paid' ? 'ОПЛАЧЕН' : 'ОЖИДАЕТ ОПЛАТЫ ПО СЧЁТУ'}</span>
|
|
1778
2015
|
</div>
|
|
1779
2016
|
<div class="meta" style="margin-top:14px;">
|
|
1780
|
-
<div><strong>Поставщик (Лицензиар):</strong>
|
|
2017
|
+
<div><strong>Поставщик (Лицензиар):</strong> ${vendorTitle}${legal.inn ? `, ИНН: ${legal.inn}` : ''}${legal.ogrn ? `, ОГРН: ${legal.ogrn}` : ''}, адрес: ${vendorAddr}, тел.: ${vendorPhone}</div>
|
|
1781
2018
|
<div style="margin-top:8px;">
|
|
1782
2019
|
<strong>Покупатель (Лицензиат):</strong> ${custName}, ИНН: ${c.inn || '—'}${c.kpp ? ', КПП: ' + c.kpp : ''}
|
|
1783
2020
|
${c.legalAddress ? `<br><strong>Юридический адрес:</strong> ${c.legalAddress}` : ''}
|
|
@@ -1812,8 +2049,8 @@ export class LicenseStore {
|
|
|
1812
2049
|
<div class="sig-grid">
|
|
1813
2050
|
<div class="sig-box">
|
|
1814
2051
|
<strong>От Поставщика (Лицензиара):</strong><br>
|
|
1815
|
-
|
|
1816
|
-
______________________ / Руководитель /
|
|
2052
|
+
${vendorTitle}<br><br>
|
|
2053
|
+
______________________ / ${legal.authorName || 'Руководитель'} /
|
|
1817
2054
|
</div>
|
|
1818
2055
|
<div class="sig-box">
|
|
1819
2056
|
<strong>От Покупателя (Лицензиата):</strong><br>
|