Skip to content

Commit

Permalink
feat(reset-password): reset password via email
Browse files Browse the repository at this point in the history
- add nodemailer
- write send mail functionalities
- write reset password functionalities
- write tests

[Delivers #166789994]
  • Loading branch information
Hervera authored and dmithamo committed Jul 16, 2019
1 parent 27e74bc commit 80437ae
Show file tree
Hide file tree
Showing 14 changed files with 490 additions and 66 deletions.
11 changes: 8 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
DATABASE_URL=postgres://wmqxaqjr:tTHUvMlywe2owxdrqEaofzdC2RKF4XYp@raja.db.elephantsql.com:5432/wmqxaqjr
DATABASE_TEST=postgres://wmqxaqjr:tTHUvMlywe2owxdrqEaofzdC2RKF4XYp@raja.db.elephantsql.com:5432/wmqxaqjr
SECRET=qwertyuiop[]\';lkjhgfdsa`zxcvbnm,./+_)(*&^%$#@!)
DATABASE_URL=postgresql://dbusername:dbpassword@localhost:5432/dbname
DATABASE_TEST=postgresql://dbusername:dbpassword@localhost:5432/dbnamefortest

SENDER_EMAIL=
SENDER_PASS=

SECRET=

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ logs
*.log
.dist
.DS_Store
.vscode/

npm-debug.log*
# Runtime data
Expand Down
5 changes: 5 additions & 0 deletions config/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,9 @@ module.exports = {
dialect: 'postgres',
logging: false,
},
email: {
user: process.env.SENDER_EMAIL,
pass: process.env.SENDER_PASS,
},
secret_key_code: process.env.SECRET,
};
97 changes: 97 additions & 0 deletions controllers/resetPasswordController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import jwt from 'jsonwebtoken';
import dotenv from 'dotenv';
import models from '../models';
import sendEmail from '../helpers/sendEmail';
import Auth from '../helpers/auth';

dotenv.config();

const { users } = models;

const { SECRET } = process.env;
const expirationTime = {
expiresIn: '1day',
};

/**
* @user controller
* @exports
* @class
*/
class ResetPasswordController {
/**
* Send password reset email.
* @param {object} req request
* @param {object} res response.
* @returns {object} response.
*/
static async sendResetLinkEmail(req, res) {
const user = {
email: req.body.email,
};
try {
const checkUser = await users.findOne({
where: {
email: user.email,
}
});
if (checkUser) {
const payload = {
email: checkUser.email,
};
const token = jwt.sign(payload, SECRET, expirationTime);
req.body.token = token;
req.body.template = 'resetPassword';
sendEmail(user.email, token, 'resetPassword');
return res.status(200).send({ message: 'We have e-mailed a password reset link, Check your email!' });
}
return res.status(404).json({ error: 'The email provided does not exist' });
} catch (error) {
return res.status(500).json({ error: `${error}` });
}
}

/**
* get the reset password token.
* @param {object} req request.
* @param {object} res response.
* @returns {object} response.
*/
static getToken(req, res) {
return res.send({ token: req.params.token });
}

/**
* Resets password.
* @param {object} req request.
* @param {object} res response.
* @returns {object} response.
*/
static async resetPassword(req, res) {
const password = Auth.hashPassword(req.body.password);
const { token } = req.body;
try {
const decoded = await jwt.decode(token, SECRET);
if (decoded) {
const checkUpdate = await users.update(
{
password,
},
{
where: {
email: decoded.email,
}
}
);
if (checkUpdate.length >= 1) {
return res.status(200).json({ message: 'Your password was reset successfully' });
}
}
return res.status(401).json({ error: 'Invalid token' });
} catch (error) {
return res.status(500).send({ error: error.message });
}
}
}

export default ResetPasswordController;
26 changes: 26 additions & 0 deletions helpers/emailTemplates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import dotenv from 'dotenv';

dotenv.config();
const clientUrl = (process.env.NODE_ENV === 'production') ? 'https://ah-92explorers-api.herokuapp.com' : 'http://127.0.0.1:3000';

