Skip to content

Commit

Permalink
#163519140 add reset password via email endpoint (#15)
Browse files Browse the repository at this point in the history
* reset-password(user): reset password reset via email

- forget password endpoint
- send reset password email to a user
- reset password endpoint
- send confirmation email to a user
- unit test
- increase test coverage
- change response body
- change status code
- changing emails response

[Delivers #163519140]
  • Loading branch information
mcaleb808 authored and kimotho-njoki committed Feb 18, 2019
1 parent 5f54a40 commit 7322627
Show file tree
Hide file tree
Showing 10 changed files with 377 additions and 16 deletions.
14 changes: 14 additions & 0 deletions __tests__/controllers/MailController.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,18 @@ describe('sendgrid', () => {
expect(res.length).toBeGreaterThan(0);
expect(res[0].statusCode).toBe(202);
}, 30000);

test('should send the Reset password link email', async () => {
expect.assertions(2);
const res = await MailController.resetPasswordEmail(db.mailUser);
expect(res).toBeDefined();
expect(res[0].statusCode).toBe(202);
}, 30000);

test('should send password changed email', async () => {
expect.assertions(2);
const res = await MailController.newPasswordEmail(db.mailUser);
expect(res).toBeDefined();
expect(res[0].statusCode).toBe(202);
}, 30000);
});
103 changes: 102 additions & 1 deletion __tests__/routes/user.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import request from 'supertest';
import app from '../../app';
import { urlPrefix } from '../mocks/variables.json';
import { User } from '../../database/models';
import { User, ResetPassword } from '../../database/models';
import { signupUser } from '../mocks/db.json';

const fakeConfirmationCode = '07e83585-41e5-4fb2-b5d0-a7b52b55aba1';
Expand All @@ -14,6 +14,11 @@ describe('users', () => {
});
user = await User.create({ ...signupUser });
});

afterAll(async () => {
await ResetPassword.destroy({ where: { userId: user.id } });
});

