Skip to content

Commit

Permalink
Merge pull request #14 from andela/ft/167190434-User-signout
Browse files Browse the repository at this point in the history
#167190434 logs out a user
  • Loading branch information
topseySuave committed Jul 30, 2019
2 parents 850bf4d + c6fa004 commit f2bfdde
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 19 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"allowImportExportEverywhere": true
},
"rules": {
"strict": "off",
"one-var": 0,
"one-var-declaration-per-line": 0,
"new-cap": 0,
Expand Down
17 changes: 15 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
"scripts": {
"start": "node build/index.js",
"dev": "export DEBUG=dev && nodemon --exec babel-node server/index.js",
"debug": "export DEBUG=dev && nodemon --exec babel-node server/index.js --inspect",
"clean": "rm -rf build && mkdir build && npm run copy-docs",
"build": "npm run clean && npm run migrate && babel -d ./build ./server",
"test": "export NODE_ENV=test && nyc mocha --require @babel/register server/tests --exit",
"pretest": "export NODE_ENV=test && npm run migrate:undo && npm run migrate",
"generate:model": "node_modules/.bin/sequelize model:generate",
"migrate": "node_modules/.bin/sequelize db:migrate",
"migrate:undo": "node_modules/.bin/sequelize db:migrate:undo",
"migrate:undo": "node_modules/.bin/sequelize db:migrate:undo:all",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"copy-docs": "cp -r server/docs/ build/docs/"
},
Expand All @@ -30,6 +31,7 @@
"express": "^4.16.3",
"jsonwebtoken": "^8.3.0",
"morgan": "^1.9.1",
"node-cron": "^2.0.3",
"pg": "^7.11.0",
"pg-hstore": "^2.3.3",
"sequelize": "^5.10.2",
Expand Down
41 changes: 29 additions & 12 deletions server/controllers/userController.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import sequelize from 'sequelize';
import models from '../db/models';
import utils from '../helpers/Utilities';
import hash from '../helpers/passwordHash';
import auth from '../helpers/auth';
import helpers from '../helpers';

const { Op } = sequelize;
const {
addToBlacklist, generateToken, errorStat, successStat,
comparePassword, hashPassword
} = helpers;

/**
* @Module UserController
Expand All @@ -29,12 +31,12 @@ class UserController {
}
});
if (existingUser) {
return utils.errorStat(res, 409, 'User Already Exists');
return errorStat(res, 409, 'User Already Exists');
}
const newUser = { ...req.body.user, password: hash.hashPassword(password) };
const newUser = { ...req.body.user, password: hashPassword(password) };
const user = await models.User.create(newUser);
const token = auth.generateToken({ id: user.id, username, email });
return utils.successStat(res, 201, 'user', {
const token = generateToken({ id: user.id, username, email });
return successStat(res, 201, 'user', {
id: user.id, token, username, firstname, lastname, email,
});
}
Expand All @@ -51,13 +53,13 @@ class UserController {
const { email, password } = req.body.user;
const user = await models.User.findOne({ where: { email } });

if (!user) return utils.errorStat(res, 401, 'Incorrect Login information');
const matchPasswords = hash.comparePassword(password, user.password);
if (!matchPasswords) return utils.errorStat(res, 401, 'Incorrect Login information');
if (!user) return errorStat(res, 401, 'Incorrect Login information');
const matchPasswords = comparePassword(password, user.password);
if (!matchPasswords) return errorStat(res, 401, 'Incorrect Login information');

return utils.successStat(res, 200, 'user', {
return successStat(res, 200, 'user', {
id: user.id,
token: await auth.generateToken({ id: user.id, username: user.username, email }),
token: await generateToken({ id: user.id, username: user.username, email }),
firstname: user.firstname,
lastname: user.firstname,
username: user.username,
Expand All @@ -66,6 +68,21 @@ class UserController {
image: user.image,
});
}

/**
* @static
* @description Allows a user to sign out
* @param {Object} req - Request object
* @param {Object} res - Response object
* @returns {Object} object containing user data and access Token
* @memberof UserController
*/
static async logout(req, res) {
const authorizationHeader = req.headers.authorization;
const token = req.headers.authorization.split(' ')[1] || authorizationHeader;
await addToBlacklist(token);
return successStat(res, 204, 'message', 'No Content');
}
}

