Skip to content

Commit

Permalink
fix: fixed 2f authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
soleil00 committed May 3, 2024
1 parent aa23d2c commit 671f0ae
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 72 deletions.
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",
"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"
}
}

102 changes: 69 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,17 @@ 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"
? `http://localhost:${env.port}/api/v1/users/2fa-verify/${token}`
: `https://eagles-ec-be-development.onrender.com/api/v1/users/2fa-verify/${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 +149,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 +295,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,
});
}
}
};
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);
userRoutes.get("/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 {
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
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

0 comments on commit 671f0ae

Please sign in to comment.