Skip to content

Commit

Permalink
feature(reset-password): create password reset logic
Browse files Browse the repository at this point in the history
- If user provides password, set it as his account password and email
user, else set a random password and email user
  • Loading branch information
chidimo committed May 23, 2019
1 parent 833ca49 commit f010bb6
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 35 deletions.
46 changes: 45 additions & 1 deletion controllers/UsersController.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
import generatePassword from 'password-generator';
import Model from '../models/Model';
import { InternalServerError } from '../utils/errorHandlers';
import {
get_existing_user, check_user_exists, update_if_exists
get_existing_user,
check_user_exists,
update_if_exists,
check_password,
update_pass,
sendPassword
} from './helpers/AuthController';
import { aws_signed_url, } from './helpers/UsersController';

const users_model = new Model('users');

const UsersController = {
reset_password: async (req, res) => {
const { email } = req.params;
const { current_password, confirm_new, new_pass } = req.body;

const remember_password = (
(current_password !== '') &&
(new_pass !== '') &&
(confirm_new !== '')
);

const clause = `WHERE email='${email}'`;
try {
const exists = await check_user_exists(users_model, clause, res);
if (exists) {
if (remember_password) {
const knows_pass = await check_password(
users_model, email, current_password, res);

if (knows_pass) {
await update_pass(users_model, new_pass, clause, res);
sendPassword(email, new_pass);
return res.status(204)
.json({ message: 'Password has been emailed to you.' });
}
return res.status(404)
.json({ error: 'You entered an incorrect password' });
}
const new_password = generatePassword();
await update_pass(users_model, new_password, clause, res);
sendPassword(email, new_password);
return res.status(204).json({ message: 'Password has been emailed to you.' });
}
return res.status(404)
.json({ error: `User with email ${email} not found` });
}
catch (e) { return; }
},

confirm_account: async (req, res) => {
try {

Expand Down
25 changes: 22 additions & 3 deletions controllers/helpers/AuthController.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import titlecase from 'titlecase';

import { InternalServerError } from '../../utils/errorHandlers';
import sendEmail from '../../utils/sendEmail';
import { async } from 'rxjs/internal/scheduler/async';
import hashPassword from '../../utils/hashPassword';

export const sendSignUpMessage = (user, req) => {
const path = `/users/${user.id}/account-confirmation`;
Expand All @@ -22,6 +22,18 @@ export const sendSignUpMessage = (user, req) => {
return;
};

export const sendPassword = (email, new_password) => {
const template_data = {
new_password,
};
const data = {
email,
template_name: 'new_password',
};
sendEmail(data, template_data);
return;
};

export const check_user_exists = async (model_instance, clause, res) => {
try {
const { rows } = await model_instance.select(
Expand All @@ -46,13 +58,12 @@ export const check_password = async (model_instance, email, password, res) => {

export const add_user_to_db = async (model_instance, req, res) => {
const { email, password, firstname, lastname } = req.body;
const hashedPassword = bcrypt.hashSync(password, 8);

try {
return await model_instance.insert_with_return(
'(email, firstname, lastname, password)',
`'${email}', '${firstname}', '${lastname}',
'${hashedPassword}'`
'${hashPassword(password)}'`
);
}
catch (e) { return InternalServerError(res, e);}
Expand Down Expand Up @@ -86,3 +97,11 @@ export const update_if_exists = async (model_instance,
}
catch (e) { return; }
};

export const update_pass = async (model_instance, password, clause, res) => {
try {
await model_instance.update(
`password='${hashPassword(password)}'`, clause);
}
catch (e) { return InternalServerError(res, e); }
};
64 changes: 45 additions & 19 deletions middleware/validators.users.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { body } from 'express-validator/check';
import { sanitizeBody } from 'express-validator/filter';
import validate_error_or_next from './validate_error_or_next';
import titlecase from 'titlecase';

const validate_password = field => (
body(field)
.not().isEmpty().withMessage('Password is required')
.isLength({ min: 8 }).trim()
.withMessage('Password must be at least 8 characters')
.isLength({ max: 16 })
.withMessage('Password must be at most 16 characters')
.isAlphanumeric().withMessage('Password must be alphanumeric')
);

const validate_name = field => (
body(field)
.not().isEmpty().withMessage(`${titlecase(field)} is required`)
);

const UsersValidators = {
validateNames: [
body('firstname')
.not().isEmpty().withMessage('First name is required'),
body('lastname')
.not().isEmpty().withMessage('Last name is required'),
validate_name('firstname'),
validate_name('lastname'),
sanitizeBody('firstname').trim().escape(),
sanitizeBody('lastname').trim().escape()

],

emailValidator: [
Expand All @@ -23,13 +36,7 @@ const UsersValidators = {
],

passwordValidator: [
body('password')
.not().isEmpty().withMessage('Password is required')
.isLength({ min: 8 }).trim()
.withMessage('Password must be at least 8 characters')
.isLength({ max: 16 })
.withMessage('Password must be at most 16 characters')
.isAlphanumeric().withMessage('Password must be alphanumeric'),
validate_password('password'),
sanitizeBody('password').trim().escape(),
sanitizeBody('confirm_password').trim().escape(),
validate_error_or_next
Expand All @@ -48,19 +55,38 @@ const UsersValidators = {
validate_error_or_next
],

newPasswordValidator: [
validate_password('current_password')
.optional({ checkFalsy: true })
.custom((value, { req }) => {
if (req.body.new_pass !== req.body.confirm_new) {
throw new Error(
'Password confirmation does not match password'
);
}
if (!req.body.new_pass) {
throw new Error('Please enter a new password');
}
else return value;
}),
validate_password('new_pass')
.optional({ checkFalsy: true }),
validate_password('confirm_new')
.optional({ checkFalsy: true }),
validate_error_or_next
],

updateProfileValidator: [
body('firstname')
.not().isEmpty().withMessage('First name cannot be empty'),
body('lastname')
.not().isEmpty().withMessage('Last namecannot be empty'),
validate_name('firstname'),
validate_name('lastname'),
body('phone')
.not().isEmpty().withMessage('Phone number cannot be empty')
.not().isEmpty().withMessage('Phone number is required')
.matches(/^0\d{10}$/).withMessage(
'Wrong number format: E.G. 07012345678'),
body('home')
.not().isEmpty().withMessage('Home address cannot be empty'),
.not().isEmpty().withMessage('Home address is required'),
body('office')
.not().isEmpty().withMessage('Office address cannot be empty'),
.not().isEmpty().withMessage('Office address is required'),

sanitizeBody('firstname').trim().escape(),
sanitizeBody('lastname').trim().escape(),
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"http-errors": "~1.6.2",
"jsonwebtoken": "^8.5.1",
"morgan": "~1.9.0",
"password-generator": "^2.2.0",
"pg": "^7.11.0",
"titlecase": "^1.1.3",
"underscore": "^1.9.1"
Expand Down
9 changes: 8 additions & 1 deletion routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ router.post('/auth/signup',
);

router.post('/auth/signin',
UsersValidators.emailValidator,
UsersValidators.passwordValidator,
AuthenticationMiddleware.generateToken,
AuthController.signin
Expand All @@ -41,8 +42,14 @@ router.get('/users/:id/photo/upload/',
router.patch('/users/:id/photo/update',
UsersController.update_photo_url
);
router.post('/users/:email/reset_password',
UsersValidators.newPasswordValidator,
UsersController.reset_password
);

router.get('/loans', LoansController.get_all_loans);
router.get('/loans',
AuthenticationMiddleware.verifyToken,
LoansController.get_all_loans);
router.get('/loans/:id', LoansController.get_loan);
router.get(
'/loans?status=approved&repaid=false', LoansController.get_all_loans);
Expand Down
1 change: 1 addition & 0 deletions settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const Settings = {
signatureVersion: 'v4'
},
skipTokenVerification: () => (process.env.NODE_ENV === 'test'),
skipEmailSend: () => (process.env.NODE_ENV === 'test'),
};

dev_logger(Settings);
Expand Down
23 changes: 18 additions & 5 deletions test/users-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe('/users', () => {
.end((err, res) => {
res.status.should.equal(422);
res.body.errors[0].msg.should.equal(
'First name is required');
'Firstname is required');
done();
});
});
Expand All @@ -126,7 +126,7 @@ describe('/users', () => {
.end((err, res) => {
res.status.should.equal(422);
res.body.errors[0].msg.should.equal(
'Last name is required');
'Lastname is required');
done();
});
});
Expand All @@ -144,10 +144,7 @@ describe('/users', () => {
done();
});
});

});


});
});

Expand Down Expand Up @@ -197,6 +194,22 @@ describe('/users', () => {
});
});

describe('POST /users/:email/reset_password', () => {
it('should return error if user does not exist', done => {
const email = 'unknown@email.com';
server
.post(`/users/${email}/reset_password`)
.expect(200)
.end((err, res) => {
res.status.should.equal(404);
res.body.error.should.equal(
`User with email ${email} not found`);
done();
});
});
});


describe('PATCH /users/:id/verify', () => {
it('should verify user', done => {
server
Expand Down
4 changes: 1 addition & 3 deletions utils/createDB.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import bcrypt from 'bcrypt';
import pool from '../models/pool';
import { dev_logger, test_logger } from './loggers';

const hashPassword = password => (bcrypt.hashSync(password, 8));
import hashPassword from './hashPassword';

const createUserTable = `
CREATE TABLE IF NOT EXISTS users (
Expand Down
5 changes: 5 additions & 0 deletions utils/hashPassword.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import bcrypt from 'bcrypt';

const hashPassword = password => (bcrypt.hashSync(password, 8));

export default hashPassword;
7 changes: 5 additions & 2 deletions utils/sendEmail.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import sgMail from '@sendgrid/mail';
import Settings from '../settings';
import { dev_logger } from './loggers';
import { dev_logger, test_logger } from './loggers';

sgMail.setApiKey(Settings.sendgridKey);

const templates = {
new_password : 'd-af4dcbb73450412480a3ed50ea04b344',
new_loan_application : 'd-a16e6d41d94c409aa99d36fa10222b25',
loan_status : 'd-e3eff2cb80e0435eaef5652dbbc58338',
password_reset : 'd-2364cc4d56294b8588358075b71d3061',
Expand All @@ -21,9 +22,11 @@ const sendEmail = (data, template_data) => {
...template_data
}
};

if (Settings.skipEmailSend()) return;
sgMail.send(msg, (err, result) => {
if (err) {
dev_logger(`There was an error sending your message ${err}\n`);
test_logger(`There was an error sending your message ${err}\n`);
} else {
dev_logger('Email sent successfully', result);
}
Expand Down
16 changes: 15 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1124,7 +1124,7 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73"
integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==

camelcase@^4.0.0:
camelcase@^4.0.0, camelcase@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd"
integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=
Expand Down Expand Up @@ -3657,6 +3657,13 @@ pascalcase@^0.1.1:
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=

password-generator@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/password-generator/-/password-generator-2.2.0.tgz#fc75cff795110923e054a5a71623433240bf5e49"
integrity sha1-/HXP95URCSPgVKWnFiNDMkC/Xkk=
dependencies:
yargs-parser "^8.0.0"

path-dirname@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
Expand Down Expand Up @@ -5069,6 +5076,13 @@ yargs-parser@^13.1.0:
camelcase "^5.0.0"
decamelize "^1.2.0"

yargs-parser@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-8.1.0.tgz#f1376a33b6629a5d063782944da732631e966950"
integrity sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==
dependencies:
camelcase "^4.1.0"

yargs-unparser@1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.5.0.tgz#f2bb2a7e83cbc87bb95c8e572828a06c9add6e0d"
Expand Down

0 comments on commit f010bb6

Please sign in to comment.