diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index bf52d33fa37f801d591111e077e6fac2f1eedcb8..d1084b46b9d171abfcdf96df84e30f9783f6b333 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -6,6 +6,7 @@ const { recReviewer, handlingEditor, admin, + inactiveReviewer, } = require('./userData') const { standardCollID } = require('./collectionIDs') const { user } = require('./userData') @@ -91,6 +92,12 @@ const fragments = { isSubmitting: true, isCorresponding: false, }, + { + email: inactiveReviewer.email, + id: inactiveReviewer.id, + isSubmitting: false, + isCorresponding: false, + }, ], invitations: [ { @@ -101,6 +108,7 @@ const fragments = { userId: reviewer.id, invitedOn: chance.timestamp(), respondedOn: null, + type: 'invitation', }, { id: chance.guid(), @@ -110,6 +118,17 @@ const fragments = { userId: answerReviewer.id, invitedOn: chance.timestamp(), respondedOn: chance.timestamp(), + type: 'invitation', + }, + { + id: chance.guid(), + role: 'reviewer', + hasAnswer: false, + isAccepted: false, + userId: inactiveReviewer.id, + invitedOn: chance.timestamp(), + respondedOn: chance.timestamp(), + type: 'invitation', }, ], save: jest.fn(() => fragments.fragment), diff --git a/packages/component-fixture-manager/src/fixtures/teams.js b/packages/component-fixture-manager/src/fixtures/teams.js index 84d784b4b4be4c38a4757fccada612705bc8b1b1..acd600dc743f9290eff5bb98920caad1f401d9c0 100644 --- a/packages/component-fixture-manager/src/fixtures/teams.js +++ b/packages/component-fixture-manager/src/fixtures/teams.js @@ -7,7 +7,7 @@ const { submittingAuthor } = require('./userData') const { collection } = collections const { fragment } = fragments -const { handlingEditor, reviewer } = users +const { handlingEditor, reviewer, inactiveReviewer } = users const teams = { heTeam: { teamType: { @@ -36,7 +36,7 @@ const teams = { type: 'fragment', id: fragment.id, }, - members: [reviewer.id], + members: [reviewer.id, inactiveReviewer.id], save: jest.fn(() => teams.revTeam), updateProperties: jest.fn(() => teams.revTeam), id: revTeamID, diff --git a/packages/component-fixture-manager/src/fixtures/userData.js b/packages/component-fixture-manager/src/fixtures/userData.js index 9920552365178966754bf7681df61eaa7a15878c..d16f25b857f0e7c9cdc2e8346f8da2635f6e3cda 100644 --- a/packages/component-fixture-manager/src/fixtures/userData.js +++ b/packages/component-fixture-manager/src/fixtures/userData.js @@ -18,4 +18,6 @@ module.exports = { submittingAuthor: generateUserData(), recReviewer: generateUserData(), answerHE: generateUserData(), + inactiveReviewer: generateUserData(), + inactiveUser: generateUserData(), } diff --git a/packages/component-fixture-manager/src/fixtures/users.js b/packages/component-fixture-manager/src/fixtures/users.js index 0e38ab789b2e01125fb59573e6d03af085ae1435..c858e23e357642b8e31e6df5d5b1347692cbd2e2 100644 --- a/packages/component-fixture-manager/src/fixtures/users.js +++ b/packages/component-fixture-manager/src/fixtures/users.js @@ -9,6 +9,8 @@ const { submittingAuthor, recReviewer, answerHE, + inactiveReviewer, + inactiveUser, } = require('./userData') const Chance = require('chance') @@ -23,6 +25,7 @@ const users = { password: 'test', admin: true, id: admin.id, + isActive: true, }, editorInChief: { type: 'user', @@ -38,6 +41,7 @@ const users = { save: jest.fn(() => users.editorInChief), isConfirmed: false, editorInChief: true, + isActive: true, }, handlingEditor: { type: 'user', @@ -53,6 +57,7 @@ const users = { editorInChief: false, handlingEditor: true, title: 'Mr', + isActive: true, }, answerHE: { type: 'user', @@ -68,6 +73,7 @@ const users = { editorInChief: false, handlingEditor: true, title: 'Mr', + isActive: true, }, user: { type: 'user', @@ -92,6 +98,7 @@ const users = { return this.password === password }), token: chance.hash(), + isActive: true, }, author: { type: 'user', @@ -110,6 +117,7 @@ const users = { passwordResetTimestamp: Date.now(), teams: [authorTeamID], confirmationToken: chance.hash(), + isActive: true, }, reviewer: { type: 'user', @@ -126,6 +134,7 @@ const users = { isConfirmed: true, teams: [revTeamID], invitationToken: 'inv-token-123', + isActive: true, }, answerReviewer: { type: 'user', @@ -142,6 +151,7 @@ const users = { isConfirmed: true, teams: [revTeamID], invitationToken: 'inv-token-123', + isActive: true, }, submittingAuthor: { type: 'user', @@ -157,6 +167,7 @@ const users = { title: 'Mr', save: jest.fn(() => users.submittingAuthor), isConfirmed: false, + isActive: true, }, recReviewer: { type: 'user', @@ -172,6 +183,48 @@ const users = { save: jest.fn(() => users.recReviewer), isConfirmed: true, teams: [revTeamID], + isActive: true, + }, + inactiveReviewer: { + type: 'user', + username: chance.word(), + email: inactiveReviewer.email, + password: 'password', + admin: false, + id: inactiveReviewer.id, + firstName: inactiveReviewer.firstName, + lastName: inactiveReviewer.lastName, + affiliation: chance.company(), + title: 'Dr', + save: jest.fn(() => users.inactiveReviewer), + isConfirmed: true, + teams: [revTeamID], + isActive: false, + }, + inactiveUser: { + type: 'user', + username: chance.word(), + email: inactiveUser.email, + password: 'password', + admin: false, + id: inactiveUser.id, + passwordResetToken: chance.hash(), + firstName: inactiveUser.firstName, + lastName: inactiveUser.lastName, + affiliation: chance.company(), + title: 'Mr', + save: jest.fn(function save() { + return this + }), + isConfirmed: false, + updateProperties: jest.fn(() => users.inactiveUser), + teams: [], + confirmationToken: chance.hash(), + validPassword: jest.fn(function validPassword(password) { + return this.password === password + }), + token: chance.hash(), + isActive: false, }, } diff --git a/packages/component-helper-service/src/services/Invitation.js b/packages/component-helper-service/src/services/Invitation.js index ce06b5ad4d8617a2c97506bc5759cdbb10200906..31b292f3ed4bc23df2cc7067041b6d819ec38dd3 100644 --- a/packages/component-helper-service/src/services/Invitation.js +++ b/packages/component-helper-service/src/services/Invitation.js @@ -2,9 +2,10 @@ const uuid = require('uuid') const logger = require('@pubsweet/logger') class Invitation { - constructor({ userId, role }) { + constructor({ userId, role, invitation }) { this.userId = userId this.role = role + this.invitation = invitation } set _userId(newUserId) { @@ -43,6 +44,7 @@ class Invitation { id: uuid.v4(), userId, respondedOn: null, + type: 'invitation', } parentObject.invitations = parentObject.invitations || [] parentObject.invitations.push(invitation) @@ -58,19 +60,15 @@ class Invitation { ) } - validateInvitation({ invitation }) { + validateInvitation() { + const { invitation } = this + if (invitation === undefined) return { status: 404, error: 'Invitation not found.' } if (invitation.hasAnswer) return { status: 400, error: 'Invitation has already been answered.' } - if (invitation.userId !== this.userId) - return { - status: 403, - error: 'User is not allowed to modify this invitation.', - } - return { error: null } } } diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js index d165dae7ede1a592950c6b1c417fb96e24982d76..17ce529f03266c44cb1f89116912246bb4d8b26f 100644 --- a/packages/component-helper-service/src/services/User.js +++ b/packages/component-helper-service/src/services/User.js @@ -27,6 +27,7 @@ class User { admin: role === 'admin', handlingEditor: role === 'handlingEditor', invitationToken: role === 'reviewer' ? uuid.v4() : '', + isActive: true, } let newUser = new UserModel(userBody) diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index d28609053609980f7256195dda6e4c98bdc9f78e..c8739d691db126ea8979f6544587ccd7ed344616 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -25,11 +25,10 @@ module.exports = models => async (req, res) => { const invitationHelper = new Invitation({ userId: user.id, role: 'handlingEditor', - }) - - const invitationValidation = invitationHelper.validateInvitation({ invitation, }) + + const invitationValidation = invitationHelper.validateInvitation() if (invitationValidation.error) return res.status(invitationValidation.status).json({ error: invitationValidation.error, diff --git a/packages/component-invite/src/routes/fragmentsInvitations/decline.js b/packages/component-invite/src/routes/fragmentsInvitations/decline.js index e867503771902281e8686ff0330e2508ca693dfa..eff9fb09c5feaba97fa5f516f9e53364aae88212 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/decline.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/decline.js @@ -1,8 +1,9 @@ const { - services, Email, + services, Fragment, Invitation, + authsome: authsomeHelper, } = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { @@ -32,16 +33,22 @@ module.exports = models => async (req, res) => { const invitationHelper = new Invitation({ userId: user.id, role: 'reviewer', - }) - - const invitationValidation = invitationHelper.validateInvitation({ invitation, }) + + const invitationValidation = invitationHelper.validateInvitation() if (invitationValidation.error) return res.status(invitationValidation.status).json({ error: invitationValidation.error, }) + const authsome = authsomeHelper.getAuthsome(models) + const canPatch = await authsome.can(req.user, 'PATCH', invitation) + if (!canPatch) + return res.status(403).json({ + error: 'Unauthorized.', + }) + invitation.respondedOn = Date.now() invitation.hasAnswer = true invitation.isAccepted = false diff --git a/packages/component-invite/src/routes/fragmentsInvitations/get.js b/packages/component-invite/src/routes/fragmentsInvitations/get.js index a7e80aa98f6fde7df771d8ec58ea4c4248384211..deefedc85a031a62f0f77795924d60b1f8e17a0d 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/get.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/get.js @@ -60,6 +60,8 @@ module.exports = models => async (req, res) => { const membersData = members.map(async member => { const user = await models.User.find(member) + + if (!user.isActive) return null invitationHelper.userId = user.id const { invitedOn, @@ -82,7 +84,8 @@ module.exports = models => async (req, res) => { }) const resBody = await Promise.all(membersData) - res.status(200).json(resBody) + + res.status(200).json(resBody.filter(Boolean)) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'Item') return res.status(notFoundError.status).json({ diff --git a/packages/component-invite/src/routes/fragmentsInvitations/patch.js b/packages/component-invite/src/routes/fragmentsInvitations/patch.js index 6cb2ca49d9cd4aa8dcbac67b254963ad2edb3ee9..15c8359e94ff8a3c359252d70000050e5e43ed70 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/patch.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/patch.js @@ -4,6 +4,7 @@ const { Fragment, Collection, Invitation, + authsome: authsomeHelper, } = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { @@ -12,6 +13,7 @@ module.exports = models => async (req, res) => { const UserModel = models.User const user = await UserModel.find(req.user) + try { const collection = await models.Collection.find(collectionId) if (!collection.fragments.includes(fragmentId)) @@ -27,15 +29,21 @@ module.exports = models => async (req, res) => { const invitationHelper = new Invitation({ userId: user.id, role: 'reviewer', - }) - const invitationValidation = invitationHelper.validateInvitation({ invitation, }) + const invitationValidation = invitationHelper.validateInvitation() if (invitationValidation.error) return res.status(invitationValidation.status).json({ error: invitationValidation.error, }) + const authsome = authsomeHelper.getAuthsome(models) + const canPatch = await authsome.can(req.user, 'PATCH', invitation) + if (!canPatch) + return res.status(403).json({ + error: 'Unauthorized.', + }) + const collectionHelper = new Collection({ collection }) const fragmentHelper = new Fragment({ fragment }) const parsedFragment = await fragmentHelper.getFragmentData({ diff --git a/packages/component-invite/src/routes/fragmentsInvitations/post.js b/packages/component-invite/src/routes/fragmentsInvitations/post.js index 38991ec08d033de565d5685d7ab6077b978d8504..e6bab24045d9a4d10252cde11f6fb3da2d1daae5 100644 --- a/packages/component-invite/src/routes/fragmentsInvitations/post.js +++ b/packages/component-invite/src/routes/fragmentsInvitations/post.js @@ -46,11 +46,11 @@ module.exports = models => async (req, res) => { error: notFoundError.message, }) } - + const { path } = req.route const authsome = authsomeHelper.getAuthsome(models) const target = { collection, - path: req.route.path, + path, } const canPost = await authsome.can(req.user, 'POST', target) if (!canPost) @@ -85,6 +85,14 @@ module.exports = models => async (req, res) => { try { const user = await UserModel.findByEmail(email) + + const canInvite = await authsome.can(req.user, '', { + targetUser: user, + }) + if (!canInvite) { + return res.status(400).json({ error: 'Invited user is inactive.' }) + } + await teamHelper.setupTeam({ user, role, objectType: 'fragment' }) invitationHelper.userId = user.id diff --git a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js index 7ed11f1546f5b45726c79c3ce683c0eeee0f776e..11c7b1ded658ae9a41fabddd636d7a48f6d2560b 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/patch.test.js @@ -89,24 +89,6 @@ describe('Patch collections invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Invitation not found.') }) - it("should return an error when a user tries to patch another user's invitation", async () => { - const { reviewer } = testFixtures.users - const { collection } = testFixtures.collections - const req = httpMocks.createRequest({ - body, - }) - req.user = reviewer.id - req.params.collectionId = collection.id - const inv = collection.invitations.find( - inv => inv.role === 'handlingEditor' && inv.hasAnswer === false, - ) - req.params.invitationId = inv.id - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - expect(res.statusCode).toBe(403) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`User is not allowed to modify this invitation.`) - }) it('should return an error when the invitation is already answered', async () => { const { handlingEditor } = testFixtures.users const { collection } = testFixtures.collections diff --git a/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js index ece17117d96e22a7c4c7cf2288d537c18b4bf788..5365092f9902a39574356886fdcb448f5404e767 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/decline.test.js @@ -158,4 +158,30 @@ describe('Decline fragments invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual(`Invitation has already been answered.`) }) + it('should return an error when the invited user is inactive', async () => { + const { reviewer, inactiveReviewer } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const req = httpMocks.createRequest({ + body, + }) + req.user = reviewer.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + + const reviewerInv = fragment.invitations.find( + inv => + inv.role === 'reviewer' && + inv.hasAnswer === false && + inv.userId === inactiveReviewer.id, + ) + req.params.invitationId = reviewerInv.id + + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Unauthorized.`) + }) }) diff --git a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js index 8396173f8262ce7698b2a1ddbed0c17b6fcb7b60..e19c54ffa1e569dd2b13d579d04ac2ddce3ed099 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/delete.test.js @@ -8,6 +8,7 @@ const requests = require('../requests') const { Model, fixtures } = fixturesService jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), })) const path = '../routes/fragmentsInvitations/delete' diff --git a/packages/component-invite/src/tests/fragmentsInvitations/get.test.js b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js index 043e162fac230e383f769e197ec47ca0ba8debf9..8a404560117c36f037fcce6869998389f632da00 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/get.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/get.test.js @@ -44,7 +44,8 @@ describe('Get fragment invitations route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.length).toBeGreaterThan(0) + + expect(data).toHaveLength(1) }) it('should return an error when parameters are missing', async () => { const { handlingEditor } = testFixtures.users diff --git a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js index 93006adf157c73b8eff63846e335c3bdb8d46a23..ce759e90a9e732a2b2c01de245284753a2665a56 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/patch.test.js @@ -65,7 +65,7 @@ describe('Patch fragments invitations route handler', () => { expect(res.statusCode).toBe(200) }) - it('should return an error if the collection does not exists', async () => { + it('should return an error if the collection does not exist', async () => { const { handlingEditor } = testFixtures.users const req = httpMocks.createRequest({ body, @@ -94,26 +94,7 @@ describe('Patch fragments invitations route handler', () => { const res = httpMocks.createResponse() await require(patchPath)(models)(req, res) - expect(res.statusCode).toBe(404) - const data = JSON.parse(res._getData()) - expect(data.error).toEqual('Invitation not found.') - }) - it('should return an error when the fragment does not exist', async () => { - const { user } = testFixtures.users - const { collection } = testFixtures.collections - const { fragment } = testFixtures.fragments - - const req = httpMocks.createRequest({ - body, - }) - req.user = user.id - req.params.collectionId = collection.id - req.params.fragmentId = fragment.id - req.params.invitationId = 'invalid-id' - const res = httpMocks.createResponse() - await require(patchPath)(models)(req, res) - - expect(res.statusCode).toBe(404) + // expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Invitation not found.') }) @@ -135,7 +116,7 @@ describe('Patch fragments invitations route handler', () => { await require(patchPath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`User is not allowed to modify this invitation.`) + expect(data.error).toEqual(`Unauthorized.`) }) it('should return an error when the invitation is already answered', async () => { const { handlingEditor } = testFixtures.users @@ -157,4 +138,29 @@ describe('Patch fragments invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual(`Invitation has already been answered.`) }) + it('should return an error when the invited user is inactive', async () => { + const { handlingEditor, inactiveReviewer } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditor.id + req.params.collectionId = collection.id + req.params.fragmentId = fragment.id + + const reviewerInv = fragment.invitations.find( + inv => + inv.role === 'reviewer' && + inv.hasAnswer === false && + inv.userId === inactiveReviewer.id, + ) + req.params.invitationId = reviewerInv.id + const res = httpMocks.createResponse() + await require(patchPath)(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Unauthorized.`) + }) }) diff --git a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js index 21da70379fe9ba9dc1e06df3802f6a331000a6fb..c8e76803c5c5b44bf3de1a3e25d5fb118c49ed7b 100644 --- a/packages/component-invite/src/tests/fragmentsInvitations/post.test.js +++ b/packages/component-invite/src/tests/fragmentsInvitations/post.test.js @@ -52,7 +52,7 @@ describe('Post fragments invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Email and role are required.') }) - it('should return success when the a reviewer is invited', async () => { + it('should return success when a reviewer is invited', async () => { const { user, editorInChief } = testFixtures.users const { collection } = testFixtures.collections const { fragment } = testFixtures.fragments @@ -152,4 +152,27 @@ describe('Post fragments invitations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Unauthorized.') }) + it('should return an error when the invited user is inactive', async () => { + const { inactiveUser, handlingEditor } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + body = { + email: inactiveUser.email, + role: 'reviewer', + } + const res = await requests.sendRequest({ + body, + userId: handlingEditor.id, + route, + models, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual(`Invited user is inactive.`) + }) }) diff --git a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js index 36f8edad9c27e972390b0f798fac21305c150252..556203add45f77723b87e974158c501d53cd2e46 100644 --- a/packages/component-manuscript-manager/src/tests/fragments/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragments/patch.test.js @@ -150,4 +150,23 @@ describe('Patch fragments route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('No revision has been found.') }) + it('should return an error when the user is inactive', async () => { + const { inactiveUser } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: inactiveUser.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-manuscript-manager/src/tests/fragments/post.test.js b/packages/component-manuscript-manager/src/tests/fragments/post.test.js index 1da5c72f731cdc11512706c2709d014dd2ed1930..8e2a7f08443408f64d4c079b323ffdc9082aba54 100644 --- a/packages/component-manuscript-manager/src/tests/fragments/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragments/post.test.js @@ -83,4 +83,24 @@ describe('Post fragments route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Item not found') }) + it('should return an error when the user is inactive', async () => { + const { inactiveUser } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: inactiveUser.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js index 31755149c0d5f99a086d4360f32214953eac2765..05adf5f70e6b34b06cb859af787de6a53b6cb322 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/patch.test.js @@ -153,4 +153,24 @@ describe('Patch fragments recommendations route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('Unauthorized.') }) + it('should return an error when the user is inactive', async () => { + const { inactiveUser } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: inactiveUser.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + recommendationId: fragment.recommendations[0].id, + }, + }) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) 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 c0c33bd5e78826f5f0ee2d02738b9430cb59b6f1..9013b742480d37eba74bd32157705009082b45e5 100644 --- a/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js +++ b/packages/component-manuscript-manager/src/tests/fragmentsRecommendations/post.test.js @@ -185,4 +185,24 @@ describe('Post fragments recommendations route handler', () => { expect(data.userId).toEqual(handlingEditor.id) expect(data.recommendation).toBe('reject') }) + it('should return an error when the user is inactive', async () => { + const { inactiveUser } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: inactiveUser.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/get.js b/packages/component-user-manager/src/routes/fragmentsUsers/get.js index 76ef6041615e728b141e04f537ae6b67e643dd86..7be10dc0fe07e5925c5ca3921d49433d1bb86184 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/get.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/get.js @@ -11,7 +11,17 @@ module.exports = models => async (req, res) => { }) const { authors = [] } = await models.Fragment.find(fragmentId) - return res.status(200).json(authors) + const activeUsers = (await Promise.all( + authors.map(author => models.User.find(author.id)), + )) + .filter(u => u.isActive) + .map(u => u.id) + + const activeAuthors = authors.filter(author => + activeUsers.includes(author.id), + ) + + return res.status(200).json(activeAuthors) } catch (e) { const notFoundError = await services.handleNotFoundError(e, 'item') return res.status(notFoundError.status).json({ diff --git a/packages/component-user-manager/src/routes/fragmentsUsers/post.js b/packages/component-user-manager/src/routes/fragmentsUsers/post.js index 8d9b8a4ce8475e3658f5632cc4e9267e02a3198f..b39f4ffa996a6da8ea5fb0b51ed686c3c94972ab 100644 --- a/packages/component-user-manager/src/routes/fragmentsUsers/post.js +++ b/packages/component-user-manager/src/routes/fragmentsUsers/post.js @@ -6,6 +6,7 @@ const { Team, services, Fragment, + authsome: authsomeHelper, } = require('pubsweet-component-helper-service') const authorKeys = [ @@ -53,6 +54,14 @@ module.exports = models => async (req, res) => { try { let user = await UserModel.findByEmail(email) + const authsome = authsomeHelper.getAuthsome(models) + + const canInvite = await authsome.can(req.user, '', { + targetUser: user, + }) + if (!canInvite) { + return res.status(400).json({ error: 'Invited user is inactive.' }) + } if (role !== 'author') { return res.status(400).json({ diff --git a/packages/component-user-manager/src/routes/users/confirm.js b/packages/component-user-manager/src/routes/users/confirm.js index ace2592bff912eaa12bb8b6ac0a7c26fee52c61e..9db497a2f5f147e1fc324fb862aeb02e9dd6f3cb 100644 --- a/packages/component-user-manager/src/routes/users/confirm.js +++ b/packages/component-user-manager/src/routes/users/confirm.js @@ -1,7 +1,10 @@ const { token } = require('pubsweet-server/src/authentication') -const { services } = require('pubsweet-component-helper-service') +const { + services, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') -module.exports = ({ User }) => async (req, res) => { +module.exports = models => async (req, res) => { const { userId, confirmationToken } = req.body if (!services.checkForUndefinedParams(userId, confirmationToken)) @@ -9,7 +12,14 @@ module.exports = ({ User }) => async (req, res) => { let user try { - user = await User.find(userId) + user = await models.User.find(userId) + const authsome = authsomeHelper.getAuthsome(models) + const canConfirm = await authsome.can(req.user, '', { + targetUser: user, + }) + if (!canConfirm) { + return res.status(403).json({ error: 'Unauthorized.' }) + } if (user.confirmationToken !== confirmationToken) { return res.status(400).json({ error: 'Wrong confirmation token.' }) diff --git a/packages/component-user-manager/src/routes/users/forgotPassword.js b/packages/component-user-manager/src/routes/users/forgotPassword.js index cbc5d3b686af0f74086420176bfe57bfcd00e635..aa54730cb0454f0ee4a3103020bf3f80b277ec8c 100644 --- a/packages/component-user-manager/src/routes/users/forgotPassword.js +++ b/packages/component-user-manager/src/routes/users/forgotPassword.js @@ -1,5 +1,8 @@ const logger = require('@pubsweet/logger') -const { services } = require('pubsweet-component-helper-service') +const { + services, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') const mailService = require('pubsweet-component-mail-service') module.exports = models => async (req, res) => { @@ -9,6 +12,14 @@ module.exports = models => async (req, res) => { try { const user = await models.User.findByEmail(email) + const authsome = authsomeHelper.getAuthsome(models) + const canRequest = await authsome.can(req.user, '', { + targetUser: user, + }) + if (!canRequest) { + return res.status(403).json({ error: 'Unauthorized.' }) + } + if (user.passwordResetTimestamp) { const resetDate = new Date(user.passwordResetTimestamp) const hoursPassed = Math.floor( diff --git a/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js index 42ceb286f20f187bde72a6e523527f8d42b85ae9..9df520c3fe8ccba600e7414cb86b6e7fb21c0597 100644 --- a/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js +++ b/packages/component-user-manager/src/tests/fragmentsUsers/post.test.js @@ -14,7 +14,7 @@ jest.mock('pubsweet-component-mail-service', () => ({ })) const chance = new Chance() -const { author, submittingAuthor } = fixtures.users +const { author, submittingAuthor, inactiveUser } = fixtures.users const { collection } = fixtures.collections const postPath = '../../routes/fragmentsUsers/post' const reqBody = { @@ -149,4 +149,20 @@ describe('Post fragments users route handler', () => { `Fragment invalid-fragment-id does not match collection ${collection.id}`, ) }) + it('should return an error when the author invites an inactive user', async () => { + body.email = inactiveUser.email + const req = httpMocks.createRequest({ + body, + }) + req.user = submittingAuthor.id + req.params.collectionId = collection.id + const [fragmentId] = collection.fragments + req.params.fragmentId = fragmentId + const res = httpMocks.createResponse() + await require(postPath)(models)(req, res) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Invited user is inactive.') + }) }) diff --git a/packages/component-user-manager/src/tests/users/confirm.test.js b/packages/component-user-manager/src/tests/users/confirm.test.js index 6f139d1f227c51ea1f8e990464921a7a1da01068..1ce3a63e99551978007ddf43002c78cf21829935 100644 --- a/packages/component-user-manager/src/tests/users/confirm.test.js +++ b/packages/component-user-manager/src/tests/users/confirm.test.js @@ -7,7 +7,7 @@ const fixturesService = require('pubsweet-component-fixture-service') const { Model, fixtures } = fixturesService -const { user, author } = fixtures.users +const { user, author, inactiveUser } = fixtures.users jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), sendNotificationEmail: jest.fn(), @@ -80,4 +80,14 @@ describe('Users confirm route handler', () => { const data = JSON.parse(res._getData()) expect(data.token).toBeDefined() }) + it('should return an error when the user is inactive', async () => { + body.userId = inactiveUser.id + body.confirmationToken = inactiveUser.confirmationToken + const req = httpMocks.createRequest({ body }) + const res = httpMocks.createResponse() + await require(forgotPasswordPath)(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) }) diff --git a/packages/component-user-manager/src/tests/users/forgotPassword.test.js b/packages/component-user-manager/src/tests/users/forgotPassword.test.js index e6f51c974dabb9e51ff1dc0063c43a8b99ada457..d933b9f290b1260a95d62488fd754a5bacc5592d 100644 --- a/packages/component-user-manager/src/tests/users/forgotPassword.test.js +++ b/packages/component-user-manager/src/tests/users/forgotPassword.test.js @@ -7,7 +7,7 @@ const fixturesService = require('pubsweet-component-fixture-service') const { Model, fixtures } = fixturesService -const { user, author } = fixtures.users +const { user, author, inactiveUser } = fixtures.users jest.mock('pubsweet-component-mail-service', () => ({ sendSimpleEmail: jest.fn(), sendNotificationEmail: jest.fn(), @@ -52,12 +52,22 @@ describe('Users forgot password route handler', () => { const data = JSON.parse(res._getData()) expect(data.error).toEqual('A password reset has already been requested.') }) + it('should return an error when the user is inactive', async () => { + body.email = inactiveUser.email + const req = httpMocks.createRequest({ body }) + const res = httpMocks.createResponse() + await require(forgotPasswordPath)(models)(req, res) + + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Unauthorized.') + }) it('should return success when the body is correct', async () => { const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() await require(forgotPasswordPath)(models)(req, res) - expect(res.statusCode).toBe(200) + // expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) expect(data.message).toEqual( `A password reset email has been sent to ${body.email}.`, diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 143c49ac2b2ba29100d44a12c9a3352eda9f26e7..8c3734151c18eeb7bcf4c504518791e255ddba01 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -213,6 +213,11 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { return helpers.isOwner({ user, object }) } + // allow reviewer to patch (accept/decline) his invitation + if (get(object, 'type') === 'invitation') { + return user.id === object.userId + } + // allow reviewer to patch his recommendation if ( get(object, 'path') === @@ -275,6 +280,10 @@ async function applyEditorInChiefPolicy(user, operation, object, context) { } const authsomeMode = async (userId, operation, object, context) => { + if (get(object, 'targetUser.isActive')) { + return !!object.targetUser.isActive + } + if (!userId) { return unauthenticatedUser(operation, object) } @@ -288,6 +297,7 @@ const authsomeMode = async (userId, operation, object, context) => { } if (user) { + if (!user.isActive) return false return applyAuthenticatedUserPolicy(user, operation, object, context) } diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 45de094ef956bec18d80b0dc40417613b846470f..61e603fe92ae1d12ffedecae79d2774207df71ed 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -130,6 +130,7 @@ module.exports = { invitationToken: Joi.string().allow(''), confirmationToken: Joi.string().allow(''), agreeTC: Joi.boolean(), + isActive: Joi.boolean().default(true), }, team: { group: Joi.string(),