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 #43

Merged
merged 1 commit into from
May 4, 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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ TEST_DB = ""-->TODO: put your own testing database connection string here
JWT_SECRET = ""-->TODO: put your own jsonwebtoken scret here
CLOUD_NAME =""
CLOUD_KEY = ""
CLOUD_SCRET = ""
CLOUD_SECRET = ""
SMTP_HOST = ""
SMTP_USER = ""
SMTP_PASS = ""
Expand Down
4 changes: 2 additions & 2 deletions __test__/product.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ describe("Testing product Routes", () => {
try {
await connect();
const testAdmin = {
name: "admin",
username: "admin",
name: "admin123",
username: "admin123",
email: "[email protected]",
password: await bcrypt.hash("password", 10),
roleId: 3
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"migrate": "npx sequelize-cli db:migrate",
"migrate:test": "npx sequelize-cli db:migrate --env test",
"seed": "npx sequelize-cli db:seed:all",
"undo:migrate" : "npx sequelize-cli db:migrate:undo:all",
"undo:migrate": "npx sequelize-cli db:migrate:undo:all",
"lint:fix": "npx eslint --fix .",
"test": "cross-env NODE_ENV=test npm run migrate && jest --detectOpenHandles --coverage",
"prepare": "husky",
Expand Down Expand Up @@ -86,6 +86,7 @@
"express": "^4.19.2",
"express-session": "^1.18.0",
"husky": "^9.0.11",
"ioredis": "^5.4.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you using this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i was about to use it but realy i'm not using it currently, since token don't last long(10min) i decided not to blacklist verification token s

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove it then

"joi": "^17.13.0",
"jsonwebtoken": "^9.0.2",
"lint-staged": "^15.2.2",
Expand All @@ -100,4 +101,3 @@
"swagger-ui-express": "^5.0.0"
}
}

100 changes: 67 additions & 33 deletions src/controllers/userControllers.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { Request, Response } from "express";
import * as userService from "../services/user.service";
import { generateToken } from "../utils/jsonwebtoken";
import { decodeMagicLinkToken, generateMagicLinkToken, generateToken } from "../utils/jsonwebtoken";
import * as mailService from "../services/mail.service";
import { IUser, STATUS, SUBJECTS } from "../types";
import { comparePasswords } from "../utils/comparePassword";
import { createUserService, getUserByEmail, updateUserPassword,loggedInUser } from "../services/user.service";
import { hashedPassword } from "../utils/hashPassword";
import Token, { TokenAttributes } from "../sequelize/models/Token";
import jwt from "jsonwebtoken"
import User from "../sequelize/models/users";
import { verifyOtpTemplate } from "../email-templates/verifyotp";

import { getProfileServices, updateProfileServices } from "../services/user.service";
import uploadFile from "../utils/handleUpload";
import { updateUserRoleService } from "../services/user.service";
import { generateRandomNumber } from "../utils/generateRandomNumber";
import { env } from "../utils/env";


export const fetchAllUsers = async (req: Request, res: Response) => {
try {
const users = await userService.getAllUsers();
Expand Down Expand Up @@ -56,13 +59,15 @@ export const userLogin = async (req: Request, res: Response) => {
} else {
// @ts-ignore
if (user.userRole.name === "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);
const otp = generateRandomNumber();
const token = await generateMagicLinkToken(otp, user);
const link =
process.env.NODE_ENV !== "production"? `${env.local_url}/${token}`: `${env.remote_url}/${token}`;
await mailService.sendEmailService(user, SUBJECTS.VERIFY_LOGIN, verifyOtpTemplate(link,otp));
return res.status(200).json({
status: STATUS.PENDING,
message: "OTP verification code has been sent ,please use it to verify that it was you",
token,
});
} else {
const userInfo = {
Expand Down Expand Up @@ -142,43 +147,31 @@ export const updatePassword = async (req: Request, res: Response) => {
};


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

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 } });

const decoded = await decodeMagicLinkToken(token);
//@ts-ignore
const { otp, userId } = decoded;
if (otp) {
const user = await User.findOne({ where: { id: userId }, attributes: { exclude: ["password"] } });
//@ts-ignore
const accessToken = await generateToken(user);
return res.status(200).json({
message: "Login successful",
token,
user,
message: "logged in successfuly",
token: accessToken,
});
} else {
return res.status(404).json({
message: "User not found",
return res.status(401).json({
message: "Token expired",
});
}
} catch (error: any) {
return res.status(500).json({
message: error.message,
});
}
}
};
export const handleSuccess = async (req: Request, res: Response) => {
// @ts-ignore
Expand Down Expand Up @@ -300,3 +293,44 @@ export const updateUserRole = async (req: Request, res: Response) => {
res.status(500).json({ message: 'Role or User Not Found' });
}
};


export const otpVerification = async (req: Request, res: Response) => {
const { token } = req.query;
const {otp} = req.body

try {
//@ts-ignore
const { otp: initialOtp, userId } = await decodeMagicLinkToken(token as string)
if (!initialOtp) {
return res.status(403).json({
message: "Token expired"
});
}

if (otp === initialOtp) {
const user = await User.findOne({ where: { id: userId }, attributes: { exclude: ["password"] } });
//@ts-ignore
const accessToken = await generateToken(user);

return res.status(200).json({
message: "Logged in successfully",
token: accessToken
});
} else {
return res.status(401).json({
message: "Invalid OTP",
});
}
} catch (error:any) {
if (error instanceof jwt.TokenExpiredError) {
return res.status(403).json({
message: "JWT token expired"
});
} else {
return res.status(500).json({
message: error.message,
});
}
}
};
15 changes: 13 additions & 2 deletions src/docs/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,14 +248,25 @@ export const passwordUpdate = {
};
export const verifyOTPToken = {
tags: ["Users"],
summary: "verify OTP token for seller during login process",
summary: "OTP verification",
parameters: [
{
name: "token",
in: "query",
required: true,
description: "jwt token",
schema: {
type: "string",
},
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
properties: {
token: {
otp: {
type: "number",
},
},
Expand Down
55 changes: 32 additions & 23 deletions src/email-templates/verifyotp.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,38 @@
export const verifyOtpTemplate = (token: number) => {
export const verifyOtpTemplate = (link: string,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>
<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>


<div style="margin-bottom: 20px;">
<p style="padding: 8px; font-size: 16px; font-weight: bold; background-color: blue; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease; display: inline-block; max-width: 200px; color: white;">${token}</p>
</div>

<p>OR</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 style="margin-bottom: 20px;">
<a href="${link}" style="padding: 12px 24px; text-decoration: none; font-size: 16px; font-weight: bold; background-color: yellow; border: none; border-radius: 5px; cursor: pointer; transition: background-color 0.3s ease; display: inline-block; max-width: 100%;">Click here to verify</a>
</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;">
<p style="font-style: italic; color: #999999;">Your account is safe 😎.</p>
</div>
</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>
</body>
</html>


`;
};
6 changes: 3 additions & 3 deletions src/routes/userRoutes.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Router } from "express";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController } from "../controllers/userControllers";
import { fetchAllUsers, createUserController, userLogin, updatePassword, tokenVerification, handleSuccess, handleFailure,updateProfileController, getProfileController, otpVerification } from "../controllers/userControllers";
import { emailValidation, validateSchema } from "../middlewares/validator";
import { isLoggedIn } from "../middlewares/isLoggedIn";
import { passwordUpdateSchema } from "../schemas/passwordUpdate";
import { isTokenFound } from "../middlewares/isTokenFound";
import { authenticateUser, callbackFn } from "../services/user.service";
require("../auth/auth");
import logInSchema from "../schemas/loginSchema";
Expand All @@ -26,7 +25,8 @@ userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchem
userRoutes.post("/login", emailValidation,validateSchema(logInSchema),userLogin);
userRoutes.post("/register", emailValidation, validateSchema(signUpSchema), createUserController);
userRoutes.put("/passwordupdate", isLoggedIn, validateSchema(passwordUpdateSchema), updatePassword);
userRoutes.post("/2fa-verify", isTokenFound, tokenVerification);
userRoutes.get("/2fa-verify/:token",tokenVerification);
Comment on lines 27 to +28
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you verifying from get method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oooh I missed this

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again for that get its verification link that user click and u can't use post using link since it will be opened directly in browser
perhaps when we put fronted url its possibl

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will be handled from front-end tha means link will be front-end link, but leave it for now

userRoutes.post("/2fa-verify",otpVerification);
userRoutes.get('/profile',
isLoggedIn,
getProfileController
Expand Down
7 changes: 2 additions & 5 deletions src/services/mail.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import Token from "../sequelize/models/Token";
import { IUser, SUBJECTS } from "../types";
import { IUser } from "../types";
import { env } from "../utils/env";
import { generateMagicLinkToken } from "../utils/jsonwebtoken";
import transporter from "../utils/transporter";
import { verifyOtpTemplate } from "../email-templates/verifyotp";

export const sendEmailService = async (user: IUser, subject: string, template: any, token: number) => {
export const sendEmailService = async (user: IUser, subject: string, template: any) => {
try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you think this function can be re-used , why that user can you put email instead whole user object

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes its reusable i removed token since token doesn't have naything todo with that sendEmailService, token or any other related info has be passed as argument in emailTemplate not in service

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also pass email to send to not whole user

const mailOptions = {
from: env.smtp_user,
Expand Down
3 changes: 1 addition & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ export interface IUser {

export enum SUBJECTS {
REQUEST_2FA = "Request for 2FA",
CONFIRM_2FA = "Confirm 2-Factor Authentication",
VERIFY_LOGIN = "Verify that It's you",
DISABLE_2FA = "Disable 2-Factor Authentication",
CONFIRM_OTP = "Verify that It's you",
}

export enum STATUS {
Expand Down
2 changes: 2 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,6 @@ export const env = {
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
callbackURL: process.env.GOOGLE_CALLBACK_URL as string,
remote_url: `${process.env.REMOTE_URL}/api/v1/users/2fa-verify`,
local_url: `${process.env.LOCAL_URL}:${process.env.PORT}/api/v1/users/2fa-verify`
};
4 changes: 4 additions & 0 deletions src/utils/generateRandomNumber.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const generateRandomNumber = () => {
const otp = Math.floor(100000 + Math.random() * 900000);
return otp;
};
8 changes: 4 additions & 4 deletions src/utils/jsonwebtoken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ export const decodeToken = async (token: string) => {
return decoded;
};

export const generateMagicLinkToken = async (user: IUser) => {
const token = sign({ email: user.email }, `${env.jwt_secret}`, {
expiresIn: "5m",
export const generateMagicLinkToken = async (otp: number, user: IUser) => {
const token = sign({ otp, userId: user.id }, `${env.jwt_secret}`, {
expiresIn: "10m",
});
return token;
};

export const verifyMagicLinkToken = async (token: string) => {
export const decodeMagicLinkToken = async (token: string) => {
try {
const decoded = verify(token, `${env.jwt_secret}`);
return decoded;
Expand Down
Loading