diff --git a/src/middleware/validators.js b/src/middleware/validators.js index 9e9dc86..842afad 100644 --- a/src/middleware/validators.js +++ b/src/middleware/validators.js @@ -1,3 +1,7 @@ +/* + REST API validator middleware. +*/ + const User = require('../models/users') const config = require('../../config') const getToken = require('../lib/auth') diff --git a/src/rpc/users/index.js b/src/rpc/users/index.js index c21729e..ca09fe0 100644 --- a/src/rpc/users/index.js +++ b/src/rpc/users/index.js @@ -7,12 +7,14 @@ const jsonrpc = require('jsonrpc-lite') // Local libraries const UserLib = require('../../lib/users') +const Validators = require('../validators') class UserRPC { constructor (localConfig) { // Encapsulate dependencies this.userLib = new UserLib() this.jsonrpc = jsonrpc + this.validators = new Validators() } // Top-level router for this library. All other methods in this class are for @@ -27,7 +29,8 @@ class UserRPC { // Route the call based on the value of the method property. switch (endpoint) { case 'getAll': - return await this.getAll() + // await this.validators.ensureUser(rpcData) + return await this.getAll(rpcData) case 'getUser': return await this.getUser(rpcData) } diff --git a/src/rpc/validators.js b/src/rpc/validators.js new file mode 100644 index 0000000..2deb376 --- /dev/null +++ b/src/rpc/validators.js @@ -0,0 +1,40 @@ +/* + Validators for the JSON RPC +*/ +/* eslint no-useless-catch: 0 */ + +// Public npm libraries +const jwt = require('jsonwebtoken') + +// Local libraries +const config = require('../../config') +const UserModel = require('../models/users') + +class Validators { + constructor () { + // Encapsulate dependencies + this.config = config + this.UserModel = UserModel + this.jwt = jwt + } + + // Returns if user passes a valid JWT token. Otherwise it throws an error. + async ensureUser (rpcData) { + try { + // console.log('rpcData: ', rpcData) + + const apiToken = rpcData.payload.params.apiToken + if (!apiToken) throw new Error('apiToken JWT required as a parameter') + + const decoded = this.jwt.verify(apiToken, this.config.token) + + const user = await this.UserModel.findById(decoded.id, '-password') + if (!user) throw new Error('User not found!') + } catch (err) { + // console.error('Error in ensureUser()') + throw err + } + } +} + +module.exports = Validators diff --git a/test/unit/json-rpc/a12-validators.unit.js b/test/unit/json-rpc/a12-validators.unit.js new file mode 100644 index 0000000..a63c043 --- /dev/null +++ b/test/unit/json-rpc/a12-validators.unit.js @@ -0,0 +1,149 @@ +/* + Unit tests for the JSON RPC validator middleware. +*/ + +// Public npm libraries +const jsonrpc = require('jsonrpc-lite') +const mongoose = require('mongoose') +const sinon = require('sinon') +const assert = require('chai').assert +const { v4: uid } = require('uuid') + +// Set the environment variable to signal this is a test. +process.env.SVC_ENV = 'test' + +// Local libraries +const config = require('../../../config') +const Validators = require('../../../src/rpc/validators') +const UserLib = require('../../../src/lib/users') +const userLib = new UserLib() + +describe('#validators', () => { + let testUser + let uut + let sandbox + + before(async () => { + // Connect to the Mongo Database. + console.log(`Connecting to database: ${config.database}`) + mongoose.Promise = global.Promise + mongoose.set('useCreateIndex', true) // Stop deprecation warning. + await mongoose.connect( + config.database, + { + useUnifiedTopology: true, + useNewUrlParser: true + } + ) + + // Create a test user. + testUser = await userLib.createUser({ + email: 'test544@test.com', + name: 'tester544', + password: 'password' + }) + // console.log('testUser: ', testUser) + }) + + beforeEach(() => { + sandbox = sinon.createSandbox() + + uut = new Validators() + }) + + afterEach(() => sandbox.restore()) + + after(async () => { + // Delete the test user. + testUser = await userLib.getUser({ id: testUser.userData._id }) + await userLib.deleteUser(testUser) + + mongoose.connection.close() + }) + + describe('#ensureUser', () => { + it('should return quietly for valid JWT token', async () => { + // Generate the parsed data that the main router would pass to this + // endpoint. + const id = uid() + const userCall = jsonrpc.request(id, 'users', { + endpoint: 'getAll', + apiToken: testUser.token + }) + const jsonStr = JSON.stringify(userCall, null, 2) + const rpcData = jsonrpc.parse(jsonStr) + + await uut.ensureUser(rpcData) + + assert.isOk('Not throwing an error is a success!') + }) + + it('should throw an error if JWT token is not included', async () => { + try { + // Generate the parsed data that the main router would pass to this + // endpoint. + const id = uid() + const userCall = jsonrpc.request(id, 'users', { + endpoint: 'getAll' + }) + const jsonStr = JSON.stringify(userCall, null, 2) + const rpcData = jsonrpc.parse(jsonStr) + + await uut.ensureUser(rpcData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, 'apiToken JWT required as a parameter') + } + }) + + it('should throw an error if JWT token can not be decoded', async () => { + try { + const token = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYwNmQxYTlkNTgxNTVjMjIzNWFmMTNhMSIsImlhdCI6MTYxNzc2Mjk3M30.6JkM1v0n71Mzsd3qzClzlMKtq6HlD0umoauG23N9FFF' + + // Generate the parsed data that the main router would pass to this + // endpoint. + const id = uid() + const userCall = jsonrpc.request(id, 'users', { + endpoint: 'getAll', + apiToken: token + }) + const jsonStr = JSON.stringify(userCall, null, 2) + const rpcData = jsonrpc.parse(jsonStr) + + await uut.ensureUser(rpcData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, 'invalid signature') + } + }) + + it('should throw an error if the user can not be found', async () => { + try { + // Force 'error not found' error + sandbox.stub(uut.UserModel, 'findById').resolves(null) + + // Generate the parsed data that the main router would pass to this + // endpoint. + const id = uid() + const userCall = jsonrpc.request(id, 'users', { + endpoint: 'getAll', + apiToken: testUser.token + }) + const jsonStr = JSON.stringify(userCall, null, 2) + const rpcData = jsonrpc.parse(jsonStr) + + await uut.ensureUser(rpcData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, 'User not found!') + } + }) + }) +}) diff --git a/test/unit/json-rpc/a12-users.unit.js b/test/unit/json-rpc/a13-users.unit.js similarity index 100% rename from test/unit/json-rpc/a12-users.unit.js rename to test/unit/json-rpc/a13-users.unit.js