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/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 subject = 'Код подтверждения ODANT-WORK';
168
- const textBody = `Ваш одноразовый код: ${code}\n\nДействует 10 минут.`;
169
- const htmlBody = `
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} &lt;${fromAddr}&gt;</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>ODANT-WORK</p>
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: `"ODANT-WORK" <${cfg.user}>`,
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 = (cfg.user.split('@')[1] || 'yandex.ru').trim();
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: "ODANT-WORK" <${cfg.user}>`,
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
- const user = Object.values(this.state.users || {}).find(
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 || 'Организация ПАК WORK',
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> ООО «${VENDOR_CONTACTS.company}», адрес: ${VENDOR_CONTACTS.address}, тел.: ${VENDOR_CONTACTS.phones.join(', ')}</div>
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
- ООО «${VENDOR_CONTACTS.company}»<br><br>
1816
- ______________________ / Руководитель /
2052
+ ${vendorTitle}<br><br>
2053
+ ______________________ / ${legal.authorName || 'Руководитель'} /
1817
2054
  </div>
1818
2055
  <div class="sig-box">
1819
2056
  <strong>От Покупателя (Лицензиата):</strong><br>