Skip to content

Commit

Permalink
feat: Adds new organisation settings API. (#1398 - LL-77)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: Adds new organisation settings API and removes organisation settings from user API.
  • Loading branch information
h-kanazawa authored and ht2 committed Aug 6, 2019
1 parent 1bdafbd commit b4c1030
Show file tree
Hide file tree
Showing 24 changed files with 1,158 additions and 79 deletions.
106 changes: 106 additions & 0 deletions api/src/controllers/UserOrganisationSettingsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import User from 'lib/models/user';
import { SITE_ADMIN } from 'lib/constants/scopes';
import { MANAGE_ALL_USERS } from 'lib/constants/orgScopes';
import NotFoundError from 'lib/errors/NotFoundError';
import ClientError from 'lib/errors/ClientError';
import catchErrors from 'api/controllers/utils/catchErrors';

const createOrganisationSetting = catchErrors(async (req, res) => {
const user = await User.findOne({ _id: req.params.userId });
if (!user) {
throw new NotFoundError();
}

const alreadyExists = user.organisationSettings.some(s => s.organisation.toString() === req.params.organisationId);
if (alreadyExists) {
throw new ClientError(`Duplicated: The user already has the organisationSettings for the organisation (${req.params.organisationId})`);
}

// Should we set organisation required parameter?
if (req.body.organisation === undefined) {
req.body.organisation = req.params.organisationId;
}

if (req.params.organisationId !== req.body.organisation) {
throw new ClientError(`Invalid: organisationId in URL path (${req.params.organisationId}) and organisation in body (${req.body.organisation}) are not matched.`);
}

user.organisationSettings.push(req.body);
const updatedUser = await user.save();
const insertedOrganisationSetting = updatedUser.organisationSettings.find(s => s.organisation.toString() === req.params.organisationId);
res.status(200).send(insertedOrganisationSetting);
});

/**
* @param {object} body
* @param {string[]} scopes
* @returns {boolean}
*/
const validateUpdatableKeys = (body, scopes) => {
if (scopes.includes(SITE_ADMIN)) {
return true;
}

const updatingKeys = Object.keys(body);

if (scopes.includes(MANAGE_ALL_USERS)) {
return updatingKeys.every(key => ['filter', 'roles', 'scopes'].includes(key));
}

return updatingKeys.every(key => ['samlEnabled'].includes(key));
};

const updateOrganisationSetting = catchErrors(async (req, res) => {
const user = await User.findOne({ _id: req.params.userId });
if (!user) {
throw new NotFoundError();
}

const scopes = req.user.authInfo.token.scopes;
const isValid = validateUpdatableKeys(req.body, scopes);
if (!isValid) {
throw new ClientError('Can not update some fields you are trying to update');
}

if (req.body.organisation && req.params.organisationId !== req.body.organisation) {
throw new ClientError(`Invalid: organisationId in URL path (${req.params.organisationId}) and organisation in body (${req.body.organisation}) are not matched.`);
}

const i = user.organisationSettings.findIndex(s => s.organisation.toString() === req.params.organisationId);
if (i < 0) {
user.organisationSettings.push({
organisation: req.params.organisationId,
...req.body,
});
} else {
user.organisationSettings[i] = {
...user.organisationSettings[i].toObject(),
...req.body,
};
}

const updatedUser = await user.save();
const insertedOrganisationSetting = updatedUser.organisationSettings[i];

res.status(200).send(insertedOrganisationSetting);
});


const deleteOrganisationSetting = catchErrors(async (req, res) => {
const user = await User.findOne({ _id: req.params.userId });
if (!user) {
throw new NotFoundError();
}

user.organisationSettings = user.organisationSettings.filter(s => s.organisation.toString() !== req.params.organisationId);
await user.save();
res.status(200).send();
});

const UserOrganisationSettingsController = {
create: createOrganisationSetting,
update: updateOrganisationSetting,
delete: deleteOrganisationSetting,
};

export default UserOrganisationSettingsController;
23 changes: 23 additions & 0 deletions api/src/controllers/UserOrganisationsController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import User from 'lib/models/user';
import NotFoundError from 'lib/errors/NotFoundError';
import catchErrors from 'api/controllers/utils/catchErrors';

const removeOrganisationFromUser = catchErrors(async (req, res) => {
const user = await User.findOne({ _id: req.params.userId });
if (!user) {
throw new NotFoundError(`Not found user (_id: ${req.params.userId})`);
}

const userOrganisationSet = new Set(user.organisations.map(o => o.toString()));
userOrganisationSet.delete(req.params.organisationId);
user.organisations = Array.from(userOrganisationSet);

await user.save();
res.status(200).send();
});

const UserOrganisationsController = {
delete: removeOrganisationFromUser,
};

export default UserOrganisationsController;
52 changes: 36 additions & 16 deletions api/src/routes/HttpRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import {
GOOGLE_AUTH_OPTIONS,
DEFAULT_PASSPORT_OPTIONS,
RESTIFY_DEFAULTS,
setNoCacheHeaders
setNoCacheHeaders,
checkOrg,
} from 'lib/constants/auth';
import { MANAGER_SELECT } from 'lib/services/auth/selects/models/user.js';

Expand Down Expand Up @@ -59,6 +60,8 @@ import SiteSettings from 'lib/models/siteSettings';
import personaRESTHandler from 'api/routes/personas/personaRESTHandler';
import personaIdentifierRESTHandler from 'api/routes/personas/personaIdentifierRESTHandler';
import personaAttributeRESTHandler from 'api/routes/personas/personaAttributeRESTHandler';
import UserOrganisationsRouter from 'api/routes/userOrganisations/router';
import UserOrganisationSettingsRouter from 'api/routes/userOrganisationSettings/router';
import BatchDelete from 'lib/models/batchDelete';
import getOrgFromAuthInfo from 'lib/services/auth/authInfoSelectors/getOrgFromAuthInfo';
import { updateStatementCountsInOrg } from 'lib/services/lrs';
Expand Down Expand Up @@ -144,6 +147,16 @@ router.use(personaRESTHandler);
router.use(personaIdentifierRESTHandler);
router.use(personaAttributeRESTHandler);

/**
* User Organisations
*/
router.use(UserOrganisationsRouter);

/**
* User OrganisationSettings
*/
router.use(UserOrganisationSettingsRouter);

/**
* UPLOADS
*/
Expand Down Expand Up @@ -242,7 +255,6 @@ router.post(
BatchDeleteController.terminateBatchDelete
);


/**
* V1 compatability
*/
Expand Down Expand Up @@ -274,23 +286,31 @@ restify.serve(router, Download);
restify.serve(router, Query);
restify.serve(router, ImportCsv);
restify.serve(router, User, {
preUpdate: (req, res, next) => {
preCreate: (req, res, next) => {
const authInfo = getAuthFromRequest(req);
const scopes = getScopesFromAuthInfo(authInfo);
const tokenType = getTokenTypeFromAuthInfo(authInfo);

// if site admin, skip over this section
if (findIndex(scopes, item => item === SITE_ADMIN) < 0) {
// remove scope changes
req.body = omit(req.body, 'scopes');
if (tokenType === 'user' || tokenType === 'organisation') {
if (req.body._id !== getUserIdFromAuthInfo(authInfo).toString()) {
// Don't allow changing of passwords
req.body = omit(req.body, 'password');
}
if (!scopes.includes(SITE_ADMIN)) {
req.body = pick(req.body, ['name', 'email', 'isExpanded', 'organisations']);
}
checkOrg(req, res, next);
},
preUpdate: (req, _, next) => {
const authInfo = getAuthFromRequest(req);
const scopes = getScopesFromAuthInfo(authInfo);

// Site admins can update any fields
if (!scopes.includes(SITE_ADMIN)) {
const tokenType = getTokenTypeFromAuthInfo(authInfo);
const isUpdatingItself =
['user', 'organisation'].includes(tokenType) &&
req.body._id === getUserIdFromAuthInfo(authInfo).toString();

// Non site admin user can update
// only name and password of the user itself or only name of other users.
if (isUpdatingItself) {
req.body = pick(req.body, ['name', 'password']);
} else {
// always strip the password from other token types
req.body = omit(req.body, 'password');
req.body = pick(req.body, ['name']);
}
}

Expand Down
Loading

0 comments on commit b4c1030

Please sign in to comment.