From ea9458ca024017ee7aa317f1cdf321320e638a0e Mon Sep 17 00:00:00 2001 From: Sebastian Mihalache <sebastian.mihalache@gmail.con> Date: Wed, 20 Jun 2018 15:55:17 +0300 Subject: [PATCH] fix tests --- .../src/fixtures/fragments.js | 2 +- .../src/helpers/Model.js | 4 +- .../src/services/Collection.js | 2 +- .../src/services/Email.js | 2 +- .../src/services/Fragment.js | 39 ++- .../src/services/User.js | 8 +- .../config/authsome-helpers.js | 59 +++- .../tests/collectionsInvitations/get.test.js | 137 -------- .../config/authsome-helpers.js | 59 +++- .../config/authsome-mode.js | 306 +++++++++--------- .../routes/fragmentsRecommendations/patch.js | 8 +- .../routes/fragmentsRecommendations/post.js | 7 +- .../fragmentsRecommendations/post.test.js | 23 +- .../config/authsome-helpers.js | 59 +++- .../config/authsome-mode.js | 306 +++++++++--------- .../src/FragmentsUsers.js | 17 +- .../src/routes/fragmentsUsers/post.js | 2 +- .../xpub-faraday/config/authsome-helpers.js | 15 +- packages/xpub-faraday/config/authsome-mode.js | 2 + 19 files changed, 539 insertions(+), 518 deletions(-) delete mode 100644 packages/component-invite/src/tests/collectionsInvitations/get.test.js diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index ab41ab428..5810c6fdd 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -41,7 +41,7 @@ const fragments = { ], authors: [ { - userId: submittingAuthor.id, + id: submittingAuthor.id, isSubmitting: true, isCorresponding: false, }, diff --git a/packages/component-fixture-manager/src/helpers/Model.js b/packages/component-fixture-manager/src/helpers/Model.js index 04bdb01f9..9ff60517c 100644 --- a/packages/component-fixture-manager/src/helpers/Model.js +++ b/packages/component-fixture-manager/src/helpers/Model.js @@ -16,6 +16,7 @@ const build = fixtures => { find: jest.fn(id => findMock(id, 'fragments', fixtures)), }, } + UserMock.find = jest.fn(id => findMock(id, 'users', fixtures)) UserMock.findByEmail = jest.fn(email => findByEmailMock(email, fixtures)) UserMock.all = jest.fn(() => Object.values(fixtures.users)) @@ -25,6 +26,7 @@ const build = fixtures => { UserMock.updateProperties = jest.fn(user => updatePropertiesMock(user, 'users'), ) + TeamMock.find = jest.fn(id => findMock(id, 'teams', fixtures)) TeamMock.updateProperties = jest.fn(team => updatePropertiesMock(team, 'teams', fixtures), @@ -41,7 +43,7 @@ const findMock = (id, type, fixtures) => { fixtureObj => fixtureObj.id === id, ) - if (foundObj === undefined) return Promise.reject(notFoundError) + if (!foundObj) return Promise.reject(notFoundError) return Promise.resolve(foundObj) } diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js index c0fb4fddd..fd58224e1 100644 --- a/packages/component-helper-service/src/services/Collection.js +++ b/packages/component-helper-service/src/services/Collection.js @@ -36,7 +36,7 @@ class Collection { async updateStatus({ newStatus }) { this.collection.status = newStatus - this.collection.visibleStatus = statuses[this.collection.status].private + this.collection.visibleStatus = statuses[this.collection.status].public await this.collection.save() } diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js index 49fb18189..7e7bcd451 100644 --- a/packages/component-helper-service/src/services/Email.js +++ b/packages/component-helper-service/src/services/Email.js @@ -332,7 +332,7 @@ class Email { } async setupReviewerUnassignEmail({ user, authorName }) { - const { collection, fragment: { title } } = this + const { collection, parsedFragment: { title = '' } } = this await mailService.sendNotificationEmail({ toEmail: user.email, diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js index c03dcdda7..37c3a22ea 100644 --- a/packages/component-helper-service/src/services/Fragment.js +++ b/packages/component-helper-service/src/services/Fragment.js @@ -1,13 +1,16 @@ +const get = require('lodash/get') + class Fragment { constructor({ fragment }) { this.fragment = fragment } + async getFragmentData({ handlingEditor = {} }) { - const { fragment: { metadata, recommendations = [], id } } = this + const { fragment: { metadata = {}, recommendations = [], id } } = this const heRecommendation = recommendations.find( rec => rec.userId === handlingEditor.id, ) - let { title, abstract } = metadata + let { title = '', abstract = '' } = metadata const { type } = metadata title = title.replace(/<(.|\n)*?>/g, '') abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : '' @@ -42,20 +45,26 @@ class Fragment { } async getAuthorData({ UserModel }) { - const { fragment: { authors } } = this - const submittingAuthorData = authors.find( - author => author.isSubmitting === true, - ) - const submittingAuthor = await UserModel.find(submittingAuthorData.id) - const authorsPromises = authors.map(async author => { - const user = await UserModel.find(author.id) - return `${user.firstName} ${user.lastName}` - }) - const authorsList = await Promise.all(authorsPromises) + const { fragment: { authors = [] } } = this + const submittingAuthorData = authors.find(author => author.isSubmitting) - return { - authorsList, - submittingAuthor, + try { + const submittingAuthor = await UserModel.find( + get(submittingAuthorData, 'id'), + ) + + const authorsPromises = authors.map(async author => { + const user = await UserModel.find(author.id) + return `${user.firstName} ${user.lastName}` + }) + const authorsList = await Promise.all(authorsPromises) + + return { + authorsList, + submittingAuthor, + } + } catch (e) { + throw e } } diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js index ed69cd655..7d9d3e520 100644 --- a/packages/component-helper-service/src/services/User.js +++ b/packages/component-helper-service/src/services/User.js @@ -31,12 +31,8 @@ class User { let newUser = new UserModel(userBody) - try { - newUser = await newUser.save() - return newUser - } catch (e) { - logger.error(e) - } + newUser = await newUser.save() + return newUser } async setupNewUser({ url, role, invitationType, body = {} }) { diff --git a/packages/component-invite/config/authsome-helpers.js b/packages/component-invite/config/authsome-helpers.js index 164a9ce8a..55148df34 100644 --- a/packages/component-invite/config/authsome-helpers.js +++ b/packages/component-invite/config/authsome-helpers.js @@ -59,8 +59,12 @@ const filterObjectData = ( return object } -const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { - const teams = await Promise.all( +const getTeamsByPermissions = async ( + teamIds = [], + permissions = [], + TeamModel, +) => + (await Promise.all( teamIds.map(async teamId => { const team = await TeamModel.find(teamId) if (!permissions.includes(team.teamType.permissions)) { @@ -68,15 +72,60 @@ const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { } return team }), + )).filter(Boolean) + +const heIsInvitedToFragment = async ({ user, Team, collectionId }) => + (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some( + // user is a member of the team with access to the fragment's parent collection + t => t.members.includes(user.id) && t.object.id === collectionId, ) - return teams.filter(Boolean) +const getUserPermissions = async ({ + user, + Team, + mapFn = t => ({ + objectId: t.object.id, + objectType: t.object.type, + role: t.teamType.permissions, + }), +}) => + (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn) + +const isOwner = ({ user: { id }, object }) => { + if (object.owners.includes(id)) return true + return !!object.owners.find(own => own.id === id) +} + +const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => { + const userPermissions = await getUserPermissions({ + user, + Team, + }) + + return !!userPermissions.find(p => { + const hasObject = + p.objectId === get(object, 'fragment.id') || + p.objectId === get(object, 'fragment.collectionId') + if (roles.length > 0) { + return hasObject && roles.includes(p.role) + } + return hasObject + }) } +const isHandlingEditor = ({ user, object }) => + get(object, 'collection.handlingEditor.id') === user.id + module.exports = { + filterObjectData, parseAuthorsData, setPublicStatuses, - filterRefusedInvitations, - filterObjectData, getTeamsByPermissions, + filterRefusedInvitations, + // + isOwner, + isHandlingEditor, + getUserPermissions, + heIsInvitedToFragment, + hasPermissionForObject, } diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/collectionsInvitations/get.test.js deleted file mode 100644 index d630fa94c..000000000 --- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js +++ /dev/null @@ -1,137 +0,0 @@ -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' -process.env.SUPPRESS_NO_CONFIG_WARNING = true - -const cloneDeep = require('lodash/cloneDeep') -const fixturesService = require('pubsweet-component-fixture-service') -const requests = require('../requests') - -const { Model, fixtures } = fixturesService -jest.mock('pubsweet-component-mail-service', () => ({ - sendSimpleEmail: jest.fn(), - sendNotificationEmail: jest.fn(), - sendReviewerInvitationEmail: jest.fn(), -})) -const path = '../routes/collectionsInvitations/get' -const route = { - path: '/api/collections/:collectionId/invitations/:invitationId?', -} -describe('Get collection invitations route handler', () => { - let testFixtures = {} - let models - beforeEach(() => { - testFixtures = cloneDeep(fixtures) - models = Model.build(testFixtures) - }) - it('should return success when the request data is correct', async () => { - const { editorInChief, handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections - const res = await requests.sendRequest({ - userId: editorInChief.id, - route, - models, - path, - query: { - role: 'handlingEditor', - userId: handlingEditor.id, - }, - params: { - collectionId: collection.id, - }, - }) - - expect(res.statusCode).toBe(200) - const data = JSON.parse(res._getData()) - expect(data.length).toBeGreaterThan(0) - }) - it('should return an error when parameters are missing', async () => { - const { editorInChief } = testFixtures.users - const res = await requests.sendRequest({ - userId: editorInChief.id, - route, - models, - path, - }) - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Role is required') - }) - it('should return an error when the collection does not exist', async () => { - const { editorInChief, handlingEditor } = testFixtures.users - const res = await requests.sendRequest({ - userId: editorInChief.id, - route, - models, - path, - query: { - role: 'handlingEditor', - userId: handlingEditor.id, - }, - params: { - collectionId: 'invalid-id', - }, - }) - expect(res.statusCode).toBe(404) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('collection not found') - }) - it('should return an error when the role is invalid', async () => { - const { editorInChief, handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections - const res = await requests.sendRequest({ - userId: editorInChief.id, - route, - models, - path, - query: { - role: 'invalidRole', - userId: handlingEditor.id, - }, - params: { - collectionId: collection.id, - }, - }) - expect(res.statusCode).toBe(400) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`Role invalidRole is invalid`) - }) - it('should return success with an empty array when the collection does not have a the requested role team', async () => { - const { editorInChief, handlingEditor } = testFixtures.users - const { collection } = testFixtures.collections - delete collection.invitations - const res = await requests.sendRequest({ - userId: editorInChief.id, - route, - models, - path, - query: { - role: 'author', - userId: handlingEditor.id, - }, - params: { - collectionId: collection.id, - }, - }) - expect(res.statusCode).toBe(200) - const data = JSON.parse(res._getData()) - expect(data).toHaveLength(0) - }) - it('should return an error when a user does not have invitation rights', async () => { - const { author } = testFixtures.users - const { collection } = testFixtures.collections - const res = await requests.sendRequest({ - userId: author.id, - route, - models, - path, - query: { - role: 'handlingEditor', - }, - params: { - collectionId: collection.id, - }, - }) - expect(res.statusCode).toBe(403) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Unauthorized.') - }) -}) diff --git a/packages/component-manuscript-manager/config/authsome-helpers.js b/packages/component-manuscript-manager/config/authsome-helpers.js index 164a9ce8a..55148df34 100644 --- a/packages/component-manuscript-manager/config/authsome-helpers.js +++ b/packages/component-manuscript-manager/config/authsome-helpers.js @@ -59,8 +59,12 @@ const filterObjectData = ( return object } -const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { - const teams = await Promise.all( +const getTeamsByPermissions = async ( + teamIds = [], + permissions = [], + TeamModel, +) => + (await Promise.all( teamIds.map(async teamId => { const team = await TeamModel.find(teamId) if (!permissions.includes(team.teamType.permissions)) { @@ -68,15 +72,60 @@ const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { } return team }), + )).filter(Boolean) + +const heIsInvitedToFragment = async ({ user, Team, collectionId }) => + (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some( + // user is a member of the team with access to the fragment's parent collection + t => t.members.includes(user.id) && t.object.id === collectionId, ) - return teams.filter(Boolean) +const getUserPermissions = async ({ + user, + Team, + mapFn = t => ({ + objectId: t.object.id, + objectType: t.object.type, + role: t.teamType.permissions, + }), +}) => + (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn) + +const isOwner = ({ user: { id }, object }) => { + if (object.owners.includes(id)) return true + return !!object.owners.find(own => own.id === id) +} + +const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => { + const userPermissions = await getUserPermissions({ + user, + Team, + }) + + return !!userPermissions.find(p => { + const hasObject = + p.objectId === get(object, 'fragment.id') || + p.objectId === get(object, 'fragment.collectionId') + if (roles.length > 0) { + return hasObject && roles.includes(p.role) + } + return hasObject + }) } +const isHandlingEditor = ({ user, object }) => + get(object, 'collection.handlingEditor.id') === user.id + module.exports = { + filterObjectData, parseAuthorsData, setPublicStatuses, - filterRefusedInvitations, - filterObjectData, getTeamsByPermissions, + filterRefusedInvitations, + // + isOwner, + isHandlingEditor, + getUserPermissions, + heIsInvitedToFragment, + hasPermissionForObject, } diff --git a/packages/component-manuscript-manager/config/authsome-mode.js b/packages/component-manuscript-manager/config/authsome-mode.js index 8a92a1301..20e0d6918 100644 --- a/packages/component-manuscript-manager/config/authsome-mode.js +++ b/packages/component-manuscript-manager/config/authsome-mode.js @@ -1,69 +1,8 @@ -const get = require('lodash/get') -const pickBy = require('lodash/pickBy') -const omit = require('lodash/omit') -const helpers = require('./authsome-helpers') - -async function teamPermissions(user, operation, object, context) { - const { models } = context - const permissions = ['handlingEditor', 'author', 'reviewer'] - const teams = await helpers.getTeamsByPermissions( - user.teams, - permissions, - context.models.Team, - ) - - let collectionsPermissions = await Promise.all( - teams.map(async team => { - let collection - if (team.object.type === 'collection') { - collection = await models.Collection.find(team.object.id) - } else if (team.object.type === 'fragment') { - const fragment = await models.Fragment.find(team.object.id) - collection = await models.Collection.find(fragment.collectionId) - } - if ( - collection.status === 'rejected' && - team.teamType.permissions === 'reviewer' - ) - return null - const collPerm = { - id: collection.id, - permission: team.teamType.permissions, - } - const objectType = get(object, 'type') - if (objectType === 'fragment') { - if (collection.fragments.includes(object.id)) - collPerm.fragmentId = object.id - else return null - } - - if (objectType === 'collection') - if (object.id !== collection.id) return null - return collPerm - }), - ) - collectionsPermissions = collectionsPermissions.filter(cp => cp !== null) - if (collectionsPermissions.length === 0) return false - - return { - filter: filterParam => { - if (!filterParam.length) { - return helpers.filterObjectData( - collectionsPermissions, - filterParam, - user, - ) - } +const config = require('config') +const { get, pickBy, omit } = require('lodash') - const collections = filterParam - .map(coll => - helpers.filterObjectData(collectionsPermissions, coll, user), - ) - .filter(Boolean) - return collections - }, - } -} +const statuses = config.get('statuses') +const helpers = require('./authsome-helpers') function unauthenticatedUser(operation, object) { // Public/unauthenticated users can GET /collections, filtered by 'published' @@ -111,133 +50,182 @@ function unauthenticatedUser(operation, object) { return false } +const publicStatusesPermissions = ['author', 'reviewer'] +const createPaths = ['/collections', '/collections/:collectionId/fragments'] + async function authenticatedUser(user, operation, object, context) { - // Allow the authenticated user to POST a collection (but not with a 'filtered' property) - if (operation === 'POST' && object.path === '/collections') { - return { - filter: collection => omit(collection, 'filtered'), + if (operation === 'GET') { + if (get(object, 'path') === '/collections') { + return { + filter: async collections => { + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) + return collections.filter(collection => { + if (collection.owners.includes(user.id)) { + return true + } + const collectionPermission = userPermissions.find( + p => p.objectId === collection.id, + ) + if (collectionPermission) { + return true + } + + const fragmentPermission = userPermissions.find(p => + collection.fragments.includes(p.objectId), + ) + if (fragmentPermission) { + return true + } + return false + }) + }, + } } - } - if ( - operation === 'POST' && - object.path === '/collections/:collectionId/fragments' - ) { - return true - } + if (object === '/users') { + return true + } - // allow authenticate owners full pass for a collection - if (get(object, 'type') === 'collection') { - if (operation === 'PATCH') { + if (get(object, 'type') === 'collection') { + if (helpers.isOwner({ user, object })) { + return true + } return { - filter: collection => omit(collection, 'filtered'), + filter: async collection => { + const status = get(collection, 'status') || 'draft' + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) + if (collection.owners.map(o => o.id).includes(user.id)) { + return collection + } + + const collectionPermission = userPermissions.find( + p => p.objectId === collection.id, + ) + if ( + publicStatusesPermissions.includes( + get(collectionPermission, 'role'), + ) + ) { + collection.visibleStatus = statuses[status].public + } + return collection + }, } } - if (object.owners.includes(user.id)) return true - const owner = object.owners.find(own => own.id === user.id) - if (owner !== undefined) return true - } - // Allow owners of a collection to GET its teams, e.g. - // GET /api/collections/1/teams - if (operation === 'GET' && get(object, 'path') === '/teams') { - const collectionId = get(object, 'params.collectionId') - if (collectionId) { - const collection = await context.models.Collection.find(collectionId) - if (collection.owners.includes(user.id)) { + if (get(object, 'type') === 'fragment') { + if (helpers.isOwner({ user, object })) { return true } - } - } - if ( - operation === 'GET' && - get(object, 'type') === 'team' && - get(object, 'object.type') === 'collection' - ) { - const collection = await context.models.Collection.find( - get(object, 'object.id'), - ) - if (collection.owners.includes(user.id)) { - return true - } - } + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) - // Advanced example - // Allow authenticated users to create a team based around a collection - // if they are one of the owners of this collection - if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') { - if (get(object, 'object.type') === 'collection') { - const collection = await context.models.Collection.find( - get(object, 'object.id'), + const permission = userPermissions.find( + p => p.objectId === object.id || p.objectId === object.collectionId, ) - if (collection.owners.includes(user.id)) { + + if (!permission) return false + + return { + filter: fragment => { + // handle other roles + if (permission.role === 'reviewer') { + fragment.files = omit(fragment.files, ['coverLetter']) + fragment.authors = fragment.authors.map(a => omit(a, ['email'])) + } + return fragment + }, + } + } + + // allow HE to get reviewer invitations + if (get(object, 'fragment.type') === 'fragment') { + const collectionId = get(object, 'fragment.collectionId') + const collection = await context.models.Collection.find(collectionId) + + if (get(collection, 'handlingEditor.id') === user.id) { return true } } - } - // only allow the HE to create, delete an invitation, or get invitation details - if ( - ['POST', 'GET', 'DELETE'].includes(operation) && - get(object.collection, 'type') === 'collection' && - object.path.includes('invitations') - ) { - const collection = await context.models.Collection.find( - get(object.collection, 'id'), - ) - const handlingEditor = get(collection, 'handlingEditor') - if (!handlingEditor) return false - if (handlingEditor.id === user.id) return true - return false + if (get(object, 'type') === 'user') { + return true + } } - // only allow a reviewer and an HE to submit and to modify a recommendation - if ( - ['POST', 'PATCH'].includes(operation) && - object.path.includes('recommendations') - ) { - const authsomeObject = get(object, 'authsomeObject') - - const teams = await helpers.getTeamsByPermissions( - user.teams, - ['reviewer', 'handlingEditor'], - context.models.Team, - ) + if (operation === 'POST') { + // allow everytone to create manuscripts and versions + if (createPaths.includes(object.path)) { + return true + } - if (teams.length === 0) return false - const matchingTeam = teams.find( - team => team.object.id === authsomeObject.id, - ) + // allow HE to invite + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations' + ) { + return helpers.isHandlingEditor({ user, object }) + } - if (matchingTeam) return true - return false + // allow HE or assigned reviewers to recommend + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + roles: ['reviewer', 'handlingEditor'], + }) + } } - if (user.teams.length !== 0 && ['GET'].includes(operation)) { - const permissions = await teamPermissions(user, operation, object, context) - - if (permissions) { - return permissions + if (operation === 'PATCH') { + if (get(object, 'type') === 'collection') { + return helpers.isOwner({ user, object }) } - return false - } + if (get(object, 'type') === 'fragment') { + return helpers.isOwner({ user, object }) + } - if (get(object, 'type') === 'fragment') { - const fragment = object + // allow reviewer to patch his recommendation + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + roles: ['reviewer'], + }) + } - if (fragment.owners.includes(user.id)) { + if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { return true } } - // A user can GET, DELETE and PATCH itself - if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { - if (['GET', 'DELETE', 'PATCH'].includes(operation)) { - return true + if (operation === 'DELETE') { + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId' + ) { + return helpers.isHandlingEditor({ user, object }) } } + // If no individual permissions exist (above), fallback to unauthenticated // user's permission return unauthenticatedUser(operation, object) diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index e2dfc1a65..39ac00b46 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -17,9 +17,11 @@ module.exports = models => async (req, res) => { }) fragment = await models.Fragment.find(fragmentId) + const recommendation = fragment.recommendations.find( rec => rec.id === recommendationId, ) + if (!recommendation) return res.status(404).json({ error: 'Recommendation not found.' }) @@ -51,9 +53,11 @@ module.exports = models => async (req, res) => { }) const baseUrl = services.getBaseUrl(req) const collectionHelper = new Collection({ collection }) + const authors = await fragmentHelper.getAuthorData({ UserModel, }) + const email = new Email({ UserModel, collection, @@ -61,14 +65,16 @@ module.exports = models => async (req, res) => { baseUrl, authors, }) + email.setupHandlingEditorEmail({ reviewSubmitted: true, reviewerName: `${user.firstName} ${user.lastName}`, }) + if (!['pendingApproval', 'revisionRequested'].includes(collection.status)) collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) } - await fragment.save() + fragment.save() return res.status(200).json(recommendation) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index 29e3bec20..302b95124 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -24,7 +24,6 @@ module.exports = models => async (req, res) => { return res.status(400).json({ error: `Collection and fragment do not match.`, }) - fragment = await models.Fragment.find(fragmentId) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') @@ -32,6 +31,7 @@ module.exports = models => async (req, res) => { error: notFoundError.message, }) } + const authsome = authsomeHelper.getAuthsome(models) const target = { fragment, @@ -42,6 +42,7 @@ module.exports = models => async (req, res) => { return res.status(403).json({ error: 'Unauthorized.', }) + fragment.recommendations = fragment.recommendations || [] const newRecommendation = { id: uuid.v4(), @@ -59,8 +60,10 @@ module.exports = models => async (req, res) => { const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, }) + const baseUrl = services.getBaseUrl(req) const authors = await fragmentHelper.getAuthorData({ UserModel }) + const email = new Email({ UserModel, collection, @@ -69,6 +72,7 @@ module.exports = models => async (req, res) => { authors, }) const FragmentModel = models.Fragment + if (reqUser.editorInChief || reqUser.admin) { if (recommendation === 'return-to-handling-editor') { collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) @@ -120,7 +124,6 @@ module.exports = models => async (req, res) => { }) } } - fragment.recommendations.push(newRecommendation) await fragment.save() return res.status(200).json(newRecommendation) diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js index 30f7ee90b..c0c33bd5e 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -57,7 +57,7 @@ describe('Post fragments recommendations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Recommendation type is required.') }) - it('should return success when the parameters are correct', async () => { + it('should return success when creating a recommendation as a reviewer', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments @@ -78,6 +78,27 @@ describe('Post fragments recommendations route handler', () => { const data = JSON.parse(res._getData()) expect(data.userId).toEqual(reviewer.id) }) + it('should return success when creating a recommendation as a HE', async () => { + const { handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: handlingEditor.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.userId).toEqual(handlingEditor.id) + }) it('should return an error when the fragmentId does not match the collectionId', async () => { const { reviewer } = testFixtures.users const { collection } = testFixtures.collections diff --git a/packages/component-user-manager/config/authsome-helpers.js b/packages/component-user-manager/config/authsome-helpers.js index 164a9ce8a..55148df34 100644 --- a/packages/component-user-manager/config/authsome-helpers.js +++ b/packages/component-user-manager/config/authsome-helpers.js @@ -59,8 +59,12 @@ const filterObjectData = ( return object } -const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { - const teams = await Promise.all( +const getTeamsByPermissions = async ( + teamIds = [], + permissions = [], + TeamModel, +) => + (await Promise.all( teamIds.map(async teamId => { const team = await TeamModel.find(teamId) if (!permissions.includes(team.teamType.permissions)) { @@ -68,15 +72,60 @@ const getTeamsByPermissions = async (teamIds = [], permissions, TeamModel) => { } return team }), + )).filter(Boolean) + +const heIsInvitedToFragment = async ({ user, Team, collectionId }) => + (await getTeamsByPermissions(user.teams, ['handlingEditor'], Team)).some( + // user is a member of the team with access to the fragment's parent collection + t => t.members.includes(user.id) && t.object.id === collectionId, ) - return teams.filter(Boolean) +const getUserPermissions = async ({ + user, + Team, + mapFn = t => ({ + objectId: t.object.id, + objectType: t.object.type, + role: t.teamType.permissions, + }), +}) => + (await Promise.all(user.teams.map(teamId => Team.find(teamId)))).map(mapFn) + +const isOwner = ({ user: { id }, object }) => { + if (object.owners.includes(id)) return true + return !!object.owners.find(own => own.id === id) +} + +const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => { + const userPermissions = await getUserPermissions({ + user, + Team, + }) + + return !!userPermissions.find(p => { + const hasObject = + p.objectId === get(object, 'fragment.id') || + p.objectId === get(object, 'fragment.collectionId') + if (roles.length > 0) { + return hasObject && roles.includes(p.role) + } + return hasObject + }) } +const isHandlingEditor = ({ user, object }) => + get(object, 'collection.handlingEditor.id') === user.id + module.exports = { + filterObjectData, parseAuthorsData, setPublicStatuses, - filterRefusedInvitations, - filterObjectData, getTeamsByPermissions, + filterRefusedInvitations, + // + isOwner, + isHandlingEditor, + getUserPermissions, + heIsInvitedToFragment, + hasPermissionForObject, } diff --git a/packages/component-user-manager/config/authsome-mode.js b/packages/component-user-manager/config/authsome-mode.js index 8a92a1301..20e0d6918 100644 --- a/packages/component-user-manager/config/authsome-mode.js +++ b/packages/component-user-manager/config/authsome-mode.js @@ -1,69 +1,8 @@ -const get = require('lodash/get') -const pickBy = require('lodash/pickBy') -const omit = require('lodash/omit') -const helpers = require('./authsome-helpers') - -async function teamPermissions(user, operation, object, context) { - const { models } = context - const permissions = ['handlingEditor', 'author', 'reviewer'] - const teams = await helpers.getTeamsByPermissions( - user.teams, - permissions, - context.models.Team, - ) - - let collectionsPermissions = await Promise.all( - teams.map(async team => { - let collection - if (team.object.type === 'collection') { - collection = await models.Collection.find(team.object.id) - } else if (team.object.type === 'fragment') { - const fragment = await models.Fragment.find(team.object.id) - collection = await models.Collection.find(fragment.collectionId) - } - if ( - collection.status === 'rejected' && - team.teamType.permissions === 'reviewer' - ) - return null - const collPerm = { - id: collection.id, - permission: team.teamType.permissions, - } - const objectType = get(object, 'type') - if (objectType === 'fragment') { - if (collection.fragments.includes(object.id)) - collPerm.fragmentId = object.id - else return null - } - - if (objectType === 'collection') - if (object.id !== collection.id) return null - return collPerm - }), - ) - collectionsPermissions = collectionsPermissions.filter(cp => cp !== null) - if (collectionsPermissions.length === 0) return false - - return { - filter: filterParam => { - if (!filterParam.length) { - return helpers.filterObjectData( - collectionsPermissions, - filterParam, - user, - ) - } +const config = require('config') +const { get, pickBy, omit } = require('lodash') - const collections = filterParam - .map(coll => - helpers.filterObjectData(collectionsPermissions, coll, user), - ) - .filter(Boolean) - return collections - }, - } -} +const statuses = config.get('statuses') +const helpers = require('./authsome-helpers') function unauthenticatedUser(operation, object) { // Public/unauthenticated users can GET /collections, filtered by 'published' @@ -111,133 +50,182 @@ function unauthenticatedUser(operation, object) { return false } +const publicStatusesPermissions = ['author', 'reviewer'] +const createPaths = ['/collections', '/collections/:collectionId/fragments'] + async function authenticatedUser(user, operation, object, context) { - // Allow the authenticated user to POST a collection (but not with a 'filtered' property) - if (operation === 'POST' && object.path === '/collections') { - return { - filter: collection => omit(collection, 'filtered'), + if (operation === 'GET') { + if (get(object, 'path') === '/collections') { + return { + filter: async collections => { + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) + return collections.filter(collection => { + if (collection.owners.includes(user.id)) { + return true + } + const collectionPermission = userPermissions.find( + p => p.objectId === collection.id, + ) + if (collectionPermission) { + return true + } + + const fragmentPermission = userPermissions.find(p => + collection.fragments.includes(p.objectId), + ) + if (fragmentPermission) { + return true + } + return false + }) + }, + } } - } - if ( - operation === 'POST' && - object.path === '/collections/:collectionId/fragments' - ) { - return true - } + if (object === '/users') { + return true + } - // allow authenticate owners full pass for a collection - if (get(object, 'type') === 'collection') { - if (operation === 'PATCH') { + if (get(object, 'type') === 'collection') { + if (helpers.isOwner({ user, object })) { + return true + } return { - filter: collection => omit(collection, 'filtered'), + filter: async collection => { + const status = get(collection, 'status') || 'draft' + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) + if (collection.owners.map(o => o.id).includes(user.id)) { + return collection + } + + const collectionPermission = userPermissions.find( + p => p.objectId === collection.id, + ) + if ( + publicStatusesPermissions.includes( + get(collectionPermission, 'role'), + ) + ) { + collection.visibleStatus = statuses[status].public + } + return collection + }, } } - if (object.owners.includes(user.id)) return true - const owner = object.owners.find(own => own.id === user.id) - if (owner !== undefined) return true - } - // Allow owners of a collection to GET its teams, e.g. - // GET /api/collections/1/teams - if (operation === 'GET' && get(object, 'path') === '/teams') { - const collectionId = get(object, 'params.collectionId') - if (collectionId) { - const collection = await context.models.Collection.find(collectionId) - if (collection.owners.includes(user.id)) { + if (get(object, 'type') === 'fragment') { + if (helpers.isOwner({ user, object })) { return true } - } - } - if ( - operation === 'GET' && - get(object, 'type') === 'team' && - get(object, 'object.type') === 'collection' - ) { - const collection = await context.models.Collection.find( - get(object, 'object.id'), - ) - if (collection.owners.includes(user.id)) { - return true - } - } + const userPermissions = await helpers.getUserPermissions({ + user, + Team: context.models.Team, + }) - // Advanced example - // Allow authenticated users to create a team based around a collection - // if they are one of the owners of this collection - if (['POST', 'PATCH'].includes(operation) && get(object, 'type') === 'team') { - if (get(object, 'object.type') === 'collection') { - const collection = await context.models.Collection.find( - get(object, 'object.id'), + const permission = userPermissions.find( + p => p.objectId === object.id || p.objectId === object.collectionId, ) - if (collection.owners.includes(user.id)) { + + if (!permission) return false + + return { + filter: fragment => { + // handle other roles + if (permission.role === 'reviewer') { + fragment.files = omit(fragment.files, ['coverLetter']) + fragment.authors = fragment.authors.map(a => omit(a, ['email'])) + } + return fragment + }, + } + } + + // allow HE to get reviewer invitations + if (get(object, 'fragment.type') === 'fragment') { + const collectionId = get(object, 'fragment.collectionId') + const collection = await context.models.Collection.find(collectionId) + + if (get(collection, 'handlingEditor.id') === user.id) { return true } } - } - // only allow the HE to create, delete an invitation, or get invitation details - if ( - ['POST', 'GET', 'DELETE'].includes(operation) && - get(object.collection, 'type') === 'collection' && - object.path.includes('invitations') - ) { - const collection = await context.models.Collection.find( - get(object.collection, 'id'), - ) - const handlingEditor = get(collection, 'handlingEditor') - if (!handlingEditor) return false - if (handlingEditor.id === user.id) return true - return false + if (get(object, 'type') === 'user') { + return true + } } - // only allow a reviewer and an HE to submit and to modify a recommendation - if ( - ['POST', 'PATCH'].includes(operation) && - object.path.includes('recommendations') - ) { - const authsomeObject = get(object, 'authsomeObject') - - const teams = await helpers.getTeamsByPermissions( - user.teams, - ['reviewer', 'handlingEditor'], - context.models.Team, - ) + if (operation === 'POST') { + // allow everytone to create manuscripts and versions + if (createPaths.includes(object.path)) { + return true + } - if (teams.length === 0) return false - const matchingTeam = teams.find( - team => team.object.id === authsomeObject.id, - ) + // allow HE to invite + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations' + ) { + return helpers.isHandlingEditor({ user, object }) + } - if (matchingTeam) return true - return false + // allow HE or assigned reviewers to recommend + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + roles: ['reviewer', 'handlingEditor'], + }) + } } - if (user.teams.length !== 0 && ['GET'].includes(operation)) { - const permissions = await teamPermissions(user, operation, object, context) - - if (permissions) { - return permissions + if (operation === 'PATCH') { + if (get(object, 'type') === 'collection') { + return helpers.isOwner({ user, object }) } - return false - } + if (get(object, 'type') === 'fragment') { + return helpers.isOwner({ user, object }) + } - if (get(object, 'type') === 'fragment') { - const fragment = object + // allow reviewer to patch his recommendation + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/recommendations/:recommendationId' + ) { + return helpers.hasPermissionForObject({ + user, + object, + Team: context.models.Team, + roles: ['reviewer'], + }) + } - if (fragment.owners.includes(user.id)) { + if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { return true } } - // A user can GET, DELETE and PATCH itself - if (get(object, 'type') === 'user' && get(object, 'id') === user.id) { - if (['GET', 'DELETE', 'PATCH'].includes(operation)) { - return true + if (operation === 'DELETE') { + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/invitations/:invitationId' + ) { + return helpers.isHandlingEditor({ user, object }) } } + // If no individual permissions exist (above), fallback to unauthenticated // user's permission return unauthenticatedUser(operation, object) diff --git a/packages/component-user-manager/src/FragmentsUsers.js b/packages/component-user-manager/src/FragmentsUsers.js index d1137db20..443397a6f 100644 --- a/packages/component-user-manager/src/FragmentsUsers.js +++ b/packages/component-user-manager/src/FragmentsUsers.js @@ -23,19 +23,12 @@ const FragmentsUsers = app => { * HTTP/1.1 200 OK * { * "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b", - * "type": "user", - * "admin": false, * "email": "email@example.com", - * "teams": [ - * "c576695a-7cda-4e27-8e9c-31f3a0e9d592" - * ], - * "username": "email@example.com", - * "fragments": [], - * "collections": [], - * "isConfirmed": false, - * "editorInChief": false, - * "handlingEditor": false, - * "passwordResetToken": "04590a2b7f6c1f37cb84881d529e516fa6fc309c205a07f1341b2bfaa6f2b46c" + * "firstName": "John", + * "lastName": "Smith", + * "affiliation": "MIT", + * "isSubmitting": true, + * "isCorresponding": false * } * @apiErrorExample {json} Invite user errors * HTTP/1.1 400 Bad Request diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js index 06740101e..8dc8df3ed 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/post.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js @@ -63,7 +63,7 @@ module.exports = models => async (req, res) => { user = await UserModel.find(user.id) fragment.authors = fragment.authors || [] - const match = fragment.authors.find(author => author.userId === user.id) + const match = fragment.authors.find(author => author.id === user.id) if (match) { return res.status(400).json({ diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index 3373de304..658f47bb6 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -96,17 +96,21 @@ const isOwner = ({ user: { id }, object }) => { return !!object.owners.find(own => own.id === id) } -const hasPermissionForObject = async ({ user, object, Team }) => { +const hasPermissionForObject = async ({ user, object, Team, roles = [] }) => { const userPermissions = await getUserPermissions({ user, Team, }) - return !!userPermissions.find( - p => + return !!userPermissions.find(p => { + const hasObject = p.objectId === get(object, 'fragment.id') || - p.objectId === get(object, 'fragment.collectionId'), - ) + p.objectId === get(object, 'fragment.collectionId') + if (roles.length > 0) { + return hasObject && roles.includes(p.role) + } + return hasObject + }) } const isHandlingEditor = ({ user, object }) => @@ -118,7 +122,6 @@ module.exports = { setPublicStatuses, getTeamsByPermissions, filterRefusedInvitations, - // isOwner, isHandlingEditor, getUserPermissions, diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 37e4ff7da..20e0d6918 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -185,6 +185,7 @@ async function authenticatedUser(user, operation, object, context) { user, object, Team: context.models.Team, + roles: ['reviewer', 'handlingEditor'], }) } } @@ -207,6 +208,7 @@ async function authenticatedUser(user, operation, object, context) { user, object, Team: context.models.Team, + roles: ['reviewer'], }) } -- GitLab