/**
* An object module that holds mails' templates
* @exports email/templates
*/
const emailTemplates = {
resetPassword: {
from: '',
to: '',
subject: 'Password reset',
html: `<div style="font-family:Avenir,Helvetica,sans-serif;box-sizing:border-box;padding:35px;">
<h1 style="color: #444;">Authors Haven</h1>
<p style="font-family:Avenir,Helvetica,sans-serif;box-sizing:border-box;color:#74787e;font-size:16px;line-height:1.5em;margin-top:0;text-align:left">You are receiving this email because we received a password reset request for your account. Click the blue button below to reset the password. If you did not request a password reset, no further action is required.</p>
<p><a style="background-color: #3097d1; border: 2px solid #3097d1; padding: 8px; color: #fff; font-size: 16px; text-decoration: none;cursor: pointer;" href="${clientUrl}/api/reset-password/$token">Reset Password</a>
</a></p>
<p style="color:#74787e;font-size:16px;line-height:1.5em;margin-top:0;text-align:left">Thank you for using our application!</p>
<p style="color:#74787e;font-size:16px;line-height:1.5em;margin-top:0;">Regards,<br>Authors Haven</p>
</div>`
},
};

export default { emailTemplates };
59 changes: 59 additions & 0 deletions helpers/mailer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import nodemailer from 'nodemailer';
import { email } from '../config/index';
import Templates from './emailTemplates';

const { emailTemplates } = Templates;
/**
* Class module to send email
* @exports
* @class
*/
class Mailer {
/**
* Initialize user and pass
* @constructor
* @param {string} _userEmail - User email
*/
constructor(_userEmail) {
this.userEmail = _userEmail;
this.senderEmail = email.user;
this.senderPass = email.pass;
}

/**
* Adds a token.
* @param {string} token token
* @param {string} template email.
* @returns {object} response.
*/
async addTokenToEmail(token, template) {
const mailOptions = emailTemplates[template];
mailOptions.from = this.senderEmail;
mailOptions.to = this.userEmail;
const addToken = mailOptions.html.replace('$token', token);
mailOptions.html = addToken;
try {
const response = await this.sendEmail(mailOptions);
return response;
} catch (error) {
return `${error}`;
}
}

/**
* Send email
* @param {Object} mailOptions - Email template
* @returns {object} response.
*/
async sendEmail(mailOptions) {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: this.senderEmail,
pass: this.senderPass
}
});
transporter.sendMail(mailOptions);
}
}
export default Mailer;
19 changes: 19 additions & 0 deletions helpers/sendEmail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Mailer from './mailer';

/**
* Call mailer class to send email to the user
* @param {string} userEmail - User email account for sending email to
* @param {string} token - Token that added to email sent
* @param {template} template email templates
* @returns {object} response.
*/
const sendEmail = async (userEmail, token, template) => {
try {
const mailer = new Mailer(userEmail);
await mailer.addTokenToEmail(token, template);
} catch (error) {
return `Email failed: ${error.message}`;
}
};

export default sendEmail;
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ app.get('/swagger.json', (req, res) => {
res.setHeader('Content-Type', 'application/json');
res.send(swagger.swaggerSpec);
});

app.use('/docs', swaggerUI.serve, swaggerUI.setup(swagger.swaggerSpec));

require('./models/users');
Expand Down
14 changes: 14 additions & 0 deletions middlewares/validations/validations.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,20 @@ class Validations {

next();
}

static async validatePasswordOnReset(req, res, next) {
const pwdRegex = /^(?=.*?[A-Z])(?=(.*[a-z]){1,})(?=(.*[\d]){1,})(?=(.*[\W]){1,})(?!.*\s).{8,}$/;
if (!pwdRegex.test(req.body.password)) {
return res.status(400).json({
error: [
'a valid password should not be alphanumeric',
'a valid password should be 8 characters long',
'an example of a valid password is alphamugerwa'
]
});
}
next();
}
}

export default Validations;
Loading

0 comments on commit 80437ae

Please sign in to comment.