Skip to content

Commit

Permalink
Merge a7b85f3 into 5dc177e
Browse files Browse the repository at this point in the history
  • Loading branch information
youngestdj committed Mar 14, 2019
2 parents 5dc177e + a7b85f3 commit 56637f1
Show file tree
Hide file tree
Showing 14 changed files with 621 additions and 207 deletions.
98 changes: 96 additions & 2 deletions controllers/user.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import bcrypt from 'bcrypt';
import dotenv from 'dotenv';
import jwt from 'jsonwebtoken';
import { compareSync } from 'bcrypt';
import shortId from 'shortid';
import sendMail from '../helpers/emails';
import getName from '../helpers/user';
import { User } from '../models';

const { HOST_URL } = process.env;

dotenv.config();
const { JWT_SECRET } = process.env;
const generateToken = (id, expiresIn = '24h') => jwt.sign({ id }, JWT_SECRET, { expiresIn });
Expand Down Expand Up @@ -75,7 +80,7 @@ export default class UserController {
body: { rememberMe, password },
user
} = req;
const passwordMatch = compareSync(password, user.password);
const passwordMatch = bcrypt.compareSync(password, user.password);
if (!passwordMatch) {
return res.status(401).json({
success: false,
Expand Down Expand Up @@ -131,4 +136,93 @@ export default class UserController {
});
});
}

/**
* @description reset user password
* @param {object} req http request object
* @param {object} res http response object
* @returns {object} response
*/
static async requestpasswordreset(req, res) {
const { email } = req.body;
const passwordResetToken = shortId.generate();
try {
// save password reset token to db
await User.update(
{ passwordResetToken },
{ where: { email } }
);

// save token expiration date (1 hr) to db
await User.update(
{ passwordResetTokenExpires: Date.now() + (60 * 60 * 1000) },
{ where: { email } }
);
} catch (error) {
return res.status(500).json({
success: false,
errors: [error.message]
});
}

// get user's name and send reset email
const name = await getName(email);
const emailPayload = {
name,
email,
link: `${HOST_URL}/api/v1/resetpassword/${passwordResetToken}`,
subject: 'Reset your password',
message: 'reset your password'
};
sendMail(emailPayload);

return res.status(201).json({
success: true,
message: 'A link to reset your password has been sent to your mail. Please note that the link is only valid for one hour.'
});
}

/**
* @description Check if password token in valid
* @param {object} req http request object
* @param {object} res http response object
* @returns {object} response
*/
static async resetpassword(req, res) {
const { params: { passwordResetToken } } = req;
const { password } = req.body;
const getPasswordResetToken = await User.findOne({ where: { passwordResetToken } });
if (!getPasswordResetToken) {
return res.status(404).json({
success: false,
errors: ['Password reset token not found']
});
}
if (getPasswordResetToken.dataValues.passwordResetTokenExpires < Date.now()) {
return res.status(410).json({
success: false,
errors: ['Your link has expired. Please try to reset password again.']
});
}
try {
// save new password to db
await User.update(
{
password: bcrypt.hashSync(password, bcrypt.genSaltSync(10)),
passwordResetToken: ''
},
{ where: { passwordResetToken } }
);
} catch (error) {
return res.status(500).json({
success: false,
errors: [error.message]
});
}

return res.status(201).json({
success: true,
message: 'Password changed successfully.'
});
}
}
104 changes: 99 additions & 5 deletions doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,84 @@
}
}
},
"/resetpassword": {
"post": {
"tags": [
"Reset password"
],
"summary": "Send reset link to user email",
"description": "Send reset link to user email",
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "Request body",
"description": "Reset user password",
"required": true,
"schema": {
"$ref": "#/definitions/resetpassword"
}
}
],
"responses": {
"201": {
"description": "Save password reset key in database and send link to email"
},
"404": {
"description": "User not registered or verified."
},
"422": {
"description": "Email is invalid"
}
}
}
},
"/verifypasswordkey/{key}": {
"post": {
"tags": [
"Reset password"
],
"summary": "Verify user reset password key",
"description": "Verify user reset password key",
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"in": "body",
"name": "key",
"description": "key sent with the link to the user email",
"required": true,
"type": "integer",
"format": "int64",
"schema": {
"$ref": "#/definitions/changepassword"
}
}
],
"responses": {
"200": {
"description": "You can now reset your password."
},
"410": {
"description": "Your link has expired. Please try to reset password again."
},
"404": {
"description": "Password reset token not found."
}
}
}
},
"/articles": {
"post": {
"tags": [
Expand Down Expand Up @@ -330,6 +408,22 @@
}
}
},
"resetpassword": {
"type": "object",
"properties": {
"email": {
"type": "string"
}
}
},
"changepassword": {
"type": "object",
"properties": {
"password": {
"type": "string"
}
}
},
"articles": {
"type": "object",
"properties": {
Expand All @@ -343,10 +437,10 @@
"type": "string"
}
}
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
}
},
"externalDocs": {
"description": "Find out more about Swagger",
"url": "http://swagger.io"
}
}
}
13 changes: 10 additions & 3 deletions helpers/emails.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@ const transporter = nodemailer.createTransport({
});

