Skip to content

Commit

Permalink
* feat(rbac): Implement role based access control
Browse files Browse the repository at this point in the history
-define roles and permissions for vendors and buyers
-assign roles and permissions to users during registration or profile update
-enforce role-based access control throughout the application
-write comprehensive unit tests

[Delivers #34]

* feat(rbac): integrate rbac into user registration

-integrate role based access control into user registration

[Delivers #34]

* feat(rbac): integrate rbac into user registration

-integrate role based access control into user registration

[Delivers #34]

---------

Co-authored-by: ambroisegithub <muhayimana21@gmail.com>

Social Logins (#45)

* squashing commits

implementing routes for auth

create passport callback function

adding new user from Google

creating new user

check if user is exist in db

implementing cookie session

Fix error of TypeError: req.session.regenerate is not a function using Passport

fix secret keys

remove Google client secret keys

working on facebook strategy

get email from fb login and update the scope

after verification save the user into db

add profile image in db

fixing minor bugs

fix minor bugs in codes

after rebasing & updating some fts

link social login with userModel

Addong Google client keys & FB client key into yml

send confrim email after register a new user

send email after register from facebook

fix minor bugs

* fix minor errors

* remove lints errors

user register

register user test

register user testing fix

register user testing fix

register user testing fix

Authentication for User

Added slint changes

removed  mocha

 added new features

 added new features

Solved comflicts

changed file

added changes

added new Test

added new Test

resolved test cases

resolved test cases

implemented two-factor authentication for enhanced security

implemented two-factor authentication for enhanced security

check whether the usertype is vendor to proceed with 2FA

test the 2fa authentication

add new tests for buyers login

bug-fixes

fixing bugs to remove conflicts with develop

ft-password-recover-and-documentation

This PR corrects some bugs on the user password recover function and add the documentation in th swagger

bug-fixes

fixing bugs on the recover password endpoints

ft-password-rover

Thi PR add a password recover by email feature, it also have a new email templates to send recovering token to email, and finally it resolve color contrast issue on the button nside the email template

ft-password-recover-and-documentation

This PR corrects some bugs on the user password recover function and add the documentation in th swagger

bug-fixes

fixing bugs on the recover password endpoints

bug-fixes

bug-fixes
  • Loading branch information
jkarenzi authored and bertrandshema committed May 20, 2024
1 parent 130f6d0 commit 2983b64
Show file tree
Hide file tree
Showing 9 changed files with 606 additions and 6 deletions.
200 changes: 199 additions & 1 deletion src/__test__/userController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import app from '../app';
import { afterAllHook, beforeAllHook } from './testSetup';
import jwt from 'jsonwebtoken';
import dbConnection from '../database';
import UserModel from '../database/models/userModel';
import UserModel from '../database/models/userModel';

import dotenv from 'dotenv';
dotenv.config();

const userRepository = dbConnection.getRepository(UserModel);

beforeAll(beforeAllHook);
Expand Down Expand Up @@ -331,3 +335,197 @@ describe('User Login Tests', () => {
expect(loginResponse.body.message).toBe('User Not Found');
});
});

describe('update user Profile', () => {
interface IUser {
id: number;
firstName: string;
lastName: string;
email: string;
password?: string;
userType?: Role;
googleId?: string;
facebookId?: string;
picture?: string;
provider?: string;
isVerified: boolean;
twoFactorCode?: number;
}

interface Role {
id: number;
name: string;
permissions: string[];
}


let user: IUser | undefined | null;
const userData = {
firstName: 'jan',
lastName: 'bosco',
email: 'bosco@gmail.com',
password: 'boscoPassword123',
};

beforeEach(async () => {

await request(app).post('/api/v1/register').send(userData);
user = await userRepository.findOne({ where: { email: userData.email } });
});

it('should update the user profile successfully', async () => {
if (user) {
const newUserData = {
firstName: 'NewFirstName',
lastName: 'NewLastName',
email: 'newemail@example.com',
password: 'bosco@gmail.com',
};

const response = await request(app)
.put(`/api/v1/updateProfile/${user?.id}`)
.send(newUserData);
expect(response.statusCode).toBe(201);
expect(response.body.message).toBe('User updated successfully');
}
});

it('should return 404 when user not found', async () => {
const Id = 999;
const response = await request(app)
.put(`/api/v1/updateProfile/${Id}`)
.send(userData);
expect(response.statusCode).toBe(404);
expect(response.body.error).toBe('User not found');
});

it('should return 400 when email already exists', async () => {
if (user) {
const newUserData = {
firstName: 'NewFirstName',
lastName: 'NewLastName',
email: 'newemail@example.com',
password: 'bosco@gmail.com',
};

const response = await request(app)
.put(`/api/v1/updateProfile/${user.id}`)
.send(newUserData);
expect(response.statusCode).toBe(400);
expect(response.body.error).toBe('Email is already taken');
}
});

it('should return 400 when validation fails for user data', async () => {
if (user) {
const emptyData = {};

const response = await request(app)
.put(`/api/v1/updateProfile/${user.id}`)
.send(emptyData);

expect(response.statusCode).toBe(400);
expect(response.body).toBeDefined();
}
});

describe('Password Recover Tests', () => {

it('should generate a password reset token and send an email', async () => {
const userData = {
firstName: 'Test',
lastName: 'User',
email: 'test@gmail.com',
password: 'TestPassword123',
userType: 'vendor'
};

// Register a user
await request(app).post('/api/v1/register').send(userData);

// Find the registered user in the database
const recoverUser = await userRepository.findOne({ where: { email: userData.email } });

// Check if the user is found
if (recoverUser) {
// Generate a recover token using the user's email


// Send a request to the recover endpoint
const response = await request(app)
.post('/api/v1/recover')
.send({ email: recoverUser.email });

// Verify the response
expect(response.status).toBe(200);
expect(response.body.message).toEqual('Password reset token generated successfully');
} else {
// Throw an error if the user is not found
throw new Error('User not found');
}
});

it('should return a 404 error if the user email is not found', async () => {
const nonExistingEmail = 'nonexisting@example.com';

// Send a request to the recover endpoint with a non-existing email
const response = await request(app)
.post('/api/v1/recover')
.send({ email: nonExistingEmail });

// Verify the response
expect(response.status).toBe(404);
expect(response.body.message).toEqual('User not found');
});


it('should update user password with the provided reset token', async () => {
const newPassword = 'NewTestPassword123';

// Generate a user and a recover token
const userData = {
firstName: 'Test',
lastName: 'User',
email: 'test@gmail.com',
password: 'TestPassword123',
userType: 'vendor'
};

await request(app).post('/api/v1/register').send(userData);

const recoverUser = await userRepository.findOne({ where: { email: userData.email } });

if (recoverUser) {
const recoverToken = jwt.sign({ email: recoverUser.email }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });

const response = await request(app)
.post(`/api/v1/recover/confirm?recoverToken=${recoverToken}`)
.send({ password: newPassword });

expect(response.status).toBe(200);
expect(response.body.message).toEqual('Password updated successfully');

const updatedUser = await userRepository.findOne({ where: { email: userData.email } });
expect(updatedUser).toBeDefined();
} else {
throw new Error('User not found');
}
});

it('should return a 401 error for an invalid reset token', async () => {
const invalidResetToken = 'invalid-token';

// Send a request to the updateNewPassword endpoint with an invalid reset token
const response = await request(app)
.post('/api/v1/recover/confirm')
.query({ recoverToken: invalidResetToken })
.send({ password: 'new-password' });

// Verify the response
expect(response.status).toBe(404);
expect(response.body.message).toEqual('Invalid or expired token');
});

});
});

85 changes: 85 additions & 0 deletions src/controller/userController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,88 @@ export const verify2FA = errorHandler(async (req: Request, res: Response) => {
});
return res.status(200).json({ token });
});

