add OTP email sending functionality and integrate with user authentication flow

This commit is contained in:
faris Aljohari
2025-04-20 22:19:08 +03:00
parent 1e6503c072
commit 2b449e61ea
3 changed files with 134 additions and 70 deletions

View File

@ -19,5 +19,7 @@ export default registerAs(
process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID, process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID,
MAILTRAP_EDIT_USER_TEMPLATE_UUID: MAILTRAP_EDIT_USER_TEMPLATE_UUID:
process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID, process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID,
MAILTRAP_SEND_OTP_TEMPLATE_UUID:
process.env.MAILTRAP_SEND_OTP_TEMPLATE_UUID,
}), }),
); );

View File

@ -181,6 +181,49 @@ export class EmailService {
); );
} }
} }
async sendOtpEmailWithTemplate(
email: string,
emailEditData: any,
): Promise<void> {
const isProduction = process.env.NODE_ENV === 'production';
const API_TOKEN = this.configService.get<string>(
'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<string>(
'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( generateUserChangesEmailBody(
addedSpaceNames: string[], addedSpaceNames: string[],
removedSpaceNames: string[], removedSpaceNames: string[],

View File

@ -181,79 +181,98 @@ export class UserAuthService {
otpCode: string; otpCode: string;
cooldown: number; cooldown: number;
}> { }> {
const otpLimiter = new Date(); try {
otpLimiter.setDate( const otpLimiter = new Date();
otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'), otpLimiter.setDate(
); otpLimiter.getDate() - this.configService.get<number>('OTP_LIMITER'),
const userExists = await this.userRepository.exists({ );
where: { const user = await this.userRepository.findOne({
region: data.regionUuid where: {
? { region: data.regionUuid ? { uuid: data.regionUuid } : undefined,
uuid: data.regionUuid, email: data.email,
} isUserVerified: data.type === OtpType.PASSWORD ? true : undefined,
: undefined, },
email: data.email, });
isUserVerified: data.type === OtpType.PASSWORD ? true : undefined, if (!user) {
}, throw new BadRequestException('User not found');
});
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,
},
});
} }
} await this.otpRepository.softDelete({
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, email: data.email,
type: data.type, type: data.type,
createdAt: MoreThan(otpLimiter), });
}, await this.otpRepository.delete({
}); email: data.email,
cooldown = 30 * Math.pow(2, countOfOtpToReturn - 1); type: data.type,
const subject = 'OTP send successfully'; createdAt: LessThan(otpLimiter),
const message = `Your OTP code is ${otpCode}`; });
this.emailService.sendEmail(data.email, subject, message); const countOfOtp = await this.otpRepository.count({
return { otpCode, cooldown }; 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( async verifyOTP(