diff --git a/serverless/functions/host.yml b/serverless/functions/host.yml index f7dbe3d..8e70ef5 100644 --- a/serverless/functions/host.yml +++ b/serverless/functions/host.yml @@ -325,3 +325,18 @@ updateSuggestionAsReviewed: - httpApi: path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed method: patch + +resendOTPmail: + handler: src/modules/host/handlers/resendOtp.handler + memorySize: 512 + package: + patterns: + - 'src/modules/host/handlers/resendOtp/**' + - ${file(./serverless/patterns/base.yml):pattern1} + - ${file(./serverless/patterns/base.yml):pattern2} + - ${file(./serverless/patterns/base.yml):pattern3} + - ${file(./serverless/patterns/base.yml):pattern4} + events: + - httpApi: + path: /resend-otp + method: post diff --git a/src/common/utils/helper/resendOtpHelper.ts b/src/common/utils/helper/resendOtpHelper.ts new file mode 100644 index 0000000..4f8eaa6 --- /dev/null +++ b/src/common/utils/helper/resendOtpHelper.ts @@ -0,0 +1,63 @@ +import * as bcrypt from "bcryptjs"; +import { OtpGenerator, OtpGeneratorSixDigit } from "./OtpGenerator"; +import { encryptUserId } from "./CodeGenerator"; + +export interface OtpResult { + otp: string; + hashedOtp: string; + expiry: Date; + encryptedId: string; +} + +export async function resendOtpHelper( + prisma: any, + userId: number, + email: string, + emailPurpose: "Register" | "Login" | "ForgotPassword", + otpLength: 4 | 6 = 4, + expiryMinutes: number = 5 +): Promise { + + // 1️⃣ Deactivate previous OTPs + await prisma.userOtp.updateMany({ + where: { + userXid: userId, + otpType: emailPurpose, + isActive: true, + }, + data: { + isActive: false, + isVerified: true, + }, + }); + + // 2️⃣ Generate new OTP + const otp = + otpLength === 6 + ? OtpGeneratorSixDigit.generateOtp() + : OtpGenerator.generateOtp(); + + const hashedOtp = await bcrypt.hash(otp, 10); + const expiry = new Date(Date.now() + expiryMinutes * 60000); + const encryptedId = encryptUserId(userId.toString()); + + // 3️⃣ Insert new OTP into table + await prisma.userOtp.create({ + data: { + userXid: userId, + otpType: emailPurpose, + otpCode: hashedOtp, + expiresOn: expiry, + isVerified: false, + isActive: true, + }, + }); + + // 4️⃣ Return new OTP (email will use this) + return { + otp, + hashedOtp, + expiry, + encryptedId, + }; +} diff --git a/src/modules/host/handlers/resendOtp.ts b/src/modules/host/handlers/resendOtp.ts new file mode 100644 index 0000000..91b8f63 --- /dev/null +++ b/src/modules/host/handlers/resendOtp.ts @@ -0,0 +1,81 @@ +import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda"; +import { PrismaService } from "../../../common/database/prisma.service"; +import { safeHandler } from "../../../common/utils/handlers/safeHandler"; +import ApiError from "../../../common/utils/helper/ApiError"; +import { resendOtpHelper } from "../../../common/utils/helper/resendOtpHelper"; +import { resendOtpEmail } from "../services/resendOTPEmail.service"; + +const prisma = new PrismaService(); + +// allowed purposes +const ALLOWED_PURPOSES = ["Register", "Login", "ForgotPassword"] as const; +type OtpPurpose = typeof ALLOWED_PURPOSES[number]; + +export const handler = safeHandler( + async (event: APIGatewayProxyEvent): Promise => { + // parse body safely + let body: { email?: string; purpose?: string } = {}; + try { + body = event.body ? JSON.parse(event.body) : {}; + } catch { + throw new ApiError(400, "Invalid JSON in request body"); + } + + // allow passing purpose via query string too (useful for GET requests) + const qsPurpose = event.queryStringParameters?.purpose; + const purposeRaw = (body.purpose || qsPurpose || "").trim(); + + if (!purposeRaw) { + throw new ApiError(400, "purpose is required. Allowed values: Register, Login, ForgotPassword"); + } + + if (!ALLOWED_PURPOSES.includes(purposeRaw as OtpPurpose)) { + throw new ApiError( + 400, + `Invalid purpose '${purposeRaw}'. Allowed values: ${ALLOWED_PURPOSES.join(", ")}` + ); + } + + const purpose = purposeRaw as OtpPurpose; + + const email = (body.email || "").trim(); + if (!email) throw new ApiError(400, "Email is required"); + + // find user (you can adapt the isActive / userStatus checks per your rules) + const user = await prisma.user.findUnique({ + where: { emailAddress: email, isActive: true }, + select: { id: true, emailAddress: true, role: true }, + }); + + if (!user) { + throw new ApiError(404, "User not found"); + } + const role = user.role.roleName + + // call resend helper (old OTPs become inactive + verified, new OTP gets created) + const otpResult = await resendOtpHelper( + prisma, + user.id, + user.emailAddress, + purpose, + 6, // 6-digit OTP + 5 // expires in 5 minutes + ); + + // send email (use appropriate template based on 'purpose' inside the email service) + await resendOtpEmail(user.emailAddress, otpResult.otp, role); + + return { + statusCode: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + }, + body: JSON.stringify({ + success: true, + message: "OTP resent successfully.", + data: { purpose }, + }), + }; + } +); diff --git a/src/modules/host/services/resendOTPEmail.service.ts b/src/modules/host/services/resendOTPEmail.service.ts new file mode 100644 index 0000000..f7a2a34 --- /dev/null +++ b/src/modules/host/services/resendOTPEmail.service.ts @@ -0,0 +1,39 @@ +import { brevoService } from "@/common/email/brevoApi"; +import ApiError from "@/common/utils/helper/ApiError"; + +export async function resendOtpEmail( + emailAddress: string, + otp: string | number, + role: string +): Promise<{ + sent: boolean; + // messageId: string +}> { + + const subject = "New OTP from Minglar Team"; + + const htmlContent = ` +

Dear ${role},

+

Your new OTP is: ${otp}

+

This code is valid for 5 minutes. Please do not share it with anyone.

+

Best regards,
Minglar Team

+ `; + + try { + const result = await brevoService.sendEmail({ + recipients: [{ email: emailAddress }], + subject, + htmlContent, + }); + + // console.log("📧 Email sent successfully:", result); + + return { + sent: true, + // messageId: result.messageId + }; + } catch (err) { + console.error("Brevo email send failed:", err); + throw new ApiError(500, "Failed to send OTP to host via email."); + } +}