From 7e2b044e1a8018ed87b8e9f73404a70de3834758 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:29:08 +0300 Subject: [PATCH 01/19] feat: add queue types for email jobs --- src/types/index.ts | 1 + src/types/queue.ts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 src/types/queue.ts diff --git a/src/types/index.ts b/src/types/index.ts index c7bdc74..80131bb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,4 +1,5 @@ export * from './jwt'; +export * from './queue'; export * from './status'; export * from './statusCodes'; export * from './user'; diff --git a/src/types/queue.ts b/src/types/queue.ts new file mode 100644 index 0000000..4b38a58 --- /dev/null +++ b/src/types/queue.ts @@ -0,0 +1,17 @@ +export type SendEmailVerificationJob = { + email: string; + name: string; + token: string; +}; + +export type SendForgetPasswordJob = { + email: string; + name: string; + otp: string; +}; + +export type EmailJobData = SendEmailVerificationJob | SendForgetPasswordJob; + +export type EmailJobName = + | 'SEND_VERIFICATION_EMAIL' + | 'SEND_FORGET_PASSWORD_EMAIL'; From 96f927d56def96266fc7d2b522edbfa70b28ac95 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:29:29 +0300 Subject: [PATCH 02/19] refactor: update MAGIC_NUMBERS and add QUEUES and WORKERS constants --- src/utils/constants.ts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b9b405c..60c9df2 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -34,18 +34,18 @@ export const MAGIC_NUMBERS = { ONE_MINUTE_IN_MILLISECONDS: 60 * 1000, ONE_DAY_IN_MILLISECONDS: 24 * 60 * 60 * 1000, ONE_WEEK_IN_MILLISECONDS: 7 * 24 * 60 * 60 * 1000, - FIFTEEN_MINUTES_IN_MILLISECONDS: 15 * 60 * 1000, + FIVE_SECONDS_IN_MILLISECONDS: 5 * 1000, FIVE_MINUTES_IN_MILLISECONDS: 5 * 60 * 1000, + ONE_HOUR_IN_SECONDS: 60 * 60, ONE_DAY_IN_SECONDS: 24 * 60 * 60, FIVE_MINUTES_IN_SECONDS: 5 * 60, - FIFTEEN_MINUTES_IN_SECONDS: 15 * 60, MAX_FILE_SIZE: 5 * 1024 * 1024, - OTP_LENGTH: 6, - NUMBER_OF_BYTES: 32, + MAX_NUMBER_OF_RETRIES: 5, + MAX_NUMBER_OF_CONCURRENT_JOBS: 5, + MAX_COUNT_FOR_REMOVE_ON_FAILURE: 100, MAX_NUMBER_OF_ALLOWED_REQUESTS: { ONE: 1, THREE: 3, - FIVE: 5, TEN: 10, }, }; @@ -63,3 +63,12 @@ export const DEFAULT_VALUES = { 'Add your top three priorities for the week and start managing your time effectively', }, }; + +export const QUEUES = { + EMAIL_QUEUE: 'EMAIL_QUEUE', +}; + +export const WORKERS = { + SEND_VERIFICATION_EMAIL: 'SEND_VERIFICATION_EMAIL', + SEND_FORGET_PASSWORD_EMAIL: 'SEND_FORGET_PASSWORD_EMAIL', +}; From 210830abaf91bc2e32d05ebc29f9163589591828 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:29:50 +0300 Subject: [PATCH 03/19] feat: add email queue implementation --- src/queues/email.queue.ts | 8 ++++++++ src/queues/index.ts | 1 + 2 files changed, 9 insertions(+) create mode 100644 src/queues/email.queue.ts diff --git a/src/queues/email.queue.ts b/src/queues/email.queue.ts new file mode 100644 index 0000000..2dc6147 --- /dev/null +++ b/src/queues/email.queue.ts @@ -0,0 +1,8 @@ +import { Queue } from 'bullmq'; +import { redisHost, redisPort } from '../config'; +import { EmailJobData } from '../types'; +import { QUEUES } from '../utils'; + +export const emailQueue = new Queue(QUEUES.EMAIL_QUEUE, { + connection: { host: redisHost, port: Number(redisPort) }, +}); diff --git a/src/queues/index.ts b/src/queues/index.ts index 3159904..a186cb8 100644 --- a/src/queues/index.ts +++ b/src/queues/index.ts @@ -1 +1,2 @@ export * from './competition.queue'; +export * from './email.queue'; From a8d6784432c05aa886dc7b7852850fcde7656572 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:30:13 +0300 Subject: [PATCH 04/19] feat: implement email worker for handling email jobs --- src/workers/email.worker.ts | 83 +++++++++++++++++++++++++++++++++++++ src/workers/index.ts | 1 + 2 files changed, 84 insertions(+) create mode 100644 src/workers/email.worker.ts diff --git a/src/workers/email.worker.ts b/src/workers/email.worker.ts new file mode 100644 index 0000000..2c05685 --- /dev/null +++ b/src/workers/email.worker.ts @@ -0,0 +1,83 @@ +import { Job, Worker } from 'bullmq'; +import 'dotenv/config'; +import { redisHost, redisPort } from '../config'; +import { emailService } from '../services'; +import { + EmailJobData, + EmailJobName, + SendEmailVerificationJob, + SendForgetPasswordJob, +} from '../types'; +import { logger, MAGIC_NUMBERS, QUEUES, WORKERS } from '../utils'; + +export const emailWorker = new Worker( + QUEUES.EMAIL_QUEUE, + async (job: Job) => { + switch (job.name) { + case WORKERS.SEND_VERIFICATION_EMAIL: + { + const data = job.data as SendEmailVerificationJob; + await emailService.sendVerificationEmail( + data.email, + data.name, + data.token, + ); + } + break; + + case WORKERS.SEND_FORGET_PASSWORD_EMAIL: + { + const data = job.data as SendForgetPasswordJob; + await emailService.sendForgetPasswordEmail( + data.email, + data.name, + data.otp, + ); + } + break; + + default: + logger.warn(`No handler for job name: ${job.name}`); + break; + } + }, + { + connection: { host: redisHost, port: Number(redisPort) }, + concurrency: MAGIC_NUMBERS.MAX_NUMBER_OF_CONCURRENT_JOBS, + }, +); + +emailWorker.on('completed', (job) => { + logger.info(`Job ${job.id} has been completed ✅`); +}); + +emailWorker.on('failed', (job: Job | undefined, err) => { + const message = err && err.message ? err.message : err; + if (job) { + logger.error(`Job ${job.id} has failed ❌ with error: ${message}`); + } else { + logger.error(`Job is undefined ❌ with error: ${message}`); + } +}); + +emailWorker.on('error', (err) => { + logger.error(`Email worker encountered an error: ${err?.message ?? err}`); +}); + +const shutdown = (signal: string) => { + logger.info(`Received ${signal}. Shutting down email worker...`); + emailWorker + .close() + .then(() => { + process.exit(0); + }) + .catch((err) => { + logger.error( + `Error while shutting down email worker: ${err?.message ?? err}`, + ); + process.exit(1); + }); +}; + +process.on('SIGINT', () => shutdown('SIGINT')); +process.on('SIGTERM', () => shutdown('SIGTERM')); diff --git a/src/workers/index.ts b/src/workers/index.ts index c665ac8..61c1551 100644 --- a/src/workers/index.ts +++ b/src/workers/index.ts @@ -1 +1,2 @@ export * from './competition.worker'; +export * from './email.worker'; From 6c28f70f875bf78c598dc469d9a6b3a27b736d6d Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:30:44 +0300 Subject: [PATCH 05/19] feat: handling email verification and password reset jobs --- src/jobs/email.job.ts | 61 +++++++++++++++++++++++++++++++++++++++++++ src/jobs/index.ts | 1 + 2 files changed, 62 insertions(+) create mode 100644 src/jobs/email.job.ts create mode 100644 src/jobs/index.ts diff --git a/src/jobs/email.job.ts b/src/jobs/email.job.ts new file mode 100644 index 0000000..eff7628 --- /dev/null +++ b/src/jobs/email.job.ts @@ -0,0 +1,61 @@ +import { JobsOptions } from 'bullmq'; +import { emailQueue } from '../queues'; +import { SendEmailVerificationJob, SendForgetPasswordJob } from '../types'; +import { logger, MAGIC_NUMBERS, WORKERS } from '../utils'; + +export class EmailJob { + static async addVerificationEmailJob( + verificationJobDetails: SendEmailVerificationJob, + ) { + try { + const key = `verify-${verificationJobDetails.email}-${Date.now()}`; + const jobOptions = this.createJobOptions(key); + await emailQueue.add( + WORKERS.SEND_VERIFICATION_EMAIL, + { + email: verificationJobDetails.email, + name: verificationJobDetails.name, + token: verificationJobDetails.token, + }, + jobOptions, + ); + } catch (error) { + logger.error(`Failed to add verification email job: ${error}`); + throw error; + } + } + + static async addForgetPasswordEmailJob( + forgetPasswordDetails: SendForgetPasswordJob, + ) { + try { + const key = `forget-${forgetPasswordDetails.email}-${Date.now()}`; + const jobOptions = this.createJobOptions(key); + await emailQueue.add( + WORKERS.SEND_FORGET_PASSWORD_EMAIL, + { + email: forgetPasswordDetails.email, + name: forgetPasswordDetails.name, + otp: forgetPasswordDetails.otp, + }, + jobOptions, + ); + } catch (error) { + logger.error(`Failed to add forget password email job: ${error}`); + throw error; + } + } + + private static createJobOptions(jobId: string): JobsOptions { + return { + jobId, + attempts: MAGIC_NUMBERS.MAX_NUMBER_OF_RETRIES, + backoff: { + type: 'fixed', + delay: MAGIC_NUMBERS.FIVE_SECONDS_IN_MILLISECONDS, + }, + removeOnComplete: 100, + removeOnFail: 100, + }; + } +} diff --git a/src/jobs/index.ts b/src/jobs/index.ts new file mode 100644 index 0000000..b80fcd9 --- /dev/null +++ b/src/jobs/index.ts @@ -0,0 +1 @@ +export * from './email.job'; From c5ec0c16135297e354bf29c169892432da5c3879 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:31:13 +0300 Subject: [PATCH 06/19] refactor: enhance error handling and logging in email service methods --- src/services/email.service.ts | 84 ++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 31 deletions(-) diff --git a/src/services/email.service.ts b/src/services/email.service.ts index 641eb9f..eb11c4f 100644 --- a/src/services/email.service.ts +++ b/src/services/email.service.ts @@ -4,6 +4,7 @@ import { getOTPTemplate, getVerifyEmailTemplate, getWinnersTemplate, + logger, } from '../utils'; import { smtpMailProvider } from './providers'; @@ -11,43 +12,64 @@ export class EmailService { constructor(private readonly mailProvider: IMailProvider) {} async sendVerificationEmail(email: string, name: string, token: string) { - const verifyEmailTemplate = getVerifyEmailTemplate(); - const verifyEmailUrl = `${frontendUrl}/verify-email?token=${token}`; - - const html = verifyEmailTemplate - .replace(/{{verifyEmailUrl}}/g, verifyEmailUrl) - .replace(/{{name}}/g, name); - - await this.mailProvider.sendEmail({ - to: email, - subject: 'Verify your email', - text: 'Verify your email', - html, - }); + try { + const verifyEmailTemplate = getVerifyEmailTemplate(); + const verifyEmailUrl = `${frontendUrl}/verify-email?token=${token}`; + + const html = verifyEmailTemplate + .replace(/{{verifyEmailUrl}}/g, verifyEmailUrl) + .replace(/{{name}}/g, name); + + await this.mailProvider.sendEmail({ + to: email, + subject: 'Verify your email', + text: 'Verify your email', + html, + }); + + logger.info(`Verification email sent to ${email}`); + } catch (error) { + logger.error(`Failed to send verification email: ${error}`); + throw error; + } } async sendForgetPasswordEmail(email: string, name: string, otp: string) { - const html = getOTPTemplate() - .replace(/{{otp}}/g, otp) - .replace(/{{name}}/g, name); - - await this.mailProvider.sendEmail({ - to: email, - subject: 'Reset your password', - text: 'Reset your password', - html, - }); + try { + const html = getOTPTemplate() + .replace(/{{otp}}/g, otp) + .replace(/{{name}}/g, name); + + await this.mailProvider.sendEmail({ + to: email, + subject: 'Reset your password', + text: 'Reset your password', + html, + }); + + logger.info(`Forget password email sent to ${email}`); + } catch (error) { + logger.error(`Failed to send forget password email: ${error}`); + throw error; + } } async notifyWinnerViaEmail(email: string, name: string) { - const html = getWinnersTemplate().replace(/{{name}}/g, name); - - await this.mailProvider.sendEmail({ - to: email, - subject: 'Congratulations! You are a winner', - text: 'Congratulations! You are a winner', - html, - }); + try { + const html = getWinnersTemplate().replace(/{{name}}/g, name); + + await this.mailProvider.sendEmail({ + to: email, + subject: 'Congratulations! You are a winner', + text: 'Congratulations! You are a winner', + html, + }); + + logger.info(`Winner notification email sent to ${email}`); + } catch (error) { + logger.error(`Failed to send winner notification email: ${error}`); + throw error; + } } } From c2cfb451ba93cfaac5ebeea2df06d7d3020c6a97 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:31:23 +0300 Subject: [PATCH 07/19] refactor: replace email service calls with email job implementations for verification and password reset --- src/services/auth.service.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index 0141386..7fa7eba 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,6 +1,5 @@ import { Provider } from '@prisma/client'; import { - emailService, HashingService, JwtService, otpService, @@ -9,6 +8,7 @@ import { userService, } from '.'; import { IAuth, IResetPassword, IUser, IVerifyOtp } from '../interfaces'; +import { EmailJob } from '../jobs'; import { IUserInfo } from '../types'; import { ApiError, @@ -82,11 +82,11 @@ export class AuthService extends BaseAuthService { MAGIC_NUMBERS.ONE_DAY_IN_SECONDS, ); - emailService.sendVerificationEmail( - user.email, - user.name, - verificationToken, - ); + await EmailJob.addVerificationEmailJob({ + email: user.email, + name: user.name, + token: verificationToken, + }); } async login(userLoginInfo: IAuth) { @@ -272,7 +272,11 @@ export class AuthService extends BaseAuthService { ), ]); - emailService.sendForgetPasswordEmail(user.email, user.name, otp); + await EmailJob.addForgetPasswordEmailJob({ + email: user.email, + name: user.name, + otp, + }); } async verifyOTP(otpInfo: IVerifyOtp) { From aea1f51a1edc3a0c62116705370b67dca482ff4e Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:31:38 +0300 Subject: [PATCH 08/19] chore: add start:worker script to run email worker --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 58af820..149e145 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "post:build": "cpy src/templates/**/*.html dist/templates", "dev": "concurrently \"tsc-watch --onSuccess 'node ./dist/index.js'\" \"rm -rf dist\" \"npm run build\"", "start": "NODE_ENV=production node dist/index.js", + "start:worker": "node dist/workers/email.worker.js", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", "lint": "eslint \"src/**/*.ts\" --fix", From b135d4fdb2761d07f29af6822286a469f22cbb17 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:31:57 +0300 Subject: [PATCH 09/19] refactor: improve deployment script by adding email worker --- deploy.sh | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/deploy.sh b/deploy.sh index 10a81ed..64663f7 100755 --- a/deploy.sh +++ b/deploy.sh @@ -15,13 +15,21 @@ npx prisma generate --schema=src/database/prisma/schema.prisma echo "🏗 Building the app..." npm run build -echo "🚀 Restarting app with PM2..." +echo "🚀 Restarting apps with PM2..." if pm2 describe taskora-api > /dev/null 2>&1; then - echo "📍 Restarting existing process..." + echo "📍 Restarting taskora-api..." pm2 restart taskora-api else - echo "🆕 Starting new process with ecosystem config..." - pm2 start ecosystem.config.js + echo "🆕 Starting taskora-api from ecosystem config..." + pm2 start ecosystem.config.js --only taskora-api +fi + +if pm2 describe taskora-email-worker > /dev/null 2>&1; then + echo "📍 Restarting taskora-email-worker..." + pm2 restart taskora-email-worker +else + echo "🆕 Starting taskora-email-worker from ecosystem config..." + pm2 start ecosystem.config.js --only taskora-email-worker fi echo "💾 Saving PM2 process list..." From 31cb9e05cf298f9d98a32fb7dae2748dd0a688c0 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:32:05 +0300 Subject: [PATCH 10/19] chore: update ecosystem config to include email worker with proper script path --- ecosystem.config.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ecosystem.config.js b/ecosystem.config.js index 92bd1a8..8176c69 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -2,7 +2,7 @@ module.exports = { apps: [ { name: 'taskora-api', - script: './dist/index.js', + script: 'dist/index.js', instances: 1, exec_mode: 'fork', node_args: '--expose-gc --max-old-space-size=1024', @@ -15,5 +15,16 @@ module.exports = { kill_timeout: 5000, watch: false, }, + { + name: 'taskora-email-worker', + script: 'dist/workers/email.worker.js', + instances: 1, + autorestart: true, + watch: false, + env: { + NODE_ENV: 'production', + FRONTEND_URL: 'https://taskora.live', + }, + }, ], }; From 466d738c877f56547906f244e29ba70a6f8e696a Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:32:13 +0300 Subject: [PATCH 11/19] docs: mark integration with BullMQ for background job processing as complete --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 20a0323..f8bc24d 100644 --- a/TODO.md +++ b/TODO.md @@ -29,6 +29,7 @@ - [x] OTP expiration must be less than 15m. Decide on the exact duration (e.g., 5m) - [ ] Add middleware to check if user is deleted - [ ] Centralize error messages +- [x] Integrate with BullMQ for background job processing ## Improvements From 5cbff71bd43202ae6c8c50809c4d094306e228fa Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:40:57 +0300 Subject: [PATCH 12/19] refactor: replace magic numbers with constants --- src/jobs/email.job.ts | 4 ++-- src/utils/constants.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/jobs/email.job.ts b/src/jobs/email.job.ts index eff7628..602fb5c 100644 --- a/src/jobs/email.job.ts +++ b/src/jobs/email.job.ts @@ -54,8 +54,8 @@ export class EmailJob { type: 'fixed', delay: MAGIC_NUMBERS.FIVE_SECONDS_IN_MILLISECONDS, }, - removeOnComplete: 100, - removeOnFail: 100, + removeOnComplete: MAGIC_NUMBERS.MAX_COUNT_FOR_REMOVE_ON_COMPLETE, + removeOnFail: MAGIC_NUMBERS.MAX_COUNT_FOR_REMOVE_ON_FAILURE, }; } } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 60c9df2..1ad0e59 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -42,6 +42,7 @@ export const MAGIC_NUMBERS = { MAX_FILE_SIZE: 5 * 1024 * 1024, MAX_NUMBER_OF_RETRIES: 5, MAX_NUMBER_OF_CONCURRENT_JOBS: 5, + MAX_COUNT_FOR_REMOVE_ON_COMPLETE: 100, MAX_COUNT_FOR_REMOVE_ON_FAILURE: 100, MAX_NUMBER_OF_ALLOWED_REQUESTS: { ONE: 1, From f2c0f276ad3e7aca2a86882ae2fbccea47b4f475 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:47:27 +0300 Subject: [PATCH 13/19] refactor: update EmailJobName type to use WORKERS constant --- src/types/queue.ts | 6 +++--- src/utils/constants.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/types/queue.ts b/src/types/queue.ts index 4b38a58..3d8d8a9 100644 --- a/src/types/queue.ts +++ b/src/types/queue.ts @@ -1,3 +1,5 @@ +import { WORKERS } from '../utils'; + export type SendEmailVerificationJob = { email: string; name: string; @@ -12,6 +14,4 @@ export type SendForgetPasswordJob = { export type EmailJobData = SendEmailVerificationJob | SendForgetPasswordJob; -export type EmailJobName = - | 'SEND_VERIFICATION_EMAIL' - | 'SEND_FORGET_PASSWORD_EMAIL'; +export type EmailJobName = (typeof WORKERS)[keyof typeof WORKERS]; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 1ad0e59..b800667 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -72,4 +72,4 @@ export const QUEUES = { export const WORKERS = { SEND_VERIFICATION_EMAIL: 'SEND_VERIFICATION_EMAIL', SEND_FORGET_PASSWORD_EMAIL: 'SEND_FORGET_PASSWORD_EMAIL', -}; +} as const; From d173f48267677a1bb0092cf4b199aa54b2b8e9e3 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 18:55:34 +0300 Subject: [PATCH 14/19] refactor: enhance error logging for email sending and job handling --- src/services/email.service.ts | 10 +++++++--- src/workers/email.worker.ts | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/services/email.service.ts b/src/services/email.service.ts index eb11c4f..278ab9b 100644 --- a/src/services/email.service.ts +++ b/src/services/email.service.ts @@ -29,7 +29,7 @@ export class EmailService { logger.info(`Verification email sent to ${email}`); } catch (error) { - logger.error(`Failed to send verification email: ${error}`); + logger.error(`Failed to send verification email to ${email}: ${error}`); throw error; } } @@ -49,7 +49,9 @@ export class EmailService { logger.info(`Forget password email sent to ${email}`); } catch (error) { - logger.error(`Failed to send forget password email: ${error}`); + logger.error( + `Failed to send forget password email to ${email}: ${error}`, + ); throw error; } } @@ -67,7 +69,9 @@ export class EmailService { logger.info(`Winner notification email sent to ${email}`); } catch (error) { - logger.error(`Failed to send winner notification email: ${error}`); + logger.error( + `Failed to send winner notification email to ${email}: ${error}`, + ); throw error; } } diff --git a/src/workers/email.worker.ts b/src/workers/email.worker.ts index 2c05685..4996e5f 100644 --- a/src/workers/email.worker.ts +++ b/src/workers/email.worker.ts @@ -37,7 +37,9 @@ export const emailWorker = new Worker( break; default: - logger.warn(`No handler for job name: ${job.name}`); + logger.warn( + `Unknown job name: ${job.name}. Expected: ${Object.values(WORKERS).join(', ')}`, + ); break; } }, From 05f6e70fa56af9e4948096c276d8b3272bda004e Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 19:18:54 +0300 Subject: [PATCH 15/19] refactor: implement email hashing for job keys and enhance logging --- src/jobs/email.job.ts | 19 +++++++++++++++++-- src/services/hashing.service.ts | 9 +++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/jobs/email.job.ts b/src/jobs/email.job.ts index 602fb5c..4dbcfd8 100644 --- a/src/jobs/email.job.ts +++ b/src/jobs/email.job.ts @@ -1,5 +1,6 @@ import { JobsOptions } from 'bullmq'; import { emailQueue } from '../queues'; +import { HashingService } from '../services'; import { SendEmailVerificationJob, SendForgetPasswordJob } from '../types'; import { logger, MAGIC_NUMBERS, WORKERS } from '../utils'; @@ -8,7 +9,8 @@ export class EmailJob { verificationJobDetails: SendEmailVerificationJob, ) { try { - const key = `verify-${verificationJobDetails.email}-${Date.now()}`; + const hashedEmail = this.hashEmail(verificationJobDetails.email); + const key = `verify-${hashedEmail}`; const jobOptions = this.createJobOptions(key); await emailQueue.add( WORKERS.SEND_VERIFICATION_EMAIL, @@ -19,6 +21,10 @@ export class EmailJob { }, jobOptions, ); + + logger.info( + `Verification email job scheduled for ${verificationJobDetails.email} with key ${key}`, + ); } catch (error) { logger.error(`Failed to add verification email job: ${error}`); throw error; @@ -29,7 +35,8 @@ export class EmailJob { forgetPasswordDetails: SendForgetPasswordJob, ) { try { - const key = `forget-${forgetPasswordDetails.email}-${Date.now()}`; + const hashedEmail = this.hashEmail(forgetPasswordDetails.email); + const key = `forget-${hashedEmail}`; const jobOptions = this.createJobOptions(key); await emailQueue.add( WORKERS.SEND_FORGET_PASSWORD_EMAIL, @@ -40,6 +47,10 @@ export class EmailJob { }, jobOptions, ); + + logger.info( + `Forget password email job scheduled for ${forgetPasswordDetails.email} with key ${key}`, + ); } catch (error) { logger.error(`Failed to add forget password email job: ${error}`); throw error; @@ -58,4 +69,8 @@ export class EmailJob { removeOnFail: MAGIC_NUMBERS.MAX_COUNT_FOR_REMOVE_ON_FAILURE, }; } + + private static hashEmail(email: string): string { + return HashingService.generateHashWithHmac(email); + } } diff --git a/src/services/hashing.service.ts b/src/services/hashing.service.ts index aac3aad..828c071 100644 --- a/src/services/hashing.service.ts +++ b/src/services/hashing.service.ts @@ -1,4 +1,5 @@ import { compare, hash } from 'bcryptjs'; +import crypto from 'crypto'; import { bcryptSaltRounds } from '../config'; import { ApiError, BAD_REQUEST } from '../utils'; @@ -9,10 +10,18 @@ export class HashingService { } return await hash(text, Number(bcryptSaltRounds)); } + static compare(text: string, hashedText: string): Promise { if (!text || !hashedText) { throw new ApiError('Missing information to compare', BAD_REQUEST); } return compare(text, hashedText); } + + static generateHashWithHmac(text: string) { + return crypto + .createHmac('sha256', bcryptSaltRounds) + .update(text) + .digest('hex'); + } } From e4765037d9ceb9888e123faf00f449dc8ab5e39d Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 19:28:26 +0300 Subject: [PATCH 16/19] refactor: improve graceful shutdown process for email worker --- src/workers/email.worker.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/workers/email.worker.ts b/src/workers/email.worker.ts index 4996e5f..cc3c62d 100644 --- a/src/workers/email.worker.ts +++ b/src/workers/email.worker.ts @@ -66,20 +66,20 @@ emailWorker.on('error', (err) => { logger.error(`Email worker encountered an error: ${err?.message ?? err}`); }); -const shutdown = (signal: string) => { - logger.info(`Received ${signal}. Shutting down email worker...`); - emailWorker - .close() - .then(() => { - process.exit(0); - }) - .catch((err) => { - logger.error( - `Error while shutting down email worker: ${err?.message ?? err}`, - ); - process.exit(1); - }); +const shutdown = async (signal: string) => { + logger.info(`Received ${signal}. Shutting down email worker gracefully...`); + + try { + await emailWorker.pause(true); + logger.info('Worker paused. Waiting for active jobs to finish...'); + + await emailWorker.close(); + logger.info('Email worker shutdown complete ✅'); + } catch (err) { + logger.error(`Error during graceful shutdown: ${err}`); + process.exit(1); + } }; -process.on('SIGINT', () => shutdown('SIGINT')); -process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => void shutdown('SIGINT')); +process.on('SIGTERM', () => void shutdown('SIGTERM')); From 309dc7006c5e856f9cd55eebb37dffb94b16bc4f Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 19:30:58 +0300 Subject: [PATCH 17/19] refactor: add HMAC secret to environment configuration and update hashing service --- .env.example | 3 +++ src/config/general.env.ts | 1 + src/services/hashing.service.ts | 7 ++----- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 02b2c2b..c1d5d73 100644 --- a/.env.example +++ b/.env.example @@ -21,6 +21,9 @@ REFRESH_TOKEN_EXPIRY= # Bcrypt Config BCRYPT_SALT_ROUNDS= +# HMAC Config +HMAC_SECRET= + # Google OAuth Config GOOGLE_CLIENT_ID= GOOGLE_CLIENT_SECRET= diff --git a/src/config/general.env.ts b/src/config/general.env.ts index 583560e..a073468 100644 --- a/src/config/general.env.ts +++ b/src/config/general.env.ts @@ -14,3 +14,4 @@ export const mailPort = env('MAIL_PORT'); export const mailAuthUser = env('MAIL_AUTH_USER'); export const mailAuthPassword = env('MAIL_AUTH_PASSWORD'); export const correctAnswer = env('CORRECT_ANSWER'); +export const hmacSecret = env('HMAC_SECRET'); diff --git a/src/services/hashing.service.ts b/src/services/hashing.service.ts index 828c071..84f03d9 100644 --- a/src/services/hashing.service.ts +++ b/src/services/hashing.service.ts @@ -1,6 +1,6 @@ import { compare, hash } from 'bcryptjs'; import crypto from 'crypto'; -import { bcryptSaltRounds } from '../config'; +import { bcryptSaltRounds, hmacSecret } from '../config'; import { ApiError, BAD_REQUEST } from '../utils'; export class HashingService { @@ -19,9 +19,6 @@ export class HashingService { } static generateHashWithHmac(text: string) { - return crypto - .createHmac('sha256', bcryptSaltRounds) - .update(text) - .digest('hex'); + return crypto.createHmac('sha256', hmacSecret).update(text).digest('hex'); } } From a436c2b1fbbe567bc752f35e49d8c2d186a78d7a Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 19:41:08 +0300 Subject: [PATCH 18/19] refactor: change job backoff strategy to exponential for better retry handling --- src/jobs/email.job.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jobs/email.job.ts b/src/jobs/email.job.ts index 4dbcfd8..8c7095c 100644 --- a/src/jobs/email.job.ts +++ b/src/jobs/email.job.ts @@ -62,7 +62,7 @@ export class EmailJob { jobId, attempts: MAGIC_NUMBERS.MAX_NUMBER_OF_RETRIES, backoff: { - type: 'fixed', + type: 'exponential', delay: MAGIC_NUMBERS.FIVE_SECONDS_IN_MILLISECONDS, }, removeOnComplete: MAGIC_NUMBERS.MAX_COUNT_FOR_REMOVE_ON_COMPLETE, From 5c3be694452f897e6d3c6064d9b3d970bf045073 Mon Sep 17 00:00:00 2001 From: MuhammedMagdyy Date: Mon, 1 Sep 2025 20:07:15 +0300 Subject: [PATCH 19/19] refactor: replace logger warning with error throw for unknown job names --- src/workers/email.worker.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/workers/email.worker.ts b/src/workers/email.worker.ts index cc3c62d..39efab3 100644 --- a/src/workers/email.worker.ts +++ b/src/workers/email.worker.ts @@ -37,10 +37,9 @@ export const emailWorker = new Worker( break; default: - logger.warn( + throw new Error( `Unknown job name: ${job.name}. Expected: ${Object.values(WORKERS).join(', ')}`, ); - break; } }, {