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_KV_MOUNT=secret
|
||||||
VAULT_SHARED_PATH=
|
VAULT_SHARED_PATH=
|
||||||
VAULT_PROJECT_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",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.13.2",
|
||||||
|
"nodemailer": "^8.0.4",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
@@ -2078,6 +2079,15 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/nypm": {
|
||||||
"version": "0.6.5",
|
"version": "0.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"graphql": "^16.13.2",
|
"graphql": "^16.13.2",
|
||||||
|
"nodemailer": "^8.0.4",
|
||||||
"pg": "^8.20.0",
|
"pg": "^8.20.0",
|
||||||
"zod": "^4.3.6"
|
"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,
|
maskAuthDestination,
|
||||||
verifyLoginChallengeCode,
|
verifyLoginChallengeCode,
|
||||||
} from './auth.js';
|
} from './auth.js';
|
||||||
|
import { sendLoginCodeEmail } from './mailer.js';
|
||||||
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
|
import { dispatchToUserConnections, sendMessengerMessage } from './messenger.js';
|
||||||
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
import { dateTimeScalar, jsonScalar } from './scalars.js';
|
||||||
|
|
||||||
@@ -287,7 +288,11 @@ export const resolvers = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const code = getStaticAuthCode();
|
const code = getStaticAuthCode();
|
||||||
console.info(`[auth] login code for ${destination}: ${code}`);
|
await sendLoginCodeEmail({
|
||||||
|
to: destination,
|
||||||
|
code,
|
||||||
|
expiresAt: challenge.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
challengeToken: challenge.challengeToken,
|
challengeToken: challenge.challengeToken,
|
||||||
|
|||||||
Reference in New Issue
Block a user