Skip to content
This repository has been archived by the owner on Jun 27, 2019. It is now read-only.

[WIP] Organisations #140

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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_mocha",
"mocha:mongo": "$npm_package_config_concurrently '$npm_package_config_mongoDev &>/dev/null' 'wait-on tcp:27017 && npm run mocha'",
"cucumber": "npm run clear && concurrently --kill-others --success first 'cross-env NODE_ENV=test node server/' 'wait-on tcp:3030 && cross-env NODE_ENV=test cucumber-js'",
Expand Down Expand Up @@ -89,6 +90,7 @@
"handlebars-layouts": "~3.1.4",
"helmet": "~3.13.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",
Expand Down
37 changes: 27 additions & 10 deletions server/helper/seed-helpers.js
Original file line number Diff line number Diff line change
@@ -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',
Expand Down Expand Up @@ -35,17 +37,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 = [];
Expand Down Expand Up @@ -101,12 +105,25 @@ 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)
});
}
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(channelNames)
});
}
return channels;
},
/**
* Get array of ids from the given seederstore items after mapping them by the key in the values
*
Expand Down
30 changes: 30 additions & 0 deletions server/hooks/is-adminowner-or-moderator.js
Original file line number Diff line number Diff line change
@@ -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 "isAdminOwnerOrModerator" hook should only be used as a "before" hook.');
}

if (!getByDot(hook, 'params.before')) {
throw new Error('The "isAdminOwnerOrModerator" 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';
};
11 changes: 6 additions & 5 deletions server/hooks/restrictToOwnerOrModerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ 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 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: [
{ userId },
{ 'users.id': userId },
{ ...query }
]
};
Expand Down
34 changes: 31 additions & 3 deletions server/models/organizations.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -15,9 +20,27 @@ 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 }
});
const channelSchema = mongooseClient.Schema({
name: { type: String, required: true },
type: {
type: String,
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 },
Expand All @@ -26,18 +49,23 @@ 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 },
users: { type: [userSchema], default: [] },
description: { type: String, required: true },
descriptionExcerpt: { type: String }, // will be generated automatically
publicEmail: { type: String },
phone: { type: String },
email: { type: String },
url: { type: String },
type: {
type: String,
required: true,
index: true,
enum: ['ngo', 'npo', 'goodpurpose', 'ev', 'eva']
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 },
isEnabled: {
Expand Down
18 changes: 12 additions & 6 deletions server/seeder/development/organizations.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,11 +15,13 @@ 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,
url: '{{internet.url}}',
publicEmail: '{{internet.email}}',
phone: '{{phone.phoneNumber}}',
email: '{{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,
Expand All @@ -36,11 +40,13 @@ 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,
url: '{{internet.url}}',
publicEmail: '{{internet.email}}',
phone: '{{phone.phoneNumber}}',
email: '{{internet.email}}',
addresses: () => seedHelpers.randomAddresses(),
type: () => seedHelpers.randomItem(['ngo', 'npo', 'goodpurpose', 'ev', 'eva']),
channels: () => seedHelpers.randomChannels(),
type: () => seedHelpers.randomItem(organizationTypes),
description: '{{lorem.text}}',
deletedAt: null,
isEnabled: true,
Expand Down
4 changes: 3 additions & 1 deletion server/services/organizations/hooks/can-edit-organization.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.users.some(
({id}) => id === currentUserId.toString()
)) {
throw new errors.Forbidden('you can\'t create or edit for that organization');
}

Expand Down
14 changes: 14 additions & 0 deletions server/services/organizations/hooks/flag-primary-address.js
Original file line number Diff line number Diff line change
@@ -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;
};
18 changes: 18 additions & 0 deletions server/services/organizations/hooks/make-users-unique.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Kick out every duplicate user
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;
});
}
return item;
};
33 changes: 33 additions & 0 deletions server/services/organizations/hooks/populate-users-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Populate user data on organization users
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;
};
Loading