From 3f0ead42651957d5aa7db14d0d2184c56167ebd1 Mon Sep 17 00:00:00 2001 From: dev-abumahid Date: Fri, 3 Apr 2026 00:54:46 +0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(account):=20update=20accou?= =?UTF-8?q?nt=20module=20structure=20and=20email=20queue=20processing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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. --- docker-compose.yml | 18 +++ package.json | 2 + .../migration.sql | 8 +- prisma/schema/account.schema.prisma | 18 +-- prisma/schema/profile.schema.prisma | 8 +- src/app/modules/account/account.controller.ts | 10 +- src/app/modules/account/account.route.ts | 2 +- src/app/modules/account/account.service.ts | 43 +++--- src/app/modules/account/account.swagger.ts | 2 +- src/app/modules/account/account.validation.ts | 8 +- src/app/queues/connection.ts | 6 + src/app/queues/email/email.processor.ts | 16 +++ src/app/queues/email/email.queue.ts | 16 +++ src/app/queues/email/email.worker.ts | 13 ++ src/app/queues/worker.ts | 3 + src/app/templates/otpTemplate.ts | 82 +++++++++++ src/app/utils/mail_sender.ts | 127 ++++++++++-------- src/server.ts | 1 + 18 files changed, 285 insertions(+), 98 deletions(-) rename prisma/migrations/{20260402152356_init => 20260402184951_init}/migration.sql (87%) create mode 100644 src/app/queues/connection.ts create mode 100644 src/app/queues/email/email.processor.ts create mode 100644 src/app/queues/email/email.queue.ts create mode 100644 src/app/queues/email/email.worker.ts create mode 100644 src/app/queues/worker.ts create mode 100644 src/app/templates/otpTemplate.ts diff --git a/docker-compose.yml b/docker-compose.yml index 6701a15..3b31902 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: \ No newline at end of file diff --git a/package.json b/package.json index 64ed3b9..f4b3d6c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/migrations/20260402152356_init/migration.sql b/prisma/migrations/20260402184951_init/migration.sql similarity index 87% rename from prisma/migrations/20260402152356_init/migration.sql rename to prisma/migrations/20260402184951_init/migration.sql index e5703e0..ce7d5b3 100644 --- a/prisma/migrations/20260402152356_init/migration.sql +++ b/prisma/migrations/20260402184951_init/migration.sql @@ -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") ); diff --git a/prisma/schema/account.schema.prisma b/prisma/schema/account.schema.prisma index 1c4fd01..675bb28 100644 --- a/prisma/schema/account.schema.prisma +++ b/prisma/schema/account.schema.prisma @@ -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? } diff --git a/prisma/schema/profile.schema.prisma b/prisma/schema/profile.schema.prisma index aaf92bb..ffca12e 100644 --- a/prisma/schema/profile.schema.prisma +++ b/prisma/schema/profile.schema.prisma @@ -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? } diff --git a/src/app/modules/account/account.controller.ts b/src/app/modules/account/account.controller.ts index 0f753fa..f0a467d 100644 --- a/src/app/modules/account/account.controller.ts +++ b/src/app/modules/account/account.controller.ts @@ -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 }; diff --git a/src/app/modules/account/account.route.ts b/src/app/modules/account/account.route.ts index 3f580d2..a8300d8 100644 --- a/src/app/modules/account/account.route.ts +++ b/src/app/modules/account/account.route.ts @@ -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", diff --git a/src/app/modules/account/account.service.ts b/src/app/modules/account/account.service.ts index 043a618..1ecb290 100644 --- a/src/app/modules/account/account.service.ts +++ b/src/app/modules/account/account.service.ts @@ -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: ` -

OTP ${newOtp}

- Otp will be expire in 5 minutes - -

- -

Or you can use Verification link

-

${verificationLink}

- `, - 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 }; diff --git a/src/app/modules/account/account.swagger.ts b/src/app/modules/account/account.swagger.ts index e47ded9..183c355 100644 --- a/src/app/modules/account/account.swagger.ts +++ b/src/app/modules/account/account.swagger.ts @@ -11,7 +11,7 @@ export const accountSwaggerDocs = { example: JSON.stringify({ email: "user@gmail.com", password: "password", - fullName: "User", + shopName: "User", }), }, }, diff --git a/src/app/modules/account/account.validation.ts b/src/app/modules/account/account.validation.ts index c4e38f6..5a99415 100644 --- a/src/app/modules/account/account.validation.ts +++ b/src/app/modules/account/account.validation.ts @@ -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"), diff --git a/src/app/queues/connection.ts b/src/app/queues/connection.ts new file mode 100644 index 0000000..dd536c2 --- /dev/null +++ b/src/app/queues/connection.ts @@ -0,0 +1,6 @@ +import { QueueOptions } from "bullmq"; + +export const redisConnection: QueueOptions["connection"] = { + host: "127.0.0.1", + port: 6379, +}; \ No newline at end of file diff --git a/src/app/queues/email/email.processor.ts b/src/app/queues/email/email.processor.ts new file mode 100644 index 0000000..496f4db --- /dev/null +++ b/src/app/queues/email/email.processor.ts @@ -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); +}; \ No newline at end of file diff --git a/src/app/queues/email/email.queue.ts b/src/app/queues/email/email.queue.ts new file mode 100644 index 0000000..31c25f4 --- /dev/null +++ b/src/app/queues/email/email.queue.ts @@ -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("email-queue", { + connection: redisConnection, +}); \ No newline at end of file diff --git a/src/app/queues/email/email.worker.ts b/src/app/queues/email/email.worker.ts new file mode 100644 index 0000000..89a66e2 --- /dev/null +++ b/src/app/queues/email/email.worker.ts @@ -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( + "email-queue", + async (job) => emailProcessor(job), + { + connection: redisConnection, + } +); \ No newline at end of file diff --git a/src/app/queues/worker.ts b/src/app/queues/worker.ts new file mode 100644 index 0000000..cc318ff --- /dev/null +++ b/src/app/queues/worker.ts @@ -0,0 +1,3 @@ +import "./email/email.worker"; + +console.log("Workers running..."); \ No newline at end of file diff --git a/src/app/templates/otpTemplate.ts b/src/app/templates/otpTemplate.ts new file mode 100644 index 0000000..ef9d04c --- /dev/null +++ b/src/app/templates/otpTemplate.ts @@ -0,0 +1,82 @@ +import { TEmailQueue } from "../queues/email/email.queue" + +export const otpTemplate = (payload: TEmailQueue) => { + return ` +
+ + + + +
+

