From 01151c08f6be31eefb2978f4c741645047a11020 Mon Sep 17 00:00:00 2001 From: Sebastian Mihalache <sebastian.mihalache@gmail.con> Date: Mon, 16 Jul 2018 13:57:11 +0300 Subject: [PATCH] feat: add inactive rule to users --- .../src/fixtures/fragments.js | 19 +++++++ .../src/fixtures/teams.js | 4 +- .../src/fixtures/userData.js | 2 + .../src/fixtures/users.js | 53 +++++++++++++++++++ .../src/services/Invitation.js | 14 +++-- .../src/services/User.js | 1 + .../routes/collectionsInvitations/patch.js | 5 +- .../routes/fragmentsInvitations/decline.js | 15 ++++-- .../src/routes/fragmentsInvitations/get.js | 5 +- .../src/routes/fragmentsInvitations/patch.js | 12 ++++- .../src/routes/fragmentsInvitations/post.js | 12 ++++- .../collectionsInvitations/patch.test.js | 18 ------- .../fragmentsInvitations/decline.test.js | 26 +++++++++ .../tests/fragmentsInvitations/delete.test.js | 1 + .../tests/fragmentsInvitations/get.test.js | 3 +- .../tests/fragmentsInvitations/patch.test.js | 50 +++++++++-------- .../tests/fragmentsInvitations/post.test.js | 25 ++++++++- .../src/tests/fragments/patch.test.js | 19 +++++++ .../src/tests/fragments/post.test.js | 20 +++++++ .../fragmentsRecommendations/patch.test.js | 20 +++++++ .../fragmentsRecommendations/post.test.js | 20 +++++++ .../src/routes/fragmentsUsers/get.js | 12 ++++- .../src/routes/fragmentsUsers/post.js | 9 ++++ .../src/routes/users/confirm.js | 16 ++++-- .../src/routes/users/forgotPassword.js | 13 ++++- .../src/tests/fragmentsUsers/post.test.js | 18 ++++++- .../src/tests/users/confirm.test.js | 12 ++++- .../src/tests/users/forgotPassword.test.js | 14 ++++- packages/xpub-faraday/config/authsome-mode.js | 10 ++++ packages/xpub-faraday/config/validations.js | 1 + 30 files changed, 376 insertions(+), 73 deletions(-) diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index bf52d33fa..d1084b46b 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 84d784b4b..acd600dc7 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 992055236..d16f25b85 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 0e38ab789..c858e23e3 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 ce06b5ad4..31b292f3e 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 d165dae7e..17ce529f0 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 d28609053..c8739d691 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 e86750377..eff9fb09c 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 a7e80aa98..deefedc85 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 6cb2ca49d..15c8359e9 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 38991ec08..e6bab2404 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 7ed11f154..11c7b1ded 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 ece17117d..5365092f9 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 8396173f8..e19c54ffa 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 043e162fa..8a4045601 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 93006adf1..ce759e90a 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 21da70379..c8e76803c 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 36f8edad9..556203add 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 1da5c72f7..8e2a7f084 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 31755149c..05adf5f70 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 c0c33bd5e..9013b7424 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 76ef60416..7be10dc0f 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 8d9b8a4ce..b39f4ffa9 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 ace2592bf..9db497a2f 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 cbc5d3b68..aa54730cb 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 42ceb286f..9df520c3f 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 6f139d1f2..1ce3a63e9 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 e6f51c974..d933b9f29 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 143c49ac2..8c3734151 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 35ebdd732..393cc2251 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -129,6 +129,7 @@ module.exports = { invitationToken: Joi.string().allow(''), confirmationToken: Joi.string().allow(''), agreeTC: Joi.boolean(), + isActive: Joi.boolean().default(true), }, team: { group: Joi.string(), -- GitLab