Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#23187419173 - fx:fixed Two Factor Authentication for Sellers #35

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading