Skip to content

Commit

Permalink
Merge 07d6498 into efee6d9
Browse files Browse the repository at this point in the history
  • Loading branch information
minega25 committed Aug 14, 2019
2 parents efee6d9 + 07d6498 commit 2511362
Show file tree
Hide file tree
Showing 8 changed files with 339 additions and 5 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
"undoseeds": "babel-node node_modules/.bin/sequelize db:seed:undo",
"migration": "babel-node node_modules/.bin/sequelize db:migrate",
"undomigration": "export NODE_ENV=test && npm run undoseeds && babel-node node_modules/.bin/sequelize db:migrate:undo:all",
"runmigrations": "npm run undoseeds && npm run migration && npm run seeds",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"test": "export NODE_ENV=test && npm run undomigration && npm run migration && npm run seeds && nyc --reporter=html --reporter=text mocha ./test --no-timeout --exit --require @babel/register",
"dev": "npm run undoseeds && npm run seeds && nodemon --exec babel-node ./src/app.js"

"dev": "nodemon --exec babel-node ./src/app.js"
},
"author": "Andela Simulations Programme",
"license": "MIT",
Expand Down
119 changes: 119 additions & 0 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import UserService from '../services/user.service';
import Helper from '../helpers/helper';
import sendEmail from '../helpers/verification-email';
import resetSendMail from '../services/resetpassword.service';

/**
*
Expand Down Expand Up @@ -367,6 +368,124 @@ class UserController {
data: rejectedToken
});
}

/**
*
*
* @static
* @param {*} req
* @param {*} res
* @returns {Object} json data
* @memberof UserController
*/
static async requestPasswordReset(req, res) {
// check if email provided exists in db
const { email } = req.body;

if (!email) {
return res.status(400).send({
status: 400,
message: 'Please provide an email address'
});
}
try {
const user = await UserService.findOne(email, '');
if (!user) {
return res.status(400).send({
status: 400,
message: `User with email ${email} is not not found `
});
}
// generate token
const payload = {
email: user.email,
role: user.role
};

const token = await Helper.generateToken(payload, (60 * 60));

// create password reset link
const resetUrl = `${process.env.BACKEND_URL}/api/${process.env.API_VERSION}/users/reset/${token}`;

// send email to user email address
const emailSent = resetSendMail(user.email, user.username, resetUrl);

if (!emailSent) { return res.status(500).send({ status: 500, message: 'Failed to send email. Please contact site administrator for support' }); }

return res.status(200).send({
status: 200,
message: 'Check your email address to reset your password',
});
} catch (error) {
return res.status(500).send({
status: 500,
message: error.message
});
}
}

/**
*
*
* @static
* @param {*} req
* @param {*} res
* @returns {Object} json data
* @memberof UserController
*/
static async handlePasswordReset(req, res) {
// verify token and if its not expired
const { token } = req.params;
const tokenDecoded = Helper.verifyToken(token);
if (tokenDecoded === 'invalid token') {
return res.status(400).send({
status: 400,
message: 'Invalid token or Token expired'
});
}

// check if payload's email address exists in database
const { email } = tokenDecoded;
const user = await UserService.findOne(email, '');
if (!user) {
return res.status(400).send({
status: 400,
message: `User with email ${email} is not not found `
});
}

// check if old password equals new password
const checkPassword = await Helper.comparePassword(user.password, req.body.password);
if (checkPassword) {
return res.status(400).send({
status: 400,
message: 'New password cannot be the same as current password'
});
}

// update password
const newPassword = req.body.password;
const password = await Helper.hashPassword(newPassword);
if (!password) {
return res.status(500).send({
status: 500,
message: 'An error occured, Contact your administrator'
});
}
try {
await UserService.updateUser(email, { password });

return res.status(200).send({
status: 200,
message: 'Password reset successfully'
});
} catch (error) {
return res.status(400).send({
status: 400,
message: 'Failed to fetch user'
});
}
}
}

export default UserController;
8 changes: 5 additions & 3 deletions src/helpers/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ class Helper {
}

/**
* Gnerate Token
* Generate Token
* @param {string} payload
* @param {string} expiresInPeriod
* @returns {string} token
*/
static generateToken(payload) {
static generateToken(payload, expiresInPeriod) {
const expiresInTime = expiresInPeriod || (24 * 60 * 60);
const token = jwt.sign(payload,
process.env.SECRET_KEY);
process.env.SECRET_KEY, { expiresIn: expiresInTime });
return token;
}

Expand Down
32 changes: 32 additions & 0 deletions src/middlewares/validators/resetpassword.validation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Joi from 'joi';
import Util from '../../helpers/util';

const util = new Util();

export default (req, res, next) => {
const { password, confirmPassword } = req.body;

if (password !== confirmPassword) {
util.setError(400, 'Passwords provided do not match');
return util.send(res);
}

const schema = {
password: Joi.string().trim().regex(/^(?:(?=.*[a-z])(?:(?=.*[A-Z])(?=.*[\d\W])|(?=.*\W)(?=.*\d))|(?=.*\W)(?=.*[A-Z])(?=.*\d)).{8,}$/).required(),
};
const { error } = Joi.validate({
password
}, schema);

if (!error) return next();
const errorMessageFromJoi = error.details[0].message;

if (errorMessageFromJoi.includes('fails to match the required pattern')) {
util.setError(400, 'Password should be altleast 8 characters, Contain both uppercase and lower case and a combination of letters, numbers and symbols');
return util.send(res);
}
if (errorMessageFromJoi === '"password" is not allowed to be empty') {
util.setError(400, 'No password was specified');
return util.send(res);
}
};
45 changes: 45 additions & 0 deletions src/routes/api/user/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,49 @@
* responses:
* 200:
* description: User logged out successfully
* /users/reset:
* post:
* description: request for a password reset
* produces:
* - application/json
* parameters:
* - in: body
* name: reset password
* description: password reset
* schema:
* type: object
* required:
* - email
* properties:
* email:
* type: string
* responses:
* 200:
* description: Check your email address to reset your password
* 400:
* description: Validation error
* /users/reset/:token:
* post:
* description: handle password reset logic
* produces:
* - application/json
* parameters:
* - in: body
* name: user
* description: request for password reset
* schema:
* type: object
* required:
* - password
* - confirmPassword
* properties:
* password:
* type: string
* confirmPassword:
* type: string
* responses:
* 200:
* description: Successfully reset your password
* 400:
* description: Validation error
*/
5 changes: 5 additions & 0 deletions src/routes/api/user/user.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import validateUser from '../../../middlewares/validators/signup.validation';
import admin from '../../../middlewares/admin';
import verifyEmail from '../../../controllers/verify-controller';
import confirmEmaiAuth from '../../../middlewares/emailVarification.middleware';
import resetPasswordValidation from '../../../middlewares/validators/resetpassword.validation';

const router = express.Router();
router.get('/verify', verifyEmail);
Expand All @@ -17,4 +18,8 @@ router.put('/update/:email', [validateToken, confirmEmaiAuth], UserController.up
router.post('/signup/admin', [validateToken, admin, confirmEmaiAuth], UserController.createAdmin);
router.post('/signout', validateToken, UserController.signoutUser);

// reset password route handlers
router.post('/reset', UserController.requestPasswordReset);
router.patch('/reset/:token', resetPasswordValidation, UserController.handlePasswordReset);

export default router;
38 changes: 38 additions & 0 deletions src/services/resetpassword.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import 'dotenv/config';
import sgMail from '@sendgrid/mail';

const sendEmail = (email, username, url) => {
sgMail.setApiKey(process.env.SendGridApiKey);

const msg = {
to: `${email}`,
from: `${process.env.SENDER_EMAIL}`,
subject: 'How to reset your Author\'s Haven account password.',
text: 'Follow this link provided to reset your password',
html: `<div style="width: 90%; margin: 5rem auto; box-shadow: 0 0 10px rgba(0,0,0,.9);">
<div>
<div>
<div style="background-color: #2084ba; height: 3rem; width: 100%">
<h2 style="text-align: center; color: white; padding-top: 10px;">Author's Heaven</h2>
</div>
<h4 style="text-align: center">Hi! ${username}</h4>
</div>
<div style=" padding: 0px 20px 20px 20px">
<div>
<p>You are recieving this because you (or someone else) requested the reset of your password for your account</p></br>
<p>This link is valid for only one hour</p>
<p>Please click on the link, or paste this into your browser to complete the process</p>
${url}
</div>
<div>
<h3 style="text-align: center">Thank you</h3>
</div>
</div>
</div>
</div>`,
};
sgMail.send(msg);
return true;
};

export default sendEmail;
Loading

0 comments on commit 2511362

Please sign in to comment.