🔧 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:
@@ -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:
|
||||
@@ -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",
|
||||
|
||||
+6
-2
@@ -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")
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { QueueOptions } from "bullmq";
|
||||
|
||||
export const redisConnection: QueueOptions["connection"] = {
|
||||
host: "127.0.0.1",
|
||||
port: 6379,
|
||||
};
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,3 @@
|
||||
import "./email/email.worker";
|
||||
|
||||
console.log("Workers running...");
|
||||
@@ -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>
|
||||
`
|
||||
}
|
||||
@@ -26,13 +26,12 @@ 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;
|
||||
@@ -52,48 +51,70 @@ const sendMail = async (payload: TMailContent) => {
|
||||
}
|
||||
}
|
||||
</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>
|
||||
<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;">
|
||||
<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"
|
||||
/>
|
||||
|
||||
<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>
|
||||
<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;">
|
||||
<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>
|
||||
<br />
|
||||
If you need assistance, feel free to contact our support team.
|
||||
<br><br>
|
||||
<br /><br />
|
||||
Thank you for choosing us!
|
||||
</p>
|
||||
|
||||
<hr>
|
||||
<div style="text-align: center; font-size: 12px; color: #999999; margin-top: 20px;">
|
||||
© {{year}} Your Company. All rights reserved.
|
||||
<hr />
|
||||
<div
|
||||
style="
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #999999;
|
||||
margin-top: 20px;
|
||||
"
|
||||
>
|
||||
© 2026 to {{year}} Quick Launch. All rights reserved.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
`,
|
||||
});
|
||||
return info
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user