Skip to content

Commit

Permalink
ft-2FA two factor authentication for seller
Browse files Browse the repository at this point in the history
  • Loading branch information
hozayves committed May 4, 2024
1 parent 512caa3 commit f917b94
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 19 deletions.
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,14 @@
"glob": "^10.3.12",
"jsonwebtoken": "^9.0.2",
"mailgen": "^2.0.28",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.13",
"passport": "^0.7.0",
"passport-google-oauth20": "^2.0.0",
"passport-jwt": "^4.0.1",
"multer": "^1.4.5-lts.1",
"pg": "^8.11.5",
"pg-hstore": "^2.3.4",
"randomstring": "^1.3.0",
"sequelize": "^6.37.2",
"swagger-ui-express": "^5.0.0",
"uuid": "^9.0.1",
Expand All @@ -61,6 +62,7 @@
"@types/nodemailer": "^6.4.14",
"@types/passport": "^1.0.16",
"@types/passport-google-oauth20": "^2.0.14",
"@types/randomstring": "^1.3.0",
"@types/sequelize": "^4.28.20",
"@types/supertest": "^6.0.2",
"@types/swagger-jsdoc": "^6.0.4",
Expand Down Expand Up @@ -91,4 +93,4 @@
"eslint --fix"
]
}
}
}
48 changes: 38 additions & 10 deletions src/controllers/authController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Request, Response, NextFunction } from 'express';
import passport from 'passport';
import jwt from 'jsonwebtoken';
import jwt, { JwtPayload } from 'jsonwebtoken';
import User, { UserAttributes } from '../database/models/user';
import { sendInternalErrorResponse, validateFields } from '../validations';
import logger from '../logs/config';
import { passwordCompare } from '../helpers/encrypt';
import { verifyIfSeller } from '../middlewares/authMiddlewares';
import { createOTPToken, saveOTPDB } from '../middlewares/otpAuthMiddleware';
import { userToken } from '../helpers/token.generator';

const authenticateViaGoogle = (req: Request, res: Response, next: NextFunction) => {
passport.authenticate('google', (err: unknown, user: UserAttributes | null) => {
Expand Down Expand Up @@ -78,20 +81,45 @@ const login = async (req: Request, res: Response): Promise<void> => {
return;
}

// Authenticate user with jwt
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY as string, {
expiresIn: process.env.JWT_EXPIRATION as string,
});

res.status(200).json({
ok: true,
token: token,
});
await verifyIfSeller(user, req, res);
} catch (err: any) {
const message = (err as Error).message;
logger.error(message);
sendInternalErrorResponse(res, err);
}
};
// Function to verify OTP
export const verifyOTP = async (req: Request, res: Response) => {
try {
const data = req.user as JwtPayload;
const token = await userToken(data.id);

res.status(200).json({ ok: true, token });
} catch (error) {
logger.error('VerifyOTP Internal Server Error', error);
sendInternalErrorResponse(res, error);
}
};
// Function to create OTP Token, Save it Postgres,
export const sendOTP = async (req: Request, res: Response, email: string) => {
const userInfo = await User.findOne({ where: { email } });
if (userInfo) {
const { id, email, firstName } = userInfo.dataValues;

const token = await createOTPToken(id, email, firstName);

const otpSaved = await saveOTPDB(id, token);

// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (otpSaved) {
/**
* The token used for comparing the received OTP via email with the
* generated token, which contains the user's ID.
*/
const accessToken = await userToken(id);
res.status(200).json({ ok: true, token: accessToken });
}
}
};

export { login, authenticateViaGoogle };
16 changes: 16 additions & 0 deletions src/database/migrations/20240503195951-addUserCol2FA.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
return queryInterface.addColumn('Users', 'enable2FA', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
});
},

async down(queryInterface, Sequelize) {
return queryInterface.removeColumn('Users', 'enable2FA');
},
};
43 changes: 43 additions & 0 deletions src/database/migrations/20240503201014-OTP.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
await queryInterface.createTable('OTP', {
id: {
type: Sequelize.UUID,
unique: true,
defaultValue: Sequelize.UUIDV4,
allowNull: false,
primaryKey: true,
},
token: {
type: Sequelize.STRING,
allowNull: false,
unique: true,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
references: {
model: 'Users',
key: 'id',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE,
},
});
},

async down(queryInterface, Sequelize) {
await queryInterface.tableExists('OTP');
},
};
51 changes: 51 additions & 0 deletions src/database/models/otp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Optional, UUIDV4 } from 'sequelize';
import { DataTypes, Model } from 'sequelize';
import sequelize from '.';
import User from './user';

export interface OtpAttributes {
id: string;
token: string;
userId?: string;
}

export interface OtpCreationAttributes extends Optional<OtpAttributes, 'id'> {}

