Skip to content

Commit

Permalink
fix: fixed 2fa implemetation by replacing verification link with otp …
Browse files Browse the repository at this point in the history
…code
  • Loading branch information
soleil00 committed Apr 29, 2024
1 parent dc505f3 commit e77f265
Show file tree
Hide file tree
Showing 23 changed files with 326 additions and 213 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ jobs:
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
32 changes: 14 additions & 18 deletions __test__/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,22 @@ 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";
import * as twoFAService from "../src/utils/2fa";

const userData: any = {
name: "yvanna",
username: "testuser",
email: "test1@gmail.com",
password: "test1234",
name: "yvanna5",
username: "testuser5",
email: "test15@gmail.com",
password: "test12345",
};

const dummySeller = {
name: "dummy",
username: "username",
email: "srukundo02@gmail.com",
name: "dummy1234",
username: "username1234",
email: "soleilcyber00@gmail.com",
password: "1234567890",
isMerchant: true,
role: "seller",
};
const userTestData = {
newPassword: "Test@123",
Expand Down Expand Up @@ -94,20 +94,16 @@ describe("Testing user Routes", () => {
spyonOne.mockRestore();
}, 20000);

test("Should return send magic link if seller try to login", async () => {
const spy = jest.spyOn(twoFAService, "sendOTP");
const user = {
email: dummySeller.email,
password: dummySeller.password,
};

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("Verification link has been sent to your email. Please verify it to continue");
}, 20000);
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({
Expand Down
32 changes: 0 additions & 32 deletions src/controllers/2faControllers.ts

This file was deleted.

73 changes: 59 additions & 14 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { Request, Response } from "express";
import * as userService from "../services/user.service";
import { generateToken } from "../utils/jsonwebtoken";
import * as twoFAService from "../utils/2fa";
import { IUser, STATUS } from "../types";
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 @@ -36,25 +37,29 @@ export const fetchAllUsers = async (req: Request, res: Response) => {
export const userLogin = async (req: Request, res: Response) => {
const { email, password } = req.body;
const user: IUser = await loggedInUser(email);
const accessToken = await generateToken(user);
if (!user) {
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({
status: 401,
message: " User email or password is incorrect!",
});
} else {
if (user?.isMerchant) {
await twoFAService.sendOTP(user);
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: "Verification link has been sent to your email. Please verify it to continue",
message: "OTP verification code has been sent ,please use it to verify that it was you",
});
} else {
return res.status(200).json({
Expand All @@ -69,23 +74,24 @@ export const userLogin = async (req: Request, res: Response) => {

export const createUserController = async (req: Request, res: Response) => {
try {
const { name, email, username, password, isMerchant } = req.body;
const user = await createUserService(name, email, username, password, isMerchant);
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: "User already exists",
});
}
res.status(201).json({
return res.status(201).json({
status: 201,
message: "User successfully created.",
user,
});
} 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 });
}
};

Expand Down Expand Up @@ -119,3 +125,42 @@ export const updatePassword = async (req: Request, res: Response) => {
});
}
};

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,
});
}
};
24 changes: 9 additions & 15 deletions src/docs/swagger.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
import express from "express";
import { serve, setup } from "swagger-ui-express";
import { env } from "../utils/env";
import {
createUsers,
getUsers,
loginAsUser,
userSchema,
loginSchema,
updatePasswordSchema,
passwordUpdate
} from "./users";
import { createUsers, getUsers, loginAsUser, userSchema, loginSchema, updatePasswordSchema, passwordUpdate, verifyOTPToken } from "./users";

const docRouter = express.Router();

Expand Down Expand Up @@ -52,15 +44,18 @@ const options = {
post: loginAsUser,
},
"/api/v1/users/passwordupdate": {
put: passwordUpdate
}
put: passwordUpdate,
},
"/api/v1/users/2fa-verify": {
post: verifyOTPToken,
},
},

components: {
schemas: {
User: userSchema,
Login: loginSchema,
updatePassword: updatePasswordSchema
updatePassword: updatePasswordSchema,
},
securitySchemes: {
bearerAuth: {
Expand All @@ -71,9 +66,8 @@ const options = {
name: "Authorization",
},
},
}

}
},
};

docRouter.use("/", serve, setup(options));

Expand Down
29 changes: 29 additions & 0 deletions src/docs/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,32 @@ export const passwordUpdate = {
},
},
};
export const verifyOTPToken = {
tags: ["Users"],
summary: "verify OTP token for seller during login process",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
properties: {
token: {
type: "number",
},
},
},
},
},
},
responses: {
200: {
description: "Successfuly logged in ",
},
403: {
description: "forbidden token expired",
},
404: {
description: "Inavalid token or not found",
},
},
};
29 changes: 29 additions & 0 deletions src/email-templates/verifyotp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export const verifyOtpTemplate = (token: number) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Account Verification</title>
</head>
<body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f8f9fa; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
<div style="width: 80%; max-width: 400px; margin:auto; padding: 30px; background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center;">
<h1 style="color: #333333; font-size: 24px; margin-bottom: 20px;">Verify that It's you</h1>
<p style="color: #666666; font-size: 16px; line-height: 1.6; margin-bottom: 20px;"> We noticed a login attempt to your Eagle E-commerce account. If this was you, please verify your new device using the following one-time verification code</p>
<p></>
<div style="display: flex; justify-content: center;width:100%">
<p style="padding: 12px 24px; font-size: 16px; font-weight: bold; color: white; background-color: blue; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease;margin:auto;">${token}</p>
</div>
<p style="color: #999999; font-size: 14px; margin-bottom: 20px;">This verification code is valid for 10 minutes. </p>
<p style="color: #999999; font-size: 14px; margin-bottom: 20px;">If you don't recognize this login attempt, someone may be trying to access your account. We recommend you change your password immediately.</p>
<div style="display: flex; justify-content: center; margin:auto;width:100%">
<p style="font-style: italic; color: #999999;margin:auto">Your account is safe 😎.</p>
</div>
</div>
</body>
</html>
`;
};
22 changes: 22 additions & 0 deletions src/middlewares/isTokenFound.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { NextFunction, Request, Response } from "express";
import Token from "../sequelize/models/Token";

export const isTokenFound = async (req: any, res: Response, next: NextFunction) => {
const { token } = req.body;
try {
const foundToken = await Token.findOne({ where: { token: token } });

if (foundToken) {
req.token = foundToken;
next();
} else {
return res.status(404).json({
message: "Invalid token",
});
}
} catch (error: any) {
return res.status(500).json({
message: error.message,
});
}
};
1 change: 1 addition & 0 deletions src/routes/homeRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Request, Response, Router } from "express";
import Token from "../sequelize/models/Token";

const homeRoute = Router();

Expand Down
6 changes: 3 additions & 3 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { Router } from "express";
import { fetchAllUsers, createUserController, userLogin, updatePassword } from "../controllers/userControllers";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification } from "../controllers/userControllers";
import { emailValidation, validateSchema } from "../middleware/validator";
import signUpSchema from "../schemas/signUpSchema";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { passwordUpdateSchema } from "../schemas/passwordUpdate";
import { otpVerification } from "../controllers/2faControllers";
import { isTokenFound } from "../middlewares/isTokenFound";

const userRoutes = Router();

userRoutes.get("/", fetchAllUsers);
userRoutes.post("/login", userLogin);
userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword);
userRoutes.get("/2fa/verify", otpVerification);
userRoutes.post("/2fa-verify", isTokenFound, tokenVerification);

export default userRoutes;
Loading

0 comments on commit e77f265

Please sign in to comment.