test('should return invalid confirmation code -fake userid and confirmationCode', async () => {
expect.assertions(2);
const res = await request(app).get(
Expand Down Expand Up @@ -51,4 +56,100 @@ describe('users', () => {
expect(res.status).toBe(401);
expect(res.body.message).toBe('test@email.com has already been confirmed');
});

test('Password reset link sent sucessfully', async () => {
expect.assertions(2);
const res = await request(app)
.post(`${urlPrefix}/users/forget`)
.send({ user: { email: 'test@email.com' } });
expect(res.status).toBe(201);
expect(res.body.message).toBe('Password reset link sent sucessfully. Please check your email!');
}, 30000);

test('No user found with that email address', async () => {
expect.assertions(2);
const res = await request(app)
.post(`${urlPrefix}/users/forget`)
.send({ user: { email: 'fake@email.com' } });
expect(res.status).toBe(404);
expect(res.body.message).toBe('No user found with that email address');
});

test('Bad request- password forget', async () => {
expect.assertions(2);
const res = await request(app)
.post(`${urlPrefix}/users/forget`)
.send({ user: { emailx: 'fake@email.com' } });
expect(res.status).toBe(400);
expect(res.body.message).toBe('Bad Request');
});

test('Bad request- password forget', async () => {
expect.assertions(2);
const res = await request(app)
.post(`${urlPrefix}/users/forget`)
.send({ user: { emailx: 'fake' } });
expect(res.status).toBe(400);
expect(res.body.message).toBe('Bad Request');
});

test('No user found with that email address- Unconfirmed email', async () => {
expect.assertions(2);
await user.update({ confirmed: 'pending' });
const res = await request(app)
.post(`${urlPrefix}/users/forget`)
.send({ user: { email: 'test@email.com' } });
expect(res.status).toBe(404);
expect(res.body.message).toBe('No user found with that email address');
});

test('Reset password- fail', async () => {
expect.assertions(2);
const reset = await ResetPassword.findOne({ where: { userId: user.id } });
const res = await request(app)
.put(`${urlPrefix}/users/${reset.userId}/reset/${reset.resetCode}`)
.send({ newPassword: 'mugiha', confirmNewpassword: 'mugisha' });
expect(res.status).toBe(400);
expect(res.body.message).toBe("Passwords don't match");
});

test('Reset password- Bad request', async () => {
expect.assertions(2);
const reset = await ResetPassword.findOne({ where: { userId: user.id } });
const res = await request(app)
.put(`${urlPrefix}/users/${reset.userId}/reset/${reset.resetCode}`)
.send({ newPassword: 'mugisha', confirmpassword: 'mugisha' });
expect(res.status).toBe(400);
expect(res.body.message).toBe('Bad Request');
});

test('Reset password- Bad request', async () => {
expect.assertions(2);
const reset = await ResetPassword.findOne({ where: { userId: user.id } });
const res = await request(app)
.put(`${urlPrefix}/users/${reset.userId}/reset/${reset.resetCode}`)
.send({ newPassword: 'mug', confirmpassword: 'mug' });
expect(res.status).toBe(400);
expect(res.body.message).toBe('Bad Request');
});

test('Reset password- invalid token', async () => {
expect.assertions(2);
const reset = await ResetPassword.findOne({ where: { userId: user.id } });
const res = await request(app)
.put(`${urlPrefix}/users/${reset.userId}/reset/7ced290d-f51a-472c-8086-7e8161fc40b9`)
.send({ newPassword: 'mugisha', confirmNewpassword: 'mugisha' });
expect(res.status).toBe(404);
expect(res.body.message).toBe('invalid token');
});

test('Reset password- success', async () => {
expect.assertions(2);
const reset = await ResetPassword.findOne({ where: { userId: user.id } });
const res = await request(app)
.put(`${urlPrefix}/users/${reset.userId}/reset/${reset.resetCode}`)
.send({ newPassword: 'mugisha', confirmNewpassword: 'mugisha' });
expect(res.status).toBe(200);
expect(res.body.message).toBe('Your password has been reset successfully!');
}, 30000);
});
91 changes: 89 additions & 2 deletions controllers/AuthController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import jwt from 'jsonwebtoken';
import 'dotenv/config';
import { Op } from 'sequelize';
import bcrypt from 'bcrypt';
import { User } from '../database/models';
import { sendEmailConfirmationLink } from './MailController';
import { User, ResetPassword } from '../database/models';
import { sendEmailConfirmationLink, resetPasswordEmail, newPasswordEmail } from './MailController';

const { JWT_SECRET } = process.env;

Expand Down Expand Up @@ -78,5 +78,92 @@ class AuthController {
}
})(req, res, next);
}

/**
* @author Caleb
* @param {Object} req
* @param {Object} res
* @param {*} next
* @returns {Object} Returns the response
*/
static async forgotPassword(req, res) {
const {
body: { user }
} = req;

try {
const reset = await User.findOne({
where: { email: user.email, confirmed: 'confirmed' },
attributes: ['id', 'email']
});
if (!reset) {
return res
.status(404)
.json({ status: 404, message: 'No user found with that email address' });
}
const { id, email } = reset.get();
const createReset = await ResetPassword.create({ userId: id });
const { resetCode } = createReset.get();
await resetPasswordEmail(id, email, resetCode);
res.status(201).json({
status: 201,
message: 'Password reset link sent sucessfully. Please check your email!'
});
} catch (error) {
return res.status(520).json({ message: 'Please try again' });
}
}

