From 960a7b999405cb1580276232d33a316ac826c4a0 Mon Sep 17 00:00:00 2001 From: hbapte Date: Thu, 11 Jul 2024 18:04:05 +0200 Subject: [PATCH] fix password expiry check --- src/helpers/notifications.ts | 60 +++++++------- src/helpers/passwordExpiryNotifications.ts | 80 ++++++------------- src/index.ts | 17 ++-- src/middlewares/passwordExpiryCheck.ts | 5 -- .../auth/controller/authControllers.ts | 4 +- src/modules/cart/test/cart.spec.ts | 2 +- .../user/controller/userControllers.ts | 2 + src/routes/userRouter.ts | 4 +- src/services/notificationSocket.ts | 15 ++-- src/services/sendEmail.ts | 4 +- 10 files changed, 87 insertions(+), 106 deletions(-) diff --git a/src/helpers/notifications.ts b/src/helpers/notifications.ts index 013987cb..3657222b 100644 --- a/src/helpers/notifications.ts +++ b/src/helpers/notifications.ts @@ -12,66 +12,68 @@ import { io } from "../index"; export const eventEmitter = new EventEmitter(); const fetchProductWithShop = async (productId: string): Promise => { - return (await Products.findOne({ - where: { id: productId }, - include: { model: Shops, as: "shops" } - })) as IProductsWithShop; + return (await Products.findOne({ + where: { id: productId }, + include: { model: Shops, as: "shops" } + })) as IProductsWithShop; }; - + +const saveAndEmitNotification = async (userId: string, message: string, event: string) => { + await userRepositories.addNotification(userId, message); + await sendEmailNotification(userId, message); + io.to(userId).emit(event, message); +}; + eventEmitter.on("productAdded", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = `Product ${product.name} has been added.`; - await userRepositories.addNotification(userId, message); - await sendEmailNotification(userId, message); - io.to(userId).emit("productAdded", message); + await saveAndEmitNotification(userId, message, "productAdded"); }); - + eventEmitter.on("productRemoved", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = "A Product has been removed in your shop."; - await userRepositories.addNotification(userId, message); - await sendEmailNotification(userId, message); - io.to(userId).emit("productRemoved", message); + await saveAndEmitNotification(userId, message, "productRemoved"); }); - + eventEmitter.on("productExpired", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = `Product ${product.name} has expired.`; - await userRepositories.addNotification(userId, message); - sendEmailNotification(userId, message); - io.to(userId).emit("productExpired", message); + await saveAndEmitNotification(userId, message, "productExpired"); }); - + eventEmitter.on("productUpdated", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = `Product ${product.name} has been updated.`; - await userRepositories.addNotification(userId, message); - await sendEmailNotification(userId, message); - io.to(userId).emit("productUpdated", message); + await saveAndEmitNotification(userId, message, "productUpdated"); }); - + eventEmitter.on("productStatusChanged", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = `Product ${product.name} status changed to ${product.status}.`; - await userRepositories.addNotification(userId, message); - await sendEmailNotification(userId, message); - io.to(userId).emit("productStatusChanged", message); + await saveAndEmitNotification(userId, message, "productStatusChanged"); }); eventEmitter.on("productBought", async (product) => { const productWithShop = await fetchProductWithShop(product.id); const userId = productWithShop.shops.userId; const message = `Product ${product.name} has been bought.`; - await userRepositories.addNotification(userId, message); - await sendEmailNotification(userId, message); - io.to(userId).emit("productBought", message); + await saveAndEmitNotification(userId, message, "productBought"); }); - + +eventEmitter.on("passwordChanged", async ({ userId, message }) => { + await saveAndEmitNotification(userId, message, "passwordChanged"); +}); + +eventEmitter.on("passwordExpiry", async ({ userId, message }) => { + await saveAndEmitNotification(userId, message, "passwordExpiry"); +}); + cron.schedule("0 0 * * *", async () => { const users = await Users.findAll(); for (const user of users) { @@ -80,4 +82,4 @@ cron.schedule("0 0 * * *", async () => { eventEmitter.emit("productExpired", product); } } -}); \ No newline at end of file +}); diff --git a/src/helpers/passwordExpiryNotifications.ts b/src/helpers/passwordExpiryNotifications.ts index 32a79372..0a093c5a 100644 --- a/src/helpers/passwordExpiryNotifications.ts +++ b/src/helpers/passwordExpiryNotifications.ts @@ -1,12 +1,12 @@ +// src/helpers/passwordExpiryNotifications.ts import { Op } from "sequelize"; import Users from "../databases/models/users"; -import { sendEmail } from "../services/sendEmail"; +import { eventEmitter } from "./notifications"; const PASSWORD_EXPIRATION_MINUTES = Number(process.env.PASSWORD_EXPIRATION_MINUTES) || 90; -const WARNING_MINUTES_TEN = 10; -const WARNING_MINUTES_FIVE = 5; -const EXPIRATION_GRACE_PERIOD_MINUTES = 2; -const PASSWORD_RESET_URL = `${process.env.SERVER_URL_PRO}/api/auth/forget-password`; +const EXPIRATION_GRACE_PERIOD_MINUTES = 1; + +const WARNING_INTERVALS = [10,5,4,3,2,1]; const subtractMinutes = (date: Date, minutes: number) => { const result = new Date(date); @@ -25,50 +25,27 @@ export const checkPasswordExpirations = async () => { const now = new Date(); try { - const usersToWarnTenMinutes = await Users.findAll({ - where: { - passwordUpdatedAt: { - [Op.between]: [ - subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - WARNING_MINUTES_TEN), - subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - WARNING_MINUTES_TEN - 1) - ] - }, - isVerified: true, - status: "enabled" - } - }); + for (const interval of WARNING_INTERVALS) { + const usersToWarn = await Users.findAll({ + where: { + passwordUpdatedAt: { + [Op.between]: [ + subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - interval), + subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - interval - 1) + ] + }, + isVerified: true, + status: "enabled" + } + }); - for (const user of usersToWarnTenMinutes) { - const salutation = getSalutation(user.lastName); - const emailMessage = `${salutation}, your password will expire in 10 minutes. Please update your password to continue using the E-commerce Ninja. You can reset your password using the following link: ${PASSWORD_RESET_URL}`; - await sendEmail(user.email, "Password Expiration Warning", emailMessage) - .then(() => console.log(`10-minute warning sent to user: ${user.email}`)) - .catch((err) => - console.error(`Failed to send 10-minute warning to ${user.email}:`, err.message) - ); - } - - const usersToWarnFiveMinutes = await Users.findAll({ - where: { - passwordUpdatedAt: { - [Op.between]: [ - subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - WARNING_MINUTES_FIVE), - subtractMinutes(now, PASSWORD_EXPIRATION_MINUTES - WARNING_MINUTES_FIVE - 1) - ] - }, - isVerified: true, - status: "enabled" + for (const user of usersToWarn) { + const salutation = getSalutation(user.lastName); + const emailMessage = `${salutation}, your password will expire in ${interval} minutes. Please update your password to continue using the platform.`; + eventEmitter.emit("passwordExpiry", { userId: user.id, message: emailMessage, minutes: interval }); } - }); - for (const user of usersToWarnFiveMinutes) { - const salutation = getSalutation(user.lastName); - const emailMessage = `${salutation}, your password will expire in 5 minutes. Please update your password to continue using the E-commerce Ninja. You can reset your password using the following link: ${PASSWORD_RESET_URL}`; - await sendEmail(user.email, "Password Expiration Warning", emailMessage) - // .then(() => console.log(`5-minute warning sent to user: ${user.email}`)) - .catch((err) => - console.error(`Failed to send 5-minute warning to ${user.email}:`, err.message) - ); + // console.log(`${usersToWarn.length} users warned for ${interval}-minute password expiration.`); } const usersToNotifyExpired = await Users.findAll({ @@ -86,17 +63,12 @@ export const checkPasswordExpirations = async () => { for (const user of usersToNotifyExpired) { const salutation = getSalutation(user.lastName); - const emailMessage = `${salutation}, your password has expired. Please update your password to continue using the E-commerce Ninja. You can reset your password using the following link: ${PASSWORD_RESET_URL}`; - await sendEmail(user.email, "Password Expiration Notice", emailMessage) - // .then(() => console.log(`Expiration notice sent to user: ${user.email}`)) - .catch((err) => - console.error(`Failed to send expiration notice to ${user.email}:`, err.message) - ); + const emailMessage = `${salutation}, your password has expired. Please update your password to continue using the platform.`; + eventEmitter.emit("passwordExpiry", { userId: user.id, message: emailMessage, minutes: 0 }); } - // console.log(`${usersToWarnTenMinutes.length} users warned for 10-minute password expiration.`); - // console.log(`${usersToWarnFiveMinutes.length} users warned for 5-minute password expiration.`); // console.log(`${usersToNotifyExpired.length} users notified for password expiration.`); + } catch (error) { console.error("Error checking password expiration:", error); } diff --git a/src/index.ts b/src/index.ts index 2161102d..b36327ee 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -import express, { Express, Request, Response ,NextFunction} from "express"; +import express, { Express, Request, Response, NextFunction } from "express"; import dotenv from "dotenv"; import morgan from "morgan"; import compression from "compression"; @@ -10,23 +10,28 @@ import httpStatus from "http-status"; import chat from "./services/chat"; import { createServer } from "http"; import { Server } from "socket.io"; -import "./services/cronJob" +import "./services/cronJob"; import setupSocket from "./services/notificationSocket"; dotenv.config(); const app: Express = express(); -const PORT = process.env.PORT +const PORT = process.env.PORT; const server = createServer(app); +const allowedOrigins = ["http://localhost:5000" , "https://e-commerce-ninja-fn-staging.netlify.app"]; + export const io = new Server(server, { cors: { - origin: "*", - methods: ["GET", "POST"] + origin: allowedOrigins, + methods: ["GET", "POST"], + credentials: true } }); + chat(io); setupSocket(io); + app.use((req: Request, res: Response, next: NextFunction) => { if (req.originalUrl === "/api/cart/webhook") { express.raw({ type: "application/json" })(req, res, next); @@ -34,6 +39,7 @@ app.use((req: Request, res: Response, next: NextFunction) => { express.json()(req, res, next); } }); + app.use(morgan(process.env.NODE_EN)); app.use(compression()); app.use(cors()); @@ -48,7 +54,6 @@ app.get("**", (req: Request, res: Response) => { }); }); - server.listen(PORT, () => { console.log(`Server is running on the port ${PORT}`); }); diff --git a/src/middlewares/passwordExpiryCheck.ts b/src/middlewares/passwordExpiryCheck.ts index c9b717ca..cc2f3fb2 100644 --- a/src/middlewares/passwordExpiryCheck.ts +++ b/src/middlewares/passwordExpiryCheck.ts @@ -16,15 +16,12 @@ const addMinutes = (date: Date, minutes: number): Date => { return result; }; - const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next: NextFunction) => { try { const user = await Users.findByPk(req.user.id); const now = new Date(); const passwordExpirationDate = addMinutes(user.passwordUpdatedAt, PASSWORD_EXPIRATION_MINUTES); const minutesRemaining = Math.floor((passwordExpirationDate.getTime() - now.getTime()) / (1000 * 60)); - console.log(`Password expiration in ${minutesRemaining} minutes.`); - if (minutesRemaining <= 0) { await sendEmail( @@ -33,7 +30,6 @@ const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next `Your password has expired. Please reset your password using the following link: ${PASSWORD_RESET_URL}` ); - return res.status(httpStatus.FORBIDDEN).json({ status: httpStatus.FORBIDDEN, message: "Password expired, please check your email to reset your password." @@ -42,7 +38,6 @@ const checkPasswordExpiration = async (req: ExtendedRequest, res: Response, next res.setHeader("Password-Expiry-Notification", `Your password will expire in ${minutesRemaining} minutes. Please update your password.`); } - next(); } catch (error: any) { res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ diff --git a/src/modules/auth/controller/authControllers.ts b/src/modules/auth/controller/authControllers.ts index 1fe52d43..b0a72df8 100644 --- a/src/modules/auth/controller/authControllers.ts +++ b/src/modules/auth/controller/authControllers.ts @@ -6,6 +6,7 @@ import httpStatus from "http-status"; import { usersAttributes } from "../../../databases/models/users"; import authRepositories from "../repository/authRepositories"; import { sendEmail } from "../../../services/sendEmail"; +import { eventEmitter } from "../../../helpers/notifications"; const registerUser = async (req: Request, res: Response): Promise => { try { @@ -129,7 +130,8 @@ const forgetPassword = async (req: any, res: Response): Promise => { const resetPassword = async (req: any, res: Response): Promise => { try { - await authRepositories.updateUserByAttributes("password", req.user.password, "id", req.user.id); + await authRepositories.updateUserByAttributes("password", req.user.password, "id", req.user.id); + eventEmitter.emit("passwordChanged", { userId: req.user.id, message: "Password changed successfully" }); res.status(httpStatus.OK).json({status: httpStatus.OK, message: "Password reset successfully." }); } catch (error) { res.status(httpStatus.INTERNAL_SERVER_ERROR).json({ error: error.message }); diff --git a/src/modules/cart/test/cart.spec.ts b/src/modules/cart/test/cart.spec.ts index 939d4293..0a6c1faf 100644 --- a/src/modules/cart/test/cart.spec.ts +++ b/src/modules/cart/test/cart.spec.ts @@ -611,7 +611,7 @@ describe("Payment Controller", () => { sandbox .stub(cartRepositories, "findCartIdbyUserId") .throws(new Error("Database error")); - console.log(await cartController.buyerPayCart(req, res)); + // console.log(await cartController.buyerPayCart(req, res)); expect(res.status).to.have.been.calledWith(httpStatus.INTERNAL_SERVER_ERROR); }); }); diff --git a/src/modules/user/controller/userControllers.ts b/src/modules/user/controller/userControllers.ts index 8ad8495b..09a2ea85 100644 --- a/src/modules/user/controller/userControllers.ts +++ b/src/modules/user/controller/userControllers.ts @@ -7,6 +7,7 @@ import uploadImages from "../../../helpers/uploadImage"; import userRepositories from "../repository/userRepositories"; import authRepositories from "../../auth/repository/authRepositories"; import { sendEmail } from "../../../services/sendEmail"; +import { eventEmitter } from "../../../helpers/notifications"; const adminGetUsers = async (req: Request, res: Response) => { try { @@ -120,6 +121,7 @@ const changePassword = async (req: any, res: Response) => { "id", req.user.id ); + eventEmitter.emit("passwordChanged", { userId: req.user.id, message: "Password changed successfully" }); return res .status(httpStatus.OK) .json({ message: "Password updated successfully", data: { user: user } }); diff --git a/src/routes/userRouter.ts b/src/routes/userRouter.ts index 73c228ed..6de39bcf 100644 --- a/src/routes/userRouter.ts +++ b/src/routes/userRouter.ts @@ -16,8 +16,8 @@ import upload from "../helpers/multer"; router.put("/user-update-profile", userAuthorization(["admin", "buyer", "seller"]), upload.single("profilePicture"), validation(userSchema), userControllers.updateUserProfile); router.put("/change-password", userAuthorization(["admin", "buyer", "seller"]), validation(changePasswordSchema), credential, userControllers.changePassword); -router.get("/user-get-notifications", userAuthorization(["seller"]), isNotificationsExist, userControllers.getAllNotifications); -router.get("/user-get-notification/:id", userAuthorization(["seller"]),isNotificationsExist, userControllers.getSingleNotification); +router.get("/user-get-notifications", userAuthorization(["seller", "buyer"]), isNotificationsExist, userControllers.getAllNotifications); +router.get("/user-get-notification/:id", userAuthorization(["seller", "buyer"]),isNotificationsExist, userControllers.getSingleNotification); router.put("/user-mark-notification/:id", userAuthorization(["seller"]), isNotificationsExist, userControllers.markNotificationAsRead); router.put("/user-mark-all-notifications", userAuthorization(["seller"]), isNotificationsExist, userControllers.markAllNotificationsAsRead); diff --git a/src/services/notificationSocket.ts b/src/services/notificationSocket.ts index 48630479..8c30ecbc 100644 --- a/src/services/notificationSocket.ts +++ b/src/services/notificationSocket.ts @@ -1,15 +1,20 @@ import { Server } from "socket.io"; +import jwt, { JwtPayload } from "jsonwebtoken"; const setupSocket = (io: Server) => { io.on("connection", (socket) => { - - socket.on("join", (userId) => { - socket.join(userId); + socket.on("join", (token) => { + try { + const decoded = jwt.verify(token, process.env.JWT_SECRET) as JwtPayload; + const userId = decoded.id; + socket.join(userId); + } catch (error) { + console.error("Invalid token"); + } }); - socket.on("disconnect", () => { }); }); }; -export default setupSocket; \ No newline at end of file +export default setupSocket; diff --git a/src/services/sendEmail.ts b/src/services/sendEmail.ts index b256dce4..9a44bdad 100644 --- a/src/services/sendEmail.ts +++ b/src/services/sendEmail.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Request, Response } from "express"; import nodemailer, { SendMailOptions } from "nodemailer"; import dotenv from "dotenv"; import authRepository from "../modules/auth/repository/authRepositories"; @@ -36,7 +34,7 @@ const sendEmailNotification = async (userId: string, message: string) => { const mailOptions: SendMailOptions = { from: process.env.MAIL_ID, to: user.email, - subject: "Product Notification", + subject: "Ninja E-commerce", text: message };