export default UserController;
28 changes: 28 additions & 0 deletions server/db/migrations/20190729061549-create-token-blacklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('TokenBlacklists', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
token: {
type: Sequelize.STRING
},
expires: {
type: Sequelize.BIGINT
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
// eslint-disable-next-line no-unused-vars
down: (queryInterface, Sequelize) => queryInterface.dropTable('TokenBlacklists')
};
9 changes: 9 additions & 0 deletions server/db/models/tokenblacklist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';

module.exports = (sequelize, DataTypes) => {
const TokenBlacklist = sequelize.define('TokenBlacklist', {
token: DataTypes.STRING,
expires: DataTypes.BIGINT
}, {});
return TokenBlacklist;
};
21 changes: 21 additions & 0 deletions server/docs/ah-commando-doc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,27 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
/users/logout:
post:
tags:
- Users
summary: User logs out
description: An endpoint to log out a user
responses:
'204':
description: Logout successful
'401':
description: Invalid login details
content:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
'500':
description: Server error
content:
application/json:
schema:
$ref: "#/components/schemas/errorResponse"
components:
securitySchemes:
bearerAuth:
Expand Down
64 changes: 64 additions & 0 deletions server/helpers/blacklistToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import cron from 'node-cron';
import jwt from 'jsonwebtoken';
import { Op } from 'sequelize';
import models from '../db/models';

const { TokenBlacklist } = models;
/**
* @Module TokenBlacklist
* @description Authentication related methods
*/
class Blacklist {
/**
* @static
* @param {string} token - user token
* @returns {null} - no value
* @memberof Blacklist
*/
static async addToBlacklist(token) {
const { exp } = jwt.decode(token);
await TokenBlacklist.create({
token,
expires: exp
});
}

/**
* @static
* @param {string} token - user token
* @returns {null} - null
* @memberof Blacklist
*/
static async checkBlacklist(token) {
const blacklistedToken = await TokenBlacklist.findOne({
where: { token }
});
return blacklistedToken;
}

/* istanbul ignore next */
/**
* @static
* @returns {null} - null
* @memberof Blacklist
*/
static async startCronJob() {
/* starts a cron job that runs 11:59 (hours)
everyday to clear the BlacklistToken table of expired tokens
*/
cron.schedule('59 23 * * *', () => {
const presentTime = Date.now().valueOf() / 1000;
TokenBlacklist.destroy({
where: {
expires: {
[Op.lt]: presentTime
}
}
});
});
}
}

Blacklist.startCronJob();

export default Blacklist;
19 changes: 19 additions & 0 deletions server/helpers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import blacklistToken from './blacklistToken';
import Auth from './auth';
import passwordHash from './passwordHash';
import Utilities from './Utilities';

const { generateToken } = Auth;
const { hashPassword, comparePassword } = passwordHash;
const { errorStat, successStat } = Utilities;
const { addToBlacklist, checkBlacklist } = blacklistToken;

export default {
addToBlacklist,
checkBlacklist,
generateToken,
hashPassword,
comparePassword,
errorStat,
successStat
};
39 changes: 39 additions & 0 deletions server/middlewares/authenticate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import jwt from 'jsonwebtoken';
import utilities from '../helpers';

const secret = process.env.SECRET_KEY;
const { errorStat, checkBlacklist } = utilities;

/**
* @Module Authenticate
* @description Authentication related methods
*/
class Authenticate {
/**
* @static
* @description Authenticate the routes
* @param {object} req - Request object
* @param {object} res - Response object
* @param {Object} next - Next function call
* @returns {object} Json
* @memberof Authenticate
*/
static async validateToken(req, res, next) {
const authorizationHeader = req.headers.authorization;
const token = req.headers.authorization.split(' ')[1] || authorizationHeader;
if (!token) return errorStat(res, 401, 'Authorization error');
jwt.verify(token, secret, async (err) => {
if (err) {
const message = (err.name === 'TokenExpiredError') ? 'token expired' : 'invalid token';
return errorStat(res, 401, message);
}
const blacklist = await checkBlacklist(token);
if (blacklist) {
return errorStat(res, 401, 'invalid token');
}
next();
});
}
}

export default Authenticate;
11 changes: 11 additions & 0 deletions server/middlewares/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Authenticate from './authenticate';
import InputValidator from './inputValidator';

const { validateToken } = Authenticate;
const { validateLogin, validateUser } = InputValidator;

export default {
validateToken,
validateLogin,
validateUser
};
11 changes: 7 additions & 4 deletions server/routes/user.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import express from 'express';
import UserController from '../controllers/userController';
import InputValidator from '../middlewares/inputValidator';
import middlewares from '../middlewares';

const userRoute = express();

const { signUp, login } = UserController;
const { validateUser, validateLogin } = InputValidator;
const userRoute = express();
const { validateToken, validateLogin, validateUser } = middlewares;
const { signUp, login, logout } = UserController;

userRoute.post('/', validateUser, signUp);
userRoute.post('/login', validateLogin, login);

// logs out a user
userRoute.post('/logout', validateToken, logout);

export default userRoute;
Loading

0 comments on commit f2bfdde

Please sign in to comment.