work-license-server 1.0.11 → 1.0.13
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/package.json +4 -1
- package/src/server.js +83 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "work-license-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.13",
|
|
4
4
|
"description": "Лицензионный сервер, биллинг ЮKassa, боты поддержки (MAX, Telegram, VK) и лендинг ПАК WORK (work.odant.org)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"license": "SEE LICENSE IN README.md",
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22.0.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"nodemailer": "^8.0.4"
|
|
31
34
|
}
|
|
32
35
|
}
|
package/src/server.js
CHANGED
|
@@ -157,28 +157,90 @@ function loadSmtpConfig() {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
* Отправка реального письма с 6-значным кодом подтверждения
|
|
160
|
+
* Отправка реального письма с 6-значным кодом подтверждения (через nodemailer — точно как в sources/host/auth-methods.js,
|
|
161
|
+
* с полноценными RFC 5322 заголовками Date, Message-ID и text+html, чтобы Gmail доставлял письмо мгновенно).
|
|
161
162
|
*/
|
|
163
|
+
let _cachedTransporter = null;
|
|
162
164
|
async function sendOtpEmail({ to, code }) {
|
|
163
165
|
const cfg = loadSmtpConfig();
|
|
164
|
-
const
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
166
|
+
const subject = 'Код подтверждения ODANT-WORK';
|
|
167
|
+
const textBody = `Ваш одноразовый код: ${code}\n\nДействует 10 минут.`;
|
|
168
|
+
const htmlBody = `
|
|
169
|
+
<h2>Код подтверждения</h2>
|
|
170
|
+
<p>Ваш код для входа/регистрации на work.odant.org:</p>
|
|
171
|
+
<h1 style="font-size: 48px; letter-spacing: 10px; font-weight: bold; text-align: center;">
|
|
172
|
+
${code}
|
|
173
|
+
</h1>
|
|
174
|
+
<p>Код действителен <strong>10 минут</strong>. Ни с кем не делитесь.</p>
|
|
175
|
+
<p>Если это не вы — просто проигнорируйте письмо.</p>
|
|
176
|
+
<p>С уважением,<br>ODANT-WORK</p>
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
let nm;
|
|
181
|
+
try {
|
|
182
|
+
nm = await import('nodemailer');
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
nm = await import(new URL('../../node_modules/nodemailer/lib/nodemailer.js', import.meta.url).href);
|
|
186
|
+
}
|
|
187
|
+
const nodemailer = nm.default || nm;
|
|
188
|
+
if (!_cachedTransporter) {
|
|
189
|
+
_cachedTransporter = nodemailer.createTransport({
|
|
190
|
+
host: cfg.host,
|
|
191
|
+
port: cfg.port,
|
|
192
|
+
secure: cfg.port === 465,
|
|
193
|
+
auth: {
|
|
194
|
+
user: cfg.user,
|
|
195
|
+
pass: cfg.pass,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const info = await _cachedTransporter.sendMail({
|
|
200
|
+
from: `"ODANT-WORK" <${cfg.user}>`,
|
|
201
|
+
to,
|
|
202
|
+
subject,
|
|
203
|
+
text: textBody,
|
|
204
|
+
html: htmlBody,
|
|
205
|
+
});
|
|
206
|
+
console.log('[work.odant.org] SMTP delivered to', to, 'messageId:', info?.messageId, 'response:', info?.response);
|
|
207
|
+
return { ok: true, messageId: info?.messageId };
|
|
208
|
+
}
|
|
209
|
+
catch (nmErr) {
|
|
210
|
+
if (nmErr?.code !== 'ERR_MODULE_NOT_FOUND') {
|
|
211
|
+
console.error('[work.odant.org] SMTP error via nodemailer:', nmErr?.message || nmErr);
|
|
212
|
+
throw nmErr;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Резервный нативный SMTPS-клиент с полными RFC 5322 заголовками (Date + Message-ID + multipart/alternative text+html)
|
|
217
|
+
const domain = (cfg.user.split('@')[1] || 'yandex.ru').trim();
|
|
218
|
+
const msgId = `<${crypto.randomUUID()}@${domain}>`;
|
|
219
|
+
const dateHdr = new Date().toUTCString();
|
|
220
|
+
const boundary = `----=_Part_${crypto.randomBytes(8).toString('hex')}`;
|
|
221
|
+
const subjectB64 = Buffer.from(subject, 'utf8').toString('base64');
|
|
222
|
+
const b64Text = (Buffer.from(textBody, 'utf8').toString('base64').match(/.{1,76}/g) || []).join('\r\n');
|
|
223
|
+
const b64Html = (Buffer.from(htmlBody, 'utf8').toString('base64').match(/.{1,76}/g) || []).join('\r\n');
|
|
173
224
|
const mimeMsg = [
|
|
225
|
+
`Date: ${dateHdr}`,
|
|
226
|
+
`Message-ID: ${msgId}`,
|
|
174
227
|
`From: "ODANT-WORK" <${cfg.user}>`,
|
|
175
228
|
`To: <${to}>`,
|
|
176
229
|
`Subject: =?UTF-8?B?${subjectB64}?=`,
|
|
177
230
|
'MIME-Version: 1.0',
|
|
231
|
+
`Content-Type: multipart/alternative; boundary="${boundary}"`,
|
|
232
|
+
'',
|
|
233
|
+
`--${boundary}`,
|
|
234
|
+
'Content-Type: text/plain; charset=utf-8',
|
|
235
|
+
'Content-Transfer-Encoding: base64',
|
|
236
|
+
'',
|
|
237
|
+
b64Text,
|
|
238
|
+
`--${boundary}`,
|
|
178
239
|
'Content-Type: text/html; charset=utf-8',
|
|
179
240
|
'Content-Transfer-Encoding: base64',
|
|
180
241
|
'',
|
|
181
|
-
|
|
242
|
+
b64Html,
|
|
243
|
+
`--${boundary}--`,
|
|
182
244
|
'.',
|
|
183
245
|
'',
|
|
184
246
|
].join('\r\n');
|
|
@@ -202,7 +264,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
202
264
|
reject(new Error('Ошибка SMTP: ' + line));
|
|
203
265
|
return;
|
|
204
266
|
}
|
|
205
|
-
if (step === 0) { step = 1; sendCmd(
|
|
267
|
+
if (step === 0) { step = 1; sendCmd(`EHLO ${domain}`); }
|
|
206
268
|
else if (step === 1) { step = 2; sendCmd('AUTH LOGIN'); }
|
|
207
269
|
else if (step === 2) { step = 3; sendCmd(Buffer.from(cfg.user, 'utf8').toString('base64')); }
|
|
208
270
|
else if (step === 3) { step = 4; sendCmd(Buffer.from(cfg.pass, 'utf8').toString('base64')); }
|
|
@@ -210,7 +272,7 @@ async function sendOtpEmail({ to, code }) {
|
|
|
210
272
|
else if (step === 5) { step = 6; sendCmd(`RCPT TO:<${to}>`); }
|
|
211
273
|
else if (step === 6) { step = 7; sendCmd('DATA'); }
|
|
212
274
|
else if (step === 7) { step = 8; sock.write(mimeMsg); }
|
|
213
|
-
else if (step === 8) { step = 9; sendCmd('QUIT'); sock.end(); resolve({ ok: true }); }
|
|
275
|
+
else if (step === 8) { step = 9; sendCmd('QUIT'); sock.end(); resolve({ ok: true, messageId: msgId }); }
|
|
214
276
|
}
|
|
215
277
|
});
|
|
216
278
|
});
|
|
@@ -330,7 +392,14 @@ export function createLicenseServerApp(options = {}) {
|
|
|
330
392
|
|
|
331
393
|
// 2. Каталог ПАК, отдельной базовой лицензии (4 млн ₽), тарифов SLA, прочих товаров, партнёрской франшизы и оферты
|
|
332
394
|
if (req.method === 'GET' && pathname === '/api/catalog') {
|
|
395
|
+
let serverVersion = '1.0.13';
|
|
396
|
+
try {
|
|
397
|
+
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
398
|
+
serverVersion = pkg.version || serverVersion;
|
|
399
|
+
}
|
|
400
|
+
catch { /* ignore */ }
|
|
333
401
|
return sendJson(res, 200, {
|
|
402
|
+
serverVersion,
|
|
334
403
|
product: PAK_PRODUCT,
|
|
335
404
|
baseLicenseProduct: BASE_LICENSE_PRODUCT,
|
|
336
405
|
additionalProducts: ADDITIONAL_PRODUCTS,
|
|
@@ -360,6 +429,7 @@ export function createLicenseServerApp(options = {}) {
|
|
|
360
429
|
expiresAt: Date.now() + 10 * 60_000,
|
|
361
430
|
attempts: 0,
|
|
362
431
|
});
|
|
432
|
+
console.log('[work.odant.org] Registration/Login code for', cleanEmail, ':', code);
|
|
363
433
|
if (!options.skipEmailSend) {
|
|
364
434
|
await sendOtpEmail({ to: cleanEmail, code });
|
|
365
435
|
}
|