made resend otp lambda
This commit is contained in:
@@ -325,3 +325,18 @@ updateSuggestionAsReviewed:
|
|||||||
- httpApi:
|
- httpApi:
|
||||||
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
|
path: /host/Activity_Hub/OnBoarding/update-suggestion-reviewed
|
||||||
method: patch
|
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
|
||||||
|
|||||||
63
src/common/utils/helper/resendOtpHelper.ts
Normal file
63
src/common/utils/helper/resendOtpHelper.ts
Normal file
@@ -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<OtpResult> {
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/modules/host/handlers/resendOtp.ts
Normal file
81
src/modules/host/handlers/resendOtp.ts
Normal file
@@ -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<APIGatewayProxyResult> => {
|
||||||
|
// 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 },
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
39
src/modules/host/services/resendOTPEmail.service.ts
Normal file
39
src/modules/host/services/resendOTPEmail.service.ts
Normal file
@@ -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 = `
|
||||||
|
<p>Dear ${role},</p>
|
||||||
|
<p>Your new OTP is: <strong>${otp}</strong></p>
|
||||||
|
<p>This code is valid for 5 minutes. Please do not share it with anyone.</p>
|
||||||
|
<p>Best regards,<br/>Minglar Team</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user