Skip to content

Commit

Permalink
fix password expiry check
Browse files Browse the repository at this point in the history
  • Loading branch information
hbapte committed Jul 11, 2024
1 parent ba7ca99 commit 960a7b9
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 106 deletions.
60 changes: 31 additions & 29 deletions src/helpers/notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,66 +12,68 @@ import { io } from "../index";
export const eventEmitter = new EventEmitter();

const fetchProductWithShop = async (productId: string): Promise<IProductsWithShop> => {
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) {
Expand All @@ -80,4 +82,4 @@ cron.schedule("0 0 * * *", async () => {
eventEmitter.emit("productExpired", product);
}
}
});
});
80 changes: 26 additions & 54 deletions src/helpers/passwordExpiryNotifications.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -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({
Expand All @@ -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);
}
Expand Down
17 changes: 11 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -10,30 +10,36 @@ 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);
} else {
express.json()(req, res, next);
}
});

app.use(morgan(process.env.NODE_EN));
app.use(compression());
app.use(cors());
Expand All @@ -48,7 +54,6 @@ app.get("**", (req: Request, res: Response) => {
});
});


server.listen(PORT, () => {
console.log(`Server is running on the port ${PORT}`);
});
Expand Down
5 changes: 0 additions & 5 deletions src/middlewares/passwordExpiryCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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."
Expand All @@ -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({
Expand Down
4 changes: 3 additions & 1 deletion src/modules/auth/controller/authControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> => {
try {
Expand Down Expand Up @@ -129,7 +130,8 @@ const forgetPassword = async (req: any, res: Response): Promise<void> => {

const resetPassword = async (req: any, res: Response): Promise<void> => {
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 });
Expand Down
2 changes: 1 addition & 1 deletion src/modules/cart/test/cart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Expand Down
2 changes: 2 additions & 0 deletions src/modules/user/controller/userControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 } });
Expand Down
4 changes: 2 additions & 2 deletions src/routes/userRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions src/services/notificationSocket.ts
Original file line number Diff line number Diff line change
@@ -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;
export default setupSocket;
4 changes: 1 addition & 3 deletions src/services/sendEmail.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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
};

Expand Down

0 comments on commit 960a7b9

Please sign in to comment.