From f0855177324fd51757516b8e1569951d115ed7b6 Mon Sep 17 00:00:00 2001 From: Vadim Date: Fri, 23 Oct 2026 11:01:13 +0300 Subject: [PATCH] email --- src/lib/auth.ts | 26 ++++++-------- src/lib/email-sender.ts | 35 +++++++++++++++++-- .../senders/email-channel.sender.ts | 2 +- 3 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index d799ec3..d056466 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -12,10 +12,7 @@ export const auth = betterAuth({ emailOTP({ async sendVerificationOTP({ email, otp, type }) { if (type === 'sign-in') { - // Send the OTP for sign in - console.log({ email, otp, type }); - - void EmailSender.getTransporter().sendMail({ + EmailSender.sendInBackground({ to: email, subject: `Your OTP code is: ${otp}`, text: `Your OTP code is: ${otp}`, @@ -23,8 +20,13 @@ export const auth = betterAuth({ } else if (type === 'email-verification') { // Send the OTP for email verification console.log({ email, otp, type }); + } else if (type === 'forget-password') { + EmailSender.sendInBackground({ + to: email, + subject: `Your password reset code is: ${otp}`, + text: `Your password reset code is: ${otp}`, + }); } else { - // Send the OTP for password reset console.log({ email, otp, type }); } }, @@ -47,26 +49,21 @@ export const auth = betterAuth({ autoSignIn: false, requireEmailVerification: true, onExistingUserSignUp: async ({ user }, request) => { - console.log('onExistingUserSignUp', { user }); - - void EmailSender.getTransporter().sendMail({ + EmailSender.sendInBackground({ to: user.email, subject: 'Sign-up attempt with your email', text: 'Someone tried to create an account using your email address. If this was you, try signing in instead. If not, you can safely ignore this email.', }); }, sendResetPassword: async ({ user, url, token }, request) => { - console.log('sendResetPassword', { user, url, token }); - void EmailSender.getTransporter().sendMail({ + EmailSender.sendInBackground({ to: user.email, subject: 'Reset your password', text: `Click the link to reset your password: ${url}`, }); }, onPasswordReset: async ({ user }, request) => { - // your logic here - console.log('onPasswordReset', { user }); - void EmailSender.getTransporter().sendMail({ + EmailSender.sendInBackground({ to: user.email, subject: 'Reset your password result', text: `Password for user ${user.email} has been reset.`, @@ -77,8 +74,7 @@ export const auth = betterAuth({ sendOnSignIn: true, autoSignInAfterVerification: true, sendVerificationEmail: async ({ user, url }) => { - console.log('emailVerification', { user, url }); - void EmailSender.getTransporter().sendMail({ + EmailSender.sendInBackground({ to: user.email, subject: 'Verify your email address', text: `Click the link to verify your email: ${url}`, diff --git a/src/lib/email-sender.ts b/src/lib/email-sender.ts index ba4ca4f..54552a3 100644 --- a/src/lib/email-sender.ts +++ b/src/lib/email-sender.ts @@ -1,13 +1,44 @@ +import { Logger } from '@nestjs/common'; import nodemailer, { SendMailOptions, Transporter } from 'nodemailer'; export default class EmailSender { - static getTransporter() { - return nodemailer.createTransport({ + private static readonly logger = new Logger(EmailSender.name); + private static transporter?: Transporter; + + private static isLocal() { + return process.env.NODE_ENV === 'local'; + } + + private static getTransporter() { + this.transporter ??= nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.GOOGLE_APP_USER, pass: process.env.GOOGLE_APP_PASSWORD, }, }); + return this.transporter; + } + + /** Sends an email; throws on failure. In local env only logs the message. */ + static async send(options: SendMailOptions): Promise { + if (this.isLocal()) { + this.logger.log( + `[local] email not sent\n to: ${String(options.to)}\n subject: ${options.subject}\n text: ${String(options.text)}`, + ); + return; + } + + await this.getTransporter().sendMail(options); + } + + /** Fire-and-forget variant: never throws, errors are logged. */ + static sendInBackground(options: SendMailOptions): void { + this.send(options).catch((error: unknown) => { + this.logger.error( + `Failed to send email "${options.subject}" to ${String(options.to)}`, + error instanceof Error ? error.stack : String(error), + ); + }); } } diff --git a/src/modules/notification/infrastructure/senders/email-channel.sender.ts b/src/modules/notification/infrastructure/senders/email-channel.sender.ts index 8010934..122f270 100644 --- a/src/modules/notification/infrastructure/senders/email-channel.sender.ts +++ b/src/modules/notification/infrastructure/senders/email-channel.sender.ts @@ -16,7 +16,7 @@ export class EmailChannelSender implements NotificationChannelSender { select: { email: true }, }); - await EmailSender.getTransporter().sendMail({ + await EmailSender.send({ to: user.email, subject: params.subject ?? 'Уведомление', text: params.content,