From 145cb1d29132f7f211254ba3bd113c51bcd82383 Mon Sep 17 00:00:00 2001
From: YvetteNyibuka <izanybukayvette@gmail.com>
Date: Tue, 23 Apr 2024 23:44:08 +0200
Subject: [PATCH] feat(Reset-password):User who forgot password can reset it
 via email

- User who forgot password can request resetting it
- Sending reset-password email containing link along with token to reset password
- Reset password using the provided token
- Token is used only once

[Delivers #187419058]
---
 .env-example                                  |  18 +-
 .github/workflows/node.js.yml                 |  13 +-
 package-lock.json                             |  14 -
 package.json                                  |   5 +-
 src/__test__/product.test.ts                  |  15 -
 src/__test__/users.test.ts                    | 333 +++++++++++++-----
 src/controllers/resetPasswort.ts              | 108 ++++++
 src/controllers/userController.ts             |  20 +-
 src/database/config/db.config.ts              |  24 +-
 .../20240429163608-reset-password-tokens.js   |  35 ++
 src/database/models/resetPassword.ts          |  33 ++
 src/documention/user/index.ts                 |  58 +++
 src/helpers/nodemailer.ts                     |  35 ++
 src/helpers/security.helpers.ts               |   5 +
 src/middlewares/auth.ts                       |  22 +-
 src/middlewares/user.middleware.ts            |  37 ++
 src/mock/static.ts                            |  14 +
 src/routes/userRoutes.ts                      |  11 +
 src/services/mailService.ts                   |  25 --
 src/utils/keys.ts                             |   5 +-
 src/validations/newPassword.validations.ts    |  14 +
 src/validations/reset.validation.ts           |  15 +
 22 files changed, 666 insertions(+), 193 deletions(-)
 delete mode 100644 src/__test__/product.test.ts
 create mode 100644 src/controllers/resetPasswort.ts
 create mode 100644 src/database/migrations/20240429163608-reset-password-tokens.js
 create mode 100644 src/database/models/resetPassword.ts
 create mode 100644 src/helpers/nodemailer.ts
 delete mode 100644 src/services/mailService.ts
 create mode 100644 src/validations/newPassword.validations.ts
 create mode 100644 src/validations/reset.validation.ts

diff --git a/.env-example b/.env-example
index 773acc4a..6b66471f 100644
--- a/.env-example
+++ b/.env-example
@@ -14,15 +14,9 @@ GOOGLE_SECRET_ID=
 GOOGLE_CALLBACK_URL=
 SESSION_SECRET=
 
-
-JWT_SECRET=
-
-
-BASE_URL = < your BASE_URL>
-HOST = < your Host >
-SERVICE = < your SERVICE >
-
-
-EMAIL=<your email>
-PASSWORD=<email password>
-
+ACCESS_TOKEN_SECRET=
+SENDER_NAME=
+EMAIL=
+PASSWORD=
+ACCESS_TOKEN_SECRET=
+SENDGRID_API_KEY=<YOUR_GRID_API_KEY>
\ No newline at end of file
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 73e8f295..6fe2265e 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -1,5 +1,4 @@
 name: build
-
 on:
   push:
     branches:
@@ -9,7 +8,6 @@ on:
     branches:
       - develop
       - "*"
-
 jobs:
   test:
     runs-on: ubuntu-latest
@@ -25,16 +23,19 @@ jobs:
       DB_HOSTED_MODE: ${{ secrets.DB_HOSTED_MODE }}
       ACCESS_TOKEN_SECRET: ${{ secrets.ACCESS_TOKEN_SECRET }}
       SESSION_SECRET: ${{ secrets.SESSION_SECRET }}
-      JWT_SECRET: ${{ secrets.JWT_SECRET }}
       CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
       COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
       SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
       SENDER_NAME: ${{ secrets.SENDER_NAME }}
+      GOOGLE_CALLBACK_URL: ${{ secrets.GOOGLE_CALLBACK_URL }}
+      GOOGLE_SECRET_ID: ${{ secrets.GOOGLE_SECRET_ID }}
+      DB_PROD_URL: ${{ secrets.DB_PROD_URL }}
+      DB_DEV_URL: ${{ secrets.DB_DEV_URL }}
+      GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
 
     strategy:
       matrix:
         node-version: ["20.x"]
-
     steps:
       - uses: actions/checkout@v4
       - name: Use Node.js ${{ matrix.node-version }}
@@ -44,7 +45,6 @@ jobs:
           cache: "npm"
       - name: Install dependencies
         run: npm install
-
       - name: Run tests
         run: npm run test
 
@@ -54,14 +54,11 @@ jobs:
           curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
           chmod +x ./cc-test-reporter
           ./cc-test-reporter before-build
-
       - name: Store coverage report
         if: always()
         run: mkdir -p coverage
-
       - name: Send coverage report to Code Climate
         if: always()
         run: ./cc-test-reporter after-build -t lcov -p coverage
-
       - name: coveralls
         run: npx coveralls < coverage/lcov.info
diff --git a/package-lock.json b/package-lock.json
index 3ffce87d..949bb66b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4845,20 +4845,6 @@
 			"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
 			"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
 		},
