Send login codes via SMTP using Mailpit-compatible transport
This commit is contained in:
@@ -7,3 +7,10 @@ VAULT_TOKEN=
|
||||
VAULT_KV_MOUNT=secret
|
||||
VAULT_SHARED_PATH=
|
||||
VAULT_PROJECT_PATH=
|
||||
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=
|
||||
SMTP_SECURE=false
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_FROM=
|
||||
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -18,6 +18,7 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"graphql": "^16.13.2",
|
||||
"nodemailer": "^8.0.4",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
@@ -2078,6 +2079,15 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.4.tgz",
|
||||
"integrity": "sha512-k+jf6N8PfQJ0Fe8ZhJlgqU5qJU44Lpvp2yvidH3vp1lPnVQMgi4yEEMPXg5eJS1gFIJTVq1NHBk7Ia9ARdSBdQ==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.5",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"dotenv": "^17.3.1",
|
||||
"express": "^5.2.1",
|
||||
"graphql": "^16.13.2",
|
||||
"nodemailer": "^8.0.4",
|
||||
"pg": "^8.20.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
|
||||
69
src/mailer.js
Normal file
69
src/mailer.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
let cachedTransporter = null;
|
||||
|
||||
function getSmtpConfig() {
|
||||
const host = process.env.SMTP_HOST;
|
||||
const portRaw = process.env.SMTP_PORT;
|
||||
const from = process.env.SMTP_FROM;
|
||||
const secureRaw = process.env.SMTP_SECURE ?? 'false';
|
||||
const user = process.env.SMTP_USER;
|
||||
const pass = process.env.SMTP_PASS;
|
||||
|
||||
if (!host) {
|
||||
throw new Error('SMTP_HOST is required for email login code delivery.');
|
||||
}
|
||||
if (!portRaw) {
|
||||
throw new Error('SMTP_PORT is required for email login code delivery.');
|
||||
}
|
||||
if (!from) {
|
||||
throw new Error('SMTP_FROM is required for email login code delivery.');
|
||||
}
|
||||
|
||||
const port = Number(portRaw);
|
||||
if (!Number.isInteger(port) || port <= 0) {
|
||||
throw new Error('SMTP_PORT must be a valid integer port number.');
|
||||
}
|
||||
|
||||
const secure = String(secureRaw).toLowerCase() === 'true';
|
||||
const hasUser = Boolean(user);
|
||||
const hasPass = Boolean(pass);
|
||||
|
||||
if (hasUser !== hasPass) {
|
||||
throw new Error('SMTP_USER and SMTP_PASS must be set together.');
|
||||
}
|
||||
|
||||
return {
|
||||
from,
|
||||
transport: {
|
||||
host,
|
||||
port,
|
||||
secure,
|
||||
...(hasUser && hasPass ? { auth: { user, pass } } : {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getTransporter() {
|
||||
if (cachedTransporter) {
|
||||
return cachedTransporter;
|
||||
}
|
||||
|
||||
const config = getSmtpConfig();
|
||||
cachedTransporter = nodemailer.createTransport(config.transport);
|
||||
return cachedTransporter;
|
||||
}
|
||||
|
||||
export async function sendLoginCodeEmail({ to, code, expiresAt }) {
|
||||
const { from } = getSmtpConfig();
|
||||
const transporter = getTransporter();
|
||||
const expiresText = new Date(expiresAt).toLocaleString('ru-RU', { hour12: false });
|
||||
|
||||
await transporter.sendMail({
|
||||
from,
|
||||
to,
|
||||
subject: 'Код входа в личный кабинет Fregat',
|
||||
text: `Код входа: ${code}\nДействует до: ${expiresText}`,
|
||||
html: `<p>Код входа: <b>${code}</b></p><p>Действует до: ${expiresText}</p>`,
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
maskAuthDestination,
|
||||
verifyLoginChallengeCode,
|
||||
} from './auth.js';
|
||||
import { sendLoginCodeEmail } from './mailer.js';
|
||||
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
|
||||
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
||||
|
||||
@@ -287,7 +288,11 @@ export const resolvers = {
|
||||
});
|
||||
|
||||
const code = getStaticAuthCode();
|
||||
console.info(`[auth] login code for ${destination}: ${code}`);
|
||||
await sendLoginCodeEmail({
|
||||
to: destination,
|
||||
code,
|
||||
expiresAt: challenge.expiresAt,
|
||||
});
|
||||
|
||||
return {
|
||||
challengeToken: challenge.challengeToken,
|
||||
|
||||
Reference in New Issue
Block a user