From 92cd9a6b2382ae3daf4338ac48c699faab1e8c2a Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 26 Jun 2018 21:34:24 +0200 Subject: [PATCH 01/12] add channels and multiple users to organisations --- server/helper/seed-helpers.js | 36 +++++++++++++------ server/hooks/restrictToOwnerOrModerator.js | 12 ++++--- server/models/organizations.model.js | 19 ++++++++-- server/seeder/development/organizations.js | 14 +++++--- .../hooks/can-edit-organization.js | 4 ++- .../organizations/organizations.hooks.js | 11 ++++-- 6 files changed, 71 insertions(+), 25 deletions(-) diff --git a/server/helper/seed-helpers.js b/server/helper/seed-helpers.js index 0e71a8d..e69c578 100644 --- a/server/helper/seed-helpers.js +++ b/server/helper/seed-helpers.js @@ -35,17 +35,19 @@ const ngoLogos = [ const difficulties = ['easy', 'medium', 'hard']; +const randomItem = (items, filter) => { + let ids = filter + ? Object.keys(items) + .filter(id => { + return filter(items[id]); + }) + : _.keys(items); + let randomIds = _.shuffle(ids); + return items[randomIds.pop()]; +}; + module.exports = { - randomItem: (items, filter) => { - let ids = filter - ? Object.keys(items) - .filter(id => { - return filter(items[id]); - }) - : _.keys(items); - let randomIds = _.shuffle(ids); - return items[randomIds.pop()]; - }, + randomItem, randomItems: (items, key = '_id', min = 1, max = 1) => { let randomIds = _.shuffle(_.keys(items)); let res = []; @@ -105,8 +107,22 @@ module.exports = { lng: 6.558838 + (Math.random() * 10) }); } + if (addresses.length) { + addresses[0].primary = true; + } return addresses; }, + randomChannels: () => { + const count = Math.round(Math.random() * 3); + let channels = []; + for (let i = 0; i < count; i++) { + channels.push({ + name: faker.internet.userName(), + type: randomItem(['telegram', 'yahoo', 'skype', 'meetup', 'twitter', 'medium']) + }); + } + return channels; + }, /** * Get array of ids from the given seederstore items after mapping them by the key in the values * diff --git a/server/hooks/restrictToOwnerOrModerator.js b/server/hooks/restrictToOwnerOrModerator.js index a031547..553b2cb 100644 --- a/server/hooks/restrictToOwnerOrModerator.js +++ b/server/hooks/restrictToOwnerOrModerator.js @@ -18,21 +18,23 @@ module.exports = function restrictToOwnerOrModerator (query = {}) { // eslint-di const role = getByDot(hook, 'params.user.role'); const isModOrAdmin = role && ['admin', 'moderator'].includes(role); - const userId = getByDot(hook, 'params.user._id'); - const ownerId = getByDot(hook, 'params.before.userId'); - const isOwner = userId && ownerId && ownerId.toString() === userId.toString(); - // allow for mods or admins if (isModOrAdmin) { return hook; } + const userId = getByDot(hook, 'params.user._id'); + const ownerIds = getByDot(hook, 'params.before.userIds'); + const isOwner = userId && ownerIds && ownerIds.some(ownerId => { + ownerId.toString() === userId.toString(); + }) + // change the query if the method is find or get if (isFindOrGet) { // restrict to owner or given query const restrictedQuery = { $or: [ - { userId }, + { userIds: userId }, { ...query } ] }; diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 0ccea85..60a5427 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -16,7 +16,16 @@ module.exports = function (app) { city: { type: String, required: true }, country: { type: String, required: true }, lat: { type: Number, required: true }, - lng: { type: Number, required: true } + lng: { type: Number, required: true }, + primary: { type: Boolean, default: false } + }); + const channel = mongooseClient.Schema({ + name: { type: String, required: true }, + type: { + type: String, + enum: ['telegram', 'yahoo', 'skype', 'meetup', 'twitter', 'medium'], + required: true + } }); const organizations = new Schema({ name: { type: String, required: true, index: true }, @@ -26,18 +35,22 @@ module.exports = function (app) { categoryIds: { type: Array, required: true, index: true }, logo: { type: String }, coverImg: { type: String }, - userId: { type: String, required: true, index: true }, + creatorId: { type: String, required: true }, + ownerIds: { type: [String], default: [] }, + userIds: { type: [String], default: [] }, description: { type: String, required: true }, descriptionExcerpt: { type: String }, // will be generated automatically + phone: { type: String }, publicEmail: { type: String }, url: { type: String }, type: { type: String, index: true, - enum: ['ngo', 'npo', 'goodpurpose', 'ev', 'eva'] + enum: ['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other'] }, language: { type: String, required: true, default: 'de', index: true }, addresses: { type: [addressSchema], default: [] }, + channels: { type: [channel], default: [] }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, isEnabled: { diff --git a/server/seeder/development/organizations.js b/server/seeder/development/organizations.js index 75ef012..fb9cfd4 100644 --- a/server/seeder/development/organizations.js +++ b/server/seeder/development/organizations.js @@ -13,11 +13,14 @@ module.exports = (seederstore) => { logo: () => seedHelpers.randomLogo(), coverImg: () => seedHelpers.randomUnsplashUrl(), categoryIds: () => seedHelpers.randomCategories(seederstore), - userId: () => seedHelpers.randomItem(seederstore.users, roleAdmin)._id, + creatorId: () => seedHelpers.randomItem(seederstore.users, roleAdmin)._id, + userIds: () => [seedHelpers.randomItem(seederstore.users, roleAdmin)._id], url: '{{internet.url}}', + phone: '{{phone.phoneNumber}}', publicEmail: '{{internet.email}}', addresses: () => seedHelpers.randomAddresses(), - type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva']), + channels: () => seedHelpers.randomChannels(), + type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other']), description: '{{lorem.text}}', deletedAt: null, isEnabled: true, @@ -36,11 +39,14 @@ module.exports = (seederstore) => { logo: () => seedHelpers.randomLogo(), coverImg: () => seedHelpers.randomItem([seedHelpers.randomUnsplashUrl(), null]), categoryIds: () => seedHelpers.randomCategories(seederstore), - userId: () => seedHelpers.randomItem(seederstore.users)._id, + creatorId: () => seedHelpers.randomItem(seederstore.users)._id, + userIds: () => [seedHelpers.randomItem(seederstore.users)._id], url: '{{internet.url}}', + phone: '{{phone.phoneNumber}}', publicEmail: '{{internet.email}}', addresses: () => seedHelpers.randomAddresses(), - type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva']), + channels: () => seedHelpers.randomChannels(), + type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other']), description: '{{lorem.text}}', deletedAt: null, isEnabled: true, diff --git a/server/services/organizations/hooks/can-edit-organization.js b/server/services/organizations/hooks/can-edit-organization.js index ca6b9af..c10242d 100644 --- a/server/services/organizations/hooks/can-edit-organization.js +++ b/server/services/organizations/hooks/can-edit-organization.js @@ -19,7 +19,9 @@ module.exports = (options = {field: 'organizationId'}) => async hook => { const organization = await hook.app.service('organizations').get(organizationId); // only allow when the user is assigned with the organization - if (!organization || (organization && organization.userId.toString() !== currentUserId.toString())) { + if (!organization || !organization.userIds.some(userId => { + userId.toString() === currentUserId.toString(); + })) { throw new errors.Forbidden('you can\'t create or edit for that organization'); } diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 7b0e9af..8f333b8 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -76,11 +76,18 @@ module.exports = { ), when(isModerator(), hook => { - hook.data.reviewedBy = hook.params.user.userId; + hook.data.reviewedBy = hook.params.user._id; + return hook; + } + ), + // Add current user as creator and user + associateCurrentUser({ as: 'creatorId' }), + unless(isProvider('server'), + hook => { + hook.data.userIds = [hook.params.user._id]; return hook; } ), - associateCurrentUser(), createSlug({ field: 'name' }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) From 204b307ad6fecd232e25d3a8ef6069dff9a4e50e Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 26 Jun 2018 23:28:57 +0200 Subject: [PATCH 02/12] refactor organisation users --- package.json | 1 + server/helper/seed-helpers.js | 4 +++- server/hooks/restrictToOwnerOrModerator.js | 9 ++++--- server/models/organizations.model.js | 24 ++++++++++++++----- server/seeder/development/organizations.js | 14 ++++++++--- .../hooks/can-edit-organization.js | 6 ++--- yarn.lock | 4 ++++ 7 files changed, 44 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 494559c..c9d00b1 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "handlebars-layouts": "~3.1.4", "helmet": "~3.12.0", "html-excerpt": "~0.1.0", + "human-connection-modules": "git+https://github.com/Human-Connection/Modules.git", "mime": "^2.3.1", "mongoose": "~4.13.2", "multer": "~1.3.0", diff --git a/server/helper/seed-helpers.js b/server/helper/seed-helpers.js index e69c578..28ddc31 100644 --- a/server/helper/seed-helpers.js +++ b/server/helper/seed-helpers.js @@ -1,5 +1,7 @@ const _ = require('lodash'); const faker = require('faker'); +const hcModules = require('human-connection-modules'); +const channelNames = hcModules.collections.socialChannels.names; const unsplashTopics = [ 'love', 'family', @@ -118,7 +120,7 @@ module.exports = { for (let i = 0; i < count; i++) { channels.push({ name: faker.internet.userName(), - type: randomItem(['telegram', 'yahoo', 'skype', 'meetup', 'twitter', 'medium']) + type: randomItem(channelNames) }); } return channels; diff --git a/server/hooks/restrictToOwnerOrModerator.js b/server/hooks/restrictToOwnerOrModerator.js index 553b2cb..e59eb1f 100644 --- a/server/hooks/restrictToOwnerOrModerator.js +++ b/server/hooks/restrictToOwnerOrModerator.js @@ -24,17 +24,16 @@ module.exports = function restrictToOwnerOrModerator (query = {}) { // eslint-di } const userId = getByDot(hook, 'params.user._id'); - const ownerIds = getByDot(hook, 'params.before.userIds'); - const isOwner = userId && ownerIds && ownerIds.some(ownerId => { - ownerId.toString() === userId.toString(); - }) + const users = getByDot(hook, 'params.before.users'); + const isOwner = userId && users && + users.some(({id}) => id === userId.toString()) // change the query if the method is find or get if (isFindOrGet) { // restrict to owner or given query const restrictedQuery = { $or: [ - { userIds: userId }, + { 'users.id': userId }, { ...query } ] }; diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 60a5427..186d4f0 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -2,6 +2,11 @@ // // See http://mongoosejs.com/docs/models.html // for more of what you can do here. + +const hcModules = require('human-connection-modules'); +const channelNames = hcModules.collections.socialChannels.names; +const organizationTypes = hcModules.collections.organizationTypes.names; + module.exports = function (app) { const mongooseClient = app.get('mongooseClient'); const { Schema } = mongooseClient; @@ -19,14 +24,22 @@ module.exports = function (app) { lng: { type: Number, required: true }, primary: { type: Boolean, default: false } }); - const channel = mongooseClient.Schema({ + const channelSchema = mongooseClient.Schema({ name: { type: String, required: true }, type: { type: String, - enum: ['telegram', 'yahoo', 'skype', 'meetup', 'twitter', 'medium'], + enum: channelNames, required: true } }); + const userSchema = mongooseClient.Schema({ + id: { type: String, required: true }, + role: { + type: String, + enum: ['admin', 'editor'], + default: 'editor' + } + }); const organizations = new Schema({ name: { type: String, required: true, index: true }, slug: { type: String, required: true, unique: true, index: true }, @@ -36,8 +49,7 @@ module.exports = function (app) { logo: { type: String }, coverImg: { type: String }, creatorId: { type: String, required: true }, - ownerIds: { type: [String], default: [] }, - userIds: { type: [String], default: [] }, + users: { type: [userSchema], default: [] }, description: { type: String, required: true }, descriptionExcerpt: { type: String }, // will be generated automatically phone: { type: String }, @@ -46,11 +58,11 @@ module.exports = function (app) { type: { type: String, index: true, - enum: ['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other'] + enum: organizationTypes }, language: { type: String, required: true, default: 'de', index: true }, addresses: { type: [addressSchema], default: [] }, - channels: { type: [channel], default: [] }, + channels: { type: [channelSchema], default: [] }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, isEnabled: { diff --git a/server/seeder/development/organizations.js b/server/seeder/development/organizations.js index fb9cfd4..f7efbca 100644 --- a/server/seeder/development/organizations.js +++ b/server/seeder/development/organizations.js @@ -1,4 +1,6 @@ const seedHelpers = require('../../helper/seed-helpers'); +const hcModules = require('human-connection-modules'); +const organizationTypes = hcModules.collections.organizationTypes.names; module.exports = (seederstore) => { let roleAdmin = ({role}) => role === 'admin'; @@ -14,7 +16,10 @@ module.exports = (seederstore) => { coverImg: () => seedHelpers.randomUnsplashUrl(), categoryIds: () => seedHelpers.randomCategories(seederstore), creatorId: () => seedHelpers.randomItem(seederstore.users, roleAdmin)._id, - userIds: () => [seedHelpers.randomItem(seederstore.users, roleAdmin)._id], + users: () => [{ + id: seedHelpers.randomItem(seederstore.users, roleAdmin)._id, + role: 'admin' + }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', publicEmail: '{{internet.email}}', @@ -40,13 +45,16 @@ module.exports = (seederstore) => { coverImg: () => seedHelpers.randomItem([seedHelpers.randomUnsplashUrl(), null]), categoryIds: () => seedHelpers.randomCategories(seederstore), creatorId: () => seedHelpers.randomItem(seederstore.users)._id, - userIds: () => [seedHelpers.randomItem(seederstore.users)._id], + users: () => [{ + id: seedHelpers.randomItem(seederstore.users)._id, + role: 'admin' + }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', publicEmail: '{{internet.email}}', addresses: () => seedHelpers.randomAddresses(), channels: () => seedHelpers.randomChannels(), - type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other']), + type: () => seedHelpers.randomItem(organizationTypes), description: '{{lorem.text}}', deletedAt: null, isEnabled: true, diff --git a/server/services/organizations/hooks/can-edit-organization.js b/server/services/organizations/hooks/can-edit-organization.js index c10242d..f36bac8 100644 --- a/server/services/organizations/hooks/can-edit-organization.js +++ b/server/services/organizations/hooks/can-edit-organization.js @@ -19,9 +19,9 @@ module.exports = (options = {field: 'organizationId'}) => async hook => { const organization = await hook.app.service('organizations').get(organizationId); // only allow when the user is assigned with the organization - if (!organization || !organization.userIds.some(userId => { - userId.toString() === currentUserId.toString(); - })) { + if (!organization || !organization.users.some( + ({id}) => id === currentUserId.toString() + )) { throw new errors.Forbidden('you can\'t create or edit for that organization'); } diff --git a/yarn.lock b/yarn.lock index d312bd9..559c88f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,6 +2518,10 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +"human-connection-modules@git+https://github.com/Human-Connection/Modules.git": + version "1.0.0" + resolved "git+https://github.com/Human-Connection/Modules.git#df661d2da572f1b5a330aef25fc64b8837177cbb" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" From 0a29c0c73c34066ab6e6fd9f74199c77446046b1 Mon Sep 17 00:00:00 2001 From: JB Date: Tue, 26 Jun 2018 23:53:14 +0200 Subject: [PATCH 03/12] renamed organisations email --- server/models/organizations.model.js | 2 +- server/seeder/development/organizations.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 186d4f0..0d634af 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -53,7 +53,7 @@ module.exports = function (app) { description: { type: String, required: true }, descriptionExcerpt: { type: String }, // will be generated automatically phone: { type: String }, - publicEmail: { type: String }, + email: { type: String }, url: { type: String }, type: { type: String, diff --git a/server/seeder/development/organizations.js b/server/seeder/development/organizations.js index f7efbca..9d53a12 100644 --- a/server/seeder/development/organizations.js +++ b/server/seeder/development/organizations.js @@ -22,7 +22,7 @@ module.exports = (seederstore) => { }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', - publicEmail: '{{internet.email}}', + email: '{{internet.email}}', addresses: () => seedHelpers.randomAddresses(), channels: () => seedHelpers.randomChannels(), type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva', 'other']), @@ -51,7 +51,7 @@ module.exports = (seederstore) => { }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', - publicEmail: '{{internet.email}}', + email: '{{internet.email}}', addresses: () => seedHelpers.randomAddresses(), channels: () => seedHelpers.randomChannels(), type: () => seedHelpers.randomItem(organizationTypes), From c75b94594e526f8e8ea88f9bbc4b9a571781ddde Mon Sep 17 00:00:00 2001 From: JB Date: Sun, 1 Jul 2018 00:41:17 +0200 Subject: [PATCH 04/12] add phone and email to organization address --- package.json | 1 + server/helper/seed-helpers.js | 2 ++ server/models/organizations.model.js | 2 ++ 3 files changed, 5 insertions(+) diff --git a/package.json b/package.json index c9d00b1..065d15e 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "dev:local": "sh scripts/run-local.sh", "dev:noseed": "concurrently 'mongod --dbpath data' 'wait-on tcp:27017 && NODE_ENV=development DEBUG=feathers nodemon server/'", "dev:win": "npm run clear && concurrently \"mongod --dbpath /data/db\" \"wait-on tcp:27017&&cross-env NODE_ENV=development&&cross-env DEBUG=feathers&& nodemon --inspect server/\"", + "refresh": "rm -rf node_modules && yarn install && yarn dev", "mocha": "npm run clear && $npm_package_config_concurrently '$npm_package_config_mongoDev &>/dev/null' 'wait-on tcp:27017 && NODE_ENV=test $npm_package_config_mocha'", "coverage": "npm run clear && $npm_package_config_concurrently '$npm_package_config_mongoDev &>/dev/null' 'wait-on tcp:27017 && NODE_ENV=test istanbul cover $npm_package_config_mochaCoverage'" }, diff --git a/server/helper/seed-helpers.js b/server/helper/seed-helpers.js index 28ddc31..cdcbf0e 100644 --- a/server/helper/seed-helpers.js +++ b/server/helper/seed-helpers.js @@ -105,6 +105,8 @@ module.exports = { zipCode: faker.address.zipCode(), street: faker.address.streetAddress(), country: faker.address.countryCode(), + email: faker.internet.email(), + phone: faker.phone.phoneNumber(), lat: 54.032726 - (Math.random() * 10), lng: 6.558838 + (Math.random() * 10) }); diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 0d634af..612efc1 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -20,6 +20,8 @@ module.exports = function (app) { zipCode: { type: String, required: true }, city: { type: String, required: true }, country: { type: String, required: true }, + phone: { type: String }, + email: { type: String }, lat: { type: Number, required: true }, lng: { type: Number, required: true }, primary: { type: Boolean, default: false } From 3ff5bf7cb876b06b26af9db9592a0d429b272715 Mon Sep 17 00:00:00 2001 From: JB Date: Sun, 1 Jul 2018 11:16:19 +0200 Subject: [PATCH 05/12] add primary address to organizations --- server/helper/seed-helpers.js | 3 - server/hooks/restrictToOwnerOrModerator.js | 2 +- server/models/organizations.model.js | 5 +- server/seeder/development/organizations.js | 8 - server/services/auth-management/notifier.js | 4 +- .../hooks/flag-primary-address.js | 14 ++ .../organizations/organizations.hooks.js | 19 +- test/assets/organizations.js | 40 ++++ test/services/organizations.test.js | 178 +++++++++++++++++- 9 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 server/services/organizations/hooks/flag-primary-address.js create mode 100644 test/assets/organizations.js diff --git a/server/helper/seed-helpers.js b/server/helper/seed-helpers.js index cdcbf0e..d91f665 100644 --- a/server/helper/seed-helpers.js +++ b/server/helper/seed-helpers.js @@ -111,9 +111,6 @@ module.exports = { lng: 6.558838 + (Math.random() * 10) }); } - if (addresses.length) { - addresses[0].primary = true; - } return addresses; }, randomChannels: () => { diff --git a/server/hooks/restrictToOwnerOrModerator.js b/server/hooks/restrictToOwnerOrModerator.js index e59eb1f..686a41d 100644 --- a/server/hooks/restrictToOwnerOrModerator.js +++ b/server/hooks/restrictToOwnerOrModerator.js @@ -26,7 +26,7 @@ module.exports = function restrictToOwnerOrModerator (query = {}) { // eslint-di const userId = getByDot(hook, 'params.user._id'); const users = getByDot(hook, 'params.before.users'); const isOwner = userId && users && - users.some(({id}) => id === userId.toString()) + users.some(({id}) => id === userId.toString()); // change the query if the method is find or get if (isFindOrGet) { diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 612efc1..99caae5 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -23,8 +23,7 @@ module.exports = function (app) { phone: { type: String }, email: { type: String }, lat: { type: Number, required: true }, - lng: { type: Number, required: true }, - primary: { type: Boolean, default: false } + lng: { type: Number, required: true } }); const channelSchema = mongooseClient.Schema({ name: { type: String, required: true }, @@ -59,11 +58,13 @@ module.exports = function (app) { url: { type: String }, type: { type: String, + required: true, index: true, enum: organizationTypes }, language: { type: String, required: true, default: 'de', index: true }, addresses: { type: [addressSchema], default: [] }, + primaryAddressIndex: { type: Number, default: 0 }, channels: { type: [channelSchema], default: [] }, createdAt: { type: Date, default: Date.now }, updatedAt: { type: Date, default: Date.now }, diff --git a/server/seeder/development/organizations.js b/server/seeder/development/organizations.js index 9d53a12..fc54ec9 100644 --- a/server/seeder/development/organizations.js +++ b/server/seeder/development/organizations.js @@ -16,10 +16,6 @@ module.exports = (seederstore) => { coverImg: () => seedHelpers.randomUnsplashUrl(), categoryIds: () => seedHelpers.randomCategories(seederstore), creatorId: () => seedHelpers.randomItem(seederstore.users, roleAdmin)._id, - users: () => [{ - id: seedHelpers.randomItem(seederstore.users, roleAdmin)._id, - role: 'admin' - }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', email: '{{internet.email}}', @@ -45,10 +41,6 @@ module.exports = (seederstore) => { coverImg: () => seedHelpers.randomItem([seedHelpers.randomUnsplashUrl(), null]), categoryIds: () => seedHelpers.randomCategories(seederstore), creatorId: () => seedHelpers.randomItem(seederstore.users)._id, - users: () => [{ - id: seedHelpers.randomItem(seederstore.users)._id, - role: 'admin' - }], url: '{{internet.url}}', phone: '{{phone.phoneNumber}}', email: '{{internet.email}}', diff --git a/server/services/auth-management/notifier.js b/server/services/auth-management/notifier.js index cd24d2a..e6d02e5 100644 --- a/server/services/auth-management/notifier.js +++ b/server/services/auth-management/notifier.js @@ -110,8 +110,8 @@ module.exports = function (app) { } function sendEmail (email) { - // Save copy to /tmp/emails while in debug mode - if (app.get('debug')) { + // Save copy to /tmp/emails while in debug or test mode + if (app.get('debug') || process.NODE_ENV === 'test') { const filename = String(Date.now()) + '.html'; const filepath = path.join(__dirname, '../../../tmp/emails/', filename); fs.outputFileSync(filepath, email.html); diff --git a/server/services/organizations/hooks/flag-primary-address.js b/server/services/organizations/hooks/flag-primary-address.js new file mode 100644 index 0000000..6454cae --- /dev/null +++ b/server/services/organizations/hooks/flag-primary-address.js @@ -0,0 +1,14 @@ +// Add flag on primary address +const alterItems = require('../../../helper/alter-items'); + +module.exports = () => alterItems(handleItem); + +const handleItem = item => { + if (item.addresses && item.addresses[item.primaryAddressIndex]) { + item.addresses.map((address, index) => { + address.primary = index === item.primaryAddressIndex; + return address; + }); + } + return item; +}; \ No newline at end of file diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 8f333b8..6296e1b 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -10,6 +10,7 @@ const isModerator = require('../../hooks/is-moderator-boolean'); const thumbnails = require('../../hooks/thumbnails'); const restrictToOwnerOrModerator = require('../../hooks/restrictToOwnerOrModerator'); const restrictReviewAndEnableChange = require('../../hooks/restrictReviewAndEnableChange'); +const flagPrimaryAddress = require('./hooks/flag-primary-address'); const search = require('feathers-mongodb-fuzzy-search'); const isSingleItem = require('../../hooks/is-single-item'); const xss = require('../../hooks/xss'); @@ -82,12 +83,15 @@ module.exports = { ), // Add current user as creator and user associateCurrentUser({ as: 'creatorId' }), - unless(isProvider('server'), - hook => { - hook.data.userIds = [hook.params.user._id]; - return hook; - } - ), + hook => { + hook.data.users = [ + { + id: hook.params.user._id, + role: 'admin' + } + ]; + return hook; + }, createSlug({ field: 'name' }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) @@ -127,7 +131,8 @@ module.exports = { after: { all: [ xss({ fields: xssFields }), - populate({ schema: reviewerSchema }) + populate({ schema: reviewerSchema }), + flagPrimaryAddress() // populate({ schema: userSchema }), // populate({ schema: followerSchema }) ], diff --git a/test/assets/organizations.js b/test/assets/organizations.js new file mode 100644 index 0000000..44c22d6 --- /dev/null +++ b/test/assets/organizations.js @@ -0,0 +1,40 @@ +const organizationData = { + name: 'a', + description: 'My content', + type: 'other', + language: 'en' +}; + +const organizationData2 = { + name: 'b', + description: 'My content', + type: 'other', + language: 'en' +}; + +const addressData = { + street: 'street', + zipCode: '123', + city: 'city', + country: 'country', + lat: 123, + lng: 321 +}; + +const addressData2 = { + street: 'street2', + zipCode: '456', + city: 'city2', + country: 'country2', + lat: 1234, + lng: 4321 +}; + + +module.exports = { + organizationData, + organizationData2, + addressData, + addressData2 +}; + diff --git a/test/services/organizations.test.js b/test/services/organizations.test.js index 3c2c864..e693e68 100644 --- a/test/services/organizations.test.js +++ b/test/services/organizations.test.js @@ -1,10 +1,184 @@ const assert = require('assert'); const app = require('../../server/app'); +const service = app.service('organizations'); +const userService = app.service('users'); +const categoryService = app.service('categories'); +const { + //userData, + adminData +} = require('../assets/users'); +const { + organizationData, + organizationData2, + addressData, + addressData2 +} = require('../assets/organizations'); +const { categoryData } = require('../assets/categories'); describe('\'organizations\' service', () => { + let user; + let category; + let params; + + before(function(done) { + this.server = app.listen(3031); + this.server.once('listening', () => done()); + }); + + after(function(done) { + this.server.close(done); + }); + + beforeEach(async () => { + await app.get('mongooseClient').connection.dropDatabase(); + user = await userService.create(adminData); + params = { + user + }; + category = await categoryService.create(categoryData); + organizationData.categoryIds = [category._id]; + organizationData2.categoryIds = [category._id]; + }); + + afterEach(async () => { + await app.get('mongooseClient').connection.dropDatabase(); + user = null; + params = null; + delete organizationData.categoryIds; + delete organizationData2.categoryIds; + }); + it('registered the service', () => { - const service = app.service('organizations'); + assert.ok(service, 'registered the service'); + }); + + describe('organizations create', () => { + it('runs create', async () => { + const organization = await service.create(organizationData, params); + assert.ok(organization, 'created organization'); + }); + + it('has required fields after create', async () => { + const organization = await service.create(organizationData, params); + assert.ok(organization._id, 'has _id'); + assert.ok(organization.slug, 'has slug'); + assert.ok(organization.categoryIds, 'has categoryIds'); + assert.ok(organization.creatorId, 'has creatorId'); + assert.ok(organization.language, 'has language'); + assert.ok(organization.type, 'has type'); + assert.ok(organization.users, 'has users'); + assert.ok(organization.description, 'has description'); + }); + + it('has correct _id after create with _id: null in data', async () => { + organizationData._id = null; + const organization = await service.create(organizationData, params); + assert.ok(organization._id !== null, 'has _id'); + delete organizationData._id; + }); + + it('has creator as admin user', async () => { + const organization = await service.create(organizationData, params); + const firstUser = organization.users[0]; + assert.ok(firstUser, 'has one user entry'); + assert.equal( + firstUser.id, + user._id.toString(), + 'has correct user id' + ); + assert.equal( + firstUser.role, + 'admin', + 'user is admin' + ); + }); + }); + + describe('organizations find', () => { + beforeEach(async () => { + await service.create(organizationData, params); + }); + + it('finds organizations', async () => { + const result = await service.find(); + assert.ok(result.data[0], 'returns data'); + }); + }); + + describe('organizations find by slug', () => { + let query; + let organization; + + beforeEach(async () => { + organization = await service.create(organizationData, params); + await service.create(organizationData2, params); + query = { + slug: organization.slug + }; + }); + + afterEach(async () => { + organization = null; + query = null; + }); + + it('returns one organization', async () => { + const result = await service.find({query}); + assert.ok(result.data[0], 'returns data'); + assert.equal(result.data.length, 1), 'returns only one entry'; + }); + }); + + describe('organizations find by user', () => { + let query; + let organization; + + beforeEach(async () => { + organization = await service.create(organizationData, params); + await service.create(organizationData2, params); + query = { + 'users.id': organization.users[0].id + }; + }); + + afterEach(async () => { + organization = null; + query = null; + }); + + it('returns organizations', async () => { + const result = await service.find({query}); + assert.ok(result.data[0], 'returns data'); + assert.equal(result.data.length, 2), 'returns two entries'; + }); + }); + + // ToDo: Check roles + // Only admin can add or delete users and change roles + // describe('organizations user roles', () => {}) + + describe('organizations addresses', () => { + beforeEach(async () => { + organizationData.addresses = [ + addressData, + addressData2 + ]; + }); + + afterEach(async () => { + delete organizationData.addresses; + }); - assert.ok(service, 'Registered the service'); + it('first address is primary address', async () => { + const result = await service.create(organizationData, params); + const address = result.addresses[0]; + assert.ok(address, 'has address'); + assert.strictEqual( + result.primaryAddressIndex, 0, 'has primary address index' + ); + assert.strictEqual( + address.primary, true, 'address has primary flag' + ); + }); }); }); From 26d81eea1645393e4245dd9e8fea469a970faea0 Mon Sep 17 00:00:00 2001 From: JB Date: Sun, 1 Jul 2018 11:22:30 +0200 Subject: [PATCH 06/12] Add creatorId as first user id for organizations --- server/services/organizations/organizations.hooks.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 6296e1b..1611efc 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -86,7 +86,7 @@ module.exports = { hook => { hook.data.users = [ { - id: hook.params.user._id, + id: hook.data.creatorId, role: 'admin' } ]; From 29e8334b75e4b49d642049b3d4cae4db44a785f8 Mon Sep 17 00:00:00 2001 From: JB Date: Sat, 14 Jul 2018 21:55:51 +0200 Subject: [PATCH 07/12] Add user data to orga users --- .../hooks/populate-users-data.js | 33 +++++++++++++++++++ .../organizations/organizations.hooks.js | 2 ++ 2 files changed, 35 insertions(+) create mode 100644 server/services/organizations/hooks/populate-users-data.js diff --git a/server/services/organizations/hooks/populate-users-data.js b/server/services/organizations/hooks/populate-users-data.js new file mode 100644 index 0000000..df5f8fb --- /dev/null +++ b/server/services/organizations/hooks/populate-users-data.js @@ -0,0 +1,33 @@ +// Add flag on primary address +const alterItems = require('../../../helper/alter-items'); + +module.exports = () => alterItems(handleItem); + +// Really impressed, that this works, as alterItems is not async +const handleItem = async (item, hook) => { + if (item.users) { + const userIds = item.users.map(user => user.id); + const result = await hook.app.service('users').find({ + query: { + _id: { + $in: userIds + } + }, + _populate: 'skip' + }); + const usersData = result.data; + if (!usersData) { + return item; + } + item.users = item.users.map(user => { + const userData = usersData.find( + data => data._id.toString() === user.id.toString() + ); + user.name = userData.name; + user.slug = userData.slug; + user.avatar = userData.avatar; + return user; + }); + } + return item; +}; \ No newline at end of file diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 1611efc..42618fd 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -11,6 +11,7 @@ const thumbnails = require('../../hooks/thumbnails'); const restrictToOwnerOrModerator = require('../../hooks/restrictToOwnerOrModerator'); const restrictReviewAndEnableChange = require('../../hooks/restrictReviewAndEnableChange'); const flagPrimaryAddress = require('./hooks/flag-primary-address'); +const populateUsersData = require('./hooks/populate-users-data'); const search = require('feathers-mongodb-fuzzy-search'); const isSingleItem = require('../../hooks/is-single-item'); const xss = require('../../hooks/xss'); @@ -143,6 +144,7 @@ module.exports = { thumbnails(thumbnailOptions) ], get: [ + populateUsersData(), populate({schema: categoriesSchema}), thumbnails(thumbnailOptions) ], From 4f312ff4650c7598c3e5d5489db4543466cc2087 Mon Sep 17 00:00:00 2001 From: JB Date: Mon, 16 Jul 2018 20:41:22 +0200 Subject: [PATCH 08/12] Add user data to orga users --- server/services/organizations/organizations.hooks.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 42618fd..361e7e5 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -149,12 +149,16 @@ module.exports = { thumbnails(thumbnailOptions) ], create: [ + populateUsersData(), thumbnails(thumbnailOptions) ], update: [ + populateUsersData(), thumbnails(thumbnailOptions) ], - patch: [], + patch: [ + populateUsersData() + ], remove: [] }, From 3d81d3f2bfb234c7d3124c5ac378112fa797d8fd Mon Sep 17 00:00:00 2001 From: JB Date: Wed, 18 Jul 2018 20:04:30 +0200 Subject: [PATCH 09/12] Restrict organization user mutation --- .../is-adminowner-or-moderator-boolean.js | 30 ++++++ .../hooks/populate-users-data.js | 2 +- .../organizations/organizations.hooks.js | 9 +- test/assets/users.js | 12 ++- test/services/organizations.test.js | 92 +++++++++++++++++-- 5 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 server/hooks/is-adminowner-or-moderator-boolean.js diff --git a/server/hooks/is-adminowner-or-moderator-boolean.js b/server/hooks/is-adminowner-or-moderator-boolean.js new file mode 100644 index 0000000..63c021f --- /dev/null +++ b/server/hooks/is-adminowner-or-moderator-boolean.js @@ -0,0 +1,30 @@ +const { getByDot } = require('feathers-hooks-common'); + +// Check if user is owner and has admin role on this item, or is moderator +module.exports = () => hook => { + if (hook.type !== 'before') { + throw new Error('The "isAdminOwnerOrModeratorBoolean" hook should only be used as a "before" hook.'); + } + + if (!getByDot(hook, 'params.before')) { + throw new Error('The "isAdminOwnerOrModeratorBoolean" hook should be used after the "stashBefore()" hook'); + } + + // If no user is given -> deny + if(!hook.params || !hook.params.user) { + return false; + } + + // If user is admin or moderator -> allow + if (['admin', 'moderator'].includes(hook.params.user.role)) { + return true; + } + + // If user is owner and has admin role -> allow + const userId = getByDot(hook, 'params.user._id'); + const users = getByDot(hook, 'params.before.users'); + const owner = userId && users && + users.find(({id}) => id === userId.toString()); + + return owner && owner.role === 'admin'; +}; diff --git a/server/services/organizations/hooks/populate-users-data.js b/server/services/organizations/hooks/populate-users-data.js index df5f8fb..2bd173c 100644 --- a/server/services/organizations/hooks/populate-users-data.js +++ b/server/services/organizations/hooks/populate-users-data.js @@ -1,4 +1,4 @@ -// Add flag on primary address +// Populate user data on organization users const alterItems = require('../../../helper/alter-items'); module.exports = () => alterItems(handleItem); diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 361e7e5..dad7226 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -1,4 +1,4 @@ -const { unless, when, isProvider, populate, softDelete, stashBefore } = require('feathers-hooks-common'); +const { unless, when, isProvider, populate, softDelete, stashBefore, discard } = require('feathers-hooks-common'); const { isVerified } = require('feathers-authentication-management').hooks; const { authenticate } = require('feathers-authentication').hooks; const { associateCurrentUser } = require('feathers-authentication-hooks'); @@ -8,6 +8,7 @@ const createExcerpt = require('../../hooks/create-excerpt'); const isModerator = require('../../hooks/is-moderator-boolean'); // const excludeDisabled = require('../../hooks/exclude-disabled'); const thumbnails = require('../../hooks/thumbnails'); +const isAdminOwnerOrModerator = require('../../hooks/is-adminowner-or-moderator-boolean'); const restrictToOwnerOrModerator = require('../../hooks/restrictToOwnerOrModerator'); const restrictReviewAndEnableChange = require('../../hooks/restrictReviewAndEnableChange'); const flagPrimaryAddress = require('./hooks/flag-primary-address'); @@ -105,6 +106,9 @@ module.exports = { stashBefore(), restrictReviewAndEnableChange(), restrictToOwnerOrModerator({ isEnabled: true }), + unless(isAdminOwnerOrModerator(), + discard('users') + ), createSlug({ field: 'name', overwrite: true }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) @@ -117,6 +121,9 @@ module.exports = { stashBefore(), restrictReviewAndEnableChange(), restrictToOwnerOrModerator({ isEnabled: true }), + unless(isAdminOwnerOrModerator(), + discard('users') + ), createSlug({ field: 'name', overwrite: true }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) diff --git a/test/assets/users.js b/test/assets/users.js index 553c1ec..c45667e 100644 --- a/test/assets/users.js +++ b/test/assets/users.js @@ -16,7 +16,17 @@ const userData = { role: 'user' }; +const userData2 = { + email: 'test3@test3.de', + password: '1234', + name: 'Smith', + timezone: 'Europe/Berlin', + badgeIds: [], + role: 'user' +}; + module.exports = { adminData, - userData + userData, + userData2 }; diff --git a/test/services/organizations.test.js b/test/services/organizations.test.js index e693e68..dad939b 100644 --- a/test/services/organizations.test.js +++ b/test/services/organizations.test.js @@ -4,7 +4,8 @@ const service = app.service('organizations'); const userService = app.service('users'); const categoryService = app.service('categories'); const { - //userData, + userData, + userData2, adminData } = require('../assets/users'); const { @@ -15,7 +16,7 @@ const { } = require('../assets/organizations'); const { categoryData } = require('../assets/categories'); -describe('\'organizations\' service', () => { +describe.only('\'organizations\' service', () => { let user; let category; let params; @@ -153,10 +154,6 @@ describe('\'organizations\' service', () => { }); }); - // ToDo: Check roles - // Only admin can add or delete users and change roles - // describe('organizations user roles', () => {}) - describe('organizations addresses', () => { beforeEach(async () => { organizationData.addresses = [ @@ -181,4 +178,87 @@ describe('\'organizations\' service', () => { ); }); }); + + describe.only('organizations admin roles', () => { + let user2; + let organization; + + beforeEach(async () => { + user2 = await userService.create(userData2); + organization = await service.create(organizationData, params); + }); + + afterEach(async () => { + user2 = null; + organization = null; + }); + + it('admin can add user to organization', async () => { + const data = { + users: [ + ...organization.users, + { id: user2._id } + ] + }; + const result = await service.patch(organization._id, data, params); + const newUser = result.users[1]; + assert.ok(newUser, 'has new user'); + assert.strictEqual( + newUser.id.toString(), user2._id.toString(), 'new user has correct id' + ); + }); + }); + + describe.only('organizations editor roles', () => { + let editor; + let user2; + let editorParams; + let organization; + + beforeEach(async () => { + editor = await userService.create(userData); + editorParams = { + user: editor + }; + organization = await service.create(organizationData, params); + const data = { + users: [ + ...organization.users, + { + id: editor._id, + role: 'editor' + } + ] + }; + organization = await service.patch(organization._id, data, params); + user2 = await userService.create(userData2); + }); + + afterEach(async () => { + editor = null; + user2 = null; + editorParams = null; + organization = null; + }); + + it('editor cannot add user to organization', async () => { + const data = { + users: [ + ...organization.users, + { id: user2._id } + ] + }; + const result = await service.patch(organization._id, data, editorParams); + assert.strictEqual(result.users[2], undefined, 'has not added user'); + }); + + it('editor cannot change roles on user', async () => { + const data = { + users: organization.users + }; + data.users[1].role = 'admin'; + const result = await service.patch(organization._id, data, editorParams); + assert.notEqual(result.users[1].role, 'admin', 'has not changed role'); + }); + }); }); From 3bfeb2bb69819d850c12f4ae06e1261bf8c95cd9 Mon Sep 17 00:00:00 2001 From: JB Date: Wed, 18 Jul 2018 20:18:41 +0200 Subject: [PATCH 10/12] Restrict organization remove --- ...olean.js => is-adminowner-or-moderator.js} | 4 ++-- .../organizations/organizations.hooks.js | 8 ++++--- test/services/organizations.test.js | 21 ++++++++++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) rename server/hooks/{is-adminowner-or-moderator-boolean.js => is-adminowner-or-moderator.js} (77%) diff --git a/server/hooks/is-adminowner-or-moderator-boolean.js b/server/hooks/is-adminowner-or-moderator.js similarity index 77% rename from server/hooks/is-adminowner-or-moderator-boolean.js rename to server/hooks/is-adminowner-or-moderator.js index 63c021f..d7843b0 100644 --- a/server/hooks/is-adminowner-or-moderator-boolean.js +++ b/server/hooks/is-adminowner-or-moderator.js @@ -3,11 +3,11 @@ const { getByDot } = require('feathers-hooks-common'); // Check if user is owner and has admin role on this item, or is moderator module.exports = () => hook => { if (hook.type !== 'before') { - throw new Error('The "isAdminOwnerOrModeratorBoolean" hook should only be used as a "before" hook.'); + throw new Error('The "isAdminOwnerOrModerator" hook should only be used as a "before" hook.'); } if (!getByDot(hook, 'params.before')) { - throw new Error('The "isAdminOwnerOrModeratorBoolean" hook should be used after the "stashBefore()" hook'); + throw new Error('The "isAdminOwnerOrModerator" hook should be used after the "stashBefore()" hook'); } // If no user is given -> deny diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index dad7226..31c3cb6 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -1,4 +1,4 @@ -const { unless, when, isProvider, populate, softDelete, stashBefore, discard } = require('feathers-hooks-common'); +const { unless, when, isProvider, populate, softDelete, stashBefore, discard, disallow } = require('feathers-hooks-common'); const { isVerified } = require('feathers-authentication-management').hooks; const { authenticate } = require('feathers-authentication').hooks; const { associateCurrentUser } = require('feathers-authentication-hooks'); @@ -8,7 +8,7 @@ const createExcerpt = require('../../hooks/create-excerpt'); const isModerator = require('../../hooks/is-moderator-boolean'); // const excludeDisabled = require('../../hooks/exclude-disabled'); const thumbnails = require('../../hooks/thumbnails'); -const isAdminOwnerOrModerator = require('../../hooks/is-adminowner-or-moderator-boolean'); +const isAdminOwnerOrModerator = require('../../hooks/is-adminowner-or-moderator'); const restrictToOwnerOrModerator = require('../../hooks/restrictToOwnerOrModerator'); const restrictReviewAndEnableChange = require('../../hooks/restrictReviewAndEnableChange'); const flagPrimaryAddress = require('./hooks/flag-primary-address'); @@ -132,7 +132,9 @@ module.exports = { authenticate('jwt'), isVerified(), stashBefore(), - restrictToOwnerOrModerator({ isEnabled: true }) + unless(isAdminOwnerOrModerator(), + disallow() + ) ] }, diff --git a/test/services/organizations.test.js b/test/services/organizations.test.js index dad939b..5776e13 100644 --- a/test/services/organizations.test.js +++ b/test/services/organizations.test.js @@ -16,7 +16,7 @@ const { } = require('../assets/organizations'); const { categoryData } = require('../assets/categories'); -describe.only('\'organizations\' service', () => { +describe('\'organizations\' service', () => { let user; let category; let params; @@ -179,7 +179,7 @@ describe.only('\'organizations\' service', () => { }); }); - describe.only('organizations admin roles', () => { + describe('organizations admin roles', () => { let user2; let organization; @@ -207,9 +207,14 @@ describe.only('\'organizations\' service', () => { newUser.id.toString(), user2._id.toString(), 'new user has correct id' ); }); + + it('admin can delete organization', async () => { + const result = await service.remove(organization._id, params); + assert.strictEqual(result.deleted, true, 'organization is deleted'); + }); }); - describe.only('organizations editor roles', () => { + describe('organizations editor roles', () => { let editor; let user2; let editorParams; @@ -260,5 +265,15 @@ describe.only('\'organizations\' service', () => { const result = await service.patch(organization._id, data, editorParams); assert.notEqual(result.users[1].role, 'admin', 'has not changed role'); }); + + it('editor cannot delete organization', async () => { + let error = false; + try { + await service.remove(organization._id, editorParams); + } catch (e) { + error = e; + } + assert.ok(error, 'throws an error'); + }); }); }); From d2b05cafefafa05dd08466b0df60c38a034505ae Mon Sep 17 00:00:00 2001 From: JB Date: Wed, 18 Jul 2018 23:50:41 +0200 Subject: [PATCH 11/12] Restrict organization user creation --- server/models/organizations.model.js | 5 +- .../hooks/flag-primary-address.js | 14 ++++-- .../organizations/hooks/make-users-unique.js | 14 ++++++ .../organizations/organizations.hooks.js | 5 ++ test/services/organizations.test.js | 48 ++++++++++++++++++- 5 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 server/services/organizations/hooks/make-users-unique.js diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 99caae5..3ac3972 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -30,11 +30,12 @@ module.exports = function (app) { type: { type: String, enum: channelNames, - required: true + required: true, + unique: true } }); const userSchema = mongooseClient.Schema({ - id: { type: String, required: true }, + id: { type: String, required: true, unique: true }, role: { type: String, enum: ['admin', 'editor'], diff --git a/server/services/organizations/hooks/flag-primary-address.js b/server/services/organizations/hooks/flag-primary-address.js index 6454cae..b804e20 100644 --- a/server/services/organizations/hooks/flag-primary-address.js +++ b/server/services/organizations/hooks/flag-primary-address.js @@ -1,13 +1,17 @@ -// Add flag on primary address +// Kick out every duplicate user const alterItems = require('../../../helper/alter-items'); module.exports = () => alterItems(handleItem); const handleItem = item => { - if (item.addresses && item.addresses[item.primaryAddressIndex]) { - item.addresses.map((address, index) => { - address.primary = index === item.primaryAddressIndex; - return address; + if (item.users) { + let ids = []; + item.users = item.users.filter(user => { + if (ids.includes(user.id)) { + return false; + } + ids.push(user.id); + return true; }); } return item; diff --git a/server/services/organizations/hooks/make-users-unique.js b/server/services/organizations/hooks/make-users-unique.js new file mode 100644 index 0000000..6454cae --- /dev/null +++ b/server/services/organizations/hooks/make-users-unique.js @@ -0,0 +1,14 @@ +// Add flag on primary address +const alterItems = require('../../../helper/alter-items'); + +module.exports = () => alterItems(handleItem); + +const handleItem = item => { + if (item.addresses && item.addresses[item.primaryAddressIndex]) { + item.addresses.map((address, index) => { + address.primary = index === item.primaryAddressIndex; + return address; + }); + } + return item; +}; \ No newline at end of file diff --git a/server/services/organizations/organizations.hooks.js b/server/services/organizations/organizations.hooks.js index 31c3cb6..b847069 100644 --- a/server/services/organizations/organizations.hooks.js +++ b/server/services/organizations/organizations.hooks.js @@ -12,6 +12,7 @@ const isAdminOwnerOrModerator = require('../../hooks/is-adminowner-or-moderator' const restrictToOwnerOrModerator = require('../../hooks/restrictToOwnerOrModerator'); const restrictReviewAndEnableChange = require('../../hooks/restrictReviewAndEnableChange'); const flagPrimaryAddress = require('./hooks/flag-primary-address'); +const makeUsersUnique = require('./hooks/make-users-unique'); const populateUsersData = require('./hooks/populate-users-data'); const search = require('feathers-mongodb-fuzzy-search'); const isSingleItem = require('../../hooks/is-single-item'); @@ -83,6 +84,8 @@ module.exports = { return hook; } ), + // Users cannot be manually added on creation + discard('users'), // Add current user as creator and user associateCurrentUser({ as: 'creatorId' }), hook => { @@ -109,6 +112,7 @@ module.exports = { unless(isAdminOwnerOrModerator(), discard('users') ), + makeUsersUnique(), createSlug({ field: 'name', overwrite: true }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) @@ -124,6 +128,7 @@ module.exports = { unless(isAdminOwnerOrModerator(), discard('users') ), + makeUsersUnique(), createSlug({ field: 'name', overwrite: true }), createExcerpt({ field: 'description' }), saveRemoteImages(['logo', 'coverImg']) diff --git a/test/services/organizations.test.js b/test/services/organizations.test.js index 5776e13..acff1a3 100644 --- a/test/services/organizations.test.js +++ b/test/services/organizations.test.js @@ -16,7 +16,7 @@ const { } = require('../assets/organizations'); const { categoryData } = require('../assets/categories'); -describe('\'organizations\' service', () => { +describe.only('\'organizations\' service', () => { let user; let category; let params; @@ -53,7 +53,7 @@ describe('\'organizations\' service', () => { assert.ok(service, 'registered the service'); }); - describe('organizations create', () => { + describe.only('organizations create', () => { it('runs create', async () => { const organization = await service.create(organizationData, params); assert.ok(organization, 'created organization'); @@ -93,6 +93,23 @@ describe('\'organizations\' service', () => { 'user is admin' ); }); + + it('cannot add users on creation', async () => { + const user2 = await userService.create(userData2); + organizationData.users = [{ id: user2._id }]; + const organization = await service.create(organizationData, params); + const users = organization.users; + assert.equal( + users[0].id, + user._id.toString(), + 'has correct user id' + ); + assert.equal( + users[1], + undefined, + 'does not have second user' + ); + }); }); describe('organizations find', () => { @@ -179,6 +196,33 @@ describe('\'organizations\' service', () => { }); }); + describe.only('organizations users', () => { + let user2; + let organization; + + beforeEach(async () => { + user2 = await userService.create(userData2); + organization = await service.create(organizationData, params); + }); + + afterEach(async () => { + user2 = null; + organization = null; + }); + + it('a user can only be added once', async () => { + const data = { + users: [ + ...organization.users, + { id: user2._id }, + { id: user2._id } + ] + }; + const result = await service.patch(organization._id, data, params); + assert.strictEqual(result.users[2], undefined, 'has not same user twice'); + }); + }); + describe('organizations admin roles', () => { let user2; let organization; From aa255de66f06db32217054a7dc8e95e4accc233d Mon Sep 17 00:00:00 2001 From: JB Date: Sun, 22 Jul 2018 10:55:48 +0200 Subject: [PATCH 12/12] Restrict organization user creation --- server/models/organizations.model.js | 5 ++--- .../organizations/hooks/flag-primary-address.js | 14 +++++--------- .../organizations/hooks/make-users-unique.js | 14 +++++++++----- test/services/organizations.test.js | 6 +++--- 4 files changed, 19 insertions(+), 20 deletions(-) diff --git a/server/models/organizations.model.js b/server/models/organizations.model.js index 3ac3972..99caae5 100644 --- a/server/models/organizations.model.js +++ b/server/models/organizations.model.js @@ -30,12 +30,11 @@ module.exports = function (app) { type: { type: String, enum: channelNames, - required: true, - unique: true + required: true } }); const userSchema = mongooseClient.Schema({ - id: { type: String, required: true, unique: true }, + id: { type: String, required: true }, role: { type: String, enum: ['admin', 'editor'], diff --git a/server/services/organizations/hooks/flag-primary-address.js b/server/services/organizations/hooks/flag-primary-address.js index b804e20..6454cae 100644 --- a/server/services/organizations/hooks/flag-primary-address.js +++ b/server/services/organizations/hooks/flag-primary-address.js @@ -1,17 +1,13 @@ -// Kick out every duplicate user +// Add flag on primary address const alterItems = require('../../../helper/alter-items'); module.exports = () => alterItems(handleItem); const handleItem = item => { - if (item.users) { - let ids = []; - item.users = item.users.filter(user => { - if (ids.includes(user.id)) { - return false; - } - ids.push(user.id); - return true; + if (item.addresses && item.addresses[item.primaryAddressIndex]) { + item.addresses.map((address, index) => { + address.primary = index === item.primaryAddressIndex; + return address; }); } return item; diff --git a/server/services/organizations/hooks/make-users-unique.js b/server/services/organizations/hooks/make-users-unique.js index 6454cae..b804e20 100644 --- a/server/services/organizations/hooks/make-users-unique.js +++ b/server/services/organizations/hooks/make-users-unique.js @@ -1,13 +1,17 @@ -// Add flag on primary address +// Kick out every duplicate user const alterItems = require('../../../helper/alter-items'); module.exports = () => alterItems(handleItem); const handleItem = item => { - if (item.addresses && item.addresses[item.primaryAddressIndex]) { - item.addresses.map((address, index) => { - address.primary = index === item.primaryAddressIndex; - return address; + if (item.users) { + let ids = []; + item.users = item.users.filter(user => { + if (ids.includes(user.id)) { + return false; + } + ids.push(user.id); + return true; }); } return item; diff --git a/test/services/organizations.test.js b/test/services/organizations.test.js index acff1a3..ade37e3 100644 --- a/test/services/organizations.test.js +++ b/test/services/organizations.test.js @@ -16,7 +16,7 @@ const { } = require('../assets/organizations'); const { categoryData } = require('../assets/categories'); -describe.only('\'organizations\' service', () => { +describe('\'organizations\' service', () => { let user; let category; let params; @@ -53,7 +53,7 @@ describe.only('\'organizations\' service', () => { assert.ok(service, 'registered the service'); }); - describe.only('organizations create', () => { + describe('organizations create', () => { it('runs create', async () => { const organization = await service.create(organizationData, params); assert.ok(organization, 'created organization'); @@ -196,7 +196,7 @@ describe.only('\'organizations\' service', () => { }); }); - describe.only('organizations users', () => { + describe('organizations users', () => { let user2; let organization;