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]

Feat(authentication): implementation of google authentication

- Add signin and sign up with google account
- return access token
- added test

delivers #187419170

--amend

Feat(authentication): implementation of google authentication

- Add signin and sign up with google account
- return access token
- added test

delivers #187419170

Fixed missing login body in swagger doc

fix: fixed 2fa implemetation by replacing verification link with otp code

fix(profile-edit): fix password not being updated in the database

 - fixed the issue of password not being updated in the database

[Fixes #187419174]
  • Loading branch information
Heisjabo authored and MugemaneBertin2001 committed Apr 29, 2024
1 parent ac7da2f commit 9ff45bd
Show file tree
Hide file tree
Showing 22 changed files with 621 additions and 74 deletions.
7 changes: 7 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,13 @@ jobs:
env:
DB_CONNECTION: ${{ secrets.DB_CONNECTION }}
TEST_DB: ${{ secrets.TEST_DB }}
SMTP_HOST: ${{ secrets.SMTP_HOST }}
SMTP_USER: ${{ secrets.SMTP_USER }}
SMTP_PASS: ${{ secrets.SMTP_PASS }}
SMTP_PORT: ${{ secrets.SMTP_PORT }}
GOOGLE_CLIENT_ID: ${{secrets.GOOGLE_CLIENT_ID}},
GOOGLE_CLIENT_SECRET: ${{secrets.GOOGLE_CLIENT_SECRET}},
GOOGLE_CALLBACK_URL: ${{secrets.GOOGLE_CALLBACK_URL}},
run: npm run test

- name: Build application
Expand Down
165 changes: 134 additions & 31 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,34 @@ import { beforeAll, afterAll, jest, test } from "@jest/globals";
import app from "../src/utils/server";
import User from "../src/sequelize/models/users";
import * as userServices from "../src/services/user.service";
import * as mailServices from "../src/services/mail.service";
import sequelize, { connect } from "../src/config/dbConnection";

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

const dummySeller = {
name: "dummyseller",
username: "username",
email: "[email protected]",
password: "1234567890",
role: "seller",
};
const userTestData = {
newPassword: "Test@123",
confirmPassword: "Test@123",
wrongPassword: "Test456",
};

const loginData: any = {
email: "[email protected]",
password: "test1234",
};
describe("Testing a user Routes", () => {
describe("Testing user Routes", () => {
beforeAll(async () => {
try {
await connect();
Expand All @@ -30,49 +44,138 @@ 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 = {
email: "[email protected]",
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();
}, 20000);

test("Should send otp verification code", async () => {
const spy = jest.spyOn(mailServices, "sendEmailService");
const response = await request(app).post("/api/v1/users/login").send({
email: dummySeller.email,
password: dummySeller.password,
});

expect(response.body.message).toBe("OTP verification code has been sent ,please use it to verify that it was you");
// expect(spy).toHaveBeenCalled();
}, 40000);

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();
});
122 changes: 106 additions & 16 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
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 * as mailService from "../services/mail.service";
import { IUser, STATUS, SUBJECTS } from "../types";
import { comparePasswords } from "../utils/comparePassword";
import { loggedInUser} from "../services/user.service";
import { createUserService, getUserByEmail, updateUserPassword } from "../services/user.service";
import { hashedPassword } from "../utils/hashPassword";
import Token, { TokenAttributes } from "../sequelize/models/Token";
import User from "../sequelize/models/users";
import { verifyOtpTemplate } from "../email-templates/verifyotp";

export const fetchAllUsers = async (req: Request, res: Response) => {
try {
// const users = await userService.getAllUsers();

const users = await userService.getAllUsers();

if (users.length <= 0) {
Expand All @@ -32,14 +36,15 @@ export const fetchAllUsers = async (req: Request, res: Response) => {

export const userLogin = async (req: Request, res: Response) => {
const { email, password } = req.body;
const user = await loggedInUser(email);
const accessToken = await generateToken(user);
if (!user) {
const user: IUser = await loggedInUser(email);
let accessToken;
if (!user || user === null) {
res.status(404).json({
status: 404,
message: "User Not Found ! Please Register new ancount",
});
} else {
accessToken = await generateToken(user);
const match = await comparePasswords(password, user.password);
if (!match) {
res.status(401).json({
Expand All @@ -52,30 +57,115 @@ export const userLogin = async (req: Request, res: Response) => {
message: "Logged in",
token: accessToken,
});
if (user.role.includes("seller")) {
const token = Math.floor(Math.random() * 90000 + 10000);
//@ts-ignore
await Token.create({ token: token, userId: user.id });
await mailService.sendEmailService(user, SUBJECTS.CONFIRM_2FA, verifyOtpTemplate(token), token);
return res.status(200).json({
status: STATUS.PENDING,
message: "OTP verification code has been sent ,please use it to verify that it was you",
});
} else {
return res.status(200).json({
status: 200,
message: "Logged in",
token: accessToken,
});
}
}
}
};

export const createUserController = async (req: Request, res: Response) => {
try {
const { name, email, username, password } = req.body;
const user = await createUserService(name, email, username, password);

if (!user) {
const { name, email, username, password, role } = req.body;
const user = await createUserService(name, email, username, password, role);
if (!user || user == null) {
return res.status(409).json({
status: 409,
message: "Username or email already exists",
});
}

res.status(201).json({
return res.status(201).json({
status: 201,
message: "User successfully created.",
message: "User successfully created."
});
} catch (err: any) {
if (err.name === "UnauthorizedError" && err.message === "User already exists") {
return res.status(409).json({ error: "User already exists" });
}
res.status(500).json({ error: err });
return 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
})
}
};

export const tokenVerification = async (req: any, res: Response) => {
const foundToken: TokenAttributes = req.token;

try {
const tokenCreationTime = new Date(String(foundToken?.createdAt)).getTime();
const currentTime = new Date().getTime();
const timeDifference = currentTime - tokenCreationTime;

if (timeDifference > 600000) {
await Token.destroy({ where: { userId: foundToken.userId } });
return res.status(401).json({
message: "Token expired",
});
}

const user: IUser | null = await User.findOne({ where: { id: foundToken.userId } });

if (user) {
const token = await generateToken(user);

await Token.destroy({ where: { userId: foundToken.userId } });

return res.status(200).json({
message: "Login successful",
token,
user,
});
} else {
return res.status(404).json({
message: "User not found",
});
}
} catch (error: any) {
return res.status(500).json({
message: error.message,
});
}
};
Loading

0 comments on commit 9ff45bd

Please sign in to comment.