-		"node_modules/fsevents": {
-			"version": "2.3.3",
-			"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
-			"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
-			"dev": true,
-			"hasInstallScript": true,
-			"optional": true,
-			"os": [
-				"darwin"
-			],
-			"engines": {
-				"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
-			}
-		},
 		"node_modules/function-bind": {
 			"version": "1.1.2",
 			"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
diff --git a/package.json b/package.json
index 8083269b..64f7c549 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,10 @@
 			"/src/utils/token.validation.ts",
 			"/src/services/mailService.ts",
 			"/src/middlewares/passport.ts",
-			"/src/database/config/db.config.ts"
+			"/src/database/config/db.config.ts",
+			"/src/utils",
+			"src/helpers",
+			"src/documention/index.ts"
 		]
 	},
 	"devDependencies": {
diff --git a/src/__test__/product.test.ts b/src/__test__/product.test.ts
deleted file mode 100644
index 0e5ade84..00000000
--- a/src/__test__/product.test.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import app from "../app";
-import request from "supertest";
-// import { connectionToDatabase } from "../database/config/db.config";
-
-jest.setTimeout(30000);
-
-describe("PRODUCT API TEST", () => {
-	// beforeAll(async () => {
-	//   await connectionToDatabase();
-	// });
-
-	it("Seller should create a product", async () => {
-		// Your test implementation goes here
-	});
-});
diff --git a/src/__test__/users.test.ts b/src/__test__/users.test.ts
index 3b7b8d5e..499aa6c9 100644
--- a/src/__test__/users.test.ts
+++ b/src/__test__/users.test.ts
@@ -4,6 +4,10 @@ import { connectionToDatabase } from "../database/config/db.config";
 import { deleteTableData } from "../utils/database.utils";
 import { User } from "../database/models/User";
 import { Token } from "../database/models/token";
+import { forgotPassword } from "../controllers/resetPasswort";
+import { resetPasswort } from "../controllers/resetPasswort";
+import { Request } from "express";
+
 import {
 	bad_two_factor_authentication_data,
 	login_user,
@@ -13,8 +17,13 @@ import {
 	partial_two_factor_authentication_data,
 	two_factor_authentication_data,
 	user_bad_request,
+	requestResetBody,
+	newPasswordBody,
+	NotUserrequestBody,
+	sameAsOldPassword,
 } from "../mock/static";
 import { generateAccessToken } from "../helpers/security.helpers";
+import { resetPassword } from "../database/models/resetPassword";
 
 jest.setTimeout(30000);
 
@@ -34,6 +43,8 @@ let token: string;
 
 const Jest_request = request(app.use(logErrors));
 
+let resetToken = "";
+
 describe("USER API TEST", () => {
 	beforeAll(async () => {
 		await connectionToDatabase();
@@ -43,6 +54,15 @@ describe("USER API TEST", () => {
 		await deleteTableData(User, "users");
 		await deleteTableData(Token, "tokens");
 	});
+	it("Welcome to Hacker's e-commerce backend and return 200", async () => {
+		const { body } = await Jest_request.get("/").expect(200);
+	});
+
+	it("should display login home page and return 200", async () => {
+		const { body } = await Jest_request.get("/api/v1/").expect(200);
+		expect(body.message).toBe("Welcome to Hacker's e-commerce backend!");
+	});
+
 	it("it should  register a user and return 201", async () => {
 		const { body } = await Jest_request.post("/api/v1/users/register")
 			.send(NewUser)
@@ -51,6 +71,7 @@ describe("USER API TEST", () => {
 		expect(body.message).toStrictEqual(
 			"Account Created successfully, Plase Verify your Account",
 		);
+
 		const tokenRecord = await Token.findOne();
 		token = tokenRecord?.dataValues.token ?? "";
 	});
@@ -68,13 +89,13 @@ describe("USER API TEST", () => {
 		expect(body.status).toStrictEqual("CONFLICT");
 		expect(body.message).toStrictEqual("User already exist!");
 	});
+
 	it("should verify a user's account and return 200", async () => {
 		// Assuming you have a way to create a user and a corresponding verification token
 
 		const { body } = await Jest_request.get(
 			`/api/v1/users/account/verify/${token}`,
 		);
-
 		expect(body.status).toStrictEqual(200);
 		expect(body.message).toStrictEqual("Email verified successfull");
 	});
@@ -92,7 +113,13 @@ describe("USER API TEST", () => {
 	 * ---------------------------- LOGIN --------------------------------------------
 	 */
 
-	it("should successfully login a user and return 202", async () => {
+	it("should successfully login a user and return 200", async () => {
+		await User.update(
+			{ isVerified: true },
+			{
+				where: { email: login_user.email },
+			},
+		);
 		const { body } = await Jest_request.post("/api/v1/users/login")
 			.send(login_user)
 			.expect(200);
@@ -124,106 +151,230 @@ describe("USER API TEST", () => {
 		expect(body.status).toStrictEqual("BAD REQUEST");
 		expect(body.message).toBeDefined();
 	});
-});
 
-/**
- * -----------------------------------------LOG OUT--------------------------------------
- */
+	/***
+	 * ---------------------------- RESET PASSWORD --------------------------------------------
+	 */
 
-it("Should log out a user and return 404", async () => {
-	const { body } = await Jest_request.post("/api/v1/users/logout").send();
-	expect(404);
-	expect(body.status).toStrictEqual("NOT FOUND");
-	expect(body.message).toStrictEqual("Token Not Found");
-});
+	it("it should return 200 when email is sent to user resetting password", async () => {
+		const authenticatetoken = generateAccessToken({
+			id: "1",
+			role: "seller",
+			email: NewUser.email,
+		});
+		await resetPassword.create({
+			resetToken: authenticatetoken,
+			email: NewUser.email,
+		});
+		const { body } = await Jest_request.post("/api/v1/users/forgot-password")
+			.send({ email: NewUser.email })
+			.expect(200);
+		resetToken = authenticatetoken;
+		expect(body.status).toStrictEqual("SUCCESS");
+		expect(body.message).toStrictEqual("Email sent successfully");
+	});
 
-/*
- * ---------------------------- TWO FACTOR AUTHENTICATION --------------------------------------------
- */
+	it("it should return 404 when user requesting reset is not found in database", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/forgot-password")
+			.send(NotUserrequestBody)
+			.expect(404);
 
-it("should authenticate the user and return SUCCESS", async () => {
-	const authenticatetoken = generateAccessToken({
-		id: "1",
-		role: "seller",
-		otp: two_factor_authentication_data.otp,
+		expect(body.message).toEqual("User not found");
 	});
-	const { body } = await Jest_request.post(
-		`/api/v1/users/2fa/${authenticatetoken}`,
-	)
-		.send(two_factor_authentication_data)
-		.expect(200);
 
-	expect(body.message).toStrictEqual("Account authentication successfully!");
-});
+	it("it should return 400 when email is not provided", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/forgot-password")
+			.send({})
+			.expect(400);
+	});
 
-it("should return 401 if user add invalid otp", async () => {
-	const authenticatetoken = generateAccessToken({
-		id: "1",
-		role: "seller",
-		otp: two_factor_authentication_data.otp,
-	});
-	const { body } = await Jest_request.post(
-		`/api/v1/users/2fa/${authenticatetoken}`,
-	)
-		.send(bad_two_factor_authentication_data)
-		.expect(401);
-	expect(body.response.message).toStrictEqual("Invalid One Time Password!!");
-});
+	it("it should return 200 when password reset successfully", async () => {
+		expect(resetToken).toBeDefined();
+		expect(resetToken).not.toEqual("");
+		const tokenRecord = await resetPassword.findOne();
+		const tokenn = tokenRecord?.dataValues.resetToken;
 
-it("should return 400 if user add with character < 6 invalid otp", async () => {
-	const authenticatetoken = generateAccessToken({
-		id: "1",
-		role: "seller",
-		otp: two_factor_authentication_data.otp,
-	});
-	const { body } = await Jest_request.post(
-		`/api/v1/users/2fa/${authenticatetoken}`,
-	)
-		.send(partial_two_factor_authentication_data)
-		.expect(400);
-	expect(body.message).toStrictEqual("OTP must be exactly 6 characters long!");
-});
+		const { body } = await Jest_request.post(
+			`/api/v1/users/reset-password/${tokenn}`,
+		)
+			.send(newPasswordBody)
+			.expect(200);
+	});
 
-it("should return 400 if user add with character < 6 invalid otp", async () => {
-	const authenticatetoken = generateAccessToken({
-		id: "1",
-		role: "seller",
-		otp: two_factor_authentication_data.otp,
-	});
-	const { body } = await Jest_request.post(
-		`/api/v1/users/2fa/${authenticatetoken}`,
-	)
-		.send({})
-		.expect(400);
-	expect(body.message).toStrictEqual("otp is required");
-});
+	it("should return 400 if no decoded token is found", async () => {
+		const response = await request(app)
+			.post(
+				"/api/v1/users/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRmYmM3NzM4LWE5YWItNDc2MC1hYzIxLWUzNTZkNGY0NDZjNyIsImVtYWlsIjoiaXphbnlpYnVrYXl2ZXR0ZTEwNUBnbWFpbC5jb20iLCJpYXQiOjE3MTQwNzcxOTksImV4cCI6MTcxNDE2MzU5OX0.wwtJXaviKcQYqmVX0LI0Yw1jG0wmBSqW4rHZA0Vh8zk",
+			)
+			.send({
+				password: "newPassword123",
+			});
 
-/**
- * -----------------------------------------LOG OUT--------------------------------------
- */
+		expect(404);
+		expect(response.body.message).toBe("Invalid link");
+	});
 
-it("Should log out a user and return 404", async () => {
-	const { body } = await Jest_request.post("/api/v1/users/logout").send();
-	expect(404);
-	expect(body.status).toStrictEqual("NOT FOUND");
-	expect(body.message).toStrictEqual("Token Not Found");
-});
+	it("it should return 400 when new password is the same to old password", async () => {
+		expect(resetToken).toBeDefined();
+		expect(resetToken).not.toEqual("");
+		const { body } = await Jest_request.post(
+			`/api/v1/users/reset-password/${resetToken}`,
+		)
+			.send(sameAsOldPassword)
+			.expect(400);
+	});
 
-it("Should log out a user and return 201", async () => {
-	const { body } = await Jest_request.post("/api/v1/users/logout")
-		.send()
-		.set("Authorization", `Bearer ${token}`);
-	expect(201);
-	expect(body.status).toStrictEqual("CREATED");
-	expect(body.message).toStrictEqual("Logged out successfully");
-	token = token;
-});
+	it("it should return 400 when invalid link is provided", async () => {
+		const { body } = await Jest_request.post(
+			`/api/v1/users/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjRmYmM3NzM4LWE5YWItNDc2MC1hYzIxLWUzNTZkNGY0NDZjNyIsImVtYWlsIjoiaXphbnlpYnVrYXl2ZXR0ZTEwNUBnbWFpbC5jb20iLCJpYXQiOjE3MTQwNzcxOTksImV4cCI6MTcxNDE2MzU5OX0.wwtJXaviKcQYqmVX0LI0Yw1jG0wmBSqW4rHZA0Vh8zk`,
+		)
+			.send(newPasswordBody)
+			.expect(400);
+	});
+
+	it("should return 400 when no token is provided", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/reset-password/")
+			.send(newPasswordBody)
+			.expect(404);
+	});
+
+	jest.mock("../helpers/nodemailer", () => ({
+		sendEmail: jest.fn(),
+	}));
+
+	it("should send an email with the correct mailOptions", async () => {
+		const req: any = {
+			body: { email: "test@example.com" },
+		} as any;
+
+		const res: any = {
+			status: jest.fn().mockReturnThis(),
+			json: jest.fn().mockReturnThis(),
+		};
+
+		await forgotPassword(req, res);
+	});
+
+	it("should return 500 if token is missing or invalid", async () => {
+		const req: any = {};
+
+		const res: any = {
+			status: jest.fn().mockReturnThis(),
+			json: jest.fn(),
+		};
+
+		await resetPasswort(req, res);
+
+		expect(res.status).toHaveBeenCalledWith(500);
+	});
+
+	jest.mock("../helpers/nodemailer", () => ({
+		sendEmail: jest.fn(),
+	}));
+
+	it("should send an email with the correct mailOptions", async () => {
+		const req: any = {
+			body: { email: "test@example.com" },
+		} as any;
+
+		const res: any = {
+			status: jest.fn().mockReturnThis(),
+			json: jest.fn().mockReturnThis(),
+		};
+
+		await forgotPassword(req, res);
+	});
+
+	/*
+	 * ---------------------------- TWO FACTOR AUTHENTICATION --------------------------------------------
+	 */
+
+	it("should authenticate the user and return SUCCESS", async () => {
+		const authenticatetoken = generateAccessToken({
+			id: "1",
+			role: "seller",
+			otp: two_factor_authentication_data.otp,
+		});
+		const { body } = await Jest_request.post(
+			`/api/v1/users/2fa/${authenticatetoken}`,
+		)
+			.send(two_factor_authentication_data)
+			.expect(200);
+
+		expect(body.message).toStrictEqual("Account authentication successfully!");
+	});
+
+	it("should return 401 if user add invalid otp", async () => {
+		const authenticatetoken = generateAccessToken({
+			id: "1",
+			role: "seller",
+			otp: two_factor_authentication_data.otp,
+		});
+		const { body } = await Jest_request.post(
+			`/api/v1/users/2fa/${authenticatetoken}`,
+		)
+			.send(bad_two_factor_authentication_data)
+			.expect(401);
+		expect(body.response.message).toStrictEqual("Invalid One Time Password!!");
+	});
+
+	it("should return 400 if user add with character < 6 invalid otp", async () => {
+		const authenticatetoken = generateAccessToken({
+			id: "1",
+			role: "seller",
+			otp: two_factor_authentication_data.otp,
+		});
+		const { body } = await Jest_request.post(
+			`/api/v1/users/2fa/${authenticatetoken}`,
+		)
+			.send(partial_two_factor_authentication_data)
+			.expect(400);
+		expect(body.message).toStrictEqual(
+			"OTP must be exactly 6 characters long!",
+		);
+	});
+
+	it("should return 400 if user add with character < 6 invalid otp", async () => {
+		const authenticatetoken = generateAccessToken({
+			id: "1",
+			role: "seller",
+			otp: two_factor_authentication_data.otp,
+		});
+		const { body } = await Jest_request.post(
+			`/api/v1/users/2fa/${authenticatetoken}`,
+		)
+			.send({})
+			.expect(400);
+		expect(body.message).toStrictEqual("otp is required");
+	});
 
-it("Should alert an error and return 401", async () => {
-	const { body } = await Jest_request.post("/api/v1/users/logout")
-		.send()
-		.set("Authorization", `Bearer ${token}`);
-	expect(401);
-	expect(body.status).toStrictEqual("UNAUTHORIZED");
-	expect(body.message).toStrictEqual("Already logged out");
+	/**
+	 * -----------------------------------------LOG OUT--------------------------------------
+	 */
+
+	it("Should log out a user and return 404", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/logout").send();
+		expect(404);
+		expect(body.status).toStrictEqual("NOT FOUND");
+		expect(body.message).toStrictEqual("Token Not Found");
+	});
+
+	it("Should log out a user and return 201", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/logout")
+			.send()
+			.set("Authorization", `Bearer ${token}`);
+		expect(201);
+		expect(body.status).toStrictEqual("CREATED");
+		expect(body.message).toStrictEqual("Logged out successfully");
+		token = token;
+	});
+
+	it("Should alert an error and return 401", async () => {
+		const { body } = await Jest_request.post("/api/v1/users/logout")
+			.send()
+			.set("Authorization", `Bearer ${token}`);
+		expect(401);
+		expect(body.status).toStrictEqual("UNAUTHORIZED");
+		expect(body.message).toStrictEqual("Already logged out");
+	});
 });
diff --git a/src/controllers/resetPasswort.ts b/src/controllers/resetPasswort.ts
new file mode 100644
index 00000000..54e0ad1d
--- /dev/null
+++ b/src/controllers/resetPasswort.ts
@@ -0,0 +1,108 @@
+import { Request, Response } from "express";
+import { User } from "../database/models/User";
+import { ACCESS_TOKEN_SECRET, PORT } from "../utils/keys";
+import { generateAccessToken } from "../helpers/security.helpers";
+import { resetPassword } from "../database/models/resetPassword";
+import Jwt from "jsonwebtoken";
+import { sendEmail } from "../helpers/nodemailer";
+import { hashPassword } from "../utils/password";
+import { resetTokenData } from "../helpers/security.helpers";
+import { isValidPassword } from "../utils/password.checks";
+
+export const forgotPassword = async (req: Request, res: Response) => {
+	try {
+		const { email } = req.body;
+
+		const isUserExist: User | null = await User.findOne({
+			where: { email: email },
+		});
+
+		if (!isUserExist) {
+			return res.status(404).json({
+				message: "User not found",
+			});
+		}
+
+		const resetToken = generateAccessToken({
+			id: isUserExist?.dataValues.id,
+			role: isUserExist?.dataValues.role,
+			email: isUserExist?.dataValues.email,
+		});
+
+		await resetPassword.destroy({
+			where: { email: email },
+		});
+
+		await resetPassword.create({
+			resetToken: resetToken,
+			email: email,
+		});
+
+		const host = process.env.BASE_URL || `http://localhost:${PORT}`;
+		const confirmlink: string = `${host}/passwordReset?token=${resetToken}`;
+
+		const mailOptions = {
+			to: email,
+			subject: "Reset Password",
+			html: `
+                <p>Click <a href="${confirmlink}">here</a> to reset your password</p>
+            `,
+		};
+
+		await sendEmail(mailOptions);
+
+		res
+			.status(200)
+			.json({ message: "Email sent successfully", status: "SUCCESS" });
+	} catch (error) {
+		res
+			.status(500)
+			.json({ message: "An error occurred while requesting password reset." });
+	}
+};
+
+export const resetPasswort = async (req: Request, res: Response) => {
+	try {
+		const { password } = req.body!;
+		const { token } = req.params;
+
+		const tokenAvailability = await resetPassword.findOne({
+			where: { resetToken: token },
+		});
+
+		if (!tokenAvailability) {
+			return res.status(400).json({ message: "Invalid link" });
+		}
+
+		const decoded = Jwt.verify(token, ACCESS_TOKEN_SECRET!) as resetTokenData;
+
+		if (!decoded || !decoded.id) {
+			return res.status(404).json({
+				message: "Invalid link",
+			});
+		}
+		const resettingUser = await User.findOne({ where: { id: decoded.id! } });
+
+		const sameAsOldPassword = await isValidPassword(
+			password,
+			resettingUser?.dataValues.password as string,
+		);
+
+		if (sameAsOldPassword) {
+			return res
+				.status(400)
+				.json({ message: "Password cannot be the same as the old password" });
+		}
+
+		const hashedPassword: string = (await hashPassword(password)) as string;
+
+		await resettingUser?.update({ password: hashedPassword });
+
+		await resetPassword.destroy({ where: { resetToken: token } });
+		res.status(200).json({ message: "Password reset successfully" });
+	} catch (error) {
+		res
+			.status(500)
+			.json({ message: "An error occurred while resetting password." });
+	}
+};
diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts
index 536ef828..a68639cb 100644
--- a/src/controllers/userController.ts
+++ b/src/controllers/userController.ts
@@ -1,18 +1,19 @@
 import { NextFunction, Request, Response } from "express";
 import { User, UserModelAttributes } from "../database/models/User";
-import { validateToken } from "../utils/token.validation";
-import { JWT_SECRET } from "../utils/keys";
 import {
 	TokenData,
 	generateAccessToken,
 	verifyAccessToken,
 } from "../helpers/security.helpers";
 import { HttpException } from "../utils/http.exception";
-import passport from "../middlewares/passport";
 import randomatic from "randomatic";
 import HTML_TEMPLATE from "../utils/mail-template";
 import { Token } from "../database/models/token";
-import { senderEmail } from "../services/mailService";
+import passport from "../middlewares/passport";
+// import sendEmail from "../utils/email";
+import { validateToken } from "../utils/token.validation";
+import { ACCESS_TOKEN_SECRET } from "../utils/keys";
+import { sendEmail } from "../helpers/nodemailer";
 
 interface InfoAttribute {
 	message: string;
@@ -36,8 +37,9 @@ const registerUser = async (
 					req.login(user, async () => {
 						const token = generateAccessToken({ id: user.id, role: user.role });
 						await Token.create({ token });
+
 						const message = `${process.env.BASE_URL}/users/account/verify/${token}`;
-						await senderEmail({
+						await sendEmail({
 							to: user.email,
 							subject: "Verify Email",
 							html: message,
@@ -112,7 +114,7 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
 					};
 					Token.create({ token: authenticationtoken });
 
-					senderEmail(options);
+					sendEmail(options);
 
 					const response = new HttpException(
 						"ACCEPTED",
@@ -136,11 +138,15 @@ const login = async (req: Request, res: Response, next: NextFunction) => {
 const accountVerify = async (req: Request, res: Response) => {
 	try {
 		const token = await Token.findOne({ where: { token: req.params.token } });
+
 		if (!token) {
 			return res.status(400).json({ status: 400, message: "Invalid link" });
 		}
 
-		const { user } = validateToken(token.dataValues.token, JWT_SECRET || "");
+		const { user } = validateToken(
+			token.dataValues.token,
+			ACCESS_TOKEN_SECRET as string,
+		);
 		if (!user) {
 			return res.status(400).json({ status: 400, message: "Invalid link" });
 		}
diff --git a/src/database/config/db.config.ts b/src/database/config/db.config.ts
index a8c9aeed..7aa85c4a 100644
--- a/src/database/config/db.config.ts
+++ b/src/database/config/db.config.ts
@@ -4,32 +4,30 @@ import { Sequelize } from "sequelize";
 config();
 
 let db_uri: string = "";
-const APP_MODE: string = (process.env.DEV_MODE as string) || "development";
-const DB_HOST_MODE: string = process.env.DB_HOSTED_MODE as string;
-
-let dialect_option: any;
+const APP_MODE: string = process.env.DEV_MODE || "development";
+const DB_HOST_MODE: string = process.env.DB_HOSTED_MODE || "local";
 
 switch (APP_MODE) {
 	case "test":
-		db_uri = process.env.DB_TEST_URL as string;
+		db_uri = process.env.DB_TEST_URL || "";
 		break;
-
 	case "production":
-		db_uri = process.env.DB_PROD_URL as string;
+		db_uri = process.env.DB_PROD_URL || "";
 		break;
 	default:
-		db_uri = process.env.DB_DEV_URL as string;
+		db_uri = process.env.DB_DEV_URL || "";
 		break;
 }
 
-DB_HOST_MODE === "local"
-	? (dialect_option = {})
-	: (dialect_option = {
+const isLocal = DB_HOST_MODE === "local";
+const dialect_option = isLocal
+	? {}
+	: {
 			ssl: {
-				require: process.env.SSL,
+				require: true, // Adjust based on your needs
 				rejectUnauthorized: true,
 			},
-		});
+		};
 
 export const sequelizeConnection: Sequelize = new Sequelize(db_uri, {
 	dialect: "postgres",
diff --git a/src/database/migrations/20240429163608-reset-password-tokens.js b/src/database/migrations/20240429163608-reset-password-tokens.js
new file mode 100644
index 00000000..5d467d2d
--- /dev/null
+++ b/src/database/migrations/20240429163608-reset-password-tokens.js
@@ -0,0 +1,35 @@
+"use strict";
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+	async up(queryInterface, Sequelize) {
+		await queryInterface.createTable("resetPassword_tokens", {
+			id: {
+				type: Sequelize.UUID,
+				defaultValue: Sequelize.UUIDV4,
+				primaryKey: true,
+				allowNull: false,
+			},
+			email: {
+				type: Sequelize.STRING,
+				allowNull: false,
+			},
+			resetToken: {
+				type: Sequelize.STRING(1000),
+				allowNull: false,
+			},
+			createdAt: {
+				allowNull: false,
+				type: Sequelize.DATE,
+			},
+			updatedAt: {
+				allowNull: false,
+				type: Sequelize.DATE,
+			},
+		});
+	},
+	// eslint-disable-next-line @typescript-eslint/no-unused-vars
+	async down(queryInterface, Sequelize) {
+		await queryInterface.dropTable("resetPassword_tokens");
+	},
+};
diff --git a/src/database/models/resetPassword.ts b/src/database/models/resetPassword.ts
new file mode 100644
index 00000000..9a6c9324
--- /dev/null
+++ b/src/database/models/resetPassword.ts
@@ -0,0 +1,33 @@
+import { DataTypes, Model } from "sequelize";
+import { sequelizeConnection } from "../config/db.config";
+
+export interface resetPasswordModelAtributes {
+	id?: string;
+	email: string;
+	resetToken: string;
+}
+
+export class resetPassword extends Model<resetPasswordModelAtributes> {}
+
+resetPassword.init(
+	{
+		id: {
+			type: DataTypes.UUID,
+			defaultValue: DataTypes.UUIDV4,
+			primaryKey: true,
+			allowNull: false,
+		},
+		email: {
+			type: DataTypes.STRING,
+			allowNull: false,
+		},
+		resetToken: {
+			type: DataTypes.STRING(1000),
+			allowNull: false,
+		},
+	},
+	{
+		sequelize: sequelizeConnection,
+		tableName: "resetPassword_tokens",
+	},
+);
diff --git a/src/documention/user/index.ts b/src/documention/user/index.ts
index 69c2068b..122a1b50 100644
--- a/src/documention/user/index.ts
+++ b/src/documention/user/index.ts
@@ -133,6 +133,64 @@ const users = {
 			tags: ["User"],
 			security: [{ JWT: [] }],
 			summary: "Log out a user",
+		},
+	},
+
+	"/users/forgot-password": {
+		post: {
+			tags: ["User"],
+			// security: [{ JWT: [] }],
+			summary: "Request password reset",
+			parameters: [
+				{
+					in: "body",
+					name: "request body",
+					required: true,
+					schema: {
+						type: "object",
+						properties: {
+							email: {
+								type: "string",
+								example: "email@example.com",
+							},
+						},
+					},
+				},
+			],
+			consumes: ["application/json"],
+			responses,
+		},
+	},
+	"/users/reset-password/{token}": {
+		post: {
+			tags: ["User"],
+			// security: [{ JWT: [] }],
+			summary: "Reset password",
+			parameters: [
+				{
+					in: "path",
+					name: "token",
+					required: true,
+					schema: {
+						type: "string",
+					},
+					description: "The reset password token",
+				},
+				{
+					in: "body",
+					name: "request body",
+					required: true,
+					schema: {
+						type: "object",
+						properties: {
+							password: {
+								type: "string",
+								example: "Password@123",
+							},
+						},
+					},
+				},
+			],
 			consumes: ["application/json"],
 		},
 	},
diff --git a/src/helpers/nodemailer.ts b/src/helpers/nodemailer.ts
new file mode 100644
index 00000000..458b7663
--- /dev/null
+++ b/src/helpers/nodemailer.ts
@@ -0,0 +1,35 @@
+import nodemailer from "nodemailer";
+import { EMAIL, PASSWORD, SENDER_NAME } from "../utils/keys";
+
+interface MailOptions {
+	to: string;
+	subject: string;
+	html: any;
+}
+
+const sender = nodemailer.createTransport({
+	service: "gmail",
+	auth: {
+		user: EMAIL,
+		pass: PASSWORD,
+	},
+	tls: {
+		rejectUnauthorized: false,
+	},
+});
+
+// SEND EMAIL FUNCTION
+export function sendEmail({ to, subject, html }: MailOptions) {
+	const mailOptions = {
+		from: `"${SENDER_NAME}" <${EMAIL}>`,
+		to,
+		subject,
+		html,
+	};
+
+	sender.sendMail(mailOptions, (error) => {
+		if (error) {
+			console.log("EMAILING USER FAILED:", error);
+		}
+	});
+}
diff --git a/src/helpers/security.helpers.ts b/src/helpers/security.helpers.ts
index f71113ea..a30aaee1 100644
--- a/src/helpers/security.helpers.ts
+++ b/src/helpers/security.helpers.ts
@@ -7,6 +7,11 @@ export interface TokenData {
 	id: string | number;
 	role: string;
 	otp?: string;
+	email?: string;
+}
+export interface resetTokenData {
+	id: string | number;
+	email: string;
 }
 
 export const generateAccessToken = (userData: TokenData) => {
diff --git a/src/middlewares/auth.ts b/src/middlewares/auth.ts
index 78f74dd6..3918f5f0 100644
--- a/src/middlewares/auth.ts
+++ b/src/middlewares/auth.ts
@@ -1,6 +1,6 @@
 import { Request, Response, NextFunction } from "express";
 import jwt, { JwtPayload } from "jsonwebtoken";
-import { JWT_SECRET } from "../utils/keys";
+import { ACCESS_TOKEN_SECRET } from "../utils/keys";
 import { Blacklist } from "../database/models/blacklist";
 
 interface ExpandedRequest extends Request {
@@ -28,7 +28,10 @@ export const authenticateUser = async (
 	}
 
 	try {
-		const verifiedToken = jwt.verify(token, JWT_SECRET as string) as JwtPayload;
+		const verifiedToken = jwt.verify(
+			token,
+			ACCESS_TOKEN_SECRET as string,
+		) as JwtPayload;
 		const isInBlcaklist = await Blacklist.findOne({ where: { token } });
 
 		if (!verifiedToken) {
@@ -65,7 +68,10 @@ export const isBuyer = async (
 		return res.status(401).json({ message: "Unauthorized" });
 	}
 	try {
-		const decoded = jwt.verify(token, JWT_SECRET as string) as JwtPayload;
+		const decoded = jwt.verify(
+			token,
+			ACCESS_TOKEN_SECRET as string,
+		) as JwtPayload;
 		const isInBlcaklist = await Blacklist.findOne({ where: { token } });
 
 		if (!decoded) {
@@ -96,7 +102,10 @@ export const isVendor = async (
 		return res.status(401).json({ message: "Unauthorized" });
 	}
 	try {
-		const decoded = jwt.verify(token, JWT_SECRET as string) as JwtPayload;
+		const decoded = jwt.verify(
+			token,
+			ACCESS_TOKEN_SECRET as string,
+		) as JwtPayload;
 		const isInBlcaklist = await Blacklist.findOne({ where: { token } });
 
 		if (!decoded) {
@@ -127,7 +136,10 @@ export const isAdmin = async (
 		return res.status(401).json({ message: "Unauthorized" });
 	}
 	try {
-		const decoded = jwt.verify(token, JWT_SECRET as string) as JwtPayload;
+		const decoded = jwt.verify(
+			token,
+			ACCESS_TOKEN_SECRET as string,
+		) as JwtPayload;
 		const isInBlcaklist = await Blacklist.findOne({ where: { token } });
 
 		if (!decoded) {
diff --git a/src/middlewares/user.middleware.ts b/src/middlewares/user.middleware.ts
index b59977c3..3393a787 100644
--- a/src/middlewares/user.middleware.ts
+++ b/src/middlewares/user.middleware.ts
@@ -2,7 +2,9 @@
 import { userValidate } from "../validations/user.valid";
 import { NextFunction, Request, Response } from "express";
 import validateLogIn from "../validations/login.validation";
+import validateReset from "../validations/reset.validation";
 import { HttpException } from "../utils/http.exception";
+import validateNewPassword from "../validations/newPassword.validations";
 const userValid = async (req: Request, res: Response, next: NextFunction) => {
 	try {
 		if (req.body) {
@@ -45,7 +47,42 @@ const logInValidated = (req: Request, res: Response, next: NextFunction) => {
 	next();
 };
 
+const resetValidated = (req: Request, res: Response, next: NextFunction) => {
+	const error = validateReset(req.body);
+
+	if (error) {
+		return res
+			.status(400)
+			.json(
+				new HttpException(
+					"BAD REQUEST",
+					error.details[0].message.replace(/\"/g, ""),
+				),
+			);
+	}
+
+	next();
+};
+const isPassword = (req: Request, res: Response, next: NextFunction) => {
+	const error = validateNewPassword(req.body);
+
+	if (error) {
+		return res
+			.status(400)
+			.json(
+				new HttpException(
+					"BAD REQUEST",
+					error.details[0].message.replace(/\"/g, ""),
+				),
+			);
+	}
+
+	next();
+};
+
 export default {
 	logInValidated,
 	userValid,
+	resetValidated,
+	isPassword,
 };
diff --git a/src/mock/static.ts b/src/mock/static.ts
index 3af6ee79..63548515 100644
--- a/src/mock/static.ts
+++ b/src/mock/static.ts
@@ -58,3 +58,17 @@ export const bad_two_factor_authentication_data = {
 export const partial_two_factor_authentication_data = {
 	otp: "20420",
 };
+export const sameAsOldPassword = {
+	password: "passwordQWE123",
+};
+export const requestResetBody = {
+	email: "peter234565@gmail.com",
+};
+export const NotUserrequestBody = {
+	email: "peter2345@gmail.com",
+	forTesting: true,
+};
+
+export const newPasswordBody = {
+	password: "New@password",
+};
diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts
index 88c4f642..fe718c2d 100644
--- a/src/routes/userRoutes.ts
+++ b/src/routes/userRoutes.ts
@@ -3,6 +3,7 @@ import userController from "../controllers/userController";
 import userMiddleware from "../middlewares/user.middleware";
 import logout from "../controllers/logoutController";
 import otpIsValid from "../middlewares/otp";
+import { resetPasswort, forgotPassword } from "../controllers/resetPasswort";
 
 const userRoutes = express.Router();
 userRoutes.post(
@@ -14,6 +15,16 @@ userRoutes.post(
 userRoutes.post("/login", userMiddleware.logInValidated, userController.login);
 
 userRoutes.post("/logout", logout);
+userRoutes.post(
+	"/forgot-password",
+	userMiddleware.resetValidated,
+	forgotPassword,
+);
+userRoutes.post(
+	"/reset-password/:token",
+	userMiddleware.isPassword,
+	resetPasswort,
+);
 userRoutes.get("/account/verify/:token", userController.accountVerify);
 
 userRoutes.post(
diff --git a/src/services/mailService.ts b/src/services/mailService.ts
deleted file mode 100644
index 94b46f2b..00000000
--- a/src/services/mailService.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import sgMail from "@sendgrid/mail";
-import { EMAIL, SENDER_NAME, SENDGRID_API_KEY } from "../utils/keys";
-interface MailOptions {
-	to: string;
-	subject: string;
-	html: any;
-}
-sgMail.setApiKey(`${SENDGRID_API_KEY}`);
-// SEND EMAIL FUNCTION
-export function senderEmail({ to, subject, html }: MailOptions) {
-	const mailOptions = {
-		from: `"${SENDER_NAME}" <${EMAIL}>`,
-		to,
-		subject,
-		html,
-	};
-	sgMail
-		.send(mailOptions)
-		.then(() => {
-			console.log("Email sent");
-		})
-		.catch((error) => {
-			console.error(error);
-		});
-}
diff --git a/src/utils/keys.ts b/src/utils/keys.ts
index b2e0066d..801f4768 100644
--- a/src/utils/keys.ts
+++ b/src/utils/keys.ts
@@ -3,9 +3,10 @@ export const SESSION_SECRET = process.env.SESSION_SECRET as string;
 export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
 export const GOOGLE_SECRET_ID = process.env.GOOGLE_SECRET_ID;
 export const GOOGLE_CALLBACK_URL = process.env.GOOGLE_CALLBACK_URL;
-export const JWT_SECRET = process.env.JWT_SECRET;
 export const ACCESS_TOKEN_SECRET = process.env.ACCESS_TOKEN_SECRET;
 export const DB_NAME_TEST = process.env.DB_NAME_TEST;
-export const EMAIL = process.env.EMAIL;
 export const SENDER_NAME = process.env.SENDER_NAME;
+export const EMAIL = process.env.EMAIL;
+export const PASSWORD = process.env.PASSWORD;
+export const HOST = process.env.HOST;
 export const SENDGRID_API_KEY = process.env.SENDGRID_API_KEY;
diff --git a/src/validations/newPassword.validations.ts b/src/validations/newPassword.validations.ts
new file mode 100644
index 00000000..96ab7dfc
--- /dev/null
+++ b/src/validations/newPassword.validations.ts
@@ -0,0 +1,14 @@
+import Joi from "joi";
+
+const newPasswordValidation = Joi.object({
+	password: Joi.string().required().messages({
+		"string.empty": "Password field can't be empty!",
+	}),
+}).options({ allowUnknown: false });
+
+const validateNewPassword = (body: any) => {
+	const { error } = newPasswordValidation.validate(body);
+	return error;
+};
+
+export default validateNewPassword;
diff --git a/src/validations/reset.validation.ts b/src/validations/reset.validation.ts
new file mode 100644
index 00000000..0f825283
--- /dev/null
+++ b/src/validations/reset.validation.ts
@@ -0,0 +1,15 @@
+import Joi from "joi";
+
+const ResetPasswordValidation = Joi.object({
+	email: Joi.string().required().email().messages({
+		"string.empty": "Email field can not be empty!",
+		"string.email": "Invalid email!",
+	}),
+}).options({ allowUnknown: true });
+
+const validateReset = (body: any) => {
+	const { error } = ResetPasswordValidation.validate(body);
+	return error;
+};
+
+export default validateReset;