diff --git a/__test__/user.test.ts b/__test__/user.test.ts index b6ab8e4..ad51009 100644 --- a/__test__/user.test.ts +++ b/__test__/user.test.ts @@ -12,11 +12,17 @@ const userData: any = { password: "test1234", }; +const userTestData = { + newPassword: "Test@123", + confirmPassword: "Test@123", + wrongPassword: "Test456", +}; + const loginData: any = { email: "test1@gmail.com", password: "test1234", }; -describe("Testing a user Routes", () => { +describe("Testing user Routes", () => { beforeAll(async () => { try { await connect(); @@ -30,17 +36,22 @@ describe("Testing a user Routes", () => { await User.destroy({ truncate: true }); await sequelize.close(); }); + let token: any; describe("Testing user authentication", () => { test("should return 201 and create a new user when registering successfully", async () => { - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(201); - }, 40000); + }, 20000); test("should return 409 when registering with an existing email", async () => { User.create(userData); - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(409); - }, 40000); + }, 20000); test("should return 400 when registering with an invalid credential", async () => { const userData = { @@ -48,31 +59,104 @@ describe("Testing a user Routes", () => { name: "", username: "existinguser", }; - const response = await request(app).post("/api/v1/users/register").send(userData); + const response = await request(app) + .post("/api/v1/users/register") + .send(userData); expect(response.status).toBe(400); - }, 40000); + }, 20000); + }); + + test("should return all users in db --> given '/api/v1/users'", async () => { + const spy = jest.spyOn(User, "findAll"); + const spy2 = jest.spyOn(userServices, "getAllUsers"); + const response = await request(app).get("/api/v1/users"); + expect(spy).toHaveBeenCalled(); + expect(spy2).toHaveBeenCalled(); + }, 20000); + test("Should return status 401 to indicate Unauthorized user", async () => { + const loggedInUser = { + email: userData.email, + password: "test", + }; + const spyonOne = jest.spyOn(User, "findOne").mockResolvedValueOnce({ + //@ts-ignore + email: userData.email, + password: loginData.password, + }); + const response = await request(app) + .post("/api/v1/users/login") + .send(loggedInUser); + expect(response.body.status).toBe(401); + spyonOne.mockRestore(); + }); + + test("should log a user in to retrieve a token", async () => { + const response = await request(app).post("/api/v1/users/login").send({ + email: userData.email, + password: userData.password, + }); + expect(response.status).toBe(200); + token = response.body.token; + }); + + test("should return 400 when adding an extra field while updating password", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + role: "seller", + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); + }); + + test("should return 401 when updating password without authorization", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + }); + expect(response.status).toBe(401); + }); + + test("should return 200 when password is updated", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.confirmPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(200); + }); + + test("should return 400 when confirm password and new password doesn't match", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userData.password, + newPassword: userTestData.newPassword, + confirmPassword: userTestData.wrongPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); }); -}); -test("should return all users in db --> given '/api/v1/users'", async () => { - const spy = jest.spyOn(User, "findAll"); - const spy2 = jest.spyOn(userServices, "getAllUsers"); - const response = await request(app).get("/api/v1/users"); - expect(spy).toHaveBeenCalled(); - expect(spy2).toHaveBeenCalled(); -}, 40000); -test("Should return status 401 to indicate Unauthorized user", async () => { - const loggedInUser = { - email: userData.email, - password: "test", - }; - const spyonOne = jest.spyOn(User, "findOne").mockResolvedValueOnce({ - //@ts-ignore - email: userData.email, - password: loginData.password, + test("should return 400 when old password is incorrect", async () => { + const response = await request(app) + .put("/api/v1/users/passwordupdate") + .send({ + oldPassword: userTestData.wrongPassword, + newPassword: userTestData.newPassword, + confirmPassword:userTestData.wrongPassword, + }) + .set("Authorization", "Bearer " + token); + expect(response.status).toBe(400); }); - const response = await request(app).post("/api/v1/users/login").send(loggedInUser); - expect(response.body.status).toBe(401); - spyonOne.mockRestore(); }); diff --git a/src/controllers/userControllers.ts b/src/controllers/userControllers.ts index 6152856..bf7a8db 100644 --- a/src/controllers/userControllers.ts +++ b/src/controllers/userControllers.ts @@ -1,9 +1,10 @@ import { Request, Response } from "express"; import * as userService from "../services/user.service"; import { generateToken } from "../utils/jsonwebtoken"; -import { comparePasswords } from "../helpers/comparePassword"; -import { loggedInUser } from "../services/user.service"; -import { createUserService } from "../services/user.service"; +import { comparePasswords } from "../utils/comparePassword"; +import { loggedInUser} from "../services/user.service"; +import { createUserService, getUserByEmail, updateUserPassword } from "../services/user.service"; +import { hashedPassword } from "../utils/hashPassword"; export const fetchAllUsers = async (req: Request, res: Response) => { try { @@ -79,3 +80,35 @@ export const createUserController = async (req: Request, res: Response) => { res.status(500).json({ error: err }); } }; + +export const updatePassword = async (req: Request, res: Response) => { + const { oldPassword, newPassword, confirmPassword } = req.body; + try { + // @ts-ignore + const user = await getUserByEmail(req.user.email); + if (!user) { + return res.status(404).json({ message: 'User not found' }); + } + const isPasswordValid = await comparePasswords(oldPassword, user.password); + if (!isPasswordValid) { + return res.status(400).json({ message: 'Old password is incorrect' }); + } + + if (newPassword !== confirmPassword) { + return res.status(400).json({ message: 'New password and confirm password do not match' }); + } + + if(await comparePasswords(newPassword, user.password)){ + return res.status(400).json({ message: 'New password is similar to the old one. Please use a new password'}) + } + + const password = await hashedPassword(newPassword); + await updateUserPassword(user, password) + return res.status(200).json({ message: 'Password updated successfully' }); + + } catch(err: any){ + return res.status(500).json({ + message: err.message + }) + } +} diff --git a/src/docs/swagger.ts b/src/docs/swagger.ts index f5fc119..676b55c 100644 --- a/src/docs/swagger.ts +++ b/src/docs/swagger.ts @@ -1,7 +1,15 @@ import express from "express"; import { serve, setup } from "swagger-ui-express"; import { env } from "../utils/env"; -import { createUsers, getUsers, loginAsUser, userSchema, loginSchema } from "./users"; +import { + createUsers, + getUsers, + loginAsUser, + userSchema, + loginSchema, + updatePasswordSchema, + passwordUpdate + } from "./users"; const docRouter = express.Router(); @@ -43,12 +51,16 @@ const options = { "/api/v1/users/login": { post: loginAsUser, }, + "/api/v1/users/passwordupdate": { + put: passwordUpdate + } }, components: { schemas: { User: userSchema, Login: loginSchema, + updatePassword: updatePasswordSchema }, securitySchemes: { bearerAuth: { @@ -59,8 +71,9 @@ const options = { name: "Authorization", }, }, - }, -}; + } + +} docRouter.use("/", serve, setup(options)); diff --git a/src/docs/users.ts b/src/docs/users.ts index ae2c33c..7776e09 100644 --- a/src/docs/users.ts +++ b/src/docs/users.ts @@ -17,6 +17,21 @@ export const userSchema = { }, }; +export const updatePasswordSchema = { + type: "object", + properties: { + oldPassword: { + type: "string", + }, + newPassword: { + type: "string", + }, + confirmPassword: { + type: "string", + } + } +} + export const loginSchema = { properties: { email: { @@ -82,19 +97,34 @@ export const createUsers = { export const loginAsUser = { tags: ["Users"], summary: "Login as user", + responses: { + 200: { + description: "OK", + } + } + }; + +export const passwordUpdate = { + tags: ["Users"], + security: [{ bearerAuth: [] }], + summary: "Update Password", requestBody: { required: true, content: { "application/json": { schema: { - $ref: "#/components/schemas/Login", - }, - }, - }, + $ref: "#/components/schemas/updatePassword" + } + } + } }, responses: { 200: { description: "OK", }, - }, -}; + 400: { + description: "Bad Request" + } + } +} + diff --git a/src/middlewares/isLoggedIn.ts b/src/middlewares/isLoggedIn.ts new file mode 100644 index 0000000..068b844 --- /dev/null +++ b/src/middlewares/isLoggedIn.ts @@ -0,0 +1,40 @@ +import { getUserByEmail } from "../services/user.service"; +import { Request, Response, NextFunction } from "express"; +import { decodeToken } from "../utils/jsonwebtoken" + +export const isLoggedIn = async (req: Request, res: Response, next: NextFunction) => { + let token: string | undefined = undefined; + try{ + if ( + req.headers.authorization && + req.headers.authorization.startsWith("Bearer ") + ) { + token = req.headers.authorization.split(" ")[1]; + } + if (!token) { + return res.status(401).json({ + status: "Unauthorized", + message: "You are not logged in. Please login to continue.", + }); + } + if (typeof token !== "string") { + throw new Error("Token is not a string."); + } + const decoded: any = await decodeToken(token) + const loggedUser: any = await getUserByEmail(decoded.email); + if (!loggedUser) { + return res.status(401).json({ + status: "Unauthorized", + message: "Token has expired. Please login again.", + }); + } + // @ts-ignore + req.user = loggedUser; + next(); + } catch (error: any) { + return res.status(401).json({ + status: "failed", + error: error.message + " Token has expired. Please login again.", + }); + } +} \ No newline at end of file diff --git a/src/routes/userRoutes.ts b/src/routes/userRoutes.ts index 5b4e507..89bf87c 100644 --- a/src/routes/userRoutes.ts +++ b/src/routes/userRoutes.ts @@ -1,14 +1,28 @@ import { Router } from "express"; -import { fetchAllUsers, createUserController, userLogin } from "../controllers/userControllers"; -import { emailValidation, validateSchema } from "../middleware/validator"; +import { + fetchAllUsers, + createUserController, + userLogin, + updatePassword} +from "../controllers/userControllers"; +import { + emailValidation, + validateSchema, + } from "../middleware/validator"; import signUpSchema from "../schemas/signUpSchema"; +import { isLoggedIn } from "../middlewares/isLoggedIn"; +import { passwordUpdateSchema } from "../schemas/passwordUpdate"; const userRoutes = Router(); userRoutes.get("/", fetchAllUsers); -userRoutes.post("/login", userLogin); -userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController); +userRoutes.post('/login',userLogin); +userRoutes.post("/register", + emailValidation, + validateSchema(signUpSchema), + createUserController +) +userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword) -userRoutes.post("/register", createUserController); export default userRoutes; diff --git a/src/schemas/passwordUpdate.ts b/src/schemas/passwordUpdate.ts new file mode 100644 index 0000000..168653e --- /dev/null +++ b/src/schemas/passwordUpdate.ts @@ -0,0 +1,10 @@ +import Joi from "joi"; + +export const passwordUpdateSchema = Joi.object({ + oldPassword: Joi.string() + .min(6).max(20).required(), + newPassword: Joi.string() + .min(6).max(20).required(), + confirmPassword: Joi.string() + .min(6).max(20).required() +}).options({ allowUnknown: false }) \ No newline at end of file diff --git a/src/services/user.service.ts b/src/services/user.service.ts index 59c9fed..d0a7f07 100644 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -55,3 +55,9 @@ export const getUserByEmail = async (email: string): Promise => { }); return user; }; + +export const updateUserPassword = async (user: User, password: string) => { + user.password = password; + const update = await user.save; + return update; +} diff --git a/src/helpers/comparePassword.ts b/src/utils/comparePassword.ts similarity index 100% rename from src/helpers/comparePassword.ts rename to src/utils/comparePassword.ts diff --git a/src/utils/jsonwebtoken.ts b/src/utils/jsonwebtoken.ts index 6bea145..48eda7d 100644 --- a/src/utils/jsonwebtoken.ts +++ b/src/utils/jsonwebtoken.ts @@ -1,6 +1,6 @@ import { IUser } from "../types"; import { env } from "../utils/env"; -import { sign } from "jsonwebtoken"; +import { sign, verify } from "jsonwebtoken"; export const generateToken = async (user: IUser) => { const accessToken = sign( @@ -13,3 +13,9 @@ export const generateToken = async (user: IUser) => { ); return accessToken; }; + +export const decodeToken = async (token: string) => { + const decoded = await verify(token, `${env.jwt_secret}`); + return decoded; +} +