From 30978daca6fd13b1d3aef1aa1540fdf2db914e48 Mon Sep 17 00:00:00 2001 From: WilliamsOhworuka Date: Fri, 26 Jul 2019 19:42:31 +0100 Subject: [PATCH] feature(verification): send verification email [starts #167164980] --- .env.sample | 1 + src/controllers/AuthController.js | 56 ++++++++- src/controllers/userController.js | 6 + .../migrations/20190724090213-create-user.js | 4 + src/database/models/index.js | 1 - src/database/models/user.js | 4 + src/helpers/emailMessages.js | 31 ++++- src/helpers/verifyUser.js | 42 +++++++ src/index.js | 5 + src/routes/index.js | 3 +- src/routes/verifyEmail.js | 10 ++ tests/auth.spec.js | 4 + tests/mockData/userMock.js | 2 +- tests/verifyUser.spec.js | 115 ++++++++++++++++++ 14 files changed, 277 insertions(+), 7 deletions(-) create mode 100644 src/helpers/verifyUser.js create mode 100644 src/routes/verifyEmail.js create mode 100644 tests/verifyUser.spec.js diff --git a/.env.sample b/.env.sample index 4738e05..985161e 100644 --- a/.env.sample +++ b/.env.sample @@ -1,6 +1,7 @@ NODE_ENV= PORT= SECRET_KEY= +VERIFY_SECRET = DATABASE_URL_DEV= DATABASE_URL_TEST= SERVER_URL= diff --git a/src/controllers/AuthController.js b/src/controllers/AuthController.js index 8f97daf..2615465 100644 --- a/src/controllers/AuthController.js +++ b/src/controllers/AuthController.js @@ -1,9 +1,13 @@ import jwt from 'jsonwebtoken'; import helpers from '../helpers'; import services from '../services'; +import models from '../database/models'; +import responseMessage from '../helpers/responseHelper'; const { forgotPasswordMessage } = helpers; const { sendMail, findUser } = services; +const { User } = models; +const { successResponse } = responseMessage; /** * @@ -12,6 +16,7 @@ const { sendMail, findUser } = services; * @param {object} response * @returns {json} - json */ + const forgotPassword = async (request, response) => { const { email } = request.body; try { @@ -25,12 +30,59 @@ const forgotPassword = async (request, response) => { message: 'you will receive a link in your mail shortly' }); } catch (error) { + console.log(error); response.status(500).json({ error: error.message }); } }; -export default { - forgotPassword +/** + * Update user verified status + * + * @param {object} req + * @param {object} res + * @returns {json} - json + */ + +const updateStatus = async (req, res) => { + const user = await User.findOne({ where: { id: req.params.id } }); + const token = await jwt.verify(req.params.token, process.env.VERIFY_SECRET, (err, decoded) => { + if (err) { + return err; + } + return decoded; + }); + + if (!user) { + return successResponse(res, 401, { error: 'Sorry could not verify email' }); + } + const { verifiedToken } = user; + + if (token.name) { + if (token.name === 'TokenExpiredError') { + return successResponse(res, 401, { error: 'Session has expired you can request for another one' }); + } + return successResponse(res, 401, { error: 'Sorry could not verify email' }); + } + + if (verifiedToken !== req.params.token) { + return successResponse(res, 401, { error: 'Sorry could not verify email' }); + } + + const { isVerified } = user.dataValues; + if (isVerified === 'true') { + return successResponse(res, 400, { error: 'user already verified' }); + } + + await User.update({ + isVerified: 'true', + }, { + where: { + id: req.params.id, + } + }); + return successResponse(res, 200, { message: 'You have sucessfully verified your email' }); }; + +export default { forgotPassword, updateStatus }; diff --git a/src/controllers/userController.js b/src/controllers/userController.js index a3ee3c7..c5d5910 100644 --- a/src/controllers/userController.js +++ b/src/controllers/userController.js @@ -1,5 +1,6 @@ import models from '../database/models'; import helpers from '../helpers/index'; +import verifyUser from '../helpers/verifyUser'; const { authHelper, responseHelper } = helpers; @@ -46,6 +47,11 @@ const signUp = async (req, res) => { }; const createdUser = await models.User.create(user); + await verifyUser({ + id: createdUser.id, + email: createdUser.email, + firstName: createdUser.firstName + }); return res.status(201).json({ status: res.statusCode, diff --git a/src/database/migrations/20190724090213-create-user.js b/src/database/migrations/20190724090213-create-user.js index 2832bfe..38d52dc 100644 --- a/src/database/migrations/20190724090213-create-user.js +++ b/src/database/migrations/20190724090213-create-user.js @@ -59,6 +59,10 @@ export const up = (queryInterface, Sequelize) => queryInterface.createTable('Use type: Sequelize.INTEGER, defaultValue: 1 }, + verifiedToken: { + allowNull: true, + type: Sequelize.STRING, + }, createdAt: { allowNull: false, type: Sequelize.DATE diff --git a/src/database/models/index.js b/src/database/models/index.js index 37312b0..4de5c8a 100644 --- a/src/database/models/index.js +++ b/src/database/models/index.js @@ -33,6 +33,5 @@ Object.keys(db).forEach((modelName) => { }); db.sequelize = sequelize; -db.Sequelize = Sequelize; export default db; diff --git a/src/database/models/user.js b/src/database/models/user.js index 9e3d5a3..9894e78 100644 --- a/src/database/models/user.js +++ b/src/database/models/user.js @@ -50,6 +50,10 @@ export default (sequelize, DataTypes) => { type: DataTypes.BOOLEAN, defaultValue: false }, + verifiedToken: { + allowNull: true, + type: DataTypes.STRING + }, paymentStatus: { allowNull: false, type: DataTypes.BOOLEAN, diff --git a/src/helpers/emailMessages.js b/src/helpers/emailMessages.js index 05a9f24..4997d45 100644 --- a/src/helpers/emailMessages.js +++ b/src/helpers/emailMessages.js @@ -23,6 +23,33 @@ const forgotPasswordMessage = (firstName, token) => { return message; }; -export default { - forgotPasswordMessage +/** + * verify user email page + * @name page + * @param {object} info + * @returns {string} html page + */ + +const page = (info) => { + const [firstName, url] = info; + return ` + + + + + + + +
+

Authors Haven

+

Welcome, ${firstName}

+

You’ve sucessfully signed up to Authors Haven

+

Share your ideas, get reviews and request collaborations

+

Bring your ideas to life

+ Confirm Email +
+ +`; }; + +export default { forgotPasswordMessage, page }; diff --git a/src/helpers/verifyUser.js b/src/helpers/verifyUser.js new file mode 100644 index 0000000..9777fdc --- /dev/null +++ b/src/helpers/verifyUser.js @@ -0,0 +1,42 @@ +import jwt from 'jsonwebtoken'; +import pages from './emailMessages'; +import mailer from '../services/sendMail'; +import model from '../database/models'; + +const { User } = model; +const { page } = pages; + +/** + * supplies subject and html page for email + * @name msg + * @param {Array} userName + * @returns {object} subject and html page for email + */ + +const msg = (...userName) => ({ + subject: 'Authors Haven - Verify Token', + html: page(userName), +}); + +/** + * verify user helper function + * @name verifyUser + * @param {object} info + */ + +const verifyUser = async (info) => { + const { id, firstName, email } = info; + const token = jwt.sign({ id }, process.env.VERIFY_SECRET, { expiresIn: '5h' }); + await User.update({ + verifiedToken: token + }, { + where: { + id, + } + }); + const url = `http://127.0.0.1:3000/api/auth/verify/${token}/${id}`; + const message = msg(firstName, url); + mailer('williamsohworuka@gmail.com', email, message); +}; + +export default verifyUser; diff --git a/src/index.js b/src/index.js index 32fde47..084a053 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,11 @@ import debug from 'debug'; import swaggerUi from 'swagger-ui-express'; import YAML from 'yamljs'; import path from 'path'; +import dotenv from 'dotenv'; import routes from './routes'; +dotenv.config(); + const isProduction = process.env.NODE_ENV === 'production'; const isTest = process.env.NODE_ENV === 'test'; @@ -41,6 +44,8 @@ app.get('/', (req, res) => { }); }); +app.use('/api', routes); + const documentation = YAML.load(path.join(__dirname, '../docs/swagger.yaml')); documentation.servers[0].url = process.env.SERVER_URL; diff --git a/src/routes/index.js b/src/routes/index.js index 3950f50..b6e8cbc 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -1,9 +1,10 @@ import express from 'express'; import user from './user'; import auth from './auth'; +import verifyEmail from './verifyEmail'; const router = express.Router(); -router.use('/', user, auth); +router.use('/', user, auth, verifyEmail); export default router; diff --git a/src/routes/verifyEmail.js b/src/routes/verifyEmail.js new file mode 100644 index 0000000..9e7a615 --- /dev/null +++ b/src/routes/verifyEmail.js @@ -0,0 +1,10 @@ +import express from 'express'; +import AuthController from '../controllers/AuthController'; + +const { updateStatus } = AuthController; +const Router = express.Router(); + +const verifyBaseRoute = '/auth'; +Router.get(`${verifyBaseRoute}/verify/:token/:id`, updateStatus); + +export default Router; diff --git a/tests/auth.spec.js b/tests/auth.spec.js index b882406..52c7cb9 100644 --- a/tests/auth.spec.js +++ b/tests/auth.spec.js @@ -4,6 +4,7 @@ import sinon from 'sinon'; import server from '../src'; import models from '../src/database/models'; import mockData from './mockData'; +import mail from '../src/services/index'; chai.use(chaiHttp); const { expect } = chai; @@ -70,6 +71,8 @@ describe('AUTH', () => { // Forgot password route describe('Forgot password', () => { it('should sucessfully return an appropiate message after sending a mail to the user', (done) => { + const stub = sinon.stub(mail, 'sendMail'); + stub.returns({}); chai.request(server) .post(FORGOT_PASSWORD_URL) .send(forgotPasswordEmail) @@ -104,6 +107,7 @@ describe('AUTH', () => { expect(response).to.have.status(500); expect(response.body).to.be.an('object'); expect(response.body.error).to.equal('error occured!'); + stub.restore(); done(); }); }); diff --git a/tests/mockData/userMock.js b/tests/mockData/userMock.js index 31e268f..e60befc 100644 --- a/tests/mockData/userMock.js +++ b/tests/mockData/userMock.js @@ -99,7 +99,7 @@ export default { password: 'Password' }, forgotPasswordEmail: { - email: 'eden@gmail.com' + email: 'lordvader@order66.com' }, wrongForgotPasswordEmail: { email: 'example@gmail.com' diff --git a/tests/verifyUser.spec.js b/tests/verifyUser.spec.js new file mode 100644 index 0000000..41be244 --- /dev/null +++ b/tests/verifyUser.spec.js @@ -0,0 +1,115 @@ +import chai from 'chai'; +import chaiHttp from 'chai-http'; +import jwt from 'jsonwebtoken'; +import app from '../src/index'; +import models from '../src/database/models'; + +const { User } = models; + +chai.use(chaiHttp); +const { expect } = chai; +const invalidToken = 'jbkebvisgijlvesbv.genjweglnkjbe'; +let url; +let fakeIdUrl; +let invalidTokenUrl; +let expiredTokenUrl; +let tokenData; +let userId; + +describe('Test for base api base url', () => { + before(async () => { + const { id } = await User.create({ + firstName: 'kiddy', + lastName: 'kuddy', + username: 'maro', + email: 'williamsohworuka@gmail.com', + password: 'ohwill949', + }); + userId = id; + + tokenData = jwt.sign({ id }, process.env.VERIFY_SECRET); + const expTokenData = jwt.sign({ id }, process.env.VERIFY_SECRET, { expiresIn: '0.2s' }); + url = `/api/auth/verify/${tokenData}/${id}`; + fakeIdUrl = `/api/auth/verify/${tokenData}/${2345}`; + invalidTokenUrl = `/api/auth/verify/${invalidToken}/${id}`; + expiredTokenUrl = `/api/auth/verify/${expTokenData}/${id}`; + }); + + it('should return a sucess message on sucessful change of verified status', async () => { + await User.update({ + verifiedToken: tokenData + }, { + where: { + id: userId, + } + }); + chai.request(app) + .get(url) + .end((err, res) => { + expect(res).to.have.status(200); + expect(res.body.message).to.equal('You have sucessfully verified your email'); + }); + }); + + it('should return an error message for invalid token', async () => { + await User.update({ + verifiedToken: invalidToken + }, { + where: { + id: userId, + } + }); + chai.request(app) + .get(`${invalidTokenUrl}`) + .end((err, res) => { + expect(res).to.have.status(401); + expect(res.body.error).to.equal('Sorry could not verify email'); + }); + }); + + it('should return an error if token does not exist in the database', (done) => { + chai.request(app) + .get(url) + .end((err, res) => { + expect(res).to.have.status(401); + expect(res.body.error).to.equal('Sorry could not verify email'); + done(); + }); + }); + + it('should return an error message if user has been verified', async () => { + await User.update({ + verifiedToken: tokenData + }, { + where: { + id: userId, + } + }); + chai.request(app) + .get(`${url}`) + .end((err, res) => { + expect(res).to.have.status(400); + expect(res.body.error).to.equal('user already verified'); + }); + }); + + it('should return an error message if user does not exist', (done) => { + chai.request(app) + .get(`${fakeIdUrl}`) + .end((err, res) => { + expect(res).to.have.status(401); + expect(res.body.error).to.equal('Sorry could not verify email'); + done(); + }); + }); + + it('should return an error message if token has expired', (done) => { + chai.request(app) + .get(`${expiredTokenUrl}`) + .end((err, res) => { + expect(res).to.have.status(401); + expect(res.body.error).to.equal('Session has expired you can request for another one'); + done(); + }); + }); +});