+ Use the following One-Time Password (OTP) to complete your + verification: +

+ + +
+ + ${payload.otp} + +
+ +

+ This OTP will expire in 5 minutes. +

+ + +
+ + +

+ Or verify using this link: +

+ +

+ + ${payload.verificationLink} + +

+
+
+ ` +} \ No newline at end of file diff --git a/src/app/utils/mail_sender.ts b/src/app/utils/mail_sender.ts index 4e25247..57a5fe1 100644 --- a/src/app/utils/mail_sender.ts +++ b/src/app/utils/mail_sender.ts @@ -26,73 +26,94 @@ const sendMail = async (payload: TMailContent) => { subject: payload.subject, text: payload.textBody, html: ` - + - - - + + Welcome Email - + - + - - -
- -
-

Hi ${payload?.name || ""},

- - ${payload?.htmlBody} - -
- - - -

The Support Team

-

Company Name

-
-
-

- This is an automated message — please do not reply to this email. -
- If you need assistance, feel free to contact our support team. -

- Thank you for choosing us! + +

+
+

+ Hi ${payload?.name || ""},

-
-
- © {{year}} Your Company. All rights reserved. + ${payload?.htmlBody} + +
+ Quick Launch + +

The Support Team

+

Quick Launch

+
+

+ This is an automated message — please do not reply to this email. +
+ If you need assistance, feel free to contact our support team. +

+ Thank you for choosing us! +

+
+
+ © 2026 to {{year}} Quick Launch. All rights reserved. +
- - + + `, }); diff --git a/src/server.ts b/src/server.ts index 875d07d..b265e0d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -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 {