/**
* @author Caleb
* @param {Object} req
* @param {Object} res
* @param {*} next
* @returns {Object} Returns the response
*/
static async resetPassword(req, res) {
const { resetCode, userId } = req.params;
const { body } = req;
try {
const reset = await ResetPassword.findOne({
where: { resetCode, userId }
});

if (body.newPassword !== body.confirmNewpassword) {
res.status(400).json({ status: 400, message: "Passwords don't match" });
}
if (!reset) {
res.status(404).json({ status: 404, message: 'invalid token' });
}

if (body.newPassword === body.confirmNewpassword) {
const expirationTime = reset.createdAt.setMinutes(reset.createdAt.getMinutes() + 30);
const presentTime = new Date();

if (expirationTime < presentTime) {
res
.status(401)
.json({ status: 401, message: 'Expired token, Please request a new Token' });
}

if (expirationTime > presentTime) {
const user = await User.findOne({
where: { id: userId },
attributes: ['id', 'email']
});

const password = await bcrypt.hash(body.newPassword, 10);
await user.update({ password });
await newPasswordEmail(user.email);
res.status(200).json({
status: 200,
message: 'Your password has been reset successfully!'
});
}
}
} catch (error) {
return res.status(520);
}
}
}
export default AuthController;
41 changes: 41 additions & 0 deletions controllers/MailController.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,44 @@ export const sendEmailVerified = (user = {}) => {
`;
return sendgrid({ to: user.email, subject: 'Email Confirmed', html: mailBody });
};

export const resetPasswordEmail = (userId, email, resetCode) => {
const mailBody = `
<div style="color: #5a5a5a;">
<div style="border-bottom: 1px solid #2ABDEB; padding: 15px;">
<h2 style="color: #2ABDEB; text-align: center;">Authors Haven - Reset your account password</h2>
</div>
<p style="font-size: 1.2rem; line-height: 2rem; color: #5a5a5a;">
To reset your password, click the link below.
<p/>
<div style="text-align: center; padding: 20px;">
<a href="${FRONTEND_URL}/users/${userId}/reset/${resetCode}"
style="color: #fff; background-color: #2ABDEB; padding: 10px 20px; font-size: 1.2rem; text-align: center; text-decoration: none;"
> Password Reset </a>
<p style="font-size: 1.5rem; margin-top: 30px; color: #5a5a5a !important">
Or copy the link below
<p><br>${FRONTEND_URL}/users/${userId}/reset/${resetCode}
</div>
<p style="color: #5a5a5a !important;">If you didn't ask for password reset, Please ignore this message.</p>
<p style="color: #5a5a5a !important;">Thank you, <br> Authors Haven Team</p>
</div>
`;
return sendgrid({ to: email, subject: 'Password Reset', html: mailBody });
};

export const newPasswordEmail = email => {
const mailBody = `
<div style="color: #5a5a5a;">
<div style="border-bottom: 1px solid #2ABDEB; padding: 15px;">
<h2 style="color: #2ABDEB; text-align: center;">Authors Haven - Password changed</h2>
</div>
<div style="text-align: center; padding: 20px;">
<p style="font-size: 1.5rem; margin-top: 30px; color: #5a5a5a !important">
Your password has been reset successfully!</p>
</div>
<p style="color: #5a5a5a !important;">Thank you, <br> Authors Haven Team</p>
</div>
`;

return sendgrid({ to: email, subject: 'Password Changed', html: mailBody });
};
30 changes: 30 additions & 0 deletions database/migrations/20190213154608-create-reset-password.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('ResetPasswords', {
id: {
allowNull: false,
primaryKey: true,
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
userId: {
type: Sequelize.UUID
},
resetCode: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('ResetPasswords');
}
};
33 changes: 33 additions & 0 deletions database/models/resetpassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = (sequelize, DataTypes) => {
const ResetPassword = sequelize.define(
'ResetPassword',
{
id: {
allowNull: false,
primaryKey: true,
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
userId: {
type: DataTypes.UUID
},
resetCode: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4
},
createdAt: {
allowNull: false,
type: DataTypes.DATE
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE
}
},
{}
);
ResetPassword.associate = function(models) {
ResetPassword.belongsTo(models.User, { foreignKey: 'userId' });
};
return ResetPassword;
};
2 changes: 1 addition & 1 deletion database/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ module.exports = (sequelize, DataTypes) => {
{}
);
User.associate = function(models) {
// Association
User.hasMany(models.ResetPassword, { foreignKey: 'userId' });
};
return User;
};
Loading

0 comments on commit 7322627

Please sign in to comment.