From 5f2291b7a33627df4a92fbcc5b564ce059114c7c Mon Sep 17 00:00:00 2001 From: Reza Akhavan Date: Sat, 3 Feb 2018 14:17:00 -0800 Subject: [PATCH] hapi 17 && async/await (#198) --- .gitignore | 1 + .travis.yml | 12 +- README.md | 68 +- config.js | 17 +- first-time-setup.js | 249 +- index.js | 11 - manifest.js | 217 +- package-lock.json | 4166 +++++++++++-------- package.json | 51 +- server.js | 26 +- server/api/accounts.js | 520 +-- server/api/admin-groups.js | 182 +- server/api/admins.js | 429 +- server/api/auth-attempts.js | 129 - server/api/contact.js | 37 +- server/api/index.js | 22 - server/api/login.js | 274 +- server/api/logout.js | 49 +- server/api/main.js | 22 + server/api/sessions.js | 180 +- server/api/signup.js | 219 +- server/api/statuses.js | 147 +- server/api/users.js | 552 +-- server/auth.js | 142 +- server/mailer.js | 66 +- server/models/account.js | 115 +- server/models/admin-group.js | 40 +- server/models/admin.js | 175 +- server/models/auth-attempt.js | 95 +- server/models/note-entry.js | 20 +- server/models/session.js | 150 +- server/models/status-entry.js | 18 +- server/models/status.js | 43 +- server/models/user.js | 254 +- server/preware.js | 43 + server/web/index.jade | 6 - server/web/index.js | 23 - server/web/main.js | 21 + test/config.js | 8 +- test/index.js | 21 - test/manifest.js | 8 +- test/server.js | 21 + test/server/api/accounts.js | 1176 ++---- test/server/api/admin-groups.js | 428 +- test/server/api/admins.js | 1017 ++--- test/server/api/auth-attempts.js | 250 -- test/server/api/contact.js | 77 +- test/server/api/index.js | 54 - test/server/api/login.js | 574 +-- test/server/api/logout.js | 159 +- test/server/api/main.js | 48 + test/server/api/sessions.js | 392 +- test/server/api/signup.js | 383 +- test/server/api/statuses.js | 370 +- test/server/api/users.js | 983 ++--- test/server/auth.js | 627 +-- test/server/fixtures/credentials-account.js | 38 - test/server/fixtures/credentials-admin.js | 34 - test/server/fixtures/creds.js | 140 + test/server/fixtures/db.js | 25 + test/server/fixtures/hapi.js | 14 + test/server/fixtures/index.js | 15 + test/server/fixtures/make-mock-model.js | 18 - test/server/mailer.js | 118 +- test/server/models/account.js | 121 +- test/server/models/admin-group.js | 112 +- test/server/models/admin.js | 402 +- test/server/models/auth-attempt.js | 165 +- test/server/models/note-entry.js | 14 +- test/server/models/session.js | 231 +- test/server/models/status-entry.js | 15 +- test/server/models/status.js | 59 +- test/server/models/user.js | 423 +- test/server/preware.js | 153 + test/server/web/index.js | 71 - test/server/web/main.js | 60 + 76 files changed, 6760 insertions(+), 10855 deletions(-) delete mode 100644 index.js delete mode 100644 server/api/auth-attempts.js delete mode 100644 server/api/index.js create mode 100644 server/api/main.js create mode 100644 server/preware.js delete mode 100644 server/web/index.jade delete mode 100644 server/web/index.js create mode 100644 server/web/main.js delete mode 100644 test/index.js create mode 100644 test/server.js delete mode 100644 test/server/api/auth-attempts.js delete mode 100644 test/server/api/index.js create mode 100644 test/server/api/main.js delete mode 100644 test/server/fixtures/credentials-account.js delete mode 100644 test/server/fixtures/credentials-admin.js create mode 100644 test/server/fixtures/creds.js create mode 100644 test/server/fixtures/db.js create mode 100644 test/server/fixtures/hapi.js create mode 100644 test/server/fixtures/index.js delete mode 100644 test/server/fixtures/make-mock-model.js create mode 100644 test/server/preware.js delete mode 100644 test/server/web/index.js create mode 100644 test/server/web/main.js diff --git a/.gitignore b/.gitignore index d3a6397..c764daf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/* test/artifacts/* .env +.eslintrc.js diff --git a/.travis.yml b/.travis.yml index 27c07a1..34af131 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,6 @@ -sudo: true language: node_js node_js: - "8" + - "9" services: - mongodb -env: - - NODE_ENV=test CXX=g++-4.8 -before_install: - - sudo apt-get install unicode-data -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 diff --git a/README.md b/README.md index 2469ded..45b9f02 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,6 @@ this repo and build one on top of Frame. | url | username | password | |:-------------------------------------------------------------------------- |:-------- |:-------- | | [https://getframe.herokuapp.com/](https://getframe.herokuapp.com/) | root | root | -| [https://getframe.herokuapp.com/docs](https://getframe.herokuapp.com/docs) | ---- | ---- | [Postman](http://www.getpostman.com/) is a great tool for testing and developing APIs. See the wiki for details on [how to @@ -44,19 +43,14 @@ login](https://github.com/jedireza/frame/wiki/How-to-login). ## Requirements -You need [Node.js](http://nodejs.org/download/) installed and you'll need -[MongoDB](http://www.mongodb.org/downloads) installed and running. - -We use [`bcrypt`](https://github.com/ncb000gt/node.bcrypt.js) for hashing -secrets. If you have issues during installation related to `bcrypt` then [refer -to this wiki -page](https://github.com/jedireza/frame/wiki/bcrypt-Installation-Trouble). +You need [Node.js](http://nodejs.org/download/) `>=8.x` and you'll need a +[MongoDB](http://www.mongodb.org/downloads) `>=2.6` server running. ## Installation ```bash -$ git clone git@github.com:jedireza/frame.git +$ git clone https://github.com/jedireza/frame.git $ cd frame $ npm install ``` @@ -101,7 +95,7 @@ $ npm run first-time-setup ```bash $ npm start -# > frame@0.0.0 start /Users/jedireza/projects/frame +# > frame@0.0.0 start /home/jedireza/projects/frame # > ./node_modules/nodemon/bin/nodemon.js -e js,md server # 09 Sep 03:47:15 - [nodemon] v1.10.2 @@ -114,11 +108,28 @@ see the welcome message. [`nodemon`](https://github.com/remy/nodemon) watches for changes in server code and restarts the app automatically. -We also pass the `--inspect` flag to Node so you have a debugger available. -Watch the output of `$ npm start` and look for the debugging URL and open it in -Chrome. It looks something like this: +### With the debugger -`chrome-devtools://devtools/remote/serve_file/@62cd277117e6f8ec53e31b1be58290a6f7ab42ef/inspector.html?experiments=true&v8only=true&ws=localhost:9229/node` +```bash +$ npm run inspect + +# > frame@0.0.0 inspect /home/jedireza/projects/frame +# > nodemon --inspect -e js,md server.js + +# [nodemon] 1.14.12 +# [nodemon] to restart at any time, enter `rs` +# [nodemon] watching: *.* +# [nodemon] starting `node --inspect server.js` +# Debugger listening on ws://127.0.0.1:9229/3d706d9a-b3e0-4fc6-b64e-e7968b7f94d0 +# For help see https://nodejs.org/en/docs/inspector +# 180203/193534.071, [log,info,mongodb] data: HapiMongoModels: successfully connected to the db. +# 180203/193534.127, [log,info,mongodb] data: HapiMongoModels: finished processing auto indexes. +# Server started on port 9000 +``` + +Once started with the debuger you can open Google Chrome and go to +[chrome://inspect](chrome://inspect). See https://nodejs.org/en/docs/inspector/ +for more details. ## Running in production @@ -157,23 +168,32 @@ use to write all of our tests. ```bash $ npm test -# > frame@0.0.0 test /Users/jedireza/projects/frame -# > ./node_modules/lab/bin/lab -c +# > frame@0.0.0 test /home/jedireza/projects/frame +# > lab -c -L -# .................................................. -# .................................................. -# .................................................. -# .................................................. -# .................................................. -# ........ +# .................................................. +# .................................................. +# .................................................. +# .............. -# 258 tests complete -# Test duration: 2398 ms +# 164 tests complete +# Test duration: 14028 ms # No global variable leaks detected # Coverage: 100.00% # Linting results: No issues ``` +### Targeted tests + +If you'd like to run a specific test or subset of tests you can use the +`test-server` npm script. + +You specificy the path(s) via the `TEST_TARGET` environment variable like: + +```bash +$ TEST_TARGET=test/server/web/main.js npm run test-server +``` + ## License MIT diff --git a/config.js b/config.js index c5950cc..2e7c3ad 100644 --- a/config.js +++ b/config.js @@ -32,11 +32,18 @@ const config = { }, hapiMongoModels: { mongodb: { - uri: { - $filter: 'env', - production: process.env.MONGODB_URI, - test: 'mongodb://localhost:27017/frame-test', - $default: 'mongodb://localhost:27017/frame' + connection: { + uri: { + $filter: 'env', + production: process.env.MONGODB_URI, + $default: 'mongodb://localhost:27017/' + }, + db: { + $filter: 'env', + production: process.env.MONGODB_DB_NAME, + test: 'frame-test', + $default: 'frame' + } } }, autoIndex: true diff --git a/first-time-setup.js b/first-time-setup.js index 0415590..c0b949f 100644 --- a/first-time-setup.js +++ b/first-time-setup.js @@ -1,172 +1,109 @@ 'use strict'; -const Async = require('async'); +const Account = require('./server/models/account'); +const Admin = require('./server/models/admin'); +const AdminGroup = require('./server/models/admin-group'); +const AuthAttempt = require('./server/models/auth-attempt'); const MongoModels = require('mongo-models'); -const Mongodb = require('mongodb'); const Promptly = require('promptly'); +const Session = require('./server/models/session'); +const Status = require('./server/models/status'); +const User = require('./server/models/user'); -Async.auto({ - mongodbUri: (done) => { +const main = async function () { - const options = { - default: 'mongodb://localhost:27017/frame' - }; + let options = {}; - Promptly.prompt(`MongoDB URI: (${options.default})`, options, done); - }, - testMongo: ['mongodbUri', (results, done) => { + // get mongodb connection info - Mongodb.MongoClient.connect(results.mongodbUri, {}, (err, db) => { + options = { + default: 'mongodb://localhost:27017/' + }; + const mongodbUri = await Promptly.prompt(`MongoDB URI: (${options.default})`, options); - if (err) { - console.error('Failed to connect to Mongodb.'); - return done(err); - } + options = { + default: 'frame' + }; + const mongodbName = await Promptly.prompt(`MongoDB name: (${options.default})`, options); - db.close(); - done(null, true); - }); - }], - rootEmail: ['testMongo', (results, done) => { - - Promptly.prompt('Root user email:', done); - }], - rootPassword: ['rootEmail', (results, done) => { - - Promptly.password('Root user password:', done); - }], - setupRootUser: ['rootPassword', (results, done) => { - - const Account = require('./server/models/account'); - const AdminGroup = require('./server/models/admin-group'); - const Admin = require('./server/models/admin'); - const AuthAttempt = require('./server/models/auth-attempt'); - const Session = require('./server/models/session'); - const Status = require('./server/models/status'); - const User = require('./server/models/user'); - - Async.auto({ - connect: function (done) { - - MongoModels.connect(results.mongodbUri, {}, done); - }, - clean: ['connect', (dbResults, done) => { - - Async.parallel([ - Account.deleteMany.bind(Account, {}), - AdminGroup.deleteMany.bind(AdminGroup, {}), - Admin.deleteMany.bind(Admin, {}), - AuthAttempt.deleteMany.bind(AuthAttempt, {}), - Session.deleteMany.bind(Session, {}), - Status.deleteMany.bind(Status, {}), - User.deleteMany.bind(User, {}) - ], done); - }], - adminGroup: ['clean', function (dbResults, done) { - - AdminGroup.create('Root', done); - }], - admin: ['clean', function (dbResults, done) { - - const document = { - _id: Admin.ObjectId('111111111111111111111111'), - name: { - first: 'Root', - middle: '', - last: 'Admin' - }, - timeCreated: new Date() - }; - - Admin.insertOne(document, (err, docs) => { - - done(err, docs && docs[0]); - }); - }], - user: ['clean', function (dbResults, done) { - - Async.auto({ - passwordHash: User.generatePasswordHash.bind(this, results.rootPassword) - }, (err, passResults) => { - - if (err) { - return done(err); - } - - const document = { - _id: Admin.ObjectId('000000000000000000000000'), - isActive: true, - username: 'root', - password: passResults.passwordHash.hash, - email: results.rootEmail.toLowerCase(), - timeCreated: new Date() - }; - - User.insertOne(document, (err, docs) => { - - done(err, docs && docs[0]); - }); - }); - }], - adminMembership: ['admin', function (dbResults, done) { - - const id = dbResults.admin._id.toString(); - const update = { - $set: { - groups: { - root: 'Root' - } - } - }; - - Admin.findByIdAndUpdate(id, update, done); - }], - linkUser: ['admin', 'user', function (dbResults, done) { - - const id = dbResults.user._id.toString(); - const update = { - $set: { - 'roles.admin': { - id: dbResults.admin._id.toString(), - name: 'Root Admin' - } - } - }; - - User.findByIdAndUpdate(id, update, done); - }], - linkAdmin: ['admin', 'user', function (dbResults, done) { - - const id = dbResults.admin._id.toString(); - const update = { - $set: { - user: { - id: dbResults.user._id.toString(), - name: 'root' - } - } - }; - - Admin.findByIdAndUpdate(id, update, done); - }] - }, (err, dbResults) => { - - if (err) { - console.error('Failed to setup root user.'); - return done(err); - } + // connect to db - done(null, true); - }); - }] -}, (err, results) => { + const db = await MongoModels.connect({ uri: mongodbUri, db: mongodbName }); - if (err) { - console.error('Setup failed.'); - console.error(err); - return process.exit(1); + if (!db) { + throw Error('Could not connect to MongoDB.'); } - console.log('Setup complete.'); + // get root user creds + + const rootEmail = await Promptly.prompt('Root user email:'); + const rootPassword = await Promptly.password('Root user password:'); + + // clear tables + + await Promise.all([ + Account.deleteMany({}), + AdminGroup.deleteMany({}), + Admin.deleteMany({}), + AuthAttempt.deleteMany({}), + Session.deleteMany({}), + Status.deleteMany({}), + User.deleteMany({}) + ]); + + // setup root group + + await AdminGroup.create('Root'); + + // setup root admin and user + + await Admin.insertOne(new Admin({ + _id: Admin.ObjectId('111111111111111111111111'), + groups: { + root: 'Root' + }, + name: { + first: 'Root', + middle: '', + last: 'Admin' + }, + user: { + id: '000000000000000000000000', + name: 'root' + } + })); + + const passwordHash = await User.generatePasswordHash(rootPassword); + + await User.insertOne(new User({ + _id: User.ObjectId('000000000000000000000000'), + email: rootEmail.toLowerCase(), + password: passwordHash.hash, + roles: { + admin: { + id: '111111111111111111111111', + name: 'Root Admin' + } + }, + username: 'root' + })); + + // all done + + MongoModels.disconnect(); + + console.log('First time setup complete.'); + process.exit(0); +}; + + +main().catch((err) => { + + console.log('First time setup failed.'); + console.error(err); + + MongoModels.disconnect(); + + process.exit(1); }); diff --git a/index.js b/index.js deleted file mode 100644 index 82d42c5..0000000 --- a/index.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; -const Glue = require('glue'); -const Manifest = require('./manifest'); - - -const composeOptions = { - relativeTo: __dirname -}; - - -module.exports = Glue.compose.bind(Glue, Manifest.get('/'), composeOptions); diff --git a/manifest.js b/manifest.js index c4c752b..71ea8a2 100644 --- a/manifest.js +++ b/manifest.js @@ -1,6 +1,7 @@ 'use strict'; const Confidence = require('confidence'); const Config = require('./config'); +const Path = require('path'); const criteria = { @@ -14,138 +15,106 @@ const manifest = { debug: { request: ['error'] }, - connections: { - routes: { - security: true - } - } - }, - connections: [{ - port: Config.get('/port/web'), - labels: ['web'] - }], - registrations: [ - { - plugin: 'hapi-auth-basic' - }, - { - plugin: 'lout' - }, - { - plugin: 'inert' - }, - { - plugin: 'vision' + routes: { + security: true }, - { - plugin: { - register: 'visionary', + port: Config.get('/port/web') + }, + register: { + plugins: [ + { + plugin: 'good', options: { - engines: { jade: 'jade' }, - path: './server/web' + reporters: { + myConsoleReporter: [ + { + module: 'good-squeeze', + name: 'Squeeze', + args: [{ + error: '*', + log: '*', + request: '*', + response:'*' + }] + }, + { + module: 'good-console', + args: [{ + color: { + $filter: 'env', + production: false, + $default: true + } + }] + }, + 'stdout' + ] + } } - } - }, - { - plugin: { - register: 'hapi-mongo-models', + }, + { + plugin: 'hapi-auth-basic' + }, + { + plugin: 'hapi-remote-address' + }, + { + plugin: 'hapi-mongo-models', options: { mongodb: Config.get('/hapiMongoModels/mongodb'), - models: { - Account: './server/models/account', - AdminGroup: './server/models/admin-group', - Admin: './server/models/admin', - AuthAttempt: './server/models/auth-attempt', - Session: './server/models/session', - Status: './server/models/status', - User: './server/models/user' - }, + models: [ + Path.resolve(__dirname, './server/models/account'), + Path.resolve(__dirname, './server/models/admin-group'), + Path.resolve(__dirname, './server/models/admin'), + Path.resolve(__dirname, './server/models/auth-attempt'), + Path.resolve(__dirname, './server/models/session'), + Path.resolve(__dirname, './server/models/status'), + Path.resolve(__dirname, './server/models/user') + ], autoIndex: Config.get('/hapiMongoModels/autoIndex') } + }, + { + plugin: './server/auth' + }, + { + plugin: './server/api/accounts' + }, + { + plugin: './server/api/admin-groups' + }, + { + plugin: './server/api/admins' + }, + { + plugin: './server/api/contact' + }, + { + plugin: './server/api/main' + }, + { + plugin: './server/api/login' + }, + { + plugin: './server/api/logout' + }, + { + plugin: './server/api/sessions' + }, + { + plugin: './server/api/signup' + }, + { + plugin: './server/api/statuses' + }, + { + plugin: './server/api/users' + }, + { + plugin: './server/web/main' } - }, - { - plugin: './server/auth' - }, - { - plugin: './server/mailer' - }, - { - plugin: './server/api/accounts', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/admin-groups', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/admins', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/auth-attempts', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/contact', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/index', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/login', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/logout', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/sessions', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/signup', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/statuses', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/api/users', - options: { - routes: { prefix: '/api' } - } - }, - { - plugin: './server/web/index' - } - ] + ] + } }; diff --git a/package-lock.json b/package-lock.json index 97be1fc..5439258 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,41 +5,24 @@ "requires": true, "dependencies": { "abbrev": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", - "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, "accept": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/accept/-/accept-2.1.4.tgz", - "integrity": "sha1-iHr1TO7lx/RDBGGXHsQAxh0JrLs=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/accept/-/accept-3.0.2.tgz", + "integrity": "sha512-bghLXFkCOsC1Y2TZ51etWfKDs6q249SAoHTZVfzWWdlZxoij+mgkj9AmUJWQpDY48TfnrTDIe43Xem4zdMe7mQ==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1", + "hoek": "5.0.2" } }, "acorn": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz", - "integrity": "sha1-q259nYhqrKiwhbwzEreaGYQz8Oc=" - }, - "acorn-globals": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz", - "integrity": "sha1-VbtemGkVB7dFedBRNBMhfDgMVM8=", - "requires": { - "acorn": "2.7.0" - } + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.4.1.tgz", + "integrity": "sha512-XLmq3H/BVvW6/GbxKryGxWORz1ebilSsUDlyC27bXhWGWAZWkGwS6FLHjOlwFXNFoWFQEO/Df4u0YYd0K3BQgQ==", + "dev": true }, "acorn-jsx": { "version": "3.0.1", @@ -59,18 +42,20 @@ } }, "ajv": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", - "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { "co": "4.6.0", - "json-stable-stringify": "1.0.1" + "fast-deep-equal": "1.0.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, "ajv-keywords": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.0.tgz", - "integrity": "sha1-opbhf3v658HOT34N5T0pyzIWLfA=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz", + "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I=", "dev": true }, "alce": { @@ -98,20 +83,62 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" }, "ammo": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/ammo/-/ammo-2.0.4.tgz", - "integrity": "sha1-v4CqshFpjqePY+9efxE91dnokX8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ammo/-/ammo-3.0.0.tgz", + "integrity": "sha512-6yoz9MXYV9sgCHrwprHWPxBaJ9/roQRfXzS//4JCNgKfPYcghFNwJQKBt6vWOoSGGRHsP6qsLJ+xtKStKJWdLQ==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1" + "boom": "6.0.0", + "hoek": "5.0.2" }, "dependencies": { "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-6.0.0.tgz", + "integrity": "sha512-LYLa8BmiiOWjvxTMVh73lcZzd2E5yczrKvxAny1UuzO2tkarLrw4tdp3rdfmus3+YfKcZP0vRSM3Obh+fGK6eA==", + "requires": { + "hoek": "5.0.2" + } + } + } + }, + "ansi-align": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz", + "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=", + "dev": true, + "requires": { + "string-width": "2.1.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, "requires": { - "hoek": "4.1.1" + "ansi-regex": "3.0.0" } } } @@ -134,19 +161,19 @@ "dev": true }, "anymatch": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.0.tgz", - "integrity": "sha1-o+Uvo5FoyCX/V7AkgSbOWo/5VQc=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", + "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "arrify": "1.0.1", - "micromatch": "2.3.11" + "micromatch": "3.1.5", + "normalize-path": "2.1.1" } }, "aproba": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.2.tgz", - "integrity": "sha512-ZpYajIfO0j2cOFTO955KUMIKNmj6zhX8kVztMAxFsDaMwz+9Z9SV0uou2pC9HJqcfpffOsjnbrDMvkNy+9RXPw==" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "are-we-there-yet": { "version": "1.1.4", @@ -167,13 +194,10 @@ } }, "arr-diff": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", - "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", - "dev": true, - "requires": { - "arr-flatten": "1.1.0" - } + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true }, "arr-flatten": { "version": "1.1.0", @@ -181,6 +205,12 @@ "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", "dev": true }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -197,9 +227,9 @@ "dev": true }, "array-unique": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", - "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, "arrify": { @@ -208,28 +238,26 @@ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, - "asap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/asap/-/asap-1.0.0.tgz", - "integrity": "sha1-sqRdpf36ILBJb8N2jMJ8EvqRan0=" - }, "asn1": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" }, "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true }, "async": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", - "requires": { - "lodash": "4.17.4" - } + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" }, "async-each": { "version": "1.0.1", @@ -242,10 +270,16 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "atob": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.0.3.tgz", + "integrity": "sha1-GcenYEc3dEaPILLS0DNyrX1Mv10=", + "dev": true + }, "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" }, "aws4": { "version": "1.6.0", @@ -253,9 +287,9 @@ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" }, "b64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/b64/-/b64-3.0.2.tgz", - "integrity": "sha1-ep1gRmrfe43hFMvfZRpf38yQiU0=" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/b64/-/b64-4.0.0.tgz", + "integrity": "sha512-EhmUQodKB0sdzPPrbIWbGqA5cQeTWxYrAgNeeT1rLZWtD3tbNTnphz8J4vkXI3cPgBNlXBjzEbzDzq0Nwi4f9A==" }, "babel-code-frame": { "version": "6.26.0", @@ -266,6 +300,27 @@ "chalk": "1.1.3", "esutils": "2.0.2", "js-tokens": "3.0.2" + }, + "dependencies": { + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "2.2.1", + "escape-string-regexp": "1.0.5", + "has-ansi": "2.0.0", + "strip-ansi": "3.0.1", + "supports-color": "2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } } }, "balanced-match": { @@ -273,14 +328,28 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "dev": true, + "requires": { + "cache-base": "1.0.1", + "class-utils": "0.3.6", + "component-emitter": "1.2.1", + "define-property": "1.0.0", + "isobject": "3.0.1", + "mixin-deep": "1.3.0", + "pascalcase": "0.1.1" + } + }, "bcrypt": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.2.tgz", - "integrity": "sha1-0F/F0iMXPg4o7DgcDwDMJf+vJzY=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-1.0.3.tgz", + "integrity": "sha512-pRyDdo73C8Nim3jwFJ7DWe3TZCgwDfWZ6nHS5LSdU77kWbj1frruvdndP02AOavtD4y8v6Fp2dolbHgp4SDrfg==", "requires": { - "bindings": "1.2.1", - "nan": "2.5.0", - "node-pre-gyp": "0.6.32" + "nan": "2.6.2", + "node-pre-gyp": "0.6.36" } }, "bcrypt-pbkdf": { @@ -292,17 +361,17 @@ "tweetnacl": "0.14.5" } }, + "big-time": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/big-time/-/big-time-2.0.0.tgz", + "integrity": "sha512-OXsmBxlRLwUc65MLta2EOyMTLcjZQkxHkJ81lVPeyVqZag8zhUfKRYIbF3E/IW/LWR8kf8a1GlRYkBXKVGqJOw==" + }, "binary-extensions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.8.0.tgz", - "integrity": "sha1-SOyNFt9Dd+rl+liEaCSAr02Vx3Q=", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", + "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", "dev": true }, - "bindings": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", - "integrity": "sha1-FK1hE4EtLTfXLme0ystLtyZQXxE=" - }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -312,21 +381,85 @@ } }, "boom": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", - "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-7.1.1.tgz", + "integrity": "sha512-qwEARHTliqgEQiVkzKkkbLt3q0vRPIW60VRZ8zRnbjsm7INkPe9NxfAYDDYLZOdhxyUHa1gIe639Cx7t6RH/4A==", "requires": { - "hoek": "4.1.1" + "hoek": "5.0.2" } }, "bossy": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/bossy/-/bossy-3.0.4.tgz", - "integrity": "sha1-+a6fJugbQaMY9O4Ng2huSlwlB7k=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bossy/-/bossy-4.0.1.tgz", + "integrity": "sha512-IrXZdXnDrjfk9ZVtnnmehlcTGK/KRqUJuNZJteMGU2/cJdXC6yWf2yhkAAbAgjOTsJJHXdlYloTeFH1ZgRNllw==", + "dev": true, + "requires": { + "boom": "7.1.1", + "hoek": "5.0.2", + "joi": "13.1.2" + } + }, + "bounce": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bounce/-/bounce-1.2.0.tgz", + "integrity": "sha512-8syCGe8B2/WC53118/F/tFy5aW00j+eaGPXmAUP7iBhxc+EBZZxS1vKelWyBCH6IqojgS2t1gF0glH30qAJKEw==", + "requires": { + "boom": "7.1.1", + "hoek": "5.0.2" + } + }, + "boxen": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz", + "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==", "dev": true, "requires": { - "hoek": "4.1.1", - "joi": "10.6.0" + "ansi-align": "2.0.0", + "camelcase": "4.1.0", + "chalk": "2.3.0", + "cli-boxes": "1.0.0", + "string-width": "2.1.1", + "term-size": "1.2.0", + "widest-line": "2.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } + } } }, "brace-expansion": { @@ -339,14 +472,22 @@ } }, "braces": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", - "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.0.tgz", + "integrity": "sha512-P4O8UQRdGiMLWSizsApmXVQDBS6KCt7dSexgLKBmH5Hr1CZq7vsnscFh8oR1sP1ab1Zj0uCHCEzZeV6SfUf3rA==", "dev": true, "requires": { - "expand-range": "1.8.2", - "preserve": "0.2.0", - "repeat-element": "1.1.2" + "arr-flatten": "1.1.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fill-range": "4.0.0", + "isobject": "3.0.1", + "repeat-element": "1.1.2", + "snapdragon": "0.8.1", + "snapdragon-node": "2.1.1", + "split-string": "3.1.0", + "to-regex": "3.0.1" } }, "bson": { @@ -354,33 +495,35 @@ "resolved": "https://registry.npmjs.org/bson/-/bson-1.0.4.tgz", "integrity": "sha1-k8ENOeqltYQVy8QFLz5T5WKwtyw=" }, - "buffer-shims": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-shims/-/buffer-shims-1.0.0.tgz", - "integrity": "sha1-mXjOMXOIxkmth5MCjDR37wRKi1E=" - }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "requires": { + "collection-visit": "1.0.0", + "component-emitter": "1.2.1", + "get-value": "2.0.6", + "has-value": "1.0.0", + "isobject": "3.0.1", + "set-value": "2.0.0", + "to-object-path": "0.3.0", + "union-value": "1.0.0", + "unset-value": "1.0.0" + } + }, "call": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/call/-/call-4.0.2.tgz", - "integrity": "sha1-33b19R7o3Ui4VqyEAPfmnm1zmcQ=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/call/-/call-5.0.1.tgz", + "integrity": "sha512-ollfFPSshiuYLp7AsrmpkQJ/PxCi6AzV81rCjBwWhyF2QGyUY/vPDMzoh4aUcWyucheRglG2LaS5qkIEfLRh6A==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1", + "hoek": "5.0.2" } }, "caller-path": { @@ -403,79 +546,91 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=" }, + "capture-stack-trace": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz", + "integrity": "sha1-Sm+gc5nCa7pH8LJJa00PtAjFVQ0=", + "dev": true + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "catbox": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/catbox/-/catbox-7.1.4.tgz", - "integrity": "sha1-ipUO0YtkuoCIwa4TLoXFhHnStsw=", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/catbox/-/catbox-10.0.2.tgz", + "integrity": "sha512-cTQTQeKMhWHU0lX8CADE3g1koGJu+AlcWFzAjMX/8P+XbkScGYw3tJsQpe2Oh8q68vOQbOLacz9k+6V/F3Z9DA==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1", - "joi": "10.6.0" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1", + "bounce": "1.2.0", + "hoek": "5.0.2", + "joi": "13.1.2" } }, "catbox-memory": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/catbox-memory/-/catbox-memory-2.0.4.tgz", - "integrity": "sha1-Qz4lWQLK9UIz0ShkKcj03xToItU=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/catbox-memory/-/catbox-memory-3.1.1.tgz", + "integrity": "sha512-fl6TI/uneeUb9NGClKWZWkpCZQrkPmuVz/Jaqqb15vqW6KGfJ/vMP/ZMp8VgAkyTrrRvFHbFcS67sbU7EkvbhQ==", "requires": { - "hoek": "4.1.1" + "big-time": "2.0.0", + "boom": "7.1.1", + "hoek": "5.0.2" } }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "optional": true, "requires": { "align-text": "0.1.4", "lazy-cache": "1.0.4" } }, "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", "dev": true, "requires": { - "ansi-styles": "2.2.1", + "ansi-styles": "3.2.0", "escape-string-regexp": "1.0.5", - "has-ansi": "2.0.0", - "strip-ansi": "3.0.1", - "supports-color": "2.0.0" + "supports-color": "4.4.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "dev": true, + "requires": { + "color-convert": "1.9.1" + } + } } }, - "character-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-1.2.1.tgz", - "integrity": "sha1-wN3kqxgnE7kZuXCVmhI+zBow/NY=" + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true }, "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.0.tgz", + "integrity": "sha512-OgXCNv2U6TnG04D3tth0gsvdbV4zdbxFG3sYUqcoQMoEFVd1j1pZR6TZ8iknC45o9IJ6PeQI/J6wT/+cHcniAw==", "dev": true, "requires": { - "anymatch": "1.3.0", + "anymatch": "2.0.0", "async-each": "1.0.1", - "fsevents": "1.1.2", - "glob-parent": "2.0.0", + "braces": "2.3.0", + "fsevents": "1.1.3", + "glob-parent": "3.1.0", "inherits": "2.0.3", "is-binary-path": "1.0.1", - "is-glob": "2.0.1", + "is-glob": "4.0.0", + "normalize-path": "2.1.1", "path-is-absolute": "1.0.1", "readdirp": "2.1.0" } @@ -486,73 +641,150 @@ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", "dev": true }, - "clean-css": { - "version": "3.4.27", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.27.tgz", - "integrity": "sha1-re91sxwWD/pdcvTeZ5ZuJmDBolU=", + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "dev": true, "requires": { - "commander": "2.8.1", - "source-map": "0.4.4" + "arr-union": "3.1.0", + "define-property": "0.2.5", + "isobject": "3.0.1", + "static-extend": "0.1.2" }, "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { - "graceful-readlink": "1.0.1" + "is-descriptor": "0.1.6" } - } - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wrap-ansi": "2.1.0" - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/code/-/code-4.1.0.tgz", - "integrity": "sha1-IJrRHQWvigwceq9pTZ+k0sfZW4U=", - "dev": true, - "requires": { - "hoek": "4.1.1" - } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "cli-boxes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz", + "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM=", + "dev": true + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "dev": true, + "requires": { + "restore-cursor": "2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, + "cliui": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", + "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", + "requires": { + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wrap-ansi": "2.1.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/code/-/code-5.1.2.tgz", + "integrity": "sha512-Typ0BuWOKPGNOY9M7hBDY60J9uSPok4Y7hhtTG/3Cpqg0/AhauZSWax0Mb0lZuzm89w1YgVvl8BTrBW/4Sw6sw==", + "dev": true, + "requires": { + "hoek": "5.0.2" + } }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "requires": { + "map-visit": "1.0.0", + "object-visit": "1.0.1" + } + }, "color-convert": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.0.tgz", - "integrity": "sha1-Gsz5fdc5uYO/mU1W/sj5WFNkG3o=", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", "dev": true, "requires": { "color-name": "1.1.3" @@ -572,10 +804,11 @@ "delayed-stream": "1.0.0" } }, - "commander": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.6.0.tgz", - "integrity": "sha1-nfflL7Kgyw+4kFjugMMQQiXzfh0=" + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", + "dev": true }, "concat-map": { "version": "0.0.1", @@ -600,7 +833,7 @@ "requires": { "alce": "1.2.0", "boom": "3.2.2", - "hoek": "4.1.1", + "hoek": "4.2.0", "yargs": "4.8.1" }, "dependencies": { @@ -609,33 +842,28 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-3.2.2.tgz", "integrity": "sha1-DwzF0ErcUAO4x9cfQsynJx/vDng=", "requires": { - "hoek": "4.1.1" + "hoek": "4.2.0" } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" } } }, "configstore": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz", - "integrity": "sha1-w1eB0FAdJowlxUuLF/YkDopPsCE=", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.1.tgz", + "integrity": "sha512-5oNkD/L++l0O6xGXxb1EWS7SivtjfGQlRyxJsYgE0Z495/L81e2h4/d3r969hoPXuFItzNOKMtsXgYG4c7dYvw==", "dev": true, "requires": { + "dot-prop": "4.2.0", "graceful-fs": "4.1.11", - "mkdirp": "0.5.1", - "object-assign": "4.1.1", - "os-tmpdir": "1.0.2", - "osenv": "0.1.4", - "uuid": "2.0.3", - "write-file-atomic": "1.3.4", - "xdg-basedir": "2.0.0" - }, - "dependencies": { - "uuid": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz", - "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho=", - "dev": true - } + "make-dir": "1.1.0", + "unique-string": "1.0.0", + "write-file-atomic": "2.3.0", + "xdg-basedir": "3.0.0" } }, "console-control-strings": { @@ -643,89 +871,73 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, - "constantinople": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.0.2.tgz", - "integrity": "sha1-S5RdmTeQe82Y7ldRIsOBdRZUQUE=", - "requires": { - "acorn": "2.7.0" - } - }, "content": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/content/-/content-3.0.4.tgz", - "integrity": "sha1-yj3eBEgPElGbcVJuxEvUiN37P+8=", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/content/-/content-4.0.3.tgz", + "integrity": "sha512-BrMfT1xXZHaXyPT/sneXc3IQzh8uL15JWV1R5tU0xo4sBGjF7BN+IRi9WoQLSbfNEs7bJ3E69rjxBKg/RL7lOQ==", "requires": { - "boom": "5.1.0" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1" } }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", + "dev": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "create-error-class": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", + "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=", + "dev": true, + "requires": { + "capture-stack-trace": "1.0.0" + } + }, "cross-spawn": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, "requires": { - "lru-cache": "4.0.2", + "lru-cache": "4.1.1", "shebang-command": "1.2.0", "which": "1.3.0" } }, "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", + "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", "requires": { - "boom": "2.10.1" + "boom": "5.2.0" }, "dependencies": { "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", + "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", "requires": { - "hoek": "2.16.3" + "hoek": "4.2.0" } }, "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" } } }, - "css": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/css/-/css-1.0.8.tgz", - "integrity": "sha1-k4aBHKgrzMnuf7WnMrHioxfIo+c=", - "requires": { - "css-parse": "1.0.4", - "css-stringify": "1.0.5" - } - }, - "css-parse": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-1.0.4.tgz", - "integrity": "sha1-OLBQP7+dqfVOnB29pg4UXHcRe90=" - }, - "css-stringify": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/css-stringify/-/css-stringify-1.0.5.tgz", - "integrity": "sha1-sNBClG2ylTu50pKQCmy19tASIDE=" + "crypto-random-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz", + "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", + "dev": true }, "dashdash": { "version": "1.14.1", @@ -733,21 +945,14 @@ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "requires": { "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } } }, "debug": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "requires": { - "ms": "0.7.1" + "ms": "2.0.0" } }, "decamelize": { @@ -755,6 +960,12 @@ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", + "dev": true + }, "deep-extend": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", @@ -766,6 +977,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "dev": true, + "requires": { + "is-descriptor": "1.0.2" + } + }, "del": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", @@ -778,7 +998,7 @@ "object-assign": "4.1.1", "pify": "2.3.0", "pinkie-promise": "2.0.1", - "rimraf": "2.5.4" + "rimraf": "2.6.2" } }, "delayed-stream": { @@ -792,25 +1012,33 @@ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.4.0.tgz", + "integrity": "sha512-QpVuMTEoJMF7cKzi6bvWhRulU1fZqZnvyVQgNhPaxxuTYwyjn/j1v9falseQ/uXWwPnO56RBfwtg4h/EQXmucA==", "dev": true }, "doctrine": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", - "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", "dev": true, "requires": { - "esutils": "2.0.2", - "isarray": "1.0.0" + "esutils": "2.0.2" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "dev": true, + "requires": { + "is-obj": "1.0.1" } }, "dotenv": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-4.0.0.tgz", - "integrity": "sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0=" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-5.0.0.tgz", + "integrity": "sha512-p4A7snaxI9Hnj3GDWhTpckHYcd9WwZDmGPcvJJV3CoRFq0Dvsp96eYgXBl9WbmbJfuxqiZ2WenNaeWSs675ghQ==" }, "duplexer": { "version": "0.1.1", @@ -818,13 +1046,18 @@ "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", "dev": true }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true + }, "duplexify": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", - "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", - "dev": true, + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.3.tgz", + "integrity": "sha512-g8ID9OroF9hKt2POf8YLayy+9594PzmM3scI00/uBXocX3TWNgoB67hjzkFe9ITAbQOne/lLdBxHXvYUM4ZgGA==", "requires": { - "end-of-stream": "1.0.0", + "end-of-stream": "1.4.1", "inherits": "2.0.3", "readable-stream": "2.3.3", "stream-shift": "1.0.0" @@ -840,23 +1073,11 @@ } }, "end-of-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", - "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=", - "dev": true, + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", "requires": { - "once": "1.3.3" - }, - "dependencies": { - "once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - } + "once": "1.4.0" } }, "error-ex": { @@ -867,11 +1088,6 @@ "is-arrayish": "0.2.1" } }, - "es6-promise": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", - "integrity": "sha1-7FYjOGgDKQkgcXDDlEjiREndH8Q=" - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -879,33 +1095,33 @@ "dev": true }, "eslint": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.5.0.tgz", - "integrity": "sha1-u3XTuL3pf7XhPvzVOXRGd/6wGcM=", + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.16.0.tgz", + "integrity": "sha512-YVXV4bDhNoHHcv0qzU4Meof7/P26B4EuaktMi5L1Tnt52Aov85KmYA8c5D+xyZr/BkhvwUqr011jDSD/QTULxg==", "dev": true, "requires": { - "ajv": "5.2.4", + "ajv": "5.5.2", "babel-code-frame": "6.26.0", - "chalk": "2.2.0", + "chalk": "2.3.0", "concat-stream": "1.6.0", "cross-spawn": "5.1.0", - "debug": "2.6.9", - "doctrine": "2.0.0", + "debug": "3.1.0", + "doctrine": "2.1.0", "eslint-scope": "3.7.1", - "espree": "3.5.1", + "eslint-visitor-keys": "1.0.0", + "espree": "3.5.3", "esquery": "1.0.0", - "estraverse": "4.2.0", "esutils": "2.0.2", "file-entry-cache": "2.0.0", "functional-red-black-tree": "1.0.1", "glob": "7.1.2", - "globals": "9.18.0", - "ignore": "3.3.6", + "globals": "11.3.0", + "ignore": "3.3.7", "imurmurhash": "0.1.4", "inquirer": "3.3.0", - "is-resolvable": "1.0.0", + "is-resolvable": "1.1.0", "js-yaml": "3.10.0", - "json-stable-stringify": "1.0.1", + "json-stable-stringify-without-jsonify": "1.0.1", "levn": "0.3.0", "lodash": "4.17.4", "minimatch": "3.0.4", @@ -913,75 +1129,31 @@ "natural-compare": "1.4.0", "optionator": "0.8.2", "path-is-inside": "1.0.2", - "pluralize": "4.0.0", + "pluralize": "7.0.0", "progress": "2.0.0", "require-uncached": "1.0.3", - "semver": "5.3.0", + "semver": "5.5.0", "strip-ansi": "4.0.0", "strip-json-comments": "2.0.1", "table": "4.0.2", "text-table": "0.2.0" }, "dependencies": { - "ajv": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.4.tgz", - "integrity": "sha1-Pa+ai2ciEpn9ro2C0RftjmyAJEs=", - "dev": true, - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "json-schema-traverse": "0.3.1", - "json-stable-stringify": "1.0.1" - } - }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-0BMM/2hG3ZaoPfR6F+h/oWpZtsh3b/s62TjSM6MGCJWEbJDN1acqCXvyhhZsDSVFklpebUoQ5O1kKC7lOzrn9g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", @@ -990,32 +1162,22 @@ "requires": { "ansi-regex": "3.0.0" } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } } } }, "eslint-config-hapi": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint-config-hapi/-/eslint-config-hapi-10.1.0.tgz", - "integrity": "sha512-tAUedyvZla1qKt6jhOx7mj5tYDVCwdSyImpEK7wk/A/atKUjg18aHUK6Q6qWWM6rq21I1F/A8JAhIpkk0SvFMQ==", - "dev": true + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-hapi/-/eslint-config-hapi-11.1.0.tgz", + "integrity": "sha512-fCw0uLgkZLQBqYu/lR5MA6cXB+D4c2EEtzrVmhHbGQq3iRCFSaUEOE/N4Sujd1Qh5jkaUc0/kuB/gdv2upt5aQ==" }, "eslint-plugin-hapi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-hapi/-/eslint-plugin-hapi-4.0.0.tgz", - "integrity": "sha1-RKouRfeTmlI5Kc2DK7mqEpqV6CM=", - "dev": true, + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-hapi/-/eslint-plugin-hapi-4.1.0.tgz", + "integrity": "sha512-z1yUoSWArx6pXaC0FoWRFpqjbHn8QWonJiTVhJmiC14jOAT7FZKdKWCkhM4jQrgrkEK9YEv3p2HuzSf5dtWmuQ==", "requires": { "hapi-capitalize-modules": "1.1.6", "hapi-for-you": "1.0.0", + "hapi-no-var": "1.0.1", "hapi-scope-start": "2.1.1", "no-arrowception": "1.0.0" } @@ -1038,22 +1200,20 @@ } } }, + "eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==", + "dev": true + }, "espree": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.1.tgz", - "integrity": "sha1-DJiLirRttTEAoZVK5LqZXd0n2H4=", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.3.tgz", + "integrity": "sha512-Zy3tAJDORxQZLl2baguiRU1syPERAIg0L+JB2MWorORgTu/CplzvxS9WWA7Xh4+Q+eOQihNs/1o1Xep8cvCxWQ==", "dev": true, "requires": { - "acorn": "5.1.2", + "acorn": "5.4.1", "acorn-jsx": "3.0.1" - }, - "dependencies": { - "acorn": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.2.tgz", - "integrity": "sha512-o96FZLJBPY1lvTuJylGA9Bk3t/GKPPJG8H0ydQQl01crzwJgspa4AEIq/pVTXigmK0PHVQhiAtn8WMBLL9D2WA==", - "dev": true - } } }, "esprima": { @@ -1122,59 +1282,159 @@ "through": "2.3.8" } }, - "expand-brackets": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", - "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", - "dev": true, - "requires": { - "is-posix-bracket": "0.1.1" - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "2.2.3" - } - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" - }, - "external-editor": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.0.5.tgz", - "integrity": "sha512-Msjo64WT5W+NhOpQXh0nOHm+n0RfU1QUwDnKYvJ8dEJ8zlwLrqXNTv5mSUTJpepf41PDJGyhueTw2vNZW+Fr/w==", + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, "requires": { - "iconv-lite": "0.4.19", - "jschardet": "1.5.1", - "tmp": "0.0.33" + "cross-spawn": "5.1.0", + "get-stream": "3.0.0", + "is-stream": "1.1.0", + "npm-run-path": "2.0.2", + "p-finally": "1.0.0", + "signal-exit": "3.0.2", + "strip-eof": "1.0.0" } }, - "extglob": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", - "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "is-extglob": "1.0.0" - } - }, - "extsprintf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", - "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" - }, - "fast-deep-equal": { - "version": "1.0.0", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "posix-character-classes": "0.1.1", + "regex-not": "1.0.0", + "snapdragon": "0.8.1", + "to-regex": "3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true, + "requires": { + "is-extendable": "0.1.1" + } + }, + "external-editor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.1.0.tgz", + "integrity": "sha512-E44iT5QVOUJBKij4IIV3uvxuNlbKS38Tw1HiupxEIHPv9qtC2PrDYohbXV5U+1jnfIXttny8gUhj+oZvflFlzA==", + "dev": true, + "requires": { + "chardet": "0.4.2", + "iconv-lite": "0.4.19", + "tmp": "0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "requires": { + "array-unique": "0.3.2", + "define-property": "1.0.0", + "expand-brackets": "2.1.4", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "regex-not": "1.0.0", + "snapdragon": "0.8.1", + "to-regex": "3.0.1" + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.0.0", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=", - "dev": true + "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -1182,6 +1442,11 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fast-safe-stringify": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-1.1.13.tgz", + "integrity": "sha1-oB6c2cnkkXFcmKdaQtXwu9EH/3Y=" + }, "figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", @@ -1201,33 +1466,16 @@ "object-assign": "4.1.1" } }, - "filename-regex": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", - "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", - "dev": true - }, - "fill-keys": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz", - "integrity": "sha1-mo+jb06K1jTjv2tPPIiCVRRS6yA=", - "dev": true, - "requires": { - "is-object": "1.0.1", - "merge-descriptors": "1.0.1" - } - }, "fill-range": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", - "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "is-number": "2.1.0", - "isobject": "2.1.0", - "randomatic": "1.1.7", - "repeat-element": "1.1.2", - "repeat-string": "1.6.1" + "extend-shallow": "2.0.1", + "is-number": "3.0.0", + "repeat-string": "1.6.1", + "to-regex-range": "2.1.1" } }, "find-rc": { @@ -1263,28 +1511,28 @@ "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", - "dev": true, - "requires": { - "for-in": "1.0.2" - } - }, "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", + "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.5", - "mime-types": "2.1.15" + "mime-types": "2.1.17" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "requires": { + "map-cache": "0.2.2" } }, "from": { @@ -1299,14 +1547,14 @@ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.2.tgz", - "integrity": "sha512-Sn44E5wQW4bTHXvQmvSHwqbuiXtduD6Rrjm2ZtUEGbyrig+nUH3t/QD4M4/ZXViY556TBpRgZkHLDx3JxPwxiw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.1.3.tgz", + "integrity": "sha512-WIr7iDkdmdbxu/Gh6eKEZJL6KPE74/5MEsf2whTOFNxbIoIixogroLdKYqB6FDav4Wavh/lZdzzd3b2KxIXC5Q==", "dev": true, "optional": true, "requires": { - "nan": "2.5.0", - "node-pre-gyp": "0.6.36" + "nan": "2.6.2", + "node-pre-gyp": "0.6.39" }, "dependencies": { "abbrev": { @@ -1464,7 +1712,6 @@ "version": "2.0.5", "bundled": true, "dev": true, - "optional": true, "requires": { "boom": "2.10.1" } @@ -1512,6 +1759,12 @@ "dev": true, "optional": true }, + "detect-libc": { + "version": "1.0.2", + "bundled": true, + "dev": true, + "optional": true + }, "ecc-jsbn": { "version": "0.1.1", "bundled": true, @@ -1653,7 +1906,6 @@ "version": "3.1.3", "bundled": true, "dev": true, - "optional": true, "requires": { "boom": "2.10.1", "cryptiles": "2.0.5", @@ -1825,11 +2077,13 @@ "optional": true }, "node-pre-gyp": { - "version": "0.6.36", + "version": "0.6.39", "bundled": true, "dev": true, "optional": true, "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", "mkdirp": "0.5.1", "nopt": "4.0.1", "npmlog": "4.1.0", @@ -2037,7 +2291,6 @@ "version": "1.0.9", "bundled": true, "dev": true, - "optional": true, "requires": { "hoek": "2.16.3" } @@ -2205,7 +2458,7 @@ "graceful-fs": "4.1.11", "inherits": "2.0.3", "mkdirp": "0.5.1", - "rimraf": "2.5.4" + "rimraf": "2.6.2" } }, "fstream-ignore": { @@ -2229,7 +2482,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", "requires": { - "aproba": "1.1.2", + "aproba": "1.2.0", "console-control-strings": "1.1.0", "has-unicode": "2.0.1", "object-assign": "4.1.1", @@ -2244,19 +2497,24 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=" }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true + }, "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", "requires": { "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } } }, "glob": { @@ -2272,29 +2530,40 @@ "path-is-absolute": "1.0.1" } }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "glob-parent": "2.0.0", - "is-glob": "2.0.1" + "is-glob": "3.1.0", + "path-dirname": "1.0.2" + }, + "dependencies": { + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + } } }, - "glob-parent": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", - "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=", "dev": true, "requires": { - "is-glob": "2.0.1" + "ini": "1.3.5" } }, "globals": { - "version": "9.18.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", - "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.3.0.tgz", + "integrity": "sha512-kkpcKNlmQan9Z5ZmgqKH/SMbSmjxQ7QjyNqfXVc8VJcoBV2UEg+sxQD15GQofGRh2hfpwUb70VC31DR7Rq5Hdw==", "dev": true }, "globby": { @@ -2312,40 +2581,95 @@ } }, "glue": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/glue/-/glue-4.1.0.tgz", - "integrity": "sha1-B6tkOMyliaMGsLvDY0e4IbaXQC8=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/glue/-/glue-5.0.0.tgz", + "integrity": "sha512-kpxc+NGRxvCYary84IGfI7aTxgJND+iwoohXQ6D4YvxAjN9gBjtxfiWmMWBw4IyVMtogotFdN2DfLCk8hVt2Fg==", + "requires": { + "hapi": "17.2.0", + "hoek": "5.0.2", + "joi": "13.1.2" + } + }, + "good": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/good/-/good-8.0.0.tgz", + "integrity": "sha1-Ty3mcd3aaGJsj4tGODvd8P20apA=", + "requires": { + "hoek": "5.0.2", + "joi": "13.1.2", + "oppsy": "2.0.0", + "pumpify": "1.3.6" + } + }, + "good-console": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/good-console/-/good-console-7.0.1.tgz", + "integrity": "sha1-VE8YTwLmhCqwfn7pAJaMwz3GpTM=", + "requires": { + "hoek": "4.2.0", + "joi": "12.0.0", + "json-stringify-safe": "5.0.1", + "moment": "2.20.1" + }, + "dependencies": { + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + }, + "joi": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-12.0.0.tgz", + "integrity": "sha512-z0FNlV4NGgjQN1fdtHYXf5kmgludM65fG/JlXzU6+rwkt9U5UWuXVYnXa2FpK0u6+qBuCmrm5byPNuiiddAHvQ==", + "requires": { + "hoek": "4.2.0", + "isemail": "3.1.1", + "topo": "2.0.2" + } + }, + "topo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", + "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", + "requires": { + "hoek": "4.2.0" + } + } + } + }, + "good-squeeze": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/good-squeeze/-/good-squeeze-5.0.2.tgz", + "integrity": "sha1-qOWCQrSgsyzb3zF7YOc6Gafwh5s=", "requires": { - "hapi": "16.4.3", - "hoek": "4.1.1", - "items": "2.1.1", - "joi": "10.6.0" + "fast-safe-stringify": "1.1.13", + "hoek": "4.2.0" + }, + "dependencies": { + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + } } }, "got": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-3.3.1.tgz", - "integrity": "sha1-5dDtSvVfw+701WAHdp2YGSvLLso=", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", "dev": true, "requires": { - "duplexify": "3.5.0", - "infinity-agent": "2.0.3", + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", "is-stream": "1.1.0", "lowercase-keys": "1.0.0", - "nested-error-stacks": "1.0.2", - "object-assign": "3.0.0", - "prepend-http": "1.0.4", - "read-all-stream": "3.1.0", - "timed-out": "2.0.0" - }, - "dependencies": { - "object-assign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", - "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", - "dev": true - } + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" } }, "graceful-fs": { @@ -2353,129 +2677,95 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=" - }, "handlebars": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.10.tgz", - "integrity": "sha1-PTDHGLCaPZbyPqTMH0A8TTup/08=", + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", "requires": { "async": "1.5.2", "optimist": "0.6.1", "source-map": "0.4.4", "uglify-js": "2.8.29" - }, - "dependencies": { - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - } } }, "hapi": { - "version": "16.4.3", - "resolved": "https://registry.npmjs.org/hapi/-/hapi-16.4.3.tgz", - "integrity": "sha512-1jN2dj3KeNAHWs5/rFCGPjYyyoDqG1orkwJyL1OHjbODnhsCbZ6AQOgroOUyNUCH3KCZ6dU/dimAW83gJiBjsA==", - "requires": { - "accept": "2.1.4", - "ammo": "2.0.4", - "boom": "5.1.0", - "call": "4.0.2", - "catbox": "7.1.4", - "catbox-memory": "2.0.4", - "cryptiles": "3.1.2", - "heavy": "4.0.4", - "hoek": "4.1.1", - "iron": "4.0.5", - "items": "2.1.1", - "joi": "10.6.0", - "mimos": "3.0.3", - "podium": "1.2.5", - "shot": "3.4.2", - "statehood": "5.0.2", - "subtext": "4.4.1", - "topo": "2.0.2" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", - "requires": { - "boom": "5.1.0" - } - } + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/hapi/-/hapi-17.2.0.tgz", + "integrity": "sha512-zw2tqNimjT+qglgUNGNpeweHJ5To1xUcJcfGKsG5dWiTzwkEZtmtHJ8mBIvxuuZ3Buu4xkAGj0yWrrR95FVqQQ==", + "requires": { + "accept": "3.0.2", + "ammo": "3.0.0", + "boom": "7.1.1", + "bounce": "1.2.0", + "call": "5.0.1", + "catbox": "10.0.2", + "catbox-memory": "3.1.1", + "heavy": "6.1.0", + "hoek": "5.0.2", + "joi": "13.1.2", + "mimos": "4.0.0", + "podium": "3.1.2", + "shot": "4.0.4", + "statehood": "6.0.5", + "subtext": "6.0.7", + "teamwork": "3.0.1", + "topo": "3.0.0" } }, "hapi-auth-basic": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hapi-auth-basic/-/hapi-auth-basic-4.2.0.tgz", - "integrity": "sha1-ovFrPhQxfUTjrXk0/oM/mV7oL8A=", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/hapi-auth-basic/-/hapi-auth-basic-5.0.0.tgz", + "integrity": "sha1-BDiwAiXk97rM1/KeBLT8UDfAErA=", "requires": { - "boom": "3.2.2", - "hoek": "4.1.1" - }, - "dependencies": { - "boom": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/boom/-/boom-3.2.2.tgz", - "integrity": "sha1-DwzF0ErcUAO4x9cfQsynJx/vDng=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1", + "hoek": "5.0.2" } }, "hapi-capitalize-modules": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/hapi-capitalize-modules/-/hapi-capitalize-modules-1.1.6.tgz", - "integrity": "sha1-eZEXFBXhXmqjIx5k3ac8gUZmUxg=", - "dev": true + "integrity": "sha1-eZEXFBXhXmqjIx5k3ac8gUZmUxg=" }, "hapi-for-you": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hapi-for-you/-/hapi-for-you-1.0.0.tgz", - "integrity": "sha1-02L77o172pwseAHiB+WlzRoLans=", - "dev": true + "integrity": "sha1-02L77o172pwseAHiB+WlzRoLans=" }, "hapi-mongo-models": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/hapi-mongo-models/-/hapi-mongo-models-6.0.0.tgz", - "integrity": "sha1-VtXMAR2TnKtyms7TDJzHtx8mVlY=", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/hapi-mongo-models/-/hapi-mongo-models-7.1.0.tgz", + "integrity": "sha512-T1bZHZIryLWj9CFUvOGJfgFq4k8fxVYSmmAs8dUEFPyZwao3vk/bLu2OaPLT1qNJj4bww05+yJy4F+HSpXmImg==", "requires": { - "hoek": "4.1.1" + "hoek": "5.0.2" } }, + "hapi-no-var": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hapi-no-var/-/hapi-no-var-1.0.1.tgz", + "integrity": "sha512-kk2xyyTzI+eQ/oA1rO4eVdCpYsrPHVERHa6+mTHD08XXFLaAkkaEs6reMg1VyqGh2o5xPt//DO4EhCacLx/cRA==" + }, + "hapi-remote-address": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/hapi-remote-address/-/hapi-remote-address-1.0.0.tgz", + "integrity": "sha512-Sj5Bjh+SKCCCr89B9CJlNOXJCFga4LcAyoDrtqMSzAXqbtd3EHmdSsRATasGhyLDpCBo1RgunEwas3DOXGGRvA==" + }, "hapi-scope-start": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/hapi-scope-start/-/hapi-scope-start-2.1.1.tgz", - "integrity": "sha1-dJWnJv5yt7yo3izcwdh82M5qtPI=", - "dev": true + "integrity": "sha1-dJWnJv5yt7yo3izcwdh82M5qtPI=" }, "har-schema": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", - "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" }, "har-validator": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", - "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", + "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "4.11.8", - "har-schema": "1.0.5" + "ajv": "5.5.2", + "har-schema": "2.0.0" } }, "has-ansi": { @@ -2498,56 +2788,78 @@ "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "requires": { - "boom": "2.10.1", - "cryptiles": "2.0.5", - "hoek": "2.16.3", - "sntp": "1.0.9" + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "1.0.0", + "isobject": "3.0.1" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" }, "dependencies": { - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true, "requires": { - "hoek": "2.16.3" + "is-buffer": "1.1.6" } - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" } } }, - "heavy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/heavy/-/heavy-4.0.4.tgz", - "integrity": "sha1-NskTNsAMz+hSyqTRUwhjNc0vAOk=", + "hawk": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-6.0.2.tgz", + "integrity": "sha512-miowhl2+U7Qle4vdLqDdPt9m09K6yZhkLDTWGoUiUzrQCn+mHHSmfJgAyGaLRZbPmTqfFFjRV1QWCW0VWUJBbQ==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1", - "joi": "10.6.0" + "boom": "4.3.1", + "cryptiles": "3.1.2", + "hoek": "4.2.0", + "sntp": "2.1.0" }, "dependencies": { "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", + "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "requires": { - "hoek": "4.1.1" + "hoek": "4.2.0" } + }, + "hoek": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" } } }, + "heavy": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/heavy/-/heavy-6.1.0.tgz", + "integrity": "sha512-TKS9DC9NOTGulHQI31Lx+bmeWmNOstbJbGMiN3pX6bF+Zc2GKSpbbym4oasNnB6yPGkqJ9TQXXYDGohqNSJRxA==", + "requires": { + "boom": "7.1.1", + "hoek": "5.0.2", + "joi": "13.1.2" + } + }, "hoek": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.1.1.tgz", - "integrity": "sha1-nMVz/7ore0CPtenCoTeWvpTN3Ok=" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.2.tgz", + "integrity": "sha512-NA10UYP9ufCtY2qYGkZktcQXwVyYK4zK0gkaFSB96xhtlo6V8tKXdQgx8eHolQTRemaW0uLn8BhjhwqrOU+QLQ==" }, "hosted-git-info": { "version": "2.5.0", @@ -2555,12 +2867,12 @@ "integrity": "sha512-pNgbURSuab90KbTqvRPsseaTxOJCZBD0a7t+haSN33piP9cCM4l0CqdzAif2hUqm716UovKB2ROmiabGAKVXyg==" }, "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", "requires": { - "assert-plus": "0.2.0", - "jsprim": "1.4.0", + "assert-plus": "1.0.0", + "jsprim": "1.4.1", "sshpk": "1.13.1" } }, @@ -2571,9 +2883,9 @@ "dev": true }, "ignore": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.6.tgz", - "integrity": "sha512-HrxmNxKTGZ9a3uAl/FNG66Sdt0G9L4TtMbbUQjP1WhGmSj0FOyHvSgx7623aGJvXfPOur8MwmarlHT+37jmzlw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.7.tgz", + "integrity": "sha512-YGG3ejvBNHRqu0559EOxxNFihD0AjpvHlC/pdGKd3X3ofe+CoJkYazwNJYTNebqpPKN+VVQbh4ZFn1DivMNuHA==", "dev": true }, "ignore-by-default": { @@ -2582,41 +2894,18 @@ "integrity": "sha1-SMptcvbGo68Aqa1K5odr44ieKwk=", "dev": true }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=", + "dev": true + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, - "inert": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/inert/-/inert-4.2.0.tgz", - "integrity": "sha1-aqXajOGZgutyqr70vdpk/WdQsAU=", - "requires": { - "ammo": "2.0.4", - "boom": "4.3.1", - "hoek": "4.1.1", - "items": "2.1.1", - "joi": "10.6.0", - "lru-cache": "4.0.2" - }, - "dependencies": { - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.1.1" - } - } - } - }, - "infinity-agent": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/infinity-agent/-/infinity-agent-2.0.3.tgz", - "integrity": "sha1-ReDi/3qesDCyfWK3SzdEt6esQhY=", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2632,9 +2921,9 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", - "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=" + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "version": "3.3.0", @@ -2643,10 +2932,10 @@ "dev": true, "requires": { "ansi-escapes": "3.0.0", - "chalk": "2.2.0", + "chalk": "2.3.0", "cli-cursor": "2.1.0", "cli-width": "2.2.0", - "external-editor": "2.0.5", + "external-editor": "2.1.0", "figures": "2.0.0", "lodash": "4.17.4", "mute-stream": "0.0.7", @@ -2664,38 +2953,12 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-0BMM/2hG3ZaoPfR6F+h/oWpZtsh3b/s62TjSM6MGCJWEbJDN1acqCXvyhhZsDSVFklpebUoQ5O1kKC7lOzrn9g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -2714,15 +2977,6 @@ "requires": { "ansi-regex": "3.0.0" } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } } } }, @@ -2732,33 +2986,42 @@ "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" }, "iron": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/iron/-/iron-4.0.5.tgz", - "integrity": "sha1-TwQszri5c480a1mqc0yDqJvDFCg=", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/iron/-/iron-5.0.4.tgz", + "integrity": "sha512-7iQ5/xFMIYaNt9g2oiNiWdhrOTdRUMFaWENUd0KghxwPUhrIH8DUY8FEyLNTTzf75jaII+jMexLdY/2HfV61RQ==", "requires": { - "boom": "5.1.0", - "cryptiles": "3.1.2", - "hoek": "4.1.1" + "boom": "7.1.1", + "cryptiles": "4.1.1", + "hoek": "5.0.2" }, "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - }, "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.1.tgz", + "integrity": "sha512-YuQUPbcOmaZsdvxJZ25DCA1W+lLIRoPJKBDKin+St1RCYEERSfoe1d25B1MvWNHN3e8SpFSVsqYvEUjp8J9H2w==", "requires": { - "boom": "5.1.0" + "boom": "7.1.1" } } } }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2770,13 +3033,13 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "requires": { - "binary-extensions": "1.8.0" + "binary-extensions": "1.11.0" } }, "is-buffer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", - "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=" + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-builtin-module": { "version": "1.0.0", @@ -2786,19 +3049,40 @@ "builtin-modules": "1.1.1" } }, - "is-dotfile": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", - "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", - "dev": true + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "dev": true, + "requires": { + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } }, - "is-equal-shallow": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", - "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-primitive": "2.0.0" + "is-accessor-descriptor": "1.0.0", + "is-data-descriptor": "1.0.0", + "kind-of": "6.0.2" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } } }, "is-extendable": { @@ -2808,20 +3092,11 @@ "dev": true }, "is-extglob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -2831,12 +3106,22 @@ } }, "is-glob": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", - "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "dev": true, + "requires": { + "is-extglob": "2.1.1" + } + }, + "is-installed-globally": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz", + "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=", "dev": true, "requires": { - "is-extglob": "1.0.0" + "global-dirs": "0.1.1", + "is-path-inside": "1.0.1" } }, "is-npm": { @@ -2846,20 +3131,29 @@ "dev": true }, "is-number": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", - "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { "kind-of": "3.2.2" } }, - "is-object": { + "is-obj": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true }, + "is-odd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-1.0.0.tgz", + "integrity": "sha1-O4qTLrAos3dcObsJ6RdnrM22kIg=", + "dev": true, + "requires": { + "is-number": "3.0.0" + } + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -2872,34 +3166,32 @@ "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", "dev": true, "requires": { - "is-path-inside": "1.0.0" + "is-path-inside": "1.0.1" } }, "is-path-inside": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", - "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", + "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", "dev": true, "requires": { "path-is-inside": "1.0.2" } }, - "is-posix-bracket": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", - "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", - "dev": true - }, - "is-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "3.0.1" + } }, "is-promise": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true }, "is-redirect": { "version": "1.0.0", @@ -2908,13 +3200,16 @@ "dev": true }, "is-resolvable": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", - "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", - "dev": true, - "requires": { - "tryit": "1.0.3" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "dev": true + }, + "is-retry-allowed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", + "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", + "dev": true }, "is-stream": { "version": "1.1.0", @@ -2938,9 +3233,19 @@ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isemail": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isemail/-/isemail-2.2.1.tgz", - "integrity": "sha1-A1PT2aYpUQgMJiwqoKQrjqjp4qY=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.1.tgz", + "integrity": "sha512-mVjAjvdPkpwXW61agT2E9AkGoegZO7SdJGCezWwxnETL58f5KwJ4vSVAMBUL5idL6rTlYAIGkX3n4suiviMLNw==", + "requires": { + "punycode": "2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.0.tgz", + "integrity": "sha1-X4Y+3Im5bbCQdLrXlHvwkFbKTn0=" + } + } }, "isexe": { "version": "2.0.0", @@ -2949,52 +3254,31 @@ "dev": true }, "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, - "items": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/items/-/items-2.1.1.tgz", - "integrity": "sha1-i9FtnIOxlSneWuoyGsqtp4NkoZg=" - }, - "jade": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/jade/-/jade-1.11.0.tgz", - "integrity": "sha1-nIDlOMEtP7lcjZu5VZ+gzAQEBf0=", - "requires": { - "character-parser": "1.2.1", - "clean-css": "3.4.27", - "commander": "2.6.0", - "constantinople": "3.0.2", - "jstransformer": "0.0.2", - "mkdirp": "0.5.1", - "transformers": "2.1.0", - "uglify-js": "2.8.29", - "void-elements": "2.0.1", - "with": "4.0.3" - } - }, "joi": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-10.6.0.tgz", - "integrity": "sha512-hBF3LcqyAid+9X/pwg+eXjD2QBZI5eXnBFJYaAkH4SK3mp9QSRiiQnDYlmlz5pccMvnLcJRS4whhDOTCkmsAdQ==", + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.1.2.tgz", + "integrity": "sha512-bZZSQYW5lPXenOfENvgCBPb9+H6E6MeNWcMtikI04fKphj5tvFL9TOb+H2apJzbCrRw/jebjTH8z6IHLpBytGg==", "requires": { - "hoek": "4.1.1", - "isemail": "2.2.1", - "items": "2.1.1", - "topo": "2.0.2" + "hoek": "5.0.2", + "isemail": "3.1.1", + "topo": "3.0.0" } }, + "joistick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/joistick/-/joistick-1.0.1.tgz", + "integrity": "sha512-D3fpYsSM7xL9PEecnlOA2JFO5ZpyDSpEq9WFxTipjmIa7ZRxmXG9rVNSn5Olh+pZ0vKAP0T9a/64b1I1BWFcEQ==" + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -3025,12 +3309,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "optional": true }, - "jschardet": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/jschardet/-/jschardet-1.5.1.tgz", - "integrity": "sha512-vE2hT1D0HLZCLLclfBSfkfTTedhVj0fubHpJBHKwwUWX0nSbhPAfk+SG9rTX95BYNmau8rGFfCeaT6T5OW1C2A==", - "dev": true - }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -3039,17 +3317,23 @@ "json-schema-traverse": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", - "dev": true + "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=" }, "json-stable-stringify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, "requires": { "jsonify": "0.0.0" } }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3058,33 +3342,18 @@ "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true }, "jsprim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "requires": { "assert-plus": "1.0.0", - "extsprintf": "1.0.2", + "extsprintf": "1.3.0", "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - } - } - }, - "jstransformer": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-0.0.2.tgz", - "integrity": "sha1-eq4pqQPRls+glz2IXT5HlH7Ndqs=", - "requires": { - "is-promise": "2.1.0", - "promise": "6.1.0" + "verror": "1.10.0" } }, "kind-of": { @@ -3092,65 +3361,56 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "requires": { - "is-buffer": "1.1.5" + "is-buffer": "1.1.6" } }, "lab": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/lab/-/lab-14.3.1.tgz", - "integrity": "sha512-sN14QhqiRuBGnfrOGo1B9UqiJJarEcZJEna/hGAT5g4uuxcowns4zc/JUBoMgT44jZgWBxbpjKPizBOGt5mYRw==", - "dev": true, - "requires": { - "bossy": "3.0.4", - "code": "4.1.0", - "diff": "3.3.1", - "eslint": "4.5.0", - "eslint-config-hapi": "10.1.0", - "eslint-plugin-hapi": "4.0.0", - "espree": "3.5.1", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/lab/-/lab-15.2.1.tgz", + "integrity": "sha512-RzswRTnRLt2NICBvJX0xOaTS/pkVNQlbdcgcCoyxBbf9jv78H8PwU9OChzn2gFvVlQ46IZirFtnZdbD7238rQg==", + "dev": true, + "requires": { + "bossy": "4.0.1", + "diff": "3.4.0", + "eslint": "4.16.0", + "eslint-config-hapi": "11.1.0", + "eslint-plugin-hapi": "4.1.0", + "espree": "3.5.3", "find-rc": "3.0.1", - "handlebars": "4.0.10", - "hoek": "4.1.1", - "items": "2.1.1", + "handlebars": "4.0.11", + "hoek": "5.0.2", "json-stable-stringify": "1.0.1", "json-stringify-safe": "5.0.1", "mkdirp": "0.5.1", "seedrandom": "2.4.3", - "source-map": "0.5.7", - "source-map-support": "0.4.18", - "supports-color": "4.4.0" + "source-map": "0.6.1", + "source-map-support": "0.5.3", + "supports-color": "4.4.0", + "will-call": "1.0.1" }, "dependencies": { "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true - }, - "supports-color": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", - "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } } } }, "latest-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-1.0.1.tgz", - "integrity": "sha1-cs/Ebj6NG+ZR4eu1Tqn26pbzdLs=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz", + "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=", "dev": true, "requires": { - "package-json": "1.2.0" + "package-json": "4.0.1" } }, "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "optional": true }, "lcid": { "version": "1.0.0", @@ -3185,51 +3445,7 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "3.0.1", - "lodash.keys": "3.1.2" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._bindcallback": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz", - "integrity": "sha1-5THCdkTPi1epnhftlbNcdIeJOS4=", - "dev": true - }, - "lodash._createassigner": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash._createassigner/-/lodash._createassigner-3.1.1.tgz", - "integrity": "sha1-g4pbri/aymOsIt7o4Z+k5taXCxE=", - "dev": true, - "requires": { - "lodash._bindcallback": "3.0.1", - "lodash._isiterateecall": "3.0.9", - "lodash.restparam": "3.6.1" - } - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", "dev": true }, "lodash.assign": { @@ -3237,83 +3453,11 @@ "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" }, - "lodash.defaults": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-3.1.2.tgz", - "integrity": "sha1-xzCLGNv4vJNy1wGnNJPGEZK9Liw=", - "dev": true, - "requires": { - "lodash.assign": "3.2.0", - "lodash.restparam": "3.6.1" - }, - "dependencies": { - "lodash.assign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-3.2.0.tgz", - "integrity": "sha1-POnwI0tLIiPilrj6CsH+6OvKZPo=", - "dev": true, - "requires": { - "lodash._baseassign": "3.2.0", - "lodash._createassigner": "3.1.1", - "lodash.keys": "3.1.2" - } - } - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "3.9.1", - "lodash.isarguments": "3.1.0", - "lodash.isarray": "3.0.4" - } - }, - "lodash.restparam": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", - "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", - "dev": true - }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" }, - "lout": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/lout/-/lout-10.0.1.tgz", - "integrity": "sha1-f4DlgMxot3Ojpdu7TlfSonzyJTI=", - "requires": { - "boom": "4.3.1", - "handlebars": "4.0.10", - "hoek": "4.1.1" - }, - "dependencies": { - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.1.1" - } - } - } - }, "lowercase-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", @@ -3321,78 +3465,112 @@ "dev": true }, "lru-cache": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", - "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", + "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", "requires": { "pseudomap": "1.0.2", "yallist": "2.1.2" } }, + "make-dir": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.1.0.tgz", + "integrity": "sha512-0Pkui4wLJ7rxvmfUvs87skoEaxmu0hCUApF8nonzpl7q//FWp9zu8W61Scz4sd/kUiqDxvUhtoam2efDyiBzcA==", + "dev": true, + "requires": { + "pify": "3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true + }, "map-stream": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "requires": { + "object-visit": "1.0.1" + } + }, "marked": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.6.tgz", - "integrity": "sha1-ssbGGPzOzk74bE/Gy4p8v1rtqNc=" - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.12.tgz", + "integrity": "sha512-k4NaW+vS7ytQn6MgJn3fYpQt20/mOgYM5Ft9BYMfQJDz2QT6yEeS9XJ8k2Nw8JTeWK/znPPW2n3UJGzyYEiMoA==" }, "micromatch": { - "version": "2.3.11", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", - "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", - "dev": true, - "requires": { - "arr-diff": "2.0.0", - "array-unique": "0.2.1", - "braces": "1.8.5", - "expand-brackets": "0.1.5", - "extglob": "0.3.2", - "filename-regex": "2.0.1", - "is-extglob": "1.0.0", - "is-glob": "2.0.1", - "kind-of": "3.2.2", - "normalize-path": "2.1.1", - "object.omit": "2.0.1", - "parse-glob": "3.0.4", - "regex-cache": "0.4.3" + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.5.tgz", + "integrity": "sha512-ykttrLPQrz1PUJcXjwsTUjGoPJ64StIGNE2lGVD1c9CuguJ+L7/navsE8IcDNndOoCMvYV0qc/exfVbMHkUhvA==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "braces": "2.3.0", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "extglob": "2.0.4", + "fragment-cache": "0.2.1", + "kind-of": "6.0.2", + "nanomatch": "1.2.7", + "object.pick": "1.3.0", + "regex-not": "1.0.0", + "snapdragon": "0.8.1", + "to-regex": "3.0.1" + }, + "dependencies": { + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } } }, "mime-db": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", + "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" }, "mime-types": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "version": "2.1.17", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", + "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", "requires": { - "mime-db": "1.27.0" + "mime-db": "1.30.0" } }, "mimic-fn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.1.0.tgz", - "integrity": "sha1-5md4PZLonb00KBi1IwudYqZyrRg=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, "mimos": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/mimos/-/mimos-3.0.3.tgz", - "integrity": "sha1-uRCQcq03jCty9qAQHEPd+ys2ZB8=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimos/-/mimos-4.0.0.tgz", + "integrity": "sha512-JvlvRLqGIlk+AYypWrbrDmhsM+6JVx/xBM5S3AMwTBz1trPCEoPN/swO2L4Wu653fL7oJdgk8DMQyG/Gq3JkZg==", "requires": { - "hoek": "4.1.1", - "mime-db": "1.27.0" + "hoek": "5.0.2", + "mime-db": "1.30.0" } }, "minimatch": { @@ -3408,6 +3586,27 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, + "mixin-deep": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.0.tgz", + "integrity": "sha512-dgaCvoh6i1nosAUBKb0l0pfJ78K8+S9fluyIR2YvAeUD/QuMahnFnF3xYty5eYXMjhGSsB0DsW6A0uAZyetoAg==", + "dev": true, + "requires": { + "for-in": "1.0.2", + "is-extendable": "1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", @@ -3416,72 +3615,79 @@ "minimist": "0.0.8" } }, - "module-not-found-error": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz", - "integrity": "sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA=", - "dev": true + "moment": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", + "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" }, "mongo-models": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mongo-models/-/mongo-models-1.3.1.tgz", - "integrity": "sha1-PT3o7TnQu6I03NDiH3p7Ke/jbwA=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mongo-models/-/mongo-models-2.0.3.tgz", + "integrity": "sha512-z+tiYu5M4lHYFhbfhpYRzxU5rddYRI2xMsQt/57LPHqZOiaUJUYI26M6/pU3XKtIJAuQO3ksr9GDVQJpROBJyg==", "requires": { - "async": "2.5.0", - "hoek": "4.1.1", - "joi": "10.6.0" + "hoek": "5.0.2", + "joi": "13.1.2" } }, "mongodb": { - "version": "2.2.30", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-2.2.30.tgz", - "integrity": "sha1-jM2AH2dsgXIEDC8rR+lgKg1WNKs=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.0.2.tgz", + "integrity": "sha512-E50FmpSQchZAimn2uPIegoNoH9UQYR1yiGHtQPmmg8/Ekc97w6owHoqaBoz+assnd9V5LxMzmQ/VEWMsQMgZhQ==", "requires": { - "es6-promise": "3.2.1", - "mongodb-core": "2.1.14", - "readable-stream": "2.2.7" - }, - "dependencies": { - "readable-stream": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.2.7.tgz", - "integrity": "sha1-BwV6y+JGeyIELTb5jFrVBwVOlbE=", - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.3", - "util-deprecate": "1.0.2" - } - } + "mongodb-core": "3.0.2" } }, "mongodb-core": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-2.1.14.tgz", - "integrity": "sha1-E8uidkImtb49GJkq8Mljzl6g8P0=", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.0.2.tgz", + "integrity": "sha512-p1B0qwFQUw6C1OlFJnrOJp8KaX7MuGoogRbTaupRt0y+pPRkMllHWtE9V6i1CDtTvI3/3sy2sQwqWez7zuXEAA==", "requires": { "bson": "1.0.4", "require_optional": "1.0.1" } }, "ms": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", - "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mute-stream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", - "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, "nan": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.5.0.tgz", - "integrity": "sha1-qo8eNFMdgH6eJ3VbI0tKbsDBUqg=" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.6.2.tgz", + "integrity": "sha1-5P805slf37WuzAjeZZb0NgWn20U=" + }, + "nanomatch": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.7.tgz", + "integrity": "sha512-/5ldsnyurvEw7wNpxLFgjVvBLMta43niEYOy0CJ4ntcYSbx6bugRUTQeFb4BR/WanEL1o3aQgHuVLHQaB6tOqg==", + "dev": true, + "requires": { + "arr-diff": "4.0.0", + "array-unique": "0.3.2", + "define-property": "1.0.0", + "extend-shallow": "2.0.1", + "fragment-cache": "0.2.1", + "is-odd": "1.0.0", + "kind-of": "5.1.0", + "object.pick": "1.3.0", + "regex-not": "1.0.0", + "snapdragon": "0.8.1", + "to-regex": "3.0.1" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } }, "natural-compare": { "version": "1.4.0", @@ -3489,83 +3695,84 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, - "nested-error-stacks": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz", - "integrity": "sha1-GfYZWRUZ8JZ2mlupqG5u7sgjw88=", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, "nigel": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/nigel/-/nigel-2.0.2.tgz", - "integrity": "sha1-k6GGb7DFLYc5CqdeKxYfS1x15bE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/nigel/-/nigel-3.0.0.tgz", + "integrity": "sha512-ufFVFCe1zS/pfIQzQNa5uJxB8v8IcVTUn1zyPvQwb4CQGRxxBfdQPSXpEnI6ZzIwbV5L+GuAoRhYgcVSvTO7fA==", "requires": { - "hoek": "4.1.1", - "vise": "2.0.2" + "hoek": "5.0.2", + "vise": "3.0.0" } }, "no-arrowception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/no-arrowception/-/no-arrowception-1.0.0.tgz", - "integrity": "sha1-W/PpXrnEG1c4SoBTM9qjtzTuMno=", - "dev": true + "integrity": "sha1-W/PpXrnEG1c4SoBTM9qjtzTuMno=" }, "node-pre-gyp": { - "version": "0.6.32", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.32.tgz", - "integrity": "sha1-/EUrN25zGbPSVfXzSFPvb9j+H9U=", + "version": "0.6.36", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz", + "integrity": "sha1-22BBEst04NR3VU6bUFsXq936t4Y=", "requires": { "mkdirp": "0.5.1", - "nopt": "3.0.6", + "nopt": "4.0.1", "npmlog": "4.1.2", - "rc": "1.1.7", - "request": "2.81.0", - "rimraf": "2.5.4", - "semver": "5.3.0", + "rc": "1.2.5", + "request": "2.83.0", + "rimraf": "2.6.2", + "semver": "5.5.0", "tar": "2.2.1", - "tar-pack": "3.3.0" + "tar-pack": "3.4.1" } }, "nodemailer": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.3.0.tgz", - "integrity": "sha512-+/fZUhto12S2xsRGTUQuzCVjV94f4knH+gbbVc3+zxrSWZUIKKBok+JEIcPPQB8ePD0eej2ZU/pT7N+t0DM2vA==" + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.4.2.tgz", + "integrity": "sha512-sstDxbCSPHDXrWRQhH++khr3yVU0CGvH2dCtAHOPQQ0lRR7xwq2txItEXckMpX481B/cN+0akER2bfExh7Gu/Q==" }, "nodemailer-markdown": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nodemailer-markdown/-/nodemailer-markdown-1.0.1.tgz", - "integrity": "sha1-s7m0IMPOWov78J4EzZoISdaD3HM=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nodemailer-markdown/-/nodemailer-markdown-1.0.2.tgz", + "integrity": "sha512-wptrT4ijaqn5Qwl5ib6Fpzfg7g4Hu/PXqX027GOziXwJTFiSHvlfQ6/xXivhH6sU8TM/ULtVd/L3Yt/oIQSPWg==", "requires": { - "marked": "0.3.6" + "marked": "0.3.12" } }, "nodemon": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.11.0.tgz", - "integrity": "sha1-ImxWK9KnsT09dRi0mtSCijYj0Gw=", + "version": "1.14.12", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-1.14.12.tgz", + "integrity": "sha512-FssRGtEmt+EwpztWwTiYrLo+jSpgoxuJbmtgbRdTo2436x/3Z4PFODUz8yD52BiWqbHVJtasKv5K2ozFwNaqxA==", "dev": true, "requires": { - "chokidar": "1.7.0", - "debug": "2.2.0", - "es6-promise": "3.2.1", + "chokidar": "2.0.0", + "debug": "3.1.0", "ignore-by-default": "1.0.1", - "lodash.defaults": "3.1.2", "minimatch": "3.0.4", - "ps-tree": "1.1.0", - "touch": "1.0.0", - "undefsafe": "0.0.3", - "update-notifier": "0.5.0" + "pstree.remy": "1.1.0", + "semver": "5.5.0", + "touch": "3.1.0", + "undefsafe": "2.0.1", + "update-notifier": "2.3.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + } } }, "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", "requires": { - "abbrev": "1.1.0" + "abbrev": "1.1.1", + "osenv": "0.1.4" } }, "normalize-package-data": { @@ -3575,7 +3782,7 @@ "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", - "semver": "5.3.0", + "semver": "5.5.0", "validate-npm-package-license": "3.0.1" } }, @@ -3585,7 +3792,16 @@ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { - "remove-trailing-separator": "1.0.2" + "remove-trailing-separator": "1.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "dev": true, + "requires": { + "path-key": "2.0.1" } }, "npmlog": { @@ -3614,14 +3830,81 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "requires": { - "for-own": "0.1.5", - "is-extendable": "0.1.1" + "copy-descriptor": "0.1.1", + "define-property": "0.2.5", + "kind-of": "3.2.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "requires": { + "isobject": "3.0.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "requires": { + "isobject": "3.0.1" } }, "once": { @@ -3638,7 +3921,15 @@ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", "dev": true, "requires": { - "mimic-fn": "1.1.0" + "mimic-fn": "1.2.0" + } + }, + "oppsy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/oppsy/-/oppsy-2.0.0.tgz", + "integrity": "sha1-OhlFF63CTDxhzcVvNfRTfpOjXjQ=", + "requires": { + "hoek": "5.0.2" } }, "optimist": { @@ -3675,8 +3966,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "1.4.0", @@ -3695,32 +3985,27 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", - "dev": true, "requires": { "os-homedir": "1.0.2", "os-tmpdir": "1.0.2" } }, - "package-json": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-1.2.0.tgz", - "integrity": "sha1-yOysCUInzfdqMWh07QXifMk5oOA=", - "dev": true, - "requires": { - "got": "3.3.1", - "registry-url": "3.1.0" - } + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "package-json": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz", + "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=", "dev": true, "requires": { - "glob-base": "0.3.0", - "is-dotfile": "1.0.3", - "is-extglob": "1.0.0", - "is-glob": "2.0.1" + "got": "6.7.1", + "registry-auth-token": "3.3.2", + "registry-url": "3.1.0", + "semver": "5.5.0" } }, "parse-json": { @@ -3731,6 +4016,18 @@ "error-ex": "1.3.1" } }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", + "dev": true + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", @@ -3750,6 +4047,12 @@ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", "dev": true }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", + "dev": true + }, "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", @@ -3770,30 +4073,20 @@ } }, "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "pez": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/pez/-/pez-2.1.5.tgz", - "integrity": "sha1-XsLMYlAMw+tCNtSkFM9aF7XrUAc=", - "requires": { - "b64": "3.0.2", - "boom": "5.1.0", - "content": "3.0.4", - "hoek": "4.1.1", - "nigel": "2.0.2" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pez/-/pez-4.0.1.tgz", + "integrity": "sha512-0c/SoW5MY7lPdc5U1Q/ixyjLZbluGWJonHVmn4mKwSq7vgO9+a9WzoCopHubIwkot6Q+fevNVElaA+1M9SqHrA==", + "requires": { + "b64": "4.0.0", + "boom": "7.1.1", + "content": "4.0.3", + "hoek": "5.0.2", + "nigel": "3.0.0" } }, "pify": { @@ -3815,21 +4108,26 @@ } }, "pluralize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-4.0.0.tgz", - "integrity": "sha1-WbcIwcAZCi9pLxx2GMRGsFL9F2I=", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz", + "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==", "dev": true }, "podium": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/podium/-/podium-1.2.5.tgz", - "integrity": "sha1-h8VmwvA2W88KHsdgLE0BlIzdKtU=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/podium/-/podium-3.1.2.tgz", + "integrity": "sha512-18VrjJAduIdPv7d9zWsfmKxTj3cQTYC5Pv5gtKxcWujYBpGbV+mhNSPYhlHW5xeWoazYyKfB9FEsPT12r5rY1A==", "requires": { - "hoek": "4.1.1", - "items": "2.1.1", - "joi": "10.6.0" + "hoek": "5.0.2", + "joi": "13.1.2" } }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true + }, "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -3842,12 +4140,6 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", "dev": true }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", @@ -3859,38 +4151,20 @@ "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", "dev": true }, - "promise": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-6.1.0.tgz", - "integrity": "sha1-LOcp9rlLRcJoka0GAsXJDgTG7vY=", - "requires": { - "asap": "1.0.0" - } - }, "promptly": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/promptly/-/promptly-2.2.0.tgz", - "integrity": "sha1-KhP6BjaIoqWYOxYf/wEIoH0m/HQ=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/promptly/-/promptly-3.0.3.tgz", + "integrity": "sha512-EWnzOsxVKUjqKeE6SStH1/cO4+DE44QolaoJ4ojGd9z6pcNkpgfJKr1ncwxrOFHSTIzoudo7jG8y0re30/LO1g==", "dev": true, "requires": { + "pify": "3.0.0", "read": "1.0.7" - } - }, - "proxyquire": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-1.8.0.tgz", - "integrity": "sha1-AtUUpb7ZhvBMuyCTrxZ0FTX3ntw=", - "dev": true, - "requires": { - "fill-keys": "1.0.2", - "module-not-found-error": "1.0.1", - "resolve": "1.1.7" }, "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -3909,64 +4183,51 @@ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" }, + "pstree.remy": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", + "integrity": "sha512-q5I5vLRMVtdWa8n/3UEzZX7Lfghzrg9eG2IKk2ENLSofKRCXVqMvMUHxCKgXNaqH/8ebhBxrqftHWnyTFweJ5Q==", + "dev": true, + "requires": { + "ps-tree": "1.1.0" + } + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "1.4.1", + "once": "1.4.0" + } + }, + "pumpify": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.3.6.tgz", + "integrity": "sha512-BurGAcvezsINL5US9T9wGHHcLNrG6MCp//ECtxron3vcR+Rfx5Anqq7HbZXNJvFQli8FGVsWCAvywEJFV5Hx/Q==", + "requires": { + "duplexify": "3.5.3", + "inherits": "2.0.3", + "pump": "2.0.1" + } + }, "punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" - }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "3.2.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "1.1.5" - } - } - } + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz", + "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A==" }, "rc": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.1.7.tgz", - "integrity": "sha1-xepWS7B6/5/TpbMukGwdOmWUD+o=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.5.tgz", + "integrity": "sha1-J1zWh/bjs2zHVrqibf7oCnkDAf0=", "requires": { "deep-extend": "0.4.2", - "ini": "1.3.4", + "ini": "1.3.5", "minimist": "1.2.0", "strip-json-comments": "2.0.1" }, @@ -3984,17 +4245,7 @@ "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", "dev": true, "requires": { - "mute-stream": "0.0.5" - } - }, - "read-all-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", - "integrity": "sha1-NcPhd/IHjveJ7kv6+kNzB06u9Po=", - "dev": true, - "requires": { - "pinkie-promise": "2.0.1", - "readable-stream": "2.3.3" + "mute-stream": "0.0.7" } }, "read-pkg": { @@ -4042,14 +4293,23 @@ "set-immediate-shim": "1.0.1" } }, - "regex-cache": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", - "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "regex-not": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.0.tgz", + "integrity": "sha1-Qvg+OXcWIt+CawKvF2Ul1qXxV/k=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1" + } + }, + "registry-auth-token": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz", + "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==", "dev": true, "requires": { - "is-equal-shallow": "0.1.3", - "is-primitive": "2.0.0" + "rc": "1.2.5", + "safe-buffer": "5.1.1" } }, "registry-url": { @@ -4058,13 +4318,13 @@ "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=", "dev": true, "requires": { - "rc": "1.1.7" + "rc": "1.2.5" } }, "remove-trailing-separator": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", - "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", "dev": true }, "repeat-element": { @@ -4078,42 +4338,33 @@ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, - "repeating": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-1.1.3.tgz", - "integrity": "sha1-PUEUIYh3U3SU+X93+Xhfq4EPpKw=", - "dev": true, - "requires": { - "is-finite": "1.0.2" - } - }, "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "version": "2.83.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", + "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", "requires": { - "aws-sign2": "0.6.0", + "aws-sign2": "0.7.0", "aws4": "1.6.0", "caseless": "0.12.0", "combined-stream": "1.0.5", "extend": "3.0.1", "forever-agent": "0.6.1", - "form-data": "2.1.4", - "har-validator": "4.2.1", - "hawk": "3.1.3", - "http-signature": "1.1.1", + "form-data": "2.3.1", + "har-validator": "5.0.3", + "hawk": "6.0.2", + "http-signature": "1.2.0", "is-typedarray": "1.0.0", "isstream": "0.1.2", "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", + "mime-types": "2.1.17", "oauth-sign": "0.8.2", - "performance-now": "0.2.0", - "qs": "6.4.0", + "performance-now": "2.1.0", + "qs": "6.5.1", "safe-buffer": "5.1.1", "stringstream": "0.0.5", - "tough-cookie": "2.3.2", + "tough-cookie": "2.3.3", "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "uuid": "3.2.1" } }, "require-directory": { @@ -4150,7 +4401,7 @@ "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", "requires": { "resolve-from": "2.0.0", - "semver": "5.3.0" + "semver": "5.5.0" } }, "resolve-from": { @@ -4158,6 +4409,12 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "dev": true + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -4172,14 +4429,15 @@ "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "optional": true, "requires": { "align-text": "0.1.4" } }, "rimraf": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.5.4.tgz", - "integrity": "sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ=", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "requires": { "glob": "7.1.2" } @@ -4220,9 +4478,9 @@ "dev": true }, "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "semver-diff": { "version": "2.1.0", @@ -4230,7 +4488,7 @@ "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=", "dev": true, "requires": { - "semver": "5.3.0" + "semver": "5.5.0" } }, "set-blocking": { @@ -4238,15 +4496,36 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "set-getter": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", + "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "dev": true, + "requires": { + "to-object-path": "0.3.0" + } + }, "set-immediate-shim": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz", "integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=", "dev": true }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "split-string": "3.1.0" + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { @@ -4260,12 +4539,12 @@ "dev": true }, "shot": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/shot/-/shot-3.4.2.tgz", - "integrity": "sha1-Hlw/bysmZJrcQvfrNQIUpaApHWc=", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/shot/-/shot-4.0.4.tgz", + "integrity": "sha512-V8wHSJSNqt8ZIgdbTQCFIrp5BwBH+tsFLNBL1REmkFN/0PJdmzUBQscZqsbdSvdLc+Qxq7J5mcwzai66vs3ozA==", "requires": { - "hoek": "4.1.1", - "joi": "10.6.0" + "hoek": "5.0.2", + "joi": "13.1.2" } }, "signal-exit": { @@ -4290,32 +4569,136 @@ } } }, - "slide": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", - "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=", - "dev": true - }, "slug": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/slug/-/slug-0.9.1.tgz", "integrity": "sha1-rwj2CKfBFRa2F3iqgA3OhMUYz9o=", "requires": { - "unicode": "9.0.1" + "unicode": "10.0.0" + } + }, + "snapdragon": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.1.tgz", + "integrity": "sha1-4StUh/re0+PeoKyR6UAL91tAE3A=", + "dev": true, + "requires": { + "base": "0.11.2", + "debug": "2.6.9", + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "map-cache": "0.2.2", + "source-map": "0.5.7", + "source-map-resolve": "0.5.1", + "use": "2.0.2" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "requires": { + "define-property": "1.0.0", + "isobject": "3.0.1", + "snapdragon-util": "3.0.1" + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "requires": { + "kind-of": "3.2.2" } }, "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "requires": { - "hoek": "2.16.3" + "hoek": "4.2.0" }, "dependencies": { "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", + "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" } } }, @@ -4327,23 +4710,42 @@ "amdefine": "1.0.1" } }, + "source-map-resolve": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.1.tgz", + "integrity": "sha512-0KW2wvzfxm8NCTb30z0LMNyPqWCdDGE2viwzUaucqJdkTRXtZiSY3I+2A6nVAjmdOy0I4gU8DwnVVGsk9jvP2A==", + "dev": true, + "requires": { + "atob": "2.0.3", + "decode-uri-component": "0.2.0", + "resolve-url": "0.2.1", + "source-map-url": "0.4.0", + "urix": "0.1.0" + } + }, "source-map-support": { - "version": "0.4.18", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", - "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.3.tgz", + "integrity": "sha512-eKkTgWYeBOQqFGXRfKabMFdnWepo51vWqEdoeikaEPFiJC7MCU5j2h4+6Q8npkZTeLGbSyecZvRxiSoWl3rh+w==", "dev": true, "requires": { - "source-map": "0.5.7" + "source-map": "0.6.1" }, "dependencies": { "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true } } }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "dev": true + }, "spdx-correct": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-1.0.2.tgz", @@ -4371,6 +4773,36 @@ "through": "2.3.8" } }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, + "requires": { + "extend-shallow": "3.0.2" + }, + "dependencies": { + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "dev": true, + "requires": { + "assign-symbols": "1.0.0", + "is-extendable": "1.0.1" + } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "requires": { + "is-plain-object": "2.0.4" + } + } + } + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4390,43 +4822,106 @@ "getpass": "0.1.7", "jsbn": "0.1.1", "tweetnacl": "0.14.5" + } + }, + "statehood": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/statehood/-/statehood-6.0.5.tgz", + "integrity": "sha512-HPa8qT5sGTBVn1Fc9czBYR1oo7gBaay3ysnb04cvcF80YrDIV7880KpjmMj54j7CrFuQFfgMRb44QCRxRmAdTg==", + "requires": { + "boom": "7.1.1", + "bounce": "1.2.0", + "cryptiles": "4.1.1", + "hoek": "5.0.2", + "iron": "5.0.4", + "joi": "13.1.2" }, "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "cryptiles": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-4.1.1.tgz", + "integrity": "sha512-YuQUPbcOmaZsdvxJZ25DCA1W+lLIRoPJKBDKin+St1RCYEERSfoe1d25B1MvWNHN3e8SpFSVsqYvEUjp8J9H2w==", + "requires": { + "boom": "7.1.1" + } } } }, - "statehood": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/statehood/-/statehood-5.0.2.tgz", - "integrity": "sha1-xrO6oW7YsSHT8Jo/+oXiIZWn8qk=", + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "dev": true, "requires": { - "boom": "5.1.0", - "cryptiles": "3.1.2", - "hoek": "4.1.1", - "iron": "4.0.5", - "items": "2.1.1", - "joi": "10.6.0" + "define-property": "0.2.5", + "object-copy": "0.1.0" }, "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, "requires": { - "hoek": "4.1.1" + "is-descriptor": "0.1.6" } }, - "cryptiles": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-3.1.2.tgz", - "integrity": "sha1-qJ+7Ig9c4l7FboxKqKT9e1sNKf4=", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { - "boom": "5.1.0" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true } } }, @@ -4442,17 +4937,7 @@ "stream-shift": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "string-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-1.0.1.tgz", - "integrity": "sha1-VpcPscOFWOnnC3KL894mmsRa36w=", - "dev": true, - "requires": { - "strip-ansi": "3.0.1" - } + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" }, "string-width": { "version": "1.0.2", @@ -4493,38 +4978,37 @@ "is-utf8": "0.2.1" } }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", + "dev": true + }, "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "subtext": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/subtext/-/subtext-4.4.1.tgz", - "integrity": "sha1-L87JRd5CkoPD0YsVH/D6HxuHrsk=", - "requires": { - "boom": "5.1.0", - "content": "3.0.4", - "hoek": "4.1.1", - "pez": "2.1.5", - "wreck": "12.2.2" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/subtext/-/subtext-6.0.7.tgz", + "integrity": "sha512-IcJUvRjeR+NB437Iq+LORFNJW4L6Knqkj3oQrBrkdhIaS2VKJvx/9aYEq7vi+PEx5/OuehOL/40SkSZotLi/MA==", + "requires": { + "boom": "7.1.1", + "content": "4.0.3", + "hoek": "5.0.2", + "pez": "4.0.1", + "wreck": "14.0.2" } }, "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } }, "table": { "version": "4.0.2", @@ -4532,52 +5016,20 @@ "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", "dev": true, "requires": { - "ajv": "5.2.4", - "ajv-keywords": "2.1.0", - "chalk": "2.2.0", + "ajv": "5.5.2", + "ajv-keywords": "2.1.1", + "chalk": "2.3.0", "lodash": "4.17.4", "slice-ansi": "1.0.0", "string-width": "2.1.1" }, "dependencies": { - "ajv": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.4.tgz", - "integrity": "sha1-Pa+ai2ciEpn9ro2C0RftjmyAJEs=", - "dev": true, - "requires": { - "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "json-schema-traverse": "0.3.1", - "json-stable-stringify": "1.0.1" - } - }, "ansi-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, - "ansi-styles": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", - "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", - "dev": true, - "requires": { - "color-convert": "1.9.0" - } - }, - "chalk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.2.0.tgz", - "integrity": "sha512-0BMM/2hG3ZaoPfR6F+h/oWpZtsh3b/s62TjSM6MGCJWEbJDN1acqCXvyhhZsDSVFklpebUoQ5O1kKC7lOzrn9g==", - "dev": true, - "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -4602,15 +5054,6 @@ "requires": { "ansi-regex": "3.0.0" } - }, - "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", - "dev": true, - "requires": { - "has-flag": "2.0.0" - } } } }, @@ -4625,47 +5068,32 @@ } }, "tar-pack": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.3.0.tgz", - "integrity": "sha1-MJMYFkGPVa/E0hd1r91nIM7kXa4=", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.1.tgz", + "integrity": "sha512-PPRybI9+jM5tjtCbN2cxmmRU7YmqT3Zv/UDy48tAh2XRkLa9bAORtSWLkVc13+GJF+cdTh1yEnHEk3cpTaL5Kg==", "requires": { - "debug": "2.2.0", + "debug": "2.6.9", "fstream": "1.0.11", "fstream-ignore": "1.0.5", - "once": "1.3.3", - "readable-stream": "2.1.5", - "rimraf": "2.5.4", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", "tar": "2.2.1", "uid-number": "0.0.6" - }, - "dependencies": { - "once": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", - "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", - "requires": { - "wrappy": "1.0.2" - } - }, - "readable-stream": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.1.5.tgz", - "integrity": "sha1-ZvqLcg4UOLNkaB8q0aY8YYRIydA=", - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "0.10.31", - "util-deprecate": "1.0.2" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - } + } + }, + "teamwork": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/teamwork/-/teamwork-3.0.1.tgz", + "integrity": "sha512-hEkJIpDOfOYe9NYaLFk00zQbzZeKNCY8T2pRH3I13Y1mJwxaSQ6NEsjY5rCp+11ezCiZpWGoGFTbOuhg4qKevQ==" + }, + "term-size": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz", + "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=", + "dev": true, + "requires": { + "execa": "0.7.0" } }, "text-table": { @@ -4681,9 +5109,9 @@ "dev": true }, "timed-out": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-2.0.0.tgz", - "integrity": "sha1-84sK6B03R9YoAB9B2vxlKs5nHAo=", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true }, "tmp": { @@ -4694,97 +5122,139 @@ "os-tmpdir": "1.0.2" } }, - "topo": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", - "integrity": "sha1-zVYVdSU5BXwNwEkaYhw7xvvh0YI=", + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "dev": true, "requires": { - "hoek": "4.1.1" + "kind-of": "3.2.2" } }, - "touch": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-1.0.0.tgz", - "integrity": "sha1-RJy+LbrlqMgDjjDXH6D/RklHxN4=", + "to-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.1.tgz", + "integrity": "sha1-FTWL7kosg712N3uh3ASdDxiDeq4=", "dev": true, "requires": { - "nopt": "1.0.10" + "define-property": "0.2.5", + "extend-shallow": "2.0.1", + "regex-not": "1.0.0" }, "dependencies": { - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "abbrev": "1.1.0" + "is-descriptor": "0.1.6" } - } - } - }, - "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", - "requires": { - "punycode": "1.4.1" - } - }, - "transformers": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/transformers/-/transformers-2.1.0.tgz", - "integrity": "sha1-XSPLNVYd2F3Gf7hIIwm0fVPM6ac=", - "requires": { - "css": "1.0.8", - "promise": "2.0.0", - "uglify-js": "2.2.5" - }, - "dependencies": { - "is-promise": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-1.0.1.tgz", - "integrity": "sha1-MVc3YcBX4zwukaq56W2gjO++duU=" }, - "optimist": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", - "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, "requires": { - "wordwrap": "0.0.3" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "promise": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/promise/-/promise-2.0.0.tgz", - "integrity": "sha1-RmSKqdYFr10ucMMCS/WUNtoCuA4=", + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, "requires": { - "is-promise": "1.0.1" + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } } }, - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, "requires": { - "amdefine": "1.0.1" + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" } }, - "uglify-js": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.2.5.tgz", - "integrity": "sha1-puAqcNg5eSuXgEiLe4sYTAlcmcc=", + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + } + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "dev": true, + "requires": { + "is-number": "3.0.0", + "repeat-string": "1.6.1" + } + }, + "topo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", + "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", + "requires": { + "hoek": "5.0.2" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "1.0.10" + }, + "dependencies": { + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha1-bd0hvSoxQXuScn3Vhfim83YI6+4=", + "dev": true, "requires": { - "optimist": "0.3.7", - "source-map": "0.1.43" + "abbrev": "1.1.1" } } } }, - "tryit": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", - "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", - "dev": true + "tough-cookie": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.3.tgz", + "integrity": "sha1-C2GKVWW23qkL80JdBNVe3EdadWE=", + "requires": { + "punycode": "1.4.1" + } }, "tunnel-agent": { "version": "0.6.0", @@ -4819,8 +5289,9 @@ "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "optional": true, "requires": { - "source-map": "0.5.6", + "source-map": "0.5.7", "uglify-to-browserify": "1.0.2", "yargs": "3.10.0" }, @@ -4828,12 +5299,14 @@ "camelcase": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "optional": true }, "cliui": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "optional": true, "requires": { "center-align": "0.1.3", "right-align": "0.1.3", @@ -4841,24 +5314,28 @@ } }, "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=" + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "optional": true }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "optional": true }, "wordwrap": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "optional": true }, "yargs": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "optional": true, "requires": { "camelcase": "1.2.1", "cliui": "2.1.0", @@ -4880,56 +5357,238 @@ "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" }, "undefsafe": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-0.0.3.tgz", - "integrity": "sha1-7Mo6A+VrmvFzhbqsgSrIO5lKli8=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.1.tgz", + "integrity": "sha1-A7LyoWyUVW4Usu3vMmzWaq+CcHo=", + "dev": true, + "requires": { + "debug": "2.6.9" + } }, "unicode": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/unicode/-/unicode-9.0.1.tgz", - "integrity": "sha1-EEcGJyxkZMV0gBvhsIb3JFzyUVg=" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", + "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "dev": true, + "requires": { + "arr-union": "3.1.0", + "get-value": "2.0.6", + "is-extendable": "0.1.1", + "set-value": "0.4.3" + }, + "dependencies": { + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "dev": true, + "requires": { + "extend-shallow": "2.0.1", + "is-extendable": "0.1.1", + "is-plain-object": "2.0.4", + "to-object-path": "0.3.0" + } + } + } + }, + "unique-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz", + "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=", + "dev": true, + "requires": { + "crypto-random-string": "1.0.0" + } + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "dev": true, + "requires": { + "has-value": "0.3.1", + "isobject": "3.0.1" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "dev": true, + "requires": { + "get-value": "2.0.6", + "has-values": "0.1.4", + "isobject": "2.1.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true, + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", + "dev": true + } + } + }, + "unzip-response": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", + "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c=", + "dev": true }, "update-notifier": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-0.5.0.tgz", - "integrity": "sha1-B7XcIGazYnqztPUwEw9+3doHpMw=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.3.0.tgz", + "integrity": "sha1-TognpruRUUCrCTVZ1wFOPruDdFE=", "dev": true, "requires": { - "chalk": "1.1.3", - "configstore": "1.4.0", + "boxen": "1.3.0", + "chalk": "2.3.0", + "configstore": "3.1.1", + "import-lazy": "2.1.0", + "is-installed-globally": "0.1.0", "is-npm": "1.0.0", - "latest-version": "1.0.1", - "repeating": "1.1.3", + "latest-version": "3.1.0", "semver-diff": "2.1.0", - "string-length": "1.0.1" + "xdg-basedir": "3.0.0" } }, - "useragent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.2.1.tgz", - "integrity": "sha1-z1k+9PLRdYdei7ZY6pLhik/QbY4=", + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "dev": true + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, "requires": { - "lru-cache": "2.2.4", - "tmp": "0.0.33" + "prepend-http": "1.0.4" + } + }, + "use": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/use/-/use-2.0.2.tgz", + "integrity": "sha1-riig1y+TvyJCKhii43mZMRLeyOg=", + "dev": true, + "requires": { + "define-property": "0.2.5", + "isobject": "3.0.1", + "lazy-cache": "2.0.2" }, "dependencies": { - "lru-cache": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.2.4.tgz", - "integrity": "sha1-bGWGGb7PFAMdDQtZSxYELOTcBj0=" + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "dev": true, + "requires": { + "is-descriptor": "0.1.6" + } + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "dev": true, + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "dev": true, + "requires": { + "is-accessor-descriptor": "0.1.6", + "is-data-descriptor": "0.1.4", + "kind-of": "5.1.0" + } + }, + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", + "dev": true + }, + "lazy-cache": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-2.0.2.tgz", + "integrity": "sha1-uRkKT5EzVGlIQIWfio9whNiCImQ=", + "dev": true, + "requires": { + "set-getter": "0.1.0" + } } } }, + "useragent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/useragent/-/useragent-2.3.0.tgz", + "integrity": "sha512-4AoH4pxuSvHCjqLO04sU6U/uE65BYza8l/KKBS0b0hnUPWi+cQ2BpeTEwejCSx9SPV5/U03nniDTrWx5NrmKdw==", + "requires": { + "lru-cache": "4.1.1", + "tmp": "0.0.33" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "validate-npm-package-license": { "version": "3.0.1", @@ -4941,55 +5600,23 @@ } }, "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "requires": { - "extsprintf": "1.0.2" + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" } }, "vise": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/vise/-/vise-2.0.2.tgz", - "integrity": "sha1-awjo+0y3bjpQzW3Q7DczjoEaDTk=", - "requires": { - "hoek": "4.1.1" - } - }, - "vision": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/vision/-/vision-4.1.1.tgz", - "integrity": "sha1-4bYSstLi8gMQoDkpD9SdUSSPgto=", - "requires": { - "boom": "4.3.1", - "hoek": "4.1.1", - "items": "2.1.1", - "joi": "10.6.0" - }, - "dependencies": { - "boom": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", - "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", - "requires": { - "hoek": "4.1.1" - } - } - } - }, - "visionary": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/visionary/-/visionary-6.0.2.tgz", - "integrity": "sha1-V1Jg6NbLinM0oF4Mt2uZQmYZXEY=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vise/-/vise-3.0.0.tgz", + "integrity": "sha512-kBFZLmiL1Vm3rHXphkhvvAcsjgeQXRrOFCbJb0I50YZZP4HGRNH+xGzK3matIMcpbsfr3I02u9odj4oCD0TWgA==", "requires": { - "hoek": "4.1.1" + "hoek": "5.0.2" } }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" - }, "which": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/which/-/which-1.3.0.tgz", @@ -5012,27 +5639,59 @@ "string-width": "1.0.2" } }, - "window-size": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", - "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" - }, - "with": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/with/-/with-4.0.3.tgz", - "integrity": "sha1-7v0VTp550sjTQXtkeo8U2f7M4U4=", + "widest-line": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz", + "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=", + "dev": true, "requires": { - "acorn": "1.2.2", - "acorn-globals": "1.0.9" + "string-width": "2.1.1" }, "dependencies": { - "acorn": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz", - "integrity": "sha1-yM4n3grMdtiW0rH6099YjZ6C8BQ=" + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "dev": true, + "requires": { + "is-fullwidth-code-point": "2.0.0", + "strip-ansi": "4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "3.0.0" + } } } }, + "will-call": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/will-call/-/will-call-1.0.1.tgz", + "integrity": "sha512-1hEeV8SfBYhNRc/bNXeQfyUBX8Dl9SCYME3qXh99iZP9wJcnhnlBsoBw8Y0lXVZ3YuPsoxImTzBiol1ouNR/hg==", + "dev": true + }, + "window-size": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.2.0.tgz", + "integrity": "sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU=" + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -5053,22 +5712,12 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "wreck": { - "version": "12.2.2", - "resolved": "https://registry.npmjs.org/wreck/-/wreck-12.2.2.tgz", - "integrity": "sha1-4hgj00w21nIATu+jR66MT2BQ49s=", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/wreck/-/wreck-14.0.2.tgz", + "integrity": "sha512-QCm3omWNJUseqrSzwX2QZi1rBbmCfbFHJAXputLLyZ37VSiFnSYQB0ms/mPnSvrlIu7GVm89Y/gBNhSY26uVIQ==", "requires": { - "boom": "5.1.0", - "hoek": "4.1.1" - }, - "dependencies": { - "boom": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-5.1.0.tgz", - "integrity": "sha1-Awj6jpJM1tQtnDv0iDvcmPDnHfg=", - "requires": { - "hoek": "4.1.1" - } - } + "boom": "7.1.1", + "hoek": "5.0.2" } }, "write": { @@ -5081,24 +5730,21 @@ } }, "write-file-atomic": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz", - "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", "dev": true, "requires": { "graceful-fs": "4.1.11", "imurmurhash": "0.1.4", - "slide": "1.1.6" + "signal-exit": "3.0.2" } }, "xdg-basedir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz", - "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=", - "dev": true, - "requires": { - "os-homedir": "1.0.2" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz", + "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ=", + "dev": true }, "y18n": { "version": "3.2.1", diff --git a/package.json b/package.json index 74debcd..010d7e1 100644 --- a/package.json +++ b/package.json @@ -4,47 +4,48 @@ "description": "A user system API starter", "main": "index.js", "scripts": { - "start": "nodemon --inspect -e js,md server.js", + "start": "nodemon -e js,md server.js", + "inspect": "nodemon --inspect -e js,md server.js", "first-time-setup": "node first-time-setup.js", - "test": "lab -c -L" + "test": "lab -c -L", + "test-server": "lab -c -L -v $TEST_TARGET" }, "author": "Reza Akhavan (http://reza.akhavan.me/)", "license": "MIT", "engines": { - "node": "8.x.x" + "node": "9.x.x" }, "dependencies": { - "async": "2.x.x", "bcrypt": "1.x.x", - "boom": "5.x.x", + "boom": "7.x.x", "confidence": "3.x.x", - "dotenv": "4.x.x", - "glue": "4.x.x", + "dotenv": "5.x.x", + "eslint-config-hapi": "11.x.x", + "eslint-plugin-hapi": "4.x.x", + "glue": "5.x.x", + "good": "8.x.x", + "good-console": "7.x.x", + "good-squeeze": "5.x.x", "handlebars": "4.x.x", - "hapi": "16.x.x", - "hapi-auth-basic": "4.x.x", - "hapi-mongo-models": "6.x.x", - "hoek": "4.x.x", - "inert": "4.x.x", - "jade": "1.x.x", - "joi": "10.x.x", - "lout": "10.x.x", - "mongo-models": "1.x.x", - "mongodb": "2.x.x", + "hapi": "17.x.x", + "hapi-auth-basic": "5.x.x", + "hapi-mongo-models": "7.x.x", + "hapi-remote-address": "1.x.x", + "hoek": "5.x.x", + "joi": "13.x.x", + "joistick": "1.x.x", + "mongo-models": "2.x.x", + "mongodb": "3.x.x", "nodemailer": "4.x.x", "nodemailer-markdown": "1.x.x", - "object-assign": "4.x.x", "slug": "0.9.x", "useragent": "2.x.x", - "uuid": "3.x.x", - "vision": "4.x.x", - "visionary": "6.x.x" + "uuid": "3.x.x" }, "devDependencies": { - "code": "4.x.x", - "lab": "14.x.x", + "code": "5.x.x", + "lab": "15.x.x", "nodemon": "1.x.x", - "promptly": "2.x.x", - "proxyquire": "1.x.x" + "promptly": "3.x.x" } } diff --git a/server.js b/server.js index 98b4174..4d8f36e 100644 --- a/server.js +++ b/server.js @@ -1,15 +1,23 @@ 'use strict'; -const Composer = require('./index'); +const Glue = require('glue'); +const Manifest = require('./manifest'); -Composer((err, server) => { +process.on('unhandledRejection', (reason, promise) => { - if (err) { - throw err; - } + console.error(`Unhandled Rejection at: ${promise} reason: ${reason}`); +}); - server.start(() => { - console.log('Started the plot device on port ' + server.info.port); - }); -}); +const main = async function () { + + const options = { relativeTo: __dirname }; + const server = await Glue.compose(Manifest.get('/'), options); + + await server.start(); + + console.log(`Server started on port ${Manifest.get('/server/port')}`); +}; + + +main(); diff --git a/server/api/accounts.js b/server/api/accounts.js index 5045601..c357d72 100644 --- a/server/api/accounts.js +++ b/server/api/accounts.js @@ -1,23 +1,19 @@ 'use strict'; -const Async = require('async'); -const AuthPlugin = require('../auth'); +const Account = require('../models/account'); const Boom = require('boom'); const Joi = require('joi'); +const NoteEntry = require('../models/note-entry'); +const Preware = require('../preware'); +const Status = require('../models/status'); +const StatusEntry = require('../models/status-entry'); +const User = require('../models/user'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Account = server.plugins['hapi-mongo-models'].Account; - const User = server.plugins['hapi-mongo-models'].User; - const Status = server.plugins['hapi-mongo-models'].Status; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/accounts', + path: '/api/accounts', config: { auth: { strategy: 'simple', @@ -25,123 +21,72 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } } }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: Account.sortAdapter(request.query.sort) + }; - Account.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); + return await Account.pagedFind(query, page, limit, options); } }); server.route({ - method: 'GET', - path: '/accounts/{id}', + method: 'POST', + path: '/api/accounts', config: { auth: { strategy: 'simple', scope: 'admin' - } - }, - handler: function (request, reply) { - - Account.findById(request.params.id, (err, account) => { - - if (err) { - return reply(err); - } - - if (!account) { - return reply(Boom.notFound('Document not found.')); + }, + validate: { + payload: { + name: Joi.string().required() } - - reply(account); - }); - } - }); - - - server.route({ - method: 'GET', - path: '/accounts/my', - config: { - auth: { - strategy: 'simple', - scope: 'account' } }, - handler: function (request, reply) { - - const id = request.auth.credentials.roles.account._id.toString(); - const fields = Account.fieldsAdapter('user name timeCreated'); - - Account.findById(id, fields, (err, account) => { - - if (err) { - return reply(err); - } - - if (!account) { - return reply(Boom.notFound('Document not found. That is strange.')); - } + handler: async function (request, h) { - reply(account); - }); + return await Account.create(request.payload.name); } }); server.route({ - method: 'POST', - path: '/accounts', + method: 'GET', + path: '/api/accounts/{id}', config: { auth: { strategy: 'simple', scope: 'admin' - }, - validate: { - payload: { - name: Joi.string().required() - } } }, - handler: function (request, reply) { + handler: async function (request, h) { - const name = request.payload.name; + const account = await Account.findById(request.params.id); - Account.create(name, (err, account) => { - - if (err) { - return reply(err); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return account; } }); server.route({ method: 'PUT', - path: '/accounts/{id}', + path: '/api/accounts/{id}', config: { auth: { strategy: 'simple', @@ -157,7 +102,7 @@ internals.applyRoutes = function (server, next) { } } }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { @@ -165,68 +110,45 @@ internals.applyRoutes = function (server, next) { name: request.payload.name } }; + const account = await Account.findByIdAndUpdate(id, update); - Account.findByIdAndUpdate(id, update, (err, account) => { - - if (err) { - return reply(err); - } - - if (!account) { - return reply(Boom.notFound('Document not found.')); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return account; } }); server.route({ - method: 'PUT', - path: '/accounts/my', + method: 'DELETE', + path: '/api/accounts/{id}', config: { auth: { strategy: 'simple', - scope: 'account' + scope: 'admin' }, - validate: { - payload: { - name: Joi.object({ - first: Joi.string().required(), - middle: Joi.string().allow(''), - last: Joi.string().required() - }).required() - } - } + pre: [ + Preware.requireAdminGroup('root') + ] }, - handler: function (request, reply) { - - const id = request.auth.credentials.roles.account._id.toString(); - const update = { - $set: { - name: request.payload.name - } - }; - const findOptions = { - fields: Account.fieldsAdapter('user name timeCreated') - }; + handler: async function (request, h) { - Account.findByIdAndUpdate(id, update, findOptions, (err, account) => { + const account = await Account.findByIdAndDelete(request.params.id); - if (err) { - return reply(err); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return { message: 'Success.' }; } }); server.route({ method: 'PUT', - path: '/accounts/{id}/user', + path: '/api/accounts/{id}/user', config: { auth: { strategy: 'simple', @@ -239,105 +161,75 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'account', - method: function (request, reply) { - - Account.findById(request.params.id, (err, account) => { + method: async function (request, h) { - if (err) { - return reply(err); - } + const account = await Account.findById(request.params.id); - if (!account) { - return reply(Boom.notFound('Document not found.')); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return account; } }, { assign: 'user', - method: function (request, reply) { - - User.findByUsername(request.payload.username, (err, user) => { - - if (err) { - return reply(err); - } + method: async function (request, h) { - if (!user) { - return reply(Boom.notFound('User document not found.')); - } + const user = await User.findByUsername(request.payload.username); - if (user.roles && - user.roles.account && - user.roles.account.id !== request.params.id) { + if (!user) { + throw Boom.notFound('User not found.'); + } - return reply(Boom.conflict('User is already linked to another account. Unlink first.')); - } + if (user.roles.account && + user.roles.account.id !== request.params.id) { - reply(user); - }); - } - }, { - assign: 'userCheck', - method: function (request, reply) { + throw Boom.conflict('User is linked to an account. Unlink first.'); + } if (request.pre.account.user && - request.pre.account.user.id !== request.pre.user._id.toString()) { + request.pre.account.user.id !== `${user._id}`) { - return reply(Boom.conflict('Account is already linked to another user. Unlink first.')); + throw Boom.conflict('Account is linked to a user. Unlink first.'); } - reply(true); + return user; } }] }, - handler: function (request, reply) { - - Async.auto({ - account: function (done) { - - const id = request.params.id; - const update = { - $set: { - user: { - id: request.pre.user._id.toString(), - name: request.pre.user.username - } - } - }; - - Account.findByIdAndUpdate(id, update, done); - }, - user: function (done) { - - const id = request.pre.user._id; - const update = { - $set: { - 'roles.account': { - id: request.pre.account._id.toString(), - name: request.pre.account.name.first + ' ' + request.pre.account.name.last - } - } - }; + handler: async function (request, h) { - User.findByIdAndUpdate(id, update, done); + const preUser = request.pre.user; + const preAccount = request.pre.account; + const accountUpdate = { + $set: { + user: { + id: `${preUser._id}`, + name: preUser.username + } } - }, (err, results) => { - - if (err) { - return reply(err); + }; + const userUpdate = { + $set: { + 'roles.account': { + id: `${preAccount._id}`, + name: `${preAccount.name.first} ${preAccount.name.last}` + } } + }; + const [account] = await Promise.all([ + Account.findByIdAndUpdate(preAccount._id, accountUpdate), + User.findByIdAndUpdate(preUser._id, userUpdate) + ]); - reply(results.account); - }); + return account; } }); server.route({ method: 'DELETE', - path: '/accounts/{id}/user', + path: '/api/accounts/{id}/user', config: { auth: { strategy: 'simple', @@ -345,84 +237,67 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'account', - method: function (request, reply) { + method: async function (request, h) { - Account.findById(request.params.id, (err, account) => { + let account = await Account.findById(request.params.id); - if (err) { - return reply(err); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - if (!account) { - return reply(Boom.notFound('Document not found.')); - } + if (!account.user || !account.user.id) { + const update = { + $unset: { + user: undefined + } + }; - if (!account.user || !account.user.id) { - return reply(account).takeover(); - } + account = await Account.findByIdAndUpdate(request.params.id, update); - reply(account); - }); + return h.response(account).takeover(); + } + + return account; } }, { assign: 'user', - method: function (request, reply) { - - User.findById(request.pre.account.user.id, (err, user) => { + method: async function (request, h) { - if (err) { - return reply(err); - } + const user = await User.findById(request.pre.account.user.id); - if (!user) { - return reply(Boom.notFound('User document not found.')); - } + if (!user) { + throw Boom.notFound('User not found.'); + } - reply(user); - }); + return user; } }] }, - handler: function (request, reply) { - - Async.auto({ - account: function (done) { + handler: async function (request, h) { - const id = request.params.id; - const update = { - $unset: { - user: undefined - } - }; - - Account.findByIdAndUpdate(id, update, done); - }, - user: function (done) { - - const id = request.pre.user._id.toString(); - const update = { - $unset: { - 'roles.account': undefined - } - }; - - User.findByIdAndUpdate(id, update, done); + const accountUpdate = { + $unset: { + user: undefined } - }, (err, results) => { - - if (err) { - return reply(err); + }; + const userUpdate = { + $unset: { + 'roles.account': undefined } + }; + const [account] = await Promise.all([ + Account.findByIdAndUpdate(request.params.id, accountUpdate), + User.findByIdAndUpdate(request.pre.user._id, userUpdate) + ]); - reply(results.account); - }); + return account; } }); server.route({ method: 'POST', - path: '/accounts/{id}/notes', + path: '/api/accounts/{id}/notes', config: { auth: { strategy: 'simple', @@ -434,37 +309,35 @@ internals.applyRoutes = function (server, next) { } } }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; + const newNote = new NoteEntry({ + data: request.payload.data, + adminCreated: { + id: `${request.auth.credentials.user._id}`, + name: request.auth.credentials.user.username + } + }); const update = { $push: { - notes: { - data: request.payload.data, - timeCreated: new Date(), - userCreated: { - id: request.auth.credentials.user._id.toString(), - name: request.auth.credentials.user.username - } - } + notes: newNote } }; + const account = await Account.findByIdAndUpdate(id, update); - Account.findByIdAndUpdate(id, update, (err, account) => { - - if (err) { - return reply(err); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return account; } }); server.route({ method: 'POST', - path: '/accounts/{id}/status', + path: '/api/accounts/{id}/status', config: { auth: { strategy: 'simple', @@ -477,31 +350,29 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'status', - method: function (request, reply) { + method: async function (request, h) { - Status.findById(request.payload.status, (err, status) => { + const status = await Status.findById(request.payload.status); - if (err) { - return reply(err); - } + if (!status) { + throw Boom.notFound('Status not found.'); + } - reply(status); - }); + return status; } }] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; - const newStatus = { - id: request.pre.status._id.toString(), + const newStatus = new StatusEntry({ + id: `${request.pre.status._id}`, name: request.pre.status.name, - timeCreated: new Date(), - userCreated: { - id: request.auth.credentials.user._id.toString(), + adminCreated: { + id: `${request.auth.credentials.user._id}`, name: request.auth.credentials.user.username } - }; + }); const update = { $set: { 'status.current': newStatus @@ -510,61 +381,74 @@ internals.applyRoutes = function (server, next) { 'status.log': newStatus } }; + const account = await Account.findByIdAndUpdate(id, update); - Account.findByIdAndUpdate(id, update, (err, account) => { - - if (err) { - return reply(err); - } + if (!account) { + throw Boom.notFound('Account not found.'); + } - reply(account); - }); + return account; } }); server.route({ - method: 'DELETE', - path: '/accounts/{id}', + method: 'GET', + path: '/api/accounts/my', config: { auth: { strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] + scope: 'account' + } }, - handler: function (request, reply) { - - Account.findByIdAndDelete(request.params.id, (err, account) => { - - if (err) { - return reply(err); - } + handler: async function (request, h) { - if (!account) { - return reply(Boom.notFound('Document not found.')); - } + const id = request.auth.credentials.roles.account._id; + const fields = Account.fieldsAdapter('user name timeCreated'); - reply({ message: 'Success.' }); - }); + return await Account.findById(id, fields); } }); - next(); -}; - - -exports.register = function (server, options, next) { + server.route({ + method: 'PUT', + path: '/api/accounts/my', + config: { + auth: { + strategy: 'simple', + scope: 'account' + }, + validate: { + payload: { + name: Joi.object({ + first: Joi.string().required(), + middle: Joi.string().allow(''), + last: Joi.string().required() + }).required() + } + } + }, + handler: async function (request, h) { - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); + const id = request.auth.credentials.roles.account._id; + const update = { + $set: { + name: request.payload.name + } + }; + const options = { + fields: Account.fieldsAdapter('user name timeCreated') + }; - next(); + return await Account.findByIdAndUpdate(id, update, options); + } + }); }; -exports.register.attributes = { - name: 'account' +module.exports = { + name: 'api-accounts', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/api/admin-groups.js b/server/api/admin-groups.js index d95a77c..1f65939 100644 --- a/server/api/admin-groups.js +++ b/server/api/admin-groups.js @@ -1,20 +1,15 @@ 'use strict'; -const AuthPlugin = require('../auth'); +const AdminGroup = require('../models/admin-group'); const Boom = require('boom'); const Joi = require('joi'); +const Preware = require('../preware'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const AdminGroup = server.plugins['hapi-mongo-models'].AdminGroup; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/admin-groups', + path: '/api/admin-groups', config: { auth: { strategy: 'simple', @@ -22,102 +17,81 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: AdminGroup.sortAdapter(request.query.sort) + }; - AdminGroup.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); + return await AdminGroup.pagedFind(query, limit, page, options); } }); server.route({ - method: 'GET', - path: '/admin-groups/{id}', + method: 'POST', + path: '/api/admin-groups', config: { auth: { strategy: 'simple', scope: 'admin' }, + validate: { + payload: { + name: Joi.string().required() + } + }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - AdminGroup.findById(request.params.id, (err, adminGroup) => { - - if (err) { - return reply(err); - } - - if (!adminGroup) { - return reply(Boom.notFound('Document not found.')); - } - - reply(adminGroup); - }); + return await AdminGroup.create(request.payload.name); } }); server.route({ - method: 'POST', - path: '/admin-groups', + method: 'GET', + path: '/api/admin-groups/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, - validate: { - payload: { - name: Joi.string().required() - } - }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const name = request.payload.name; + const adminGroup = await AdminGroup.findById(request.params.id); - AdminGroup.create(name, (err, adminGroup) => { - - if (err) { - return reply(err); - } + if (!adminGroup) { + throw Boom.notFound('AdminGroup not found.'); + } - reply(adminGroup); - }); + return adminGroup; } }); server.route({ method: 'PUT', - path: '/admin-groups/{id}', + path: '/api/admin-groups/{id}', config: { auth: { strategy: 'simple', @@ -132,10 +106,10 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { @@ -143,26 +117,20 @@ internals.applyRoutes = function (server, next) { name: request.payload.name } }; + const adminGroup = await AdminGroup.findByIdAndUpdate(id, update); - AdminGroup.findByIdAndUpdate(id, update, (err, adminGroup) => { - - if (err) { - return reply(err); - } - - if (!adminGroup) { - return reply(Boom.notFound('Document not found.')); - } + if (!adminGroup) { + throw Boom.notFound('AdminGroup not found.'); + } - reply(adminGroup); - }); + return adminGroup; } }); server.route({ - method: 'PUT', - path: '/admin-groups/{id}/permissions', + method: 'DELETE', + path: '/api/admin-groups/{id}', config: { auth: { strategy: 'simple', @@ -171,39 +139,28 @@ internals.applyRoutes = function (server, next) { validate: { params: { id: Joi.string().invalid('root') - }, - payload: { - permissions: Joi.object().required() } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { - - const id = request.params.id; - const update = { - $set: { - permissions: request.payload.permissions - } - }; + handler: async function (request, h) { - AdminGroup.findByIdAndUpdate(id, update, (err, adminGroup) => { + const adminGroup = await AdminGroup.findByIdAndDelete(request.params.id); - if (err) { - return reply(err); - } + if (!adminGroup) { + throw Boom.notFound('AdminGroup not found.'); + } - reply(adminGroup); - }); + return { message: 'Success.' }; } }); server.route({ - method: 'DELETE', - path: '/admin-groups/{id}', + method: 'PUT', + path: '/api/admin-groups/{id}/permissions', config: { auth: { strategy: 'simple', @@ -212,42 +169,37 @@ internals.applyRoutes = function (server, next) { validate: { params: { id: Joi.string().invalid('root') + }, + payload: { + permissions: Joi.object().required() } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - AdminGroup.findByIdAndDelete(request.params.id, (err, adminGroup) => { - - if (err) { - return reply(err); + const id = request.params.id; + const update = { + $set: { + permissions: request.payload.permissions } + }; + const adminGroup = await AdminGroup.findByIdAndUpdate(id, update); - if (!adminGroup) { - return reply(Boom.notFound('Document not found.')); - } + if (!adminGroup) { + throw Boom.notFound('AdminGroup not found.'); + } - reply({ message: 'Success.' }); - }); + return adminGroup; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'admin-groups' +module.exports = { + name: 'api-admin-groups', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/api/admins.js b/server/api/admins.js index db0b5d3..f762efe 100644 --- a/server/api/admins.js +++ b/server/api/admins.js @@ -1,22 +1,16 @@ 'use strict'; -const Async = require('async'); -const AuthPlugin = require('../auth'); +const Admin = require('../models/admin'); const Boom = require('boom'); const Joi = require('joi'); +const Preware = require('../preware'); +const User = require('../models/user'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Admin = server.plugins['hapi-mongo-models'].Admin; - const User = server.plugins['hapi-mongo-models'].User; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/admins', + path: '/api/admins', config: { auth: { strategy: 'simple', @@ -24,102 +18,81 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: Admin.sortAdapter(request.query.sort) + }; - Admin.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); + return await Admin.pagedFind(query, page, limit, options); } }); server.route({ - method: 'GET', - path: '/admins/{id}', + method: 'POST', + path: '/api/admins', config: { auth: { strategy: 'simple', scope: 'admin' }, + validate: { + payload: { + name: Joi.string().required() + } + }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - Admin.findById(request.params.id, (err, admin) => { - - if (err) { - return reply(err); - } - - if (!admin) { - return reply(Boom.notFound('Document not found.')); - } - - reply(admin); - }); + return await Admin.create(request.payload.name); } }); server.route({ - method: 'POST', - path: '/admins', + method: 'GET', + path: '/api/admins/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, - validate: { - payload: { - name: Joi.string().required() - } - }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const name = request.payload.name; + const admin = await Admin.findById(request.params.id); - Admin.create(name, (err, admin) => { + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - if (err) { - return reply(err); - } - - reply(admin); - }); + return admin; } }); server.route({ method: 'PUT', - path: '/admins/{id}', + path: '/api/admins/{id}', config: { auth: { strategy: 'simple', @@ -138,10 +111,10 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { @@ -149,26 +122,45 @@ internals.applyRoutes = function (server, next) { name: request.payload.name } }; + const admin = await Admin.findByIdAndUpdate(id, update); - Admin.findByIdAndUpdate(id, update, (err, admin) => { + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - if (err) { - return reply(err); - } + return admin; + } + }); - if (!admin) { - return reply(Boom.notFound('Document not found.')); - } - reply(admin); - }); + server.route({ + method: 'DELETE', + path: '/api/admins/{id}', + config: { + auth: { + strategy: 'simple', + scope: 'admin' + }, + pre: [ + Preware.requireAdminGroup('root') + ] + }, + handler: async function (request, h) { + + const admin = await Admin.findByIdAndDelete(request.params.id); + + if (!admin) { + throw Boom.notFound('Admin not found.'); + } + + return { message: 'Success.' }; } }); server.route({ method: 'PUT', - path: '/admins/{id}/permissions', + path: '/api/admins/{id}/groups', config: { auth: { strategy: 'simple', @@ -179,37 +171,35 @@ internals.applyRoutes = function (server, next) { id: Joi.string().invalid('111111111111111111111111') }, payload: { - permissions: Joi.object().required() + groups: Joi.object().required() } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { $set: { - permissions: request.payload.permissions + groups: request.payload.groups } }; + const admin = await Admin.findByIdAndUpdate(id, update); - Admin.findByIdAndUpdate(id, update, (err, admin) => { - - if (err) { - return reply(err); - } + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - reply(admin); - }); + return admin; } }); server.route({ method: 'PUT', - path: '/admins/{id}/groups', + path: '/api/admins/{id}/permissions', config: { auth: { strategy: 'simple', @@ -220,37 +210,35 @@ internals.applyRoutes = function (server, next) { id: Joi.string().invalid('111111111111111111111111') }, payload: { - groups: Joi.object().required() + permissions: Joi.object().required() } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { $set: { - groups: request.payload.groups + permissions: request.payload.permissions } }; + const admin = await Admin.findByIdAndUpdate(id, update); - Admin.findByIdAndUpdate(id, update, (err, admin) => { + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - if (err) { - return reply(err); - } - - reply(admin); - }); + return admin; } }); server.route({ method: 'PUT', - path: '/admins/{id}/user', + path: '/api/admins/{id}/user', config: { auth: { strategy: 'simple', @@ -265,109 +253,79 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root'), + Preware.requireAdminGroup('root'), { assign: 'admin', - method: function (request, reply) { - - Admin.findById(request.params.id, (err, admin) => { + method: async function (request, h) { - if (err) { - return reply(err); - } + const admin = await Admin.findById(request.params.id); - if (!admin) { - return reply(Boom.notFound('Document not found.')); - } + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - reply(admin); - }); + return admin; } }, { assign: 'user', - method: function (request, reply) { + method: async function (request, h) { - User.findByUsername(request.payload.username, (err, user) => { + const user = await User.findByUsername(request.payload.username); - if (err) { - return reply(err); - } - - if (!user) { - return reply(Boom.notFound('User document not found.')); - } - - if (user.roles && - user.roles.admin && - user.roles.admin.id !== request.params.id) { + if (!user) { + throw Boom.notFound('User not found.'); + } - return reply(Boom.conflict('User is already linked to another admin. Unlink first.')); - } + if (user.roles.admin && + user.roles.admin.id !== request.params.id) { - reply(user); - }); - } - }, { - assign: 'userCheck', - method: function (request, reply) { + throw Boom.conflict('User is linked to an admin. Unlink first.'); + } if (request.pre.admin.user && - request.pre.admin.user.id !== request.pre.user._id.toString()) { + request.pre.admin.user.id !== `${user._id}`) { - return reply(Boom.conflict('Admin is already linked to another user. Unlink first.')); + throw Boom.conflict('Admin is linked to a user. Unlink first.'); } - reply(true); + return user; } } ] }, - handler: function (request, reply) { - - Async.auto({ - admin: function (done) { - - const id = request.params.id; - const update = { - $set: { - user: { - id: request.pre.user._id.toString(), - name: request.pre.user.username - } - } - }; + handler: async function (request, h) { - Admin.findByIdAndUpdate(id, update, done); - }, - user: function (done) { - - const id = request.pre.user._id; - const update = { - $set: { - 'roles.admin': { - id: request.pre.admin._id.toString(), - name: request.pre.admin.name.first + ' ' + request.pre.admin.name.last - } - } - }; - - User.findByIdAndUpdate(id, update, done); + const preUser = request.pre.user; + const preAdmin = request.pre.admin; + const adminUpdate = { + $set: { + user: { + id: `${preUser._id}`, + name: preUser.username + } } - }, (err, results) => { - - if (err) { - return reply(err); + }; + const userUpdate = { + $set: { + 'roles.admin': { + id: `${preAdmin._id}`, + name: `${preAdmin.name.first} ${preAdmin.name.last}` + } } + }; + const [admin] = await Promise.all([ + Admin.findByIdAndUpdate(preAdmin._id, adminUpdate), + User.findByIdAndUpdate(preUser._id, userUpdate) + ]); - reply(results.admin); - }); + return admin; } }); server.route({ method: 'DELETE', - path: '/admins/{id}/user', + path: '/api/admins/{id}/user', config: { auth: { strategy: 'simple', @@ -379,128 +337,71 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root'), + Preware.requireAdminGroup('root'), { assign: 'admin', - method: function (request, reply) { + method: async function (request, h) { - Admin.findById(request.params.id, (err, admin) => { + let admin = await Admin.findById(request.params.id); + + if (!admin) { + throw Boom.notFound('Admin not found.'); + } - if (err) { - return reply(err); - } + if (!admin.user || !admin.user.id) { + const update = { + $unset: { + user: undefined + } + }; - if (!admin) { - return reply(Boom.notFound('Document not found.')); - } + admin = await Admin.findByIdAndUpdate(request.params.id, update); - if (!admin.user || !admin.user.id) { - return reply(admin).takeover(); - } + return h.response(admin).takeover(); + } - reply(admin); - }); + return admin; } }, { assign: 'user', - method: function (request, reply) { - - User.findById(request.pre.admin.user.id, (err, user) => { - - if (err) { - return reply(err); - } - - if (!user) { - return reply(Boom.notFound('User document not found.')); - } - - reply(user); - }); - } - } - ] - }, - handler: function (request, reply) { - - Async.auto({ - admin: function (done) { - - const id = request.params.id; - const update = { - $unset: { - user: undefined - } - }; + method: async function (request, h) { - Admin.findByIdAndUpdate(id, update, done); - }, - user: function (done) { + const user = await User.findById(request.pre.admin.user.id); - const id = request.pre.user._id.toString(); - const update = { - $unset: { - 'roles.admin': undefined + if (!user) { + throw Boom.notFound('User not found.'); } - }; - - User.findByIdAndUpdate(id, update, done); - } - }, (err, results) => { - if (err) { - return reply(err); + return user; + } } - - reply(results.admin); - }); - } - }); - - - server.route({ - method: 'DELETE', - path: '/admins/{id}', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') ] }, - handler: function (request, reply) { - + handler: async function (request, h) { - Admin.findByIdAndDelete(request.params.id, (err, admin) => { - - if (err) { - return reply(err); + const adminUpdate = { + $unset: { + user: undefined } - - if (!admin) { - return reply(Boom.notFound('Document not found.')); + }; + const userUpdate = { + $unset: { + 'roles.admin': undefined } + }; + const [admin] = await Promise.all([ + Admin.findByIdAndUpdate(request.params.id, adminUpdate), + User.findByIdAndUpdate(request.pre.user._id, userUpdate) + ]); - reply({ message: 'Success.' }); - }); + return admin; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'admins' +module.exports = { + name: 'api-admins', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/api/auth-attempts.js b/server/api/auth-attempts.js deleted file mode 100644 index da9373f..0000000 --- a/server/api/auth-attempts.js +++ /dev/null @@ -1,129 +0,0 @@ -'use strict'; -const AuthPlugin = require('../auth'); -const Boom = require('boom'); -const Joi = require('joi'); - - -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const AuthAttempt = server.plugins['hapi-mongo-models'].AuthAttempt; - - - server.route({ - method: 'GET', - path: '/auth-attempts', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - validate: { - query: { - fields: Joi.string(), - sort: Joi.string().default('_id'), - limit: Joi.number().default(20), - page: Joi.number().default(1) - } - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] - }, - handler: function (request, reply) { - - const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; - const limit = request.query.limit; - const page = request.query.page; - - AuthAttempt.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); - } - }); - - - server.route({ - method: 'GET', - path: '/auth-attempts/{id}', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] - }, - handler: function (request, reply) { - - AuthAttempt.findById(request.params.id, (err, authAttempt) => { - - if (err) { - return reply(err); - } - - if (!authAttempt) { - return reply(Boom.notFound('Document not found.')); - } - - reply(authAttempt); - }); - } - }); - - - server.route({ - method: 'DELETE', - path: '/auth-attempts/{id}', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] - }, - handler: function (request, reply) { - - AuthAttempt.findByIdAndDelete(request.params.id, (err, authAttempt) => { - - if (err) { - return reply(err); - } - - if (!authAttempt) { - return reply(Boom.notFound('Document not found.')); - } - - reply({ message: 'Success.' }); - }); - } - }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); -}; - - -exports.register.attributes = { - name: 'auth-attempts' -}; diff --git a/server/api/contact.js b/server/api/contact.js index 21ff682..dc6b8b8 100644 --- a/server/api/contact.js +++ b/server/api/contact.js @@ -1,16 +1,14 @@ 'use strict'; const Config = require('../../config'); const Joi = require('joi'); +const Mailer = require('../mailer'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { +const register = function (server, serverOptions) { server.route({ method: 'POST', - path: '/contact', + path: '/api/contact', config: { validate: { payload: { @@ -20,9 +18,8 @@ internals.applyRoutes = function (server, next) { } } }, - handler: function (request, reply) { + handler: async function (request, h) { - const mailer = request.server.plugins.mailer; const emailOptions = { subject: Config.get('/projectName') + ' contact form', to: Config.get('/system/toAddress'), @@ -33,30 +30,16 @@ internals.applyRoutes = function (server, next) { }; const template = 'contact'; - mailer.sendEmail(emailOptions, template, request.payload, (err, info) => { - - if (err) { - return reply(err); - } + await Mailer.sendEmail(emailOptions, template, request.payload); - reply({ message: 'Success.' }); - }); + return { message: 'Success.' }; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency('mailer', internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'contact' +module.exports = { + name: 'api-contact', + dependencies: [], + register }; diff --git a/server/api/index.js b/server/api/index.js deleted file mode 100644 index 467cc8a..0000000 --- a/server/api/index.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - - -exports.register = function (server, options, next) { - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - reply({ message: 'Welcome to the plot device.' }); - } - }); - - - next(); -}; - - -exports.register.attributes = { - name: 'index' -}; diff --git a/server/api/login.js b/server/api/login.js index a4a20a3..f084389 100644 --- a/server/api/login.js +++ b/server/api/login.js @@ -1,24 +1,19 @@ 'use strict'; -const Async = require('async'); +const AuthAttempt = require('../models/auth-attempt'); const Bcrypt = require('bcrypt'); const Boom = require('boom'); const Config = require('../../config'); const Joi = require('joi'); +const Mailer = require('../mailer'); +const Session = require('../models/session'); +const User = require('../models/user'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const AuthAttempt = server.plugins['hapi-mongo-models'].AuthAttempt; - const Session = server.plugins['hapi-mongo-models'].Session; - const User = server.plugins['hapi-mongo-models'].User; - +const register = function (server, serverOptions) { server.route({ method: 'POST', - path: '/login', + path: '/api/login', config: { validate: { payload: { @@ -28,85 +23,55 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'abuseDetected', - method: function (request, reply) { + method: async function (request, h) { - const ip = request.info.remoteAddress; + const ip = request.remoteAddress; const username = request.payload.username; + const detected = await AuthAttempt.abuseDetected(ip, username); - AuthAttempt.abuseDetected(ip, username, (err, detected) => { - - if (err) { - return reply(err); - } - - if (detected) { - return reply(Boom.badRequest('Maximum number of auth attempts reached. Please try again later.')); - } + if (detected) { + throw Boom.badRequest('Maximum number of auth attempts reached.'); + } - reply(); - }); + return h.continue; } }, { assign: 'user', - method: function (request, reply) { + method: async function (request, h) { + const ip = request.remoteAddress; const username = request.payload.username; const password = request.payload.password; + const user = await User.findByCredentials(username, password); - User.findByCredentials(username, password, (err, user) => { - - if (err) { - return reply(err); - } - - reply(user); - }); - } - }, { - assign: 'logAttempt', - method: function (request, reply) { + if (!user) { + await AuthAttempt.create(ip, username); - if (request.pre.user) { - return reply(); + throw Boom.badRequest('Credentials are invalid or account is inactive.'); } - const ip = request.headers['x-forwarded-for'] || request.info.remoteAddress; - const username = request.payload.username; - const userAgent = request.headers['user-agent']; - - AuthAttempt.create(ip, username, userAgent, (err, authAttempt) => { - - if (err) { - return reply(err); - } - - return reply(Boom.badRequest('Username and password combination not found or account is inactive.')); - }); + return user; } }, { assign: 'session', - method: function (request, reply) { + method: async function (request, h) { + const userId = `${request.pre.user._id}`; + const ip = request.remoteAddress; const userAgent = request.headers['user-agent']; - const ip = request.headers['x-forwarded-for'] || request.info.remoteAddress; - - Session.create(request.pre.user._id.toString(), ip, userAgent, (err, session) => { - - if (err) { - return reply(err); - } - return reply(session); - }); + return await Session.create(userId, ip, userAgent); } }] }, - handler: function (request, reply) { + handler: function (request, h) { - const credentials = request.pre.session._id.toString() + ':' + request.pre.session.key; - const authHeader = 'Basic ' + new Buffer(credentials).toString('base64'); + const sessionId = request.pre.session._id; + const sessionKey = request.pre.session.key; + const credentials = `${sessionId}:${sessionKey}`; + const authHeader = `Basic ${new Buffer(credentials).toString('base64')}`; - reply({ + return { user: { _id: request.pre.user._id, username: request.pre.user.username, @@ -115,14 +80,14 @@ internals.applyRoutes = function (server, next) { }, session: request.pre.session, authHeader - }); + }; } }); server.route({ method: 'POST', - path: '/login/forgot', + path: '/api/login/forgot', config: { validate: { payload: { @@ -131,165 +96,118 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'user', - method: function (request, reply) { + method: async function (request, h) { - const conditions = { - email: request.payload.email - }; - - User.findOne(conditions, (err, user) => { + const query = { email: request.payload.email }; + const user = await User.findOne(query); - if (err) { - return reply(err); - } + if (!user) { + const response = h.response({ message: 'Success.' }); - if (!user) { - return reply({ message: 'Success.' }).takeover(); - } + return response.takeover(); + } - reply(user); - }); + return user; } }] }, - handler: function (request, reply) { + handler: async function (request, h) { - const mailer = request.server.plugins.mailer; + // set reset token - Async.auto({ - keyHash: function (done) { - - Session.generateKeyHash(done); - }, - user: ['keyHash', function (results, done) { - - const id = request.pre.user._id.toString(); - const update = { - $set: { - resetPassword: { - token: results.keyHash.hash, - expires: Date.now() + 10000000 - } - } - }; + const keyHash = await Session.generateKeyHash(); + const update = { + $set: { + resetPassword: { + token: keyHash.hash, + expires: Date.now() + 10000000 + } + } + }; - User.findByIdAndUpdate(id, update, done); - }], - email: ['user', function (results, done) { + await User.findByIdAndUpdate(request.pre.user._id, update); - const emailOptions = { - subject: 'Reset your ' + Config.get('/projectName') + ' password', - to: request.payload.email - }; - const template = 'forgot-password'; - const context = { - key: results.keyHash.key - }; + // send email - mailer.sendEmail(emailOptions, template, context, done); - }] - }, (err, results) => { + const projectName = Config.get('/projectName'); + const emailOptions = { + subject: `Reset your ${projectName} password`, + to: request.payload.email + }; + const template = 'forgot-password'; + const context = { key: keyHash.key }; - if (err) { - return reply(err); - } + await Mailer.sendEmail(emailOptions, template, context); - reply({ message: 'Success.' }); - }); + return { message: 'Success.' }; } }); server.route({ method: 'POST', - path: '/login/reset', + path: '/api/login/reset', config: { validate: { payload: { - key: Joi.string().required(), email: Joi.string().email().lowercase().required(), + key: Joi.string().required(), password: Joi.string().required() } }, pre: [{ assign: 'user', - method: function (request, reply) { + method: async function (request, h) { - const conditions = { + const query = { email: request.payload.email, 'resetPassword.expires': { $gt: Date.now() } }; + const user = await User.findOne(query); - User.findOne(conditions, (err, user) => { - - if (err) { - return reply(err); - } - - if (!user) { - return reply(Boom.badRequest('Invalid email or key.')); - } + if (!user) { + throw Boom.badRequest('Invalid email or key.'); + } - reply(user); - }); + return user; } }] }, - handler: function (request, reply) { + handler: async function (request, h) { - Async.auto({ - keyMatch: function (done) { + // validate reset token - const key = request.payload.key; - const token = request.pre.user.resetPassword.token; - Bcrypt.compare(key, token, done); - }, - passwordHash: ['keyMatch', function (results, done) { + const key = request.payload.key; + const token = request.pre.user.resetPassword.token; + const keyMatch = await Bcrypt.compare(key, token); - if (!results.keyMatch) { - return reply(Boom.badRequest('Invalid email or key.')); - } + if (!keyMatch) { + throw Boom.badRequest('Invalid email or key.'); + } - User.generatePasswordHash(request.payload.password, done); - }], - user: ['passwordHash', function (results, done) { - - const id = request.pre.user._id.toString(); - const update = { - $set: { - password: results.passwordHash.hash - }, - $unset: { - resetPassword: undefined - } - }; - - User.findByIdAndUpdate(id, update, done); - }] - }, (err, results) => { + // update user - if (err) { - return reply(err); + const password = request.payload.password; + const passwordHash = await User.generatePasswordHash(password); + const update = { + $set: { + password: passwordHash.hash + }, + $unset: { + resetPassword: undefined } + }; - reply({ message: 'Success.' }); - }); + await User.findByIdAndUpdate(request.pre.user._id, update); + + return { message: 'Success.' }; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['mailer', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'login' +module.exports = { + name: 'api-login', + dependencies: ['hapi-mongo-models', 'hapi-remote-address'], + register }; diff --git a/server/api/logout.js b/server/api/logout.js index 72c265b..79fa853 100644 --- a/server/api/logout.js +++ b/server/api/logout.js @@ -1,57 +1,36 @@ 'use strict'; -const Boom = require('boom'); +const Session = require('../models/session'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Session = server.plugins['hapi-mongo-models'].Session; - +const register = function (server, serverOptions) { server.route({ method: 'DELETE', - path: '/logout', + path: '/api/logout', config: { auth: { mode: 'try', strategy: 'simple' } }, - handler: function (request, reply) { + handler: function (request, h) { - const credentials = request.auth.credentials || { session: {} }; - const session = credentials.session || {}; + const credentials = request.auth.credentials; - Session.findByIdAndDelete(session._id, (err, sessionDoc) => { - - if (err) { - return reply(err); - } + if (!credentials) { + return { message: 'Success.' }; + } - if (!sessionDoc) { - return reply(Boom.notFound('Document not found.')); - } + Session.findByIdAndDelete(credentials.session._id); - reply({ message: 'Success.' }); - }); + return { message: 'Success.' }; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'logout' +module.exports = { + name: 'api-logout', + dependencies: ['auth', 'hapi-mongo-models'], + register }; diff --git a/server/api/main.js b/server/api/main.js new file mode 100644 index 0000000..edaef45 --- /dev/null +++ b/server/api/main.js @@ -0,0 +1,22 @@ +'use strict'; + + +const register = function (server, serverOptions) { + + server.route({ + method: 'GET', + path: '/api', + handler: function (request, h) { + + return { + message: 'Welcome to the API.' + }; + } + }); +}; + + +module.exports = { + name: 'api-main', + register +}; diff --git a/server/api/sessions.js b/server/api/sessions.js index 670381a..86f64a0 100644 --- a/server/api/sessions.js +++ b/server/api/sessions.js @@ -1,20 +1,15 @@ 'use strict'; -const AuthPlugin = require('../auth'); const Boom = require('boom'); const Joi = require('joi'); +const Preware = require('../preware'); +const Session = require('../models/session'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Session = server.plugins['hapi-mongo-models'].Session; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/sessions', + path: '/api/sessions', config: { auth: { strategy: 'simple', @@ -22,185 +17,132 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: Session.sortAdapter(request.query.sort) + }; - Session.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); + return await Session.pagedFind(query, limit, page, options); } }); server.route({ method: 'GET', - path: '/sessions/my', + path: '/api/sessions/{id}', config: { auth: { strategy: 'simple', - scope: ['admin', 'account'] - } + scope: 'admin' + }, + pre: [ + Preware.requireAdminGroup('root') + ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.auth.credentials.user._id.toString(); + const session = await Session.findById(request.params.id); - Session.find({ userId: id }, (err, session) => { - - if (err) { - return reply(err); - } - - if (!session) { - return reply(Boom.notFound('Document not found.')); - } + if (!session) { + throw Boom.notFound('Session not found.'); + } - reply(session); - }); + return session; } }); server.route({ - method: 'GET', - path: '/sessions/{id}', + method: 'DELETE', + path: '/api/sessions/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { - - Session.findById(request.params.id, (err, session) => { + handler: async function (request, h) { - if (err) { - return reply(err); - } + const session = await Session.findByIdAndDelete(request.params.id); - if (!session) { - return reply(Boom.notFound('Document not found.')); - } + if (!session) { + throw Boom.notFound('Session not found.'); + } - reply(session); - }); + return { message: 'Success.' }; } }); server.route({ - method: 'DELETE', - path: '/sessions/my/{id}', + method: 'GET', + path: '/api/sessions/my', config: { auth: { - strategy: 'simple' - }, - pre: [{ - assign: 'current', - method: function (request, reply) { - - const currentSession = request.auth.credentials.session._id.toString(); - - if (currentSession === request.params.id) { - - return reply(Boom.badRequest('Unable to close your current session. You can use logout instead.')); - } - - reply(true); - } - }] + strategy: 'simple', + scope: ['admin', 'account'] + } }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.params.id; - const userId = request.auth.credentials.user._id.toString(); - - const filter = { - _id: Session.ObjectID(id), - userId + const query = { + userId: `${request.auth.credentials.user._id}` }; - Session.findOneAndDelete(filter, (err, session) => { - - if (err) { - return reply(err); - } - - if (!session) { - return reply(Boom.notFound('Document not found.')); - } - - reply({ message: 'Success.' }); - }); + return await Session.find(query); } }); server.route({ method: 'DELETE', - path: '/sessions/{id}', + path: '/api/sessions/my/{id}', config: { auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] + strategy: 'simple' + } }, - handler: function (request, reply) { + handler: async function (request, h) { - Session.findByIdAndDelete(request.params.id, (err, session) => { + const currentSession = `${request.auth.credentials.session._id}`; - if (err) { - return reply(err); - } + if (currentSession === request.params.id) { + throw Boom.badRequest( + 'Cannot destroy your current session. Also see `/api/logout`.' + ); + } - if (!session) { - return reply(Boom.notFound('Document not found.')); - } + const query = { + _id: Session.ObjectID(request.params.id), + userId: `${request.auth.credentials.user._id}` + }; + + await Session.findOneAndDelete(query); - reply({ message: 'Success.' }); - }); + return { message: 'Success.' }; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'sessions' +module.exports = { + name: 'api-sessions', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/api/signup.js b/server/api/signup.js index f211e1e..fc09e63 100644 --- a/server/api/signup.js +++ b/server/api/signup.js @@ -1,23 +1,18 @@ 'use strict'; -const Async = require('async'); +const Account = require('../models/account'); const Boom = require('boom'); const Config = require('../../config'); const Joi = require('joi'); +const Mailer = require('../mailer'); +const Session = require('../models/session'); +const User = require('../models/user'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Account = server.plugins['hapi-mongo-models'].Account; - const Session = server.plugins['hapi-mongo-models'].Session; - const User = server.plugins['hapi-mongo-models'].User; - +const register = function (server, serverOptions) { server.route({ method: 'POST', - path: '/signup', + path: '/api/signup', config: { validate: { payload: { @@ -29,161 +24,93 @@ internals.applyRoutes = function (server, next) { }, pre: [{ assign: 'usernameCheck', - method: function (request, reply) { - - const conditions = { - username: request.payload.username - }; + method: async function (request, h) { - User.findOne(conditions, (err, user) => { + const user = await User.findByUsername(request.payload.username); - if (err) { - return reply(err); - } + if (user) { + throw Boom.conflict('Username already in use.'); + } - if (user) { - return reply(Boom.conflict('Username already in use.')); - } - - reply(true); - }); + return h.continue; } }, { assign: 'emailCheck', - method: function (request, reply) { - - const conditions = { - email: request.payload.email - }; - - User.findOne(conditions, (err, user) => { + method: async function (request, h) { - if (err) { - return reply(err); - } + const user = await User.findByEmail(request.payload.email); - if (user) { - return reply(Boom.conflict('Email already in use.')); - } + if (user) { + throw Boom.conflict('Email already in use.'); + } - reply(true); - }); + return h.continue; } }] }, - handler: function (request, reply) { - - const mailer = request.server.plugins.mailer; - - Async.auto({ - user: function (done) { - - const username = request.payload.username; - const password = request.payload.password; - const email = request.payload.email; - - User.create(username, password, email, done); - }, - account: ['user', function (results, done) { - - const name = request.payload.name; - - Account.create(name, done); - }], - linkUser: ['account', function (results, done) { - - const id = results.account._id.toString(); - const update = { - $set: { - user: { - id: results.user._id.toString(), - name: results.user.username - } - } - }; - - Account.findByIdAndUpdate(id, update, done); - }], - linkAccount: ['account', function (results, done) { - - const id = results.user._id.toString(); - const update = { - $set: { - roles: { - account: { - id: results.account._id.toString(), - name: results.account.name.first + ' ' + results.account.name.last - } - } - } - }; - - User.findByIdAndUpdate(id, update, done); - }], - welcome: ['linkUser', 'linkAccount', function (results, done) { - - const emailOptions = { - subject: 'Your ' + Config.get('/projectName') + ' account', - to: { - name: request.payload.name, - address: request.payload.email - } - }; - const template = 'welcome'; - - mailer.sendEmail(emailOptions, template, request.payload, (err) => { - - if (err) { - console.warn('sending welcome email failed:', err.stack); - } - }); - - done(); - }], - session: ['linkUser', 'linkAccount', function (results, done) { - - const userAgent = request.headers['user-agent']; - const ip = request.headers['x-forwarded-for'] || request.info.remoteAddress; - - Session.create(results.user._id.toString(), ip, userAgent, done); - }] - }, (err, results) => { - - if (err) { - return reply(err); + handler: async function (request, h) { + + // create and link account and user documents + + let [account, user] = await Promise.all([ + Account.create(request.payload.name), + User.create( + request.payload.username, + request.payload.password, + request.payload.email + ) + ]); + + [account, user] = await Promise.all([ + account.linkUser(`${user._id}`, user.username), + user.linkAccount(`${account._id}`, `${account.name.first} ${account.name.last}`) + ]); + + // send welcome email + + const emailOptions = { + subject: `Your ${Config.get('/projectName')} account`, + to: { + name: request.payload.name, + address: request.payload.email } + }; - const user = results.linkAccount; - const credentials = results.session._id + ':' + results.session.key; - const authHeader = 'Basic ' + new Buffer(credentials).toString('base64'); - - reply({ - user: { - _id: user._id, - username: user.username, - email: user.email, - roles: user.roles - }, - session: results.session, - authHeader - }); - }); - } - }); - + try { + await Mailer.sendEmail(emailOptions, 'welcome', request.payload); + } + catch (err) { + request.log(['mailer', 'error'], err); + } - next(); -}; + // create session + const userAgent = request.headers['user-agent']; + const ip = request.remoteAddress; + const session = await Session.create(`${user._id}`, ip, userAgent); -exports.register = function (server, options, next) { + // create auth header - server.dependency(['mailer', 'hapi-mongo-models'], internals.applyRoutes); + const credentials = `${session._id}:${session.key}`; + const authHeader = `Basic ${new Buffer(credentials).toString('base64')}`; - next(); + return { + user: { + _id: user._id, + username: user.username, + email: user.email, + roles: user.roles + }, + session, + authHeader + }; + } + }); }; -exports.register.attributes = { - name: 'signup' +module.exports = { + name: 'api-signup', + dependencies: ['hapi-remote-address', 'hapi-mongo-models'], + register }; diff --git a/server/api/statuses.js b/server/api/statuses.js index f9d48a7..ac737ae 100644 --- a/server/api/statuses.js +++ b/server/api/statuses.js @@ -1,20 +1,15 @@ 'use strict'; -const AuthPlugin = require('../auth'); const Boom = require('boom'); const Joi = require('joi'); +const Preware = require('../preware'); +const Status = require('../models/status'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const Status = server.plugins['hapi-mongo-models'].Status; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/statuses', + path: '/api/statuses', config: { auth: { strategy: 'simple', @@ -22,104 +17,82 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: Status.sortAdapter(request.query.sort) + }; - Status.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); + return await Status.pagedFind(query, limit, page, options); } }); server.route({ - method: 'GET', - path: '/statuses/{id}', + method: 'POST', + path: '/api/statuses', config: { auth: { strategy: 'simple', scope: 'admin' }, + validate: { + payload: { + name: Joi.string().required(), + pivot: Joi.string().required() + } + }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { - - Status.findById(request.params.id, (err, status) => { - - if (err) { - return reply(err); - } - - if (!status) { - return reply(Boom.notFound('Document not found.')); - } + handler: async function (request, h) { - reply(status); - }); + return await Status.create(request.payload.pivot, request.payload.name); } }); server.route({ - method: 'POST', - path: '/statuses', + method: 'GET', + path: '/api/statuses/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, - validate: { - payload: { - pivot: Joi.string().required(), - name: Joi.string().required() - } - }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const pivot = request.payload.pivot; - const name = request.payload.name; + const status = await Status.findById(request.params.id); - Status.create(pivot, name, (err, status) => { + if (!status) { + throw Boom.notFound('Status not found.'); + } - if (err) { - return reply(err); - } - - reply(status); - }); + return status; } }); server.route({ method: 'PUT', - path: '/statuses/{id}', + path: '/api/statuses/{id}', config: { auth: { strategy: 'simple', @@ -131,10 +104,10 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const id = request.params.id; const update = { @@ -142,65 +115,45 @@ internals.applyRoutes = function (server, next) { name: request.payload.name } }; + const status = await Status.findByIdAndUpdate(id, update); - Status.findByIdAndUpdate(id, update, (err, status) => { - - if (err) { - return reply(err); - } - - if (!status) { - return reply(Boom.notFound('Document not found.')); - } + if (!status) { + throw Boom.notFound('Status not found.'); + } - reply(status); - }); + return status; } }); server.route({ method: 'DELETE', - path: '/statuses/{id}', + path: '/api/statuses/{id}', config: { auth: { strategy: 'simple', scope: 'admin' }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - Status.findByIdAndDelete(request.params.id, (err, status) => { + const status = await Status.findByIdAndDelete(request.params.id); - if (err) { - return reply(err); - } - - if (!status) { - return reply(Boom.notFound('Document not found.')); - } + if (!status) { + throw Boom.notFound('Status not found.'); + } - reply({ message: 'Success.' }); - }); + return { message: 'Success.' }; } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'statuses' +module.exports = { + name: 'api-statuses', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/api/users.js b/server/api/users.js index 75160a4..51346b0 100644 --- a/server/api/users.js +++ b/server/api/users.js @@ -1,23 +1,17 @@ 'use strict'; -const AuthPlugin = require('../auth'); -const Async = require('async'); +const Admin = require('../models/admin'); +const Account = require('../models/account'); const Boom = require('boom'); const Joi = require('joi'); +const Preware = require('../preware'); +const User = require('../models/user'); -const internals = {}; - - -internals.applyRoutes = function (server, next) { - - const User = server.plugins['hapi-mongo-models'].User; - const Account = server.plugins['hapi-mongo-models'].Account; - const Admin = server.plugins['hapi-mongo-models'].Admin; - +const register = function (server, serverOptions) { server.route({ method: 'GET', - path: '/users', + path: '/api/users', config: { auth: { strategy: 'simple', @@ -25,111 +19,32 @@ internals.applyRoutes = function (server, next) { }, validate: { query: { - username: Joi.string().token().lowercase(), - isActive: Joi.string(), - role: Joi.string(), - fields: Joi.string(), sort: Joi.string().default('_id'), limit: Joi.number().default(20), page: Joi.number().default(1) } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { const query = {}; - if (request.query.username) { - query.username = new RegExp('^.*?' + request.query.username + '.*$', 'i'); - } - if (request.query.isActive) { - query.isActive = request.query.isActive === 'true'; - } - if (request.query.role) { - query['roles.' + request.query.role] = { $exists: true }; - } - const fields = request.query.fields; - const sort = request.query.sort; const limit = request.query.limit; const page = request.query.page; + const options = { + sort: User.sortAdapter(request.query.sort) + }; - User.pagedFind(query, fields, sort, limit, page, (err, results) => { - - if (err) { - return reply(err); - } - - reply(results); - }); - } - }); - - - server.route({ - method: 'GET', - path: '/users/{id}', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] - }, - handler: function (request, reply) { - - User.findById(request.params.id, (err, user) => { - - if (err) { - return reply(err); - } - - if (!user) { - return reply(Boom.notFound('Document not found.')); - } - - reply(user); - }); - } - }); - - - server.route({ - method: 'GET', - path: '/users/my', - config: { - auth: { - strategy: 'simple', - scope: ['admin', 'account'] - } - }, - handler: function (request, reply) { - - const id = request.auth.credentials.user._id.toString(); - const fields = User.fieldsAdapter('username email roles'); - - User.findById(id, fields, (err, user) => { - - if (err) { - return reply(err); - } - - if (!user) { - return reply(Boom.notFound('Document not found. That is strange.')); - } - - reply(user); - }); + return await User.pagedFind(query, page, limit, options); } }); server.route({ method: 'POST', - path: '/users', + path: '/api/users', config: { auth: { strategy: 'simple', @@ -143,73 +58,73 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root'), + Preware.requireAdminGroup('root'), { assign: 'usernameCheck', - method: function (request, reply) { - - const conditions = { - username: request.payload.username - }; + method: async function (request, h) { - User.findOne(conditions, (err, user) => { + const user = await User.findByUsername(request.payload.username); - if (err) { - return reply(err); - } + if (user) { + throw Boom.conflict('Username already in use.'); + } - if (user) { - return reply(Boom.conflict('Username already in use.')); - } - - reply(true); - }); + return h.continue; } }, { assign: 'emailCheck', - method: function (request, reply) { - - const conditions = { - email: request.payload.email - }; - - User.findOne(conditions, (err, user) => { + method: async function (request, h) { - if (err) { - return reply(err); - } + const user = await User.findByEmail(request.payload.email); - if (user) { - return reply(Boom.conflict('Email already in use.')); - } + if (user) { + throw Boom.conflict('Email already in use.'); + } - reply(true); - }); + return h.continue; } } ] }, - handler: function (request, reply) { + handler: async function (request, h) { const username = request.payload.username; const password = request.payload.password; const email = request.payload.email; - User.create(username, password, email, (err, user) => { + return await User.create(username, password, email); + } + }); - if (err) { - return reply(err); - } - reply(user); - }); + server.route({ + method: 'GET', + path: '/api/users/{id}', + config: { + auth: { + strategy: 'simple', + scope: 'admin' + }, + pre: [ + Preware.requireAdminGroup('root') + ] + }, + handler: async function (request, h) { + + const user = await User.findById(request.params.id); + + if (!user) { + throw Boom.notFound('User not found.'); + } + + return user; } }); server.route({ method: 'PUT', - path: '/users/{id}', + path: '/api/users/{id}', config: { auth: { strategy: 'simple', @@ -226,218 +141,108 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root'), + Preware.requireAdminGroup('root'), { assign: 'usernameCheck', - method: function (request, reply) { + method: async function (request, h) { const conditions = { username: request.payload.username, _id: { $ne: User._idClass(request.params.id) } }; + const user = await User.findOne(conditions); - User.findOne(conditions, (err, user) => { + if (user) { + throw Boom.conflict('Username already in use.'); + } - if (err) { - return reply(err); - } - - if (user) { - return reply(Boom.conflict('Username already in use.')); - } - - reply(true); - }); + return h.continue; } }, { assign: 'emailCheck', - method: function (request, reply) { + method: async function (request, h) { const conditions = { email: request.payload.email, _id: { $ne: User._idClass(request.params.id) } }; + const user = await User.findOne(conditions); - User.findOne(conditions, (err, user) => { - - if (err) { - return reply(err); - } + if (user) { + throw Boom.conflict('Email already in use.'); + } - if (user) { - return reply(Boom.conflict('Email already in use.')); - } - - reply(true); - }); + return h.continue; } } ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.params.id; - const update = { + const updateUser = { $set: { isActive: request.payload.isActive, username: request.payload.username, email: request.payload.email } }; - - const filterById = { + const queryByUserId = { 'user.id': request.params.id }; - - const updateReference = { + const updateRole = { $set: { 'user.name': request.payload.username } }; + const user = await User.findByIdAndUpdate(request.params.id, updateUser); - Async.auto({ - user: function (done) { - - User.findByIdAndUpdate(id, update, done); - }, - account: function (done) { - - Account.findOneAndUpdate(filterById, updateReference, done); - }, - admin: function (done) { - - Admin.findOneAndUpdate(filterById, updateReference, done); - } - }, (err, results) => { - - if (err) { - return reply(err); - } + if (!user) { + throw Boom.notFound('User not found.'); + } - if (!results.user) { - return reply(Boom.notFound('Document not found.')); - } + await Promise.all([ + Account.findOneAndUpdate(queryByUserId, updateRole), + Admin.findOneAndUpdate(queryByUserId, updateRole) + ]); - reply(results.user); - }); + return user; } }); server.route({ - method: 'PUT', - path: '/users/my', + method: 'DELETE', + path: '/api/users/{id}', config: { auth: { strategy: 'simple', - scope: ['admin', 'account'] + scope: 'admin' }, validate: { - payload: { - username: Joi.string().token().lowercase().required(), - email: Joi.string().email().lowercase().required() + params: { + id: Joi.string().invalid('000000000000000000000000') } }, pre: [ - AuthPlugin.preware.ensureNotRoot, - { - assign: 'usernameCheck', - method: function (request, reply) { - - const conditions = { - username: request.payload.username, - _id: { $ne: request.auth.credentials.user._id } - }; - - User.findOne(conditions, (err, user) => { - - if (err) { - return reply(err); - } - - if (user) { - return reply(Boom.conflict('Username already in use.')); - } - - reply(true); - }); - } - }, { - assign: 'emailCheck', - method: function (request, reply) { - - const conditions = { - email: request.payload.email, - _id: { $ne: request.auth.credentials.user._id } - }; - - User.findOne(conditions, (err, user) => { - - if (err) { - return reply(err); - } - - if (user) { - return reply(Boom.conflict('Email already in use.')); - } - - reply(true); - }); - } - } + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.auth.credentials.user._id.toString(); - const update = { - $set: { - username: request.payload.username, - email: request.payload.email - } - }; - const findOptions = { - fields: User.fieldsAdapter('username email roles') - }; - - const filterById = { - 'user.id': id - }; - - const updateReference = { - $set: { - 'user.name': request.payload.username - } - }; + const user = await User.findByIdAndDelete(request.params.id); - Async.auto({ - user: function (done) { - - User.findByIdAndUpdate(id, update, findOptions, done); - }, - account: function (done) { - - Account.findOneAndUpdate(filterById, updateReference, done); - }, - admin: function (done) { - - Admin.findOneAndUpdate(filterById, updateReference, done); - } - }, (err, results) => { - - if (err) { - return reply(err); - } + if (!user) { + throw Boom.notFound('User not found.'); + } - reply(results.user); - }); + return { message: 'Success.' }; } }); server.route({ method: 'PUT', - path: '/users/{id}/password', + path: '/api/users/{id}/password', config: { auth: { strategy: 'simple', @@ -452,47 +257,50 @@ internals.applyRoutes = function (server, next) { } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root'), - { - assign: 'password', - method: function (request, reply) { - - User.generatePasswordHash(request.payload.password, (err, hash) => { - - if (err) { - return reply(err); - } - - reply(hash); - }); - } - } + Preware.requireAdminGroup('root') ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.params.id; + const password = await User.generatePasswordHash(request.payload.password); const update = { $set: { - password: request.pre.password.hash + password: password.hash } }; + const user = await User.findByIdAndUpdate(request.params.id, update); - User.findByIdAndUpdate(id, update, (err, user) => { + if (!user) { + throw Boom.notFound('User not found.'); + } + + return user; + } + }); - if (err) { - return reply(err); - } - reply(user); - }); + server.route({ + method: 'GET', + path: '/api/users/my', + config: { + auth: { + strategy: 'simple', + scope: ['admin', 'account'] + } + }, + handler: async function (request, h) { + + const id = request.auth.credentials.user._id; + const fields = User.fieldsAdapter('username email roles'); + + return await User.findById(id, fields); } }); server.route({ method: 'PUT', - path: '/users/my/password', + path: '/api/users/my', config: { auth: { strategy: 'simple', @@ -500,98 +308,116 @@ internals.applyRoutes = function (server, next) { }, validate: { payload: { - password: Joi.string().required() + username: Joi.string().token().lowercase().required(), + email: Joi.string().email().lowercase().required() } }, pre: [ - AuthPlugin.preware.ensureNotRoot, + Preware.requireNotRootUser, { - assign: 'password', - method: function (request, reply) { + assign: 'usernameCheck', + method: async function (request, h) { + + const conditions = { + username: request.payload.username, + _id: { $ne: request.auth.credentials.user._id } + }; + const user = await User.findOne(conditions); + + if (user) { + throw Boom.conflict('Username already in use.'); + } + + return h.continue; + } + }, { + assign: 'emailCheck', + method: async function (request, h) { - User.generatePasswordHash(request.payload.password, (err, hash) => { + const conditions = { + email: request.payload.email, + _id: { $ne: request.auth.credentials.user._id } + }; + const user = await User.findOne(conditions); - if (err) { - return reply(err); - } + if (user) { + throw Boom.conflict('Email already in use.'); + } - reply(hash); - }); + return h.continue; } } ] }, - handler: function (request, reply) { + handler: async function (request, h) { - const id = request.auth.credentials.user._id.toString(); - const update = { + const userId = `${request.auth.credentials.user._id}`; + const updateUser = { $set: { - password: request.pre.password.hash + username: request.payload.username, + email: request.payload.email } }; const findOptions = { - fields: User.fieldsAdapter('username email') + fields: User.fieldsAdapter('username email roles') }; - - User.findByIdAndUpdate(id, update, findOptions, (err, user) => { - - if (err) { - return reply(err); + const queryByUserId = { + 'user.id': userId + }; + const updateRole = { + $set: { + 'user.name': request.payload.username } + }; + const [user] = await Promise.all([ + User.findByIdAndUpdate(userId, updateUser, findOptions), + Account.findOneAndUpdate(queryByUserId, updateRole), + Admin.findOneAndUpdate(queryByUserId, updateRole) + ]); - reply(user); - }); + return user; } }); server.route({ - method: 'DELETE', - path: '/users/{id}', + method: 'PUT', + path: '/api/users/my/password', config: { auth: { strategy: 'simple', - scope: 'admin' + scope: ['admin', 'account'] }, validate: { - params: { - id: Joi.string().invalid('000000000000000000000000') + payload: { + password: Joi.string().required() } }, pre: [ - AuthPlugin.preware.ensureAdminGroup('root') + Preware.requireNotRootUser ] }, - handler: function (request, reply) { - - User.findByIdAndDelete(request.params.id, (err, user) => { - - if (err) { - return reply(err); - } + handler: async function (request, h) { - if (!user) { - return reply(Boom.notFound('Document not found.')); + const userId = `${request.auth.credentials.user._id}`; + const password = await User.generatePasswordHash(request.payload.password); + const update = { + $set: { + password: password.hash } + }; + const findOptions = { + fields: User.fieldsAdapter('username email') + }; - reply({ message: 'Success.' }); - }); + return await User.findByIdAndUpdate(userId, update, findOptions); } }); - - - next(); -}; - - -exports.register = function (server, options, next) { - - server.dependency(['auth', 'hapi-mongo-models'], internals.applyRoutes); - - next(); }; -exports.register.attributes = { - name: 'users' +module.exports = { + name: 'api-users', + dependencies: ['auth', 'hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/auth.js b/server/auth.js index c0dbba0..3ac8420 100644 --- a/server/auth.js +++ b/server/auth.js @@ -1,133 +1,47 @@ 'use strict'; -const Async = require('async'); -const Boom = require('boom'); +const Session = require('./models/session'); +const User = require('./models/user'); -const internals = {}; - - -internals.applyStrategy = function (server, next) { - - const Session = server.plugins['hapi-mongo-models'].Session; - const User = server.plugins['hapi-mongo-models'].User; +const register = function (server, options) { server.auth.strategy('simple', 'basic', { - validateFunc: function (request, username, password, callback) { - - Async.auto({ - session: function (done) { - - Session.findByCredentials(username, password, done); - }, - user: ['session', function (results, done) { - - if (!results.session) { - return done(); - } - - User.findById(results.session.userId, done); - }], - roles: ['user', function (results, done) { - - if (!results.user) { - return done(); - } - - results.user.hydrateRoles(done); - }], - scope: ['user', function (results, done) { - - if (!results.user || !results.user.roles) { - return done(); - } - - done(null, Object.keys(results.user.roles)); - }], - updateSession: ['scope', function (results, done) { - - if (!results.scope) { - return done(); - } - - const update = { - $set: { - lastActive: new Date() - } - }; - - Session.findByIdAndUpdate(results.session._id.toString(), update, done); - }] - }, (err, results) => { - - if (err) { - return callback(err); - } - - if (!results.session) { - return callback(null, false); - } - - callback(null, Boolean(results.user), results); - }); - } - }); - - - next(); -}; - + validate: async function (request, sessionId, key, h) { -internals.preware = { - ensureNotRoot: { - assign: 'ensureNotRoot', - method: function (request, reply) { + const session = await Session.findByCredentials(sessionId, key); - if (request.auth.credentials.user.username === 'root') { - const message = 'Not permitted for root user.'; - - return reply(Boom.badRequest(message)); + if (!session) { + return { isValid: false }; } - reply(); - } - }, - ensureAdminGroup: function (groups) { - - return { - assign: 'ensureAdminGroup', - method: function (request, reply) { - - if (Object.prototype.toString.call(groups) !== '[object Array]') { - groups = [groups]; - } + session.updateLastActive(); - const groupFound = groups.some((group) => { + const user = await User.findById(session.userId); - return request.auth.credentials.roles.admin.isMemberOf(group); - }); - - if (!groupFound) { - return reply(Boom.notFound('Permission denied to this resource.')); - } - - reply(); + if (!user) { + return { isValid: false }; } - }; - } -}; + if (!user.isActive) { + return { isValid: false }; + } -exports.register = function (server, options, next) { - - server.dependency('hapi-mongo-models', internals.applyStrategy); + const roles = await user.hydrateRoles(); + const credentials = { + scope: Object.keys(user.roles), + roles, + session, + user + }; - next(); + return { credentials, isValid: true }; + } + }); }; -exports.preware = internals.preware; - - -exports.register.attributes = { - name: 'auth' +module.exports = { + name: 'auth', + dependencies: ['hapi-auth-basic', 'hapi-mongo-models'], + register }; diff --git a/server/mailer.js b/server/mailer.js index d362d85..38f460e 100644 --- a/server/mailer.js +++ b/server/mailer.js @@ -5,69 +5,47 @@ const Handlebars = require('handlebars'); const Hoek = require('hoek'); const Markdown = require('nodemailer-markdown').markdown; const Nodemailer = require('nodemailer'); +const Path = require('path'); +const Util = require('util'); -const internals = {}; +const readFile = Util.promisify(Fs.readFile); -internals.transport = Nodemailer.createTransport(Config.get('/nodemailer')); -internals.transport.use('compile', Markdown({ useEmbeddedImages: true })); +class Mailer { + static async renderTemplate(signature, context) { + if (this.templateCache[signature]) { + return this.templateCache[signature](context); + } -internals.templateCache = {}; - + const filePath = Path.resolve(__dirname, `./emails/${signature}.hbs.md`); + const options = { encoding: 'utf-8' }; + const source = await readFile(filePath, options); -internals.renderTemplate = function (signature, context, callback) { + this.templateCache[signature] = Handlebars.compile(source); - if (internals.templateCache[signature]) { - return callback(null, internals.templateCache[signature](context)); + return this.templateCache[signature](context); } - const filePath = __dirname + '/emails/' + signature + '.hbs.md'; - const options = { encoding: 'utf-8' }; - - Fs.readFile(filePath, options, (err, source) => { - - if (err) { - return callback(err); - } - - internals.templateCache[signature] = Handlebars.compile(source); - callback(null, internals.templateCache[signature](context)); - }); -}; - -internals.sendEmail = function (options, template, context, callback) { + static async sendEmail(options, template, context) { - internals.renderTemplate(template, context, (err, content) => { - - if (err) { - return callback(err); - } + const content = await this.renderTemplate(template, context); options = Hoek.applyToDefaults(options, { from: Config.get('/system/fromAddress'), markdown: content }); - internals.transport.sendMail(options, callback); - }); -}; - - -exports.register = function (server, options, next) { - - server.expose('sendEmail', internals.sendEmail); - server.expose('transport', internals.transport); - - next(); -}; + return await this.transport.sendMail(options); + } +} -exports.sendEmail = internals.sendEmail; +Mailer.templateCache = {}; +Mailer.transport = Nodemailer.createTransport(Config.get('/nodemailer')); +Mailer.transport.use('compile', Markdown({ useEmbeddedImages: true })); -exports.register.attributes = { - name: 'mailer' -}; +module.exports = Mailer; diff --git a/server/models/account.js b/server/models/account.js index b056cae..174deb9 100644 --- a/server/models/account.js +++ b/server/models/account.js @@ -1,70 +1,99 @@ 'use strict'; +const Assert = require('assert'); const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewArray = require('joistick/new-array'); +const NewDate = require('joistick/new-date'); const NoteEntry = require('./note-entry'); const StatusEntry = require('./status-entry'); -class Account extends MongoModels { - static create(name, callback) { - - const nameParts = name.trim().split(/\s/); +const schema = Joi.object({ + _id: Joi.object(), + name: Joi.object({ + first: Joi.string().required(), + middle: Joi.string().allow(''), + last: Joi.string().allow('') + }), + notes: Joi.array().items(NoteEntry.schema) + .default(NewArray(), 'array of notes'), + status: Joi.object({ + current: StatusEntry.schema, + log: Joi.array().items(StatusEntry.schema) + .default(NewArray(), 'array of statuses') + }).default(), + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + user: Joi.object({ + id: Joi.string().required(), + name: Joi.string().lowercase().required() + }) +}); - const document = { - name: { - first: nameParts.shift(), - middle: nameParts.length > 1 ? nameParts.shift() : undefined, - last: nameParts.join(' ') - }, - timeCreated: new Date() - }; - this.insertOne(document, (err, docs) => { +class Account extends MongoModels { + static async create(name) { - if (err) { - return callback(err); - } + Assert.ok(name, 'Missing name argument.'); - callback(null, docs[0]); + const document = new this({ + name: this.nameAdapter(name.trim()) }); + const accounts = await this.insertOne(document); + + return accounts[0]; } - static findByUsername(username, callback) { + static findByUsername(username) { + + Assert.ok(username, 'Missing username argument.'); const query = { 'user.name': username.toLowerCase() }; - this.findOne(query, callback); + return this.findOne(query); } -} + static nameAdapter(name) { -Account.collection = 'accounts'; + Assert.ok(name, 'Missing name argument.'); + const nameParts = name.trim().split(/\s/); -Account.schema = Joi.object({ - _id: Joi.object(), - user: Joi.object({ - id: Joi.string().required(), - name: Joi.string().lowercase().required() - }), - name: Joi.object({ - first: Joi.string().required(), - middle: Joi.string().allow(''), - last: Joi.string().required() - }), - status: Joi.object({ - current: StatusEntry.schema, - log: Joi.array().items(StatusEntry.schema) - }), - notes: Joi.array().items(NoteEntry.schema), - verification: Joi.object({ - complete: Joi.boolean(), - token: Joi.string() - }), - timeCreated: Joi.date() -}); + return { + first: nameParts.shift(), + middle: nameParts.length > 1 ? nameParts.shift() : '', + last: nameParts.join(' ') + }; + } + + async linkUser(id, name) { + + Assert.ok(id, 'Missing id argument.'); + Assert.ok(name, 'Missing name argument.'); + + const update = { + $set: { + user: { id, name } + } + }; + + return await Account.findByIdAndUpdate(this._id, update); + } + + async unlinkUser() { + + const update = { + $unset: { + user: undefined + } + }; + + return await Account.findByIdAndUpdate(this._id, update); + } +} +Account.collectionName = 'accounts'; +Account.schema = schema; Account.indexes = [ { key: { 'user.id': 1 } }, { key: { 'user.name': 1 } } diff --git a/server/models/admin-group.js b/server/models/admin-group.js index e45cc16..4154c7e 100644 --- a/server/models/admin-group.js +++ b/server/models/admin-group.js @@ -1,29 +1,35 @@ 'use strict'; +const Assert = require('assert'); const Joi = require('joi'); const MongoModels = require('mongo-models'); const Slug = require('slug'); -class AdminGroup extends MongoModels { - static create(name, callback) { +const schema = Joi.object({ + _id: Joi.string(), + name: Joi.string().required(), + permissions: Joi.object().description('{ permission: boolean, ... }') +}); - const document = { - _id: Slug(name).toLowerCase(), - name - }; - this.insertOne(document, (err, docs) => { +class AdminGroup extends MongoModels { + static async create(name) { - if (err) { - return callback(err); - } + Assert.ok(name, 'Missing name argument.'); - callback(null, docs[0]); + const document = new this({ + _id: Slug(name).toLowerCase(), + name }); + const groups = await this.insertOne(document); + + return groups[0]; } hasPermissionTo(permission) { + Assert.ok(permission, 'Missing permission argument.'); + if (this.permissions && this.permissions.hasOwnProperty(permission)) { return this.permissions[permission]; } @@ -33,17 +39,9 @@ class AdminGroup extends MongoModels { } -AdminGroup.collection = 'adminGroups'; - - AdminGroup._idClass = String; - - -AdminGroup.schema = Joi.object({ - _id: Joi.string(), - name: Joi.string().required(), - permissions: Joi.object().description('{ permission: boolean, ... }') -}); +AdminGroup.collectionName = 'adminGroups'; +AdminGroup.schema = schema; module.exports = AdminGroup; diff --git a/server/models/admin.js b/server/models/admin.js index ee2a338..c2cb015 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -1,39 +1,61 @@ 'use strict'; const AdminGroup = require('./admin-group'); -const Async = require('async'); +const Assert = require('assert'); const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewDate = require('joistick/new-date'); -class Admin extends MongoModels { - static create(name, callback) { - - const nameParts = name.trim().split(/\s/); +const schema = Joi.object({ + _id: Joi.object(), + groups: Joi.object().description('{ groupId: name, ... }').default(), + name: Joi.object({ + first: Joi.string().required(), + middle: Joi.string().allow(''), + last: Joi.string().allow('') + }), + permissions: Joi.object().description('{ permission: boolean, ... }'), + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + user: Joi.object({ + id: Joi.string().required(), + name: Joi.string().lowercase().required() + }) +}); - const document = { - name: { - first: nameParts.shift(), - middle: nameParts.length > 1 ? nameParts.shift() : undefined, - last: nameParts.join(' ') - }, - timeCreated: new Date() - }; - this.insertOne(document, (err, docs) => { +class Admin extends MongoModels { + static async create(name) { - if (err) { - return callback(err); - } + Assert.ok(name, 'Missing name argument.'); - callback(null, docs[0]); + const document = new this({ + name: this.nameAdapter(name) }); + const admins = await this.insertOne(document); + + return admins[0]; } - static findByUsername(username, callback) { + static findByUsername(username) { + + Assert.ok(username, 'Missing username argument.'); const query = { 'user.name': username.toLowerCase() }; - this.findOne(query, callback); + return this.findOne(query); + } + + static nameAdapter(name) { + + Assert.ok(name, 'Missing name argument.'); + + const nameParts = name.trim().split(/\s/); + + return { + first: nameParts.shift(), + middle: nameParts.length > 1 ? nameParts.shift() : '', + last: nameParts.join(' ') + }; } constructor(attrs) { @@ -46,95 +68,88 @@ class Admin extends MongoModels { }); } - isMemberOf(group) { + async hasPermissionTo(permission) { + + Assert.ok(permission, 'Missing permission argument.'); - if (!this.groups) { - return false; + if (this.permissions && this.permissions.hasOwnProperty(permission)) { + return this.permissions[permission]; } - return this.groups.hasOwnProperty(group); - } + await this.hydrateGroups(); - hydrateGroups(callback) { + let groupHasPermission = false; - if (!this.groups) { - this._groups = {}; - return callback(null, this._groups); - } + Object.keys(this._groups).forEach((group) => { - if (this._groups) { - return callback(null, this._groups); - } + if (this._groups[group].hasPermissionTo(permission)) { + groupHasPermission = true; + } + }); - const tasks = {}; + return groupHasPermission; + } - Object.keys(this.groups).forEach((group) => { + async hydrateGroups() { - tasks[group] = function (done) { + if (this._groups) { + return this._groups; + } - AdminGroup.findById(group, done); - }; + this._groups = {}; + + const groups = await AdminGroup.find({ + _id: { + $in: Object.keys(this.groups) + } }); - Async.auto(tasks, (err, results) => { + this._groups = groups.reduce((accumulator, group) => { - if (err) { - return callback(err); - } + accumulator[group._id] = group; - this._groups = results; + return accumulator; + }, {}); - callback(null, this._groups); - }); + return this._groups; } - hasPermissionTo(permission, callback) { - - if (this.permissions && this.permissions.hasOwnProperty(permission)) { - return callback(null, this.permissions[permission]); - } + isMemberOf(group) { - this.hydrateGroups((err) => { + Assert.ok(group, 'Missing group argument.'); - if (err) { - return callback(err); - } + return this.groups.hasOwnProperty(group); + } - let groupHasPermission = false; + async linkUser(id, name) { - Object.keys(this._groups).forEach((group) => { + Assert.ok(id, 'Missing id argument.'); + Assert.ok(name, 'Missing name argument.'); - if (this._groups[group].hasPermissionTo(permission)) { - groupHasPermission = true; - } - }); + const update = { + $set: { + user: { id, name } + } + }; - callback(null, groupHasPermission); - }); + return await Admin.findByIdAndUpdate(this._id, update); } -} - -Admin.collection = 'admins'; + async unlinkUser() { + const update = { + $unset: { + user: undefined + } + }; -Admin.schema = Joi.object({ - _id: Joi.object(), - user: Joi.object({ - id: Joi.string().required(), - name: Joi.string().lowercase().required() - }), - groups: Joi.object().description('{ groupId: name, ... }'), - permissions: Joi.object().description('{ permission: boolean, ... }'), - name: Joi.object({ - first: Joi.string().required(), - middle: Joi.string().allow(''), - last: Joi.string().required() - }), - timeCreated: Joi.date() -}); + return await Admin.findByIdAndUpdate(this._id, update); + } +} +Admin.collectionName = 'admins'; +Admin.schema = schema; Admin.indexes = [ { key: { 'user.id': 1 } }, { key: { 'user.name': 1 } } diff --git a/server/models/auth-attempt.js b/server/models/auth-attempt.js index 3201b2d..07c572b 100644 --- a/server/models/auth-attempt.js +++ b/server/models/auth-attempt.js @@ -1,87 +1,54 @@ 'use strict'; -const Async = require('async'); +const Assert = require('assert'); const Config = require('../../config'); const Joi = require('joi'); const MongoModels = require('mongo-models'); -const Useragent = require('useragent'); +const NewDate = require('joistick/new-date'); -class AuthAttempt extends MongoModels { - static create(ip, username, userAgent, callback) { - - const parsedAgent = Useragent.lookup(userAgent); - let browser = parsedAgent.family; +const schema = Joi.object({ + _id: Joi.object(), + ip: Joi.string().required(), + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + username: Joi.string().required() +}); - if (browser === 'Other') { - browser = parsedAgent.source; - } - const document = { - ip, - browser, - os: parsedAgent.os.toString(), - username: username.toLowerCase(), - time: new Date() - }; +class AuthAttempt extends MongoModels { + static async abuseDetected(ip, username) { - this.insertOne(document, (err, docs) => { + Assert.ok(ip, 'Missing ip argument.'); + Assert.ok(username, 'Missing username argument.'); - if (err) { - return callback(err); - } + const [countByIp, countByIpAndUser] = await Promise.all([ + this.count({ ip }), + this.count({ ip, username }) + ]); + const config = Config.get('/authAttempts'); + const ipLimitReached = countByIp >= config.forIp; + const ipUserLimitReached = countByIpAndUser >= config.forIpAndUser; - callback(null, docs[0]); - }); + return ipLimitReached || ipUserLimitReached; } - static abuseDetected(ip, username, callback) { - - const self = this; - - Async.auto({ - abusiveIpCount: function (done) { - - const query = { ip }; - self.count(query, done); - }, - abusiveIpUserCount: function (done) { - - const query = { - ip, - username: username.toLowerCase() - }; + static async create(ip, username) { - self.count(query, done); - } - }, (err, results) => { + Assert.ok(ip, 'Missing ip argument.'); + Assert.ok(username, 'Missing username argument.'); - if (err) { - return callback(err); - } - - const authAttemptsConfig = Config.get('/authAttempts'); - const ipLimitReached = results.abusiveIpCount >= authAttemptsConfig.forIp; - const ipUserLimitReached = results.abusiveIpUserCount >= authAttemptsConfig.forIpAndUser; - - callback(null, ipLimitReached || ipUserLimitReached); + const document = new this({ + ip, + username }); + const authAttempts = await this.insertOne(document); + + return authAttempts[0]; } } -AuthAttempt.collection = 'authAttempts'; - - -AuthAttempt.schema = Joi.object({ - _id: Joi.object(), - username: Joi.string().lowercase().required(), - ip: Joi.string().required(), - browser: Joi.string().required(), - os: Joi.string().required(), - time: Joi.date().required() -}); - - +AuthAttempt.collectionName = 'authAttempts'; +AuthAttempt.schema = schema; AuthAttempt.indexes = [ { key: { ip: 1, username: 1 } }, { key: { username: 1 } } diff --git a/server/models/note-entry.js b/server/models/note-entry.js index 1b428eb..23d7fb8 100644 --- a/server/models/note-entry.js +++ b/server/models/note-entry.js @@ -1,19 +1,23 @@ 'use strict'; const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewDate = require('joistick/new-date'); + + +const schema = Joi.object({ + adminCreated: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() + }).required(), + data: Joi.string().required(), + timeCreated: Joi.date().default(NewDate(), 'time of creation') +}); class NoteEntry extends MongoModels {} -NoteEntry.schema = Joi.object({ - data: Joi.string().required(), - timeCreated: Joi.date().required(), - userCreated: Joi.object({ - id: Joi.string().required(), - name: Joi.string().lowercase().required() - }).required() -}); +NoteEntry.schema = schema; module.exports = NoteEntry; diff --git a/server/models/session.js b/server/models/session.js index 86ec78c..13bc86c 100644 --- a/server/models/session.js +++ b/server/models/session.js @@ -1,127 +1,91 @@ 'use strict'; -const Async = require('async'); +const Assert = require('assert'); const Bcrypt = require('bcrypt'); const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewDate = require('joistick/new-date'); const Useragent = require('useragent'); const Uuid = require('uuid'); -class Session extends MongoModels { - static generateKeyHash(callback) { - - const key = Uuid.v4(); - - Async.auto({ - salt: function (done) { - - Bcrypt.genSalt(10, done); - }, - hash: ['salt', function (results, done) { - - Bcrypt.hash(key, results.salt, done); - }] - }, (err, results) => { +const schema = Joi.object({ + _id: Joi.object(), + browser: Joi.string().required(), + ip: Joi.string().required(), + key: Joi.string().required(), + lastActive: Joi.date().default(NewDate(), 'time of last activity'), + os: Joi.string().required(), + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + userId: Joi.string().required() +}); - if (err) { - return callback(err); - } - callback(null, { - key, - hash: results.hash - }); +class Session extends MongoModels { + static async create(userId, ip, userAgent) { + + Assert.ok(userId, 'Missing userId argument.'); + Assert.ok(ip, 'Missing ip argument.'); + Assert.ok(userAgent, 'Missing userAgent argument.'); + + const keyHash = await this.generateKeyHash(); + const agentInfo = Useragent.lookup(userAgent); + const browser = agentInfo.family; + const document = new this({ + browser, + ip, + key: keyHash.hash, + os: agentInfo.os.toString(), + userId }); - } + const sessions = await this.insertOne(document); - static create(userId, ip, userAgent, callback) { + sessions[0].key = keyHash.key; - const self = this; - - Async.auto({ - keyHash: this.generateKeyHash.bind(this), - newSession: ['keyHash', function (results, done) { - - const parsedAgent = Useragent.lookup(userAgent); - let browser = parsedAgent.family; + return sessions[0]; + } - if (browser === 'Other') { - browser = parsedAgent.source; - } + static async findByCredentials(id, key) { - const document = { - userId, - key: results.keyHash.hash, - time: new Date(), - lastActive: new Date(), - ip, - browser, - os: parsedAgent.os.toString() - }; + Assert.ok(id, 'Missing id argument.'); + Assert.ok(key, 'Missing key argument.'); - self.insertOne(document, done); - }] - }, (err, results) => { + const session = await this.findById(id); - if (err) { - return callback(err); - } + if (!session) { + return; + } - results.newSession[0].key = results.keyHash.key; + const keyMatch = await Bcrypt.compare(key, session.key); - callback(null, results.newSession[0]); - }); + if (keyMatch) { + return session; + } } - static findByCredentials(id, key, callback) { - - const self = this; - - Async.auto({ - session: function (done) { + static async generateKeyHash() { - self.findById(id, done); - }, - keyMatch: ['session', function (results, done) { - - if (!results.session) { - return done(null, false); - } + const key = Uuid.v4(); + const salt = await Bcrypt.genSalt(10); + const hash = await Bcrypt.hash(key, salt); - const source = results.session.key; - Bcrypt.compare(key, source, done); - }] - }, (err, results) => { + return { key, hash }; + } - if (err) { - return callback(err); - } + async updateLastActive() { - if (results.keyMatch) { - return callback(null, results.session); + const update = { + $set: { + lastActive: new Date() } + }; - callback(); - }); + await Session.findByIdAndUpdate(this._id, update); } } -Session.collection = 'sessions'; - - -Session.schema = Joi.object({ - _id: Joi.object(), - userId: Joi.string().required(), - key: Joi.string().required(), - time: Joi.date().required(), - lastActive: Joi.date().required(), - ip: Joi.string().required(), - browser: Joi.string().required(), - os: Joi.string().required() -}); - - +Session.collectionName = 'sessions'; +Session.schema = schema; Session.indexes = [ { key: { userId: 1 } } ]; diff --git a/server/models/status-entry.js b/server/models/status-entry.js index 90e9913..2bb9bb3 100644 --- a/server/models/status-entry.js +++ b/server/models/status-entry.js @@ -1,20 +1,24 @@ 'use strict'; const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewDate = require('joistick/new-date'); -class StatusEntry extends MongoModels {} - - -StatusEntry.schema = Joi.object({ +const schema = Joi.object({ id: Joi.string().required(), name: Joi.string().required(), - timeCreated: Joi.date().required(), - userCreated: Joi.object({ + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + adminCreated: Joi.object({ id: Joi.string().required(), - name: Joi.string().lowercase().required() + name: Joi.string().required() }).required() }); +class StatusEntry extends MongoModels {} + + +StatusEntry.schema = schema; + + module.exports = StatusEntry; diff --git a/server/models/status.js b/server/models/status.js index 65f3ea3..b9960a4 100644 --- a/server/models/status.js +++ b/server/models/status.js @@ -1,43 +1,38 @@ 'use strict'; +const Assert = require('assert'); const Joi = require('joi'); const MongoModels = require('mongo-models'); const Slug = require('slug'); -class Status extends MongoModels { - static create(pivot, name, callback) { +const schema = Joi.object({ + _id: Joi.string(), + name: Joi.string().required(), + pivot: Joi.string().required() +}); - const document = { - _id: Slug(pivot + ' ' + name).toLowerCase(), - pivot, - name - }; - this.insertOne(document, (err, docs) => { +class Status extends MongoModels { + static async create(pivot, name) { - if (err) { - return callback(err); - } + Assert.ok(pivot, 'Missing pivot argument.'); + Assert.ok(name, 'Missing name argument.'); - callback(null, docs[0]); + const document = new this({ + _id: Slug(`${pivot}-${name}`).toLowerCase(), + name, + pivot }); + const statuses = await this.insertOne(document); + + return statuses[0]; } } -Status.collection = 'statuses'; - - Status._idClass = String; - - -Status.schema = Joi.object({ - _id: Joi.string(), - pivot: Joi.string().required(), - name: Joi.string().required() -}); - - +Status.collectionName = 'statuses'; +Status.schema = schema; Status.indexes = [ { key: { pivot: 1 } }, { key: { name: 1 } } diff --git a/server/models/user.js b/server/models/user.js index c8da413..e3e1cfb 100644 --- a/server/models/user.js +++ b/server/models/user.js @@ -1,115 +1,111 @@ 'use strict'; const Account = require('./account'); const Admin = require('./admin'); -const Async = require('async'); +const Assert = require('assert'); const Bcrypt = require('bcrypt'); const Joi = require('joi'); const MongoModels = require('mongo-models'); +const NewDate = require('joistick/new-date'); -class User extends MongoModels { - static generatePasswordHash(password, callback) { - - Async.auto({ - salt: function (done) { +const schema = Joi.object({ + _id: Joi.object(), + email: Joi.string().email().lowercase().required(), + isActive: Joi.boolean().default(true), + password: Joi.string(), + resetPassword: Joi.object({ + token: Joi.string().required(), + expires: Joi.date().required() + }), + roles: Joi.object({ + admin: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() + }), + account: Joi.object({ + id: Joi.string().required(), + name: Joi.string().required() + }) + }).default(), + timeCreated: Joi.date().default(NewDate(), 'time of creation'), + username: Joi.string().token().lowercase().required() +}); - Bcrypt.genSalt(10, done); - }, - hash: ['salt', function (results, done) { - Bcrypt.hash(password, results.salt, done); - }] - }, (err, results) => { +class User extends MongoModels { + static async create(username, password, email) { + + Assert.ok(username, 'Missing username argument.'); + Assert.ok(password, 'Missing password argument.'); + Assert.ok(email, 'Missing email argument.'); + + const passwordHash = await this.generatePasswordHash(password); + const document = new this({ + email, + isActive: true, + password: passwordHash.hash, + username + }); + const users = await this.insertOne(document); - if (err) { - return callback(err); - } + users[0].password = passwordHash.password; - callback(null, { - password, - hash: results.hash - }); - }); + return users[0]; } - static create(username, password, email, callback) { + static async findByCredentials(username, password) { - const self = this; + Assert.ok(username, 'Missing username argument.'); + Assert.ok(password, 'Missing password argument.'); - Async.auto({ - passwordHash: this.generatePasswordHash.bind(this, password), - newUser: ['passwordHash', function (results, done) { + const query = { isActive: true }; - const document = { - isActive: true, - username: username.toLowerCase(), - password: results.passwordHash.hash, - email: email.toLowerCase(), - timeCreated: new Date() - }; + if (username.indexOf('@') > -1) { + query.email = username.toLowerCase(); + } + else { + query.username = username.toLowerCase(); + } - self.insertOne(document, done); - }] - }, (err, results) => { + const user = await this.findOne(query); - if (err) { - return callback(err); - } + if (!user) { + return; + } - results.newUser[0].password = results.passwordHash.password; + const passwordMatch = await Bcrypt.compare(password, user.password); - callback(null, results.newUser[0]); - }); + if (passwordMatch) { + return user; + } } - static findByCredentials(username, password, callback) { - - const self = this; + static findByEmail(email) { - Async.auto({ - user: function (done) { + Assert.ok(email, 'Missing email argument.'); - const query = { - isActive: true - }; + const query = { email: email.toLowerCase() }; - if (username.indexOf('@') > -1) { - query.email = username.toLowerCase(); - } - else { - query.username = username.toLowerCase(); - } - - self.findOne(query, done); - }, - passwordMatch: ['user', function (results, done) { + return this.findOne(query); + } - if (!results.user) { - return done(null, false); - } + static findByUsername(username) { - const source = results.user.password; - Bcrypt.compare(password, source, done); - }] - }, (err, results) => { + Assert.ok(username, 'Missing username argument.'); - if (err) { - return callback(err); - } - - if (results.passwordMatch) { - return callback(null, results.user); - } + const query = { username: username.toLowerCase() }; - callback(); - }); + return this.findOne(query); } - static findByUsername(username, callback) { + static async generatePasswordHash(password) { - const query = { username: username.toLowerCase() }; + Assert.ok(password, 'Missing password argument.'); - this.findOne(query, callback); + const salt = await Bcrypt.genSalt(10); + const hash = await Bcrypt.hash(password, salt); + + return { password, hash }; } constructor(attrs) { @@ -124,82 +120,84 @@ class User extends MongoModels { canPlayRole(role) { - if (!this.roles) { - return false; - } + Assert.ok(role, 'Missing role argument.'); return this.roles.hasOwnProperty(role); } - hydrateRoles(callback) { - - if (!this.roles) { - this._roles = {}; - return callback(null, this._roles); - } + async hydrateRoles() { if (this._roles) { - return callback(null, this._roles); + return this._roles; } - const self = this; - const tasks = {}; + this._roles = {}; if (this.roles.account) { - tasks.account = function (done) { - - Account.findById(self.roles.account.id, done); - }; + this._roles.account = await Account.findById(this.roles.account.id); } if (this.roles.admin) { - tasks.admin = function (done) { - - Admin.findById(self.roles.admin.id, done); - }; + this._roles.admin = await Admin.findById(this.roles.admin.id); } - Async.auto(tasks, (err, results) => { + return this._roles; + } + + async linkAccount(id, name) { + + Assert.ok(id, 'Missing id argument.'); + Assert.ok(name, 'Missing name argument.'); - if (err) { - return callback(err); + const update = { + $set: { + 'roles.account': { id, name } } + }; - self._roles = results; + return await User.findByIdAndUpdate(this._id, update); + } - callback(null, self._roles); - }); + async linkAdmin(id, name) { + + Assert.ok(id, 'Missing id argument.'); + Assert.ok(name, 'Missing name argument.'); + + const update = { + $set: { + 'roles.admin': { id, name } + } + }; + + return await User.findByIdAndUpdate(this._id, update); } -} + async unlinkAccount() { -User.collection = 'users'; + const update = { + $unset: { + 'roles.account': undefined + } + }; + return await User.findByIdAndUpdate(this._id, update); + } -User.schema = Joi.object({ - _id: Joi.object(), - isActive: Joi.boolean().default(true), - username: Joi.string().token().lowercase().required(), - password: Joi.string(), - email: Joi.string().email().lowercase().required(), - roles: Joi.object({ - admin: Joi.object({ - id: Joi.string().required(), - name: Joi.string().required() - }), - account: Joi.object({ - id: Joi.string().required(), - name: Joi.string().required() - }) - }), - resetPassword: Joi.object({ - token: Joi.string().required(), - expires: Joi.date().required() - }), - timeCreated: Joi.date() -}); + async unlinkAdmin() { + + const update = { + $unset: { + 'roles.admin': undefined + } + }; + + return await User.findByIdAndUpdate(this._id, update); + } +} +User.collectionName = 'users'; +User.schema = schema; User.indexes = [ { key: { username: 1 }, unique: true }, { key: { email: 1 }, unique: true } diff --git a/server/preware.js b/server/preware.js new file mode 100644 index 0000000..0f2b686 --- /dev/null +++ b/server/preware.js @@ -0,0 +1,43 @@ +'use strict'; +const Boom = require('boom'); + + +class Preware { + static requireAdminGroup(groups) { + + return { + assign: 'ensureAdminGroup', + method: function (request, h) { + + if (Object.prototype.toString.call(groups) !== '[object Array]') { + groups = [groups]; + } + + const admin = request.auth.credentials.roles.admin; + const groupFound = groups.some((group) => admin.isMemberOf(group)); + + if (!groupFound) { + throw Boom.forbidden('Missing required group membership.'); + } + + return h.continue; + } + }; + }; +} + + +Preware.requireNotRootUser = { + assign: 'requireNotRootUser', + method: function (request, h) { + + if (request.auth.credentials.user.username === 'root') { + throw Boom.forbidden('Not permitted for the root user.'); + } + + return h.continue; + } +}; + + +module.exports = Preware; diff --git a/server/web/index.jade b/server/web/index.jade deleted file mode 100644 index 860c48b..0000000 --- a/server/web/index.jade +++ /dev/null @@ -1,6 +0,0 @@ -doctype html -html - head - title A user api kit for Node.js - body - h1 Activate the plot device! diff --git a/server/web/index.js b/server/web/index.js deleted file mode 100644 index 3263b4f..0000000 --- a/server/web/index.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - - -exports.register = function (server, options, next) { - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - return reply.view('index'); - } - }); - - - next(); -}; - - -exports.register.attributes = { - name: 'home', - dependencies: 'visionary' -}; diff --git a/server/web/main.js b/server/web/main.js new file mode 100644 index 0000000..8d07ddf --- /dev/null +++ b/server/web/main.js @@ -0,0 +1,21 @@ +'use strict'; + + +const register = function (server, options) { + + server.route({ + method: 'GET', + path: '/', + handler: function (request, h) { + + return '

Welcome to the website.

'; + } + }); +}; + + +module.exports = { + name: 'web-main', + dependencies: [], + register +}; diff --git a/test/config.js b/test/config.js index df369d4..37345d5 100644 --- a/test/config.js +++ b/test/config.js @@ -9,18 +9,14 @@ const lab = exports.lab = Lab.script(); lab.experiment('Config', () => { - lab.test('it gets config data', (done) => { + lab.test('it gets config data', () => { Code.expect(Config.get('/')).to.be.an.object(); - - done(); }); - lab.test('it gets config meta data', (done) => { + lab.test('it gets config meta data', () => { Code.expect(Config.meta('/')).to.match(/this file configures the plot device/i); - - done(); }); }); diff --git a/test/index.js b/test/index.js deleted file mode 100644 index a040120..0000000 --- a/test/index.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; -const Code = require('code'); -const Composer = require('../index'); -const Lab = require('lab'); - - -const lab = exports.lab = Lab.script(); - - -lab.experiment('App', () => { - - lab.test('it composes a server', (done) => { - - Composer((err, composedServer) => { - - Code.expect(composedServer).to.be.an.object(); - - done(err); - }); - }); -}); diff --git a/test/manifest.js b/test/manifest.js index b2c6b6c..cdaf34d 100644 --- a/test/manifest.js +++ b/test/manifest.js @@ -9,18 +9,14 @@ const lab = exports.lab = Lab.script(); lab.experiment('Manifest', () => { - lab.test('it gets manifest data', (done) => { + lab.test('it gets manifest data', () => { Code.expect(Manifest.get('/')).to.be.an.object(); - - done(); }); - lab.test('it gets manifest meta data', (done) => { + lab.test('it gets manifest meta data', () => { Code.expect(Manifest.meta('/')).to.match(/this file defines the plot device/i); - - done(); }); }); diff --git a/test/server.js b/test/server.js new file mode 100644 index 0000000..58b7302 --- /dev/null +++ b/test/server.js @@ -0,0 +1,21 @@ +// 'use strict'; +// const Code = require('code'); +// const Composer = require('../server'); +// const Lab = require('lab'); +// +// +// const lab = exports.lab = Lab.script(); +// +// +// lab.experiment('App', () => { +// +// lab.test('it composes a server', (done) => { +// +// Composer((err, composedServer) => { +// +// Code.expect(composedServer).to.be.an.object(); +// +// done(err); +// }); +// }); +// }); diff --git a/test/server/api/accounts.js b/test/server/api/accounts.js index 72fab67..77e1f09 100644 --- a/test/server/api/accounts.js +++ b/test/server/api/accounts.js @@ -1,1115 +1,655 @@ 'use strict'; -const AccountPlugin = require('../../../server/api/accounts'); -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedAccount = require('../fixtures/credentials-account'); -const AuthenticatedAdmin = require('../fixtures/credentials-admin'); +const Account = require('../../../server/models/account'); +const Accounts = require('../../../server/api/accounts'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); +const Status = require('../../../server/models/status'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; +let adminAuthHeader; +let accountAuthHeader; -lab.before((done) => { +lab.before(async () => { - stub = { - Account: MakeMockModel(), - Status: MakeMockModel(), - User: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/account')] = stub.Account; - proxy[Path.join(process.cwd(), './server/models/status')] = stub.Status; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Accounts.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(Accounts); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, AccountPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const [root, admin, account] = await Promise.all([ + Fixtures.Creds.createRootAdminUser(), + Fixtures.Creds.createAdminUser('Ren Hoek', 'ren', 'baddog', 'ren@stimpy.show'), + Fixtures.Creds.createAccountUser('Stimpson Cat', 'stimpy', 'goodcat', 'stimpy@ren.show') + ]); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; + adminAuthHeader = admin.authHeader; + accountAuthHeader = account.authHeader; }); -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); +lab.after(async () => { - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Accounts Plugin Result List', () => { +lab.experiment('GET /api/accounts', () => { + + let request; - lab.beforeEach((done) => { + + lab.beforeEach(() => { request = { method: 'GET', - url: '/accounts', - credentials: AuthenticatedAdmin + url: '/api/accounts', + headers: { + authorization: adminAuthHeader + } }; - - done(); }); - lab.test('it returns an error when paged find fails', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Account.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('paged find failed')); - }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns an array of documents successfully', (done) => { - - stub.Account.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); }); -lab.experiment('Accounts Plugin Read', () => { +lab.experiment('POST /api/accounts', () => { - lab.beforeEach((done) => { + let request; - request = { - method: 'GET', - url: '/accounts/93EP150D35', - credentials: AuthenticatedAdmin - }; - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { + lab.beforeEach(() => { - stub.Account.findById = function (id, callback) { - - callback(Error('find by id failed')); + request = { + method: 'POST', + url: '/api/accounts', + headers: { + authorization: adminAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Account.findById = function (id, callback) { - - callback(); + request.payload = { + name: 'Steve' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.and.object(); + Code.expect(response.result.name).to.be.and.object(); + Code.expect(response.result.name.first).to.equal('Steve'); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.Account.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { +lab.experiment('GET /api/accounts/{id}', () => { - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + let request; - done(); - }); - }); -}); - -lab.experiment('Accounts Plugin (My) Read', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'GET', - url: '/accounts/my', - credentials: AuthenticatedAccount - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - - stub.Account.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('find by id failed')); + url: '/api/accounts/{id}', + headers: { + authorization: adminAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.Account.findById = function () { + lab.test('it returns HTTP 404 when `Account.findById` misses', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns a document successfully', (done) => { - - stub.Account.findById = function () { + lab.test('it returns HTTP 200 when all is well', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const account = await Account.create('Steve'); - callback(null, { _id: '93EP150D35' }); - }; + request.url = request.url.replace(/{id}/, account._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Steve'); }); }); -lab.experiment('Accounts Plugin Create', () => { - - lab.beforeEach((done) => { - - request = { - method: 'POST', - url: '/accounts', - payload: { - name: 'Muddy Mudskipper' - }, - credentials: AuthenticatedAdmin - }; - - done(); - }); - - - lab.test('it returns an error when create fails', (done) => { +lab.experiment('PUT /api/accounts/{id}', () => { - stub.Account.create = function (name, callback) { + let request; - callback(Error('create failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - done(); - }); - }); - - lab.test('it creates a document successfully', (done) => { - - stub.Account.create = function (name, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Accounts Plugin Update', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'PUT', - url: '/accounts/93EP150D35', - payload: { - name: { - first: 'Muddy', - last: 'Mudskipper' - } - }, - credentials: AuthenticatedAdmin + url: '/api/accounts/{id}', + headers: { + authorization: adminAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { - - stub.Account.findByIdAndUpdate = function (id, update, callback) { + lab.test('it returns HTTP 404 when `Account.findByIdAndUpdate` misses', async () => { - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + name: { + first: 'Stephen', + middle: '', + last: 'Colbert' + } }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Account.findByIdAndUpdate = function (id, update, callback) { + const account = await Account.create('Steve'); - callback(null, undefined); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + name: { + first: 'Stephen', + middle: '', + last: 'Colbert' + } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(404); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Stephen'); + Code.expect(response.result.name.middle).to.equal(''); + Code.expect(response.result.name.last).to.equal('Colbert'); }); +}); - lab.test('it updates a document successfully', (done) => { - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('DELETE /api/accounts/{id}', () => { + let request; -lab.experiment('Accounts Plugin (My) Update', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'PUT', - url: '/accounts/my', - payload: { - name: { - first: 'Mud', - last: 'Skipper' - } - }, - credentials: AuthenticatedAccount + method: 'DELETE', + url: '/api/accounts/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { + lab.test('it returns HTTP 404 when `Account.findByIdAndDelete` misses', async () => { - stub.Account.findByIdAndUpdate = function () { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const response = await server.inject(request); - callback(Error('update failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it updates a document successfully', (done) => { - - stub.Account.findByIdAndUpdate = function () { + lab.test('it returns HTTP 200 when all is well', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const account = await Account.create('Steve'); - callback(null, {}); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, account._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); }); -lab.experiment('Accounts Plugin Link User', () => { +lab.experiment('PUT /api/accounts/{id}/user', () => { - lab.beforeEach((done) => { + let request; + + + lab.beforeEach(() => { request = { method: 'PUT', - url: '/accounts/93EP150D35/user', - payload: { - username: 'ren' - }, - credentials: AuthenticatedAdmin + url: '/api/accounts/{id}/user', + headers: { + authorization: adminAuthHeader + } }; - - done(); }); - lab.test('it returns an error when (Account) find by id fails', (done) => { - - stub.Account.findById = function (id, callback) { + lab.test('it returns HTTP 404 when `Account.findById` misses', async () => { - callback(Error('find by id failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + username: 'colbert' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when (Account) find by id misses', (done) => { + lab.test('it returns HTTP 404 when `User.findByUsername` misses', async () => { - stub.Account.findById = function (id, callback) { + const account = await Account.create('Stephen Colbert'); - callback(); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + username: 'colbert' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when (User) find by username fails', (done) => { - - stub.Account.findById = function (id, callback) { + lab.test('it returns HTTP 409 when the user is linked to another account', async () => { - callback(null, {}); - }; + const { account: accountA } = await Fixtures.Creds.createAccountUser( + 'Trevor Noah', 'trevor', 'haha', 'trevor@daily.show' + ); - stub.User.findByUsername = function (id, callback) { + const { user: userB } = await Fixtures.Creds.createAccountUser( + 'Jon Stewart', 'jon', 'stew', 'jon@daily.show' + ); - callback(Error('find by username failed')); + request.url = request.url.replace(/{id}/, accountA._id); + request.payload = { + username: userB.username }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/linked to an account/i); }); - lab.test('it returns not found when (User) find by username misses', (done) => { + lab.test('it returns HTTP 409 when the account is currently linked to user', async () => { - stub.Account.findById = function (id, callback) { + const { account } = await Fixtures.Creds.createAccountUser( + 'Mr Horse', 'mrh', 'negh', 'mrh@stimpy.show' + ); - callback(null, {}); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + username: 'ren' }; - stub.User.findByUsername = function (id, callback) { + const response = await server.inject(request); - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/linked to a user/i); }); - lab.test('it returns conflict when an account role already exists', (done) => { - - stub.Account.findById = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, {}); - }; - - stub.User.findByUsername = function (id, callback) { - - const user = { - roles: { - account: { - id: '535H0W35', - name: 'Stimpson J Cat' - } - } - }; + const account = await Account.create('Rand Rando'); + const user = await User.create('random', 'passw0rd', 'random@user.gov'); - callback(null, user); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + username: user.username }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(409); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Rand'); + Code.expect(response.result.name.last).to.equal('Rando'); + Code.expect(response.result.user).to.be.an.object(); + Code.expect(response.result.user.name).to.equal('random'); }); +}); - lab.test('it returns conflict when the account is linked to another user', (done) => { - - stub.Account.findById = function (id, callback) { - - const account = { - _id: 'DUD3N0T1T', - user: { - id: '535H0W35', - name: 'ren' - } - }; +lab.experiment('DELETE /api/accounts/{id}/user', () => { - callback(null, account); - }; + let request; - stub.User.findByUsername = function (id, callback) { - const user = { - _id: 'N0T1TDUD3', - roles: { - account: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; + lab.beforeEach(() => { - callback(null, user); + request = { + method: 'DELETE', + url: '/api/accounts/{id}/user', + headers: { + authorization: adminAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); - - done(); - }); }); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Account.findById = function (id, callback) { - - const account = { - _id: '93EP150D35', - name: { - first: 'Ren', - last: 'Höek' - } - }; - - callback(null, account); - }; - - stub.User.findByUsername = function (id, callback) { - - const user = { - _id: '535H0W35', - username: 'ren' - }; + lab.test('it returns HTTP 404 when `Account.findById` misses', async () => { - callback(null, user); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it successfuly links an account and user', (done) => { + lab.test('it returns HTTP 200 when `account.user` is not present', async () => { - const account = { - _id: '93EP150D35', - name: { - first: 'Ren', - last: 'Höek' - } - }; - const user = { - _id: '535H0W35', - username: 'ren', - roles: {} - }; + const account = await Account.create('Randomoni Randomie'); - stub.Account.findById = function (id, callback) { + request.url = request.url.replace(/{id}/, account._id); - callback(null, account); - }; + const response = await server.inject(request); - stub.User.findByUsername = function (id, callback) { + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Randomoni'); + Code.expect(response.result.name.last).to.equal('Randomie'); + }); - callback(null, user); - }; - stub.Account.findByIdAndUpdate = function (id, update, callback) { + lab.test('it returns HTTP 404 when `User.findById` misses', async () => { - callback(null, account); - }; + const { account, user } = await Fixtures.Creds.createAccountUser( + 'Lil Horse', 'lilh', 'negh', 'lilh@stimpy.show' + ); - stub.User.findByIdAndUpdate = function (id, update, callback) { + await User.findByIdAndDelete(user._id); - callback(null, user); - }; + request.url = request.url.replace(/{id}/, account._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); -}); -lab.experiment('Accounts Plugin Add Note', () => { + lab.test('it returns HTTP 200 when all is good', async () => { - lab.beforeEach((done) => { + const { account, user } = await Fixtures.Creds.createAccountUser( + 'Jr Horse', 'jrh', 'negh', 'jrh@stimpy.show' + ); - request = { - method: 'POST', - url: '/accounts/93EP150D35/notes', - payload: { - data: 'This is a wonderful note.' - }, - credentials: AuthenticatedAdmin - }; - - done(); - }); + request.url = request.url.replace(/{id}/, account._id); + const response = await server.inject(request); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); - }; + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Jr'); + Code.expect(response.result.name.last).to.equal('Horse'); + Code.expect(response.result.user).to.not.exist(); - server.inject(request, (response) => { + const user_ = await User.findByIdAndDelete(user._id); - Code.expect(response.statusCode).to.equal(500); - done(); - }); + Code.expect(user_).to.be.an.object(); + Code.expect(user_.roles).to.be.an.object(); + Code.expect(user_.roles.account).to.not.exist(); }); +}); - lab.test('it successfully adds a note', (done) => { - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); - }); -}); +lab.experiment('POST /api/accounts/{id}/notes', () => { + let request; -lab.experiment('Accounts Plugin Update Status', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'POST', - url: '/accounts/93EP150D35/status', - payload: { - status: 'account-happy' - }, - credentials: AuthenticatedAdmin - }; - - done(); - }); - - - lab.test('it returns an error when find by id (Status) fails', (done) => { - - stub.Status.findById = function (id, callback) { - - callback(Error('find by id failed')); + url: '/api/accounts/{id}/notes', + headers: { + authorization: adminAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Status.findById = function (id, callback) { - - callback(null, { _id: 'account-happy', name: 'Happy' }); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { + lab.test('it returns HTTP 404 when `Account.findByIdAndUpdate` misses', async () => { - callback(Error('find by id and update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + data: 'Super duper note!' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it successfully updates the status', (done) => { - - stub.Status.findById = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, { _id: 'account-happy', name: 'Happy' }); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { + const account = await Account.create('Here And Gone'); - callback(null, {}); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + data: 'Super duper note!' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Here'); + Code.expect(response.result.name.middle).to.equal('And'); + Code.expect(response.result.name.last).to.equal('Gone'); + Code.expect(response.result.notes.length).to.be.greaterThan(0); }); }); -lab.experiment('Accounts Plugin Unlink User', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/accounts/93EP150D35/user', - credentials: AuthenticatedAdmin - }; - - done(); - }); - - - lab.test('it returns an error when (Account) find by id fails', (done) => { - - stub.Account.findById = function (id, callback) { - - callback(Error('find by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - +lab.experiment('POST /api/accounts/{id}/status', () => { - lab.test('it returns not found when (Account) find by id misses', (done) => { - - stub.Account.findById = function (id, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - - done(); - }); - }); + let request; - lab.test('it returns early account is void of a user', (done) => { + lab.beforeEach(() => { - stub.Account.findById = function (id, callback) { - - callback(null, {}); + request = { + method: 'POST', + url: '/api/accounts/{id}/status', + headers: { + authorization: adminAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); }); - lab.test('it returns early account is void of a user.id', (done) => { - - stub.Account.findById = function (id, callback) { + lab.test('it returns HTTP 404 when `Status.findById` misses', async () => { - callback(null, { user: {} }); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + status: 'poison-pill' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when (User) find by id fails', (done) => { + lab.test('it returns HTTP 404 when `Account.findByIdAndUpdate` misses', async () => { - stub.Account.findById = function (id, callback) { + const status = await Status.create('Account', 'Sad'); - const account = { - user: { - id: '93EP150D35', - name: 'ren' - } - }; - - callback(null, account); - }; - - stub.User.findById = function (id, callback) { - - callback(Error('find by id failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + status: status._id }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when (User) find by username misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Account.findById = function (id, callback) { + const status = await Status.create('Account', 'Happy'); + const account = await Account.create('Here And Now'); - const account = { - user: { - id: '93EP150D35', - name: 'ren' - } - }; - - callback(null, account); + request.url = request.url.replace(/{id}/, account._id); + request.payload = { + status: status._id }; - stub.User.findById = function (id, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Here'); + Code.expect(response.result.name.middle).to.equal('And'); + Code.expect(response.result.name.last).to.equal('Now'); + Code.expect(response.result.status).to.be.an.object(); + Code.expect(response.result.status.current).to.be.an.object(); + Code.expect(response.result.status.current.id).to.equal(`${status._id}`); + Code.expect(response.result.status.current.name).to.equal('Happy'); + Code.expect(response.result.status.log).to.be.an.array(); + Code.expect(response.result.status.log.length).to.be.greaterThan(0); }); +}); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Account.findById = function (id, callback) { - - const account = { - _id: '93EP150D35', - user: { - id: '535H0W35', - name: 'ren' - } - }; - - callback(null, account); - }; - - stub.User.findById = function (id, callback) { - - const user = { - _id: '535H0W35', - roles: { - account: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; - - callback(null, user); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); +lab.experiment('GET /api/accounts/my', () => { - done(); - }); - }); + let request; - lab.test('it successfully unlinks an account from a user', (done) => { + lab.beforeEach(() => { - const user = { - _id: '535H0W35', - roles: { - account: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; - const account = { - _id: '93EP150D35', - user: { - id: '535H0W35', - name: 'ren' + request = { + method: 'GET', + url: '/api/accounts/my', + headers: { + authorization: accountAuthHeader } }; - - stub.Account.findById = function (id, callback) { - - callback(null, account); - }; - - stub.User.findById = function (id, callback) { - - callback(null, user); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, account); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, user); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); }); -}); - -lab.experiment('Accounts Plugin Delete', () => { - lab.beforeEach((done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - request = { - method: 'DELETE', - url: '/accounts/93EP150D35', - credentials: AuthenticatedAdmin - }; + const response = await server.inject(request); - done(); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Stimpson'); }); +}); - lab.test('it returns an error when delete by id fails', (done) => { - - stub.Account.findByIdAndDelete = function (id, callback) { - - callback(Error('delete by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); +lab.experiment('PUT /api/accounts/my', () => { - done(); - }); - }); + let request; - lab.test('it returns a not found when delete by id misses', (done) => { + lab.beforeEach(() => { - stub.Account.findByIdAndDelete = function (id, callback) { - - callback(null, undefined); + request = { + method: 'PUT', + url: '/api/accounts/my', + headers: { + authorization: accountAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); }); - lab.test('it deletes a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Account.findByIdAndDelete = function (id, callback) { - - callback(null, 1); + request.payload = { + name: { + first: 'Stimpson', + middle: 'J', + last: 'Cat' + } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Stimpson'); + Code.expect(response.result.name.middle).to.equal('J'); + Code.expect(response.result.name.last).to.equal('Cat'); }); }); diff --git a/test/server/api/admin-groups.js b/test/server/api/admin-groups.js index 52aa497..4a86b29 100644 --- a/test/server/api/admin-groups.js +++ b/test/server/api/admin-groups.js @@ -1,419 +1,301 @@ 'use strict'; -const AdminGroupsPlugin = require('../../../server/api/admin-groups'); -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); +const AdminGroup = require('../../../server/models/admin-group'); +const AdminGroups = require('../../../server/api/admin-groups'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; -lab.before((done) => { +lab.before(async () => { - stub = { - AdminGroup: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/admin-group')] = stub.AdminGroup; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => AdminGroups.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(AdminGroups); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, AdminGroupsPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const root = await Fixtures.Creds.createRootAdminUser(); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; }); -lab.after((done) => { +lab.after(async () => { - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Admin Groups Plugin Result List', () => { +lab.experiment('GET /api/admin-groups', () => { - lab.beforeEach((done) => { + let request; - request = { - method: 'GET', - url: '/admin-groups', - credentials: AuthenticatedUser - }; - done(); - }); - - - lab.test('it returns an error when paged find fails', (done) => { - - stub.AdminGroup.pagedFind = function () { + lab.beforeEach(() => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('paged find failed')); + request = { + method: 'GET', + url: '/api/admin-groups', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns an array of documents successfully', (done) => { - - stub.AdminGroup.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); }); -lab.experiment('Admin Groups Plugin Read', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/admin-groups/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); +lab.experiment('POST /api/admin-groups', () => { + let request; - lab.test('it returns an error when find by id fails', (done) => { - stub.AdminGroup.findById = function (id, callback) { + lab.beforeEach(() => { - callback(Error('find by id failed')); + request = { + method: 'POST', + url: '/api/admin-groups', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.AdminGroup.findById = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(); + request.payload = { + name: 'Sales' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.and.object(); + Code.expect(response.result.name).to.be.equal('Sales'); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.AdminGroup.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('GET /api/admin-groups/{id}', () => { + let request; -lab.experiment('Admin Groups Plugin Create', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'POST', - url: '/admin-groups', - payload: { - name: 'Sales' - }, - credentials: AuthenticatedUser + method: 'GET', + url: '/api/admin-groups/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when create fails', (done) => { + lab.test('it returns HTTP 404 when `AdminGroup.findById` misses', async () => { - stub.AdminGroup.create = function (name, callback) { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - callback(Error('create failed')); - }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it creates a document successfully', (done) => { - - stub.AdminGroup.create = function (name, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, {}); - }; + const adminGroup = await AdminGroup.create('Support'); - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, adminGroup._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.equal('Support'); }); }); -lab.experiment('Admin Groups Plugin Update', () => { +lab.experiment('PUT /api/admin-groups/{id}', () => { + + let request; + - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'PUT', - url: '/admin-groups/sales', - payload: { - name: 'Salez' - }, - credentials: AuthenticatedUser + url: '/api/admin-groups/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { + lab.test('it returns HTTP 404 when `AdminGroup.findByIdAndUpdate` misses', async () => { - stub.AdminGroup.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + name: 'Wrecking Crew' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.AdminGroup.findByIdAndUpdate = function (id, update, callback) { + const adminGroup = await AdminGroup.create('Shipping'); - callback(null, undefined); + request.url = request.url.replace(/{id}/, adminGroup._id); + request.payload = { + name: 'Fulfillment' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.equal('Fulfillment'); }); +}); - lab.test('it updates a document successfully', (done) => { - - stub.AdminGroup.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('DELETE /api/admin-groups/{id}', () => { + let request; -lab.experiment('Admin Groups Plugin Update Permissions', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'PUT', - url: '/admin-groups/sales/permissions', - payload: { - permissions: { SPACE_RACE: true } - }, - credentials: AuthenticatedUser + method: 'DELETE', + url: '/api/admin-groups/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { + lab.test('it returns HTTP 404 when `AdminGroup.findByIdAndDelete` misses', async () => { - stub.AdminGroup.findByIdAndUpdate = function (id, update, callback) { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - callback(Error('update failed')); - }; - - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it updates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.AdminGroup.findByIdAndUpdate = function (id, update, callback) { + const adminGroup = await AdminGroup.create('Steve'); - callback(null, {}); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, adminGroup._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); }); -lab.experiment('Admin Groups Plugin Delete', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/admin-groups/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); +lab.experiment('PUT /api/admin-groups/{id}/permissions', () => { + let request; - lab.test('it returns an error when delete by id fails', (done) => { - stub.AdminGroup.findByIdAndDelete = function (id, callback) { + lab.beforeEach(() => { - callback(Error('delete by id failed')); + request = { + method: 'PUT', + url: '/api/admin-groups/{id}/permissions', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when delete by id misses', (done) => { + lab.test('it returns HTTP 404 when `AdminGroup.findByIdAndUpdate` misses', async () => { - stub.AdminGroup.findByIdAndDelete = function (id, callback) { - - callback(null, undefined); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + permissions: { + CAN_CREATE_ACCOUNTS: true, + CAN_DELETE_ACCOUNTS: false + } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it deletes a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.AdminGroup.findByIdAndDelete = function (id, callback) { + const adminGroup = await AdminGroup.create('Executive'); - callback(null, 1); + request.url = request.url.replace(/{id}/, adminGroup._id); + request.payload = { + permissions: { + CAN_CREATE_ACCOUNTS: true, + CAN_DELETE_ACCOUNTS: false + } }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.equal('Executive'); + Code.expect(response.result.permissions).to.be.an.object(); + Code.expect(response.result.permissions.CAN_CREATE_ACCOUNTS).to.be.true(); + Code.expect(response.result.permissions.CAN_DELETE_ACCOUNTS).to.be.false(); }); }); diff --git a/test/server/api/admins.js b/test/server/api/admins.js index bbf9437..81763ff 100644 --- a/test/server/api/admins.js +++ b/test/server/api/admins.js @@ -1,957 +1,578 @@ 'use strict'; -const AdminPlugin = require('../../../server/api/admins'); -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); +const Admin = require('../../../server/models/admin'); +const Admins = require('../../../server/api/admins'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; -lab.before((done) => { +lab.before(async () => { - stub = { - Admin: MakeMockModel(), - User: MakeMockModel() - }; + server = Hapi.Server(); + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Admins.dependencies.includes(entry.plugin)) + .map((entry) => { - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/admin')] = stub.Admin; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + entry.plugin = require(entry.plugin); - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { - - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(Admins); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, AdminPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const [root] = await Promise.all([ + Fixtures.Creds.createRootAdminUser(), + Fixtures.Creds.createAdminUser('Ren Hoek', 'ren', 'baddog', 'ren@stimpy.show') + ]); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; }); -lab.after((done) => { +lab.after(async () => { - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Admins Plugin Result List', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/admins', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when paged find fails', (done) => { - - stub.Admin.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('paged find failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns an array of documents successfully', (done) => { - - stub.Admin.pagedFind = function () { +lab.experiment('GET /api/admins', () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; + let request; - server.inject(request, (response) => { - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Admins Plugin Read', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'GET', - url: '/admins/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - - stub.Admin.findById = function (id, callback) { - - callback(Error('find by id failed')); + url: '/api/admins', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.findById = function (id, callback) { + const response = await server.inject(request); - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.Admin.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); +lab.experiment('POST /api/admins', () => { - done(); - }); - }); -}); + let request; -lab.experiment('Admins Plugin Create', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'POST', - url: '/admins', - payload: { - name: 'Toast Man' - }, - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when create fails', (done) => { - - stub.Admin.create = function (name, callback) { - - callback(Error('create failed')); + url: '/api/admins', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it creates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.create = function (name, callback) { - - callback(null, {}); + request.payload = { + name: 'Steve' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.and.object(); + Code.expect(response.result.name).to.be.and.object(); + Code.expect(response.result.name.first).to.equal('Steve'); }); }); -lab.experiment('Admins Plugin Update', () => { - - lab.beforeEach((done) => { - - request = { - method: 'PUT', - url: '/admins/93EP150D35', - payload: { - name: { - first: 'Ren', - last: 'Höek' - } - }, - credentials: AuthenticatedUser - }; - - done(); - }); +lab.experiment('GET /api/admins/{id}', () => { + let request; - lab.test('it returns an error when update fails', (done) => { - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + lab.beforeEach(() => { - callback(Error('update failed')); + request = { + method: 'GET', + url: '/api/admins/{id}', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns not found when find by id misses', (done) => { - - stub.Admin.findByIdAndUpdate = function (id, update, callback) { - - callback(null, undefined); - }; + lab.test('it returns HTTP 404 when `Admin.findById` misses', async () => { - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it updates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + const admin = await Admin.create('Steve'); - callback(null, {}); - }; + request.url = request.url.replace(/{id}/, admin._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Steve'); }); }); -lab.experiment('Admins Plugin Update Permissions', () => { +lab.experiment('PUT /api/admins/{id}', () => { - lab.beforeEach((done) => { + let request; - request = { - method: 'PUT', - url: '/admins/93EP150D35/permissions', - payload: { - permissions: { SPACE_RACE: true } - }, - credentials: AuthenticatedUser - }; - - done(); - }); - - lab.test('it returns an error when update fails', (done) => { - - stub.Admin.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('update failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it updates a document successfully', (done) => { - - stub.Admin.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Admins Plugin Update Groups', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'PUT', - url: '/admins/93EP150D35/groups', - payload: { - groups: { sales: 'Sales' } - }, - credentials: AuthenticatedUser + url: '/api/admins/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { - - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + lab.test('it returns HTTP 404 when `Admin.findByIdAndUpdate` misses', async () => { - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + name: { + first: 'Stephen', + middle: '', + last: 'Colbert' + } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it updates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + const admin = await Admin.create('Steve'); - callback(null, {}); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + name: { + first: 'Stephen', + middle: '', + last: 'Colbert' + } }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Stephen'); + Code.expect(response.result.name.middle).to.equal(''); + Code.expect(response.result.name.last).to.equal('Colbert'); }); }); -lab.experiment('Admins Plugin Link User', () => { - - lab.beforeEach((done) => { - - request = { - method: 'PUT', - url: '/admins/93EP150D35/user', - payload: { - username: 'ren' - }, - credentials: AuthenticatedUser - }; - - done(); - }); +lab.experiment('DELETE /api/admins/{id}', () => { + let request; - lab.test('it returns an error when (Admin) find by id fails', (done) => { - stub.Admin.findById = function (id, callback) { + lab.beforeEach(() => { - callback(Error('find by id failed')); + request = { + method: 'DELETE', + url: '/api/admins/{id}', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns not found when (Admin) find by id misses', (done) => { - - stub.Admin.findById = function (id, callback) { - - callback(); - }; + lab.test('it returns HTTP 404 when `Admin.findByIdAndDelete` misses', async () => { - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when (User) find by username fails', (done) => { - - stub.Admin.findById = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, {}); - }; + const admin = await Admin.create('Steve'); - stub.User.findByUsername = function (id, callback) { - - callback(Error('find by username failed')); - }; + request.url = request.url.replace(/{id}/, admin._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); +}); - lab.test('it returns not found when (User) find by username misses', (done) => { +lab.experiment('PUT /api/admins/{id}/groups', () => { - stub.Admin.findById = function (id, callback) { + let request; - callback(null, {}); - }; - stub.User.findByUsername = function (id, callback) { + lab.beforeEach(() => { - callback(); + request = { + method: 'PUT', + url: '/api/admins/{id}/groups', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - - done(); - }); }); - lab.test('it returns conflict when an admin role already exists', (done) => { - - stub.Admin.findById = function (id, callback) { - - callback(null, {}); - }; - - stub.User.findByUsername = function (id, callback) { - - const user = { - roles: { - admin: { - id: '535H0W35', - name: 'Stimpson J Cat' - } - } - }; + lab.test('it returns HTTP 404 when `Admin.findByIdAndUpdate` misses', async () => { - callback(null, user); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + groups: { + sales: 'Sales', + support: 'Support' + } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(409); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns conflict when the admin is linked to another user', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.findById = function (id, callback) { + const admin = await Admin.create('Group Membership'); - const admin = { - _id: 'DUD3N0T1T', - user: { - id: '535H0W35', - name: 'ren' - } - }; - - callback(null, admin); - }; - - stub.User.findByUsername = function (id, callback) { - - const user = { - _id: 'N0T1TDUD3', - roles: { - admin: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; - - callback(null, user); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + groups: { + sales: 'Sales', + support: 'Support' + } }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Group'); + Code.expect(response.result.name.last).to.equal('Membership'); + Code.expect(response.result.groups).to.be.an.object(); + Code.expect(response.result.groups.sales).to.equal('Sales'); + Code.expect(response.result.groups.support).to.equal('Support'); }); +}); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Admin.findById = function (id, callback) { - - const admin = { - _id: '93EP150D35', - name: { - first: 'Ren', - last: 'Höek' - } - }; - - callback(null, admin); - }; - - stub.User.findByUsername = function (id, callback) { - - const user = { - _id: '535H0W35', - username: 'ren' - }; +lab.experiment('PUT /api/admins/{id}/permissions', () => { - callback(null, user); - }; + let request; - stub.Admin.findByIdAndUpdate = function (id, update, callback) { - callback(Error('find by id and update failed')); - }; + lab.beforeEach(() => { - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('find by id and update failed')); + request = { + method: 'PUT', + url: '/api/admins/{id}/permissions', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it successfuly links an admin and user', (done) => { + lab.test('it returns HTTP 404 when `Admin.findByIdAndUpdate` misses', async () => { - const admin = { - _id: '93EP150D35', - name: { - first: 'Ren', - last: 'Höek' + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + permissions: { + CAN_CREATE_ACCOUNTS: true, + CAN_DELETE_ACCOUNTS: false } }; - const user = { - _id: '535H0W35', - username: 'ren', - roles: {} - }; - stub.Admin.findById = function (id, callback) { + const response = await server.inject(request); - callback(null, admin); - }; - - stub.User.findByUsername = function (id, callback) { - - callback(null, user); - }; + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); + }); - stub.Admin.findByIdAndUpdate = function (id, update, callback) { - callback(null, admin); - }; + lab.test('it returns HTTP 200 when all is well', async () => { - stub.User.findByIdAndUpdate = function (id, update, callback) { + const admin = await Admin.create('Granular Permisssions'); - callback(null, user); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + permissions: { + CAN_CREATE_ACCOUNTS: true, + CAN_DELETE_ACCOUNTS: false + } }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Granular'); + Code.expect(response.result.name.last).to.equal('Permisssions'); + Code.expect(response.result.permissions).to.be.an.object(); + Code.expect(response.result.permissions.CAN_CREATE_ACCOUNTS).to.be.true(); + Code.expect(response.result.permissions.CAN_DELETE_ACCOUNTS).to.be.false(); }); }); -lab.experiment('Admins Plugin Unlink User', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/admins/93EP150D35/user', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when (Admin) find by id fails', (done) => { - - stub.Admin.findById = function (id, callback) { - - callback(Error('find by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); +lab.experiment('PUT /api/admins/{id}/user', () => { + let request; - lab.test('it returns not found when (Admin) find by id misses', (done) => { - stub.Admin.findById = function (id, callback) { + lab.beforeEach(() => { - callback(); + request = { + method: 'PUT', + url: '/api/admins/{id}/user', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - - done(); - }); }); - lab.test('it returns early admin is void of a user', (done) => { - - stub.Admin.findById = function (id, callback) { + lab.test('it returns HTTP 404 when `Admin.findById` misses', async () => { - callback(null, {}); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + username: 'colbert' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns early admin is void of a user.id', (done) => { + lab.test('it returns HTTP 404 when `User.findByUsername` misses', async () => { - stub.Admin.findById = function (id, callback) { + const admin = await Admin.create('Stephen Colbert'); - callback(null, { user: {} }); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + username: 'colbert' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when (User) find by id fails', (done) => { + lab.test('it returns HTTP 409 when the user is linked to another admin', async () => { - stub.Admin.findById = function (id, callback) { + const { admin: adminA } = await Fixtures.Creds.createAdminUser( + 'Trevor Noah', 'trevor', 'haha', 'trevor@daily.show' + ); - const admin = { - user: { - id: '93EP150D35', - name: 'ren' - } - }; + const { user: userB } = await Fixtures.Creds.createAdminUser( + 'Jon Stewart', 'jon', 'stew', 'jon@daily.show' + ); - callback(null, admin); + request.url = request.url.replace(/{id}/, adminA._id); + request.payload = { + username: userB.username }; - stub.User.findById = function (id, callback) { + const response = await server.inject(request); - callback(Error('find by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/linked to an admin/i); }); - lab.test('it returns not found when (User) find by username misses', (done) => { + lab.test('it returns HTTP 409 when the admin is currently linked to user', async () => { - stub.Admin.findById = function (id, callback) { + const user = await User.create('hay', 'st4ck', 'hay@stimpy.show'); + const { admin } = await Fixtures.Creds.createAdminUser( + 'Mr Horse', 'mrh', 'negh', 'mrh@stimpy.show' + ); - const admin = { - user: { - id: '93EP150D35', - name: 'ren' - } - }; - - callback(null, admin); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + username: user.username }; - stub.User.findById = function (id, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/linked to a user/i); }); - lab.test('it returns an error when find by id and update fails', (done) => { - - stub.Admin.findById = function (id, callback) { - - const admin = { - _id: '93EP150D35', - user: { - id: '535H0W35', - name: 'ren' - } - }; - - callback(null, admin); - }; - - stub.User.findById = function (id, callback) { - - const user = { - _id: '535H0W35', - roles: { - admin: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; - - callback(null, user); - }; + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + const admin = await Admin.create('Rand Rando'); + const user = await User.create('random', 'passw0rd', 'random@user.gov'); - callback(Error('find by id and update failed')); + request.url = request.url.replace(/{id}/, admin._id); + request.payload = { + username: user.username }; - stub.User.findByIdAndUpdate = function (id, update, callback) { + const response = await server.inject(request); - callback(Error('find by id and update failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Rand'); + Code.expect(response.result.name.last).to.equal('Rando'); + Code.expect(response.result.user).to.be.an.object(); + Code.expect(response.result.user.name).to.equal('random'); }); +}); - lab.test('it successfully unlinks an admin from a user', (done) => { - - const user = { - _id: '535H0W35', - roles: { - admin: { - id: '93EP150D35', - name: 'Ren Höek' - } - } - }; - const admin = { - _id: '93EP150D35', - user: { - id: '535H0W35', - name: 'ren' - } - }; - - stub.Admin.findById = function (id, callback) { - - callback(null, admin); - }; - - stub.User.findById = function (id, callback) { - - callback(null, user); - }; +lab.experiment('DELETE /api/admins/{id}/user', () => { - stub.Admin.findByIdAndUpdate = function (id, update, callback) { + let request; - callback(null, admin); - }; - stub.User.findByIdAndUpdate = function (id, update, callback) { + lab.beforeEach(() => { - callback(null, user); + request = { + method: 'DELETE', + url: '/api/admins/{id}/user', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); }); -}); -lab.experiment('Admins Plugin Delete', () => { + lab.test('it returns HTTP 404 when `Admin.findById` misses', async () => { - lab.beforeEach((done) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - request = { - method: 'DELETE', - url: '/admins/93EP150D35', - credentials: AuthenticatedUser - }; + const response = await server.inject(request); - done(); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when delete by id fails', (done) => { - - stub.Admin.findByIdAndDelete = function (id, callback) { + lab.test('it returns HTTP 200 when `admin.user` is not present', async () => { - callback(Error('delete by id failed')); - }; + const admin = await Admin.create('Randomoni Randomie'); - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, admin._id); - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Randomoni'); + Code.expect(response.result.name.last).to.equal('Randomie'); }); - lab.test('it returns a not found when delete by id misses', (done) => { + lab.test('it returns HTTP 404 when `User.findById` misses', async () => { - stub.Admin.findByIdAndDelete = function (id, callback) { + const { admin, user } = await Fixtures.Creds.createAdminUser( + 'Lil Horse', 'lilh', 'negh', 'lilh@stimpy.show' + ); - callback(null, undefined); - }; + await User.findByIdAndDelete(user._id); - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, admin._id); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it deletes a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is good', async () => { - stub.Admin.findByIdAndDelete = function (id, callback) { + const { admin, user } = await Fixtures.Creds.createAdminUser( + 'Jr Horse', 'jrh', 'negh', 'jrh@stimpy.show' + ); - callback(null, 1); - }; + request.url = request.url.replace(/{id}/, admin._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.be.an.object(); + Code.expect(response.result.name.first).to.equal('Jr'); + Code.expect(response.result.name.last).to.equal('Horse'); + Code.expect(response.result.user).to.not.exist(); - done(); - }); + const user_ = await User.findByIdAndDelete(user._id); + + Code.expect(user_).to.be.an.object(); + Code.expect(user_.roles).to.be.an.object(); + Code.expect(user_.roles.admin).to.not.exist(); }); }); diff --git a/test/server/api/auth-attempts.js b/test/server/api/auth-attempts.js deleted file mode 100644 index 7835a42..0000000 --- a/test/server/api/auth-attempts.js +++ /dev/null @@ -1,250 +0,0 @@ -'use strict'; -const AuthAttemptPlugin = require('../../../server/api/auth-attempts'); -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); -const Code = require('code'); -const Config = require('../../../config'); -const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); -const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); -const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); - - -const lab = exports.lab = Lab.script(); -let request; -let server; -let stub; - - -lab.before((done) => { - - stub = { - AuthAttempt: MakeMockModel() - }; - - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/auth-attempt')] = stub.AuthAttempt; - - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { - - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } - - return false; - })[0].plugin.options - }; - - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, AuthAttemptPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - - if (err) { - return done(err); - } - - server.initialize(done); - }); -}); - - -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); -}); - - -lab.experiment('Auth Attempts Plugin Result List', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/auth-attempts', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when paged find fails', (done) => { - - stub.AuthAttempt.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('paged find failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns an array of documents successfully', (done) => { - - stub.AuthAttempt.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Auth Attempts Plugin Read', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/auth-attempts/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - - stub.AuthAttempt.findById = function (id, callback) { - - callback(Error('find by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns a not found when find by id misses', (done) => { - - stub.AuthAttempt.findById = function (id, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); - }); - - - lab.test('it returns a document successfully', (done) => { - - stub.AuthAttempt.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Auth Attempt Plugin Delete', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/auth-attempts/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when delete by id fails', (done) => { - - stub.AuthAttempt.findByIdAndDelete = function (id, callback) { - - callback(Error('delete by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns a not found when delete by id misses', (done) => { - - stub.AuthAttempt.findByIdAndDelete = function (id, callback) { - - callback(null, undefined); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); - }); - - - lab.test('it deletes a document successfully', (done) => { - - stub.AuthAttempt.findByIdAndDelete = function (id, callback) { - - callback(null, 1); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); - - done(); - }); - }); -}); diff --git a/test/server/api/contact.js b/test/server/api/contact.js index 6ab6d8c..d76e877 100644 --- a/test/server/api/contact.js +++ b/test/server/api/contact.js @@ -1,85 +1,64 @@ 'use strict'; const Code = require('code'); -const Config = require('../../../config'); -const ContactPlugin = require('../../../server/api/contact'); +const Contact = require('../../../server/api/contact'); const Hapi = require('hapi'); const Lab = require('lab'); -const MailerPlugin = require('../../../server/mailer'); +const Mailer = require('../../../server/mailer'); const lab = exports.lab = Lab.script(); -let request; let server; -lab.beforeEach((done) => { +lab.before(async () => { - const plugins = [MailerPlugin, ContactPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + server = Hapi.Server(); - if (err) { - return done(err); - } - - server.initialize(done); - }); + await server.register(Contact); + await server.start(); }); -lab.experiment('Contact Plugin', () => { +lab.after(async () => { - lab.beforeEach((done) => { + await server.stop(); +}); - request = { - method: 'POST', - url: '/contact', - payload: { - name: 'Toast Man', - email: 'mr@toast.show', - message: 'I love you man.' - } - }; - done(); - }); +lab.experiment('POST /api/contact', () => { + const Mailer_sendEmail = Mailer.sendEmail; + let request; - lab.test('it returns an error when send email fails', (done) => { - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { + lab.beforeEach(() => { - callback(Error('send email failed')); + request = { + method: 'POST', + url: '/api/contact' }; + }); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - server.plugins.mailer.sendEmail = realSendEmail; + lab.afterEach(() => { - done(); - }); + Mailer.sendEmail = Mailer_sendEmail; }); - lab.test('it returns success after sending an email', (done) => { + lab.test('it returns HTTP 200 when all is good', async () => { - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { + Mailer.sendEmail = () => undefined; - callback(null, {}); + request.payload = { + name: 'Foo Barzley', + email: 'foo@stimpy.show', + message: 'Hello. How are you?' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - server.plugins.mailer.sendEmail = realSendEmail; + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); }); diff --git a/test/server/api/index.js b/test/server/api/index.js deleted file mode 100644 index 6af6150..0000000 --- a/test/server/api/index.js +++ /dev/null @@ -1,54 +0,0 @@ -'use strict'; -const Code = require('code'); -const Config = require('../../../config'); -const Hapi = require('hapi'); -const IndexPlugin = require('../../../server/api/index'); -const Lab = require('lab'); - - -const lab = exports.lab = Lab.script(); -let request; -let server; - - -lab.beforeEach((done) => { - - const plugins = [IndexPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - - if (err) { - return done(err); - } - - done(); - }); -}); - - -lab.experiment('Index Plugin', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/' - }; - - done(); - }); - - - - lab.test('it returns the default message', (done) => { - - server.inject(request, (response) => { - - Code.expect(response.result.message).to.match(/welcome to the plot device/i); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); - }); -}); diff --git a/test/server/api/login.js b/test/server/api/login.js index a0fb9d9..e29c0d3 100644 --- a/test/server/api/login.js +++ b/test/server/api/login.js @@ -1,599 +1,235 @@ 'use strict'; const AuthAttempt = require('../../../server/models/auth-attempt'); -const AuthPlugin = require('../../../server/auth'); -const Bcrypt = require('bcrypt'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const LoginPlugin = require('../../../server/api/login'); -const MailerPlugin = require('../../../server/mailer'); -const MakeMockModel = require('../fixtures/make-mock-model'); +const Login = require('../../../server/api/login'); +const Mailer = require('../../../server/mailer'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); -const Session = require('../../../server/models/session'); const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; -lab.before((done) => { +lab.before(async () => { - stub = { - AuthAttempt: MakeMockModel(), - Session: MakeMockModel(), - User: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/auth-attempt')] = stub.AuthAttempt; - proxy[Path.join(process.cwd(), './server/models/session')] = stub.Session; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Login.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } - - return false; - })[0].plugin.options - }; + return entry; + }); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, MailerPlugin, LoginPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + plugins.push(Login); - if (err) { - return done(err); - } + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - server.initialize(done); - }); + await User.create('ren', 'baddog', 'ren@stimpy.show'); }); -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); +lab.after(async () => { - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Login Plugin (Create Session)', () => { +lab.experiment('POST /api/login', () => { + + const AuthAttempt_abuseDetected = AuthAttempt.abuseDetected; + const User_findByCredentials = User.findByCredentials; + let request; - lab.beforeEach((done) => { + + lab.beforeEach(() => { request = { method: 'POST', - url: '/login', + url: '/api/login', payload: { username: 'ren', password: 'baddog' } }; - - done(); - }); - - - lab.test('it returns an error when detecting abuse fails', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(Error('abuse detection failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns early when abuse is detected', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, true); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(400); - Code.expect(response.result.message).to.match(/maximum number of auth attempts reached/i); - - done(); - }); - }); - - - lab.test('it returns an error when find by credentials fails', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(Error('find by credentials failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns an error when creating a new auth attempt fails', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.AuthAttempt.create = function (ip, username, userAgent, callback) { - - callback(Error('create auth attempt failed')); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns early after creating a new auth attempt', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.AuthAttempt.create = function (ip, username, userAgent, callback) { - - callback(null, new AuthAttempt({})); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(400); - Code.expect(response.result.message).to.match(/username and password combination not found/i); + lab.afterEach(() => { - done(); - }); + AuthAttempt.abuseDetected = AuthAttempt_abuseDetected; + User.findByCredentials = User_findByCredentials; }); - lab.test('it returns early after creating a new auth attempt with a x-forwarded-for header', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.AuthAttempt.create = function (ip, username, userAgent, callback) { - - callback(null, new AuthAttempt({})); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(); - }; + lab.test('it returns HTTP 400 when login abuse is detected', async () => { - request.headers = { - 'x-forwarded-for': '127.0.0.1' - }; + AuthAttempt.abuseDetected = () => true; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(400); - Code.expect(response.result.message).to.match(/username and password combination not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(400); + Code.expect(response.result.message) + .to.match(/maximum number of auth attempts reached/i); }); - lab.test('it returns an error when creating a new session fails', (done) => { + lab.test('it returns HTTP 400 when a user is not found', async () => { - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { + User.findByCredentials = () => undefined; - callback(null, false); - }; + const response = await server.inject(request); - stub.AuthAttempt.create = function (ip, username, userAgent, callback) { - - callback(null, new AuthAttempt({})); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(null, new User({ _id: '1D', username: 'ren' })); - }; - - stub.Session.findOne = function (query, callback) { - - callback(null, null); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(Error('create session failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(400); + Code.expect(response.result.message) + .to.match(/credentials are invalid or account is inactive/i); }); - lab.test('it returns a session successfully', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.AuthAttempt.create = function (username, ip, userAgent, callback) { - - callback(null, new AuthAttempt({})); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(null, new User({ _id: '1D', username: 'ren' })); - }; - - stub.Session.findOne = function (query, callback) { - - callback(null, null); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D' })); - }; - - server.inject(request, (response) => { + lab.test('it returns HTTP 200 when all is well', async () => { - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.user).to.be.an.object(); + Code.expect(response.result.session).to.be.an.object(); + Code.expect(response.result.authHeader).to.be.a.string(); }); +}); - lab.test('it returns a session successfully with a x-forwarded-for header', (done) => { - - stub.AuthAttempt.abuseDetected = function (ip, username, callback) { - - callback(null, false); - }; - - stub.AuthAttempt.create = function (ip, username, userAgent, callback) { - - callback(null, new AuthAttempt({})); - }; - - stub.User.findByCredentials = function (username, password, callback) { - - callback(null, new User({ _id: '1D', username: 'ren' })); - }; - - stub.Session.findOne = function (query, callback) { - - callback(null, null); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D' })); - }; - - request.headers = { - 'x-forwarded-for': '127.0.0.1' - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); +lab.experiment('POST /api/login/forgot', () => { - done(); - }); - }); -}); + const Mailer_sendEmail = Mailer.sendEmail; + const User_findOne = User.findOne; + let request; -lab.experiment('Login Plugin Forgot Password', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'POST', - url: '/login/forgot', + url: '/api/login/forgot', payload: { email: 'ren@stimpy.show' } }; - - done(); }); - lab.test('it returns an error when find one fails', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(Error('find one failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + lab.afterEach(() => { - done(); - }); + Mailer.sendEmail = Mailer_sendEmail; + User.findOne = User_findOne; }); - lab.test('it returns early when find one misses', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; + lab.test('it returns HTTP 200 when the user query misses', async () => { - server.inject(request, (response) => { + User.findOne = () => undefined; - Code.expect(response.statusCode).to.equal(200); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); - lab.test('it returns an error if any critical step fails', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - const user = { - _id: 'BL4M0' - }; + Mailer.sendEmail = () => undefined; - callback(null, user); - }; + const response = await server.inject(request); - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('update failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); +}); - lab.test('it succussfully sends a reset password request', (done) => { - - stub.User.findOne = function (conditions, callback) { - - const user = { - _id: 'BL4M0' - }; +lab.experiment('POST /api/login/reset', () => { - callback(null, user); - }; + const User_findOne = User.findOne; + const Mailer_sendEmail = Mailer.sendEmail; + let request; + let key; - stub.User.findByIdAndUpdate = function (id, update, callback) { - callback(null, {}); - }; + lab.before(async () => { - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { + Mailer.sendEmail = (_, __, context) => { - callback(null, {}); + key = context.key; }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - server.plugins.mailer.sendEmail = realSendEmail; - - done(); + await server.inject({ + method: 'POST', + url: '/api/login/forgot', + payload: { + email: 'ren@stimpy.show' + } }); }); -}); - -lab.experiment('Login Plugin Reset Password', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'POST', - url: '/login/reset', + url: '/api/login/reset', payload: { - key: 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456', email: 'ren@stimpy.show', - password: 'letmein' + key, + password: 'badcat' } }; - - done(); }); - lab.test('it returns an error when find one fails', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(Error('find one failed')); - }; + lab.afterEach(() => { - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Mailer.sendEmail = Mailer_sendEmail; + User.findOne = User_findOne; }); - lab.test('it returns a bad request when find one misses', (done) => { + lab.test('it returns HTTP 400 when the user query misses', async () => { - stub.User.findOne = function (conditions, callback) { + User.findOne = () => undefined; - callback(); - }; - - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(400); - - done(); - }); + Code.expect(response.statusCode).to.equal(400); + Code.expect(response.result.message).to.match(/invalid email or key/i); }); - lab.test('it returns an error if any critical step fails', (done) => { - - stub.User.findOne = function (conditions, callback) { - - const user = { - _id: 'BL4M0', - resetPassword: { - token: 'O0HL4L4' - } - }; - - callback(null, user); - }; - - const realBcryptCompare = Bcrypt.compare; - Bcrypt.compare = function (key, token, callback) { - - callback(Error('compare failed')); - }; + lab.test('it returns HTTP 400 when the key match misses', async () => { - server.inject(request, (response) => { + request.payload.key += 'poison'; - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - Bcrypt.compare = realBcryptCompare; - - done(); - }); + Code.expect(response.statusCode).to.equal(400); + Code.expect(response.result.message).to.match(/invalid email or key/i); }); - lab.test('it returns a bad request if the key does not match', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - const user = { - _id: 'BL4M0', - resetPassword: { - token: 'O0HL4L4' - } - }; + const response = await server.inject(request); - callback(null, user); - }; - - const realBcryptCompare = Bcrypt.compare; - Bcrypt.compare = function (key, token, callback) { - - callback(null, false); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(400); - - Bcrypt.compare = realBcryptCompare; - - done(); - }); - }); - - - lab.test('it succussfully sets a password', (done) => { - - stub.User.findOne = function (conditions, callback) { - - const user = { - _id: 'BL4M0', - resetPassword: { - token: 'O0HL4L4' - } - }; - - callback(null, user); - }; - - const realBcryptCompare = Bcrypt.compare; - Bcrypt.compare = function (key, token, callback) { - - callback(null, true); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - Bcrypt.compare = realBcryptCompare; - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); }); diff --git a/test/server/api/logout.js b/test/server/api/logout.js index ff1f6de..ea4cdd4 100644 --- a/test/server/api/logout.js +++ b/test/server/api/logout.js @@ -1,166 +1,87 @@ 'use strict'; const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); const HapiAuthBasic = require('hapi-auth-basic'); -const Hoek = require('hoek'); const Lab = require('lab'); -const LogoutPlugin = require('../../../server/api/logout'); -const MakeMockModel = require('../fixtures/make-mock-model'); +const Logout = require('../../../server/api/logout'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); +const Session = require('../../../server/models/session'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; -lab.before((done) => { +lab.before(async () => { - stub = { - Session: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/session')] = stub.Session; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Logout.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { + return entry; + }); - return true; - } + plugins.push(HapiAuthBasic); + plugins.push(AuthPlugin); + plugins.push(Logout); - return false; - })[0].plugin.options - }; + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); +}); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, LogoutPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - if (err) { - return done(err); - } +lab.after(async () => { - server.initialize(done); - }); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); -}); +lab.experiment('DELETE /api/logout', () => { + let request; -lab.experiment('Logout Plugin (Delete Session)', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'DELETE', - url: '/logout', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when delete fails', (done) => { - - stub.Session.findByIdAndDelete = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('delete failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns a not found when delete misses (no credentials)', (done) => { - - stub.Session.findByIdAndDelete = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, 0); + url: '/api/logout' }; - - delete request.credentials; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); }); - lab.test('it returns a not found when delete misses (missing user from credentials)', (done) => { + lab.test('it returns HTTP 200 when credentials are missing', async () => { - stub.Session.deleteOne = function () { + const response = await server.inject(request); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, 0); - }; - - const CorruptedAuthenticatedUser = Hoek.clone(AuthenticatedUser); - CorruptedAuthenticatedUser.user = undefined; - request.credentials = CorruptedAuthenticatedUser; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); - lab.test('it deletes the authenticated user session successfully', (done) => { - - stub.Session.findByIdAndDelete = function () { + lab.test('it returns HTTP 200 when credentials are present', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const user = await User.create('ren', 'baddog', 'ren@stimpy.show'); + const session = await Session.create('ren', 'baddog', 'ren@stimpy.show'); - callback(null, 1); + request.credentials = { + roles: [], + session, + user }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/success/i); }); }); diff --git a/test/server/api/main.js b/test/server/api/main.js new file mode 100644 index 0000000..1605e9b --- /dev/null +++ b/test/server/api/main.js @@ -0,0 +1,48 @@ +'use strict'; +const Code = require('code'); +const Hapi = require('hapi'); +const Lab = require('lab'); +const Main = require('../../../server/api/main'); + + +const lab = exports.lab = Lab.script(); +let server; + + +lab.before(async () => { + + server = Hapi.Server(); + + await server.register(Main); + await server.start(); +}); + + +lab.after(async () => { + + await server.stop(); +}); + + +lab.experiment('GET /api', () => { + + let request; + + + lab.beforeEach(() => { + + request = { + method: 'GET', + url: '/api' + }; + }); + + + lab.test('it returns HTTP 200 when all is good', async () => { + + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.message).to.match(/welcome/i); + }); +}); diff --git a/test/server/api/sessions.js b/test/server/api/sessions.js index 5bf73ce..5477b4a 100644 --- a/test/server/api/sessions.js +++ b/test/server/api/sessions.js @@ -1,393 +1,237 @@ 'use strict'; -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); -const AuthenticatedAccount = require('../fixtures/credentials-account'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); -const SessionPlugin = require('../../../server/api/sessions'); +const Session = require('../../../server/models/session'); +const Sessions = require('../../../server/api/sessions'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; +let rootSession; -lab.before((done) => { +lab.before(async () => { - stub = { - Session: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/session')] = stub.Session; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Sessions.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(Sessions); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, SessionPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const root = await Fixtures.Creds.createRootAdminUser(); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; + rootSession = root.session; }); -lab.after((done) => { +lab.after(async () => { - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Session Plugin Result List', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/sessions', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when paged find fails', (done) => { +lab.experiment('GET /api/sessions', () => { - stub.Session.pagedFind = function () { + let request; - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('find failed')); - }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns an array of documents successfully', (done) => { - - stub.Session.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Session Plugin Read', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'GET', - url: '/sessions/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - - stub.Session.findById = function (id, callback) { - - callback(Error('find by id failed')); + url: '/api/sessions', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.Session.findById = function (id, callback) { - - callback(); - }; - - server.inject(request, (response) => { + lab.test('it returns HTTP 200 when all is well', async () => { - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.Session.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('GET /api/sessions/{id}', () => { + let request; -lab.experiment('Sessions Plugin (My) Read', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'GET', - url: '/sessions/my', - credentials: AuthenticatedUser + url: '/api/sessions/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when find by id fails', (done) => { - - stub.Session.find = function (id, callback) { + lab.test('it returns HTTP 404 when `Session.findById` misses', async () => { - callback(Error('find by id failed')); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.Session.find = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(); - }; + const user = await User.create('darcie', 'uplate', 'darcie@late.night'); + const session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, session._id); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.userId).to.equal(`${user._id}`); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.Session.find = function (id, callback) { - - callback(null, [{ _id: '93EP150D35' }]); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result[0]).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('DELETE /api/sessions/{id}', () => { + let request; -lab.experiment('Session Plugin Delete By User', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'DELETE', - url: '/sessions/my/59dcd4f5f57ad17c99ac3f19', - credentials: AuthenticatedAccount + url: '/api/sessions/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when delete by id fails', (done) => { + lab.test('it returns HTTP 404 when `Session.findByIdAndDelete` misses', async () => { - stub.Session.findOneAndDelete = function (query, callback) { - - callback(Error('find one and delete failed')); - }; + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns a not found when update by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Session.findOneAndDelete = function (query, callback) { - - callback(null, undefined); - }; + const user = await User.create('aldon', 'thirsty', 'aldon@late.night'); + const session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, session._id); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); +}); - lab.test('it updates a document successfully', (done) => { +lab.experiment('GET /api/sessions/my', () => { - stub.Session.findOneAndDelete = function (query, callback) { + let request; - callback(null, 1); - }; - server.inject(request, (response) => { + lab.beforeEach(() => { - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + request = { + method: 'GET', + url: '/api/sessions/my', + headers: { + authorization: rootAuthHeader + } + }; }); - lab.test('it returns an error if you delete the current session', (done) => { - - - AuthenticatedAccount.session._id = '59dcd4f5f57ad17c99ac3f19'; + lab.test('it returns HTTP 200 when all is well', async () => { - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(400); - Code.expect(response.result.message).to.match(/Unable to close your current session. You can use logout instead./i); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.array(); + Code.expect(response.result.length).to.equal(1); }); }); -lab.experiment('Session Plugin Delete by Admin', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/sessions/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); +lab.experiment('DELETE /api/sessions/my/{id}', () => { + let request; - lab.test('it returns an error when delete by id fails', (done) => { - stub.Session.findByIdAndDelete = function (id, callback) { + lab.beforeEach(() => { - callback(Error('delete by id failed')); + request = { + method: 'DELETE', + url: '/api/sessions/my/{id}', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when delete by id misses', (done) => { + lab.test('it returns HTTP 400 when tryint to destroy current session', async () => { - stub.Session.findByIdAndDelete = function (id, callback) { + request.url = request.url.replace(/{id}/, rootSession._id); - callback(null, undefined); - }; - - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(400); + Code.expect(response.result.message).to.match(/current session/i); }); - lab.test('it deletes a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Session.findByIdAndDelete = function (id, callback) { + const session = await Session.create(rootSession.userId, '127.0.0.2', 'Lab'); - callback(null, 1); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, session._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); }); diff --git a/test/server/api/signup.js b/test/server/api/signup.js index f3e0424..ea9d340 100644 --- a/test/server/api/signup.js +++ b/test/server/api/signup.js @@ -1,377 +1,150 @@ 'use strict'; const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MailerPlugin = require('../../../server/mailer'); -const MakeMockModel = require('../fixtures/make-mock-model'); +const Mailer = require('../../../server/mailer'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); -const SignupPlugin = require('../../../server/api/signup'); +const Signup = require('../../../server/api/signup'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; -lab.before((done) => { +lab.before(async () => { - stub = { - Account: MakeMockModel(), - Session: MakeMockModel(), - User: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/account')] = stub.Account; - proxy[Path.join(process.cwd(), './server/models/session')] = stub.Session; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Signup.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { + return entry; + }); - return true; - } + plugins.push(Signup); - return false; - })[0].plugin.options - }; + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); +}); - const plugins = [HapiAuthBasic, ModelsPlugin, MailerPlugin, SignupPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - if (err) { - return done(err); - } +lab.after(async () => { - server.initialize(done); - }); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); -}); +lab.experiment('DELETE /api/signup', () => { + const Mailer_sendEmail = Mailer.sendEmail; + let request; -lab.experiment('Signup Plugin', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'POST', - url: '/signup', - payload: { - name: 'Muddy Mudskipper', - username: 'muddy', - password: 'dirtandwater', - email: 'mrmud@mudmail.mud' - } + url: '/api/signup' }; - - done(); }); - lab.test('it returns an error when find one fails for username check', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.afterEach(() => { - if (conditions.username) { - callback(Error('find one failed')); - } - else { - callback(); - } - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Mailer.sendEmail = Mailer_sendEmail; }); - lab.test('it returns a conflict when find one hits for username check', (done) => { + lab.test('it returns HTTP 409 when the username is already in use', async () => { - stub.User.findOne = function (conditions, callback) { + await User.create('ren', 'baddog', 'ren@stimpy.show'); - if (conditions.username) { - callback(null, {}); - } - else { - callback(Error('find one failed')); - } + request.payload = { + name: 'Unoriginal Bill', + email: 'bill@hotmail.gov', + username: 'ren', + password: 'pass123' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/username already in use/i); }); - lab.test('it returns an error when find one fails for email check', (done) => { + lab.test('it returns HTTP 409 when the email is already in use', async () => { - stub.User.findOne = function (conditions, callback) { - - if (conditions.email) { - callback(Error('find one failed')); - } - else { - callback(); - } + request.payload = { + name: 'Unoriginal Bill', + email: 'ren@stimpy.show', + username: 'bill', + password: 'pass123' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/email already in use/i); }); - lab.test('it returns a conflict when find one hits for email check', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.User.findOne = function (conditions, callback) { + Mailer.sendEmail = () => undefined; - if (conditions.email) { - callback(null, {}); - } - else { - callback(); - } + request.payload = { + name: 'Captain Original', + email: 'captain@stimpy.show', + username: 'captain', + password: 'allaboard' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.user).to.be.an.object(); + Code.expect(response.result.session).to.be.an.object(); + Code.expect(response.result.authHeader).to.be.a.string(); }); - lab.test('it returns an error if any critical setup step fails', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; + lab.test('it returns HTTP 200 when all is well and logs any mailer errors', async () => { - stub.User.create = function (username, password, email, callback) { + Mailer.sendEmail = function () { - callback(Error('create failed')); + throw new Error('Failed to send mail.'); }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); + const mailerLogEvent = server.events.once({ + name: 'request', + filter: ['error', 'mailer'] }); - }); - - - lab.test('it finishes successfully (even if sending welcome email fails)', (done) => { - - stub.User.findOne = function (conditions, callback) { - callback(); + request.payload = { + name: 'Assistant Manager', + email: 'manager@stimpy.show', + username: 'assistant', + password: 'totheregionalmanager' }; - stub.User.create = function (username, password, email, callback) { - - callback(null, { _id: 'BL4M0' }); - }; - - stub.Account.create = function (name, callback) { - - const account = { - _id: 'BL4M0', - name: { - first: 'Muddy', - last: 'Mudskipper' - } - }; - - callback(null, account); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { - - callback(new Error('Whoops.')); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(null, {}); - }; - - const realWarn = console.warn; - console.warn = function () { - - console.warn = realWarn; - - done(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - server.plugins.mailer.sendEmail = realSendEmail; - }); - }); - - - lab.test('it finishes successfully', (done) => { + const response = await server.inject(request); + const [, event] = await mailerLogEvent; - stub.User.findOne = function (conditions, callback) { + Code.expect(event.error.message).to.match(/failed to send mail/i); - callback(); - }; - - stub.User.create = function (username, password, email, callback) { - - callback(null, { _id: 'BL4M0' }); - }; - - stub.Account.create = function (name, callback) { - - const account = { - _id: 'BL4M0', - name: { - first: 'Muddy', - last: 'Mudskipper' - } - }; - - callback(null, account); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { - - callback(null, {}); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - server.plugins.mailer.sendEmail = realSendEmail; - - done(); - }); - }); - - - lab.test('it finishes successfully with an x-forwarded-for header', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; - - stub.User.create = function (username, password, email, callback) { - - callback(null, { _id: 'BL4M0' }); - }; - - stub.Account.create = function (name, callback) { - - const account = { - _id: 'BL4M0', - name: { - first: 'Muddy', - last: 'Mudskipper' - } - }; - - callback(null, account); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - stub.Account.findByIdAndUpdate = function (id, update, callback) { - - callback(null, [{}, {}]); - }; - - const realSendEmail = server.plugins.mailer.sendEmail; - server.plugins.mailer.sendEmail = function (options, template, context, callback) { - - callback(null, {}); - }; - - stub.Session.create = function (username, ip, userAgent, callback) { - - callback(null, {}); - }; - - request.headers = { - 'x-forwarded-for': '127.0.0.1' - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - server.plugins.mailer.sendEmail = realSendEmail; - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.user).to.be.an.object(); + Code.expect(response.result.session).to.be.an.object(); + Code.expect(response.result.authHeader).to.be.a.string(); }); }); diff --git a/test/server/api/statuses.js b/test/server/api/statuses.js index 11edd11..c07ee82 100644 --- a/test/server/api/statuses.js +++ b/test/server/api/statuses.js @@ -1,369 +1,247 @@ 'use strict'; -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); -const StatusesPlugin = require('../../../server/api/statuses'); +const Status = require('../../../server/models/status'); +const Statuses = require('../../../server/api/statuses'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; -lab.before((done) => { +lab.before(async () => { - stub = { - Status: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/status')] = stub.Status; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Statuses.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(Statuses); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, StatusesPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const root = await Fixtures.Creds.createRootAdminUser(); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; }); -lab.after((done) => { +lab.after(async () => { - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('Statuses Plugin Result List', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/statuses', - credentials: AuthenticatedUser - }; - - done(); - }); - +lab.experiment('GET /api/statuses', () => { - lab.test('it returns an error when paged find fails', (done) => { + let request; - stub.Status.pagedFind = function () { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.beforeEach(() => { - callback(Error('paged find failed')); + request = { + method: 'GET', + url: '/api/statuses', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns an array of documents successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Status.pagedFind = function () { + const response = await server.inject(request); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); }); -lab.experiment('Statuses Plugin Read', () => { +lab.experiment('POST /api/statuses', () => { - lab.beforeEach((done) => { + let request; - request = { - method: 'GET', - url: '/statuses/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - stub.Status.findById = function (id, callback) { + lab.beforeEach(() => { - callback(Error('find by id failed')); + request = { + method: 'POST', + url: '/api/statuses', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.Status.findById = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(); + request.payload = { + name: 'Happy', + pivot: 'Account' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.and.object(); + Code.expect(response.result.name).to.be.equal('Happy'); + Code.expect(response.result.pivot).to.be.equal('Account'); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.Status.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('GET /api/statuses/{id}', () => { + let request; -lab.experiment('Statuses Plugin Create', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'POST', - url: '/statuses', - payload: { - pivot: 'Account', - name: 'Happy' - }, - credentials: AuthenticatedUser + method: 'GET', + url: '/api/statuses/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when create fails', (done) => { + lab.test('it returns HTTP 404 when `Status.findById` misses', async () => { - stub.Status.create = function (pivot, name, callback) { + request.url = request.url.replace(/{id}/, 'missing-status'); - callback(Error('create failed')); - }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it creates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Status.create = function (pivot, name, callback) { + const status = await Status.create('Account', 'Sad'); - callback(null, {}); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, status._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.equal('Sad'); + Code.expect(response.result.pivot).to.equal('Account'); }); }); -lab.experiment('Statuses Plugin Update', () => { +lab.experiment('PUT /api/statuses/{id}', () => { - lab.beforeEach((done) => { + let request; + + + lab.beforeEach(() => { request = { method: 'PUT', - url: '/statuses/account-happy', - payload: { - name: 'Happy' - }, - credentials: AuthenticatedUser + url: '/api/statuses/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when update fails', (done) => { + lab.test('it returns HTTP 404 when `Status.findByIdAndUpdate` misses', async () => { - stub.Status.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, 'account-emojiface'); + request.payload = { + name: 'Wrecking Crew' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Status.findByIdAndUpdate = function (id, update, callback) { + const status = await Status.create('Admin', 'Cold'); - callback(null, undefined); + request.url = request.url.replace(/{id}/, status._id); + request.payload = { + name: 'Hot' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(404); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.name).to.equal('Hot'); + Code.expect(response.result.pivot).to.equal('Admin'); }); +}); - lab.test('it updates a document successfully', (done) => { - - stub.Status.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('DELETE /api/statuses/{id}', () => { + let request; -lab.experiment('Statuses Plugin Delete', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'DELETE', - url: '/statuses/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when delete by id fails', (done) => { - - stub.Status.findByIdAndDelete = function (id, callback) { - - callback(Error('delete by id failed')); + url: '/api/statuses/{id}', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when delete by id misses', (done) => { - - stub.Status.findByIdAndDelete = function (id, callback) { + lab.test('it returns HTTP 404 when `Status.findByIdAndDelete` misses', async () => { - callback(null, undefined); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it deletes a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.Status.findByIdAndDelete = function (id, callback) { + const status = await Status.create('Account', 'Above'); - callback(null, 1); - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, status._id); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); }); diff --git a/test/server/api/users.js b/test/server/api/users.js index a066bae..791bf2b 100644 --- a/test/server/api/users.js +++ b/test/server/api/users.js @@ -1,972 +1,491 @@ 'use strict'; -const AuthPlugin = require('../../../server/auth'); -const AuthenticatedUser = require('../fixtures/credentials-admin'); +const Auth = require('../../../server/auth'); const Code = require('code'); -const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('../fixtures/make-mock-model'); const Manifest = require('../../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); -const UserPlugin = require('../../../server/api/users'); +const User = require('../../../server/models/user'); +const Users = require('../../../server/api/users'); const lab = exports.lab = Lab.script(); -let request; let server; -let stub; +let rootAuthHeader; +let accountAuthHeader; -lab.before((done) => { +lab.before(async () => { - stub = { - User: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Users.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } + return entry; + }); - return false; - })[0].plugin.options - }; + plugins.push(Auth); + plugins.push(Users); - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin, UserPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - if (err) { - return done(err); - } + const [root, account] = await Promise.all([ + Fixtures.Creds.createRootAdminUser(), + Fixtures.Creds.createAccountUser('Stimpson Cat', 'stimpy', 'goodcat', 'stimpy@ren.show'), + Fixtures.Creds.createAdminUser('Ren Hoek', 'ren', 'baddog', 'ren@stimpy.show') + ]); - server.initialize(done); - }); + rootAuthHeader = root.authHeader; + accountAuthHeader = account.authHeader; }); -lab.after((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); +lab.after(async () => { - done(); + await Fixtures.Db.removeAllData(); + await server.stop(); }); -lab.experiment('User Plugin Result List', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/users', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when paged find fails', (done) => { +lab.experiment('GET /api/users', () => { - stub.User.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('paged find failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); + let request; - lab.test('it returns an array of documents successfully', (done) => { - - stub.User.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); - }); - - - lab.test('it returns an array of documents successfully using filters', (done) => { - - stub.User.pagedFind = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { data: [{}, {}, {}] }); - }; - - request.url = '/users?username=ren&isActive=true&role=admin&limit=10&page=1'; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.data).to.be.an.array(); - Code.expect(response.result.data[0]).to.be.an.object(); - - done(); - }); - }); -}); - - -lab.experiment('Users Plugin Read', () => { - - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'GET', - url: '/users/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find by id fails', (done) => { - - stub.User.findById = function (id, callback) { - - callback(Error('find by id failed')); + url: '/api/users', + headers: { + authorization: rootAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a not found when find by id misses', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.User.findById = function (id, callback) { + const response = await server.inject(request); - callback(); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.data).to.be.an.array(); + Code.expect(response.result.pages).to.be.an.object(); + Code.expect(response.result.items).to.be.an.object(); }); +}); - lab.test('it returns a document successfully', (done) => { - - stub.User.findById = function (id, callback) { - - callback(null, { _id: '93EP150D35' }); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('POST /api/users', () => { + let request; -lab.experiment('Users Plugin (My) Read', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'GET', - url: '/users/my', - credentials: AuthenticatedUser + method: 'POST', + url: '/api/users', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when find by id fails', (done) => { - - stub.User.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns HTTP 409 when the username is already in use', async () => { - callback(Error('find by id failed')); + request.payload = { + email: 'steve@stimpy.show', + password: 'lovely', + username: 'ren' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/username already in use/i); }); - lab.test('it returns a not found when find by id misses', (done) => { - - stub.User.findById = function () { + lab.test('it returns HTTP 409 when the email is already in use', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(); + request.payload = { + email: 'ren@stimpy.show', + password: 'lovely', + username: 'steveplease' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/email already in use/i); }); - lab.test('it returns a document successfully', (done) => { - - stub.User.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, { _id: '93EP150D35' }); + request.payload = { + email: 'steve@stimpy.show', + password: 'lovely', + username: 'steveplease' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.and.object(); + Code.expect(response.result.username).to.equal('steveplease'); }); }); -lab.experiment('Users Plugin Create', () => { - - lab.beforeEach((done) => { - - request = { - method: 'POST', - url: '/users', - payload: { - username: 'muddy', - password: 'dirtandwater', - email: 'mrmud@mudmail.mud' - }, - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find one fails for username check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.username) { - callback(Error('find one failed')); - } - else { - callback(); - } - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); +lab.experiment('GET /api/users/{id}', () => { + let request; - lab.test('it returns a conflict when find one hits for username check', (done) => { - stub.User.findOne = function (conditions, callback) { + lab.beforeEach(() => { - if (conditions.username) { - callback(null, {}); - } - else { - callback(Error('find one failed')); - } - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); - - done(); - }); - }); - - - lab.test('it returns an error when find one fails for email check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.email) { - callback(Error('find one failed')); - } - else { - callback(); + request = { + method: 'GET', + url: '/api/users/{id}', + headers: { + authorization: rootAuthHeader } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns a conflict when find one hits for email check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.email) { - callback(null, {}); - } - else { - callback(); - } - }; + lab.test('it returns HTTP 404 when `User.findById` misses', async () => { - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns an error when create fails', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(); - }; + const user = await User.create('mrcolbert', 'colbert123', 'mr@colbert.baz'); - stub.User.create = function (username, password, email, callback) { - - callback(Error('create failed')); - }; + request.url = request.url.replace(/{id}/, user._id); - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('mrcolbert'); }); +}); - lab.test('it creates a document successfully', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; - - stub.User.create = function (username, password, email, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('PUT /api/users/{id}', () => { + let request; -lab.experiment('Users Plugin Update', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'PUT', - url: '/users/420000000000000000000000', - payload: { - isActive: true, - username: 'muddy', - email: 'mrmud@mudmail.mud' - }, - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when find one fails for username check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.username) { - callback(Error('find one failed')); - } - else { - callback(); - } - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); - - - lab.test('it returns a conflict when find one hits for username check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.username) { - callback(null, {}); - } - else { - callback(Error('find one failed')); + url: '/api/users/{id}', + headers: { + authorization: rootAuthHeader } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); - - done(); - }); }); - lab.test('it returns an error when find one fails for email check', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 409 when the username is already in use', async () => { - if (conditions.email) { - callback(Error('find one failed')); - } - else { - callback(); - } + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + isActive: true, + email: 'ren@stimpy.show', + username: 'ren' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/username already in use/i); }); - lab.test('it returns a conflict when find one hits for email check', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 409 when the email is already in use', async () => { - if (conditions.email) { - callback(null, {}); - } - else { - callback(); - } + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + isActive: true, + email: 'ren@stimpy.show', + username: 'pleasesteve' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/email already in use/i); }); - lab.test('it returns an error when update fails', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { + lab.test('it returns HTTP 404 when `User.findByIdAndUpdate` misses', async () => { - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + isActive: true, + email: 'pleasesteve@stimpy.show', + username: 'pleasesteve' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns not found when find by id misses', (done) => { - - stub.User.findOne = function (conditions, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { + const user = await User.create('finally', 'gue55', 'finally@made.it'); - callback(null, undefined); + request.url = request.url.replace(/{id}/, user._id); + request.payload = { + isActive: true, + email: 'finally@made.io', + username: 'yllanif' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('yllanif'); + Code.expect(response.result.email).to.equal('finally@made.io'); }); +}); - lab.test('it updates a document successfully', (done) => { - - stub.User.findOne = function (conditions, callback) { - - callback(); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); - }); -}); +lab.experiment('DELETE /api/users/{id}', () => { + let request; -lab.experiment('Users Plugin (My) Update', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { - method: 'PUT', - url: '/users/my', - payload: { - username: 'muddy', - email: 'mrmud@mudmail.mud' - }, - credentials: AuthenticatedUser + method: 'DELETE', + url: '/api/users/{id}', + headers: { + authorization: rootAuthHeader + } }; - - done(); }); - lab.test('it returns an error when find one fails for username check', (done) => { + lab.test('it returns HTTP 404 when `User.findByIdAndDelete` misses', async () => { - stub.User.findOne = function (conditions, callback) { + request.url = request.url.replace(/{id}/, '555555555555555555555555'); - if (conditions.username) { - callback(Error('find one failed')); - } - else { - callback(); - } - }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it returns a conflict when find one hits for username check', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.User.findOne = function (conditions, callback) { + const user = await User.create('deleteme', '0000', 'delete@me.please'); - if (conditions.username) { - callback(null, {}); - } - else { - callback(Error('find one failed')); - } - }; - - server.inject(request, (response) => { + request.url = request.url.replace(/{id}/, user._id); - Code.expect(response.statusCode).to.equal(409); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.message).to.match(/success/i); }); +}); - lab.test('it returns an error when find one fails for email check', (done) => { - - stub.User.findOne = function (conditions, callback) { - - if (conditions.email) { - callback(Error('find one failed')); - } - else { - callback(); - } - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); +lab.experiment('PUT /api/users/{id}/password', () => { + let request; - lab.test('it returns a conflict when find one hits for email check', (done) => { - stub.User.findOne = function (conditions, callback) { + lab.beforeEach(() => { - if (conditions.email) { - callback(null, {}); - } - else { - callback(); + request = { + method: 'PUT', + url: '/api/users/{id}/password', + headers: { + authorization: rootAuthHeader } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(409); - - done(); - }); }); - lab.test('it returns an error when update fails', (done) => { + lab.test('it returns HTTP 404 when `User.findByIdAndUpdate` misses', async () => { - stub.User.findOne = function (conditions, callback) { - - callback(); - }; - - stub.User.findByIdAndUpdate = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + password: '53cur3p455' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(404); + Code.expect(response.result.message).to.match(/not found/i); }); - lab.test('it updates a document successfully', (done) => { + lab.test('it returns HTTP 200 when all is well', async () => { - stub.User.findOne = function (conditions, callback) { + const user = await User.create('finally', 'gue55', 'finally@made.it'); - callback(); + request.url = request.url.replace(/{id}/, user._id); + request.payload = { + password: '53cur3p455' }; - stub.User.findByIdAndUpdate = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { _id: '1D', username: 'muddy' }); - }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result).to.be.an.object(); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('finally'); }); }); -lab.experiment('Users Plugin Set Password', () => { +lab.experiment('GET /api/users/my', () => { - lab.beforeEach((done) => { + let request; - request = { - method: 'PUT', - url: '/users/420000000000000000000000/password', - payload: { - password: 'fromdirt' - }, - credentials: AuthenticatedUser - }; - done(); - }); - - - lab.test('it returns an error when generate password hash fails', (done) => { + lab.beforeEach(() => { - stub.User.generatePasswordHash = function (password, callback) { - - callback(Error('generate password hash failed')); + request = { + method: 'GET', + url: '/api/users/my', + headers: { + authorization: accountAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); }); - lab.test('it returns an error when update fails', (done) => { - - stub.User.generatePasswordHash = function (password, callback) { - - callback(null, { password: '', hash: '' }); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(Error('update failed')); - }; - - server.inject(request, (response) => { + lab.test('it returns HTTP 200 when all is well', async () => { - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('stimpy'); }); +}); - lab.test('it sets the password successfully', (done) => { - - stub.User.generatePasswordHash = function (password, callback) { - - callback(null, { password: '', hash: '' }); - }; - - stub.User.findByIdAndUpdate = function (id, update, callback) { - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); - }); -}); +lab.experiment('PUT /api/users/my', () => { + let request; -lab.experiment('Users Plugin (My) Set Password', () => { - lab.beforeEach((done) => { + lab.beforeEach(() => { request = { method: 'PUT', - url: '/users/my/password', - payload: { - password: 'fromdirt' - }, - credentials: AuthenticatedUser + url: '/api/users/my', + headers: { + authorization: accountAuthHeader + } }; - - done(); }); - lab.test('it returns an error when generate password hash fails', (done) => { + lab.test('it returns HTTP 409 when the username is already in use', async () => { - stub.User.generatePasswordHash = function (password, callback) { - - callback(Error('generate password hash failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + email: 'ren@stimpy.show', + username: 'ren' }; - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/username already in use/i); }); - lab.test('it returns an error when update fails', (done) => { - - stub.User.generatePasswordHash = function (password, callback) { - - callback(null, { password: '', hash: '' }); - }; - - stub.User.findByIdAndUpdate = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns HTTP 409 when the email is already in use', async () => { - callback(Error('update failed')); + request.url = request.url.replace(/{id}/, '555555555555555555555555'); + request.payload = { + email: 'ren@stimpy.show', + username: 'pleasesteve' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(500); - - done(); - }); + Code.expect(response.statusCode).to.equal(409); + Code.expect(response.result.message).to.match(/email already in use/i); }); - lab.test('it sets the password successfully', (done) => { - - stub.User.generatePasswordHash = function (password, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, { password: '', hash: '' }); + request.payload = { + email: 'stimpy@gmail.gov', + username: 'stimpson' }; - stub.User.findByIdAndUpdate = function () { + const response = await server.inject(request); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, {}); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(200); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('stimpson'); + Code.expect(response.result.email).to.equal('stimpy@gmail.gov'); }); }); -lab.experiment('Users Plugin Delete', () => { - - lab.beforeEach((done) => { - - request = { - method: 'DELETE', - url: '/users/93EP150D35', - credentials: AuthenticatedUser - }; - - done(); - }); - - - lab.test('it returns an error when delete by id fails', (done) => { - - stub.User.findByIdAndDelete = function (id, callback) { - - callback(Error('delete by id failed')); - }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(500); - - done(); - }); - }); +lab.experiment('PUT /api/users/my/password', () => { + let request; - lab.test('it returns a not found when delete by id misses', (done) => { - stub.User.findByIdAndDelete = function (id, callback) { + lab.beforeEach(() => { - callback(null, undefined); + request = { + method: 'PUT', + url: '/api/users/my/password', + headers: { + authorization: accountAuthHeader + } }; - - server.inject(request, (response) => { - - Code.expect(response.statusCode).to.equal(404); - Code.expect(response.result.message).to.match(/document not found/i); - - done(); - }); }); - lab.test('it deletes a document successfully', (done) => { - - stub.User.findByIdAndDelete = function (id, callback) { + lab.test('it returns HTTP 200 when all is well', async () => { - callback(null, 1); + request.payload = { + password: '53cur3p455' }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.statusCode).to.equal(200); - Code.expect(response.result.message).to.match(/success/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.be.an.object(); + Code.expect(response.result.username).to.equal('stimpson'); }); }); diff --git a/test/server/auth.js b/test/server/auth.js index 9adb3cc..a713df0 100644 --- a/test/server/auth.js +++ b/test/server/auth.js @@ -1,649 +1,160 @@ 'use strict'; -const Admin = require('../../server/models/admin'); -const AuthPlugin = require('../../server/auth'); +const Auth = require('../../server/auth'); const Code = require('code'); -const Config = require('../../config'); +const Fixtures = require('./fixtures'); const Hapi = require('hapi'); -const HapiAuthBasic = require('hapi-auth-basic'); const Lab = require('lab'); -const MakeMockModel = require('./fixtures/make-mock-model'); const Manifest = require('../../manifest'); -const Path = require('path'); -const Proxyquire = require('proxyquire'); const Session = require('../../server/models/session'); const User = require('../../server/models/user'); const lab = exports.lab = Lab.script(); let server; -let stub; -lab.beforeEach((done) => { +lab.before(async () => { - stub = { - Session: MakeMockModel(), - User: MakeMockModel() - }; + server = Hapi.Server(); - const proxy = {}; - proxy[Path.join(process.cwd(), './server/models/session')] = stub.Session; - proxy[Path.join(process.cwd(), './server/models/user')] = stub.User; + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Auth.dependencies.includes(entry.plugin)) + .map((entry) => { - const ModelsPlugin = { - register: Proxyquire('hapi-mongo-models', proxy), - options: Manifest.get('/registrations').filter((reg) => { + entry.plugin = require(entry.plugin); - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'hapi-mongo-models') { - - return true; - } - - return false; - })[0].plugin.options - }; - - const plugins = [HapiAuthBasic, ModelsPlugin, AuthPlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - - if (err) { - return done(err); - } - - server.initialize(done); - }); -}); - - -lab.afterEach((done) => { - - server.plugins['hapi-mongo-models'].MongoModels.disconnect(); - - done(); -}); - - -lab.experiment('Auth Plugin', () => { - - lab.test('it returns authentication credentials', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (username, callback) { - - callback(null, new User({ _id: '1D', username: 'ren' })); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - server.auth.test('simple', request, (err, credentials) => { - - Code.expect(err).to.not.exist(); - Code.expect(credentials).to.be.an.object(); - reply('ok'); - }); - } - }); - - const request = { - method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') - } - }; - - server.inject(request, (response) => { - - done(); - }); - }); - - - lab.test('it returns an error when the session is not found', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(); - }; - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - server.auth.test('simple', request, (err, credentials) => { - - Code.expect(err).to.be.an.object(); - Code.expect(credentials).to.not.exist(); - reply('ok'); - }); - } - }); - - const request = { - method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') - } - }; - - server.inject(request, (response) => { - - done(); - }); - }); - - - lab.test('it returns an error when the user is not found', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ username: 'ren', key: 'baddog' })); - }; - - stub.User.findByUsername = function (username, callback) { - - callback(); - }; - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - server.auth.test('simple', request, (err, credentials) => { - - Code.expect(err).to.be.an.object(); - reply('ok'); - }); - } + return entry; }); - const request = { - method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') - } - }; - - server.inject(request, (response) => { - - done(); - }); - }); + plugins.push(Auth); + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); - lab.test('it returns an error when a model error occurs', (done) => { + server.route({ + method: 'GET', + path: '/', + handler: async function (request, h) { - stub.Session.findByCredentials = function (username, key, callback) { + try { + await request.server.auth.test('simple', request); - callback(Error('session fail')); - }; - - server.route({ - method: 'GET', - path: '/', - handler: function (request, reply) { - - server.auth.test('simple', request, (err, credentials) => { - - Code.expect(err).to.be.an.object(); - Code.expect(credentials).to.not.exist(); - reply('ok'); - }); + return { isValid: true }; } - }); - - const request = { - method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') + catch (err) { + return { isValid: false }; } - }; - - server.inject(request, (response) => { - - done(); - }); + } }); +}); - lab.test('it takes over when the required role is missing', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { +lab.after(async () => { - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { + await Fixtures.Db.removeAllData(); + await server.stop(); +}); - callback(null, new User({ _id: '1D', username: 'ren' })); - }; - stub.Session.findByIdAndUpdate = function (id, update, callback) { +lab.experiment('Simple Auth Strategy', () => { - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - } - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); + lab.test('it returns as invalid without authentication provided', async () => { const request = { method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('2D:baddog')).toString('base64') - } + url: '/' }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.result.message).to.match(/insufficient scope/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.isValid).to.equal(false); }); - lab.test('it continues through pre handler when role is present', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { - - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - } - }); - - user._roles = { - admin: { - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - } - } - }; - - callback(null, user); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: ['account', 'admin'] - } - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); + lab.test('it returns as invalid when the session query misses', async () => { + const sessionId = '000000000000000000000001'; + const sessionKey = '01010101-0101-0101-0101-010101010101'; const request = { method: 'GET', url: '/', headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') + authorization: Fixtures.Creds.authHeader(sessionId, sessionKey) } }; - server.inject(request, (response) => { - - Code.expect(response.result).to.match(/ok/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.isValid).to.equal(false); }); - lab.test('it takes over when the required group is missing', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { - - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - } - }); - - user._roles = { - admin: new Admin({ - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - } - }) - }; - - callback(null, user); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup('root') - ] - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); + lab.test('it returns as invalid when the user query misses', async () => { + const session = await Session.create('000000000000000000000000', '127.0.0.1', 'Lab'); const request = { method: 'GET', url: '/', headers: { - authorization: 'Basic ' + (new Buffer('2D:baddog')).toString('base64') + authorization: Fixtures.Creds.authHeader(session._id, session.key) } }; + const response = await server.inject(request); - server.inject(request, (response) => { - - Code.expect(response.result.message).to.match(/permission denied/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.isValid).to.equal(false); }); - lab.test('it continues through pre handler when group is present', (done) => { + lab.test('it returns as invalid when the user is not active', async () => { - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { - - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - } - }); - - user._roles = { - admin: new Admin({ - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - root: 'Root' - } - }) - }; - - callback(null, user); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureAdminGroup(['sales', 'root']) - ] - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); - - const request = { - method: 'GET', - url: '/', - headers: { - authorization: 'Basic ' + (new Buffer('2D:baddog')).toString('base64') + const { user } = await Fixtures.Creds.createAdminUser( + 'Ben Hoek', 'ben', 'badben', 'ben@stimpy.show' + ); + const session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); + const update = { + $set: { + isActive: false } }; - server.inject(request, (response) => { - - Code.expect(response.result).to.match(/ok/i); - - done(); - }); - }); - - - lab.test('it continues through pre handler when not acting the root user', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { - - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - } - }); - - user._roles = { - admin: new Admin({ - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - } - }) - }; - - callback(null, user); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureNotRoot - ] - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); + await User.findByIdAndUpdate(user._id, update); const request = { method: 'GET', url: '/', headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') + authorization: Fixtures.Creds.authHeader(session._id, session.key) } }; - server.inject(request, (response) => { + const response = await server.inject(request); - Code.expect(response.result).to.match(/ok/i); - - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.isValid).to.equal(false); }); - lab.test('it takes over when acting as the root user', (done) => { - - stub.Session.findByCredentials = function (username, key, callback) { + lab.test('it returns as valid when all is well', async () => { - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - stub.User.findById = function (id, callback) { - - const user = new User({ - username: 'root', - roles: { - admin: { - id: '953P150D35', - name: 'Root Admin' - } - } - }); - - user._roles = { - admin: new Admin({ - _id: '953P150D35', - name: { - first: 'Root', - last: 'Admin' - } - }) - }; - - callback(null, user); - }; - - stub.Session.findByIdAndUpdate = function (id, update, callback) { - - callback(null, new Session({ _id: '2D', userId: '1D', key: 'baddog' })); - }; - - server.route({ - method: 'GET', - path: '/', - config: { - auth: { - strategy: 'simple', - scope: 'admin' - }, - pre: [ - AuthPlugin.preware.ensureNotRoot - ] - }, - handler: function (request, reply) { - - Code.expect(request.auth.credentials).to.be.an.object(); - - reply('ok'); - } - }); + const { user } = await Fixtures.Creds.createAdminUser( + 'Ren Hoek', 'ren', 'baddog', 'ren@stimpy.show' + ); + const session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); const request = { method: 'GET', url: '/', headers: { - authorization: 'Basic ' + (new Buffer('ren:baddog')).toString('base64') + authorization: Fixtures.Creds.authHeader(session._id, session.key) } }; - server.inject(request, (response) => { - - Code.expect(response.result.message).to.match(/not permitted for root user/i); + const response = await server.inject(request); - done(); - }); + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result.isValid).to.equal(true); }); }); diff --git a/test/server/fixtures/credentials-account.js b/test/server/fixtures/credentials-account.js deleted file mode 100644 index cacf7be..0000000 --- a/test/server/fixtures/credentials-account.js +++ /dev/null @@ -1,38 +0,0 @@ -'use strict'; -const Account = require('../../../server/models/account'); -const User = require('../../../server/models/user'); -const Session = require('../../../server/models/session'); - - -const user = new User({ - _id: '535HOW35', - username: 'stimpy', - roles: { - account: { - id: '5250W35', - name: 'Stimpson J Cat' - } - }, - _roles: { - account: new Account({ - _id: '5250W35', - name: { - first: 'Stimpson', - middle: 'J', - last: 'Cat' - } - }) - } -}); - -const session = new Session({ - '_id': '5250W35' -}); - - -module.exports = { - user, - roles: user._roles, - scope: Object.keys(user.roles), - session -}; diff --git a/test/server/fixtures/credentials-admin.js b/test/server/fixtures/credentials-admin.js deleted file mode 100644 index e88a165..0000000 --- a/test/server/fixtures/credentials-admin.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; -const Admin = require('../../../server/models/admin'); -const User = require('../../../server/models/user'); - - -const user = new User({ - _id: '535HOW35', - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - }, - _roles: { - admin: new Admin({ - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - root: 'Root' - } - }) - } -}); - - -module.exports = { - user, - roles: user._roles, - scope: Object.keys(user.roles) -}; diff --git a/test/server/fixtures/creds.js b/test/server/fixtures/creds.js new file mode 100644 index 0000000..6489425 --- /dev/null +++ b/test/server/fixtures/creds.js @@ -0,0 +1,140 @@ +'use strict'; +const Account = require('../../../server/models/account'); +const Admin = require('../../../server/models/admin'); +const Session = require('../../../server/models/session'); +const Slug = require('slug'); +const User = require('../../../server/models/user'); + + +class Credentials { + static authHeader(username, password) { + + const combo = `${username}:${password}`; + const combo64 = (new Buffer(combo)).toString('base64'); + + return `Basic ${combo64}`; + } + + static async createRootAdminUser() { + + let [admin, user, session, authHeader] = await Promise.all([ + Admin.create('Root Admin'), + User.create('root', 'root', 'root@stimpy.show'), + undefined, + undefined + ]); + const adminUpdate = { + $set: { + groups: { + root: 'Root' + }, + user: { + id: `${user._id}`, + name: 'root' + } + } + }; + const userUpdate = { + $set: { + 'roles.admin': { + id: `${admin._id}`, + name: 'Root Admin' + } + } + }; + + session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); + + [admin, user, session, authHeader] = await Promise.all([ + Admin.findByIdAndUpdate(admin._id, adminUpdate), + User.findByIdAndUpdate(user._id, userUpdate), + session, + this.authHeader(session._id, session.key) + ]); + + return { admin, user, session, authHeader }; + } + + static async createAdminUser(name, username, password, email, groups = []) { + + let [admin, user, session, authHeader] = await Promise.all([ + Admin.create(name), + User.create(username, password, email), + undefined, + undefined + ]); + const adminUpdate = { + $set: { + groups: groups.reduce((accumulator, group) => { + + accumulator[Slug(group).toLowerCase()] = group; + + return accumulator; + }, {}), + user: { + id: `${user._id}`, + name: username + } + } + }; + const userUpdate = { + $set: { + 'roles.admin': { + id: `${admin._id}`, + name + } + } + }; + + session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); + + [admin, user, session, authHeader] = await Promise.all([ + Admin.findByIdAndUpdate(admin._id, adminUpdate), + User.findByIdAndUpdate(user._id, userUpdate), + session, + this.authHeader(session._id, session.key) + ]); + + return { admin, user, session, authHeader }; + } + + static async createAccountUser(name, username, password, email) { + + let [account, user, session, authHeader] = await Promise.all([ + Account.create(name), + User.create(username, password, email), + undefined, + undefined + ]); + const adminUpdate = { + $set: { + user: { + id: `${user._id}`, + name: username + } + } + }; + const userUpdate = { + $set: { + 'roles.account': { + id: `${account._id}`, + name + } + } + }; + + session = await Session.create(`${user._id}`, '127.0.0.1', 'Lab'); + + [account, user, session, authHeader] = await Promise.all([ + Account.findByIdAndUpdate(account._id, adminUpdate), + User.findByIdAndUpdate(user._id, userUpdate), + session, + this.authHeader(session._id, session.key) + ]); + + return { account, user, session, authHeader }; + } +} + + +module.exports = Credentials; diff --git a/test/server/fixtures/db.js b/test/server/fixtures/db.js new file mode 100644 index 0000000..63c4596 --- /dev/null +++ b/test/server/fixtures/db.js @@ -0,0 +1,25 @@ +'use strict'; +const Account = require('../../../server/models/account'); +const Admin = require('../../../server/models/admin'); +const AdminGroup = require('../../../server/models/admin-group'); +const Session = require('../../../server/models/session'); +const Status = require('../../../server/models/status'); +const User = require('../../../server/models/user'); + + +class Db { + static async removeAllData() { + + return await Promise.all([ + Account.deleteMany({}), + Admin.deleteMany({}), + AdminGroup.deleteMany({}), + Session.deleteMany({}), + Status.deleteMany({}), + User.deleteMany({}) + ]); + } +} + + +module.exports = Db; diff --git a/test/server/fixtures/hapi.js b/test/server/fixtures/hapi.js new file mode 100644 index 0000000..c3497a9 --- /dev/null +++ b/test/server/fixtures/hapi.js @@ -0,0 +1,14 @@ +'use strict'; + + +class Hapi {} + + +Hapi.debugServerConfig = { + debug: { + request: ['error'] + } +}; + + +module.exports = Hapi; diff --git a/test/server/fixtures/index.js b/test/server/fixtures/index.js new file mode 100644 index 0000000..85a35d3 --- /dev/null +++ b/test/server/fixtures/index.js @@ -0,0 +1,15 @@ +'use strict'; +const Creds = require('./creds'); +const Db = require('./db'); +const Hapi = require('./hapi'); + + +class Fixtures {} + + +Fixtures.Creds = Creds; +Fixtures.Db = Db; +Fixtures.Hapi = Hapi; + + +module.exports = Fixtures; diff --git a/test/server/fixtures/make-mock-model.js b/test/server/fixtures/make-mock-model.js deleted file mode 100644 index a6fff78..0000000 --- a/test/server/fixtures/make-mock-model.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; -const MongoModels = require('mongo-models'); - - -const MakeMockModel = function () { - - const mock = {}; - - Reflect.ownKeys(MongoModels).forEach((key) => { - - mock[key] = MongoModels[key]; - }); - - return mock; -}; - - -module.exports = MakeMockModel; diff --git a/test/server/mailer.js b/test/server/mailer.js index f9af14e..c35d760 100644 --- a/test/server/mailer.js +++ b/test/server/mailer.js @@ -1,119 +1,69 @@ 'use strict'; const Code = require('code'); const Config = require('../../config'); -const Hapi = require('hapi'); const Lab = require('lab'); -const Proxyquire = require('proxyquire'); +const Mailer = require('../../server/mailer'); const lab = exports.lab = Lab.script(); -const stub = { - fs: {}, - nodemailer: { - createTransport: function (smtp) { - - return { - use: function () { - - return; - }, - sendMail: function (options, callback) { - - return callback(null, {}); - } - }; - } - } -}; -const MailerPlugin = Proxyquire('../../server/mailer', { - 'fs': stub.fs, - 'nodemailer': stub.nodemailer -}); - - -lab.experiment('Mailer Plugin', () => { - let server; +lab.experiment('Mailer', () => { - lab.before((done) => { + const Mailer_transport = Mailer.transport; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(MailerPlugin, (err) => { - if (err) { - return done(err); - } + lab.afterEach(() => { - done(); - }); + Mailer.transport = Mailer_transport; }); - lab.test('it successfuly registers itself', (done) => { + lab.test('it populates the template cache on first render', async () => { - Code.expect(server.plugins.mailer).to.be.an.object(); - Code.expect(server.plugins.mailer.sendEmail).to.be.a.function(); + const context = { username: 'ren', email: 'ren@stimpy.show' }; + const content = await Mailer.renderTemplate('welcome', context); - done(); + Code.expect(content).to.match(/ren@stimpy.show/i); }); - lab.test('it returns error when read file fails', (done) => { - - const realReadFile = stub.fs.readFile; - stub.fs.readFile = function (path, options, callback) { + lab.test('it uses the template cache on subsequent renders', async () => { - return callback(Error('read file failed')); - }; + const context = { username: 'stimpy', email: 'stimpy@ren.show' }; + const content = await Mailer.renderTemplate('welcome', context); - server.plugins.mailer.sendEmail({}, 'path', {}, (err, info) => { - - stub.fs.readFile = realReadFile; - Code.expect(err).to.be.an.object(); - - done(); - }); + Code.expect(content).to.match(/stimpy@ren.show/i); }); - lab.test('it sends an email', (done) => { - - const realReadFile = stub.fs.readFile; - stub.fs.readFile = function (path, options, callback) { - - return callback(null, ''); - }; + lab.test('it sends the email through the the transport', async () => { - server.plugins.mailer.sendEmail({}, 'path', {}, (err, info) => { + Mailer.transport = { + sendMail: function (options) { - Code.expect(err).to.not.exist(); - Code.expect(info).to.be.an.object(); + Code.expect(options).to.be.an.object(); + Code.expect(options.from).to.equal(Config.get('/system/fromAddress')); + Code.expect(options.cc).to.be.an.object(); + Code.expect(options.cc.email).to.equal('stimpy@ren.show'); - stub.fs.readFile = realReadFile; - - done(); - }); - }); - - - lab.test('it returns early with the template is cached', (done) => { - - const realReadFile = stub.fs.readFile; - stub.fs.readFile = function (path, options, callback) { - - return callback(null, ''); + return { wasSent: true }; + } }; - server.plugins.mailer.sendEmail({}, 'path', {}, (err, info) => { - - Code.expect(err).to.not.exist(); - Code.expect(info).to.be.an.object(); + const context = { username: 'stimpy', email: 'stimpy@ren.show' }; + const content = await Mailer.renderTemplate('welcome', context); + const options = { + cc: { + name: 'Stimpson J Cat', + email: 'stimpy@ren.show' + } + }; - stub.fs.readFile = realReadFile; + const info = await Mailer.sendEmail(options, 'welcome', context); - done(); - }); + Code.expect(info).to.be.an.object(); + Code.expect(info.wasSent).to.equal(true); + Code.expect(content).to.match(/stimpy@ren.show/i); }); }); diff --git a/test/server/models/account.js b/test/server/models/account.js index b4b26a3..870716a 100644 --- a/test/server/models/account.js +++ b/test/server/models/account.js @@ -1,118 +1,97 @@ 'use strict'; const Account = require('../../../server/models/account'); -const Async = require('async'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('Account Class Methods', () => { +lab.experiment('Account Model', () => { - lab.before((done) => { + lab.before(async () => { - Account.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); - }); + await Account.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.after((done) => { + lab.after(async () => { - Account.deleteMany({}, (err, count) => { + await Fixtures.Db.removeAllData(); - Account.disconnect(); - done(err); - }); + Account.disconnect(); }); - lab.test('it returns a new instance when create succeeds', (done) => { + lab.test('it parses names into name fields', () => { - Account.create('Ren Höek', (err, result) => { + const justFirst = Account.nameAdapter('Steve'); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Account); + Code.expect(justFirst).to.be.an.object(); + Code.expect(justFirst.first).to.equal('Steve'); + Code.expect(justFirst.middle).to.equal(''); + Code.expect(justFirst.last).to.equal(''); - done(); - }); - }); + const firstAndLast = Account.nameAdapter('Ren Höek'); + Code.expect(firstAndLast).to.be.an.object(); + Code.expect(firstAndLast.first).to.equal('Ren'); + Code.expect(firstAndLast.middle).to.equal(''); + Code.expect(firstAndLast.last).to.equal('Höek'); - lab.test('it correctly sets the middle name when create is called', (done) => { + const withMiddle = Account.nameAdapter('Stimpson J Cat'); - Account.create('Stimpson J Cat', (err, account) => { - - Code.expect(err).to.not.exist(); - Code.expect(account).to.be.an.instanceOf(Account); - Code.expect(account.name.middle).to.equal('J'); - - done(); - }); + Code.expect(withMiddle).to.be.an.object(); + Code.expect(withMiddle.first).to.equal('Stimpson'); + Code.expect(withMiddle.middle).to.equal('J'); + Code.expect(withMiddle.last).to.equal('Cat'); }); - lab.test('it returns an error when create fails', (done) => { - - const realInsertOne = Account.insertOne; - Account.insertOne = function () { + lab.test('it returns an instance when finding by username', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const document = new Account({ + name: Account.nameAdapter('Stimpson J Cat'), + user: { + id: '95EP150D35', + name: 'stimpy' + } + }); - callback(Error('insert failed')); - }; + await Account.insertOne(document); - Account.create('Stimpy Cat', (err, result) => { + const account = await Account.findByUsername('stimpy'); - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); + Code.expect(account).to.be.an.instanceOf(Account); + }); - Account.insertOne = realInsertOne; - done(); - }); - }); + lab.test('it returns a new instance when create succeeds', async () => { + const account = await Account.create('Ren Höek'); - lab.test('it returns a result when finding by username', (done) => { + Code.expect(account).to.be.an.instanceOf(Account); + }); - Async.auto({ - account: function (cb) { - Account.create('Stimpson J Cat', cb); - }, - accountUpdated: ['account', function (results, cb) { + lab.test('it links and unlinks users', async () => { - const fieldsToUpdate = { - $set: { - user: { - id: '95EP150D35', - name: 'stimpy' - } - } - }; + let account = await Account.create('Guinea Pig'); + const user = await User.create('guineapig', 'wheel', 'wood@chips.gov'); - Account.findByIdAndUpdate(results.account._id, fieldsToUpdate, cb); - }] - }, (err, results) => { + Code.expect(account.user).to.not.exist(); - if (err) { - return done(err); - } + account = await account.linkUser(`${user._id}`, user.username); - Account.findByUsername('stimpy', (err, account) => { + Code.expect(account.user).to.be.an.object(); - Code.expect(err).to.not.exist(); - Code.expect(account).to.be.an.instanceOf(Account); + account = await account.unlinkUser(); - done(); - }); - }); + Code.expect(account.user).to.not.exist(); }); }); diff --git a/test/server/models/admin-group.js b/test/server/models/admin-group.js index cb9c109..01231f9 100644 --- a/test/server/models/admin-group.js +++ b/test/server/models/admin-group.js @@ -2,123 +2,57 @@ const AdminGroup = require('../../../server/models/admin-group'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('AdminGroup Class Methods', () => { +lab.experiment('AdminGroup Model', () => { - lab.before((done) => { + lab.before(async () => { - AdminGroup.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); - }); - }); - - - lab.after((done) => { - - AdminGroup.deleteMany({}, (err, count) => { - - AdminGroup.disconnect(); - - done(err); - }); + await AdminGroup.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.test('it returns a new instance when create succeeds', (done) => { - - AdminGroup.create('Sales', (err, result) => { + lab.after(async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(AdminGroup); + await Fixtures.Db.removeAllData(); - done(); - }); + AdminGroup.disconnect(); }); - lab.test('it returns an error when create fails', (done) => { + lab.test('it returns a new instance when create succeeds', async () => { - const realInsertOne = AdminGroup.insertOne; - AdminGroup.insertOne = function () { + const adminGroup = await AdminGroup.create('Sales'); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('insert failed')); - }; - - AdminGroup.create('Support', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - AdminGroup.insertOne = realInsertOne; - - done(); - }); + Code.expect(adminGroup).to.be.an.instanceOf(AdminGroup); }); -}); - -lab.experiment('AdminGroup Instance Methods', () => { - lab.before((done) => { + lab.test('it returns false when permissions are missing', async () => { - AdminGroup.connect(mongoUri, mongoOptions, (err, db) => { + const adminGroup = await AdminGroup.create('Missing'); - done(err); - }); + Code.expect(adminGroup.hasPermissionTo('SPACE_MADNESS')).to.equal(false); }); - lab.after((done) => { + lab.test('it returns boolean values for set permissions', async () => { - AdminGroup.deleteMany({}, (err, result) => { + const adminGroup = await AdminGroup.create('Support'); - AdminGroup.disconnect(); - - done(err); - }); - }); - - - lab.test('it returns false when permissions are not found', (done) => { - - AdminGroup.create('Sales', (err, adminGroup) => { - - Code.expect(err).to.not.exist(); - Code.expect(adminGroup).to.be.an.instanceOf(AdminGroup); - Code.expect(adminGroup.hasPermissionTo('SPACE_MADNESS')).to.equal(false); - - done(); - }); - }); - - - lab.test('it returns boolean values for set permissions', (done) => { - - AdminGroup.create('Support', (err, adminGroup) => { - - Code.expect(err).to.not.exist(); - Code.expect(adminGroup).to.be.an.instanceOf(AdminGroup); - - adminGroup.permissions = { - SPACE_MADNESS: true, - UNTAMED_WORLD: false - }; - - Code.expect(adminGroup.hasPermissionTo('SPACE_MADNESS')).to.equal(true); - Code.expect(adminGroup.hasPermissionTo('UNTAMED_WORLD')).to.equal(false); + adminGroup.permissions = { + SPACE_MADNESS: true, + UNTAMED_WORLD: false + }; - done(); - }); + Code.expect(adminGroup.hasPermissionTo('SPACE_MADNESS')).to.equal(true); + Code.expect(adminGroup.hasPermissionTo('UNTAMED_WORLD')).to.equal(false); }); }); diff --git a/test/server/models/admin.js b/test/server/models/admin.js index c9ef8d1..c7d0e95 100644 --- a/test/server/models/admin.js +++ b/test/server/models/admin.js @@ -1,404 +1,186 @@ 'use strict'; -const Async = require('async'); +const Admin = require('../../../server/models/admin'); +const AdminGroup = require('../../../server/models/admin-group'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); -const Proxyquire = require('proxyquire'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); -const stub = { - AdminGroup: {} -}; -const Admin = Proxyquire('../../../server/models/admin', { - './admin-group': stub.AdminGroup -}); -const AdminGroup = require('../../../server/models/admin-group'); - +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('Admin Class Methods', () => { - lab.before((done) => { +lab.experiment('Admin Model', () => { - Admin.connect(mongoUri, mongoOptions, (err, db) => { + lab.before(async () => { - done(err); - }); + await Admin.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.after((done) => { - - Admin.deleteMany({}, (err, count) => { + lab.after(async () => { - Admin.disconnect(); + await Fixtures.Db.removeAllData(); - done(err); - }); + Admin.disconnect(); }); - lab.test('it returns a new instance when create succeeds', (done) => { + lab.test('it parses names into name fields', () => { - Admin.create('Ren Höek', (err, result) => { + const justFirst = Admin.nameAdapter('Steve'); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Admin); + Code.expect(justFirst).to.be.an.object(); + Code.expect(justFirst.first).to.equal('Steve'); + Code.expect(justFirst.middle).to.equal(''); + Code.expect(justFirst.last).to.equal(''); - done(); - }); - }); - - - lab.test('it correctly sets the middle name when create is called', (done) => { + const firstAndLast = Admin.nameAdapter('Ren Höek'); - Admin.create('Stimpson J Cat', (err, admin) => { + Code.expect(firstAndLast).to.be.an.object(); + Code.expect(firstAndLast.first).to.equal('Ren'); + Code.expect(firstAndLast.middle).to.equal(''); + Code.expect(firstAndLast.last).to.equal('Höek'); - Code.expect(err).to.not.exist(); - Code.expect(admin).to.be.an.instanceOf(Admin); - Code.expect(admin.name.middle).to.equal('J'); + const withMiddle = Admin.nameAdapter('Stimpson J Cat'); - done(); - }); + Code.expect(withMiddle).to.be.an.object(); + Code.expect(withMiddle.first).to.equal('Stimpson'); + Code.expect(withMiddle.middle).to.equal('J'); + Code.expect(withMiddle.last).to.equal('Cat'); }); - lab.test('it returns an error when create fails', (done) => { - - const realInsertOne = Admin.insertOne; - Admin.insertOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns a new instance when create succeeds', async () => { - callback(Error('insert failed')); - }; + const admin = await Admin.create('Ren Höek'); - Admin.create('Stimpy Cat', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - Admin.insertOne = realInsertOne; - - done(); - }); + Code.expect(admin).to.be.an.instanceOf(Admin); }); - lab.test('it returns a result when finding by username', (done) => { - - Async.auto({ - admin: function (cb) { - - Admin.create('Ren Höek', cb); - }, - adminUpdated: ['admin', function (results, cb) { - - const fieldsToUpdate = { - $set: { - user: { - id: '95EP150D35', - name: 'ren' - } - } - }; + lab.test('it returns an instance when finding by username', async () => { - Admin.findByIdAndUpdate(results.admin._id, fieldsToUpdate, cb); - }] - }, (err, results) => { - - if (err) { - return done(err); + const document = new Admin({ + name: Admin.nameAdapter('Stimpson J Cat'), + user: { + id: '95EP150D35', + name: 'stimpy' } - - Admin.findByUsername('ren', (err, admin) => { - - Code.expect(err).to.not.exist(); - Code.expect(admin).to.be.an.instanceOf(Admin); - - done(); - }); - }); - }); -}); - - -lab.experiment('Admin Instance Methods', () => { - - lab.before((done) => { - - Admin.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); }); - }); - - lab.after((done) => { + await Admin.insertOne(document); - Admin.deleteMany({}, (err, result) => { + const account = await Admin.findByUsername('stimpy'); - Admin.disconnect(); - - done(err); - }); + Code.expect(account).to.be.an.instanceOf(Admin); }); - lab.test('it returns false when groups are not found', (done) => { + lab.test('it returns false when checking for membership when groups are missing', async () => { - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - } - }); + const admin = await Admin.create('Ren Höek'); Code.expect(admin.isMemberOf('sales')).to.equal(false); - - done(); - }); - - - lab.test('it returns boolean values for set group memberships', (done) => { - - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - sales: 'Sales', - support: 'Support' - } - }); - - Code.expect(admin.isMemberOf('sales')).to.equal(true); - Code.expect(admin.isMemberOf('support')).to.equal(true); - - done(); - }); - - - lab.test('it exits early when hydrating groups where groups are missing', (done) => { - - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - } - }); - - admin.hydrateGroups((err) => { - - Code.expect(err).to.not.exist(); - - done(); - }); }); - lab.test('it exits early when hydrating groups where hydrated groups exist', (done) => { - - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - sales: 'Sales' - }, - _groups: { - sales: new AdminGroup({ - _id: 'sales', - name: 'Sales', - permissions: { - SPACE_MADNESS: true, - UNTAMED_WORLD: false - } - }) - } - }); - - admin.hydrateGroups((err) => { + lab.test('it returns false when permissions are missing', async () => { - Code.expect(err).to.not.exist(); + const admin = await Admin.create('Ren Höek'); + const hasPermission = await admin.hasPermissionTo('SPACE_MADNESS'); - done(); - }); + Code.expect(hasPermission).to.equal(false); }); - lab.test('it returns an error when hydrating groups and find by id fails', (done) => { - - const realFindById = stub.AdminGroup.findById; - stub.AdminGroup.findById = function (id, callback) { - - callback(Error('find by id failed')); - }; + lab.test('it returns boolean values when the permission exists on the admin', async () => { const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - sales: 'Sales' + name: Admin.nameAdapter('Ren Höek'), + permissions: { + SPACE_MADNESS: true, + UNTAMED_WORLD: false } }); + const hasPermission = await admin.hasPermissionTo('SPACE_MADNESS'); - admin.hydrateGroups((err) => { - - Code.expect(err).to.be.an.object(); - - stub.AdminGroup.findById = realFindById; - - done(); - }); + Code.expect(hasPermission).to.equal(true); }); - lab.test('it successfully hydrates groups', (done) => { + lab.test('it returns boolean values when permission exits on the admin group', async () => { - const realFindById = stub.AdminGroup.findById; - stub.AdminGroup.findById = function (id, callback) { + // create groups - const group = new AdminGroup({ - _id: 'sales', - name: 'Sales', - permissions: { - SPACE_MADNESS: true, - UNTAMED_WORLD: false - } - }); - - callback(null, group); - }; - - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - sales: 'Sales' + const salesGroup = new AdminGroup({ + _id: 'sales', + name: 'Sales', + permissions: { + UNTAMED_WORLD: false, + WORLD_UNTAMED: true } }); - - admin.hydrateGroups((err) => { - - Code.expect(err).to.not.exist(); - - stub.AdminGroup.findById = realFindById; - - done(); - }); - }); - - - lab.test('it exits early when the permission exists on the admin', (done) => { - - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, + const supportGroup = new AdminGroup({ + _id: 'support', + name: 'Support', permissions: { SPACE_MADNESS: true, - UNTAMED_WORLD: false + MADNESS_SPACE: false } }); - admin.hasPermissionTo('SPACE_MADNESS', (err, permit) => { + await AdminGroup.insertMany([salesGroup, supportGroup]); - Code.expect(err).to.not.exist(); - Code.expect(permit).to.equal(true); + // admin without group membership - done(); + const documentA = new Admin({ + name: Admin.nameAdapter('Ren Höek') }); - }); + const testA1 = await documentA.hasPermissionTo('SPACE_MADNESS'); + Code.expect(testA1).to.equal(false); - lab.test('it returns an error when checking permission and hydrating groups fails', (done) => { + const testA2 = await documentA.hasPermissionTo('UNTAMED_WORLD'); - const realHydrateGroups = Admin.prototype.hydrateGroups; - Admin.prototype.hydrateGroups = function (callback) { + Code.expect(testA2).to.equal(false); - callback(Error('hydrate groups failed')); - }; + // admin with group membership - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, + const documentB = new Admin({ + name: Admin.nameAdapter('Ren B Höek'), groups: { - sales: 'Sales' + sales: 'Sales', + support: 'Support' } }); - admin.hasPermissionTo('SPACE_MADNESS', (err) => { + const testB1 = await documentB.hasPermissionTo('SPACE_MADNESS'); - Code.expect(err).to.be.an.object(); + Code.expect(testB1).to.equal(true); - Admin.prototype.hydrateGroups = realHydrateGroups; + const testB2 = await documentB.hasPermissionTo('UNTAMED_WORLD'); - done(); - }); + Code.expect(testB2).to.equal(false); }); - lab.test('it returns correct permission from hydrated group permissions', (done) => { + lab.test('it links and unlinks users', async () => { - const admin = new Admin({ - name: { - first: 'Ren', - last: 'Höek' - }, - groups: { - sales: 'Sales', - support: 'Support' - } - }); + let admin = await Admin.create('Guinea Pig'); + const user = await User.create('guineapig', 'wheel', 'wood@chips.gov'); - admin._groups = { - sales: new AdminGroup({ - _id: 'sales', - name: 'Sales', - permissions: { - UNTAMED_WORLD: false, - WORLD_UNTAMED: true - } - }), - support: new AdminGroup({ - _id: 'support', - name: 'Support', - permissions: { - SPACE_MADNESS: true, - MADNESS_SPACE: false - } - }) - }; - - Async.auto({ - test1: function (cb) { - - admin.hasPermissionTo('SPACE_MADNESS', cb); - }, - test2: function (cb) { - - admin.hasPermissionTo('UNTAMED_WORLD', cb); - } - }, (err, results) => { + Code.expect(admin.user).to.not.exist(); - Code.expect(err).to.not.exist(); - Code.expect(results.test1).to.equal(true); - Code.expect(results.test2).to.equal(false); + admin = await admin.linkUser(`${user._id}`, user.username); - done(err); - }); + Code.expect(admin.user).to.be.an.object(); + + admin = await admin.unlinkUser(); + + Code.expect(admin.user).to.not.exist(); }); }); diff --git a/test/server/models/auth-attempt.js b/test/server/models/auth-attempt.js index a048e6c..5387fc2 100644 --- a/test/server/models/auth-attempt.js +++ b/test/server/models/auth-attempt.js @@ -1,181 +1,78 @@ 'use strict'; -const Async = require('async'); const AuthAttempt = require('../../../server/models/auth-attempt'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('AuthAttempt Class Methods', () => { +lab.experiment('AuthAttempt Model', () => { - lab.before((done) => { + lab.before(async () => { - AuthAttempt.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); - }); - }); - - - lab.after((done) => { - - AuthAttempt.deleteMany({}, (err, count) => { - - AuthAttempt.disconnect(); - - done(err); - }); - }); - - - lab.test('it returns a new instance when create succeeds', (done) => { - - AuthAttempt.create('127.0.0.1', 'ren', 'userAgent', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(AuthAttempt); - - done(); - }); + await AuthAttempt.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.test('it returns a new instance when create succeeds with a userAgent', (done) => { - - AuthAttempt.create('127.0.0.1', 'ren', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', (err, result) => { + lab.after(async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(AuthAttempt); + await Fixtures.Db.removeAllData(); - done(); - }); + AuthAttempt.disconnect(); }); - lab.test('it returns an error when create fails', (done) => { - - const realInsertOne = AuthAttempt.insertOne; - AuthAttempt.insertOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('insert failed')); - }; - - AuthAttempt.create('127.0.0.1', 'ren', 'userAgent', (err, result) => { + lab.afterEach(async () => { - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - AuthAttempt.insertOne = realInsertOne; - - done(); - }); + await AuthAttempt.deleteMany({}); }); - lab.test('it returns false when abuse is not detected', (done) => { - - AuthAttempt.abuseDetected('127.0.0.1', 'ren', (err, result) => { + lab.test('it returns false when abuse is not detected', async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.equal(false); + const result = await AuthAttempt.abuseDetected('127.0.0.1', 'ren'); - done(); - }); + Code.expect(result).to.equal(false); }); - lab.test('it returns true when abuse is detected for user + ip combo', (done) => { - - const authAttemptsConfig = Config.get('/authAttempts'); - const authSpam = []; - const authRequest = function (cb) { - - AuthAttempt.create('127.0.0.1', 'stimpy', 'userAgent', (err, result) => { + lab.test('it detects login abuse from an ip and many users', async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.object(); + const attemptConfig = Config.get('/authAttempts'); + const authRequest = (i) => AuthAttempt.create('127.0.0.2', `mudskipper${i}`); + const authSpam = Array(attemptConfig.forIp).fill().map((_, i) => authRequest(i)); - cb(); - }); - }; + await Promise.all(authSpam); - for (let i = 0; i < authAttemptsConfig.forIpAndUser; ++i) { - authSpam.push(authRequest); - } + const result = await AuthAttempt.abuseDetected('127.0.0.2', 'yak'); - Async.parallel(authSpam, () => { - - AuthAttempt.abuseDetected('127.0.0.1', 'stimpy', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.equal(true); - - done(); - }); - }); + Code.expect(result).to.equal(true); }); - lab.test('it returns true when abuse is detected for an ip and multiple users', (done) => { - - const authAttemptsConfig = Config.get('/authAttempts'); - const authSpam = []; - const authRequest = function (i, cb) { + lab.test('it detects login abuse from an ip and one user', async () => { - const randomUsername = 'mudskipper' + i; - AuthAttempt.create('127.0.0.2', randomUsername, 'userAgent',(err, result) => { + const attemptConfig = Config.get('/authAttempts'); + const authRequest = () => AuthAttempt.create('127.0.0.3', 'steve'); + const authSpam = Array(attemptConfig.forIpAndUser).fill().map((_) => authRequest()); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.object(); + await Promise.all(authSpam); - cb(); - }); - }; + const result = await AuthAttempt.abuseDetected('127.0.0.3', 'steve'); - for (let i = 0; i < authAttemptsConfig.forIp; ++i) { - authSpam.push(authRequest.bind(null, i)); - } - - Async.parallel(authSpam, () => { - - AuthAttempt.abuseDetected('127.0.0.2', 'yak', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.equal(true); - - done(); - }); - }); + Code.expect(result).to.equal(true); }); - lab.test('it returns an error when count fails', (done) => { - - const realCount = AuthAttempt.count; - AuthAttempt.count = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('count failed')); - }; - - AuthAttempt.abuseDetected('127.0.0.1', 'toastman', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); + lab.test('it returns a new instance when create succeeds', async () => { - AuthAttempt.count = realCount; + const authAttempt = await AuthAttempt.create('127.0.0.4', 'ren'); - done(); - }); + Code.expect(authAttempt).to.be.an.instanceOf(AuthAttempt); }); }); diff --git a/test/server/models/note-entry.js b/test/server/models/note-entry.js index 3ba6287..c65929f 100644 --- a/test/server/models/note-entry.js +++ b/test/server/models/note-entry.js @@ -7,14 +7,18 @@ const NoteEntry = require('../../../server/models/note-entry'); const lab = exports.lab = Lab.script(); -lab.experiment('Status Entry Class', () => { +lab.experiment('NoteEntry Model', () => { - lab.test('it instantiates an instance', (done) => { + lab.test('it instantiates an instance', () => { - const noteEntry = new NoteEntry({}); + const noteEntry = new NoteEntry({ + data: 'Important stuff.', + adminCreated: { + id: '111111111111111111111111', + name: 'Root Admin' + } + }); Code.expect(noteEntry).to.be.an.instanceOf(NoteEntry); - - done(); }); }); diff --git a/test/server/models/session.js b/test/server/models/session.js index f6c0b9a..2fc307c 100644 --- a/test/server/models/session.js +++ b/test/server/models/session.js @@ -1,226 +1,111 @@ 'use strict'; -const Async = require('async'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); -const Proxyquire = require('proxyquire'); +const Session = require('../../../server/models/session'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); -const stub = { - bcrypt: {} -}; -const Session = Proxyquire('../../../server/models/session', { bcrypt: stub.bcrypt }); +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('Session Class Methods', () => { +lab.experiment('Session Model', () => { - lab.before((done) => { + lab.before(async () => { - Session.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); - }); + await Session.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.after((done) => { - - Session.deleteMany({}, (err, count) => { + lab.after(async () => { - Session.disconnect(); + await Fixtures.Db.removeAllData(); - done(err); - }); + Session.disconnect(); }); - lab.test('it creates a key hash combination', (done) => { - - Session.generateKeyHash((err, result) => { + lab.test('it returns a new instance when create succeeds', async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.object(); - Code.expect(result.key).to.be.a.string(); - Code.expect(result.hash).to.be.a.string(); + const session = await Session.create('ren', 'ip', 'userAgent'); - done(); - }); + Code.expect(session).to.be.an.instanceOf(Session); }); - lab.test('it returns an error when key hash fails', (done) => { - - const realGenSalt = stub.bcrypt.genSalt; - stub.bcrypt.genSalt = function (rounds, callback) { - - callback(Error('bcrypt failed')); - }; - - Session.generateKeyHash((err, result) => { + lab.test('it returns undefined when finding by credentials session misses', async () => { - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); + const id = '555555555555555555555555'; + const keyHash = await Session.generateKeyHash(); + const session = await Session.findByCredentials(id, keyHash.key); - stub.bcrypt.genSalt = realGenSalt; - - done(); - }); + Code.expect(session).to.be.undefined(); }); - lab.test('it returns a new instance when create succeeds', (done) => { - - Session.create('ren', 'ip', 'userAgent', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Session); - - done(); - }); - }); - - lab.test('it returns an error when create fails', (done) => { - - const realInsertOne = Session.insertOne; - Session.insertOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('insert failed')); - }; - - Session.create('ren', 'ip', 'userAgent', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - Session.insertOne = realInsertOne; - - done(); - }); - }); - - - lab.test('it returns a result when finding by credentials', (done) => { - - Async.auto({ - session: function (cb) { + lab.test('it returns undefined when finding by credentials session hits and key match misses', async () => { - Session.create('1D', 'ip', 'userAgent', (err, result) => { + const userId = '000000000000000000000000'; + const ip = '127.0.0.1'; + const userAgent = [ + 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)', + ' AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405' + ].join(''); + const session = await Session.create(userId, ip, userAgent); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Session); + Code.expect(session).to.be.an.instanceOf(Session); - cb(null, result); - }); - } - }, (err, results) => { + const key = `${session.key}poison`; + const result = await Session.findByCredentials(session._id, key); - if (err) { - return done(err); - } - - const id = results.session._id.toString(); - const key = results.session.key; - - Session.findByCredentials(id, key, (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Session); - - done(); - }); - }); + Code.expect(result).to.be.undefined(); }); - lab.test('it returns nothing for find by credentials when key match fails', (done) => { - - const realFindById = Session.findById; - Session.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + lab.test('it returns a session instance when finding by credentials hits and key match hits', async () => { - callback(null, { _id: '2D', userId: '1D', key: 'letmein' }); - }; + const userId = '000000000000000000000000'; + const ip = '127.0.0.1'; + const userAgent = [ + 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)', + ' AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405' + ].join(''); + const session = await Session.create(userId, ip, userAgent); - const realCompare = stub.bcrypt.compare; - stub.bcrypt.compare = function (key, source, callback) { + Code.expect(session).to.be.an.instanceOf(Session); - callback(null, false); - }; + const key = session.key; + const result = await Session.findByCredentials(session._id, key); - Session.findByCredentials('2D', 'doorislocked', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.not.exist(); - - Session.findById = realFindById; - stub.bcrypt.compare = realCompare; - - done(); - }); + Code.expect(result).to.be.an.instanceOf(Session); + Code.expect(session._id).to.equal(result._id); }); - lab.test('it returns an error when finding by credentials fails', (done) => { - - const realFindById = Session.findById; - Session.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('find by id failed')); - }; + lab.test('it creates a key hash combination', async () => { - Session.findByCredentials('2D', 'dog', (err, result) => { + const result = await Session.generateKeyHash(); - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - Session.findById = realFindById; - - done(); - }); + Code.expect(result).to.be.an.object(); + Code.expect(result.key).to.be.a.string(); + Code.expect(result.hash).to.be.a.string(); }); - lab.test('it returns early when finding by credentials misses', (done) => { - - const realFindById = Session.findById; - Session.findById = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(); - }; - - Session.findByCredentials('2D', 'dog', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.not.exist(); - - Session.findById = realFindById; - - done(); - }); - }); - - lab.test('it returns a new instance when parsed a userAgent ', (done) => { + lab.test('it updates the last active time of an instance', async () => { - Session.create('ren', 'ip', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36', (err, result) => { + const userId = '000000000000000000000000'; + const ip = '127.0.0.1'; + const userAgent = [ + 'Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)', + ' AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405' + ].join(''); + const session = await Session.create(userId, ip, userAgent); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Session); + await session.updateLastActive(); - done(); - }); + Code.expect(session.lastActive).to.be.a.date(); }); }); diff --git a/test/server/models/status-entry.js b/test/server/models/status-entry.js index c392e43..dfd5966 100644 --- a/test/server/models/status-entry.js +++ b/test/server/models/status-entry.js @@ -7,14 +7,19 @@ const StatusEntry = require('../../../server/models/status-entry'); const lab = exports.lab = Lab.script(); -lab.experiment('Status Entry Class', () => { +lab.experiment('Status Model', () => { - lab.test('it instantiates an instance', (done) => { + lab.test('it instantiates an instance', () => { - const statusEntry = new StatusEntry({}); + const statusEntry = new StatusEntry({ + id: 'account-happy', + name: 'Happy', + adminCreated: { + id: '111111111111111111111111', + name: 'Root Admin' + } + }); Code.expect(statusEntry).to.be.an.instanceOf(StatusEntry); - - done(); }); }); diff --git a/test/server/models/status.js b/test/server/models/status.js index af93bf6..6552f5f 100644 --- a/test/server/models/status.js +++ b/test/server/models/status.js @@ -1,68 +1,39 @@ 'use strict'; const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); const Status = require('../../../server/models/status'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('Status Class Methods', () => { +lab.experiment('Status Model', () => { - lab.before((done) => { + lab.before(async () => { - Status.connect(mongoUri, mongoOptions, (err, db) => { - - done(err); - }); + await Status.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.after((done) => { - - Status.deleteMany({}, (err, count) => { + lab.after(async () => { - Status.disconnect(); + await Fixtures.Db.removeAllData(); - done(err); - }); + Status.disconnect(); }); - lab.test('it returns a new instance when create succeeds', (done) => { - - Status.create('Order', 'Complete', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(Status); - - done(); - }); - }); - - - lab.test('it returns an error when create fails', (done) => { - - const realInsertOne = Status.insertOne; - Status.insertOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(Error('insert failed')); - }; - - Status.create('Order', 'Fulfilled', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); + lab.test('it returns a new instance when create succeeds', async () => { - Status.insertOne = realInsertOne; + const status = await Status.create('Order', 'Complete'); - done(); - }); + Code.expect(status).to.be.an.instanceOf(Status); + Code.expect(status._id).to.equal('order-complete'); + Code.expect(status.name).to.equal('Complete'); + Code.expect(status.pivot).to.equal('Order'); }); }); diff --git a/test/server/models/user.js b/test/server/models/user.js index 4a10958..aca22ea 100644 --- a/test/server/models/user.js +++ b/test/server/models/user.js @@ -1,418 +1,225 @@ 'use strict'; -const Async = require('async'); +const Account = require('../../../server/models/account'); +const Admin = require('../../../server/models/admin'); const Code = require('code'); const Config = require('../../../config'); +const Fixtures = require('../fixtures'); const Lab = require('lab'); -const Proxyquire = require('proxyquire'); +const User = require('../../../server/models/user'); const lab = exports.lab = Lab.script(); -const mongoUri = Config.get('/hapiMongoModels/mongodb/uri'); -const mongoOptions = Config.get('/hapiMongoModels/mongodb/options'); -const stub = { - Account: {}, - Admin: {}, - bcrypt: {} -}; -const User = Proxyquire('../../../server/models/user', { - './account': stub.Account, - './admin': stub.Admin, - bcrypt: stub.bcrypt -}); -const Admin = require('../../../server/models/admin'); -const Account = require('../../../server/models/account'); - +const config = Config.get('/hapiMongoModels/mongodb'); -lab.experiment('User Class Methods', () => { - lab.before((done) => { +lab.experiment('User Model', () => { - User.connect(mongoUri, mongoOptions, (err, db) => { + lab.before(async () => { - done(err); - }); + await User.connect(config.connection, config.options); + await Fixtures.Db.removeAllData(); }); - lab.after((done) => { - - User.deleteMany({}, (err, count) => { + lab.after(async () => { - User.disconnect(); + await Fixtures.Db.removeAllData(); - done(err); - }); + User.disconnect(); }); - lab.test('it creates a password hash combination', (done) => { + lab.test('it returns a new instance when create succeeds', async () => { - User.generatePasswordHash('bighouseblues', (err, result) => { + const user = await User.create('ren', 'bighouseblues', 'ren@stimpy.show'); - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.object(); - Code.expect(result.password).to.be.a.string(); - Code.expect(result.hash).to.be.a.string(); - - done(); - }); + Code.expect(user).to.be.an.instanceOf(User); }); - lab.test('it returns an error when password hash fails', (done) => { - - const realGenSalt = stub.bcrypt.genSalt; - stub.bcrypt.genSalt = function (rounds, callback) { + lab.test('it returns undefined when finding by credentials user misses', async () => { - callback(Error('bcrypt failed')); - }; + const user = await User.findByCredentials('steve', '123456'); - User.generatePasswordHash('bighouseblues', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - stub.bcrypt.genSalt = realGenSalt; - - done(); - }); + Code.expect(user).to.be.undefined(); }); - lab.test('it returns a new instance when create succeeds', (done) => { - - User.create('ren', 'bighouseblues', 'ren@stimpy.show', (err, result) => { + lab.test('it returns undefined when finding by credentials user hits and password match misses', async () => { - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(User); + const user = await User.findByCredentials('ren', '123456'); - done(); - }); + Code.expect(user).to.be.undefined(); }); - lab.test('it returns an error when create fails', (done) => { + lab.test('it returns an instance when finding by credentials user hits and password match hits', async () => { - const realInsertOne = User.insertOne; - User.insertOne = function () { + const withUsername = await User.findByCredentials('ren', 'bighouseblues'); - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + Code.expect(withUsername).to.be.an.instanceOf(User); - callback(Error('insert failed')); - }; + const withEmail = await User.findByCredentials('ren@stimpy.show', 'bighouseblues'); - User.create('ren', 'bighouseblues', 'ren@stimpy.show', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - User.insertOne = realInsertOne; - - done(); - }); + Code.expect(withEmail).to.be.an.instanceOf(User); }); - lab.test('it returns a result when finding by login', (done) => { - - Async.auto({ - user: function (cb) { - - User.create('stimpy', 'thebigshot', 'stimpy@ren.show', cb); - }, - username: ['user', function (results, cb) { + lab.test('it returns an instance when finding by email', async () => { - User.findByCredentials(results.user.username, results.user.password, cb); - }], - email: ['user', function (results, cb) { + const user = await User.findByEmail('ren@stimpy.show'); - User.findByCredentials(results.user.email, results.user.password, cb); - }] - }, (err, results) => { - - Code.expect(err).to.not.exist(); - Code.expect(results.user).to.be.an.instanceOf(User); - Code.expect(results.username).to.be.an.instanceOf(User); - Code.expect(results.email).to.be.an.instanceOf(User); - - done(); - }); + Code.expect(user).to.be.an.instanceOf(User); }); - lab.test('it returns nothing for find by credentials when password match fails', (done) => { - - const realFindOne = User.findOne; - User.findOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(null, { username: 'toastman', password: 'letmein' }); - }; + lab.test('it returns an instance when finding by username', async () => { - const realCompare = stub.bcrypt.compare; - stub.bcrypt.compare = function (key, source, callback) { + const user = await User.findByUsername('ren'); - callback(null, false); - }; - - User.findByCredentials('toastman', 'doorislocked', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.not.exist(); - - User.findOne = realFindOne; - stub.bcrypt.compare = realCompare; - - done(); - }); + Code.expect(user).to.be.an.instanceOf(User); }); - lab.test('it returns early when finding by login misses', (done) => { - - const realFindOne = User.findOne; - User.findOne = function () { - - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); - - callback(); - }; - - User.findByCredentials('stimpy', 'dog', (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.not.exist(); - - User.findOne = realFindOne; - - done(); - }); - }); - - - lab.test('it returns an error when finding by login fails', (done) => { - - const realFindOne = User.findOne; - User.findOne = function () { + lab.test('it creates a password hash combination', async () => { - const args = Array.prototype.slice.call(arguments); - const callback = args.pop(); + const password = '3l1t3f00&&b4r'; + const result = await User.generatePasswordHash(password); - callback(Error('find one failed')); - }; - - User.findByCredentials('stimpy', 'dog', (err, result) => { - - Code.expect(err).to.be.an.object(); - Code.expect(result).to.not.exist(); - - User.findOne = realFindOne; - - done(); - }); + Code.expect(result).to.be.an.object(); + Code.expect(result.password).to.equal(password); + Code.expect(result.hash).to.be.a.string(); }); - lab.test('it returns a result when finding by username', (done) => { - - Async.auto({ - user: function (cb) { + lab.test('it returns boolean values when checking if a user can play roles', async () => { - User.create('horseman', 'eathay', 'horse@man.show', (err, result) => { + let user; - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(User); - - cb(null, result); - }); - } - }, (err, results) => { - - if (err) { - return done(err); + user = await User.findByUsername('ren'); + user = await User.findByIdAndUpdate(user._id, { + $set: { + roles: { + account: { + id: '555555555555555555555555', + name: 'Ren Hoek' + } + } } - - const username = results.user.username; - - User.findByUsername(username, (err, result) => { - - Code.expect(err).to.not.exist(); - Code.expect(result).to.be.an.instanceOf(User); - - done(); - }); }); - }); -}); - - -lab.experiment('User Instance Methods', () => { - - lab.test('it returns false when roles are missing', (done) => { - - const user = new User({ username: 'ren' }); Code.expect(user.canPlayRole('admin')).to.equal(false); - - done(); + Code.expect(user.canPlayRole('account')).to.equal(true); }); - lab.test('it returns correctly for the specified role', (done) => { + lab.test('it hydrates roles when both admin and account are missing', async () => { + + let user; - const user = new User({ - username: 'ren', - roles: { - account: { _id: '953P150D35' } + user = await User.findByUsername('ren'); + user = await User.findByIdAndUpdate(user._id, { + $set: { + roles: {} } }); - Code.expect(user.canPlayRole('admin')).to.equal(false); - Code.expect(user.canPlayRole('account')).to.equal(true); + await user.hydrateRoles(); - done(); + Code.expect(user._roles).to.be.an.object(); + Code.expect(Object.keys(user._roles)).to.have.length(0); }); - lab.test('it exits early when hydrating roles where roles are missing', (done) => { - - const user = new User({ username: 'ren' }); - - user.hydrateRoles((err) => { - - Code.expect(err).to.not.exist(); - - done(); - }); - }); + lab.test('it hydrates roles when an account role is present', async () => { + const account = await Account.create('Run Hoek'); - lab.test('it exits early when hydrating roles where hydrated roles exist', (done) => { + let user; - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' + user = await User.findByUsername('ren'); + user = await User.findByIdAndUpdate(user._id, { + $set: { + roles: { + account: { + id: `${account._id}`, + name: `${account.name.first} ${account.name.last}` + } } } }); - user._roles = { - admin: { - _id: '953P150D35', - name: 'Ren Höek' - } - }; - - user.hydrateRoles((err) => { + await user.hydrateRoles(); - Code.expect(err).to.not.exist(); - - done(); - }); + Code.expect(user._roles).to.be.an.object(); + Code.expect(Object.keys(user._roles)).to.have.length(1); + Code.expect(user._roles.account).to.be.an.instanceOf(Account); }); - lab.test('it returns an error when hydrating roles and find by id fails', (done) => { + lab.test('it hydrates roles when an admin role is present', async () => { - const realFindById = stub.Admin.findById; - stub.Admin.findById = function (id, callback) { + const admin = await Admin.create('Run Hoek'); - callback(Error('find by id failed')); - }; + let user; - const user = new User({ - username: 'ren', - roles: { - admin: { - id: '953P150D35', - name: 'Ren Höek' + user = await User.findByUsername('ren'); + user = await User.findByIdAndUpdate(user._id, { + $set: { + roles: { + admin: { + id: `${admin._id}`, + name: `${admin.name.first} ${admin.name.last}` + } } } }); - user.hydrateRoles((err) => { - - Code.expect(err).to.be.an.object(); + await user.hydrateRoles(); - stub.Admin.findById = realFindById; - - done(); - }); + Code.expect(user._roles).to.be.an.object(); + Code.expect(Object.keys(user._roles)).to.have.length(1); + Code.expect(user._roles.admin).to.be.an.instanceOf(Admin); }); - lab.test('it returns successful when hydrating roles', (done) => { + lab.test('it links and unlinks roles', async () => { - const realAccountFindById = stub.Account.findById; - stub.Admin.findById = function (id, callback) { + let user = await User.create('guineapig', 'wheel', 'wood@chips.gov'); + const [admin, account] = await Promise.all([ + Admin.create('Guinea Pig'), + Account.create('Guinea Pig') + ]); - callback(null, new Admin({ - _id: '953P150D35', - name: { - first: 'Ren', - last: 'Höek' - } - })); - }; - - const realAdminFindById = stub.Admin.findById; - stub.Account.findById = function (id, callback) { - - callback(null, new Account({ - _id: '5250W35', - name: { - first: 'Stimpson', - middle: 'J', - last: 'Cat' - } - })); - }; - - const user = new User({ - username: 'ren', - roles: { - account: { - id: '5250W35', - name: 'Stimpson J Cat' - }, - admin: { - id: '953P150D35', - name: 'Ren Höek' - } - } - }); + Code.expect(user.roles.admin).to.not.exist(); + Code.expect(user.roles.account).to.not.exist(); - user.hydrateRoles((err) => { + user = await user.linkAdmin(`${admin._id}`, `${admin.name.first} ${admin.name.last}`); + user = await user.linkAccount(`${account._id}`, `${account.name.first} ${account.name.last}`); - Code.expect(err).to.not.exist(); + Code.expect(user.roles.admin).to.be.an.object(); + Code.expect(user.roles.account).to.be.an.object(); - stub.Account.findById = realAccountFindById; - stub.Admin.findById = realAdminFindById; + user = await user.unlinkAdmin(); + user = await user.unlinkAccount(); - done(); - }); + Code.expect(user.roles.admin).to.not.exist(); + Code.expect(user.roles.account).to.not.exist(); }); - lab.test('it returns successful when hydrating roles where there are none defined', (done) => { + lab.test('it hydrates roles and caches the results for subsequent access', async () => { - const user = new User({ - username: 'ren', - roles: {} - }); + const user = await User.findByUsername('ren'); - user.hydrateRoles((err) => { + await user.hydrateRoles(); - Code.expect(err).to.not.exist(); + Code.expect(user._roles).to.be.an.object(); + Code.expect(Object.keys(user._roles)).to.have.length(1); + Code.expect(user._roles.admin).to.be.an.instanceOf(Admin); - done(); - }); + const roles = await user.hydrateRoles(); + + Code.expect(user._roles).to.equal(roles); }); }); diff --git a/test/server/preware.js b/test/server/preware.js new file mode 100644 index 0000000..522de89 --- /dev/null +++ b/test/server/preware.js @@ -0,0 +1,153 @@ +'use strict'; +const Auth = require('../../server/auth'); +const Code = require('code'); +const Fixtures = require('./fixtures'); +const Hapi = require('hapi'); +const Lab = require('lab'); +const Manifest = require('../../manifest'); +const Preware = require('../../server/preware'); +const Session = require('../../server/models/session'); + + +const lab = exports.lab = Lab.script(); +let server; +let adminAuthHeader; + + +lab.before(async () => { + + server = Hapi.Server(); + + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Auth.dependencies.includes(entry.plugin)) + .map((entry) => { + + entry.plugin = require(entry.plugin); + + return entry; + }); + + plugins.push(Auth); + + await server.register(plugins); + await server.start(); + await Fixtures.Db.removeAllData(); + + const auth = { strategy: 'simple', scope: 'admin' }; + const handler = (request, h) => ({ message: 'ok' }); + + server.route({ + method: 'GET', + path: '/limited/to/root/group', + config: { + auth, + pre: [ + Preware.requireAdminGroup('root') + ] + }, + handler + }); + + server.route({ + method: 'GET', + path: '/limited/to/multiple/groups', + config: { + auth, + pre: [ + Preware.requireAdminGroup(['sales', 'support']) + ] + }, + handler + }); + + server.route({ + method: 'GET', + path: '/just/not/the/root/user', + config: { + auth, + pre: [ + Preware.requireNotRootUser + ] + }, + handler + }); + + const { user: adminUser } = await Fixtures.Creds.createAdminUser( + 'Ren Hoek', 'ren', 'baddog', 'ren@stimpy.show', ['Sales'] + ); + const adminSession = await Session.create(`${adminUser._id}`, '127.0.0.1', 'Lab'); + + adminAuthHeader = Fixtures.Creds.authHeader(adminSession._id, adminSession.key); +}); + + +lab.after(async () => { + + await Fixtures.Db.removeAllData(); + await server.stop(); +}); + + +lab.experiment('Preware', () => { + + lab.test('it prevents access when group membership misses', async () => { + + const request = { + method: 'GET', + url: '/limited/to/root/group', + headers: { + authorization: adminAuthHeader + } + }; + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(403); + }); + + + lab.test('it grants access when group membership hits', async () => { + + const request = { + method: 'GET', + url: '/limited/to/multiple/groups', + headers: { + authorization: adminAuthHeader + } + }; + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(200); + }); + + + lab.test('it prevents access to the root user', async () => { + + const root = await Fixtures.Creds.createRootAdminUser(); + const rootSession = await Session.create(`${root.user._id}`, '127.0.0.1', 'Lab'); + const request = { + method: 'GET', + url: '/just/not/the/root/user', + headers: { + authorization: Fixtures.Creds.authHeader(rootSession._id, rootSession.key) + } + }; + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(403); + }); + + + lab.test('it grants access to non-root users', async () => { + + const request = { + method: 'GET', + url: '/just/not/the/root/user', + headers: { + authorization: adminAuthHeader + } + }; + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(200); + }); +}); diff --git a/test/server/web/index.js b/test/server/web/index.js deleted file mode 100644 index e85caba..0000000 --- a/test/server/web/index.js +++ /dev/null @@ -1,71 +0,0 @@ -'use strict'; -const Code = require('code'); -const Config = require('../../../config'); -const Hapi = require('hapi'); -const HomePlugin = require('../../../server/web/index'); -const Lab = require('lab'); -const Manifest = require('../../../manifest'); -const Vision = require('vision'); -const Visionary = require('visionary'); - - -const VisionaryPlugin = { - register: Visionary, - options: Manifest.get('/registrations').filter((reg) => { - - if (reg.plugin && - reg.plugin.register && - reg.plugin.register === 'visionary') { - - return true; - } - - return false; - })[0].plugin.options -}; -const lab = exports.lab = Lab.script(); -let request; -let server; - - -lab.beforeEach((done) => { - - const plugins = [Vision, VisionaryPlugin, HomePlugin]; - server = new Hapi.Server(); - server.connection({ port: Config.get('/port/web') }); - server.register(plugins, (err) => { - - if (err) { - return done(err); - } - - server.initialize(done); - }); -}); - - -lab.experiment('Home Page View', () => { - - lab.beforeEach((done) => { - - request = { - method: 'GET', - url: '/' - }; - - done(); - }); - - - - lab.test('home page renders properly', (done) => { - - server.inject(request, (response) => { - - Code.expect(response.result).to.match(/activate the plot device/i); - Code.expect(response.statusCode).to.equal(200); - - done(); - }); - }); -}); diff --git a/test/server/web/main.js b/test/server/web/main.js new file mode 100644 index 0000000..008d3ea --- /dev/null +++ b/test/server/web/main.js @@ -0,0 +1,60 @@ +'use strict'; +const Code = require('code'); +const Hapi = require('hapi'); +const Lab = require('lab'); +const Main = require('../../../server/web/main'); +const Manifest = require('../../../manifest'); + + +const lab = exports.lab = Lab.script(); +let server; + + +lab.before(async () => { + + server = Hapi.Server(); + + const plugins = Manifest.get('/register/plugins') + .filter((entry) => Main.dependencies.includes(entry.plugin)) + .map((entry) => { + + entry.plugin = require(entry.plugin); + + return entry; + }); + + plugins.push(Main); + + await server.register(plugins); + await server.start(); +}); + + +lab.after(async () => { + + await server.stop(); +}); + + +lab.experiment('GET /', () => { + + let request; + + + lab.beforeEach(() => { + + request = { + method: 'GET', + url: '/' + }; + }); + + + lab.test('it returns HTTP 200 when all is good', async () => { + + const response = await server.inject(request); + + Code.expect(response.statusCode).to.equal(200); + Code.expect(response.result).to.match(/welcome/i); + }); +});