From dfcaf046c680691e61488f6909c75505984ca23b Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sun, 1 Mar 2020 15:37:52 -0600 Subject: [PATCH] DB API Reads --- .../api/v1/DatabaseAPI.controller.js | 121 ++++++++++++++++++ app/controllers/api/v1/Misc.controller.js | 14 +- app/models/api/Node.model.js | 6 +- app/models/api/Page.model.js | 58 +++++++++ app/models/api/Token.model.js | 95 ++++++++++++++ app/models/api/db/ColumnDef.model.js | 10 ++ app/models/api/db/DBEntry.model.js | 16 +++ app/models/api/db/Database.model.js | 45 ++++++- app/models/scopes/Valid.scope.js | 11 ++ .../api/DatabaseRoute.middleware.js | 30 +++++ .../api/auth/BearerToken.middleware.js | 52 ++++++++ app/routing/routers/api/v1.routes.js | 4 + app/routing/routers/api/v1/database.routes.js | 43 +++++++ config/auth.config.js | 3 +- example.env | 2 +- package.json | 3 +- yarn.lock | 90 ++++++++++++- 17 files changed, 585 insertions(+), 18 deletions(-) create mode 100644 app/controllers/api/v1/DatabaseAPI.controller.js create mode 100644 app/models/api/Token.model.js create mode 100644 app/models/scopes/Valid.scope.js create mode 100644 app/routing/middleware/api/DatabaseRoute.middleware.js create mode 100644 app/routing/middleware/api/auth/BearerToken.middleware.js create mode 100644 app/routing/routers/api/v1/database.routes.js diff --git a/app/controllers/api/v1/DatabaseAPI.controller.js b/app/controllers/api/v1/DatabaseAPI.controller.js new file mode 100644 index 0000000..f809585 --- /dev/null +++ b/app/controllers/api/v1/DatabaseAPI.controller.js @@ -0,0 +1,121 @@ +const Controller = require('libflitter/controller/Controller') + +/* + * DatabaseAPI Controller + * ------------------------------------------------------------- + * Put some description here! + */ +class DatabaseAPI extends Controller { + static get services() { + return [...super.services, 'models', 'utility'] + } + + async databases(req, res, next) { + const Database = this.models.get('api:db:Database') + const dbs = await Database.visible_by_user(req.user) + return res.api(dbs.map(x => x.to_api_object())) + } + + async get_database(req, res, next) { + return res.api(req.form.database.to_api_object()) + } + + async get_columns(req, res, next) { + const db = req.form.database + const cols = (await db.get_columns()).map(x => x.to_api_object()) + return res.api(cols) + } + + async get_columns_order(req, res, next) { + return res.api(req.form.database.ColumnIds) + } + + async get_data(req, res, next) { + const DBEntry = this.models.get('api:db:DBEntry') + const db = req.form.database + const cursor = await DBEntry.cursor({DatabaseId: db.UUID}) + + if ( req.query.sort ) { + if ( this.utility.is_json(req.query.sort) ) { + const sort = JSON.parse(req.query.sort) + if ( typeof sort !== 'object' ) return res.status(400).message('Invalid sort field. Should be JSON object.').api() + const sort_obj = {} + for ( const field in sort ) { + if ( !sort.hasOwnProperty(field) ) continue + sort_obj[`RowData.${field}`] = (Number(sort[field]) < 0 ? -1 : 1) + } + cursor.sort(sort_obj) + } else if ( req.query.sort ) { + const sort_obj = {} + sort_obj[`RowData.${req.query.sort}`] = (req.query.reverse ? -1 : 1) + cursor.sort(sort_obj) + } + } + + if ( req.query.limit ) { + const limit = Number(req.query.limit) + if ( !isNaN(limit) ) cursor.limit(limit) + } + + if ( req.query.skip ) { + const skip = Number(req.query.skip) + if ( !isNaN(skip) ) cursor.skip(skip) + } + + if ( req.query.where ) { + if ( !this.utility.is_json(req.query.where) ) { + return res.status(400).message('Invalid where field. Should be JSON object.').api() + } + + const wheres = JSON.parse(req.query.where) + + if ( typeof wheres !== 'object' ) { + return res.status(400).message('Invalid where field. Should be JSON object.').api() + } + + const wheres_object = {} + for ( const field in wheres ) { + if ( !wheres.hasOwnProperty(field) ) continue + const value = wheres[field] + if ( typeof value !== 'object' ) { + wheres_object[`RowData.${field}`] = value + } else { + const sub_where = {} + } + } + } + + const data = (await DBEntry.from_cursor(cursor)).map(x => { + x = x.to_api_object() + if ( req.query.flatten ) { + x = { + uuid: x.uuid, + database_id: x.database_id, + ...x.data, + } + } + return x + }) + return res.api(data) + } + + async get_record(req, res, next) { + const DBEntry = this.models.get('api:db:DBEntry') + const db = req.form.database + const record = await DBEntry.findOne({UUID: req.params.record_id, DatabaseId: db.UUID}) + if ( record ) { + const api_obj = record.to_api_object() + if ( req.query.flatten ) { + return res.api({ + uuid: api_obj.uuid, + database_id: api_obj.database_id, + ...api_obj.data, + }) + } + return res.api(api_obj) + } + else return res.status(404).message('Database record not found with that ID.').api() + } +} + +module.exports = exports = DatabaseAPI diff --git a/app/controllers/api/v1/Misc.controller.js b/app/controllers/api/v1/Misc.controller.js index f2d7a12..2535785 100644 --- a/app/controllers/api/v1/Misc.controller.js +++ b/app/controllers/api/v1/Misc.controller.js @@ -8,12 +8,18 @@ const Page = require("../../../models/api/Page.model") * Put some description here! */ class Misc extends Controller { + #default_token_grants = ['database'] - hello_world(req, res) { - return res.api({ - hello: 'world', - }) + static get services() { + return [...super.services, 'models'] } + + async get_token(req, res, next) { + const Token = this.models.get('api:Token') + const token = await Token.for_user(req.user) + return res.api(token.token) + } + } module.exports = exports = Misc diff --git a/app/models/api/Node.model.js b/app/models/api/Node.model.js index 47fd9d0..1f1433a 100644 --- a/app/models/api/Node.model.js +++ b/app/models/api/Node.model.js @@ -7,6 +7,10 @@ const uuid = require('uuid/v4'); * Put some description here! */ class Node extends Model { + static get services() { + return [...super.services, 'models'] + } + static get schema() { // Return a flitter-orm schema here. return { @@ -26,7 +30,7 @@ class Node extends Model { // Static and instance methods can go here get page() { - const Page = this.model.get("api:Page") + const Page = this.models.get('api:Page') return this.belongs_to_one(Page, "PageId", "_id") } diff --git a/app/models/api/Page.model.js b/app/models/api/Page.model.js index 15ffe89..1a07b46 100644 --- a/app/models/api/Page.model.js +++ b/app/models/api/Page.model.js @@ -44,6 +44,64 @@ class Page extends Model { static scopes = [new ActiveScope] + static async visible_by_user(user) { + const user_root = await user.get_root_page() + const user_root_children = await user_root.visible_flat_children(user) + + const view_only_trees = await this.find({ shared_users_view: user._id }) + let view_only_children = [] + for ( const tree of view_only_trees ) { + if ( await tree.is_accessible_by(user) ) { + view_only_children.push(tree) + view_only_children = [...view_only_children, await tree.visible_flat_children(user)] + } + } + + const update_trees = await this.find({ shared_users_update: user._id }) + let update_children = [] + for ( const tree of update_trees ) { + if ( await tree.is_accessible_by(user) ) { + update_children.push(tree) + update_children = [...update_children, await tree.visible_flat_children(user)] + } + } + + const manage_trees = await this.find({ shared_users_manage: user._id }) + let manage_children = [] + for ( const tree of manage_trees ) { + if ( await tree.is_accessible_by(user) ) { + manage_children.push(tree) + manage_children = [...manage_children, await tree.visible_flat_children(user)] + } + } + + const all_children = [...user_root_children, ...view_only_children, ...update_children, ...manage_children] + const unique_children = [] + const seen_UUIDs = [] + all_children.forEach(child => { + if ( !seen_UUIDs.includes(child.UUID) ) { + unique_children.push(child) + seen_UUIDs.push(child.UUID) + } + }) + + return unique_children + } + + async visible_flat_children(user) { + const children = await this.childPages + let visible = [] + if ( children ) { + for ( const child of children ) { + if ( !(await child.is_accessible_by(user)) ) continue + visible.push(child) + visible = [...visible, ...(await child.visible_flat_children(user))] + } + } + + return visible + } + is_shared() { return this.shared_users_view.length > 0 || this.shared_users_update.length > 0 || this.shared_users_manage.length > 0 } diff --git a/app/models/api/Token.model.js b/app/models/api/Token.model.js new file mode 100644 index 0000000..223bb92 --- /dev/null +++ b/app/models/api/Token.model.js @@ -0,0 +1,95 @@ +const Model = require('flitter-orm/src/model/Model') +const { ObjectId } = require('mongodb') +const User = require('../auth/User.model') +const ValidScope = require('../scopes/Valid.scope') +const uuid = require('uuid/v4') +const jwt = require('jsonwebtoken') + +/* + * Token Model + * ------------------------------------------------------------- + * Put some description here! + */ +class Token extends Model { + static #default_grants = ['database'] + + static get services() { + return [...super.services, 'configs'] + } + + static get schema() { + // Return a flitter-orm schema here. + return { + user_id: ObjectId, + token: String, + issued: { type: Date, default: () => new Date }, + expires: { + type: Date, + default: () => { + const d = new Date + d.setDate(d.getDate() + 7) + return d + }, + }, + valid: { type: Boolean, default: true }, + grants: [String], + } + } + + static scopes = [new ValidScope] + + static async create(user) { + const token = new this({ user_id: user._id, grants: this.#default_grants }) + await token.save() + token.token = token.generate() + return await token.save() + } + + static async for_user(user) { + const token = await this.findOne({ user_id: user._id }) + return token ? token : await this.create(user) + } + + static verify(token) { + const secret = this.prototype.configs.get('server.session.secret') + const server = this.prototype.configs.get('app.url') + + return new Promise((res, rej) => { + jwt.verify(token, secret, (err, decoded) => { + if ( err ) rej(err) + else if ( decoded.iss !== server ) rej(new Error('Invalid token issuer: '+decoded.iss)) + else { + this.findOne({ user_id: ObjectId(decoded.sub) }).then(result => { + if ( !result ) rej(new Error('Unable to find associated token. It may have been manually invalidated.')) + else res(result) + }) + } + }) + }) + } + + generate() { + const secret = this.configs.get('server.session.secret') + const server = this.configs.get('app.url') + + const payload = { + sub: String(this.user_id), + iss: server, + permissions: this.grants.join(','), + exp: (this.expires.getTime()/1000 | 0), + iat: (this.issued.getTime()/1000 | 0), + } + + return jwt.sign(payload, secret) + } + + user() { + return this.has_one(User, 'user_id', '_id') + } + + can(grant) { + return this.grants.includes(grant) + } +} + +module.exports = exports = Token diff --git a/app/models/api/db/ColumnDef.model.js b/app/models/api/db/ColumnDef.model.js index c137628..57cbc79 100644 --- a/app/models/api/db/ColumnDef.model.js +++ b/app/models/api/db/ColumnDef.model.js @@ -33,6 +33,16 @@ class ColumnDef extends Model { data() { return JSON.parse(this.additionalData ? this.additionalData : '{}') } + + to_api_object() { + return { + name: this.headerName, + uuid: this.UUID, + database_id: this.DatabaseId, + type: this.Type, + metadata: this.data() + } + } } module.exports = exports = ColumnDef diff --git a/app/models/api/db/DBEntry.model.js b/app/models/api/db/DBEntry.model.js index 2397e71..53d7841 100644 --- a/app/models/api/db/DBEntry.model.js +++ b/app/models/api/db/DBEntry.model.js @@ -17,6 +17,22 @@ class DBEntry extends Model { } // Static and instance methods can go here + to_api_object() { + return { + uuid: this.UUID, + database_id: this.DatabaseId, + data: this.RowData, + } + } + + static async from_cursor(cursor) { + const arr = await cursor.toArray() + const collection = [] + for ( const rec of arr ) { + collection.push(new this(rec)) + } + return collection + } } module.exports = exports = DBEntry diff --git a/app/models/api/db/Database.model.js b/app/models/api/db/Database.model.js index c18b8bb..c152fde 100644 --- a/app/models/api/db/Database.model.js +++ b/app/models/api/db/Database.model.js @@ -1,6 +1,9 @@ const Model = require('flitter-orm/src/model/Model') const uuid = require('uuid/v4') const ColumnDef = require('./ColumnDef.model') +const Page = require('../Page.model') +const Node = require('../Node.model') +const ActiveScope = require('../../scopes/Active.scope') /* * Database Model @@ -16,18 +19,50 @@ class Database extends Model { PageId: String, ColumnIds: [String], UUID: { type: String, default: () => uuid() }, + Active: { type: Boolean, default: true }, } } - accessible_by(user, mode = 'view') { - return user.can(`database:${this.UUID}:${mode}`) + static scopes = [new ActiveScope] + + static async visible_by_user(user) { + const page_ids = (await Page.visible_by_user(user)).map(x => x.UUID) + return this.find({PageId: {$in: page_ids}}) + } + + async is_accessible_by(user, mode = 'view') { + const page = await this.page + return page.is_accessible_by(user, mode) } async get_columns() { - return ColumnDef.find({DatabaseId: this.UUID}); + const cols = await ColumnDef.find({DatabaseId: this.UUID}) + const assoc_cols = {} + cols.forEach(col => assoc_cols[col.UUID] = col) + return this.ColumnIds.map(x => assoc_cols[x]) + } + + get page() { + return this.belongs_to_one(Page, 'PageId', 'UUID') + } + + get node() { + return this.belongs_to_one(Node, 'NodeId', 'UUID') + } + + async delete() { + this.Active = false + await this.save() + } + + to_api_object() { + return { + name: this.Name, + uuid: this.UUID, + page_id: this.PageId, + column_ids: this.ColumnIds, + } } - - // Static and instance methods can go here } module.exports = exports = Database diff --git a/app/models/scopes/Valid.scope.js b/app/models/scopes/Valid.scope.js new file mode 100644 index 0000000..3f06fae --- /dev/null +++ b/app/models/scopes/Valid.scope.js @@ -0,0 +1,11 @@ +const Scope = require('flitter-orm/src/model/Scope') + +class ValidScope extends Scope { + async filter(to_filter) { + return to_filter.equal('valid', true) + .greater_than('expires', new Date) + .less_than('issued', new Date) + } +} + +module.exports = exports = ValidScope diff --git a/app/routing/middleware/api/DatabaseRoute.middleware.js b/app/routing/middleware/api/DatabaseRoute.middleware.js new file mode 100644 index 0000000..c37dbfd --- /dev/null +++ b/app/routing/middleware/api/DatabaseRoute.middleware.js @@ -0,0 +1,30 @@ +const Middleware = require('libflitter/middleware/Middleware') + +/* + * DatabaseRoute Middleware + * ------------------------------------------------------------- + * Put some description here! + */ +class DatabaseRoute extends Middleware { + static get services() { + return [...super.services, 'models'] + } + + async test(req, res, next, args = {}){ + const Database = this.models.get('api:db:Database') + + const id = req.params.database_id ? req.params.database_id : (req.query.database_id ? req.query.database_id : false) + if ( !id ) return res.status(400).message('Missing required: database_id').api() + + const db = await Database.findOne({UUID: id}) + if ( !db ) return res.status(404).message('Unable to find database with that ID.').api() + if ( !(await db.is_accessible_by(req.user)) ) return req.security.deny() + + if ( !req.form ) req.form = {} + req.form.database = db + + next() + } +} + +module.exports = exports = DatabaseRoute diff --git a/app/routing/middleware/api/auth/BearerToken.middleware.js b/app/routing/middleware/api/auth/BearerToken.middleware.js new file mode 100644 index 0000000..66d6234 --- /dev/null +++ b/app/routing/middleware/api/auth/BearerToken.middleware.js @@ -0,0 +1,52 @@ +const Middleware = require('libflitter/middleware/Middleware') + +/* + * BearerToken Middleware + * ------------------------------------------------------------- + * Put some description here! + */ +class BearerToken extends Middleware { + static get services() { + return [...super.services, 'models'] + } + + /* + * Run the middleware test. + * This method is required by all Flitter middleware. + * It should either call the next function in the stack, + * or it should handle the response accordingly. + */ + async test(req, res, next, args = []){ + const Token = this.models.get('api:Token') + + const token_string = req.headers.authorization + if ( !token_string ) return this.fail(res, ) + else if ( !token_string.startsWith('bearer ') ) return this.fail(res, 'Invalid authorization token. Prefix with "bearer".') + + try { + const token = await Token.verify(token_string.replace('bearer ', '')) + const user = await token.user() + if ( !user || !token ) return this.fail(res) + + if ( Array.isArray(args) ) { + for (const grant of args) { + if (!token.can(grant)) { + return this.fail(res) + } + } + } + + req.user = user + req.token = token + next() + } catch (e) { + return this.fail(res, String(e)) + } + } + + fail(res, msg = 'Unauthorized') { + return res.status(401).message(msg).api({}) + } +} + +module.exports = exports = BearerToken diff --git a/app/routing/routers/api/v1.routes.js b/app/routing/routers/api/v1.routes.js index ca69608..9ebdcaf 100644 --- a/app/routing/routers/api/v1.routes.js +++ b/app/routing/routers/api/v1.routes.js @@ -12,6 +12,10 @@ const index = { ], get: { + '/token': [ + 'controller::api:v1:Misc.get_token', + ], + // Get the file ref node config for the specified file ref '/files/:PageId/:NodeId/get/:FilesId': ['controller::api:v1:File.get_config'], diff --git a/app/routing/routers/api/v1/database.routes.js b/app/routing/routers/api/v1/database.routes.js new file mode 100644 index 0000000..92b645e --- /dev/null +++ b/app/routing/routers/api/v1/database.routes.js @@ -0,0 +1,43 @@ +module.exports = exports = { + + prefix: '/db_api/v1', + + middleware: [ + // JWT authorization middleware. Sets req.user and req.token. + // Second param is array of required grants. + ['api:auth:BearerToken', ['database']], + ], + + get: { + '/': [ 'controller::api:v1:DatabaseAPI.databases' ], + + '/:database_id': [ + 'middleware::api:DatabaseRoute', + 'controller::api:v1:DatabaseAPI.get_database', + ], + + '/:database_id/columns': [ + 'middleware::api:DatabaseRoute', + 'controller::api:v1:DatabaseAPI.get_columns', + ], + + '/:database_id/columns/order': [ + 'middleware::api:DatabaseRoute', + 'controller::api:v1:DatabaseAPI.get_columns_order', + ], + + '/:database_id/data': [ + 'middleware::api:DatabaseRoute', + 'controller::api:v1:DatabaseAPI.get_data', + ], + + '/:database_id/record/:record_id': [ + 'middleware::api:DatabaseRoute', + 'controller::api:v1:DatabaseAPI.get_record', + ], + }, + + post: { + + }, +} diff --git a/config/auth.config.js b/config/auth.config.js index 86d4776..2c2dd6e 100644 --- a/config/auth.config.js +++ b/config/auth.config.js @@ -16,7 +16,8 @@ const auth_config = { // Get the token user's data user: { - enable: env('OAUTH2_SERVER_ENABLE', true), + // enable: env('OAUTH2_SERVER_ENABLE', true), + enable: false, // Fields to return to the endpoint // The keys are the keys in the request. The values are the keys in the user. diff --git a/example.env b/example.env index 94b491b..ea62fcb 100644 --- a/example.env +++ b/example.env @@ -21,4 +21,4 @@ AUTH_FLITTER_ENABLE=true #insert client ID for oauth AUTH_OAUTH2_CLIENT_ID= -AUTH_OAUTH2_CLIENT_SECRET= \ No newline at end of file +AUTH_OAUTH2_CLIENT_SECRET= diff --git a/package.json b/package.json index 93a8244..734a4b2 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "flitter-forms": "^0.8.1", "flitter-orm": "^0.2.4", "flitter-upload": "^0.8.0", - "libflitter": "^0.46.7" + "jsonwebtoken": "^8.5.1", + "libflitter": "^0.46.8" } } diff --git a/yarn.lock b/yarn.lock index 5a71c6d..c9b0d42 100644 --- a/yarn.lock +++ b/yarn.lock @@ -435,6 +435,11 @@ bson@^1.1.1, bson@~1.1.1: resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.1.tgz#4330f5e99104c4e751e7351859e2d408279f2f13" integrity sha512-jCGVYLoYMHDkOsbwJZBCqwMHyH4c+wzgI9hG7Z6SZJRXWr+x58pdIbm2i9a/jFGCkRJqRUr8eoI7lDWa0hTkxg== +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + bunyan@1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/bunyan/-/bunyan-1.3.3.tgz#bf4e301c1f0bf888ec678829531f7b5d212e9e81" @@ -798,6 +803,13 @@ dtrace-provider@0.4.0, dtrace-provider@~0.4: dependencies: nan "~1.5.1" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + editorconfig@^0.15.3: version "0.15.3" resolved "https://registry.yarnpkg.com/editorconfig/-/editorconfig-0.15.3.tgz#bef84c4e75fb8dcb0ce5cee8efd51c15999befc5" @@ -1436,6 +1448,22 @@ jsonfile@^2.1.0: optionalDependencies: graceful-fs "^4.1.6" +jsonwebtoken@^8.5.1: + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== + dependencies: + jws "^3.2.2" + lodash.includes "^4.3.0" + lodash.isboolean "^3.0.3" + lodash.isinteger "^4.0.4" + lodash.isnumber "^3.0.3" + lodash.isplainobject "^4.0.6" + lodash.isstring "^4.0.1" + lodash.once "^4.0.0" + ms "^2.1.1" + semver "^5.6.0" + jstransformer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3" @@ -1444,6 +1472,23 @@ jstransformer@1.0.0: is-promise "^2.0.0" promise "^7.0.1" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kareem@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.0.tgz#ef33c42e9024dce511eeaf440cd684f3af1fc769" @@ -1509,10 +1554,10 @@ leven@^1.0.2: resolved "https://registry.yarnpkg.com/leven/-/leven-1.0.2.tgz#9144b6eebca5f1d0680169f1a6770dcea60b75c3" integrity sha1-kUS27ryl8dBoAWnxpncNzqYLdcM= -libflitter@^0.46.7: - version "0.46.7" - resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.46.7.tgz#be55386a53747e1e21b2aefd356977900fd40dd1" - integrity sha512-PPPEp4vR36xAvjPyxmU8K6NBv6n5ggP/bfSJU/PZ6qCChbYh0gKIN6kH88GM38imSZJxjZM1hjvzio5veIGaAw== +libflitter@^0.46.8: + version "0.46.8" + resolved "https://registry.yarnpkg.com/libflitter/-/libflitter-0.46.8.tgz#1500d70956628ff1db9719ff4086a8b0b313aa6e" + integrity sha512-ZAo4iUeTLz+6crqQG9UUJyliT6L7+kuZyTB39q8FC8qIdwtLd6C28Jkx82sBcYp9JZ5qJsbNYjSlF42Yt/msbg== dependencies: colors "^1.3.3" connect-mongodb-session "^2.2.0" @@ -1554,6 +1599,41 @@ lodash.clonedeep@4.x: resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= +lodash.includes@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= + +lodash.isboolean@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= + +lodash.isinteger@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= + +lodash.isnumber@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.once@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= + lodash.set@4.x: version "4.3.2" resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23" @@ -2508,7 +2588,7 @@ safe-buffer@5.1.2, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@^5.1.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.1: version "5.2.0" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==