Skip to content

Commit

Permalink
feature(verification): send verification email
Browse files Browse the repository at this point in the history
[starts #167164980]
  • Loading branch information
WilliamsOhworuka committed Jul 31, 2019
1 parent 1248009 commit 7e8e36c
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 12 deletions.
1 change: 1 addition & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
NODE_ENV=
PORT=
SECRET_KEY=
VERIFY_SECRET =
DATABASE_URL_DEV=
DATABASE_URL_TEST=
SERVER_URL=
Expand Down
65 changes: 64 additions & 1 deletion docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,50 @@ paths:
409:
description: resource already exists
500:
description: internal server error
description: internal server error
/auth/verify/{token}/{id}:
get:
tags:
- Users
summary: "Verifies user account"
description: "Verifies user account"
parameters:
- name: "token"
in: "path"
description: "Token sent to email of user to be verified."
required: true
schema:
$ref: '#/components/schemas/token'

- name: "id"
in: "path"
description: "ID of user to be verified."
required: true
schema:
$ref: '#/components/schemas/token'


responses:
200:
description: "successful operation"
content:
application/json:
schema:
$ref: "#/components/schemas/veriryUserSuccess"
401:
description: "Invalid token, User with ID does not exist, Token does not exit in the database"
content:
application/json:
schema:
$ref: "#/components/schemas/verifyUserFailure1"

400:
description: "Already verified user"
content:
application/json:
schema:
$ref: "#/components/schemas/verifyUserFailure2"

/auth/forgotpassword:
post:
summary: Forgot password
Expand Down Expand Up @@ -103,6 +146,26 @@ paths:
$ref: '#/components/schemas/StandardServerResponse'
components:
schemas:
token:
type: string
veriryUserSuccess:
type: object
properties:
message:
type: string
example: "You have sucessfully verified your email"
verifyUserFailure1:
type: object
properties:
error:
type: string
example: "Sorry could not verify email"
verifyUserFailure2:
type: object
properties:
error:
type: string
example: "User already verified"
User:
required:
- firstName
Expand Down
56 changes: 54 additions & 2 deletions src/controllers/AuthController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import jwt from 'jsonwebtoken';
import helpers from '../helpers';
import services from '../services';
import models from '../database/models';
import responseMsg from '../helpers/responseHelper';

const { forgotPasswordMessage } = helpers;
const { sendMail, findUser } = services;
const { User } = models;
const { responseMessage } = responseMsg;

/**
*
Expand All @@ -12,6 +16,7 @@ const { sendMail, findUser } = services;
* @param {object} response
* @returns {json} - json
*/

const forgotPassword = async (request, response) => {
const { email } = request.body;
try {
Expand All @@ -31,6 +36,53 @@ const forgotPassword = async (request, response) => {
}
};

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 secret = process.env.ACCOUNT_VERIFICATION_SECRET;
const token = await jwt.verify(req.params.token, secret, (err, decoded) => {
if (err) {
return err;
}
return decoded;
});

if (!user) {
return responseMessage(res, 401, { error: 'Sorry could not verify email' });
}
const { verifiedToken } = user;

if (token.name) {
if (token.name === 'TokenExpiredError') {
return responseMessage(res, 401, { error: 'Session has expired you can request for another one' });
}
return responseMessage(res, 401, { error: 'Sorry could not verify email' });
}

if (verifiedToken !== req.params.token) {
return responseMessage(res, 401, { error: 'Sorry could not verify email' });
}

const { isVerified } = user.dataValues;
if (isVerified === 'true') {
return responseMessage(res, 400, { error: 'user already verified' });
}

await User.update({
isVerified: 'true',
}, {
where: {
id: req.params.id,
}
});
return responseMessage(res, 200, { message: 'You have sucessfully verified your email' });
};

export default { forgotPassword, updateStatus };
10 changes: 8 additions & 2 deletions src/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import models from '../database/models';
import helpers from '../helpers/index';
import verifyUser from '../helpers/verifyUser';

const { authHelper, responseHelper } = helpers;

Expand All @@ -24,7 +25,7 @@ const signUp = async (req, res) => {
message: 'user with email already exists'
}]
};
return responseHelper.successResponse(res, 409, data);
return responseHelper.responseMessage(res, 409, data);
}