export const updateProfile = errorHandler(async (req: Request, res: Response) => {
const userId: number = parseInt(req.params.id);
const { firstName, lastName, email } = req.body as UpdateRrofileRequestBody;

const user = await userRepository.findOne({ where: { id: userId } });

if (!user) {
return res.status(404).json({ error: 'User not found' });
}

user.firstName = firstName || user.firstName;
user.lastName = lastName || user.lastName;


const emailExists = await userRepository.findOne({ where: { email } });

if (emailExists) {
return res.status(400).json({ error: 'Email is already taken' });
}

user.email = email;


const errors = await validate(user);

if (errors.length > 0) {
return res.status(400).json({ errors });
}

await userRepository.save(user);

return res.status(201).json({ message: 'User updated successfully' });
});

export const recoverPassword = errorHandler(async (req: Request, res: Response) => {
const { email } = req.body as { email: string };

const user = await userRepository.findOne({ where: { email } });

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

// Generate a JWT token with the user's email as the payload
const recoverToken = jwt.sign({ email : user.email }, process.env.JWT_SECRET as jwt.Secret, { expiresIn: '1h' });

const confirmLink = `${process.env.APP_URL}/api/v1/recover/confirm?recoverToken=${recoverToken}`;
await sendEmail('confirmPassword', email, { name: user.firstName, link: confirmLink });

return res.status(200).json({ message: 'Password reset token generated successfully', recoverToken });

});

//password Recover Confirmation
export const updateNewPassword = errorHandler(async (req: Request, res: Response) => {
const recoverToken = req.query.recoverToken as string;

const { password } = req.body as { password: string };

if (!recoverToken) {
return res.status(404).json({ message: 'Token is required' });
}

const decoded = jwt.verify(recoverToken, process.env.JWT_SECRET as jwt.Secret) as {
email : string;
};
const user = await userRepository.findOne({
where: { email: decoded.email },
});

if (!user) {
return res.status(404).json({ message: 'User not found' });
}

const hashedPassword : string = await bcrypt.hash(password, 10);
user.password = hashedPassword;

await userRepository.save(user);

return res.status(200).json({ message: 'Password updated successfully' });

});


2 changes: 1 addition & 1 deletion src/database/models/userModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export default class UserModel {
@Column({ default: 'active' })
status: 'active' | 'inactive';

@Column({ nullable: true })
@Column({ nullable: true })
twoFactorCode: number;

constructor(user: Partial<UserModel>) {
Expand Down
Loading

0 comments on commit 2983b64

Please sign in to comment.