diff --git a/src/middleware/validators.js b/src/middleware/validators.js index d17773a..3de4f21 100644 --- a/src/middleware/validators.js +++ b/src/middleware/validators.js @@ -4,124 +4,146 @@ const getToken = require('../lib/auth') const jwt = require('jsonwebtoken') const wlogger = require('../lib/wlogger') -async function ensureUser (ctx, next) { - // console.log(`getToken: ${typeof (getToken)}`) - const token = getToken(ctx) +let _this - if (!token) { - // console.log(`Err: Token not provided.`) - ctx.throw(401) +class Validators { + constructor () { + this.User = User + this.getToken = getToken + this.jwt = jwt + this.config = config + + _this = this } - let decoded = null - try { - // console.log(`token: ${JSON.stringify(token, null, 2)}`) - // console.log(`config: ${JSON.stringify(config, null, 2)}`) - decoded = jwt.verify(token, config.token) - } catch (err) { - // console.log(`Err: Token could not be decoded: ${err}`) - ctx.throw(401) - } + async ensureUser (ctx, next) { + try { + // console.log(`getToken: ${typeof (getToken)}`) + const token = _this.getToken(ctx) - ctx.state.user = await User.findById(decoded.id, '-password') - if (!ctx.state.user) { - // console.log(`Err: Could not find user.`) - ctx.throw(401) - } + if (!token) { + // console.log(`Err: Token not provided.`) + ctx.throw(401) + } - return next() -} + let decoded = null + try { + // console.log(`token: ${JSON.stringify(token, null, 2)}`) + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + decoded = _this.jwt.verify(token, config.token) + } catch (err) { + // console.log(`Err: Token could not be decoded: ${err}`) + ctx.throw(401) + } -// This funciton is almost identical to ensureUser, except at the end, it verifies -// that the 'type' associated with the user equals 'admin'. -async function ensureAdmin (ctx, next) { - // console.log(`getToken: ${typeof (getToken)}`) - const token = getToken(ctx) + ctx.state.user = await _this.User.findById(decoded.id, '-password') + if (!ctx.state.user) { + // console.log(`Err: Could not find user.`) + ctx.throw(401) + } - if (!token) { - // console.log(`Err: Token not provided.`) - ctx.throw(401) - } - - let decoded = null - try { - // console.log(`token: ${JSON.stringify(token, null, 2)}`) - // console.log(`config: ${JSON.stringify(config, null, 2)}`) - decoded = jwt.verify(token, config.token) - } catch (err) { - // console.log(`Err: Token could not be decoded: ${err}`) - ctx.throw(401) - } - - ctx.state.user = await User.findById(decoded.id, '-password') - if (!ctx.state.user) { - // console.log(`Err: Could not find user.`) - ctx.throw(401) - } - - if (ctx.state.user.type !== 'admin') { - ctx.throw(401, 'not admin') - } - - return next() -} - -// This middleware ensures that the :id used in the API endpoint matches the -// the ID used in the JWT, or failing that, the ID used in the JWT matches -// an Admin user. This prevents situations like users updating other users -// profiles or non-admins deleting users. -// TODO Tests must be developed before developing this function. -async function ensureTargetUserOrAdmin (ctx, next) { - // console.log(`getToken: ${typeof (getToken)}`) - const token = getToken(ctx) - - if (!token) { - // console.log(`Err: Token not provided.`) - ctx.throw(401) - } - - // The user ID targeted in this API call. - const targetId = ctx.params.id - // console.log(`targetId: ${JSON.stringify(targetId, null, 2)}`) - - let decoded = null - try { - // console.log(`token: ${JSON.stringify(token, null, 2)}`) - // console.log(`config: ${JSON.stringify(config, null, 2)}`) - decoded = jwt.verify(token, config.token) - } catch (err) { - // console.log(`Err: Token could not be decoded: ${err}`) - ctx.throw(401) - } - - ctx.state.user = await User.findById(decoded.id, '-password') - if (!ctx.state.user) { - // console.log(`Err: Could not find user.`) - ctx.throw(401) - } - - // console.log(`ctx.state.user: ${JSON.stringify(ctx.state.user, null, 2)}`) - // Ensure the calling user and the target user are the same. - if (ctx.state.user._id.toString() !== targetId.toString()) { - wlogger.verbose( - `Calling user and target user do not match! Calling user: ${ - ctx.state.user._id - }, Target user: ${targetId}` - ) - - // If they don't match, then the calling user better be an admin. - if (ctx.state.user.type !== 'admin') { - ctx.throw(401, 'not admin') - } else { - wlogger.verbose('It\'s ok. The user is an admin.') + return next() + } catch (error) { + ctx.throw(401) } } - return next() + // This funciton is almost identical to ensureUser, except at the end, it verifies + // that the 'type' associated with the user equals 'admin'. + async ensureAdmin (ctx, next) { + try { + // console.log(`getToken: ${typeof (getToken)}`) + const token = _this.getToken(ctx) + + if (!token) { + // console.log(`Err: Token not provided.`) + ctx.throw(401) + } + + let decoded = null + try { + // console.log(`token: ${JSON.stringify(token, null, 2)}`) + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + decoded = _this.jwt.verify(token, config.token) + } catch (err) { + // console.log(`Err: Token could not be decoded: ${err}`) + ctx.throw(401) + } + + ctx.state.user = await _this.User.findById(decoded.id, '-password') + if (!ctx.state.user) { + // console.log(`Err: Could not find user.`) + ctx.throw(401) + } + + if (ctx.state.user.type !== 'admin') { + ctx.throw(401, 'not admin') + } + + return next() + } catch (error) { + ctx.throw(401, error.message) + } + } + + // This middleware ensures that the :id used in the API endpoint matches the + // the ID used in the JWT, or failing that, the ID used in the JWT matches + // an Admin user. This prevents situations like users updating other users + // profiles or non-admins deleting users. + // TODO Tests must be developed before developing this function. + async ensureTargetUserOrAdmin (ctx, next) { + try { + // console.log(`getToken: ${typeof (getToken)}`) + const token = _this.getToken(ctx) + + if (!token) { + // console.log(`Err: Token not provided.`) + ctx.throw(401) + } + + // The user ID targeted in this API call. + const targetId = ctx.params.id + // console.log(`targetId: ${JSON.stringify(targetId, null, 2)}`) + + let decoded = null + try { + // console.log(`token: ${JSON.stringify(token, null, 2)}`) + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + decoded = _this.jwt.verify(token, config.token) + } catch (err) { + // console.log(`Err: Token could not be decoded: ${err}`) + ctx.throw(401) + } + + ctx.state.user = await _this.User.findById(decoded.id, '-password') + if (!ctx.state.user) { + // console.log(`Err: Could not find user.`) + ctx.throw(401) + } + + // console.log(`ctx.state.user: ${JSON.stringify(ctx.state.user, null, 2)}`) + // Ensure the calling user and the target user are the same. + + if (ctx.state.user._id.toString() !== targetId.toString()) { + wlogger.verbose( + `Calling user and target user do not match! Calling user: ${ + ctx.state.user._id + }, Target user: ${targetId}` + ) + + // If they don't match, then the calling user better be an admin. + if (ctx.state.user.type !== 'admin') { + ctx.throw(401, 'not admin') + } else { + wlogger.verbose('It\'s ok. The user is an admin.') + } + } + + return next() + } catch (error) { + ctx.throw(401, error.message) + } + } } -module.exports = { - ensureUser, - ensureAdmin, - ensureTargetUserOrAdmin -} +module.exports = Validators diff --git a/src/modules/users/router.js b/src/modules/users/router.js index f79b6f2..20d4869 100644 --- a/src/modules/users/router.js +++ b/src/modules/users/router.js @@ -1,4 +1,6 @@ -const validator = require('../../middleware/validators') +const VALIDATOR = require('../../middleware/validators') +const validator = new VALIDATOR() + const CONTROLLER = require('./controller') const controller = new CONTROLLER() diff --git a/test/a08-validators.spec.js b/test/a08-validators.spec.js new file mode 100644 index 0000000..b957fbb --- /dev/null +++ b/test/a08-validators.spec.js @@ -0,0 +1,317 @@ +const assert = require('chai').assert +const testUtils = require('./utils') + +const Validators = require('../src/middleware/validators') + +const sinon = require('sinon') +const mockContext = require('./mocks/ctx-mock').context + +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +const context = {} + +let sandbox +let uut +describe('Validators', () => { + before(async () => { + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + + // Create a second test user. + const userObj = { + email: 'test2@test.com', + password: 'pass2' + } + const testUser = await testUtils.createUser(userObj) + // console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`) + + context.user = testUser.user + context.token = testUser.token + context.id = testUser.user._id + + // Get the JWT used to log in as the admin 'system' user. + const adminJWT = await testUtils.getAdminJWT() + // console.log(`adminJWT: ${adminJWT}`) + context.adminJWT = adminJWT + + // const admin = await testUtils.loginAdminUser() + // context.adminJWT = admin.token + + // const admin = await adminLib.loginAdmin() + // console.log(`admin: ${JSON.stringify(admin, null, 2)}`) + }) + beforeEach(() => { + uut = new Validators() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('ensureUser()', () => { + it('should throw 401 if user cant be found', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'findById').resolves(false) + + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: `Bearer ${context.token}` + } + } + + await uut.ensureUser(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if token not found', async () => { + try { + // Mock the context object. + const ctx = mockContext() + + await uut.ensureUser(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if token is invalid', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: 'Bearer 1' + } + } + await uut.ensureUser(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should trigger the "next" function if user is admin', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: context.id } + + ctx.request = { + header: { + authorization: `Bearer ${context.adminJWT}` + } + } + // Function that execute if the validations + // are successful + const next = () => { return 'next function' } + + const result = await uut.ensureUser(ctx, next) + + assert.isString(result) + assert.equal(result, 'next function') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) + + describe('ensureAdmin()', () => { + it('should throw 401 if token not found', async () => { + try { + // Mock the context object. + const ctx = mockContext() + + await uut.ensureAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if token is invalid', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: 'Bearer 1' + } + } + await uut.ensureAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if user cant be found', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'findById').resolves(false) + + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: `Bearer ${context.token}` + } + } + await uut.ensureAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if user is not admin type', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: `Bearer ${context.token}` + } + } + await uut.ensureAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'not admin') + } + }) + it('should trigger the "next" function if user is admin', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.request = { + header: { + authorization: `Bearer ${context.adminJWT}` + } + } + // Function that execute if the validations + // are successful + const next = () => { return 'next function' } + + const result = await uut.ensureAdmin(ctx, next) + + assert.isString(result) + assert.equal(result, 'next function') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) + + describe('ensureTargetUserOrAdmin()', () => { + it('should throw 401 if token not found', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: context.id } + await uut.ensureTargetUserOrAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if token is invalid', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: context.id } + + ctx.request = { + header: { + authorization: 'Bearer 1' + } + } + await uut.ensureTargetUserOrAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if user cant be found', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'findById').resolves(false) + + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: context.id } + + ctx.request = { + header: { + authorization: `Bearer ${context.token}` + } + } + await uut.ensureTargetUserOrAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'Unauthorized') + } + }) + it('should throw 401 if user is not admin type', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: 'Target Id' } + + ctx.request = { + header: { + authorization: `Bearer ${context.token}` + } + } + await uut.ensureTargetUserOrAdmin(ctx) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.status, 401) + assert.include(err.message, 'not admin') + } + }) + it('should trigger the "next" function if user is admin', async () => { + try { + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: context.id } + + ctx.request = { + header: { + authorization: `Bearer ${context.adminJWT}` + } + } + // Function that execute if the validations + // are successful + const next = () => { return 'next function' } + + const result = await uut.ensureTargetUserOrAdmin(ctx, next) + + assert.isString(result) + assert.equal(result, 'next function') + } catch (err) { + assert(false, 'Unexpected result') + } + }) + }) +})