Merge pull request #1 from techzaaa/abumahid

🔧 chore(account): update account module structure and email queue pro…
This commit is contained in:
2026-04-03 00:55:43 +06:00
committed by GitHub
18 changed files with 285 additions and 98 deletions
+18
View File
@@ -23,5 +23,23 @@ services:
timeout: 5s
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:
postgres_data:
redis_data:
+2
View File
@@ -16,11 +16,13 @@
"@prisma/adapter-pg": "^7.5.0",
"@prisma/client": "^7.5.0",
"bcrypt": "^6.0.0",
"bullmq": "^5.72.1",
"cloudinary": "^2.7.0",
"cookie-parser": "^1.4.7",
"cors": "^2.8.5",
"dotenv": "^17.3.1",
"express": "^5.1.0",
"ioredis": "^5.10.1",
"jsonwebtoken": "^9.0.2",
"multer": "^2.0.2",
"nodemailer": "^7.0.9",
@@ -21,8 +21,12 @@ CREATE TABLE "Account" (
CREATE TABLE "Profile" (
"id" TEXT NOT NULL,
"accountId" TEXT NOT NULL,
"fullName" TEXT NOT NULL,
"profilePhoto" TEXT,
"shopName" TEXT NOT NULL,
"shopLogo" TEXT,
"contactNumber" TEXT,
"shopAddress" TEXT,
"shopMapLocation" TEXT,
"shopCategory" TEXT,
CONSTRAINT "Profile_pkey" PRIMARY KEY ("id")
);
+9 -9
View File
@@ -4,16 +4,16 @@ enum ROLE {
}
model Account {
id String @id @default(uuid())
email String @unique
password String
role ROLE @default(USER)
lastOtp String?
id String @id @default(uuid())
email String @unique
password String
role ROLE @default(USER)
lastOtp String?
lastOtpSendingTime DateTime?
isDeleted Boolean @default(false)
isAccountVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
isDeleted Boolean @default(false)
isAccountVerified Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now())
profile Profile?
}
+6 -2
View File
@@ -3,6 +3,10 @@ model Profile {
accountId String @unique
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
fullName String
profilePhoto String?
shopName 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, {
statusCode: 200,
success: true,
message: "Otp verification successfull",
message: "Otp verification successful",
data: result,
});
});
@@ -28,7 +28,7 @@ const verify_account_using_link = catchAsync(async (req, res) => {
manageResponse(res, {
statusCode: 200,
success: true,
message: "Account verification successfull",
message: "Account verification successful",
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 =
await account_services.forget_password_genereate_reset_token_from_db(req);
await account_services.forget_password_generate_reset_token_from_db(req);
manageResponse(res, {
statusCode: 200,
success: true,
@@ -111,6 +111,6 @@ export const account_controller = {
verify_account_using_otp,
resend_otp_and_verification_link,
verify_account_using_link,
forget_password_genereate_reset_token,
forget_password_generate_reset_token,
reset_password_using_token
};
+1 -1
View File
@@ -45,7 +45,7 @@ accountRouter.put(
accountRouter.put(
"/forget-password",
RequestValidator(account_validation.resend_otp),
account_controller.forget_password_genereate_reset_token,
account_controller.forget_password_generate_reset_token,
);
accountRouter.put(
"/reset-password",
+22 -21
View File
@@ -2,14 +2,22 @@ import bcrypt from "bcrypt";
import { Request } from "express";
import { configs } from "../../configs";
import { prisma } from "../../lib/prisma";
import { emailQueue } from "../../queues/email/email.queue";
import { AppError } from "../../utils/app_error";
import { jwtHelpers } from "../../utils/JWT";
import { otpGenerator } from "../../utils/otpGenerator";
import sendMail from "../../utils/mail_sender";
import { otpGenerator } from "../../utils/otpGenerator";
const create_account_into_db = async (req: Request) => {
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
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({
data: {
fullName: payload.fullName,
shopName: payload.shopName,
accountId: account.id,
},
});
@@ -56,22 +64,14 @@ const create_account_into_db = async (req: Request) => {
lastOtpSendingTime: new Date(),
},
});
await sendMail({
to: payload.email as string,
subject: "welcome to - Please verify your account",
htmlBody: `
<p><strong>OTP</strong> ${newOtp}</p>
<small>Otp will be expire in 5 minutes</small>
<br/> <br/>
<p>Or you can use Verification link </p>
<p>${verificationLink}</p>
`,
textBody: "You can use otp or direct link",
name: payload.fullName,
});
await emailQueue.add("email-queue", {
name: payload.shopName,
otp: newOtp,
verificationLink: verificationLink,
subject: "Welcome to Quick Launch - Verification OTP",
email: payload.email,
textBody: "You can use otp or verification link for verifying your account"
})
return result;
};
@@ -96,7 +96,7 @@ const verify_account_using_otp_into_db = async (req: Request) => {
const OTP_EXPIRY_TIME = 5 * 60 * 1000; // 5 minutes in ms
const isOtpExpired = account.lastOtpSendingTime
? new Date().getTime() - new Date(account.lastOtpSendingTime).getTime() >
OTP_EXPIRY_TIME
OTP_EXPIRY_TIME
: true;
if (isOtpExpired) {
@@ -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 account = await prisma.account.findUnique({
where: {
@@ -389,6 +389,7 @@ const reset_password_using_token_into_db = async (req: Request) => {
// infuter user alart for changing password
return "";
};
export const account_services = {
create_account_into_db,
login_user_into_db,
@@ -397,6 +398,6 @@ export const account_services = {
verify_account_using_otp_into_db,
resend_otp_and_verification_link_from_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
};
+1 -1
View File
@@ -11,7 +11,7 @@ export const accountSwaggerDocs = {
example: JSON.stringify({
email: "user@gmail.com",
password: "password",
fullName: "User",
shopName: "User",
}),
},
},
@@ -3,7 +3,7 @@ import z from "zod";
const sign_up = z.object({
email: z.string("Email 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({
@@ -12,19 +12,19 @@ const sing_in = 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"),
});
const verify_otp = z.object({
email: z.string("Email is requied"),
email: z.string("Email is required"),
otp: z.string("OTP is required"),
});
const verify_link = z.object({
token: z.string("Token is required "),
});
const resend_otp = z.object({
email: z.string("Email is requied"),
email: z.string("Email is required"),
});
const reset_pass = z.object({
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>
`
}
+74 -53
View File
@@ -26,73 +26,94 @@ const sendMail = async (payload: TMailContent) => {
subject: payload.subject,
text: payload.textBody,
html: `
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<head>
<meta charset="UTF-8" />
<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>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
@media only screen and (max-width: 600px) {
.container {
padding: 20px !important;
}
/* Fallback styles for unsupported clients (some email clients ignore <style> tags) */
@media only screen and (max-width: 600px) {
.container {
padding: 20px !important;
}
.btn {
padding: 12px 18px !important;
font-size: 16px !important;
}
.btn {
padding: 12px 18px !important;
font-size: 16px !important;
}
}
</style>
</head>
</head>
<body
style="margin: 0; padding: 0; font-family: Arial, sans-serif;">
<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);"
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}
<div
style=" margin-top: 60px; text-align: center;">
<img style="width: 50px; height: 50px; border-radius: 50%;"
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"
alt="">
<p style="font-size: 12px;">The Support Team</p>
<h3>Company Name</h3>
</div>
</div>
<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.
<br>
If you need assistance, feel free to contact our support team.
<br><br>
Thank you for choosing us!
<body style="margin: 0; padding: 0; font-family: Arial, sans-serif">
<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);
"
class="container"
>
<div style="font-size: 16px; color: #555555; line-height: 1.6">
<p style="margin-bottom: 30px">
Hi <strong>${payload?.name || ""}</strong>,
</p>
<hr>
<div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;">
&copy; {{year}} Your Company. All rights reserved.
${payload?.htmlBody}
<div 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"
/>
<p style="font-size: 12px">The Support Team</p>
<h3>Quick Launch</h3>
</div>
</div>
<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.
<br />
If you need assistance, feel free to contact our support team.
<br /><br />
Thank you for choosing us!
</p>
<hr />
<div
style="
text-align: center;
font-size: 12px;
color: #999999;
margin-top: 20px;
"
>
&copy; 2026 to {{year}} Quick Launch. All rights reserved.
</div>
</div>
</body>
</body>
</html>
`,
});
+1
View File
@@ -1,6 +1,7 @@
import app from "./app";
import { configs } from "./app/configs/index";
import { prisma } from "./app/lib/prisma";
import "./app/queues/worker";
async function main() {
try {