Skip to content

Commit

Permalink
feat(edit-profile): Implement password update feature
Browse files Browse the repository at this point in the history
   - Implement the feature to allow users update their password
   - Added validation check for old password, new password and confirm
     password
   - hash the new password before updating
   - documented the feature using swagger

[Delivers #187419174]
  • Loading branch information
Heisjabo committed Apr 25, 2024
1 parent 1a54b1d commit 2a5c5d2
Show file tree
Hide file tree
Showing 9 changed files with 265 additions and 47 deletions.
172 changes: 128 additions & 44 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,67 @@ import User from "../src/sequelize/models/users";
import * as userServices from "../src/services/user.service";
import sequelize, { connect } from "../src/config/dbConnection";

const userData:any = {
name: 'yvanna',
username: 'testuser',
email: '[email protected]',
password:'test1234',
};
const userData: any = {
name: "yvanna",
username: "testuser",
email: "[email protected]",
password: "test1234",
};

const loginData:any = {
email:'[email protected]',
password:"test1234"
}
const userTestData = {
newPassword: "Test@123",
confirmPassword: "Test@123",
wrongPassword: "Test@456",
};

const loginData: any = {
email: "[email protected]",
password: "test1234",
};
describe("Testing user Routes", () => {
beforeAll(async () => {
try {
await connect();
await User.destroy({truncate:true})
await User.destroy({ truncate: true });
} catch (error) {
sequelize.close();
}
}, 20000);

afterAll(async () => {
afterAll(async () => {
await User.destroy({ truncate: true });
await sequelize.close();
});
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);
expect(response.status).toBe(201); }, 20000);
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);
expect(response.status).toBe(201);
}, 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);
expect(response.status).toBe(409); }, 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);
expect(response.status).toBe(409);
}, 20000);

test('should return 400 when registering with an invalid credential', async () => {
const userData = {
email: '[email protected]', name: "", username: 'existinguser', };
const response = await request(app)
.post('/api/v1/users/register')
.send(userData);

expect(response.status).toBe(400); }, 20000); });
test("should return 400 when registering with an invalid credential", async () => {
const userData = {
email: "[email protected]",
name: "",
username: "existinguser",
};
const response = await request(app)
.post("/api/v1/users/register")
.send(userData);

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

test("should return all users in db --> given '/api/v1/users'", async () => {
const spy = jest.spyOn(User, "findAll");
Expand All @@ -60,19 +74,89 @@ test('should return 400 when registering with an invalid credential', async () =
expect(spy).toHaveBeenCalled();
expect(spy2).toHaveBeenCalled();
}, 20000);
test("Should return status 401 to indicate Unauthorized user",async() =>{
const loggedInUser ={
email:userData.email,
password:"test",
test("Should return status 401 to indicate Unauthorized user", async () => {
const loggedInUser = {
email: userData.email,
password: "test",
};
const spyonOne = jest.spyOn(User,"findOne").mockResolvedValueOnce({
const spyonOne = jest.spyOn(User, "findOne").mockResolvedValueOnce({
//@ts-ignore
email:userData.email,
password:loginData.password,
email: userData.email,
password: loginData.password,
});
const response = await request(app).post("/api/v1/users/login")
.send(loggedInUser)
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 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);
});
});
31 changes: 30 additions & 1 deletion src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -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 { comparePasswords } from "../utils/comparePassword";
import { loggedInUser} from "../services/user.service";
import { createUserService, getUserByEmail } from "../services/user.service";
import { hashedPassword } from "../utils/hashPassword";

export const fetchAllUsers = async (req: Request, res: Response) => {
try {
Expand Down Expand Up @@ -79,3 +80,31 @@ 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' });
}
const password = await hashedPassword(newPassword);
user.password = password;
await user.save();
return res.status(200).json({ message: 'Password updated successfully' });

} catch(err: any){
return res.status(500).json({
message: err.message
})
}
}
8 changes: 7 additions & 1 deletion src/docs/swagger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import {
getUsers,
loginAsUser,
userSchema,
loginSchema
loginSchema,
updatePasswordSchema,
passwordUpdate
} from "./users";

const docRouter = express.Router();
Expand Down Expand Up @@ -43,12 +45,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: {
Expand Down
39 changes: 39 additions & 0 deletions src/docs/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ export const userSchema = {
},
}

export const updatePasswordSchema = {
type: "object",
properties: {
oldPassword: {
type: "string",
},
newPassword: {
type: "string",
},
confirmPassword: {
type: "string",
}
}
}

export const loginSchema ={
properties :{
email: {
Expand Down Expand Up @@ -101,4 +116,28 @@ export const getUsers = {
}
}
};

export const passwordUpdate = {
tags: ["Users"],
security: [{ bearerAuth: [] }],
summary: "Update Password",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
$ref: "#/components/schemas/updatePassword"
}
}
}
},
responses: {
200: {
description: "OK",
},
400: {
description: "Bad Request"
}
}
}

40 changes: 40 additions & 0 deletions src/middlewares/isLoggedIn.ts
Original file line number Diff line number Diff line change
@@ -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.",
});
}
}
7 changes: 6 additions & 1 deletion src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,16 @@ import { Router } from "express";
import {
fetchAllUsers,
createUserController,
userLogin }
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();

Expand All @@ -19,6 +22,8 @@ userRoutes.post("/register",
validateSchema(signUpSchema),
createUserController
)
userRoutes.post("/register", createUserController);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword)


export default userRoutes;
Loading

0 comments on commit 2a5c5d2

Please sign in to comment.