🔧 chore(account): update account module structure and email queue processing

- Enhanced account management with new validation and Swagger documentation.
- Updated Prisma schemas and migrations for the account and profile.
- Improved email handling mechanisms in the email queue system with new worker functionality.
- Adjusted Docker configurations and package dependencies for better integration.
This commit is contained in:
2026-04-03 00:54:46 +06:00
parent 81f9801487
commit 3f0ead4265
18 changed files with 285 additions and 98 deletions
+18
View File
@@ -23,5 +23,23 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
redis:
image: redis:7-alpine
container_name: server_redis
restart: always
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes: volumes:
postgres_data: postgres_data:
redis_data:
+2
View File
@@ -16,11 +16,13 @@
"@prisma/adapter-pg": "^7.5.0", "@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0", "@prisma/client": "^7.5.0",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"bullmq": "^5.72.1",
"cloudinary": "^2.7.0", "cloudinary": "^2.7.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"express": "^5.1.0", "express": "^5.1.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"multer": "^2.0.2", "multer": "^2.0.2",
"nodemailer": "^7.0.9", "nodemailer": "^7.0.9",
@@ -21,8 +21,12 @@ CREATE TABLE "Account" (
CREATE TABLE "Profile" ( CREATE TABLE "Profile" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"accountId" TEXT NOT NULL, "accountId" TEXT NOT NULL,
"fullName" TEXT NOT NULL, "shopName" TEXT NOT NULL,
"profilePhoto" TEXT, "shopLogo" TEXT,
"contactNumber" TEXT,
"shopAddress" TEXT,
"shopMapLocation" TEXT,
"shopCategory" TEXT,
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id") CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
); );
+6 -2
View File
@@ -3,6 +3,10 @@ model Profile {
accountId String @unique accountId String @unique
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade) account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
fullName String shopName String
profilePhoto String? shopLogo String?
contactNumber String?
shopAddress String?
shopMapLocation String?
shopCategory String?
} }
@@ -18,7 +18,7 @@ const verify_account_using_otp = catchAsync(async (req, res) => {
manageResponse(res, { manageResponse(res, {
statusCode: 200, statusCode: 200,
success: true, success: true,
message: "Otp verification successfull", message: "Otp verification successful",
data: result, data: result,
}); });
}); });
@@ -28,7 +28,7 @@ const verify_account_using_link = catchAsync(async (req, res) => {
manageResponse(res, { manageResponse(res, {
statusCode: 200, statusCode: 200,
success: true, success: true,
message: "Account verification successfull", message: "Account verification successful",
data: result, data: result,
}); });
}); });
@@ -83,9 +83,9 @@ const resend_otp_and_verification_link = catchAsync(async (req, res) => {
}); });
}); });
const forget_password_genereate_reset_token = catchAsync(async (req, res) => { const forget_password_generate_reset_token = catchAsync(async (req, res) => {
const result = const result =
await account_services.forget_password_genereate_reset_token_from_db(req); await account_services.forget_password_generate_reset_token_from_db(req);
manageResponse(res, { manageResponse(res, {
statusCode: 200, statusCode: 200,
success: true, success: true,
@@ -111,6 +111,6 @@ export const account_controller = {
verify_account_using_otp, verify_account_using_otp,
resend_otp_and_verification_link, resend_otp_and_verification_link,
verify_account_using_link, verify_account_using_link,
forget_password_genereate_reset_token, forget_password_generate_reset_token,
reset_password_using_token reset_password_using_token
}; };
+1 -1
View File
@@ -45,7 +45,7 @@ accountRouter.put(
accountRouter.put( accountRouter.put(
"/forget-password", "/forget-password",
RequestValidator(account_validation.resend_otp), RequestValidator(account_validation.resend_otp),
account_controller.forget_password_genereate_reset_token, account_controller.forget_password_generate_reset_token,
); );
accountRouter.put( accountRouter.put(
"/reset-password", "/reset-password",
+21 -20
View File
@@ -2,14 +2,22 @@ import bcrypt from "bcrypt";
import { Request } from "express"; import { Request } from "express";
import { configs } from "../../configs"; import { configs } from "../../configs";
import { prisma } from "../../lib/prisma"; import { prisma } from "../../lib/prisma";
import { emailQueue } from "../../queues/email/email.queue";
import { AppError } from "../../utils/app_error"; import { AppError } from "../../utils/app_error";
import { jwtHelpers } from "../../utils/JWT"; import { jwtHelpers } from "../../utils/JWT";
import { otpGenerator } from "../../utils/otpGenerator";
import sendMail from "../../utils/mail_sender"; import sendMail from "../../utils/mail_sender";
import { otpGenerator } from "../../utils/otpGenerator";
const create_account_into_db = async (req: Request) => { const create_account_into_db = async (req: Request) => {
const payload = req?.body; const payload = req?.body;
// check account exist or not
const existingAccount = await prisma.account.findUnique({
where: { email: payload.email },
});
if (existingAccount) {
throw new AppError("Email already exists", 403);
}
// hash password // hash password
const hashPassword = bcrypt.hashSync(payload.password, 10); const hashPassword = bcrypt.hashSync(payload.password, 10);
@@ -24,7 +32,7 @@ const create_account_into_db = async (req: Request) => {
const profile = await tx.profile.create({ const profile = await tx.profile.create({
data: { data: {
fullName: payload.fullName, shopName: payload.shopName,
accountId: account.id, accountId: account.id,
}, },
}); });
@@ -56,22 +64,14 @@ const create_account_into_db = async (req: Request) => {
lastOtpSendingTime: new Date(), lastOtpSendingTime: new Date(),
}, },
}); });
await emailQueue.add("email-queue", {
await sendMail({ name: payload.shopName,
to: payload.email as string, otp: newOtp,
subject: "welcome to - Please verify your account", verificationLink: verificationLink,
htmlBody: ` subject: "Welcome to Quick Launch - Verification OTP",
<p><strong>OTP</strong> ${newOtp}</p> email: payload.email,
<small>Otp will be expire in 5 minutes</small> textBody: "You can use otp or verification link for verifying your account"
})
<br/> <br/>
<p>Or you can use Verification link </p>
<p>${verificationLink}</p>
`,
textBody: "You can use otp or direct link",
name: payload.fullName,
});
return result; return result;
}; };
@@ -316,7 +316,7 @@ const resend_otp_and_verification_link_from_db = async (req: Request) => {
}); });
}; };
const forget_password_genereate_reset_token_from_db = async (req: Request) => { const forget_password_generate_reset_token_from_db = async (req: Request) => {
const email = req?.body?.email as string; const email = req?.body?.email as string;
const account = await prisma.account.findUnique({ const account = await prisma.account.findUnique({
where: { where: {
@@ -389,6 +389,7 @@ const reset_password_using_token_into_db = async (req: Request) => {
// infuter user alart for changing password // infuter user alart for changing password
return ""; return "";
}; };
export const account_services = { export const account_services = {
create_account_into_db, create_account_into_db,
login_user_into_db, login_user_into_db,
@@ -397,6 +398,6 @@ export const account_services = {
verify_account_using_otp_into_db, verify_account_using_otp_into_db,
resend_otp_and_verification_link_from_db, resend_otp_and_verification_link_from_db,
verify_account_using_link_into_db, verify_account_using_link_into_db,
forget_password_genereate_reset_token_from_db, forget_password_generate_reset_token_from_db,
reset_password_using_token_into_db reset_password_using_token_into_db
}; };
+1 -1
View File
@@ -11,7 +11,7 @@ export const accountSwaggerDocs = {
example: JSON.stringify({ example: JSON.stringify({
email: "user@gmail.com", email: "user@gmail.com",
password: "password", password: "password",
fullName: "User", shopName: "User",
}), }),
}, },
}, },
@@ -3,7 +3,7 @@ import z from "zod";
const sign_up = z.object({ const sign_up = z.object({
email: z.string("Email is required."), email: z.string("Email is required."),
password: z.string("Password is required."), password: z.string("Password is required."),
fullName: z.string("Full name is required."), shopName: z.string("Full name is required."),
}); });
const sing_in = z.object({ const sing_in = z.object({
@@ -12,19 +12,19 @@ const sing_in = z.object({
}); });
const change_password = z.object({ const change_password = z.object({
oldPassword: z.string("Old Password is requied"), oldPassword: z.string("Old Password is required"),
newPassword: z.string("New Password is required"), newPassword: z.string("New Password is required"),
}); });
const verify_otp = z.object({ const verify_otp = z.object({
email: z.string("Email is requied"), email: z.string("Email is required"),
otp: z.string("OTP is required"), otp: z.string("OTP is required"),
}); });
const verify_link = z.object({ const verify_link = z.object({
token: z.string("Token is required "), token: z.string("Token is required "),
}); });
const resend_otp = z.object({ const resend_otp = z.object({
email: z.string("Email is requied"), email: z.string("Email is required"),
}); });
const reset_pass = z.object({ const reset_pass = z.object({
token: z.string("Token is required"), token: z.string("Token is required"),
+6
View File
@@ -0,0 +1,6 @@
import { QueueOptions } from "bullmq";
export const redisConnection: QueueOptions["connection"] = {
host: "127.0.0.1",
port: 6379,
};
+16
View File
@@ -0,0 +1,16 @@
import { otpTemplate } from "../../templates/otpTemplate";
import sendMail from "../../utils/mail_sender";
import { TEmailQueue } from "./email.queue";
// email.processor.ts
export const emailProcessor = async (job: any) => {
const payload: TEmailQueue = job.data;
await sendMail({
to: payload.email as string,
subject: payload.subject,
htmlBody: otpTemplate(payload),
textBody: payload.textBody || "",
name: payload.name,
});
console.log("Sending email job complete:", job.id);
};
+16
View File
@@ -0,0 +1,16 @@
// email.queue.ts
import { Queue } from "bullmq";
import { redisConnection } from "../connection";
export type TEmailQueue = {
email: string;
name?: string;
otp?: string;
verificationLink?: string;
subject: string;
textBody?:string
}
export const emailQueue = new Queue<TEmailQueue>("email-queue", {
connection: redisConnection,
});
+13
View File
@@ -0,0 +1,13 @@
// email.worker.ts
import { Worker } from "bullmq";
import { redisConnection } from "../connection";
import { emailProcessor } from "./email.processor";
import { TEmailQueue } from "./email.queue";
export const emailWorker = new Worker<TEmailQueue>(
"email-queue",
async (job) => emailProcessor(job),
{
connection: redisConnection,
}
);
+3
View File
@@ -0,0 +1,3 @@
import "./email/email.worker";
console.log("Workers running...");
+82
View File
@@ -0,0 +1,82 @@
import { TEmailQueue } from "../queues/email/email.queue"
export const otpTemplate = (payload: TEmailQueue) => {
return `
<div
style="
margin: 0;
padding: 0;
background-color: #f4f6f8;
font-family: Arial, Helvetica, sans-serif;
"
>
<table
align="center"
width="100%"
cellpadding="0"
cellspacing="0"
style="
max-width: 600px;
margin: auto;
background: #ffffff;
border-radius: 8px;
overflow: hidden;
"
>
<tr>
<td style="padding: 30px; color: #333333">
<p style="margin: 0 0 20px 0; font-size: 15px">
Use the following One-Time Password (OTP) to complete your
verification:
</p>
<!-- OTP Box -->
<div style="text-align: center; margin: 25px 0">
<span
style="
display: inline-block;
background: #f1f5f9;
padding: 15px 25px;
font-size: 24px;
letter-spacing: 4px;
font-weight: bold;
color: #111827;
border-radius: 6px;
"
>
${payload.otp}
</span>
</div>
<p style="margin: 0 0 20px 0; font-size: 13px; color: #6b7280">
This OTP will expire in <strong>5 minutes</strong>.
</p>
<!-- Divider -->
<hr
style="
border: none;
border-top: 1px solid #e5e7eb;
margin: 25px 0;
"
/>
<!-- Verification Link -->
<p style="margin: 0 0 10px 0; font-size: 15px">
Or verify using this link:
</p>
<p style="word-break: break-all; font-size: 14px">
<a
href="${payload.verificationLink}"
style="color: #4f46e5; text-decoration: none"
>
${payload.verificationLink}
</a>
</p>
</td>
</tr>
</table>
</div>
`
}
+52 -31
View File
@@ -26,13 +26,12 @@ const sendMail = async (payload: TMailContent) => {
subject: payload.subject, subject: payload.subject,
text: payload.textBody, text: payload.textBody,
html: ` html: `
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head>
<head> <meta charset="UTF-8" />
<meta charset="UTF-8">
<title>Welcome Email</title> <title>Welcome Email</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style> <style>
* { * {
margin: 0; margin: 0;
@@ -52,48 +51,70 @@ const sendMail = async (payload: TMailContent) => {
} }
} }
</style> </style>
</head> </head>
<body <body style="margin: 0; padding: 0; font-family: Arial, sans-serif">
style="margin: 0; padding: 0; font-family: Arial, sans-serif;"> <div
style="
<div style="max-width: 600px; margin: 40px auto; background-color: #f4f4f4; padding: 40px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.05);" max-width: 600px;
class="container"> margin: 40px auto;
background-color: #f4f4f4;
<div style="font-size: 16px; color: #555555; line-height: 1.6;"> padding: 40px;
<p style="margin-bottom: 30px;">Hi <strong>${payload?.name || ""}</strong>,</p> border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
"
class="container"
>
<div style="font-size: 16px; color: #555555; line-height: 1.6">
<p style="margin-bottom: 30px">
Hi <strong>${payload?.name || ""}</strong>,
</p>
${payload?.htmlBody} ${payload?.htmlBody}
<div <div style="margin-top: 60px; text-align: center">
style=" margin-top: 60px; text-align: center;"> <img
style="width: 50px; height: 50px; border-radius: 50%"
src="https://i.ibb.co.com/RkFJjPWg/quick-launch-1.png"
alt="Quick Launch"
/>
<img style="width: 50px; height: 50px; border-radius: 50%;" <p style="font-size: 12px">The Support Team</p>
src="https://imgs.search.brave.com/IZoN38NQxnIIuB1I9E70bW6q5OvbEtz68YaxTe1j-o0/rs:fit:860:0:0:0/g:ce/aHR0cHM6Ly9lbGVt/ZW50cy1yZXNpemVk/LmVudmF0b3VzZXJj/b250ZW50LmNvbS9l/bGVtZW50cy1jb3Zl/ci1pbWFnZXMvMjhi/NmVjMTQtMGMwOS00/NGY1LWE5NGUtNmIy/OTM5NTZkMDM2P3c9/NDMzJmNmX2ZpdD1z/Y2FsZS1kb3duJnE9/ODUmZm9ybWF0PWF1/dG8mcz04Mjc0OWYy/ZDUyMmJiM2NlMjNi/OWNhNjhlZmFhNjdk/MTg5OGI4NWIwNzBh/MjQ1NjM4NmI1ZmFj/NWVmNmM5ZTNl" <h3>Quick Launch</h3>
alt="">
<p style="font-size: 12px;">The Support Team</p>
<h3>Company Name</h3>
</div> </div>
</div> </div>
<p style="font-size: 14px; color: #999999; margin-top: 20px; margin-bottom: 10px; text-align: center;"> <p
style="
font-size: 14px;
color: #999999;
margin-top: 20px;
margin-bottom: 10px;
text-align: center;
"
>
This is an automated message — please do not reply to this email. This is an automated message — please do not reply to this email.
<br> <br />
If you need assistance, feel free to contact our support team. If you need assistance, feel free to contact our support team.
<br><br> <br /><br />
Thank you for choosing us! Thank you for choosing us!
</p> </p>
<hr> <hr />
<div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;"> <div
&copy; {{year}} Your Company. All rights reserved. style="
text-align: center;
font-size: 12px;
color: #999999;
margin-top: 20px;
"
>
&copy; 2026 to {{year}} Quick Launch. All rights reserved.
</div> </div>
</div> </div>
</body> </body>
</html> </html>
`, `,
}); });
return info return info
+1
View File
@@ -1,6 +1,7 @@
import app from "./app"; import app from "./app";
import { configs } from "./app/configs/index"; import { configs } from "./app/configs/index";
import { prisma } from "./app/lib/prisma"; import { prisma } from "./app/lib/prisma";
import "./app/queues/worker";
async function main() { async function main() {
try { try {