Skip to content

Commit

Permalink
Merge pull request #26 from andela/feature/164198182/request-password…
Browse files Browse the repository at this point in the history
…-change

#164198182 Implement forgot password
  • Loading branch information
aanchirinah committed Mar 12, 2019
2 parents 1aaf579 + de9a696 commit b9de6e3
Show file tree
Hide file tree
Showing 15 changed files with 913 additions and 285 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ MAIL_SECURE=
MAIL_USER=
MAIL_PASS=

API_DOMAIN=localhost:3000/
API_DOMAIN=localhost:3000
19 changes: 19 additions & 0 deletions controllers/profileController.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import models from '../models';
import { STATUS } from '../helpers/constants';
import Response from '../helpers/responseHelper';
import Logger from '../helpers/logger';

const { Profile } = models;

Expand Down Expand Up @@ -75,6 +76,24 @@ class ProfileController {
return Response.send(res, SERVER_ERROR, error.message, 'Profile update failed, try again later!', false);
}
}

/**
* Verifies if the username already exists
*
* @static
* @param {string} username The username
* @returns {boolean} true if username exists and false otherwise
* @memberof ProfileController
*/
static async usernameExists(username) {
let user = null;
try {
user = await Profile.findOne({ where: { username } });
} catch (error) {
Logger.log(error);
}
return user !== null;
}
}

export default ProfileController;
44 changes: 41 additions & 3 deletions controllers/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class UsersController {
// generate confirm token
const confirmToken = await generateToken({ email: user.email });
// generate confirm link
const confrimLink = `${env('API_DOMAIN')}api/v1/users/confirm_account?token=${confirmToken}`;
const confrimLink = `${env('API_DOMAIN')}/api/v1/users/confirm_account?token=${confirmToken}`;
// send the user a mail
const data = {
email: user.email,
Expand All @@ -45,14 +45,52 @@ class UsersController {
},
template: 'signup'
};
user.firstname = '';
user.lastname = '';
user.gender = '';
user.bio = '';
user.username = request.body.username.toLowerCase();
user.user_id = user.id;
await models.Profile.create(user);
Response.send(response, STATUS.CREATED, { token, id: user.id });
await Mail.sendMail(data);
return Response.send(response, STATUS.CREATED, { token, id: user.id });
return;
} catch (error) {
logger.error(error);
return next(error);
}
}

/**
* Creates an email reset link and sends to the user
*
* @static
* @param {object} request The express request object
* @param {object} response The express response object
* @param {function} next The express next function
* @returns {void}
* @memberof UsersController
*/
static async sendPasswordRecoveryLink(request, response, next) {
try {
const { email } = request.body;
const token = jwt.sign({ email }, env('APP_KEY'), { expiresIn: '1h' });
const link = `${env('API_DOMAIN')}/users/reset_password?token=${token}`;
const data = {
email,
subject: 'Reset your Password',
mailContext: {
link
},
template: 'password'
};
Response.send(response, STATUS.OK, [], MESSAGE.PASSWORD_REQUEST_SUCCESSFUL);
await Mail.sendMail(data);
return;
} catch (error) {
next(error);
}
}

/**
* Get a user where the column(param) equals the specified value
*
Expand Down
11 changes: 10 additions & 1 deletion helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ export const MESSAGE = {
EMAIL_EMPTY: 'Email is required',
EMAIL_INVALID: 'Provide a valid email address',
EMAIL_EXISTS: 'The email address already exists. If you are registered, proceed to login instead',
EMAIL_NOT_EXISTS: 'The email address is not registered. Please try again',

USERNAME_EMPTY: 'Username is required',
USERNAME_EXITS: 'The username is already taken, try another one',
USERNAME_NOT_ALPHANUMERIC: 'Username should contain only letters and numbers',

ACCOUNT_CONFIRM: 'Your account has been successfully confirmed'
ACCOUNT_CONFIRM: 'Your account has been successfully confirmed',

LOGIN_SUCCESSFUL: 'Login successful',

PASSWORD_REQUEST_FAILED: 'Password reset failed',
PASSWORD_REQUEST_SUCCESSFUL: 'We have sent you an email',
};

export const TOKEN_VALIDITY = 604800; // 7 days

export const FIELD = {
USERNAME: 'username',
PASSWORD: 'password',
Expand Down
3 changes: 2 additions & 1 deletion helpers/utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import jwt from 'jsonwebtoken';
import { TOKEN_VALIDITY } from '../helpers/constants';

/**
* Retrieves the value for environment config variable
Expand Down Expand Up @@ -27,8 +28,8 @@ export const validateConfigVariable = (requiredEnv) => {
* Generates JWT token using provided payload
*
* @param {Object} payload - Payload to encrypt
* @param {string} expiresIn Validity perion of the token
* @return {string} JWT token string
*/
export const generateToken = async payload => jwt.sign(payload, env('APP_KEY'));

