Skip to content

Commit

Permalink
Merge pull request #59 from atlp-rwanda/187419196-prompt-to-change-pa…
Browse files Browse the repository at this point in the history
…ssword

feat(change password):user should be prompted to change their password every x amount of time
  • Loading branch information
teerenzo authored May 23, 2024
2 parents c709966 + 6332f57 commit 3d86200
Show file tree
Hide file tree
Showing 25 changed files with 215 additions and 58 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ POSTGRES_USER = database user
POSTGRES_DB = name of your database
POSTGRES_HOST = your database host
DOCKER_DB_CONNECTION = postgres://postgres:postgres_password@database_host:postgres_port/postgres_db
TIME_FOR_PASSWORD_EXPIRATION = days --> password expiration time
4 changes: 3 additions & 1 deletion __test__/product.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ expect(response.body).toEqual({
test("Return 500 for handle error", async () => {
const response = await request(app)
.get("/api/v1/products/review")
.set("Authorization", "Bearer " + token);
expect(response.status).toBe(500)
})
test('It should return status 200 for removed Product',async() =>{
Expand Down Expand Up @@ -453,7 +454,8 @@ test('It should return status 200 for removed category',async() =>{
expect(response.status).toBe(200);
});
test("return status 200 when none seller role search products", async () => {
const response = await request(app).get("/api/v1/products/search").send(searchProduct);
const response = await request(app).get("/api/v1/products/search").send(searchProduct)
.set("Authorization", "Bearer " + buyerToken);
expect(response.status).toBe(200);
});
test("it should return status product is not available when searching product", async () => {
Expand Down
9 changes: 6 additions & 3 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import request from "supertest";
import { mocked } from "jest-mock";
import { beforeAll, beforeEach, afterEach, afterAll, test } from "@jest/globals";
import app from "../src/utils/server";
import User from "../src/sequelize/models/users";
Expand All @@ -25,14 +26,15 @@ const userData: any = {
username: "testuser5",
email: "[email protected]",
password: "test12345",
lastPasswordUpdateTime: new Date()
};


const dummySeller = {
name: "dummy1234",
username: "username1234",
email: "[email protected]",
password: "1234567890",
lastPasswordUpdateTime: "3000, 11, 18"
};
const userTestData = {
newPassword: "Test@123",
Expand Down Expand Up @@ -62,7 +64,8 @@ const updateData:any = {
country: "Rwanda",
}


jest.mock('../src/jobs/isPasswordExpired');

describe("Testing user Routes", () => {
beforeAll(async () => {
try {
Expand Down Expand Up @@ -103,7 +106,6 @@ describe("Testing user Routes", () => {
const response = await request(app)
.post("/api/v1/users/register")
.send(userData);

expect(response.status).toBe(201);
}, 20000);

Expand Down Expand Up @@ -225,6 +227,7 @@ describe("Testing user Routes", () => {
roleId: 2,
})
.set("Authorization", "Bearer " + adminToken);

expect(response.status).toBe(200);

});
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,12 @@
"cryptr": "^6.3.0",
"dotenv": "^16.4.5",
"email-validator": "^2.0.4",
"events": "^3.3.0",
"express": "^4.19.2",
"express-session": "^1.18.0",
"husky": "^9.0.11",
"ioredis": "^5.4.1",
"jest-mock": "^29.7.0",
"joi": "^17.13.0",
"jsonwebtoken": "^9.0.2",
"lint-staged": "^15.2.2",
Expand Down
11 changes: 8 additions & 3 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import { updateUserRoleService } from "../services/user.service";
import { generateRandomNumber } from "../utils/generateRandomNumber";
import { env } from "../utils/env";
import { Emailschema, resetPasswordSchema } from "../schemas/resetPasswordSchema";
import Joi from "joi";
import { clearExpiredUserData } from "../jobs/isPasswordExpired";
import { use } from "passport";


export const fetchAllUsers = async (req: Request, res: Response) => {
Expand Down Expand Up @@ -95,8 +96,9 @@ export const createUserController = async (req: Request, res: Response) => {
const { name, email, username, password, role } = req.body;

try {
let currentUpdateTime = new Date();
const { name, email, username, password } = req.body;
const user = await createUserService(name, email, username, password);
const user = await createUserService(name, email, username, password,currentUpdateTime);
if (!user || user == null) {
return res.status(409).json({
status: 409,
Expand Down Expand Up @@ -136,9 +138,12 @@ export const updatePassword = async (req: Request, res: Response) => {
}

const password = await hashedPassword(newPassword);
const currentUpdateTime = new Date();
// @ts-ignore
const update = await updateUserPassword(user, password);
const update = await updateUserPassword(user, password,currentUpdateTime);
if(update){
//@ts-ignore
clearExpiredUserData(user.id)
return res.status(200).json({ message: "Password updated successfully" });
}
} catch (err: any) {
Expand Down
57 changes: 57 additions & 0 deletions src/email-templates/passwordExpiredNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const passwordExpirationHtmlContent = (userName: string): string => {
const htmlContent = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Updated Confirmation</title>
<style>
body {
font-family: Arial, sans-serif;
line-height: 1.6;
background-color: #f5f5f5;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background-color: #fff;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
}
p {
color: #666;
}
.footer {
margin-top: 20px;
text-align: center;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<h1>Password Expiration </h1>
<p>Dear ${userName},</p>
<p>Your password has been Expired.
Vist our website to update it to continue using the system.</p>
<p>Thank you.</p>
<p style="color: #666;">Your Website Team</p>
<div class="footer">
<p>This is an automated email. Please do not reply.</p>
</div>
</div>
</body>
</html>
`;
return htmlContent;
};

export { passwordExpirationHtmlContent };

42 changes: 42 additions & 0 deletions src/jobs/isPasswordExpired.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import cron from 'node-cron';
import EventEmitter from 'events';
import { getAllUsers } from '../services/user.service';
import { env } from '../utils/env';
import { sendEmailService } from '../services/mail.service';
import { passwordExpirationHtmlContent } from '../email-templates/passwordExpiredNotification';

let latestExpiredUserData = new Set<any>();
export let expiredUserData = new Set<any>();

class UpdatePasswordEventsEmitter extends EventEmitter {};
export const passwordEventEmitter = new UpdatePasswordEventsEmitter();

export const isPasswordExpired = () =>{
const millseconddPerMin = 1000 * 60;
cron.schedule('* * * * * *', async() => {
const currentTime = Date.now();
const users = await getAllUsers();
const emailPromises = [];
for (const user of users) {
const lastPasswordUpdateTime:any = user.dataValues.lastPasswordUpdateTime;
const timeDifference:any = currentTime - lastPasswordUpdateTime;
if(timeDifference >= (millseconddPerMin * parseInt(env.password_expiration_time))&& !latestExpiredUserData.has(user.id)){
passwordEventEmitter.emit("password expired",user);
latestExpiredUserData.add(user.id);
emailPromises.push(
sendEmailService(user, 'Password expired', passwordExpirationHtmlContent(user.name))
);
};
};
await Promise.all(emailPromises);
expiredUserData = new Set([...latestExpiredUserData]);
});
}
passwordEventEmitter.on("password expired", (user) => {
expiredUserData.add(user);
});

export const clearExpiredUserData = (userId:any) => {
latestExpiredUserData.delete(userId);
expiredUserData.delete(userId);
};
24 changes: 24 additions & 0 deletions src/middlewares/isPasswordOutOfDate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Request,Response,NextFunction } from 'express';
import { isLoggedIn } from './isLoggedIn';
import { expiredUserData } from '../jobs/isPasswordExpired';

export const isPasswordOutOfDate = async(req:Request,res:Response,next:NextFunction) =>{
try {
await isLoggedIn(req,res,() => {});
//@ts-ignore
const loggedInUserId: any = req.user.id;
const expiredUserIds = new Set([...expiredUserData].map((user: any) => user));
if (expiredUserIds.has(loggedInUserId)) {
return res.status(403).json({
message: "Your password expired, Update it to continue"
});
};
next();
} catch (error:any) {
console.log('Error has occured',error.message);
next(error)
}
}



12 changes: 6 additions & 6 deletions src/routes/cartRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { isLoggedIn } from "../middlewares/isLoggedIn";
import { isQuantityValid } from "../middlewares/isQuantityValid";
import { isProductFound } from "../middlewares/isProductFound";
import { validateCart, validateRemoveProductQty, validateUpdateProductQty } from "../middlewares/cartValidation";
import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate";

const cartRoutes = Router();

cartRoutes.get("/",isLoggedIn,viewUserCart);
cartRoutes.post("/",isLoggedIn,validateCart, isQuantityValid, addItemToCart);
// cartRoutes.post("/",isLoggedIn,validateCart, isQuantityValid, addItemToCart);
cartRoutes.put("/",isLoggedIn,validateRemoveProductQty, isProductFound,removeProductFromCart);
cartRoutes.delete("/",isLoggedIn, clearAllProductFromCart);
cartRoutes.patch("/",isLoggedIn,validateUpdateProductQty, isProductFound, updateProductQuantity);
cartRoutes.get("/",isLoggedIn,isPasswordOutOfDate,viewUserCart);
cartRoutes.post("/",isLoggedIn,isPasswordOutOfDate,validateCart, isQuantityValid, addItemToCart);
cartRoutes.put("/",isLoggedIn,isPasswordOutOfDate,validateRemoveProductQty, isProductFound,removeProductFromCart);
cartRoutes.delete("/",isLoggedIn,isPasswordOutOfDate, clearAllProductFromCart);
cartRoutes.patch("/",isLoggedIn,isPasswordOutOfDate,validateUpdateProductQty, isProductFound, updateProductQuantity);

export default cartRoutes;
11 changes: 6 additions & 5 deletions src/routes/categoriesRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {
import { categoriesDataSchema } from "../schemas/categorySchema";
import { isAseller } from "../middlewares/sellerAuth";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate";
const categoriesRouter = Router();
categoriesRouter.get("/",isLoggedIn,isAseller,fetchCategories);
categoriesRouter.get("/:id",isLoggedIn,isAseller,fetchSingleCategory);
categoriesRouter.post("/",isLoggedIn,isAseller,upload.single('image'),validateSchema(categoriesDataSchema)
categoriesRouter.get("/",isLoggedIn,isPasswordOutOfDate,isAseller,fetchCategories);
categoriesRouter.get("/:id",isLoggedIn,isPasswordOutOfDate,isAseller,fetchSingleCategory);
categoriesRouter.post("/",isLoggedIn,isPasswordOutOfDate,isAseller,upload.single('image'),validateSchema(categoriesDataSchema)
,addCategories);
categoriesRouter.patch("/:id",isAseller,upload.single('image'),categoriesUpdate);
categoriesRouter.delete("/:id",isLoggedIn,isAseller,removeCategories);
categoriesRouter.patch("/:id",isAseller,isPasswordOutOfDate,upload.single('image'),categoriesUpdate);
categoriesRouter.delete("/:id",isLoggedIn,isPasswordOutOfDate,isAseller,removeCategories);

export default categoriesRouter;
5 changes: 3 additions & 2 deletions src/routes/notificationRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Router } from "express";
import { getUserNotifications, readNotification } from "../controllers/notificationController";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate";


const notificationRoutes = Router()


notificationRoutes.get("/",isLoggedIn,getUserNotifications)
notificationRoutes.get("/:id",isLoggedIn,readNotification)
notificationRoutes.get("/",isLoggedIn,isPasswordOutOfDate,getUserNotifications)
notificationRoutes.get("/:id",isLoggedIn,isPasswordOutOfDate,readNotification)



Expand Down
3 changes: 2 additions & 1 deletion src/routes/paymentRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import * as paymentController from "../controllers/paymentController"
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { hasItemsInCart } from "../middlewares/payments";
import { isAbuyer } from "../middlewares/isAbuyer";
import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate";

const paymentRouter = express.Router()

paymentRouter.post('/checkout', isLoggedIn, isAbuyer, hasItemsInCart, paymentController.createCheckoutSession);
paymentRouter.post('/checkout', isLoggedIn,isPasswordOutOfDate, isAbuyer, hasItemsInCart, paymentController.createCheckoutSession);
paymentRouter.get('/success', paymentController.handleSuccess);
paymentRouter.get('/canceled', paymentController.handleFailure);

Expand Down
21 changes: 11 additions & 10 deletions src/routes/productsRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,21 @@ import { isCategoryExist } from "../middlewares/isCategoryExist";
import { addReviewController, deleteReviewController, getreviewController, updateReviewController} from "../controllers/productControllers"
import { addReviewValidate, updateReviewValidate } from "../schemas/review";
import { hasPurchasedProduct } from "../middlewares/hasPurchased";
import { isPasswordOutOfDate } from "../middlewares/isPasswordOutOfDate";

const productsRouter = Router();
productsRouter.get("/search", searchProductController)
productsRouter.get("/search",isPasswordOutOfDate, searchProductController)

productsRouter.get("/",fetchProducts);
productsRouter.get("/:id",fetchSingleProduct);
productsRouter.post("/",isLoggedIn,isAseller,upload.array('images'),
productsRouter.get("/",isLoggedIn,isPasswordOutOfDate,fetchProducts);
productsRouter.get("/:id",isLoggedIn,isPasswordOutOfDate,fetchSingleProduct);
productsRouter.post("/",isLoggedIn,isPasswordOutOfDate,isAseller,upload.array('images'),
validateSchema(productDataSchema),isCategoryExist,addProducts);
productsRouter.patch("/:id",isLoggedIn,isAseller,upload.array('images'),productsUpdate);
productsRouter.patch("/:id/status",isLoggedIn,isAseller,productAvailability);
productsRouter.delete("/:id",isLoggedIn,isAseller,removeProducts);
productsRouter.patch("/:id",isLoggedIn,isPasswordOutOfDate,isAseller,upload.array('images'),productsUpdate);
productsRouter.patch("/:id/status",isLoggedIn,isPasswordOutOfDate,isAseller,productAvailability);
productsRouter.delete("/:id",isLoggedIn,isPasswordOutOfDate,isAseller,removeProducts);

productsRouter.get("/:pid/reviews", getreviewController)
productsRouter.post("/:pid/reviews",isLoggedIn, validateSchema(addReviewValidate), hasPurchasedProduct, addReviewController)
productsRouter.delete("/:pid/reviews", isLoggedIn, deleteReviewController)
productsRouter.patch("/:pid/reviews", isLoggedIn, validateSchema(updateReviewValidate), updateReviewController)
productsRouter.post("/:pid/reviews",isLoggedIn,isPasswordOutOfDate,validateSchema(addReviewValidate), hasPurchasedProduct, addReviewController)
productsRouter.delete("/:pid/reviews", isLoggedIn,isPasswordOutOfDate, deleteReviewController)
productsRouter.patch("/:pid/reviews", isLoggedIn,isPasswordOutOfDate, validateSchema(updateReviewValidate), updateReviewController)
export default productsRouter;
9 changes: 5 additions & 4 deletions src/routes/roleRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { isLoggedIn } from '../middlewares/isLoggedIn';
import{isAdmin} from '../middlewares/isAdmin';
import { validateSchema } from '../middlewares/validator';
import {roleSchema} from '../schemas/roleSchema';
import { isPasswordOutOfDate } from '../middlewares/isPasswordOutOfDate';

const RoleRouter = express.Router();

RoleRouter.post('/', isLoggedIn, isAdmin, validateSchema(roleSchema), roleController.createRole);
RoleRouter.get('/', roleController.getRoles);
RoleRouter.patch('/:id', isLoggedIn, isAdmin, validateSchema(roleSchema),roleController.updateRole);
RoleRouter.delete('/:id', isLoggedIn, isAdmin, roleController.deleteRole);
RoleRouter.post('/', isLoggedIn,isPasswordOutOfDate, isAdmin, validateSchema(roleSchema), roleController.createRole);
RoleRouter.get('/',roleController.getRoles);
RoleRouter.patch('/:id', isLoggedIn,isPasswordOutOfDate, isAdmin, validateSchema(roleSchema),roleController.updateRole);
RoleRouter.delete('/:id', isLoggedIn,isPasswordOutOfDate, isAdmin, roleController.deleteRole);

export default RoleRouter;
Loading

0 comments on commit 3d86200

Please sign in to comment.