From 6cef6386c2f2c953bfb77829e803c62c7b2738ce Mon Sep 17 00:00:00 2001 From: mrkvon Date: Wed, 2 Aug 2017 13:13:42 +0300 Subject: [PATCH 01/11] POST userTag: validation refactored to json-schema --- controllers/validators/errorHandler.js | 17 +++++++ controllers/validators/schema.js | 30 ++++++++++++ controllers/validators/userTags.js | 48 +------------------- controllers/validators/validate-by-schema.js | 14 ++++++ 4 files changed, 63 insertions(+), 46 deletions(-) create mode 100644 controllers/validators/validate-by-schema.js diff --git a/controllers/validators/errorHandler.js b/controllers/validators/errorHandler.js index df03e75..ebac182 100644 --- a/controllers/validators/errorHandler.js +++ b/controllers/validators/errorHandler.js @@ -8,6 +8,23 @@ module.exports = function (err, req, res, next) { if (_.isArray(err)) { const errors = _.map(err, (e) => { + + // errors from json-schema should be remapped (and TODO treated well by default) + if (e.hasOwnProperty('dataPath')) { + // invalid attributes + if (e.keyword === 'additionalProperties') { + e.param = 'attributes'; + e.msg = 'unexpected attribute'; + } else if (e.keyword === 'required') { + e.param = 'attributes'; + e.msg = 'missing attribute'; + } else { + const dataPath = e.dataPath.split('.'); + const param = dataPath[dataPath.length - 1]; + e.param = param; + } + } + return { meta: e.msg || e, title: (e.param) ? `invalid ${e.param}` : 'invalid', diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index d759aab..5f0fbaa 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -36,6 +36,15 @@ module.exports = { minLength: 1, pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' } + }, + userTag: { + story: { + type: 'string', + maxLength: 1024 + }, + relevance: { + enum: [1, 2, 3, 4, 5] + } } }, postUsers: { @@ -127,5 +136,26 @@ module.exports = { required: ['filter'], additionalProperties: false } + }, + postUserTags: { + id: 'postUserTags', + properties: { + body: { + properties: { + tag: { + properties: { + tagname: { + $ref: 'sch#/definitions/tag/tagname' + } + } + }, + story: { $ref: 'sch#/definitions/userTag/story' }, + relevance: { $ref: 'sch#/definitions/userTag/relevance' } + }, + additionalProperties: false, + required: ['tag', 'story', 'relevance'] + } + }, + required: ['body'] } }; diff --git a/controllers/validators/userTags.js b/controllers/validators/userTags.js index bd2e5a3..1831202 100644 --- a/controllers/validators/userTags.js +++ b/controllers/validators/userTags.js @@ -3,53 +3,9 @@ const _ = require('lodash'); const rules = require('./rules'); -exports.post = function (req, res, next) { - let errors = []; - - // check that only valid attributes are present - const fields = ['story', 'relevance', 'tag']; - const invalidAttrs = _.difference(Object.keys(req.body), fields); - if (invalidAttrs.length > 0) { - errors.push({ - param: 'attributes', - msg: 'unexpected attribute' - }); - } +const validateBySchema = require('./validate-by-schema'); - // check that no required attributes are missing - const missingAttrs = _.difference(fields, Object.keys(req.body)); - if (missingAttrs.length > 0) { - errors.push({ - param: 'attributes', - msg: 'missing attribute' - }); - } - - // validate tagname - req.body.tagname = _.get(req.body, 'tag.tagname'); - req.checkBody(_.pick(rules.tag, ['tagname'])); - delete req.body.tagname; - - // validate story - req.checkBody(_.pick(rules.userTag, ['story'])); - - // validate relevance - if (!validateRelevance(req.body.relevance)) { - errors.push({ - param: 'relevance', - msg: 'relevance should be a number 1, 2, 3, 4 or 5', - value: req.body.relevance - }); - } - - errors = errors.concat(req.validationErrors() || []); - - if (errors.length === 0) { - return next(); - } - - return next(errors); -}; +exports.post = validateBySchema('postUserTags'); exports.patch = function (req, res, next) { let errors = []; diff --git a/controllers/validators/validate-by-schema.js b/controllers/validators/validate-by-schema.js new file mode 100644 index 0000000..a5715b9 --- /dev/null +++ b/controllers/validators/validate-by-schema.js @@ -0,0 +1,14 @@ +const { ajv } = require('./ajvInit'); + +module.exports = function (schema) { + return function (req, res, next) { + // console.log(req.body, req.params, req.query); + const valid = ajv.validate(`sch#/${schema}`, req); + + if (!valid) { + return next(ajv.errors); + } + return next(); + }; +}; + From 8c82ceacb426ad6ce1ad4cf88568fda08537c261 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Wed, 2 Aug 2017 14:49:01 +0300 Subject: [PATCH 02/11] PATCH userTag: refactor to json schema, removed a test unimplementable with json-schema for now (data consistency) --- controllers/validators/index.js | 1 - controllers/validators/schema.js | 22 ++++++++++ controllers/validators/userTags.js | 67 ------------------------------ routes/users.js | 6 ++- test/user-tags.js | 19 +++++---- 5 files changed, 37 insertions(+), 78 deletions(-) delete mode 100644 controllers/validators/userTags.js diff --git a/controllers/validators/index.js b/controllers/validators/index.js index 30826e8..1486d00 100644 --- a/controllers/validators/index.js +++ b/controllers/validators/index.js @@ -8,7 +8,6 @@ exports.contacts = require('./contacts'); exports.messages = require('./messages'); exports.account = require('./account'); exports.users = require('./users'); -exports.userTags = require('./userTags'); exports.tags = require('./tags'); diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 5f0fbaa..105f0e8 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -157,5 +157,27 @@ module.exports = { } }, required: ['body'] + }, + patchUserTag: { + id: 'patchUserTag', + properties: { + body: { + properties: { + story: { $ref: 'sch#/definitions/userTag/story' }, + relevance: { $ref: 'sch#/definitions/userTag/relevance' }, + id: {} // for now id is ignored TODO should be compared to params + }, + additionalProperties: false, + required: ['id'] + }, + params: { + properties: { + tagname: { + $ref: 'sch#/definitions/tag/tagname' + } + } + } + }, + required: ['body', 'params'] } }; diff --git a/controllers/validators/userTags.js b/controllers/validators/userTags.js deleted file mode 100644 index 1831202..0000000 --- a/controllers/validators/userTags.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const _ = require('lodash'); -const rules = require('./rules'); - -const validateBySchema = require('./validate-by-schema'); - -exports.post = validateBySchema('postUserTags'); - -exports.patch = function (req, res, next) { - let errors = []; - - /* - * Check that url matches body id - */ - const { username, tagname } = req.params; - const [ usernameBody, tagnameBody ] = req.body.id.split('--'); - - const urlMatchesBodyId = username === usernameBody && tagname === tagnameBody; - - if (!urlMatchesBodyId) { - errors.push({ - msg: 'url should match body id' - }); - } - - req.checkParams(_.pick(rules.tag, ['tagname'])); - - req.checkBody(_.pick(rules.userTag, ['story'])); - - if (req.body.relevance && !validateRelevance(req.body.relevance)) { - errors.push({ - msg: 'relevance should be a number 1, 2, 3, 4 or 5' - }); - } - - // check that only valid attributes are present - const fields = ['story', 'relevance', 'id']; - const invalidAttrs = _.difference(Object.keys(req.body), fields); - if (invalidAttrs.length > 0) { - errors.push({ - msg: `invalid attributes: ${invalidAttrs.join(', ')}` - }); - } - - - errors = errors.concat(req.validationErrors() || []); - - if (errors.length === 0) { - return next(); - } - - return next(errors); -}; - -exports.postUserTags = function (req, res, next) { - return next(); -}; - -exports.patchUserTag = function (req, res, next) { - // TODO - return next(); -}; - -function validateRelevance(relevance) { - return [1, 2, 3, 4, 5].indexOf(relevance) > -1; -} diff --git a/routes/users.js b/routes/users.js index 6a12484..45b57d2 100644 --- a/routes/users.js +++ b/routes/users.js @@ -8,6 +8,8 @@ const userController = require(path.resolve('./controllers/users')); const validators = require(path.resolve('./controllers/validators')), authorize = require(path.resolve('./controllers/authorize')); +const validateBySchema = require(path.resolve('./controllers/validators/validate-by-schema')); + // post a new user router.route('/') .post(validators.users.postUsers, userController.postUsers); @@ -41,12 +43,12 @@ router.route('/:username') * Routers for userTags */ router.route('/:username/tags') - .post(authorize.onlyLoggedMe, validators.userTags.post, userController.postUserTags) + .post(authorize.onlyLoggedMe, validateBySchema('postUserTags'), userController.postUserTags) .get(userController.getUserTags); router.route('/:username/tags/:tagname') .get(userController.getUserTag) - .patch(authorize.onlyLoggedMe, validators.userTags.patch, userController.patchUserTag, userController.getUserTag) + .patch(authorize.onlyLoggedMe, validateBySchema('patchUserTag'), userController.patchUserTag, userController.getUserTag) .delete(authorize.onlyLoggedMe, userController.deleteUserTag); router.route('/:username/avatar') diff --git a/test/user-tags.js b/test/user-tags.js index fbf5dd5..c23e9da 100644 --- a/test/user-tags.js +++ b/test/user-tags.js @@ -642,6 +642,8 @@ describe('Tags of user', function () { .expect('Content-Type', /^application\/vnd\.api\+json/); }); + /* + * TODO implement! it('[JSON API id doesn\'t match url] 400 and msg', async function () { const [me, other] = dbData.users; const [userTag] = dbData.userTag; @@ -670,6 +672,7 @@ describe('Tags of user', function () { should(response.body).have.propertyByPath('errors', 0, 'meta') .eql('url should match body id'); }); + */ it('[invalid story] 400 and msg', async function () { const { userTag: [userTag], users: [me] } = dbData; @@ -693,8 +696,8 @@ describe('Tags of user', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(response.body).have.propertyByPath('errors', 0, 'meta') - .eql('userTag story can be at most 1024 characters long'); + should(response.body).have.propertyByPath('errors', 0, 'title') + .eql('invalid story'); }); it('[invalid relevance] 400 and msg', async function () { @@ -719,8 +722,8 @@ describe('Tags of user', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(response.body).have.propertyByPath('errors', 0, 'meta') - .eql('relevance should be a number 1, 2, 3, 4 or 5'); + should(response.body).have.propertyByPath('errors', 0, 'title') + .eql('invalid relevance'); }); @@ -747,8 +750,8 @@ describe('Tags of user', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(response.body).have.propertyByPath('errors', 0, 'meta') - .match(/Invalid Tagname/); + should(response.body).have.propertyByPath('errors', 0, 'title') + .match('invalid tagname'); }); @@ -802,8 +805,8 @@ describe('Tags of user', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(response.body).have.propertyByPath('errors', 0, 'meta') - .eql('invalid attributes: invalid'); + should(response.body).have.propertyByPath('errors', 0, 'title') + .eql('invalid attributes'); }); }); From 84866805ed46cc4239cfa8de779d924f2d2c3376 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Thu, 3 Aug 2017 15:31:55 +0300 Subject: [PATCH 03/11] comments --- controllers/validators/errorHandler.js | 11 +++++------ controllers/validators/validate-by-schema.js | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/controllers/validators/errorHandler.js b/controllers/validators/errorHandler.js index ebac182..70c41f4 100644 --- a/controllers/validators/errorHandler.js +++ b/controllers/validators/errorHandler.js @@ -9,16 +9,15 @@ module.exports = function (err, req, res, next) { const errors = _.map(err, (e) => { - // errors from json-schema should be remapped (and TODO treated well by default) - if (e.hasOwnProperty('dataPath')) { - // invalid attributes - if (e.keyword === 'additionalProperties') { + // remapping errors from json-schema (ajv) validation (and TODO make it a default when validation is fully refactored) + if (e.hasOwnProperty('dataPath')) { // is this an ajv json-schema error? + if (e.keyword === 'additionalProperties') { // invalid attributes are failing additionalProperties: false e.param = 'attributes'; e.msg = 'unexpected attribute'; - } else if (e.keyword === 'required') { + } else if (e.keyword === 'required') { // missing attributes are missing fields from required: ['required', 'fields'] e.param = 'attributes'; e.msg = 'missing attribute'; - } else { + } else { // otherwise we use the last part of failing dataPath to name the error. const dataPath = e.dataPath.split('.'); const param = dataPath[dataPath.length - 1]; e.param = param; diff --git a/controllers/validators/validate-by-schema.js b/controllers/validators/validate-by-schema.js index a5715b9..fdbe11c 100644 --- a/controllers/validators/validate-by-schema.js +++ b/controllers/validators/validate-by-schema.js @@ -1,5 +1,11 @@ const { ajv } = require('./ajvInit'); +/** + * Creates the express middleware function for validating requests with json-schema. + * + * @param {string} schema - the id of the schema for validating the request. The schema should be specified in ./schema.js. It validates the whole express req object, so the body, query and/or params should be specified in the schema. See examples of `postUserTags` and `patchUserTag` in ./schema.js. + * @returns {function} - the express middleware function which will execute the validation as needed. The error handler for validation is in ./errorHandler.js + */ module.exports = function (schema) { return function (req, res, next) { // console.log(req.body, req.params, req.query); From f9d2b509b79f325d14bede641d59b299b7573a97 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Fri, 18 Aug 2017 19:38:47 +0300 Subject: [PATCH 04/11] added checking request consistency to validate-by-schema --- controllers/validators/errorHandler.js | 2 +- controllers/validators/index.js | 1 + controllers/validators/user-tags.js | 12 +++ controllers/validators/validate-by-schema.js | 85 +++++++++++++++++++- routes/users.js | 6 +- test/user-tags.js | 5 +- 6 files changed, 100 insertions(+), 11 deletions(-) create mode 100644 controllers/validators/user-tags.js diff --git a/controllers/validators/errorHandler.js b/controllers/validators/errorHandler.js index 70c41f4..89ab7de 100644 --- a/controllers/validators/errorHandler.js +++ b/controllers/validators/errorHandler.js @@ -25,7 +25,7 @@ module.exports = function (err, req, res, next) { } return { - meta: e.msg || e, + meta: e.msg || e.message || e, title: (e.param) ? `invalid ${e.param}` : 'invalid', detail: e.msg || '' }; diff --git a/controllers/validators/index.js b/controllers/validators/index.js index 1486d00..78f30ec 100644 --- a/controllers/validators/index.js +++ b/controllers/validators/index.js @@ -9,6 +9,7 @@ exports.messages = require('./messages'); exports.account = require('./account'); exports.users = require('./users'); exports.tags = require('./tags'); +exports.userTags = require('./user-tags'); diff --git a/controllers/validators/user-tags.js b/controllers/validators/user-tags.js new file mode 100644 index 0000000..037f195 --- /dev/null +++ b/controllers/validators/user-tags.js @@ -0,0 +1,12 @@ +const validate = require('./validate-by-schema'); + +// PATCH /users/:username/tags/:tagname +const patch = validate('patchUserTag', [['body.id', ['params.username', 'params.tagname'], function (id, names) { + // id should equal username--tagname + return id === names.join('--'); +}]]); + +// POST /users/:username/tags +const post = validate('postUserTags'); + +module.exports = { patch, post }; diff --git a/controllers/validators/validate-by-schema.js b/controllers/validators/validate-by-schema.js index fdbe11c..f3939fd 100644 --- a/controllers/validators/validate-by-schema.js +++ b/controllers/validators/validate-by-schema.js @@ -1,20 +1,101 @@ const { ajv } = require('./ajvInit'); +const _ = require('lodash'); /** * Creates the express middleware function for validating requests with json-schema. * * @param {string} schema - the id of the schema for validating the request. The schema should be specified in ./schema.js. It validates the whole express req object, so the body, query and/or params should be specified in the schema. See examples of `postUserTags` and `patchUserTag` in ./schema.js. + * @param {[string, string, function][]} consistency - the consistency of request fields * @returns {function} - the express middleware function which will execute the validation as needed. The error handler for validation is in ./errorHandler.js */ -module.exports = function (schema) { +module.exports = function (schema, consistency) { return function (req, res, next) { - // console.log(req.body, req.params, req.query); const valid = ajv.validate(`sch#/${schema}`, req); if (!valid) { return next(ajv.errors); } + + const consistencyCheck = validateConsistency(consistency, req); + if (!consistencyCheck.valid) { + return next(consistencyCheck.errors); + } + return next(); }; }; +/** + * TODO make correct JSDoc + * @param {[string, string, ?function][]} consistency - paths to fields in req to compare + * + * @returns { valid: boolean, errors: ValidationError[]} + */ +function validateConsistency(requirements, req) { + // if nothing is provided, return valid + if (!requirements) return { valid: true }; + + let valid = true; + const errors = []; + // if array provided, process every element of it + requirements.forEach((requirement) => { + const { valid: isValid, error } = checkRequirement(requirement, req); + valid = valid && isValid; + if (!isValid) { + errors.push(error); + } + }); + + return { valid, errors }; +} + +/** + * @param {string|string[]} field0 - path to the value from request object to compare + * @param {string|string[]} field1 - path to the value from request object to compare (2) + * @param {function} [compareFunction=equality] - a function to compare the provided fields + * @param {object} req - an object on which to perform the search for fields + * + * + */ +function checkRequirement([field0, field1, compareFunction], req) { + const fields = [field0, field1]; + + // read the values on provided paths + const values = []; + fields.forEach((field) => { + if (typeof(field) === 'string') { + values.push(_.get(req, field)); + } + if (Array.isArray(field)) { + const value = []; + field.forEach((fieldItem) => { + value.push(_.get(req, fieldItem)); + }); + values.push(value); + } + }); + + let valid; + if (compareFunction) { + valid = compareFunction(...values); + } else { + valid = values[0] === values[1]; + } + + const responseObject = { valid }; + + if (!valid) { + responseObject.error = { fields, values }; + responseObject.error.message = generateErrorMessage(responseObject.error); + } + + return responseObject; +} + +function generateErrorMessage({ fields: [field0, field1] }) { + function fieldToString(field) { + return Array.isArray(field) ? field.join(', ') : field; + } + + return `${fieldToString(field0)} should match ${fieldToString(field1)}`; +} diff --git a/routes/users.js b/routes/users.js index 45b57d2..6a12484 100644 --- a/routes/users.js +++ b/routes/users.js @@ -8,8 +8,6 @@ const userController = require(path.resolve('./controllers/users')); const validators = require(path.resolve('./controllers/validators')), authorize = require(path.resolve('./controllers/authorize')); -const validateBySchema = require(path.resolve('./controllers/validators/validate-by-schema')); - // post a new user router.route('/') .post(validators.users.postUsers, userController.postUsers); @@ -43,12 +41,12 @@ router.route('/:username') * Routers for userTags */ router.route('/:username/tags') - .post(authorize.onlyLoggedMe, validateBySchema('postUserTags'), userController.postUserTags) + .post(authorize.onlyLoggedMe, validators.userTags.post, userController.postUserTags) .get(userController.getUserTags); router.route('/:username/tags/:tagname') .get(userController.getUserTag) - .patch(authorize.onlyLoggedMe, validateBySchema('patchUserTag'), userController.patchUserTag, userController.getUserTag) + .patch(authorize.onlyLoggedMe, validators.userTags.patch, userController.patchUserTag, userController.getUserTag) .delete(authorize.onlyLoggedMe, userController.deleteUserTag); router.route('/:username/avatar') diff --git a/test/user-tags.js b/test/user-tags.js index c23e9da..92580f3 100644 --- a/test/user-tags.js +++ b/test/user-tags.js @@ -642,8 +642,6 @@ describe('Tags of user', function () { .expect('Content-Type', /^application\/vnd\.api\+json/); }); - /* - * TODO implement! it('[JSON API id doesn\'t match url] 400 and msg', async function () { const [me, other] = dbData.users; const [userTag] = dbData.userTag; @@ -670,9 +668,8 @@ describe('Tags of user', function () { .expect('Content-Type', /^application\/vnd\.api\+json/); should(response.body).have.propertyByPath('errors', 0, 'meta') - .eql('url should match body id'); + .eql('body.id should match params.username, params.tagname'); }); - */ it('[invalid story] 400 and msg', async function () { const { userTag: [userTag], users: [me] } = dbData; From 2b6dcc033bdca8899979cd2161892670a4912a0f Mon Sep 17 00:00:00 2001 From: mrkvon Date: Fri, 18 Aug 2017 22:10:09 +0300 Subject: [PATCH 05/11] refactored validation of GET and PATCH users to json-schema --- controllers/validators/schema.js | 80 ++++++++++++++++++++++++++++++-- controllers/validators/users.js | 71 +++------------------------- routes/users.js | 4 +- test/location.js | 2 +- 4 files changed, 84 insertions(+), 73 deletions(-) diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 105f0e8..bc9dcc6 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -1,3 +1,44 @@ +const paths = { + username: { $ref : 'sch#/definitions/user/username' }, + givenName: { $ref : 'sch#/definitions/user/givenName' }, + familyName: { $ref : 'sch#/definitions/user/familyName' }, + description: { $ref : 'sch#/definitions/user/desc' }, + location: { $ref : 'sch#/definitions/user/location' }, +}; + +const getUser = { + id: 'getUser', + properties: { + params: { + properties: { + username: paths.username + } + } + } +}; + +const patchUser = { + id: 'patchUser', + properties: { + params: { + properties: { + username: paths.username + } + }, + body: { + properties: { + id: paths.username, + givenName: paths.givenName, + familyName: paths.familyName, + description: paths.description, + location: paths.location + }, + required: ['id'], + additionalProperties: false + } + } +}; + module.exports = { definitions: { user: { @@ -12,18 +53,46 @@ module.exports = { pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$' }, givenName: { + type: 'string', maxLength: 128 }, familyName: { + type: 'string', maxLength: 128 }, - desc: { + desc: { // description + type: 'string', maxLength: 2048 }, password: { + type: 'string', maxLength: 512, minLength: 8 }, + location: { + oneOf: [ + { + type: 'null' + }, + { + type: 'array', + minItems: 2, + maxItems: 2, + items: [ + { + type: 'number', + minimum: -90, + maximum: 90 + }, + { + type: 'number', + minimum: -180, + maximum: 180 + } + ] + } + ] + }, code: { type: 'string', minLength: 1, @@ -54,8 +123,8 @@ module.exports = { email: { $ref : 'sch#/definitions/user/email' }, - username: { $ref : 'sch#/definitions/user/username'}, - password: { '$ref': 'sch#/definitions/user/password'} + username: paths.username, + password: { $ref: 'sch#/definitions/user/password'} } } }, @@ -165,7 +234,7 @@ module.exports = { properties: { story: { $ref: 'sch#/definitions/userTag/story' }, relevance: { $ref: 'sch#/definitions/userTag/relevance' }, - id: {} // for now id is ignored TODO should be compared to params + id: {} }, additionalProperties: false, required: ['id'] @@ -179,5 +248,6 @@ module.exports = { } }, required: ['body', 'params'] - } + }, + getUser, patchUser }; diff --git a/controllers/validators/users.js b/controllers/validators/users.js index ea1a40b..c248513 100644 --- a/controllers/validators/users.js +++ b/controllers/validators/users.js @@ -3,10 +3,11 @@ const _ = require('lodash'); const parser = require('./parser'), - rules = require('./rules'), schema = require('./schema'), { ajv } = require('./ajvInit'); +const validate = require('./validate-by-schema'); + exports.postUsers = function (req, res, next) { // req.checkBody(_.pick(rules.user, ['username', 'email', 'password'])); @@ -62,71 +63,11 @@ exports.getUsersWithLocation = function (req, res, next) { return next(); }; -exports.getUser = function (req, res, next) { - req.checkParams(_.pick(rules.user, ['username'])); - - const errors = req.validationErrors(); - - const errorOutput = { errors: [] }; - - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.patchUser = function (req, res, next) { - req.checkParams(_.pick(rules.user, ['username'])); - req.checkBody(_.pick(rules.user, ['id', 'givenName', 'familyName', 'description'])); - let errors = req.validationErrors(); +const get = validate('getUser'); +const patch = validate('patchUser'); - const errorOutput = { errors: [] }; - - // validate location - if (req.body.hasOwnProperty('location')) { - const location = req.body.location; - - const locationErrors = []; - - /* - * check that the location is an array of 2 numbers in coordinate range - * or empty (null, '', false) - * - * - */ - const isEmpty = !location; - const isArrayOf2Numbers = _.isArray(location) - && location.length === 2 - && _.isNumber(location[0]) - && _.isNumber(location[1]) - && _.inRange(location[0], -90, 90) - && _.inRange(location[1], -180, 180); - - - if (!(isArrayOf2Numbers || isEmpty)) { - locationErrors.push('location should be an array of 2 numbers or falsy'); - } - - if (locationErrors.length > 0) { - errors = (errors) - ? errors.concat(locationErrors) - : locationErrors; - } - } - - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; +exports.get = get; +exports.patch = patch; exports.getNewUsers = function (req, res, next) { diff --git a/routes/users.js b/routes/users.js index 6a12484..f870c18 100644 --- a/routes/users.js +++ b/routes/users.js @@ -34,8 +34,8 @@ router.route('/') // get and patch user profile router.route('/:username') - .get(validators.users.getUser, userController.getUser) - .patch(authorize.onlyLoggedMe, validators.users.patchUser, userController.patchUser, userController.getUser); + .get(validators.users.get, userController.getUser) + .patch(authorize.onlyLoggedMe, validators.users.patch, userController.patchUser, userController.getUser); /** * Routers for userTags diff --git a/test/location.js b/test/location.js index 56c6efc..69aa255 100644 --- a/test/location.js +++ b/test/location.js @@ -244,7 +244,7 @@ describe('Location of people, tags, ideas, projects, ...', function () { type: 'users', id: loggedUser.username, attributes: { - location: '' + location: null } } }) From e169a8d9dd9a9381c6d1e1f6025d1d2f4cf6603c Mon Sep 17 00:00:00 2001 From: mrkvon Date: Fri, 18 Aug 2017 22:30:53 +0300 Subject: [PATCH 06/11] refactored validation of POST /users --- controllers/validators/schema.js | 20 ++++++++++--------- controllers/validators/users.js | 33 ++------------------------------ routes/users.js | 2 +- 3 files changed, 14 insertions(+), 41 deletions(-) diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index bc9dcc6..04249d9 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -44,12 +44,10 @@ module.exports = { user: { username: { type: 'string', - minLength: 1, pattern: '^(?=.{2,32}$)[a-z0-9]+([\\_\\-\\.][a-z0-9]+)*$' }, email: { type: 'string', - minLength: 1, pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$' }, givenName: { @@ -118,14 +116,18 @@ module.exports = { }, postUsers: { id: 'postUsers', - body: { - properties: { - email: { - $ref : 'sch#/definitions/user/email' + properties: { + body: { + properties: { + email: { + $ref : 'sch#/definitions/user/email' + }, + username: paths.username, + password: { $ref: 'sch#/definitions/user/password'} }, - username: paths.username, - password: { $ref: 'sch#/definitions/user/password'} - } + required: ['username', 'email', 'password'] + }, + required: ['body'] } }, newUsers: { diff --git a/controllers/validators/users.js b/controllers/validators/users.js index c248513..2ab7f23 100644 --- a/controllers/validators/users.js +++ b/controllers/validators/users.js @@ -8,19 +8,6 @@ const parser = require('./parser'), const validate = require('./validate-by-schema'); -exports.postUsers = function (req, res, next) { - // req.checkBody(_.pick(rules.user, ['username', 'email', 'password'])); - - const validate = ajv.compile(schema.postUsers.body); - const valid = validate(req.body); - - if (!valid) { - const errorOutput = ajv.errorsText(validate.errors); - return res.status(400).json({'errors':errorOutput}); - } - return next(); -}; - exports.getUsersWithTags = function (req, res, next) { req.query = parser.parseQuery(req.query, parser.parametersDictionary); @@ -63,11 +50,13 @@ exports.getUsersWithLocation = function (req, res, next) { return next(); }; +const post = validate('postUsers'); const get = validate('getUser'); const patch = validate('patchUser'); exports.get = get; exports.patch = patch; +exports.post = post; exports.getNewUsers = function (req, res, next) { @@ -104,21 +93,3 @@ exports.getNewUsersWithMyTags = function (req, res, next) { return next(); }; - -/* exports.getNewUsers = function (req, res, next) { - - req.checkQuery(rules.newUsers); - - const errors = req.validationErrors(); - - const errorOutput = { errors: [] }; - - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -};*/ diff --git a/routes/users.js b/routes/users.js index f870c18..ef47a41 100644 --- a/routes/users.js +++ b/routes/users.js @@ -10,7 +10,7 @@ const validators = require(path.resolve('./controllers/validators')), // post a new user router.route('/') - .post(validators.users.postUsers, userController.postUsers); + .post(validators.users.post, userController.postUsers); // get new users who have common tags with me router.route('/') From 2f3241b95aaa9d25f6b5847da360fe8e972cc13f Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sat, 19 Aug 2017 00:58:30 +0300 Subject: [PATCH 07/11] refactored rest of user validators --- controllers/users.js | 17 +--- controllers/validators/parser.js | 21 +++- controllers/validators/schema.js | 162 ++++++++++++++++++------------- controllers/validators/users.js | 104 +++----------------- routes/users.js | 10 +- test/users.js | 13 ++- 6 files changed, 150 insertions(+), 177 deletions(-) diff --git a/controllers/users.js b/controllers/users.js index ad38ed3..374c67f 100644 --- a/controllers/users.js +++ b/controllers/users.js @@ -37,7 +37,7 @@ exports.gotoGetNewUsersWithMyTags = function (req, res, next) { exports.gotoGetUsersWithMyTags = function (req, res, next) { // TODO DZIK req.query.filter.byMyTags returns string not bool (how was it checked before?) - if (_.has(req, 'query.filter.byMyTags') && req.query.filter.byMyTags === 'true') { + if (_.has(req, 'query.filter.byMyTags')) { return next(); } return next('route'); @@ -316,24 +316,9 @@ exports.getUser = async function (req, res, next) { // edit a user with PATCH request // presumption: data in body should already be valid and user should be logged in as herself exports.patchUser = async function (req, res, next) { - // check that user id in body equals username from url - if (req.body.id !== req.params.username) { - const e = new Error('username in url parameter and in body don\'t match'); - e.status = 400; - return next(e); - } - // the list of allowed profile fields (subset of these needs to be provided) const profileFields = ['givenName', 'familyName', 'description']; - // check that only profile fields are present in the request body - const unwantedParams = _.difference(Object.keys(req.body), _.union(profileFields, ['id', 'location'])); - if (unwantedParams.length > 0) { // if any unwanted fields are present, error. - const e = new Error('The request body contains unexpected attributes'); - e.status = 400; - return next(e); - } - // pick only the profile fields from the body of the request const profile = _.pick(req.body, profileFields); diff --git a/controllers/validators/parser.js b/controllers/validators/parser.js index 140f879..006f6f5 100644 --- a/controllers/validators/parser.js +++ b/controllers/validators/parser.js @@ -21,7 +21,8 @@ const parametersDictionary = { }, filter: { tag: 'array', - withMyTags: 'int' + withMyTags: 'int', + location: 'coordinates' }, }; @@ -48,6 +49,17 @@ const parseQuery = function (query, parametersDictionary) { query[q] = array; break; } + case 'coordinates': { + // parse the location + const queryString = query[q]; + const array = queryString.split(','); + + // parse location to numbers + const [lat1, lon1, lat2, lon2] = array.map(loc => +loc); + + query[q] = [[lat1, lon1], [lat2, lon2]]; + break; + } } } } @@ -55,4 +67,9 @@ const parseQuery = function (query, parametersDictionary) { return query; }; -module.exports = { parseQuery, parametersDictionary }; +const parse = function (req, res, next) { + req.query = parseQuery(req.query, parametersDictionary); + next(); +}; + +module.exports = { parseQuery, parametersDictionary, parse }; diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 04249d9..66f5b75 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -39,6 +39,30 @@ const patchUser = { } }; +const getUsersWithMyTags = { + id: 'getUsersWithMyTags', + properties: { + query: { + properties: { + filter: { + properties: { + byMyTags: { + enum: [''] + } + }, + required: ['byMyTags'] + } + }, + required: ['filter'] + }, + }, + required: ['query'] +}; + +const getUsersWithLocation = { + id: 'getUsersWithLocation' +}; + module.exports = { definitions: { user: { @@ -132,81 +156,89 @@ module.exports = { }, newUsers: { id: 'newUsers', - query: { - properties: { - sort: { - type: 'string' - }, - page: { - type: 'object', - properties: { - limit: { - type: 'number' - }, - offset: { - type: 'number' - } + properties: { + query: { + properties: { + sort: { + type: 'string' }, - required: ['limit', 'offset'], - additionalProperties: false - } - }, - required: ['sort', 'page'], - additionalProperties: false + page: { + type: 'object', + properties: { + limit: { + type: 'number' + }, + offset: { + type: 'number' + } + }, + required: ['limit', 'offset'], + additionalProperties: false + } + }, + required: ['sort', 'page'], + additionalProperties: false + } } }, newUsersWithMyTags: { id: 'newUsersWithMyTags', - query:{ - properties:{ - sort: { - type: 'string', - const: '-created' - }, - filter: { - properties: { - withMyTags: { - type: 'number', - } + properties: { + query:{ + properties:{ + sort: { + type: 'string', + const: '-created' }, - required: ['withMyTags'], - additionalProperties: false - }, - page: { - properties: { - offset: { - type: 'number' + filter: { + properties: { + withMyTags: { + type: 'number', + } }, - limit: { - type: 'number' - } + required: ['withMyTags'], + additionalProperties: false }, - required: ['offset', 'limit'], - additionalProperties: false - } - }, - required: ['sort', 'filter', 'page'], - additionalProperties: false - } + page: { + properties: { + offset: { + type: 'number' + }, + limit: { + type: 'number' + } + }, + required: ['offset', 'limit'], + additionalProperties: false + } + }, + required: ['sort', 'filter', 'page'], + additionalProperties: false + } + }, + required: ['query'] }, getUsersWithTags: { id: 'getUsersWithTags', - query: { - properties: { - filter: { - properties: { - tag: { - type: 'array', - items: { $ref : 'sch#/definitions/tag/tagname' } - } - }, - required: ['tag'], - additionalProperties : false - } - }, - required: ['filter'], - additionalProperties: false - } + properties: { + query: { + properties: { + filter: { + properties: { + tag: { + type: 'array', + items: { $ref : 'sch#/definitions/tag/tagname' } + } + }, + required: ['tag'], + additionalProperties : false + } + }, + required: ['filter'], + additionalProperties: false + } + }, + required: ['query'] }, postUserTags: { id: 'postUserTags', @@ -251,5 +283,5 @@ module.exports = { }, required: ['body', 'params'] }, - getUser, patchUser + getUser, patchUser, getUsersWithMyTags, getUsersWithLocation }; diff --git a/controllers/validators/users.js b/controllers/validators/users.js index 2ab7f23..2a9dfd0 100644 --- a/controllers/validators/users.js +++ b/controllers/validators/users.js @@ -1,95 +1,23 @@ 'use strict'; -const _ = require('lodash'); - -const parser = require('./parser'), - schema = require('./schema'), - { ajv } = require('./ajvInit'); - const validate = require('./validate-by-schema'); -exports.getUsersWithTags = function (req, res, next) { - - req.query = parser.parseQuery(req.query, parser.parametersDictionary); - - const validate = ajv.compile(schema.getUsersWithTags.query); - - const valid = validate(req.query); - - if (!valid) { - const errorOutput = ajv.errorsText(validate.errors); - return res.status(400).json({'errors':errorOutput}); - } - return next(); - - // TODO validate the tagnames in req.query.filter.tag -}; - -exports.getUsersWithMyTags = function (req, res, next) { - if (_.has(req, 'query.filter.byMyTags')) { - const filter = req.query.filter; - filter.byMyTags = (filter.byMyTags === 'true') ? true : false; - } - return next(); -}; - -exports.getUsersWithLocation = function (req, res, next) { - // parse the location - if (_.has(req, 'query.filter.location')) { - - const rawLocation = req.query.filter.location.split(','); - - if (rawLocation.length !== 4) throw new Error('invalid amount of parameters for location provided (TODO to error 400)'); - - // parse location to numbers - const [lat1, lon1, lat2, lon2] = _.map(rawLocation, loc => +loc); - - req.query.filter.location = [[lat1, lon1], [lat2, lon2]]; - } - - return next(); -}; - +const getUsersWithTags = validate('getUsersWithTags'); +const getNewUsersWithMyTags = validate('newUsersWithMyTags'); +const getUsersWithLocation = validate('getUsersWithLocation'); const post = validate('postUsers'); const get = validate('getUser'); -const patch = validate('patchUser'); - -exports.get = get; -exports.patch = patch; -exports.post = post; - -exports.getNewUsers = function (req, res, next) { - - // req.query = parser.newUsers(req.query); - // TODO where should be parser placed - req.query = parser.parseQuery(req.query, parser.parametersDictionary); - - // console.log(p.parseQuery(req.query, parametersDictionary)) - - const validate = ajv.compile(schema.newUsers.query); - const valid = validate(req.query); - - if (!valid) { - const errorOutput = ajv.errorsText(validate.errors); - - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.getNewUsersWithMyTags = function (req, res, next) { - - req.query = parser.parseQuery(req.query, parser.parametersDictionary); - - const validate = ajv.compile(schema.newUsersWithMyTags.query); - const valid = validate(req.query); - - if (!valid) { - const errorOutput = ajv.errorsText(validate.errors); - - return res.status(400).json(errorOutput); - } - - return next(); +const patch = validate('patchUser', [['params.username', 'body.id']]); +const getUsersWithMyTags = validate('getUsersWithMyTags'); +const getNewUsers = validate('newUsers'); + +module.exports = { + get, + patch, + post, + getUsersWithMyTags, + getUsersWithTags, + getNewUsers, + getNewUsersWithMyTags, + getUsersWithLocation }; diff --git a/routes/users.js b/routes/users.js index ef47a41..2e9cb31 100644 --- a/routes/users.js +++ b/routes/users.js @@ -8,13 +8,15 @@ const userController = require(path.resolve('./controllers/users')); const validators = require(path.resolve('./controllers/validators')), authorize = require(path.resolve('./controllers/authorize')); +const { parse } = require(path.resolve('./controllers/validators/parser')); + // post a new user router.route('/') .post(validators.users.post, userController.postUsers); // get new users who have common tags with me router.route('/') - .get(userController.gotoGetNewUsersWithMyTags, authorize.onlyLogged, validators.users.getNewUsersWithMyTags, userController.getNewUsersWithMyTags); + .get(userController.gotoGetNewUsersWithMyTags, authorize.onlyLogged, parse, validators.users.getNewUsersWithMyTags, userController.getNewUsersWithMyTags); // get users who have common tags with me router.route('/') @@ -22,15 +24,15 @@ router.route('/') // get users who have given tags router.route('/') - .get(userController.gotoGetUsersWithTags, authorize.onlyLogged, validators.users.getUsersWithTags, userController.getUsersWithTags); + .get(userController.gotoGetUsersWithTags, authorize.onlyLogged, parse, validators.users.getUsersWithTags, userController.getUsersWithTags); // get users from given location router.route('/') - .get(userController.gotoGetUsersWithLocation, authorize.onlyLogged, validators.users.getUsersWithLocation, userController.getUsersWithLocation); + .get(userController.gotoGetUsersWithLocation, authorize.onlyLogged, parse, validators.users.getUsersWithLocation, userController.getUsersWithLocation); // get new users router.route('/') - .get(userController.gotoGetNewUsers, authorize.onlyLogged, validators.users.getNewUsers, userController.getNewUsers); + .get(userController.gotoGetNewUsers, authorize.onlyLogged, parse, validators.users.getNewUsers, userController.getNewUsers); // get and patch user profile router.route('/:username') diff --git a/test/users.js b/test/users.js index e76158f..47be335 100644 --- a/test/users.js +++ b/test/users.js @@ -455,7 +455,7 @@ describe('/users', function () { it('list users sorted by relevance weight', async function () { const res = await agent - .get('/users?filter[byMyTags]=true') + .get('/users?filter[byMyTags]') .set('Content-Type', 'application/vnd.api+json') .auth(loggedUser.username, loggedUser.password) .expect('Content-Type', /^application\/vnd\.api\+json/) @@ -485,7 +485,7 @@ describe('/users', function () { it('include user-tag relationships', async function () { const res = await agent - .get('/users?filter[byMyTags]=true') + .get('/users?filter[byMyTags]') .set('Content-Type', 'application/vnd.api+json') .auth(loggedUser.username, loggedUser.password) .expect('Content-Type', /^application\/vnd\.api\+json/) @@ -558,6 +558,15 @@ describe('/users', function () { } }]); }); + + it('[invalid query.filter.byMyTags] should fail with 400', async () => { + await agent + .get('/users?filter[byMyTags]=asdf') + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect('Content-Type', /^application\/vnd\.api\+json/) + .expect(400); + }); }); describe('show new users', function () { From 762da4d94e67285c8af3884d8ad1d3f485a66d37 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sat, 19 Aug 2017 15:42:25 +0300 Subject: [PATCH 08/11] refactor tag validation --- controllers/tags.js | 6 +-- controllers/validators/parser.js | 3 +- controllers/validators/schema.js | 66 +++++++++++++++++++++++++++----- controllers/validators/tags.js | 60 +++-------------------------- routes/tags.js | 7 ++-- test/tags.js | 26 +++++++++++-- 6 files changed, 94 insertions(+), 74 deletions(-) diff --git a/controllers/tags.js b/controllers/tags.js index 261afdc..49bc9ba 100644 --- a/controllers/tags.js +++ b/controllers/tags.js @@ -122,11 +122,11 @@ exports.relatedToMyTags = async function (req, res, next) { */ exports.relatedToTags = async function (req, res, next) { - const tagsArray = req.query.filter.relatedToTags.split(','); + const tags = req.query.filter.relatedToTags; - try{ + try { // get tags from database - const foundTags = await models.tag.findTagsRelatedToTags(tagsArray); + const foundTags = await models.tag.findTagsRelatedToTags(tags); // define the parameters for self link foundTags.urlParam = encodeURIComponent('filter[relatedToTags]'); diff --git a/controllers/validators/parser.js b/controllers/validators/parser.js index 006f6f5..5af1ebc 100644 --- a/controllers/validators/parser.js +++ b/controllers/validators/parser.js @@ -22,7 +22,8 @@ const parametersDictionary = { filter: { tag: 'array', withMyTags: 'int', - location: 'coordinates' + location: 'coordinates', + relatedToTags: 'array' }, }; diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 66f5b75..7d1f07c 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -4,6 +4,7 @@ const paths = { familyName: { $ref : 'sch#/definitions/user/familyName' }, description: { $ref : 'sch#/definitions/user/desc' }, location: { $ref : 'sch#/definitions/user/location' }, + tagname: { $ref : 'sch#/definitions/tag/tagname' }, }; const getUser = { @@ -63,6 +64,55 @@ const getUsersWithLocation = { id: 'getUsersWithLocation' }; +const postTags = { + id: 'postTags', + properties: { + body: { + properties: { + tagname: paths.tagname + }, + required: ['tagname'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const getTag = { + id: 'getTag', + properties: { + params: { + properties: { + tagname: paths.tagname + }, + required: ['tagname'], + additionalProperties: false + } + }, + required: ['params'] +}; + +const getTagsRelatedToTags = { + id: 'getTagsRelatedToTags', + properties: { + query: { + properties: { + filter: { + properties: { + relatedToTags: { + type: 'array', + items: paths.tagname + } + }, + required: ['relatedToTags'] + } + }, + required: ['filter'] + } + }, + required: ['query'] +}; + module.exports = { definitions: { user: { @@ -124,7 +174,8 @@ module.exports = { tag: { tagname: { type: 'string', - minLength: 1, + minLength: 2, + maxLength: 64, pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' } }, @@ -227,7 +278,7 @@ module.exports = { properties: { tag: { type: 'array', - items: { $ref : 'sch#/definitions/tag/tagname' } + items: paths.tagname } }, required: ['tag'], @@ -247,9 +298,7 @@ module.exports = { properties: { tag: { properties: { - tagname: { - $ref: 'sch#/definitions/tag/tagname' - } + tagname: paths.tagname } }, story: { $ref: 'sch#/definitions/userTag/story' }, @@ -275,13 +324,12 @@ module.exports = { }, params: { properties: { - tagname: { - $ref: 'sch#/definitions/tag/tagname' - } + tagname: paths.tagname } } }, required: ['body', 'params'] }, - getUser, patchUser, getUsersWithMyTags, getUsersWithLocation + getUser, patchUser, getUsersWithMyTags, getUsersWithLocation, + postTags, getTag, getTagsRelatedToTags }; diff --git a/controllers/validators/tags.js b/controllers/validators/tags.js index 3a2b552..041e578 100644 --- a/controllers/validators/tags.js +++ b/controllers/validators/tags.js @@ -1,59 +1,9 @@ 'use strict'; -const _ = require('lodash'); -const rules = require('./rules'); -exports.postTags = function (req, res, next) { - req.checkBody(_.pick(rules.tag, ['tagname', 'description'])); +const validate = require('./validate-by-schema'); - const errors = req.validationErrors(); +const post = validate('postTags'); +const get = validate('getTag'); +const getTagsRelatedToTags = validate('getTagsRelatedToTags'); - const errorOutput = { errors: [] }; - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.getTag = function (req, res, next) { - req.checkParams(_.pick(rules.tag, ['tagname'])); - - const errors = req.validationErrors(); - - const errorOutput = {errors: []}; - if (errors) { - for(const e of errors) { - errorOutput.errors.push({meta: e}); - } - - return res.status(400).json(errorOutput); - } - return next(); -}; - -exports.getTags = function (req, res, next) { - return next(); -}; - -exports.getTagsRelatedToTags = function (req, res, next) { - // TODO how to react for this filter, istnt it already checked? - // spliting tags just for checking or forever? - if (_.has(req, 'query.filter.relatedToTags')) { - - req.checkQuery('filter.relatedToTags', 'invalid tagnames').isTags(); - - const errors = req.validationErrors(); - const errorOutput = { errors: [] }; - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - } - - return next(); -}; +module.exports = { post, get, getTagsRelatedToTags }; diff --git a/routes/tags.js b/routes/tags.js index 380da14..3ea2559 100644 --- a/routes/tags.js +++ b/routes/tags.js @@ -7,10 +7,11 @@ const path = require('path'); const tagController = require(path.resolve('./controllers/tags')); const validators = require(path.resolve('./controllers/validators')); const authorize = require(path.resolve('./controllers/authorize')); +const { parse } = require(path.resolve('./controllers/validators/parser')); router.route('/') // post a new tag - .post(authorize.onlyLogged, validators.tags.postTags, tagController.postTags) + .post(authorize.onlyLogged, validators.tags.post, tagController.postTags) // get tags like a string .get(tagController.gotoGetTagsLike, authorize.onlyLogged, tagController.getTagsLike); @@ -20,13 +21,13 @@ router.route('/') // get tags related to given tags router.route('/') -.get(tagController.gotoRelatedToTags, validators.tags.getTagsRelatedToTags, authorize.onlyLogged, tagController.relatedToTags); +.get(tagController.gotoRelatedToTags, authorize.onlyLogged, parse, validators.tags.getTagsRelatedToTags, tagController.relatedToTags); // get random tags router.route('/') .get(tagController.gotoGetRandomTags, authorize.onlyLogged, tagController.getRandomTags); router.route('/:tagname') - .get(validators.tags.getTag, tagController.getTag); + .get(validators.tags.get, tagController.getTag); module.exports = router; diff --git a/test/tags.js b/test/tags.js index 10e7a7c..da3366c 100644 --- a/test/tags.js +++ b/test/tags.js @@ -296,7 +296,7 @@ describe('/tags', function () { }); - it('[example 4] respond with tags related to the list of tags', async function() { + it('[example 5] respond with tags related to the list of tags', async function() { const [loggedUser] = dbData.users; const resp = await agent .get('/tags?filter[relatedToTags]=tag0,tag1,tag2,tag3,tag4,tag5,tag6') @@ -309,7 +309,7 @@ describe('/tags', function () { }); - it('[example 5] respond with tags related to the list of tags', async function() { + it('[example 6] respond with tags related to the list of tags', async function() { const [loggedUser] = dbData.users; const resp = await agent .get('/tags?filter[relatedToTags]=tag0,tag1,tag2,tag3,tag4,tag5,tag6') @@ -322,7 +322,7 @@ describe('/tags', function () { }); - it('[example 6] respond with tags related to the list of tags', async function() { + it('[example 7] respond with tags related to the list of tags', async function() { const [loggedUser] = dbData.users; const resp = await agent .get('/tags?filter[relatedToTags]=tag3,tag4,tag4') @@ -447,6 +447,26 @@ describe('/tags', function () { .expect('Content-Type', /^application\/vnd\.api\+json/); }); + it('[too short tagname] should error with 400', async function () { + await agent + .post('/tags') + .send({ data: { type: 'tags', attributes: { tagname: 'a' } } }) + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + + it('[too long tagname] should error with 400', async function () { + await agent + .post('/tags') + .send({ data: { type: 'tags', attributes: { tagname: 'a'.repeat(65) } } }) + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect(400) + .expect('Content-Type', /^application\/vnd\.api\+json/); + }); + it('[duplicate tagname] should error with 409', async function () { await agent .post('/tags') From 4ece7ee2f9c539f257142052403100d6a51f4222 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sat, 19 Aug 2017 18:47:55 +0300 Subject: [PATCH 09/11] test + implement some validation for searching users within a square (GET /users?filter[location]=lat0,lon0,lat1,lon1) --- controllers/validators/parser.js | 8 ++++--- controllers/validators/schema.js | 19 ++++++++++++++++- controllers/validators/users.js | 4 +++- test/location.js | 36 +++++++++++++++++++++++--------- 4 files changed, 52 insertions(+), 15 deletions(-) diff --git a/controllers/validators/parser.js b/controllers/validators/parser.js index 5af1ebc..02d5adb 100644 --- a/controllers/validators/parser.js +++ b/controllers/validators/parser.js @@ -55,10 +55,12 @@ const parseQuery = function (query, parametersDictionary) { const queryString = query[q]; const array = queryString.split(','); - // parse location to numbers - const [lat1, lon1, lat2, lon2] = array.map(loc => +loc); + // create a location from every pair of coordinates, and return array of such locations + query[q] = []; + for(let i = 0, len = array.length; i < len; i = i + 2) { + query[q].push([+array[i], +array[i + 1]]); + } - query[q] = [[lat1, lon1], [lat2, lon2]]; break; } } diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 7d1f07c..4d424ae 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -61,7 +61,24 @@ const getUsersWithMyTags = { }; const getUsersWithLocation = { - id: 'getUsersWithLocation' + id: 'getUsersWithLocation', + properties: { + query: { + properties: { + filter: { + properties: { + location: { + type: 'array', + items: paths.location + } + }, + require: ['location'] + } + }, + require: ['filter'] + } + }, + require: ['query'] }; const postTags = { diff --git a/controllers/validators/users.js b/controllers/validators/users.js index 2a9dfd0..1c5e9ec 100644 --- a/controllers/validators/users.js +++ b/controllers/validators/users.js @@ -4,7 +4,9 @@ const validate = require('./validate-by-schema'); const getUsersWithTags = validate('getUsersWithTags'); const getNewUsersWithMyTags = validate('newUsersWithMyTags'); -const getUsersWithLocation = validate('getUsersWithLocation'); +const getUsersWithLocation = validate('getUsersWithLocation', [['query.filter.location[0]', 'query.filter.location[1]', ([loc00, loc01], [loc10, loc11]) => { + return loc00 < loc10 && loc01 < loc11; +}]]); const post = validate('postUsers'); const get = validate('getUser'); const patch = validate('patchUser', [['params.username', 'body.id']]); diff --git a/test/location.js b/test/location.js index 69aa255..d3f9da8 100644 --- a/test/location.js +++ b/test/location.js @@ -12,13 +12,6 @@ const agent = supertest.agent(app); let dbData; -/* -const nonexistentUser = { - username: 'nonexistent-user', - email: 'nonexistent-email@example.com', -}; -*/ - describe('Location of people, tags, ideas, projects, ...', function () { let sandbox; @@ -342,7 +335,6 @@ describe('Location of people, tags, ideas, projects, ...', function () { }); it('limit the size of the rectangle'); - it('TODO should we filter only verified users?'); // well, only verified people can have location in the first place it('don\'t leak the preciseLocation', async function () { const res = await agent @@ -358,8 +350,32 @@ describe('Location of people, tags, ideas, projects, ...', function () { }); context('invalid location', function () { - it('[wrong corners] error with 400'); - it('[wrong amount of coordinates] error with 400'); + it('[wrong corners] error with 400', async function () { + await agent + .get('/users?filter[location]=-5.1,15.1,5.1,4.9') + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect('Content-Type', /^application\/vnd\.api\+json/) + .expect(400); + }); + + it('[wrong amount of coordinates] error with 400', async function () { + await agent + .get('/users?filter[location]=-5,5,5,15,0') + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect('Content-Type', /^application\/vnd\.api\+json/) + .expect(400); + }); + + it('[out of range] error with 400', async () => { + await agent + .get('/users?filter[location]=-91,5,-80,15') + .set('Content-Type', 'application/vnd.api+json') + .auth(loggedUser.username, loggedUser.password) + .expect('Content-Type', /^application\/vnd\.api\+json/) + .expect(400); + }); }); }); From ca3a57c81d98189e19e305bd423c2b42e909fe18 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sun, 20 Aug 2017 03:30:37 +0300 Subject: [PATCH 10/11] refactor validation to json-schema and away from express-validator --- app.js | 12 +- controllers/messages.js | 3 +- controllers/validators/account.js | 180 +------------------ controllers/validators/contacts.js | 269 ++--------------------------- controllers/validators/custom.js | 23 --- controllers/validators/index.js | 54 ------ controllers/validators/messages.js | 71 +------- controllers/validators/rules.js | 123 ------------- controllers/validators/schema.js | 244 +++++++++++++++++++++++++- package.json | 1 - routes/contacts.js | 4 +- test/account.js | 20 +-- test/contacts.js | 14 +- test/messages.js | 2 +- 14 files changed, 282 insertions(+), 738 deletions(-) delete mode 100644 controllers/validators/custom.js delete mode 100644 controllers/validators/rules.js diff --git a/app.js b/app.js index fc862ac..61242c9 100644 --- a/app.js +++ b/app.js @@ -4,15 +4,13 @@ const express = require('express'), bodyParser = require('body-parser'), passport = require('passport'), - helmet = require('helmet'), - expressValidator = require('express-validator'); + helmet = require('helmet'); // load internal dependencies const models = require('./models'), config = require('./config'), authenticate = require('./controllers/authenticate'), - deserialize = require('./controllers/deserialize'), - customValidators = require('./controllers/validators/custom'); + deserialize = require('./controllers/deserialize'); // configure the database for all the models @@ -58,10 +56,6 @@ app.use(deserialize); app.use(passport.initialize()); app.use(authenticate); -app.use(expressValidator({ - customValidators: customValidators -})); - // we set Content-Type header of all requests to JSON API app.use(function (req, res, next) { res.contentType('application/vnd.api+json'); @@ -122,4 +116,4 @@ app.use(function(err, req, res, next) { // eslint-disable-line no-unused-vars }); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/controllers/messages.js b/controllers/messages.js index 13aaf57..1877f83 100644 --- a/controllers/messages.js +++ b/controllers/messages.js @@ -12,7 +12,8 @@ const path = require('path'), exports.postMessages = async function (req, res, next) { try { // message body and receiver should be provided - const { body, to: { username: to } } = req.body; + const { body: rawBody, to: { username: to } } = req.body; + const body = rawBody.trim(); // message sender is the authorized user const from = req.auth.username; diff --git a/controllers/validators/account.js b/controllers/validators/account.js index e96bd8c..0fc489c 100644 --- a/controllers/validators/account.js +++ b/controllers/validators/account.js @@ -1,175 +1,9 @@ -const rules = require('./rules'); -const _ = require('lodash'); +const validate = require('./validate-by-schema'); -exports.resetPassword = function (req, res, next) { - - // check that only expected attributes are present in request body - const expectedAttrs = ['id']; - const actualAttrs = Object.keys(req.body); - - const unexpectedAttrs = _.difference(actualAttrs, expectedAttrs); - - if (unexpectedAttrs.length > 0) { - return res.status(400).end(); - } - - - // check that id is a valid username or email - const { username: usernameRules, email: emailRules } = rules.user; - - req.checkBody({ id: usernameRules }); - req.checkBody({ id: emailRules }); - - const errors = req.validationErrors(); - - // if id is not valid username nor email, the errors length is 2 or more. Otherwise 1 - if (errors.length >= 2) { - - const errorOutput = { errors: [] }; - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.updateResetPassword = function (req, res, next) { - - const { username, code, password } = rules.user; - const pickedRules = { - id: username, - code, - password - }; - - req.checkBody(pickedRules); - const errors = req.validationErrors(); - - // if id is not valid username nor email, the errors length is 2 or more. Otherwise 1 - if (errors) { - - const errorOutput = { errors: [] }; - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.updateUnverifiedEmail = function (req, res, next) { - - const expectedAttrs = ['id', 'email', 'password']; - const actualAttrs = Object.keys(req.body); - - // only the right attributes - const unexpectedAttrs = _.difference(actualAttrs, expectedAttrs); - const missingAttrs = _.difference(expectedAttrs, actualAttrs); - - if (unexpectedAttrs.length > 0) { - return res.status(400).end(); - } - - if (missingAttrs.length > 0) { - return res.status(400).json({ - errors: [{ meta: 'missing password attribute' }] - }); - } - - // mismatch body.id & auth.username - if (req.auth.username !== req.body.id) { - return res.status(403).json({ - errors: [{ meta: `not enough rights to update user ${req.body.id}` }] - }); - } - - - - const pickedRules = _.pick(rules.user, ['email', 'password']); - - req.checkBody(pickedRules); - const errors = req.validationErrors(); - - if (errors) { - const errorOutput = { errors: [] }; - - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.verifyEmail = function (req, res, next) { - - let errors = []; - - const { username, code } = rules.user; - - // check that the username is valid - req.body.username = req.body.id; - req.checkBody({ username }); - delete req.body.username; - - // check that the code is valid - req.body.code = req.body.emailVerificationCode; - req.checkBody({ code }); - delete req.body.code; - - errors = errors.concat(req.validationErrors() || []); - - if (errors.length === 0) { - return next(); - } - - return next(errors); -}; - -exports.changePassword = function (req, res, next) { - - let errors = []; - - // username in url should match username in body should match logged user - if (req.body.id !== req.auth.username) { - errors.push({ - param: 'parameters', - msg: 'document id doesn\'t match logged user' - }); - } - - // only expected fields should be present - const passwordFields = ['id', 'password', 'oldPassword']; - const requestBodyFields = Object.keys(req.body); - - const unexpectedFields = _.difference(requestBodyFields, passwordFields); - - if (unexpectedFields.length > 0) { - errors.push({ - param: 'attributes', - msg: 'unexpected body attributes', - value: unexpectedFields - }); - } - - // both passwords should be valid - const passwordRules = rules.user.password; - - req.checkBody({ - password: passwordRules, - oldPassword: passwordRules - }); - - errors = errors.concat(req.validationErrors() || []); - - if (errors.length === 0) { - return next(); - } - - return next(errors); -}; +const changePassword = validate('changePassword', [['auth.username', 'body.id']]); +const resetPassword = validate('resetPassword'); +const updateResetPassword = validate('updateResetPassword'); +const updateUnverifiedEmail = validate('updateUnverifiedEmail', [['auth.username', 'body.id']]); +const verifyEmail = validate('verifyEmail'); +module.exports = { resetPassword, updateResetPassword, updateUnverifiedEmail, verifyEmail, changePassword }; diff --git a/controllers/validators/contacts.js b/controllers/validators/contacts.js index 05cedfa..c0f16b3 100644 --- a/controllers/validators/contacts.js +++ b/controllers/validators/contacts.js @@ -1,263 +1,20 @@ 'use strict'; -const _ = require('lodash'); +const validate = require('./validate-by-schema'); -const rules = require('./rules'); +const get = validate('getContact'); -exports.post = function (req, res, next) { - let errors = []; +const post = validate('postContacts', [ + ['body.to.username','auth.username', (a, b) => a !== b ] +]); - // check if the body has and only has the expected attributes - const expectedAttrs = ['trust', 'reference', 'message', 'to']; - const attrs = Object.keys(req.body); - const missingAttrs = _.difference(expectedAttrs, attrs); +const patchConfirm = validate('patchConfirmContact', [ + ['body.id', ['params.from', 'params.to'], (bodyId, params) => bodyId === params.join('--')] +]); - const invalidAttrs = _.difference(attrs, expectedAttrs); +const patchUpdate = validate('patchUpdateContact', [ + ['body.id', ['params.from', 'params.to'], (bodyId, params) => bodyId === params.join('--')], + ['auth.username', 'params.from'] +]); - if (missingAttrs.length > 0) { - errors.push({ - msg: 'incomplete request', - value: `missing attributes: ${missingAttrs.join(', ')}` - }); - } - - if (invalidAttrs.length > 0) { - errors.push({ - msg: 'invalid request', - value: `invalid attributes: ${invalidAttrs.join(', ')}` - }); - } - - // check that the target username is valid - req.body.username = req.body.to.username; - req.checkBody(_.pick(rules.user, ['username'])); - delete req.body.username; - - // check that reference and message is valid - req.checkBody(_.pick(rules.contact, ['reference', 'message'])); - - // check that trust level is valid - if (!validateTrust(req.body.trust)) errors.push({ - param: 'trust', - msg: 'the trust level is invalid', - value: req.body.trust - }); - - // prepare and return errors - errors = errors.concat(req.validationErrors() || []); - - // check whether the contact is not sent to self - const isToSelf = req.body.to.username === req.auth.username; - - if (isToSelf) { - errors.push({ - param: 'to', - msg: 'you cannot create a contact to yourself', - value: req.body.to.username - }); - } - - const errorOutput = { errors: [] }; - if (_.isArray(errors) && errors.length > 0) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.patchConfirm = function (req, res, next) { - let errors = []; - - // check if the body has and only has the expected attributes - const expectedAttrs = ['trust', 'reference', 'isConfirmed', 'id']; - const attrs = Object.keys(req.body); - const missingAttrs = _.difference(expectedAttrs, attrs); - const invalidAttrs = _.difference(attrs, expectedAttrs); - - if (missingAttrs.length > 0) { - errors.push({ - msg: 'incomplete request', - value: `missing attributes: ${missingAttrs.join(', ')}` - }); - } - - if (invalidAttrs.length > 0) { - errors.push({ - msg: 'invalid request', - value: `invalid attributes: ${invalidAttrs.join(', ')}` - }); - } - - // check that query params match jsonapi document id - const { from: queryFrom, to: queryTo } = req.params; - const [from, to] = req.body.id.split('--'); - - const matchFromTo = queryFrom === from && queryTo === to; - if (!matchFromTo) { - errors.push({ - param: 'id', - msg: 'document id doesn\'t match the url parameters', - value: req.body.id - }); - } - - // check that the target username is valid - req.body.username = to; - req.checkBody(_.pick(rules.user, ['username'])); - delete req.body.username; - - // check that reference is valid - req.checkBody(_.pick(rules.contact, ['reference'])); - - // check that trust level is valid - const isTrustValid = [1, 2, 4, 8].indexOf(req.body.trust) > -1; - if(!isTrustValid) { - errors.push({ - param: 'trust', - msg: 'the trust level is invalid', - value: req.body.trust - }); - } - - // check that trust level is valid - const isConfirmedValid = req.body.isConfirmed === true; - if(!isConfirmedValid) { - errors.push({ - param: 'isConfirmed', - msg: 'isConfirmed must be true (we can only confirm the contact (use DELETE to refuse the contact))', - value: req.body.isConfirmed - }); - } - - // prepare and return errors - errors = errors.concat(req.validationErrors() || []); - - - if (errors.length > 0) { - - const errorOutput = { errors: [] }; - - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -/** - * Validators for updating a contact - * - * Rules: - * - id in body needs to match url - * - contact needs to belong to me (i must be the originator) - * - only expected attributes - * - attributes need to be valid - * - */ -exports.patchUpdate = function (req, res, next) { - const { from, to } = req.params; - const [fromBody, toBody] = req.body.id.split('--'); - // id should match url - const idMatchesUrl = from === fromBody && to === toBody; - - let errors = []; - - if (!idMatchesUrl) { - return res.status(400).json({ - errors: [{ meta: 'id doesn\'t match url' }] - }); - } - - // when the contact doesn't belong to me, i should be Forbidden - const belongsToMe = from === req.auth.username; - if (!belongsToMe) { - return res.status(403).end(); - } - - // when some invalid attributes are present, error 400 - const validAttrs = ['trust', 'reference', 'message', 'id']; - const presentAttrs = Object.keys(req.body); - const invalidAttrs = _.difference(presentAttrs, validAttrs); - if (invalidAttrs.length > 0) { - return res.status(400).json({ - errors: [ - { meta: 'invalid attributes provided' } - ] - }); - } - - // check that trust is valid - if (_.has(req.body, 'trust') && !validateTrust(req.body.trust)) { - errors.push('trust is invalid'); - } - - // check that reference and message is valid - req.checkBody(_.pick(rules.contact, ['reference', 'message'])); - - // prepare and return errors - errors = errors.concat(req.validationErrors() || []); - - errors = _.map(errors, (err) => { - switch(err.param) { - case 'reference': { - return 'reference is invalid'; - } - case 'message': { - return 'message is invalid (too long)'; - } - default: - return err; - } - }); - - const errorOutput = { errors: [] }; - if (_.isArray(errors) && errors.length > 0) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.getOne = function (req, res, next) { - let errors = []; - - // check that reference is valid - req.checkParams({ - from: rules.user.username, - to: rules.user.username - }); - - // prepare and return errors - errors = errors.concat(req.validationErrors() || []); - - - if (errors.length > 0) { - - const errorOutput = { errors: [] }; - - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -/** - * Provided trust, check that it is valid - * @param {any} trust - a value to be validated - * @returns boolean - true when valid, otherwise false - */ -function validateTrust(trust) { - // check that trust level is valid - return [1, 2, 4, 8].indexOf(trust) > -1; -} +module.exports = { get, post, patchUpdate, patchConfirm }; diff --git a/controllers/validators/custom.js b/controllers/validators/custom.js deleted file mode 100644 index cc90028..0000000 --- a/controllers/validators/custom.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -// TODO this is likely to be removed or updated in future - -const path = require('path'); -const models = require(path.resolve('./models')); - -// check if given value is a string of tags separated by commas ('tag0,tag1,tag2') -exports.isTags = function(value) { - // TODO Regex should be taken from somewhere (rules?) - // old regex const re = new RegExp('^$|^[a-z][a-z0-9]*([,][a-z][a-z0-9]*)*$'); - const re = new RegExp('^$|^[a-z0-9][\-a-z0-9]*[a-z0-9]$'); - const arrayOfTags = value.split('\,'); - for (const t of arrayOfTags) { - if (!re.test(t)) { - return false; - } - } - return true; -}; - - -models; diff --git a/controllers/validators/index.js b/controllers/validators/index.js index 78f30ec..994e526 100644 --- a/controllers/validators/index.js +++ b/controllers/validators/index.js @@ -1,62 +1,8 @@ - 'use strict'; -const _ = require('lodash'); -const rules = require('./rules'); - exports.contacts = require('./contacts'); exports.messages = require('./messages'); exports.account = require('./account'); exports.users = require('./users'); exports.tags = require('./tags'); exports.userTags = require('./user-tags'); - - - - -exports.postTags = function (req, res, next) { - req.checkBody(_.pick(rules.tag, ['tagname', 'description'])); - - const errors = req.validationErrors(); - - const errorOutput = { errors: [] }; - if (errors) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; - -exports.getTag = function (req, res, next) { - req.checkParams(_.pick(rules.tag, ['tagname'])); - - const errors = req.validationErrors(); - - const errorOutput = {errors: []}; - if (errors) { - for(const e of errors) { - errorOutput.errors.push({meta: e}); - } - - return res.status(400).json(errorOutput); - } - return next(); -}; - -exports.getTags = function (req, res, next) { - return next(); -}; - -exports.postUserTags = function (req, res, next) { - return next(); -}; - -exports.patchUserTag = function (req, res, next) { - // TODO - return next(); -}; - - diff --git a/controllers/validators/messages.js b/controllers/validators/messages.js index d9d1a0d..b1d4f52 100644 --- a/controllers/validators/messages.js +++ b/controllers/validators/messages.js @@ -1,72 +1,9 @@ 'use strict'; -const _ = require('lodash'); +const validate = require('./validate-by-schema'); -const rules = require('./rules'); +const post = validate('postMessages', [['auth.username', 'body.to.username', (a, b) => a !== b]]); -exports.post = function (req, res, next) { - // remove whitespaces from beginning and end of message body - req.body.body = req.body.body.trim(); - // validate the message body - req.checkBody(_.pick(rules.message, ['body'])); +const patch = validate('patchMessage', [['params.id', 'body.id']]); - // validate message receiver - req.body.username = _.get(req.body, 'to.username'); - req.checkBody(_.pick(rules.user, ['username'])); - delete req.body.username; - - // prepare and return errors - let errors = req.validationErrors(); - - - // check whether the receiver is different from sender - const isSenderEqualReceiver = req.body.to.username === req.auth.username; - - if (isSenderEqualReceiver) { - errors = errors || []; - errors.push({ - param: 'to', - msg: 'Receiver can\'t be the sender', - value: req.body.to.username - }); - } - - if (errors.length === 0) { - return next(); - } - - return next(errors); -}; - -exports.patch = function (req, res, next) { - const errors = []; - if (req.body.hasOwnProperty('read')) { - // ids don't match - const idsMatch = req.params.id && req.params.id === req.body.id; - if (!idsMatch) { - errors.push('ids in request body and url don\'t match'); - } - - // body contains more attributes than just id and read - const containsMore = _.difference(Object.keys(req.body), ['id', 'read']).length > 0; - - if (containsMore) { - errors.push('other attributes shouldn\'t be provided with \'read\''); - } - - // req.body.read === true - if (req.body.read !== true) { - errors.push('Invalid value for the attribute \'read\' provided'); - } - } - - const errorOutput = { errors: [] }; - if (_.isArray(errors) && errors.length > 0) { - for(const e of errors) { - errorOutput.errors.push({ meta: e }); - } - return res.status(400).json(errorOutput); - } - - return next(); -}; +module.exports = { post, patch }; diff --git a/controllers/validators/rules.js b/controllers/validators/rules.js deleted file mode 100644 index 635b746..0000000 --- a/controllers/validators/rules.js +++ /dev/null @@ -1,123 +0,0 @@ -'use strict'; - -const user = { - username: { - notEmpty: true, - matches: { - options: [/^(?=.{2,32}$)[a-z0-9]+([_\-.][a-z0-9]+)*$/] - }, - errorMessage: 'Invalid Username (only a-z0-9.-_)' // Error message for the parameter - }, - email: { - notEmpty: true, - isEmail: true, - errorMessage: 'Invalid Email' - }, - givenName: { - isLength: { - options: [{ max: 128 }] - } - }, - familyName: { - isLength: { - options: [{ max: 128 }] - } - }, - description: { - isLength: { - options: [{ max: 2048 }] - } - }, - password: { - isLength: { - options: [{ min: 8, max: 512 }] - }, - errorMessage: 'Password should be 8-512 characters long' - }, - code: { - notEmpty: true, - matches: { - options: [/^[0-9a-f]{32}$/], - errorMessage: 'code is invalid' - }, - errorMessage: 'Invalid code' - }, - get id() { - return this.username; - } -}; - -const userTag = { - story: { - isLength: { - options: [{ max: 1024 }], - errorMessage: 'userTag story can be at most 1024 characters long' - } - } -}; - -const tag = { - tagname: { - notEmpty: true, - isLength: { - options: [{ min: 2, max: 64 }] - }, - matches: { - options: [/^[a-z0-9]+(-[a-z0-9]+)*$/] - }, - errorMessage: 'Invalid Tagname (2-64 characters; only a-z, -, i.e. tag-name; but not -tag-name, tag--name, tagname-)' - }, - get id() { - return this.tagname; - } -}; - -const message = { - body: { - notEmpty: true, - isLength: { - options: [{ max: 2048 }] - } - } -}; - -const contact = { - reference: { - isLength: { - options: [{ max: 2048 }] - } - }, - message: { - isLength: { - options: [{ max: 2048 }] - } - } -}; - -const newUsers = { - 'sort': { - notEmpty: true, - matches: { - options: [/^-created$/] - }, - errorMessage: 'Invalid "sorted" parameter for getting new users' - }, - 'page.offset': { - notEmpty: true, - matches: { - options: [/^0$/] - }, - errorMessage: 'Invalid "offset" parameter for getting new users' - }, - 'page.limit': { - notEmpty: true, - matches: { - options: [/^[0-9]+$/] - }, - errorMessage: 'Invalid "limit" parameter for getting new users' - - } -}; - -module.exports = { user, tag, message, contact, userTag, newUsers }; - diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js index 4d424ae..67ff38f 100644 --- a/controllers/validators/schema.js +++ b/controllers/validators/schema.js @@ -1,10 +1,20 @@ +'use strict'; + const paths = { username: { $ref : 'sch#/definitions/user/username' }, + email: { $ref : 'sch#/definitions/user/email' }, + password: { $ref : 'sch#/definitions/user/password' }, + code: { $ref : 'sch#/definitions/user/code' }, givenName: { $ref : 'sch#/definitions/user/givenName' }, familyName: { $ref : 'sch#/definitions/user/familyName' }, description: { $ref : 'sch#/definitions/user/desc' }, location: { $ref : 'sch#/definitions/user/location' }, tagname: { $ref : 'sch#/definitions/tag/tagname' }, + trust: { $ref : 'sch#/definitions/contact/trust' }, + contactMessage: { $ref : 'sch#/definitions/contact/message' }, + reference: { $ref : 'sch#/definitions/contact/reference' }, + messageBody: { $ref : 'sch#/definitions/message/body' }, + messageId: { $ref : 'sch#/definitions/message/messageId' }, }; const getUser = { @@ -130,6 +140,196 @@ const getTagsRelatedToTags = { required: ['query'] }; +const resetPassword = { + id: 'resetPassword', + properties: { + body: { + type: 'object', + properties: { + id: { + anyOf: [paths.username, paths.email] + } + }, + required: ['id'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const updateResetPassword = { + id: 'updateResetPassword', + properties: { + body: { + properties: { + id: paths.username, + code: paths.code, + password: paths.password + } + } + }, + required: ['body'] +}; + +const updateUnverifiedEmail = { + id: 'updateUnverifiedEmail', + properties: { + body: { + properties: { + email: paths.email, + password: paths.password, + id: paths.username + }, + required: ['email', 'password', 'id'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const verifyEmail = { + id: 'verifyEmail', + properties: { + body: { + properties: { + emailVerificationCode: paths.code, + id: paths.username + }, + required: ['emailVerificationCode', 'id'], + additionalProperties: false // untested + } + }, + required: ['body'] +}; + +const changePassword = { + id: 'changePassword', + properties: { + body: { + properties: { + password: paths.password, + oldPassword: { + type: 'string', + maxLength: 512 + }, + id: paths.username + }, + required: ['password', 'oldPassword', 'id'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const postContacts = { + id: 'postContacts', + properties: { + body: { + properties: { + trust: paths.trust, + to: { + properties: { + username: paths.username + }, + required: ['username'] + }, + message: paths.contactMessage, + reference: paths.reference + }, + required: ['trust', 'to'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const patchConfirmContact = { + id: 'patchConfirmContacts', + properties: { + body: { + properties: { + trust: paths.trust, + reference: paths.reference, + isConfirmed: { + enum: [true] + }, + id: {} + }, + required: ['id', 'isConfirmed', 'trust', 'reference'], + additionalProperties: false + }, + params: { + properties: { + from: paths.username, + to: paths.username + }, + required: ['from', 'to'] + } + }, + required: ['body', 'params'] +}; + +const patchUpdateContact = { + id: 'patchUpdateContact', + properties: { + body: { + properties: { + trust: paths.trust, + reference: paths.reference, + message: paths.contactMessage, + id: {} + }, + additionalProperties: false + } + } +}; + +const getContact = { + properties: { + params: { + properties: { + from: paths.username, + to: paths.username + }, + required: ['from', 'to'] + } + }, + required: ['params'] +}; + +const postMessages = { + properties: { + body: { + properties: { + body: paths.messageBody, + to: { + properties: { + username: paths.username + }, + required: ['username'] + } + }, + required: ['body', 'to'] + } + } +}; + +const patchMessage = { + properties: { + body: { + properties: { + read: { + enum: [true] + }, + id: paths.messageId + }, + required: ['id', 'read'], + additionalProperties: false + } + }, + required: ['body'] +}; + module.exports = { definitions: { user: { @@ -155,8 +355,8 @@ module.exports = { }, password: { type: 'string', - maxLength: 512, - minLength: 8 + minLength: 8, + maxLength: 512 }, location: { oneOf: [ @@ -184,7 +384,6 @@ module.exports = { }, code: { type: 'string', - minLength: 1, pattern: '^[0-9a-f]{32}$' } }, @@ -204,6 +403,30 @@ module.exports = { relevance: { enum: [1, 2, 3, 4, 5] } + }, + contact: { + trust: { + enum: [1, 2, 4, 8] + }, + message: { + type: 'string', + maxLength: 2048 + }, + reference: { + type: 'string', + maxLength: 2048 + }, + }, + message: { + body: { + type: 'string', + minLength: 1, + maxLength: 2048, + pattern: '\\S' // at least one non-space character + }, + messageId: { + type: 'string' + } } }, postUsers: { @@ -234,10 +457,10 @@ module.exports = { type: 'object', properties: { limit: { - type: 'number' + type: 'integer' }, offset: { - type: 'number' + type: 'integer' } }, required: ['limit', 'offset'], @@ -270,10 +493,10 @@ module.exports = { page: { properties: { offset: { - type: 'number' + type: 'integer' }, limit: { - type: 'number' + type: 'integer' } }, required: ['offset', 'limit'], @@ -299,7 +522,7 @@ module.exports = { } }, required: ['tag'], - additionalProperties : false + additionalProperties: false } }, required: ['filter'], @@ -348,5 +571,8 @@ module.exports = { required: ['body', 'params'] }, getUser, patchUser, getUsersWithMyTags, getUsersWithLocation, - postTags, getTag, getTagsRelatedToTags + postTags, getTag, getTagsRelatedToTags, + resetPassword, updateResetPassword, updateUnverifiedEmail, verifyEmail, changePassword, + postContacts, patchConfirmContact, patchUpdateContact, getContact, + postMessages, patchMessage }; diff --git a/package.json b/package.json index 1055d68..0e07e5c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "ejs": "~2.4.1", "email-templates": "^2.5.2", "express": "~4.13.4", - "express-validator": "^2.20.10", "gulp": "^3.9.1", "helmet": "^3.1.0", "identicon.js": "^2.1.0", diff --git a/routes/contacts.js b/routes/contacts.js index 3efed60..95de03a 100644 --- a/routes/contacts.js +++ b/routes/contacts.js @@ -16,8 +16,8 @@ router.route('/') router.route('/:from/:to') .patch(contactController.gotoPatchConfirmContact, authorize.onlyLogged, validators.contacts.patchConfirm, contactController.patchConfirmContact) - .get(authorize.onlyLogged, validators.contacts.getOne, contactController.getContact) - .delete(authorize.onlyLogged, validators.contacts.getOne, contactController.deleteContact); + .get(authorize.onlyLogged, validators.contacts.get, contactController.getContact) + .delete(authorize.onlyLogged, validators.contacts.get, contactController.deleteContact); router.route('/:from/:to').patch(authorize.onlyLogged, validators.contacts.patchUpdate, contactController.patchContact, contactController.getContact); module.exports = router; diff --git a/test/account.js b/test/account.js index 4c79f28..311fd27 100644 --- a/test/account.js +++ b/test/account.js @@ -249,7 +249,7 @@ describe('/account', function () { .set('Content-Type', 'application/vnd.api+json') .expect(400); - should(resp.body).have.propertyByPath('errors', 0, 'meta', 'value').eql('invalid username'); + should(resp.body).have.propertyByPath('errors', 0, 'title').eql('invalid id'); }); it('[nonexistent username] 404', async function () { @@ -302,8 +302,8 @@ describe('/account', function () { .set('Content-Type', 'application/vnd.api+json') .expect(400); - should(resp.body).have.propertyByPath('errors', 0, 'meta', 'msg') - .eql('code is invalid'); + should(resp.body).have.propertyByPath('errors', 0, 'title') + .eql('invalid code'); }); it('[wrong code] 400', async function () { @@ -383,7 +383,7 @@ describe('/account', function () { .set('Content-Type', 'application/vnd.api+json') .expect(400); - should(resp.body).have.propertyByPath('errors', 0, 'meta', 'msg').eql('Password should be 8-512 characters long'); + should(resp.body).have.propertyByPath('errors', 0, 'title').eql('invalid password'); }); }); }); @@ -564,7 +564,7 @@ describe('/account', function () { .expect(403); }); - it('[auth.username vs. req.body.id mismatch] 403', async function () { + it('[auth.username vs. req.body.id mismatch] 400', async function () { const [{ username, password }] = dbData.users; const patchBody = { @@ -583,7 +583,7 @@ describe('/account', function () { .send(patchBody) .set('Content-Type', 'application/vnd.api+json') .auth(username, password) - .expect(403); + .expect(400); }); }); }); @@ -692,7 +692,7 @@ describe('/account', function () { .expect(400); should(response.body).have.propertyByPath('errors', 0, 'title') - .eql('invalid username'); + .eql('invalid id'); }); @@ -716,11 +716,7 @@ describe('/account', function () { .expect(400); should(response.body).have.propertyByPath('errors', 0, 'title') - .eql('invalid code'); - - should(response.body).have.propertyByPath('errors', 0, 'detail') - .eql('code is invalid'); - + .eql('invalid emailVerificationCode'); }); it('[wrong code] should error', async function () { diff --git a/test/contacts.js b/test/contacts.js index b4a1cfc..4518ad9 100644 --- a/test/contacts.js +++ b/test/contacts.js @@ -863,7 +863,7 @@ describe('contacts', function () { }); context('bad data', function () { - it('[contact not from me] 403', async function () { + it('[contact not from me] 400', async function () { const [userA, userB, me] = dbData.users; await agent .patch(`/contacts/${userA.username}/${userB.username}`) @@ -879,7 +879,7 @@ describe('contacts', function () { }) .set('Content-Type', 'application/vnd.api+json') .auth(me.username, me.password) - .expect(403) + .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); }); @@ -903,7 +903,7 @@ describe('contacts', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(resp).have.propertyByPath('body', 'errors', 0, 'meta').eql('invalid attributes provided'); + should(resp).have.propertyByPath('body', 'errors', 0, 'title').eql('invalid attributes'); }); @@ -926,7 +926,7 @@ describe('contacts', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(resp).have.propertyByPath('body', 'errors', 0, 'meta').eql('id doesn\'t match url'); + should(resp).have.propertyByPath('body', 'errors', 0, 'title').eql('invalid'); }); it('[invalid trust] 400', async function () { @@ -947,7 +947,7 @@ describe('contacts', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(resp).have.propertyByPath('body', 'errors', 0, 'meta').eql('trust is invalid'); + should(resp).have.propertyByPath('body', 'errors', 0, 'title').eql('invalid trust'); }); it('[invalid reference] 400', async function () { @@ -968,7 +968,7 @@ describe('contacts', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(resp).have.propertyByPath('body', 'errors', 0, 'meta').eql('reference is invalid'); + should(resp).have.propertyByPath('body', 'errors', 0, 'title').eql('invalid reference'); }); it('[invalid message] 400', async function () { @@ -989,7 +989,7 @@ describe('contacts', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(resp).have.propertyByPath('body', 'errors', 0, 'meta').eql('message is invalid (too long)'); + should(resp).have.propertyByPath('body', 'errors', 0, 'title').eql('invalid message'); }); it('[provided message when already confirmed] 400', async function () { diff --git a/test/messages.js b/test/messages.js index a27f8b8..bbeb7d3 100644 --- a/test/messages.js +++ b/test/messages.js @@ -241,7 +241,7 @@ describe('/messages', function () { .expect(400) .expect('Content-Type', /^application\/vnd\.api\+json/); - should(response.body).have.propertyByPath('errors', 0, 'title').eql('invalid username'); + should(response.body).have.propertyByPath('errors', 0, 'title').eql('invalid attributes'); }); }); // end of context invalid data From 11a1cd304c650fe1c96e8560021ed429eb85ff77 Mon Sep 17 00:00:00 2001 From: mrkvon Date: Sun, 20 Aug 2017 04:40:41 +0300 Subject: [PATCH 11/11] split validators/schema to multiple files --- controllers/validators/schema.js | 578 ------------------- controllers/validators/schema/account.js | 85 +++ controllers/validators/schema/contacts.js | 81 +++ controllers/validators/schema/definitions.js | 100 ++++ controllers/validators/schema/index.js | 12 + controllers/validators/schema/messages.js | 38 ++ controllers/validators/schema/paths.js | 20 + controllers/validators/schema/tags.js | 54 ++ controllers/validators/schema/user-tags.js | 46 ++ controllers/validators/schema/users.js | 195 +++++++ 10 files changed, 631 insertions(+), 578 deletions(-) delete mode 100644 controllers/validators/schema.js create mode 100644 controllers/validators/schema/account.js create mode 100644 controllers/validators/schema/contacts.js create mode 100644 controllers/validators/schema/definitions.js create mode 100644 controllers/validators/schema/index.js create mode 100644 controllers/validators/schema/messages.js create mode 100644 controllers/validators/schema/paths.js create mode 100644 controllers/validators/schema/tags.js create mode 100644 controllers/validators/schema/user-tags.js create mode 100644 controllers/validators/schema/users.js diff --git a/controllers/validators/schema.js b/controllers/validators/schema.js deleted file mode 100644 index 67ff38f..0000000 --- a/controllers/validators/schema.js +++ /dev/null @@ -1,578 +0,0 @@ -'use strict'; - -const paths = { - username: { $ref : 'sch#/definitions/user/username' }, - email: { $ref : 'sch#/definitions/user/email' }, - password: { $ref : 'sch#/definitions/user/password' }, - code: { $ref : 'sch#/definitions/user/code' }, - givenName: { $ref : 'sch#/definitions/user/givenName' }, - familyName: { $ref : 'sch#/definitions/user/familyName' }, - description: { $ref : 'sch#/definitions/user/desc' }, - location: { $ref : 'sch#/definitions/user/location' }, - tagname: { $ref : 'sch#/definitions/tag/tagname' }, - trust: { $ref : 'sch#/definitions/contact/trust' }, - contactMessage: { $ref : 'sch#/definitions/contact/message' }, - reference: { $ref : 'sch#/definitions/contact/reference' }, - messageBody: { $ref : 'sch#/definitions/message/body' }, - messageId: { $ref : 'sch#/definitions/message/messageId' }, -}; - -const getUser = { - id: 'getUser', - properties: { - params: { - properties: { - username: paths.username - } - } - } -}; - -const patchUser = { - id: 'patchUser', - properties: { - params: { - properties: { - username: paths.username - } - }, - body: { - properties: { - id: paths.username, - givenName: paths.givenName, - familyName: paths.familyName, - description: paths.description, - location: paths.location - }, - required: ['id'], - additionalProperties: false - } - } -}; - -const getUsersWithMyTags = { - id: 'getUsersWithMyTags', - properties: { - query: { - properties: { - filter: { - properties: { - byMyTags: { - enum: [''] - } - }, - required: ['byMyTags'] - } - }, - required: ['filter'] - }, - }, - required: ['query'] -}; - -const getUsersWithLocation = { - id: 'getUsersWithLocation', - properties: { - query: { - properties: { - filter: { - properties: { - location: { - type: 'array', - items: paths.location - } - }, - require: ['location'] - } - }, - require: ['filter'] - } - }, - require: ['query'] -}; - -const postTags = { - id: 'postTags', - properties: { - body: { - properties: { - tagname: paths.tagname - }, - required: ['tagname'], - additionalProperties: false - } - }, - required: ['body'] -}; - -const getTag = { - id: 'getTag', - properties: { - params: { - properties: { - tagname: paths.tagname - }, - required: ['tagname'], - additionalProperties: false - } - }, - required: ['params'] -}; - -const getTagsRelatedToTags = { - id: 'getTagsRelatedToTags', - properties: { - query: { - properties: { - filter: { - properties: { - relatedToTags: { - type: 'array', - items: paths.tagname - } - }, - required: ['relatedToTags'] - } - }, - required: ['filter'] - } - }, - required: ['query'] -}; - -const resetPassword = { - id: 'resetPassword', - properties: { - body: { - type: 'object', - properties: { - id: { - anyOf: [paths.username, paths.email] - } - }, - required: ['id'], - additionalProperties: false - } - }, - required: ['body'] -}; - -const updateResetPassword = { - id: 'updateResetPassword', - properties: { - body: { - properties: { - id: paths.username, - code: paths.code, - password: paths.password - } - } - }, - required: ['body'] -}; - -const updateUnverifiedEmail = { - id: 'updateUnverifiedEmail', - properties: { - body: { - properties: { - email: paths.email, - password: paths.password, - id: paths.username - }, - required: ['email', 'password', 'id'], - additionalProperties: false - } - }, - required: ['body'] -}; - -const verifyEmail = { - id: 'verifyEmail', - properties: { - body: { - properties: { - emailVerificationCode: paths.code, - id: paths.username - }, - required: ['emailVerificationCode', 'id'], - additionalProperties: false // untested - } - }, - required: ['body'] -}; - -const changePassword = { - id: 'changePassword', - properties: { - body: { - properties: { - password: paths.password, - oldPassword: { - type: 'string', - maxLength: 512 - }, - id: paths.username - }, - required: ['password', 'oldPassword', 'id'], - additionalProperties: false - } - }, - required: ['body'] -}; - -const postContacts = { - id: 'postContacts', - properties: { - body: { - properties: { - trust: paths.trust, - to: { - properties: { - username: paths.username - }, - required: ['username'] - }, - message: paths.contactMessage, - reference: paths.reference - }, - required: ['trust', 'to'], - additionalProperties: false - } - }, - required: ['body'] -}; - -const patchConfirmContact = { - id: 'patchConfirmContacts', - properties: { - body: { - properties: { - trust: paths.trust, - reference: paths.reference, - isConfirmed: { - enum: [true] - }, - id: {} - }, - required: ['id', 'isConfirmed', 'trust', 'reference'], - additionalProperties: false - }, - params: { - properties: { - from: paths.username, - to: paths.username - }, - required: ['from', 'to'] - } - }, - required: ['body', 'params'] -}; - -const patchUpdateContact = { - id: 'patchUpdateContact', - properties: { - body: { - properties: { - trust: paths.trust, - reference: paths.reference, - message: paths.contactMessage, - id: {} - }, - additionalProperties: false - } - } -}; - -const getContact = { - properties: { - params: { - properties: { - from: paths.username, - to: paths.username - }, - required: ['from', 'to'] - } - }, - required: ['params'] -}; - -const postMessages = { - properties: { - body: { - properties: { - body: paths.messageBody, - to: { - properties: { - username: paths.username - }, - required: ['username'] - } - }, - required: ['body', 'to'] - } - } -}; - -const patchMessage = { - properties: { - body: { - properties: { - read: { - enum: [true] - }, - id: paths.messageId - }, - required: ['id', 'read'], - additionalProperties: false - } - }, - required: ['body'] -}; - -module.exports = { - definitions: { - user: { - username: { - type: 'string', - pattern: '^(?=.{2,32}$)[a-z0-9]+([\\_\\-\\.][a-z0-9]+)*$' - }, - email: { - type: 'string', - pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$' - }, - givenName: { - type: 'string', - maxLength: 128 - }, - familyName: { - type: 'string', - maxLength: 128 - }, - desc: { // description - type: 'string', - maxLength: 2048 - }, - password: { - type: 'string', - minLength: 8, - maxLength: 512 - }, - location: { - oneOf: [ - { - type: 'null' - }, - { - type: 'array', - minItems: 2, - maxItems: 2, - items: [ - { - type: 'number', - minimum: -90, - maximum: 90 - }, - { - type: 'number', - minimum: -180, - maximum: 180 - } - ] - } - ] - }, - code: { - type: 'string', - pattern: '^[0-9a-f]{32}$' - } - }, - tag: { - tagname: { - type: 'string', - minLength: 2, - maxLength: 64, - pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' - } - }, - userTag: { - story: { - type: 'string', - maxLength: 1024 - }, - relevance: { - enum: [1, 2, 3, 4, 5] - } - }, - contact: { - trust: { - enum: [1, 2, 4, 8] - }, - message: { - type: 'string', - maxLength: 2048 - }, - reference: { - type: 'string', - maxLength: 2048 - }, - }, - message: { - body: { - type: 'string', - minLength: 1, - maxLength: 2048, - pattern: '\\S' // at least one non-space character - }, - messageId: { - type: 'string' - } - } - }, - postUsers: { - id: 'postUsers', - properties: { - body: { - properties: { - email: { - $ref : 'sch#/definitions/user/email' - }, - username: paths.username, - password: { $ref: 'sch#/definitions/user/password'} - }, - required: ['username', 'email', 'password'] - }, - required: ['body'] - } - }, - newUsers: { - id: 'newUsers', - properties: { - query: { - properties: { - sort: { - type: 'string' - }, - page: { - type: 'object', - properties: { - limit: { - type: 'integer' - }, - offset: { - type: 'integer' - } - }, - required: ['limit', 'offset'], - additionalProperties: false - } - }, - required: ['sort', 'page'], - additionalProperties: false - } - } - }, - newUsersWithMyTags: { - id: 'newUsersWithMyTags', - properties: { - query:{ - properties:{ - sort: { - type: 'string', - const: '-created' - }, - filter: { - properties: { - withMyTags: { - type: 'number', - } - }, - required: ['withMyTags'], - additionalProperties: false - }, - page: { - properties: { - offset: { - type: 'integer' - }, - limit: { - type: 'integer' - } - }, - required: ['offset', 'limit'], - additionalProperties: false - } - }, - required: ['sort', 'filter', 'page'], - additionalProperties: false - } - }, - required: ['query'] - }, - getUsersWithTags: { - id: 'getUsersWithTags', - properties: { - query: { - properties: { - filter: { - properties: { - tag: { - type: 'array', - items: paths.tagname - } - }, - required: ['tag'], - additionalProperties: false - } - }, - required: ['filter'], - additionalProperties: false - } - }, - required: ['query'] - }, - postUserTags: { - id: 'postUserTags', - properties: { - body: { - properties: { - tag: { - properties: { - tagname: paths.tagname - } - }, - story: { $ref: 'sch#/definitions/userTag/story' }, - relevance: { $ref: 'sch#/definitions/userTag/relevance' } - }, - additionalProperties: false, - required: ['tag', 'story', 'relevance'] - } - }, - required: ['body'] - }, - patchUserTag: { - id: 'patchUserTag', - properties: { - body: { - properties: { - story: { $ref: 'sch#/definitions/userTag/story' }, - relevance: { $ref: 'sch#/definitions/userTag/relevance' }, - id: {} - }, - additionalProperties: false, - required: ['id'] - }, - params: { - properties: { - tagname: paths.tagname - } - } - }, - required: ['body', 'params'] - }, - getUser, patchUser, getUsersWithMyTags, getUsersWithLocation, - postTags, getTag, getTagsRelatedToTags, - resetPassword, updateResetPassword, updateUnverifiedEmail, verifyEmail, changePassword, - postContacts, patchConfirmContact, patchUpdateContact, getContact, - postMessages, patchMessage -}; diff --git a/controllers/validators/schema/account.js b/controllers/validators/schema/account.js new file mode 100644 index 0000000..9acd646 --- /dev/null +++ b/controllers/validators/schema/account.js @@ -0,0 +1,85 @@ +'use strict'; + +const { username, email, code, password } = require('./paths'); + +const resetPassword = { + id: 'resetPassword', + properties: { + body: { + type: 'object', + properties: { + id: { + anyOf: [username, email] + } + }, + required: ['id'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const updateResetPassword = { + id: 'updateResetPassword', + properties: { + body: { + properties: { + id: username, + code, + password + } + } + }, + required: ['body'] +}; + +const updateUnverifiedEmail = { + id: 'updateUnverifiedEmail', + properties: { + body: { + properties: { + email, + password, + id: username + }, + required: ['email', 'password', 'id'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const verifyEmail = { + id: 'verifyEmail', + properties: { + body: { + properties: { + emailVerificationCode: code, + id: username + }, + required: ['emailVerificationCode', 'id'], + additionalProperties: false // untested + } + }, + required: ['body'] +}; + +const changePassword = { + id: 'changePassword', + properties: { + body: { + properties: { + password, + oldPassword: { + type: 'string', + maxLength: 512 + }, + id: username + }, + required: ['password', 'oldPassword', 'id'], + additionalProperties: false + } + }, + required: ['body'] +}; +module.exports = { resetPassword, updateResetPassword, updateUnverifiedEmail, verifyEmail, changePassword }; diff --git a/controllers/validators/schema/contacts.js b/controllers/validators/schema/contacts.js new file mode 100644 index 0000000..100e414 --- /dev/null +++ b/controllers/validators/schema/contacts.js @@ -0,0 +1,81 @@ +'use strict'; + +const { username, trust, reference, contactMessage } = require('./paths'); + +const postContacts = { + id: 'postContacts', + properties: { + body: { + properties: { + trust, + to: { + properties: { + username + }, + required: ['username'] + }, + message: contactMessage, + reference + }, + required: ['trust', 'to'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const patchConfirmContact = { + id: 'patchConfirmContacts', + properties: { + body: { + properties: { + trust, + reference, + isConfirmed: { + enum: [true] + }, + id: {} + }, + required: ['id', 'isConfirmed', 'trust', 'reference'], + additionalProperties: false + }, + params: { + properties: { + from: username, + to: username + }, + required: ['from', 'to'] + } + }, + required: ['body', 'params'] +}; + +const patchUpdateContact = { + id: 'patchUpdateContact', + properties: { + body: { + properties: { + trust, + reference, + message: contactMessage, + id: {} + }, + additionalProperties: false + } + } +}; + +const getContact = { + properties: { + params: { + properties: { + from: username, + to: username + }, + required: ['from', 'to'] + } + }, + required: ['params'] +}; + +module.exports = { postContacts, patchConfirmContact, patchUpdateContact, getContact }; diff --git a/controllers/validators/schema/definitions.js b/controllers/validators/schema/definitions.js new file mode 100644 index 0000000..1724cff --- /dev/null +++ b/controllers/validators/schema/definitions.js @@ -0,0 +1,100 @@ +'use strict'; + +module.exports = { + user: { + username: { + type: 'string', + pattern: '^(?=.{2,32}$)[a-z0-9]+([\\_\\-\\.][a-z0-9]+)*$' + }, + email: { + type: 'string', + pattern: '^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\\.[a-zA-Z0-9-.]+$' + }, + givenName: { + type: 'string', + maxLength: 128 + }, + familyName: { + type: 'string', + maxLength: 128 + }, + desc: { // description + type: 'string', + maxLength: 2048 + }, + password: { + type: 'string', + minLength: 8, + maxLength: 512 + }, + location: { + oneOf: [ + { + type: 'null' + }, + { + type: 'array', + minItems: 2, + maxItems: 2, + items: [ + { + type: 'number', + minimum: -90, + maximum: 90 + }, + { + type: 'number', + minimum: -180, + maximum: 180 + } + ] + } + ] + }, + code: { + type: 'string', + pattern: '^[0-9a-f]{32}$' + } + }, + tag: { + tagname: { + type: 'string', + minLength: 2, + maxLength: 64, + pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' + } + }, + userTag: { + story: { + type: 'string', + maxLength: 1024 + }, + relevance: { + enum: [1, 2, 3, 4, 5] + } + }, + contact: { + trust: { + enum: [1, 2, 4, 8] + }, + message: { + type: 'string', + maxLength: 2048 + }, + reference: { + type: 'string', + maxLength: 2048 + }, + }, + message: { + body: { + type: 'string', + minLength: 1, + maxLength: 2048, + pattern: '\\S' // at least one non-space character + }, + messageId: { + type: 'string' + } + } +}; diff --git a/controllers/validators/schema/index.js b/controllers/validators/schema/index.js new file mode 100644 index 0000000..a93d83c --- /dev/null +++ b/controllers/validators/schema/index.js @@ -0,0 +1,12 @@ +'use strict'; + +const tags = require('./tags'); +const userTags = require('./user-tags'); +const users = require('./users'); +const account = require('./account'); +const contacts = require('./contacts'); +const messages = require('./messages'); + +const definitions = require('./definitions'); + +module.exports = Object.assign({ definitions }, account, contacts, messages, tags, userTags, users); diff --git a/controllers/validators/schema/messages.js b/controllers/validators/schema/messages.js new file mode 100644 index 0000000..416388a --- /dev/null +++ b/controllers/validators/schema/messages.js @@ -0,0 +1,38 @@ +'use strict'; + +const { username, messageId, messageBody } = require('./paths'); + +const postMessages = { + properties: { + body: { + properties: { + body: messageBody, + to: { + properties: { + username + }, + required: ['username'] + } + }, + required: ['body', 'to'] + } + } +}; + +const patchMessage = { + properties: { + body: { + properties: { + read: { + enum: [true] + }, + id: messageId + }, + required: ['id', 'read'], + additionalProperties: false + } + }, + required: ['body'] +}; + +module.exports = { postMessages, patchMessage }; diff --git a/controllers/validators/schema/paths.js b/controllers/validators/schema/paths.js new file mode 100644 index 0000000..7696294 --- /dev/null +++ b/controllers/validators/schema/paths.js @@ -0,0 +1,20 @@ +'use strict'; + +module.exports = { + username: { $ref : 'sch#/definitions/user/username' }, + email: { $ref : 'sch#/definitions/user/email' }, + password: { $ref : 'sch#/definitions/user/password' }, + code: { $ref : 'sch#/definitions/user/code' }, + givenName: { $ref : 'sch#/definitions/user/givenName' }, + familyName: { $ref : 'sch#/definitions/user/familyName' }, + description: { $ref : 'sch#/definitions/user/desc' }, + location: { $ref : 'sch#/definitions/user/location' }, + tagname: { $ref : 'sch#/definitions/tag/tagname' }, + trust: { $ref : 'sch#/definitions/contact/trust' }, + contactMessage: { $ref : 'sch#/definitions/contact/message' }, + reference: { $ref : 'sch#/definitions/contact/reference' }, + messageBody: { $ref : 'sch#/definitions/message/body' }, + messageId: { $ref : 'sch#/definitions/message/messageId' }, + story: { $ref: 'sch#/definitions/userTag/story' }, + relevance: { $ref: 'sch#/definitions/userTag/relevance' }, +}; diff --git a/controllers/validators/schema/tags.js b/controllers/validators/schema/tags.js new file mode 100644 index 0000000..c1b2d51 --- /dev/null +++ b/controllers/validators/schema/tags.js @@ -0,0 +1,54 @@ +'use strict'; + +const { tagname } = require('./paths'); + +const postTags = { + id: 'postTags', + properties: { + body: { + properties: { + tagname + }, + required: ['tagname'], + additionalProperties: false + } + }, + required: ['body'] +}; + +const getTag = { + id: 'getTag', + properties: { + params: { + properties: { + tagname + }, + required: ['tagname'], + additionalProperties: false + } + }, + required: ['params'] +}; + +const getTagsRelatedToTags = { + id: 'getTagsRelatedToTags', + properties: { + query: { + properties: { + filter: { + properties: { + relatedToTags: { + type: 'array', + items: tagname + } + }, + required: ['relatedToTags'] + } + }, + required: ['filter'] + } + }, + required: ['query'] +}; + +module.exports = { postTags, getTag, getTagsRelatedToTags }; diff --git a/controllers/validators/schema/user-tags.js b/controllers/validators/schema/user-tags.js new file mode 100644 index 0000000..02c9ca4 --- /dev/null +++ b/controllers/validators/schema/user-tags.js @@ -0,0 +1,46 @@ +'use strict'; + +const { tagname, story, relevance } = require('./paths'); + +const postUserTags = { + id: 'postUserTags', + properties: { + body: { + properties: { + tag: { + properties: { + tagname + } + }, + story, + relevance + }, + additionalProperties: false, + required: ['tag', 'story', 'relevance'] + } + }, + required: ['body'] +}; + +const patchUserTag = { + id: 'patchUserTag', + properties: { + body: { + properties: { + story, + relevance, + id: {} + }, + additionalProperties: false, + required: ['id'] + }, + params: { + properties: { + tagname + } + } + }, + required: ['body', 'params'] +}; + +module.exports = { postUserTags, patchUserTag }; diff --git a/controllers/validators/schema/users.js b/controllers/validators/schema/users.js new file mode 100644 index 0000000..5ba2767 --- /dev/null +++ b/controllers/validators/schema/users.js @@ -0,0 +1,195 @@ +'use strict'; + +const { username, givenName, familyName, description, location, tagname } = require('./paths'); + +const getUser = { + id: 'getUser', + properties: { + params: { + properties: { + username + } + } + } +}; + +const patchUser = { + id: 'patchUser', + properties: { + params: { + properties: { + username + } + }, + body: { + properties: { + id: username, + givenName, + familyName, + description, + location + }, + required: ['id'], + additionalProperties: false + } + } +}; + +const getUsersWithMyTags = { + id: 'getUsersWithMyTags', + properties: { + query: { + properties: { + filter: { + properties: { + byMyTags: { + enum: [''] + } + }, + required: ['byMyTags'] + } + }, + required: ['filter'] + }, + }, + required: ['query'] +}; + +const getUsersWithLocation = { + id: 'getUsersWithLocation', + properties: { + query: { + properties: { + filter: { + properties: { + location: { + type: 'array', + items: location + } + }, + require: ['location'] + } + }, + require: ['filter'] + } + }, + require: ['query'] +}; + +const postUsers = { + id: 'postUsers', + properties: { + body: { + properties: { + email: { + $ref : 'sch#/definitions/user/email' + }, + username, + password: { $ref: 'sch#/definitions/user/password'} + }, + required: ['username', 'email', 'password'] + }, + required: ['body'] + } +}; + +const newUsers = { + id: 'newUsers', + properties: { + query: { + properties: { + sort: { + type: 'string' + }, + page: { + type: 'object', + properties: { + limit: { + type: 'integer' + }, + offset: { + type: 'integer' + } + }, + required: ['limit', 'offset'], + additionalProperties: false + } + }, + required: ['sort', 'page'], + additionalProperties: false + } + } +}; + +const newUsersWithMyTags = { + id: 'newUsersWithMyTags', + properties: { + query:{ + properties:{ + sort: { + type: 'string', + const: '-created' + }, + filter: { + properties: { + withMyTags: { + type: 'number', + } + }, + required: ['withMyTags'], + additionalProperties: false + }, + page: { + properties: { + offset: { + type: 'integer' + }, + limit: { + type: 'integer' + } + }, + required: ['offset', 'limit'], + additionalProperties: false + } + }, + required: ['sort', 'filter', 'page'], + additionalProperties: false + } + }, + required: ['query'] +}; + +const getUsersWithTags = { + id: 'getUsersWithTags', + properties: { + query: { + properties: { + filter: { + properties: { + tag: { + type: 'array', + items: tagname + } + }, + required: ['tag'], + additionalProperties: false + } + }, + required: ['filter'], + additionalProperties: false + } + }, + required: ['query'] +}; + + +module.exports = { + getUser, + patchUser, + postUsers, + getUsersWithMyTags, + getUsersWithTags, + getUsersWithLocation, + newUsers, + newUsersWithMyTags +};