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

add new tests for buyers login #63

Merged
merged 1 commit into from
May 15, 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: 0 additions & 2 deletions .github/workflows/workflow_for_ecomm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,3 @@ jobs:
FACEBOOK_APP_SECRET: ${{ secrets.FACEBOOK_APP_SECRET }}
FACEBOOK_CALLBACK_URL: ${{ secrets.FACEBOOK_CALLBACK_URL }}
COOKIES_KEY: ${{ secrets.COOKIES_KEY }}
EMAIL_USER: ${{ secrets.EMAIL_USER }}
EMAIL_PASS: ${{ secrets.EMAIL_PASS }}
588 changes: 578 additions & 10 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"jest": "^29.7.0",
"joi": "^17.13.1",
"jsonwebtoken": "^9.0.2",
"mailgun-js": "^0.22.0",
"morgan": "^1.10.0",
"nodemailer": "^6.9.13",
"nodemon": "^3.1.0",
Expand Down Expand Up @@ -72,6 +73,7 @@
"@types/express": "^4.17.21",
"@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.6",
"@types/mailgun-js": "^0.22.18",
"@types/morgan": "^1.9.9",
"@types/nodemailer": "^6.4.15",
"@types/passport": "^1.0.16",
Expand Down
10 changes: 7 additions & 3 deletions src/__test__/testSetup.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { DbConnection } from '../database/index';
import UserModel from '../database/models/userModel';

import { Role } from '../database/models';

export async function beforeAllHook() {
await DbConnection.instance.initializeDb();
// removing all data from role table

// Get repositories
const userRepository = await DbConnection.connection.getRepository(UserModel);
const roleRepository = await DbConnection.connection.getRepository(Role);

// Delete all users and roles
await userRepository.createQueryBuilder().delete().execute();
await roleRepository.createQueryBuilder().delete().execute();
}

Expand All @@ -17,4 +21,4 @@ export async function afterAllHook() {
console.log(repository);

await DbConnection.instance.disconnectDb();
}
}
58 changes: 42 additions & 16 deletions src/__test__/userController.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import request from 'supertest';
import app from '../app';
// import { Role } from '../database/models';
import { afterAllHook, beforeAllHook } from './testSetup';
import jwt from 'jsonwebtoken';
import dbConnection from '../database';
// import bcrpt from 'bcrypt';
import UserModel from '../database/models/userModel';
// import { use } from 'passport';
const userRepository = dbConnection.getRepository(UserModel);

