Skip to content

Commit

Permalink
Merge 59c95f8 into 1f5c883
Browse files Browse the repository at this point in the history
  • Loading branch information
Rythae committed Aug 23, 2019
2 parents 1f5c883 + 59c95f8 commit 0adaa31
Show file tree
Hide file tree
Showing 13 changed files with 209 additions and 21 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ after_success:
- npm run coveralls

git:
depth: 20
depth: 50
25 changes: 25 additions & 0 deletions docs/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,31 @@ paths:
application/json:
schema:
type: object
properties:
error:
type: string
example: error occured
/auth/logout:
get:
tags:
- Users
summary: Logs out a user
responses:
200:
description: Logout was successful
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Logout was successful
401:
description: unauthorised request
500:
description: internal server error

components:
schemas:
token:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"heroku-postbuild": "npm run db:ready",
"start": "babel-node ./src/index.js",
"deleteBlToken": "babel-node ./src/services/deleteBlacklistedToken.js --exit",
"mocha-test": "nyc mocha --require @babel/register tests/*.js --timeout 20000 --exit",
"test": "cross-env NODE_ENV=test npm-run-all db:ready mocha-test",
"dev:start": "cross-env DEBUG=dev nodemon --exec babel-node ./src/index.js",
Expand Down
27 changes: 25 additions & 2 deletions src/controllers/AuthController.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const {
forgotPasswordMessage, responseMessage, errorResponse, successResponse,
} = helpers;
const { sendMail, userServices: { findUser } } = services;
const { User } = models;
const { User, BlacklistedToken } = models;

/**
*
Expand Down Expand Up @@ -65,4 +65,27 @@ const updateStatus = async (req, res) => {
return successResponse(res, 200, { message: 'You have sucessfully verified your email' });
};

export default { forgotPassword, updateStatus };
/**
*
* @name logOut
* @param {object} req
* @param {object} res
* @returns {json} - json
*/
const logOut = (req, res) => {
try {
const { token, decoded: { exp } } = req;
BlacklistedToken.create({ token, expTime: exp });
return res.status(200).json({
message: 'Logout was successful'
});
} catch (error) {
return responseMessage(res, 500, { error: error.message });
}
};