class Otp extends Model<OtpAttributes, OtpCreationAttributes> implements OtpAttributes {
public id!: string;
public token!: string;
public userId!: string | undefined;
public readonly createdAt: Date | undefined;
public readonly updatedAt: Date | undefined;
}
Otp.init(
{
id: {
type: DataTypes.UUID,
defaultValue: UUIDV4,
allowNull: false,
unique: true,
primaryKey: true,
},
token: {
type: DataTypes.STRING,
allowNull: false,
},
userId: {
type: DataTypes.UUID,
defaultValue: UUIDV4,
references: {
model: User,
key: 'id',
},
},
},
{
sequelize,
timestamps: true,
tableName: 'OTP',
}
);
Otp.belongsTo(User, { foreignKey: 'userId' });

export default Otp;
7 changes: 7 additions & 0 deletions src/database/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface UserAttributes {
createdAt?: Date;
updatedAt?: Date;
RoleId?: string;
enable2FA?: boolean;
}

export interface UserCreationAttributes extends Optional<UserAttributes, 'id'> {}
Expand All @@ -42,6 +43,7 @@ class User extends Model<UserAttributes, UserCreationAttributes> implements User
public verified!: boolean;
public status!: UserStatus;
public RoleId!: string | undefined;
public enable2FA?: boolean | undefined;
public readonly createdAt!: Date | undefined;
public readonly updatedAt!: Date | undefined;
}
Expand Down Expand Up @@ -120,6 +122,11 @@ User.init(
key: 'id',
},
},
enable2FA: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
},
},
{ sequelize: sequelize, timestamps: true }
);
Expand Down
59 changes: 58 additions & 1 deletion src/helpers/send-email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ interface IData {
email: string;
name: string;
link?: string;
otp?: string;
}
interface EmailContent {
name: string;
intro: string;
otp: {
bold: boolean;
content: string;
};
outro: string;
}

const { EMAIL, PASSWORD } = process.env;
Expand Down Expand Up @@ -142,9 +152,56 @@ export const sendEmail = async (type: string, data: IData) => {
mailOptions.subject = 'Reset password';
mailOptions.html = mailGenerator.generate(email);
break;
case 'OTP':
const emailContent = `
<html>
<head>
<style>
/* CSS styles */
body {
font-family: Arial, sans-serif;
line-height: 1.3;
background-color: #f4f4f4;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: #fff;
border-radius: 8px;
padding: 40px;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.1);
}
.otp {
font-size: 24px;
font-weight: bold;
color: #007bff;
}
.footer {
font-size: 14px;
color: #666;
}
</style>
</head>
<body>
<div class="container">
<h1 style="text-align: center; color: #007bff;">Verification Code</h1>
<p style="text-align: center;">Hello ${data.name},</p>
<p style="text-align: center;">Your verification code is:</p>
<p class="otp" style="text-align: center;">${data.otp}</p>
<p style="text-align: center;">Use this code to verify your account.</p>
<p style="text-align: center;">Regards,<br/>Mavericks Team</p>
</div>
</body>
</html>
`;

mailOptions.subject = 'Verification code';
mailOptions.html = emailContent;
break;
}
const info = await transporter.sendMail(mailOptions);
logger.info(info);
logger.info('Send Mailer', info);
} catch (error) {
if (error instanceof Error) logger.error(error.message);
}
Expand Down
15 changes: 10 additions & 5 deletions src/helpers/token.generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import dotenv from 'dotenv';
dotenv.config();
export interface UserPayload {
id: string;
email: string;
email?: string;
}
// Function to generate token
export const userToken = async (userId: string, userEmail: string) => {
export const userToken = async (userId: string, userEmail?: string) => {
const payload: UserPayload = {
id: userId,
email: userEmail,
email: userEmail ?? undefined,
};
const token: string = jwt.sign(payload, process.env.SECRET_KEY as string, {
expiresIn: process.env.JWT_EXPIRATION as string,
Expand All @@ -19,6 +19,11 @@ export const userToken = async (userId: string, userEmail: string) => {
return token;
};

// Function for token verification
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const verifyToken = (token: string, results?: any) => {
return jwt.verify(token, process.env.SECRET_KEY as string, results);
};

// Function for token Decode
export const decodedToken = (token: string) => {
return jwt.decode(token);
};
23 changes: 22 additions & 1 deletion src/middlewares/authMiddlewares.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import { Request, Response, NextFunction } from 'express';
import { config } from 'dotenv';
import jwt from 'jsonwebtoken';

import logger from '../logs/config';
import User from '../database/models/user';
import Role from '../database/models/role';
import { sendInternalErrorResponse } from '../validations';
import { sendOTP } from '../controllers/authController';

config();

Expand Down Expand Up @@ -69,3 +69,24 @@ export const checkUserRoles = (requiredRole: string) => {
next();
};
};

export const verifyIfSeller = async (user: any, req: Request, res: Response) => {
const userRoleId = await user.dataValues.RoleId;
const userRole = await Role.findOne({ where: { id: userRoleId } });

// Check if it's seller
if (userRole?.dataValues.name === 'seller') {
// Find seller information
sendOTP(req, res, user.dataValues.email);
} else {
// Authenticate user with jwt
const token = jwt.sign({ id: user.id }, process.env.SECRET_KEY as string, {
expiresIn: process.env.JWT_EXPIRATION as string,
});

res.status(200).json({
ok: true,
token: token,
});
}
};
Loading

0 comments on commit f917b94

Please sign in to comment.