beforeAll(beforeAllHook);
Expand Down Expand Up @@ -151,7 +148,7 @@ describe('User Registration Tests', () => {


describe('User Login Tests', () => {
it('should log in a user with valid credentials', async () => {
it('should log in a vendor with valid credentials', async () => {
const formData = {
name: 'Vendor',
permissions: ['test-permission1', 'test-permission2'],
Expand Down Expand Up @@ -180,11 +177,8 @@ describe('User Login Tests', () => {

expect(loginResponse.status).toBe(200);
expect(loginResponse.body.message).toBe('Please provide the 2FA code sent to your email.');
} else {
throw new Error('User not found');
}
});


it('should verify the 2FA code for a vendor user', async () => {
const userData = {
Expand All @@ -203,11 +197,7 @@ describe('User Login Tests', () => {
if (user) {
user.isVerified = true;
await userRepository.save(user);
} else {
throw new Error('User not found');
}


const loginResponse = await request(app).post('/api/v1/login').send({
email: userData.email,
password: userData.password,
Expand All @@ -228,12 +218,50 @@ describe('User Login Tests', () => {

expect(verifyResponse.status).toBe(200);
expect(verifyResponse.body).toHaveProperty('token');
} else {
throw new Error('User not found');
}
});


it('should log in a buyer with valid credentials', async () => {
const formData = {
name: 'Buyer',
permissions: ['test-permission1', 'test-permission2'],
};

// Create the role first
const roleResponse = await request(app).post('/api/v1/roles/create_role').send(formData);

const userData = {
firstName: 'Test',
lastName: 'User',
email: 'test2@gmail.com',
password: 'TestPassword123',
userType: roleResponse.body.id
};
await request(app).post('/api/v1/register').send(userData);

const updatedUser = await userRepository.findOne({ where: { email: userData.email } });
if (updatedUser) {
updatedUser.isVerified = true;
await userRepository.save(updatedUser);

const loginResponse = await request(app).post('/api/v1/login').send({
email: userData.email,
password: userData.password,
});

expect(loginResponse.status).toBe(200);
expect(loginResponse.body.token).toBeDefined();
expect(loginResponse.body.message).toBe('Buyer Logged in successfully');

// Decode the token and check its properties
const decodedToken = jwt.decode(loginResponse.body.token);
expect(decodedToken).toHaveProperty('userId');
expect(decodedToken).toHaveProperty('iat');
expect(decodedToken).toHaveProperty('exp');
}
});

it('should return a 401 status code if the email is not verified', async () => {
const userData = {
firstName: 'Test',
Expand All @@ -257,9 +285,7 @@ describe('User Login Tests', () => {

expect(loginResponse.status).toBe(401);
expect(loginResponse.body.message).toBe('Please verify your email. Confirmation link has been sent.'); // Corrected message
} else {
throw new Error('User not found');
}
}
});

it('should return a 401 status code if the password does not match', async () => {
Expand Down
121 changes: 50 additions & 71 deletions src/controller/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import UserModel from '../database/models/userModel';
import sendEmail from '../emails/index';
import { sendCode } from '../emails/mailer';
import jwt from 'jsonwebtoken';
import errorHandler from '../middlewares/errorHandler'

// Assuming dbConnection.getRepository(UserModel) returns a repository instance
const userRepository = dbConnection.getRepository(UserModel);
Expand Down Expand Up @@ -167,83 +168,61 @@ export const deleteUser = async (req: Request, res: Response) => {
}}


export const Login = async (req: Request, res: Response) => {
try {
const user = await userRepository.findOne({
where: { email: req.body['email'] },
relations: ['userType']
});
if (!user) {
return res.status(404).send({ message: 'User Not Found' });
}
const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database
if (!passwordMatch) {
return res.status(401).send({ message: 'Password does not match' });
}
if (!user.isVerified) {
// Send confirmation email if user is not verified
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET as jwt.Secret,
{ expiresIn: '1d' }
);
const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`;
await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink });
return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' });
}

// If user is a vendor, proceed with 2FA
if (user.userType.name === 'Vendor') {
// Generate a new 2FA code
const twoFactorCode = Math.floor(100000 + Math.random() * 900000);

// Store the 2FA code in the user's record in the database
await userRepository.update(user.id, { twoFactorCode });

// Send 2FA code to user's email
await sendCode(
user.email,
'Your 2FA Code',
'./templates/2fa.html',
{ name: user.firstName, twoFactorCode: twoFactorCode.toString() }
);

// Respond with a message asking for the 2FA code
res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' });
} else {
// If user is not a vendor,
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
res.status(200).json({ token });
}
} catch (error) {
res.status(500).send(error);
export const Login = errorHandler(async (req: Request, res: Response) => {
const user = await userRepository.findOne({
where: { email: req.body['email'] },
relations: ['userType']
});
if (!user) {
return res.status(404).send({ message: 'User Not Found' });
}
const passwordMatch = await bcrypt.compare(req.body.password, user.password); // Compare with hashed password from the database
if (!passwordMatch) {
return res.status(401).send({ message: 'Password does not match' });
}
if (!user.isVerified) {
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET as jwt.Secret,
{ expiresIn: '1d' }
);
const confirmLink = `${process.env.APP_URL}/api/v1/confirm?token=${token}`;
await sendEmail('confirm', user.email, { name: user.firstName, link: confirmLink });
return res.status(401).send({ message: 'Please verify your email. Confirmation link has been sent.' });
}
}

export const verify2FA = async (req: Request, res: Response): Promise<void> => {
try {
const { code } = req.body;
const { userId } = req.params;
if (user.userType.name === 'Vendor') {
const twoFactorCode = Math.floor(100000 + Math.random() * 900000);

// Use the repository to find the user by their id
const user = await userRepository.findOne({ where: { id: Number(userId) } });
await userRepository.update(user.id, { twoFactorCode });

if (!user) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
if (code !== user.twoFactorCode) {
res.status(401).json({ error: 'Invalid code' });
return;
}
await sendCode(
user.email,
'Your 2FA Code',
'./templates/2fa.html',
{ name: user.firstName, twoFactorCode: twoFactorCode.toString() }
);

// Generate JWT
res.status(200).json({ message: 'Please provide the 2FA code sent to your email.' });
} else {
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
res.status(200).json({ token, message: 'Buyer Logged in successfully'});
}
});

// Send JWT to the user
res.status(200).json({ token });
export const verify2FA = errorHandler(async (req: Request, res: Response) => {
const { code } = req.body;
const { userId } = req.params;

} catch (error) {
res.status(500).json({ error: (error as Error).message });
const user = await userRepository.findOne({ where: { id: Number(userId) } });

if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
if (code !== user.twoFactorCode) {
return res.status(401).json({ error: 'Invalid code' });
}
};

const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });
return res.status(200).json({ token });
});
6 changes: 2 additions & 4 deletions src/database/models/userModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
ManyToOne,
} from 'typeorm';
import { Role } from './roleEntity';

Expand All @@ -25,8 +24,7 @@ export default class UserModel {
@Column({ nullable: true })
password: string;

@OneToOne(() => Role, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
@JoinColumn()
@ManyToOne(() => Role)
userType: Role;

@Column({ nullable: true })
Expand Down
29 changes: 17 additions & 12 deletions src/emails/mailer.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import nodemailer from 'nodemailer';
import mailgun from 'mailgun-js';
import fs from 'fs';
import path from 'path';
import dotenv from 'dotenv';
dotenv.config()

const mg = mailgun({
apiKey: process.env.MAILGUN_TOKEN || 'default_api_key',
domain: process.env.MAILGUN_DOMAIN || 'default_domain'
});
export async function sendCode(to: string, subject: string, htmlTemplatePath: string, replacements: Record<string, string>) {
const template = await fs.promises.readFile(path.resolve(__dirname, htmlTemplatePath), 'utf8');

let html = template;
for (const placeholder in replacements) {
html = html.replace(new RegExp(`{{${placeholder}}}`, 'g'), replacements[placeholder]);
}

const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.EMAIL_USER,
pass: process.env.EMAIL_PASS
}
});

const mailOptions = {
from: 'dynamitesecommerce@gmail.com',
to,
Expand All @@ -26,5 +23,13 @@ export async function sendCode(to: string, subject: string, htmlTemplatePath: st
};

// Send the email
return transporter.sendMail(mailOptions);
}
return new Promise((resolve, reject) => {
mg.messages().send(mailOptions, (error, body) => {
if (error) {
reject(error);
} else {
resolve(body);
}
});
});
}
18 changes: 9 additions & 9 deletions src/middlewares/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { Request, Response, NextFunction } from 'express';
import { Request, Response } from 'express';

type MiddlewareFunction = (req: Request, res: Response, next: NextFunction) => Promise<void>;
type MiddlewareFunction = (req: Request, res: Response) => Promise<Response<Record<string, unknown>> | undefined>;

function errorHandler(func: MiddlewareFunction) {
return async (req: Request, res: Response, next: NextFunction) => {
function errorHandler(func: MiddlewareFunction): MiddlewareFunction {
return async (req: Request, res: Response) => {
try {
await func(req, res, next);
} catch (error) { // Removed the type annotation from the catch clause variable because it caused liting errors
const message = error.detail || 'Internal Server Error';
res.status(500).send(message);
return await func(req, res);
} catch (error) {
const message = (error as { detail?: string }).detail || 'Internal Server Error';
return res.status(500).send(message);
}
};
}

export default errorHandler;
export default errorHandler;
Loading