const sendMail = (payload) => {
const { email, name, link } = payload;
const {
email,
name,
link,
subject,
message
} = payload;

const mailOptions = {
from: EMAIL_ADDRESS,
to: email,
subject: 'Welcome to Authors Haven',
subject,
html: `<h1>Hi ${name}</h1>
<p>Please click <a href="${link}">here</a> to verify your account.</p>
<p>Please click <a href="${link}">here</a> to ${message}.</p>
`,
};
return transporter.sendMail(mailOptions);
Expand Down
12 changes: 12 additions & 0 deletions helpers/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { User } from '../models';

/**
* @description Return user name
* @param {string} email user email
* @returns {promise} promise object
*/
const getName = async (email) => {
const name = await User.findOne({ where: { email } });
return name.dataValues.name;
};
export default getName;
16 changes: 16 additions & 0 deletions middleware/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,19 @@ export const validateLogin = [
.custom(value => !/\s/.test(value))
.withMessage('Please provide a valid password.'),
];

export const validateEmail = [
check('email')
.isEmail()
.withMessage('Email is invalid.')
.custom(value => !/\s/.test(value))
.withMessage('No spaces are allowed in the email.')
];

export const validatePassword = [
check('password')
.isLength({ min: 6 })
.withMessage('Password must be at least 6 characters long.')
.custom(value => !/\s/.test(value))
.withMessage('No spaces are allowed in the password.')
];
54 changes: 37 additions & 17 deletions middleware/verifyUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,43 @@ export default (req, res, next) => {
}

const { fieldName, fieldValue } = options;
User.findOne({ where: { [fieldName]: fieldValue } }).then((foundUser) => {
if (foundUser) {
const {
dataValues: { verified }
} = foundUser;
if (!verified) {
return res.status(403).json({
success: false,
errors: ['User has not been verified.']
});
User.findOne({ where: { [fieldName]: fieldValue } })
.then((foundUser) => {
if (foundUser) {
const {
id,
email,
username,
name,
verified,
verificationId,
bio,
password
} = foundUser.dataValues;
const userObj = {
id,
email,
username,
name,
verified,
verificationId,
bio,
password
};
if (!verified) {
return res.status(403).json({
success: false,
errors: [
'User has not been verified.'
]
});
}
req.user = userObj;
return next();
}
req.user = foundUser.dataValues;
return next();
}
return res.status(404).json({
success: false,
errors: ['User not found.']
return res.status(404).json({
success: false,
errors: ['User not found.']
});
});
});
};
20 changes: 20 additions & 0 deletions migrations/20190305110744-password-reset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn(
'Users',
'passwordResetToken',
{ type: Sequelize.STRING }
)
.then(() => queryInterface.addColumn(
'Users',
'passwordResetTokenExpires',
{ type: Sequelize.STRING }
)),
down: queryInterface => queryInterface.removeColumn(
'Users',
'passwordResetToken'
)
.then(() => queryInterface.removeColumn(
'Users',
'passwordResetTokenExpires'
))
};
Loading

0 comments on commit 56637f1

Please sign in to comment.