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/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/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/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/errorHandler.js b/controllers/validators/errorHandler.js index df03e75..89ab7de 100644 --- a/controllers/validators/errorHandler.js +++ b/controllers/validators/errorHandler.js @@ -8,8 +8,24 @@ module.exports = function (err, req, res, next) { if (_.isArray(err)) { const errors = _.map(err, (e) => { + + // 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') { // missing attributes are missing fields from required: ['required', 'fields'] + e.param = 'attributes'; + e.msg = 'missing attribute'; + } 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; + } + } + 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 30826e8..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.userTags = require('./userTags'); exports.tags = require('./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(); -}; - - +exports.userTags = require('./user-tags'); 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/parser.js b/controllers/validators/parser.js index 140f879..02d5adb 100644 --- a/controllers/validators/parser.js +++ b/controllers/validators/parser.js @@ -21,7 +21,9 @@ const parametersDictionary = { }, filter: { tag: 'array', - withMyTags: 'int' + withMyTags: 'int', + location: 'coordinates', + relatedToTags: 'array' }, }; @@ -48,6 +50,19 @@ const parseQuery = function (query, parametersDictionary) { query[q] = array; break; } + case 'coordinates': { + // parse the location + const queryString = query[q]; + const array = queryString.split(','); + + // 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]]); + } + + break; + } } } } @@ -55,4 +70,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/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 deleted file mode 100644 index d759aab..0000000 --- a/controllers/validators/schema.js +++ /dev/null @@ -1,131 +0,0 @@ -module.exports = { - definitions: { - 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: { - maxLength: 128 - }, - familyName: { - maxLength: 128 - }, - desc: { - maxLength: 2048 - }, - password: { - maxLength: 512, - minLength: 8 - }, - code: { - type: 'string', - minLength: 1, - pattern: '^[0-9a-f]{32}$' - } - }, - tag: { - tagname: { - type: 'string', - minLength: 1, - pattern: '^[a-z0-9]+(-[a-z0-9]+)*$' - } - } - }, - postUsers: { - id: 'postUsers', - body: { - properties: { - email: { - $ref : 'sch#/definitions/user/email' - }, - username: { $ref : 'sch#/definitions/user/username'}, - password: { '$ref': 'sch#/definitions/user/password'} - } - } - }, - newUsers: { - id: 'newUsers', - query: { - properties: { - sort: { - type: 'string' - }, - 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', - } - }, - required: ['withMyTags'], - additionalProperties: false - }, - page: { - properties: { - offset: { - type: 'number' - }, - limit: { - type: 'number' - } - }, - required: ['offset', 'limit'], - additionalProperties: false - } - }, - required: ['sort', 'filter', 'page'], - additionalProperties: false - } - }, - getUsersWithTags: { - id: 'getUsersWithTags', - query: { - properties: { - filter: { - properties: { - tag: { - type: 'array', - items: { $ref : 'sch#/definitions/tag/tagname' } - } - }, - required: ['tag'], - additionalProperties : false - } - }, - required: ['filter'], - additionalProperties: false - } - } -}; 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 +}; 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/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/userTags.js b/controllers/validators/userTags.js deleted file mode 100644 index bd2e5a3..0000000 --- a/controllers/validators/userTags.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -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' - }); - } - - // 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.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/controllers/validators/users.js b/controllers/validators/users.js index ea1a40b..1c5e9ec 100644 --- a/controllers/validators/users.js +++ b/controllers/validators/users.js @@ -1,183 +1,25 @@ 'use strict'; -const _ = require('lodash'); - -const parser = require('./parser'), - rules = require('./rules'), - schema = require('./schema'), - { ajv } = require('./ajvInit'); - -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); - - 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(); -}; - -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 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(); +const validate = require('./validate-by-schema'); + +const getUsersWithTags = validate('getUsersWithTags'); +const getNewUsersWithMyTags = validate('newUsersWithMyTags'); +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']]); +const getUsersWithMyTags = validate('getUsersWithMyTags'); +const getNewUsers = validate('newUsers'); + +module.exports = { + get, + patch, + post, + getUsersWithMyTags, + getUsersWithTags, + getNewUsers, + getNewUsersWithMyTags, + getUsersWithLocation }; - -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(); -}; - -/* 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/controllers/validators/validate-by-schema.js b/controllers/validators/validate-by-schema.js new file mode 100644 index 0000000..f3939fd --- /dev/null +++ b/controllers/validators/validate-by-schema.js @@ -0,0 +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, consistency) { + return function (req, res, next) { + 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/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/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/routes/users.js b/routes/users.js index 6a12484..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.postUsers, userController.postUsers); + .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,20 +24,20 @@ 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') - .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/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/location.js b/test/location.js index 56c6efc..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; @@ -244,7 +237,7 @@ describe('Location of people, tags, ideas, projects, ...', function () { type: 'users', id: loggedUser.username, attributes: { - location: '' + location: null } } }) @@ -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); + }); }); }); 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 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') diff --git a/test/user-tags.js b/test/user-tags.js index 181dcb7..61651ce 100644 --- a/test/user-tags.js +++ b/test/user-tags.js @@ -693,7 +693,7 @@ 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 () { @@ -718,8 +718,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 () { @@ -744,8 +744,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'); }); @@ -772,8 +772,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'); }); @@ -827,8 +827,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'); }); }); 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 () {