export default {};
57 changes: 44 additions & 13 deletions middlewares/handleValidation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,56 @@ import { validationResult } from 'express-validator/check';
import { STATUS, MESSAGE, expressValidatorFormater } from '../helpers/constants';
import Response from '../helpers/responseHelper';
/**
* Wrapper class to handle validation results from exprsss-validator
*
* @export
* @class Handler
*/
export default class Handler {
/**
* Validates the registration parameters
*
* @param {object} request The request object
* @param {object} response The response object
* @param {function} next The next function to transfer control to the next middleware
* @returns {void}
*/
export const handleRegistration = (request, response, next) => {
const errors = validationResult(request).formatWith(expressValidatorFormater);
if (!errors.isEmpty()) {
return Response.send(
response,
STATUS.BAD_REQUEST,
errors.array({ onlyFirstError: true }),
MESSAGE.REGISTRATION_ERROR,
false,
);
static handleRegistration(request, response, next) {
Handler.handle(request, response, next, MESSAGE.REGISTRATION_ERROR);
}
next();
};

export default {};
/**
* Validates the recover password parameters
*
* @param {object} request The request object
* @param {object} response The response object
* @param {function} next The next function to transfer control to the next middleware
* @returns {void}
*/
static handleForgotPassword(request, response, next) {
Handler.handle(request, response, next, MESSAGE.PASSWORD_REQUEST_FAILED);
}

/**
* Validates the error array from express validator
*
* @param {object} request The request object
* @param {object} response The response object
* @param {function} next The next function to transfer control to the next middleware
* @param {string} message The message to send if there are errors
* @returns {void}
*/
static handle(request, response, next, message) {
const errors = validationResult(request).formatWith(expressValidatorFormater);
if (!errors.isEmpty()) {
return Response.send(
response,
STATUS.BAD_REQUEST,
errors.array({ onlyFirstError: true }),
message,
false,
);
}
next();
}
}
48 changes: 42 additions & 6 deletions middlewares/validator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { body } from 'express-validator/check';
import { MESSAGE, FIELD } from '../helpers/constants';
import UsersController from '../controllers/users';

import ProfileController from '../controllers/profileController';
/**
* Used with express validator to validate input paramters
* @export
Expand All @@ -17,11 +17,33 @@ export default class Validator {
static validateRegistration() {
return [
...Validator.validateEmail(),
...Validator.verifyEmail(),
...Validator.validatePassword(),
...Validator.validateUsername()
];
}

/**
* Validates email when recovering password
* @static
* @returns {array} The array of express validator chains
* @memberof Validator
*/
static validateForgotPassword() {
return [
...Validator.validateEmail(),
[
body(FIELD.EMAIL)
.trim()
.custom(async (email) => {
if (!await UsersController.getUser('email', email)) {
return Promise.reject(MESSAGE.EMAIL_NOT_EXISTS);
}
}),
]
];
}

/**
* Validates password
* @static
Expand Down Expand Up @@ -55,7 +77,9 @@ export default class Validator {
.not().isEmpty()
.withMessage(MESSAGE.USERNAME_EMPTY)
.custom(async (username) => {
// TODO: Check if username exists
if (await ProfileController.usernameExists(username.toLowerCase())) {
return Promise.reject(MESSAGE.USERNAME_EXITS);
}
}),
];
}
Expand All @@ -73,11 +97,23 @@ export default class Validator {
.not().isEmpty()
.withMessage(MESSAGE.EMAIL_EMPTY)
.isEmail()
.withMessage(MESSAGE.EMAIL_INVALID)
.withMessage(MESSAGE.EMAIL_INVALID),
];
}

/**
* Verifies that the email exists
* @static
* @returns {array} The array of express validator chains
* @memberof Validator
*/
static verifyEmail() {
return [
body(FIELD.EMAIL)
.trim()
.custom(async (email) => {
const user = await UsersController.getUser('email', email);
if (user) {
return Promise.reject((MESSAGE.EMAIL_EXISTS));
if (await UsersController.getUser('email', email)) {
return Promise.reject(MESSAGE.EMAIL_EXISTS);
}
}),
];
Expand Down
Loading

0 comments on commit b9de6e3

Please sign in to comment.