export default {
forgotPassword,
updateStatus,
logOut
};
23 changes: 23 additions & 0 deletions src/database/migrations/20190807135714-create-blacklisted-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export const up = (queryInterface, Sequelize) => queryInterface.createTable('BlacklistedTokens', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
expTime: {
type: Sequelize.STRING
},
token: {
type: Sequelize.STRING
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
export const down = queryInterface => queryInterface.dropTable('BlacklistedTokens');
8 changes: 8 additions & 0 deletions src/database/models/blacklistedtoken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default (sequelize, DataTypes) => {
const BlacklistedToken = sequelize.define('BlacklistedToken', {
expTime: DataTypes.STRING,
token: DataTypes.STRING
}, {});
BlacklistedToken.associate = () => {};
return BlacklistedToken;
};
29 changes: 18 additions & 11 deletions src/middlewares/verifyToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dotenv.config();
const { SECRET_KEY, ACCOUNT_VERIFICATION_SECRET } = process.env;
const verifyPath = '/auth/verify/:token';
const { responseMessage } = helpers;
const { userServices: { findUser } } = services;
const { userServices: { findUser }, blacklistedTokenService: { findBlacklistedToken } } = services;

/**
*
Expand All @@ -28,17 +28,24 @@ export default (request, response, next) => {
jwt.verify(token, secret, async (error, decoded) => {
if (error) {
const message = (error.name === 'TokenExpiredError') ? 'token expired' : 'invalid token';
return responseMessage(response, 401, { error: message });
}
try {
const user = await findUser(decoded.id);
if (!user) {
return responseMessage(response, 404, { error: 'user not found' });
responseMessage(response, 401, { error: message });
} else {
try {
const isBlacklisted = await findBlacklistedToken(token);
if (isBlacklisted) {
return responseMessage(response, 401, { error: 'invalid token' });
}
const user = await findUser(decoded.id);
if (!user) {
return responseMessage(response, 404, { error: 'user not found' });
}
request.user = user;
request.token = token;
request.decoded = decoded;
return next();
} catch (err) {
responseMessage(response, 500, { error: err.message });
}
request.user = user;
return next();
} catch (err) {
return responseMessage(response, 500, { error: err.message });
}
});
};
4 changes: 3 additions & 1 deletion src/routes/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ const auth = express.Router();
const AUTH_URL = '/auth';

const { userValidator, verifyToken } = middlewares;
const { forgotPassword, updateStatus } = AuthController;
const { forgotPassword, updateStatus, logOut } = AuthController;

// forgot password endpoint
auth.post(`${AUTH_URL}/forgotpassword`, userValidator.forgotPassword, forgotPassword);
auth.patch(`${AUTH_URL}/verify/:token`, verifyToken, updateStatus);

auth.get(`${AUTH_URL}/logout`, verifyToken, logOut);

export default auth;
9 changes: 9 additions & 0 deletions src/services/blacklistedToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import db from '../database/models';

const { BlacklistedToken } = db;

const findBlacklistedToken = token => BlacklistedToken.findOne({ where: { token } });

export default {
findBlacklistedToken
};
19 changes: 19 additions & 0 deletions src/services/deleteBlacklistedToken.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import db from '../database/models';

const { BlacklistedToken } = db;

const deleteExpiredBlacklistedToken = async () => {
const tokens = await BlacklistedToken.findAll();
const todaysDate = new Date();
const currentUnixTime = Math.floor(todaysDate.getTime() / 1000);
tokens.forEach((token) => {
const { expTime, id } = token;
if (currentUnixTime >= expTime) {
BlacklistedToken.destroy({
where: { id }
});
}
});
};

deleteExpiredBlacklistedToken();
4 changes: 3 additions & 1 deletion src/services/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import userServices from './userService';
import novelServices from './novelService';
import commentServices from './commentService';
import notificationServices from './notification';
import blacklistedTokenService from './blacklistedToken';

export default {
sendMail,
userServices,
novelServices,
commentServices,
notificationServices
notificationServices,
blacklistedTokenService
};
69 changes: 68 additions & 1 deletion tests/auth.spec.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import sinon from 'sinon';
import sendgridMail from '@sendgrid/mail';
import jwt from 'jsonwebtoken';
import sendgridMail from '@sendgrid/mail';
import server from '../src';
import mockData from './mockData';
import models from '../src/database/models';

chai.use(chaiHttp);
const { expect } = chai;

const { userMock } = mockData;
const { forgotPasswordEmail, wrongForgotPasswordEmail } = userMock;

const { BlacklistedToken } = models;

const BASE_URL = '/api/v1';
const FORGOT_PASSWORD_URL = `${BASE_URL}/auth/forgotPassword`;
const SIGN_OUT_URL = `${BASE_URL}/auth`;

const logoutToken = jwt.sign({ id: '122a0d86-8b78-4bb8-b28f-8e5f7811c456' }, process.env.SECRET_KEY, { expiresIn: '60 minutes' });
const logoutTokenTwo = jwt.sign({ id: 'ce87299b-0dfa-44ed-bb53-45d434647eb2' }, process.env.SECRET_KEY, { expiresIn: '60 minutes' });

describe('AUTH', () => {
describe('POST /auth/signup', () => {
Expand Down Expand Up @@ -166,3 +173,63 @@ describe('POST /api/users/login', () => {
});
});
});
// Logout route
describe('GET api/v1/auth/logout', () => {
it('should return a 401 error accessing the logout route without a token', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.end((err, res) => {
expect(res).status(401);
expect(res.body.error)
.to.eql('no token provided');
done();
});
});

it('should be able to logout successfully and return a status code of 200', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutToken}`)
.end((err, res) => {
expect(res).status(200);
expect(res.body)
.to.be.a('object');
expect(res.body.message)
.to.eql('Logout was successful');
done();
});
});

it('should not be allowed to sign in if token is blacklisted', (done) => {
chai
.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutToken}`)
.end((err, res) => {
expect(res).status(401);
expect(res.body)
.to.be.a('object');
expect(res.body.error)
.to.eql('invalid token');
done();
});
});

it('should return a failure response if a server error occurs', (done) => {
const stub = sinon.stub(BlacklistedToken, 'create');
stub.throws(new Error('error occurred!'));

chai.request(server)
.get(`${SIGN_OUT_URL}/logout`)
.set('Authorization', `${logoutTokenTwo}`)
.end((error, response) => {
expect(response).to.have.status(500);
expect(response.body).to.be.an('object');
expect(response.body.error).to.equal('error occurred!');
stub.restore();
done();
});
});
});
10 changes: 6 additions & 4 deletions tests/verifyUser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ describe('Test for base api base url', () => {
invalidIdUrl = `/api/v1/auth/verify/${invalidIdTokenData}`;
});

it('should return a sucess message on sucessful change of verified status', async () => {
await User.update({
it('should return a sucess message on sucessful change of verified status', (done) => {
User.update({
verifiedToken: tokenData
}, {
where: {
Expand All @@ -52,11 +52,12 @@ describe('Test for base api base url', () => {
.end((err, res) => {
expect(res).to.have.status(200);
expect(res.body.message).to.equal('You have sucessfully verified your email');
done();
});
});

it('should return an error message for invalid token', async () => {
await User.update({
it('should return an error message for invalid token', (done) => {
User.update({
verifiedToken: invalidToken
}, {
where: {
Expand All @@ -68,6 +69,7 @@ describe('Test for base api base url', () => {
.end((err, res) => {
expect(res).to.have.status(401);
expect(res.body.error).to.equal('invalid token');
done();
});
});

Expand Down

0 comments on commit 0adaa31

Please sign in to comment.