Send login codes via SMTP using Mailpit-compatible transport

This commit is contained in:
Ruslan Bakiev
2026-04-02 14:58:00 +07:00
parent dd92172cac
commit 5ba87a1242
5 changed files with 93 additions and 1 deletions

69
src/mailer.js Normal file
View 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>`,
});
}

View File

@@ -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,