if (foundUsername) {
Expand All @@ -34,7 +35,7 @@ const signUp = async (req, res) => {
message: 'username already taken'
}]
};
return responseHelper.successResponse(res, 409, data);
return responseHelper.responseMessage(res, 409, data);
}

const user = {
Expand All @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions src/database/migrations/20190724090213-create-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion src/database/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,5 @@ Object.keys(db).forEach((modelName) => {
});

db.sequelize = sequelize;
db.Sequelize = Sequelize;

export default db;
4 changes: 4 additions & 0 deletions src/database/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 29 additions & 2 deletions src/helpers/emailMessages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 VerifyAccountEmailPage = (info) => {
const [firstName, url] = info;
return `
<html>
<head>
<link href="https://fonts.googleapis.com/css?family=Courgette&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Courgette|Roboto&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css" integrity="sha384-50oBUHEmvpQ+1lW4y57PTFmhCaXp0ML5d60M1M7uH2+nqUivzIebhndOJK28anvf" crossorigin="anonymous">
</head>
<body>
<div style="border: 1px solid #E5E5E5; text-align: center; width: 550px; height: 300px">
<h1 style="font-family: 'Roboto', sans-serif; font-weight:normal; text-align: left; margin-left: 25px"><i style="color: #73C81F; margin-right:7px" class="fas fa-feather"></i>Authors Haven</h1>
<h3 style="font-weight: normal; font-family: 'Roboto', sans-serif; normal">Welcome, ${firstName}</h3>
<p style="font-family: 'Roboto', sans-serif">You’ve sucessfully signed up to Authors Haven</p>
<p style="font-weight: bold; font-family:'Courgette', cursive">Share your ideas, get reviews and request collaborations</p>
<p style="font-weight: bold; font-family:'Courgette', cursive">Bring your ideas to life</p>
<a href = "${url}" style="font-family:'Roboto', sans-serif; margin: 10px auto auto auto; color: black; display: block; width: 120px; border: 1px solid #73C81F; text-decoration: none; padding:14px; text-transform: uppercase">Confirm Email</a>
</div>
</body>
</html>`;
};

export default { forgotPasswordMessage, VerifyAccountEmailPage };
4 changes: 2 additions & 2 deletions src/helpers/responseHelper.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* @param {Object} data - application resource
* @returns {Object} custom response
*/
const successResponse = (res, statusCode, data) => res.status(statusCode).json(data);
const responseMessage = (res, statusCode, data) => res.status(statusCode).json(data);

/**
* helper for sending server success messages
Expand All @@ -16,4 +16,4 @@ const successResponse = (res, statusCode, data) => res.status(statusCode).json(d
*/
const errorResponse = (res, statusCode, error) => res.status(statusCode).json(error);

export default { successResponse, errorResponse };
export default { responseMessage, errorResponse };
42 changes: 42 additions & 0 deletions src/helpers/verifyUser.js
Original file line number Diff line number Diff line change
@@ -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 { VerifyAccountEmailPage } = 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: VerifyAccountEmailPage(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.ACCOUNT_VERIFICATION_SECRET, { expiresIn: '5h' });
await User.update({
verifiedToken: token
}, {
where: {
id,
}
});
const url = `http://127.0.0.1:3000/api/v1/auth/verify/${token}/${id}`;
const message = msg(firstName, url);
mailer('williamsohworuka@gmail.com', email, message);
};

export default verifyUser;
3 changes: 3 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down
3 changes: 2 additions & 1 deletion src/routes/index.js
Original file line number Diff line number Diff line change
@@ -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;
10 changes: 10 additions & 0 deletions src/routes/verifyEmail.js
Original file line number Diff line number Diff line change
@@ -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;
4 changes: 4 additions & 0 deletions tests/auth.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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();
});
});
Expand Down
2 changes: 1 addition & 1 deletion tests/mockData/userMock.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default {
password: 'Password'
},
forgotPasswordEmail: {
email: 'eden@gmail.com'
email: 'lordvader@order66.com'
},
wrongForgotPasswordEmail: {
email: 'example@gmail.com'
Expand Down
Loading

0 comments on commit 7e8e36c

Please sign in to comment.