diff --git a/server/controllers/user.js b/server/controllers/user.js index f33e8bd..1aff987 100644 --- a/server/controllers/user.js +++ b/server/controllers/user.js @@ -4,6 +4,7 @@ import db from '../database/models'; import { HelperUtils } from '../utils'; import response from '../utils/response'; import verifyEmailMarkup from '../utils/markups/emailVerificationMarkup'; +import passwordResetMarkup from '../utils/markups/passwordResetMarkup'; import '@babel/polyfill'; const { User } = db; @@ -93,12 +94,105 @@ export default class Users { } /** -* @description This controller method completes the social sign in process -* -* @param {object} req - Express request object -* @param {object} res - Express response object -* @return {undefined} -*/ + * @description This controller method sends password reset link e-mail + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @returns {object} Json response + */ + static async resetPasswordEmail(req, res) { + const { email } = req.body; + + const hashedEmail = HelperUtils.hashPassword(email); + + try { + const user = await User.findOne({ + where: { email } + }); + + if (user === null) { + response(res).notFound({ + message: 'user not found in our records' + }); + } else { + HelperUtils.sendMail( + email, + 'Authors Haven ', + 'Password Reset', + 'Reset Password', + passwordResetMarkup(user.firstname, email, hashedEmail) + ); + response(res).success({ + message: 'Please, verify password reset link in your email box' + }); + } + } catch (err) { + response(res).sendData(400, { + message: err + }); + } + } + + /** + * @description This controller method resets user password + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @return {object} Json response + */ + static async resetPassword(req, res) { + const { newPassword, confirmPassword } = req.body; + const isPassword = newPassword === confirmPassword; + + if (!isPassword) { + response(res).sendData(400, { + message: 'The supplied passwords do not match' + }); + } + + try { + const hashPassword = HelperUtils.hashPassword(newPassword); + + const { email, hash } = req.query; + const isEmail = await HelperUtils.comparePasswordOrEmail(email, hash); + + if (isEmail) { + const user = await User.findOne({ + where: { email } + }); + + if (!user) { + response(res).notFound({ + message: 'User not found' + }) + } else { + await user.update({ + password: hashPassword + }); + response(res).success({ + message: 'Password reset successful. Please, login using your new password.' + }); + } + } else { + response(res).sendData(400, { + message: 'Invalid password reset link' + }) + } + } catch (err) { + response(res).sendData(400, { + message: err + }); + } + } + + + /** + * @description This controller method completes the social sign in process + * + * @param {object} req - Express request object + * @param {object} res - Express response object + * @return {undefined} + */ static async socialLogin(req, res) { const { data } = req.user; diff --git a/server/routes/users.js b/server/routes/users.js index 331d16d..001c2be 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -6,6 +6,8 @@ const authRoute = express.Router(); authRoute.post('/users', Users.signupUser); authRoute.get('/users/verifyemail', Users.verifyUserEmail); +authRoute.post('/users/reset-password', Users.resetPasswordEmail); +authRoute.patch('/users/reset-password', Users.resetPassword); authRoute.get('/users/auth/google', passport.authenticate('google', { scope: [ 'https://www.googleapis.com/auth/userinfo.profile', diff --git a/server/test/user.spec.js b/server/test/user.spec.js index a286989..2ba693b 100644 --- a/server/test/user.spec.js +++ b/server/test/user.spec.js @@ -4,9 +4,15 @@ import app from '../app'; chai.use(chaiHttp); +const QueryURL = '?email=nwabuzor.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; +const invalidQueryURL = '?email=invalid.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; const signupURL = '/api/users'; -const verifyURL = '/api/users/verifyemail?email=nwabuzor.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; -const invalidVerifyURL = '/api/users/verifyemail?email=invalid.obiora@gmail.com&hash=$2a$08$vu6Gwj1EgU7/6IJv6juphuraxOv6tOHaeNOvWmsjh0oYHOLRO8/9q'; +const resetPassword = '/api/users/reset-password'; +const resetPasswordURL = `/api/users/reset-password${QueryURL}`; +const invalidResetPasswordURL = `/api/users/reset-password${invalidQueryURL}`; +const verifyURL = `/api/users/verifyemail${QueryURL}`; +const invalidVerifyURL = `/api/users/verifyemail${invalidQueryURL}`; + describe('Test signup endpoint and email verification endpoint', () => { it('It should return a 404 if user don\'t exist during email verification', (done) => { @@ -66,6 +72,78 @@ describe('Test signup endpoint and email verification endpoint', () => { }); }); +describe('Test reset password mail endpoint and password link endpoint', () => { + it('It should return a 404 if user records not found', (done) => { + const data = { email: 'ayo-oluwa.adebayo@andela.com' }; + chai + .request(app) + .post(resetPassword) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(404); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('user not found in our records'); + done(); + }); + }); + + it('It should return a 200 if user email is found in the database', (done) => { + const data = { email: 'nwabuzor.obiora@gmail.com' }; + chai + .request(app) + .post(resetPassword) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Please, verify password reset link in your email box'); + done(); + }); + }); + + it('It should return a 400 if user passwords do not match', (done) => { + const data = { newPassword: 'hello', confirmPassword: 'hell' }; + chai + .request(app) + .patch(resetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('The supplied passwords do not match'); + done(); + }); + }); + + it('It should return a 200 if user passwords match', (done) => { + const data = { newPassword: 'hello', confirmPassword: 'hello' }; + chai + .request(app) + .patch(resetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(200); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Password reset successful. Please, login using your new password.'); + done(); + }); + }); + + it('It should return a 400 if reset link is invalid', (done) => { + const data = { newPassword: 'hello', confirmPassword: 'hello' }; + chai + .request(app) + .patch(invalidResetPasswordURL) + .send(data) + .end((err, res) => { + expect(res.status).to.equal(400); + expect(res.body.message).to.be.a('string'); + expect(res.body.message).to.equal('Invalid password reset link'); + done(); + }); + }); +}); + describe('Social Login with Google', () => { it('should return the google authentication webpage', (done) => { chai diff --git a/server/utils/markups/passwordResetMarkup.js b/server/utils/markups/passwordResetMarkup.js new file mode 100644 index 0000000..8c3fcd5 --- /dev/null +++ b/server/utils/markups/passwordResetMarkup.js @@ -0,0 +1,89 @@ +const passwordResetMarkup = (username, email, hash) => ( + ` + + + + + + Authors Haven + + + + +
+
+ AH_logo +

Authors Haven

+
+
+

Hello ${username},

+

+ You recently requested to reset your password for your Authors Haven account. +


+

Please, click the button below to proceed.

+
+ + Reset Password + +
+ + +` +); + +export default passwordResetMarkup;