From 2b449e61ea46eda439c27a99cef515e8123cc47c Mon Sep 17 00:00:00 2001 From: faris Aljohari <83524184+farisaljohari@users.noreply.github.com> Date: Sun, 20 Apr 2025 22:19:08 +0300 Subject: [PATCH] add OTP email sending functionality and integrate with user authentication flow --- libs/common/src/config/email.config.ts | 2 + libs/common/src/util/email.service.ts | 43 +++++++ src/auth/services/user-auth.service.ts | 159 ++++++++++++++----------- 3 files changed, 134 insertions(+), 70 deletions(-) diff --git a/libs/common/src/config/email.config.ts b/libs/common/src/config/email.config.ts index 7c9b776..57ae480 100644 --- a/libs/common/src/config/email.config.ts +++ b/libs/common/src/config/email.config.ts @@ -19,5 +19,7 @@ export default registerAs( process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID, MAILTRAP_EDIT_USER_TEMPLATE_UUID: process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID, + MAILTRAP_SEND_OTP_TEMPLATE_UUID: + process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID, }), ); diff --git a/libs/common/src/util/email.service.ts b/libs/common/src/util/email.service.ts index c6e09ec..aee78fe 100644 --- a/libs/common/src/util/email.service.ts +++ b/libs/common/src/util/email.service.ts @@ -181,6 +181,49 @@ export class EmailService { ); } } + async sendOtpEmailWithTemplate( + email: string, + emailEditData: any, + ): Promise { + const isProduction = process.env.NODE_ENV === 'production'; + const API_TOKEN = this.configService.get( + 'email-config.MAILTRAP_API_TOKEN', + ); + const API_URL = isProduction + ? SEND_EMAIL_API_URL_PROD + : SEND_EMAIL_API_URL_DEV; + const TEMPLATE_UUID = this.configService.get( + 'email-config.MAILTRAP_SEND_OTP_TEMPLATE_UUID', + ); + + const emailData = { + from: { + email: this.smtpConfig.sender, + }, + to: [ + { + email: email, + }, + ], + template_uuid: TEMPLATE_UUID, + template_variables: emailEditData, + }; + + try { + await axios.post(API_URL, emailData, { + headers: { + Authorization: `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/json', + }, + }); + } catch (error) { + throw new HttpException( + error.response?.data?.message || + 'Error sending email using Mailtrap template', + error.response?.status || HttpStatus.INTERNAL_SERVER_ERROR, + ); + } + } generateUserChangesEmailBody( addedSpaceNames: string[], removedSpaceNames: string[], diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index eea8285..6a73fcc 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -181,79 +181,98 @@ export class UserAuthService { otpCode: string; cooldown: number; }> { - const otpLimiter = new Date(); - otpLimiter.setDate( - otpLimiter.getDate() - this.configService.get('OTP_LIMITER'), - ); - const userExists = await this.userRepository.exists({ - where: { - region: data.regionUuid - ? { - uuid: data.regionUuid, - } - : undefined, - email: data.email, - isUserVerified: data.type === OtpType.PASSWORD ? true : undefined, - }, - }); - if (!userExists) { - throw new BadRequestException('User not found'); - } - await this.otpRepository.softDelete({ email: data.email, type: data.type }); - await this.otpRepository.delete({ - email: data.email, - type: data.type, - createdAt: LessThan(otpLimiter), - }); - const countOfOtp = await this.otpRepository.count({ - withDeleted: true, - where: { - email: data.email, - type: data.type, - createdAt: MoreThan(otpLimiter), - }, - }); - const lastOtp = await this.otpRepository.findOne({ - where: { email: data.email, type: data.type }, - order: { createdAt: 'DESC' }, - withDeleted: true, - }); - let cooldown = 30 * Math.pow(2, countOfOtp - 1); - if (lastOtp) { - const now = new Date(); - const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt); - - if (timeSinceLastOtp < cooldown) { - throw new BadRequestException({ - message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`, - data: { - cooldown: cooldown - timeSinceLastOtp, - }, - }); + try { + const otpLimiter = new Date(); + otpLimiter.setDate( + otpLimiter.getDate() - this.configService.get('OTP_LIMITER'), + ); + const user = await this.userRepository.findOne({ + where: { + region: data.regionUuid ? { uuid: data.regionUuid } : undefined, + email: data.email, + isUserVerified: data.type === OtpType.PASSWORD ? true : undefined, + }, + }); + if (!user) { + throw new BadRequestException('User not found'); } - } - const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); - const expiryTime = new Date(); - expiryTime.setMinutes(expiryTime.getMinutes() + 10); - await this.otpRepository.save({ - email: data.email, - otpCode, - expiryTime, - type: data.type, - }); - const countOfOtpToReturn = await this.otpRepository.count({ - withDeleted: true, - where: { + await this.otpRepository.softDelete({ email: data.email, type: data.type, - createdAt: MoreThan(otpLimiter), - }, - }); - cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1); - const subject = 'OTP send successfully'; - const message = `Your OTP code is ${otpCode}`; - this.emailService.sendEmail(data.email, subject, message); - return { otpCode, cooldown }; + }); + await this.otpRepository.delete({ + email: data.email, + type: data.type, + createdAt: LessThan(otpLimiter), + }); + const countOfOtp = await this.otpRepository.count({ + withDeleted: true, + where: { + email: data.email, + type: data.type, + createdAt: MoreThan(otpLimiter), + }, + }); + const lastOtp = await this.otpRepository.findOne({ + where: { email: data.email, type: data.type }, + order: { createdAt: 'DESC' }, + withDeleted: true, + }); + let cooldown = 30 * Math.pow(2, countOfOtp - 1); + if (lastOtp) { + const now = new Date(); + const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt); + + if (timeSinceLastOtp < cooldown) { + throw new BadRequestException({ + message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`, + data: { + cooldown: cooldown - timeSinceLastOtp, + }, + }); + } + } + const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); + const expiryTime = new Date(); + expiryTime.setMinutes(expiryTime.getMinutes() + 10); + await this.otpRepository.save({ + email: data.email, + otpCode, + expiryTime, + type: data.type, + }); + const countOfOtpToReturn = await this.otpRepository.count({ + withDeleted: true, + where: { + email: data.email, + type: data.type, + createdAt: MoreThan(otpLimiter), + }, + }); + cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1); + + const [otp1, otp2, otp3, otp4, otp5, otp6] = otpCode.split(''); + + await this.emailService.sendOtpEmailWithTemplate(data.email, { + name: user.firstName, + otp1, + otp2, + otp3, + otp4, + otp5, + otp6, + }); + + return { otpCode, cooldown }; + } catch (error) { + if (error instanceof BadRequestException) { + throw error; + } + console.error('OTP generation error:', error); + throw new BadRequestException( + 'An unexpected error occurred while generating the OTP.', + ); + } } async verifyOTP(