diff --git a/packages/component-invite/src/controllers/assignCollectionRole.js b/packages/component-invite/src/controllers/assignCollectionRole.js index aa194324155156c40f30ed7945d2118a51876a8b..c3f8fd95e6e144ab1694120492e15234fe683387 100644 --- a/packages/component-invite/src/controllers/assignCollectionRole.js +++ b/packages/component-invite/src/controllers/assignCollectionRole.js @@ -1,6 +1,7 @@ const logger = require('@pubsweet/logger') const config = require('config') const helpers = require('../helpers/helpers') +const teamHelper = require('../helpers/Team') const mailService = require('pubsweet-component-mail-service') const configRoles = config.get('roles') @@ -29,6 +30,27 @@ module.exports = async ( .json({ error: `Role ${role} cannot be set on collections` }) } + if (!reqUser.editorInChief && reqUser.teams === undefined) { + return res + .status(403) + .json({ error: `User ${reqUser.username} is not part of any teams` }) + } else if (reqUser.editorInChief === false) { + const matchingTeams = await teamHelper.getMatchingTeams( + reqUser.teams, + models.Team, + collectionId, + role, + ) + + if (matchingTeams.length === 0) { + return res.status(403).json({ + error: `User ${ + reqUser.email + } cannot invite a ${role} to ${collectionId}`, + }) + } + } + try { await models.Collection.find(collectionId) } catch (e) { @@ -40,7 +62,6 @@ module.exports = async ( try { let user = await models.User.findByEmail(email) - user.roles.push(role) const assignation = { type: role, hasAnswer: false, @@ -56,8 +77,9 @@ module.exports = async ( url, ) - // TODO: create a team and add the team id to the user's teams array + await teamHelper.setupManuscriptTeam(models, user, collectionId, role) + user = await models.User.find(user) return res.status(200).json(user) } catch (e) { const notFoundError = await helpers.handleNotFoundError(e, 'user') diff --git a/packages/component-invite/src/controllers/inviteGlobalRole.js b/packages/component-invite/src/controllers/inviteGlobalRole.js index ea05a8d5c09bf0c2913a9a0c7c55a63b0f33c469..a3f7e57a828f8bcc73bc532d5609899b2d3290b7 100644 --- a/packages/component-invite/src/controllers/inviteGlobalRole.js +++ b/packages/component-invite/src/controllers/inviteGlobalRole.js @@ -1,15 +1,25 @@ const logger = require('@pubsweet/logger') const helpers = require('../helpers/helpers') const mailService = require('pubsweet-component-mail-service') +const config = require('config') -module.exports = async (body, UserModel, res, url) => { +const configRoles = config.get('roles') + +module.exports = async (body, models, res, url) => { const { email, role, firstName, lastName, affiliation, title } = body + if (!configRoles.inviteRights.admin.includes(role)) { + logger.error(`admin ${email} tried to invite a ${role}`) + return res + .status(403) + .json({ error: `admin tried to invite an invalid role: ${role}` }) + } + try { - const user = await UserModel.findByEmail(email) + const user = await models.User.findByEmail(email) if (user) { - logger.error('someone tried to invite existing user') + logger.error(`admin tried to invite existing user: ${email}`) return res.status(400).json({ error: 'User already exists' }) } } catch (e) { @@ -20,12 +30,12 @@ module.exports = async (body, UserModel, res, url) => { const newUser = await helpers.createNewUser( email, - role, firstName, lastName, affiliation, title, - UserModel, + models.User, + role, ) await mailService.setupInviteEmail( diff --git a/packages/component-invite/src/helpers/Team.js b/packages/component-invite/src/helpers/Team.js new file mode 100644 index 0000000000000000000000000000000000000000..aa967186b165ae6141c764e6cc76bc249314f775 --- /dev/null +++ b/packages/component-invite/src/helpers/Team.js @@ -0,0 +1,121 @@ +const logger = require('@pubsweet/logger') +const config = require('config') + +const configRoles = config.get('roles') + +const createNewTeam = async (collectionId, role, userId, TeamModel) => { + let permissions, group, name + switch (role) { + case 'handlingEditor': + permissions = 'handlingEditor' + group = 'handlingEditor' + name = 'Handling Editor' + break + case 'reviewer': + permissions = 'reviewer' + group = 'reviewer' + name = 'Reviewer' + break + default: + break + } + + const teamBody = { + teamType: { + name: role, + permissions, + }, + group, + name, + object: { + type: 'collection', + id: collectionId, + }, + members: [userId], + } + const team = new TeamModel(teamBody) + try { + await team.save() + } catch (e) { + logger.error(e) + } +} + +const setupEiCTeams = async (models, user) => { + const collections = await models.Collection.all() + const teams = await models.Team.all() + user.teams = [] + const collectionIDs = [] + /* eslint-disable */ + for (const collection of collections) { + for (let team of teams) { + if ( + team.group === 'editorInChief' && + team.object.type === 'collection' && + team.object.id === collection.id + ) { + collectionIDs.push(collection.id) + team.members.push(user.id) + try { + team = await team.updateProperties(team) + team = await team.save() + } catch (e) { + logger.error(e) + } + } + } + + if (!collectionIDs.includes(collection.id)) { + await createNewTeam(collection.id, 'editorInChief', user.id, models.Team) + } + } + /* eslint-enable */ + user = await models.User.find(user.id) + return user +} + +const setupManuscriptTeam = async (models, user, collectionId, role) => { + const teams = await models.Team.all() + user.teams = [] + const filteredTeams = teams.filter( + team => + team.group === role && + team.object.type === 'collection' && + team.object.id === collectionId, + ) + + if (filteredTeams.length > 0) { + let team = filteredTeams[0] + team.members.push(user.id) + + try { + team = await team.updateProperties(team) + team = await team.save() + } catch (e) { + logger.error(e) + } + } else { + await createNewTeam(collectionId, role, user.id, models.Team) + } +} + +const getMatchingTeams = (teams, TeamModel, collectionId, role) => + teams + .map(async teamId => { + const team = await TeamModel.find(teamId) + if ( + team.object.id === collectionId && + configRoles.inviteRights[team.group].includes(role) + ) { + return team + } + return null + }) + .filter(Boolean) + +module.exports = { + createNewTeam, + setupEiCTeams, + setupManuscriptTeam, + getMatchingTeams, +} diff --git a/packages/component-invite/src/helpers/helpers.js b/packages/component-invite/src/helpers/helpers.js index 8ea26ed992dfad7ca59241f211e348a87c506c18..b575118780be1e6c3da1932adac332d543051a5b 100644 --- a/packages/component-invite/src/helpers/helpers.js +++ b/packages/component-invite/src/helpers/helpers.js @@ -58,26 +58,6 @@ const validateEmailAndToken = async (email, token, userModel) => { } } -const hasInviteRight = (configRoles, userRoles, role) => { - const includesRole = existingRole => - configRoles.inviteRights[existingRole].includes(role) - if (!userRoles.some(includesRole)) { - logger.error(`incorrect role when inviting a user`) - - return { - success: false, - status: 403, - message: `${userRoles} cannot invite a ${role}`, - } - } - - return { - success: true, - status: null, - message: null, - } -} - const handleNotFoundError = async (error, item) => { const response = { success: false, @@ -95,72 +75,40 @@ const handleNotFoundError = async (error, item) => { return response } -const createNewTeam = async (collectionId, user, TeamModel) => { - let permissions, group, name - switch (user.roles[0]) { - case 'handlingEditor': - permissions = 'editor' - group = 'editor' - name = 'Handling Editor' - break - case 'reviewer': - permissions = 'reviewer' - group = 'reviewer' - name = 'Reviewer' - break - default: - break - } - - const teamBody = { - teamType: { - name: user.roles[0], - permissions, - }, - group, - name, - object: { - type: 'collection', - id: collectionId, - }, - members: [user.id], - } - const team = new TeamModel(teamBody) - await team.save() -} - const createNewUser = async ( email, - role, firstName, lastName, affiliation, title, UserModel, + role, ) => { const userBody = { username: uuid.v4().slice(0, 8), email, password: uuid.v4(), - roles: [role], passwordResetToken: crypto.randomBytes(32).toString('hex'), isConfirmed: false, firstName, lastName, affiliation, title, + editorInChief: role === 'editorInChief', admin: role === 'admin', } let newUser = new UserModel(userBody) - newUser = await newUser.save() - return newUser + try { + newUser = await newUser.save() + return newUser + } catch (e) { + logger.error(e) + } } module.exports = { checkForUndefinedParams, validateEmailAndToken, - hasInviteRight, handleNotFoundError, - createNewTeam, createNewUser, } diff --git a/packages/component-invite/src/routes/postInvite.js b/packages/component-invite/src/routes/postInvite.js index 40050fad233caed4334c8a7a1379c89ac43659b5..113f8b21a436110a610d08c694d1a777773796fd 100644 --- a/packages/component-invite/src/routes/postInvite.js +++ b/packages/component-invite/src/routes/postInvite.js @@ -1,10 +1,7 @@ const logger = require('@pubsweet/logger') const get = require('lodash/get') -const config = require('config') const helpers = require('../helpers/helpers') -const configRoles = config.get('roles') - module.exports = models => async (req, res) => { const { email, role } = req.body @@ -16,15 +13,6 @@ module.exports = models => async (req, res) => { const reqUser = await models.User.find(req.user) const collectionId = get(req, 'params.collectionId') - if (reqUser.admin) reqUser.roles = reqUser.roles || ['admin'] - const inviteRight = helpers.hasInviteRight(configRoles, reqUser.roles, role) - if (!inviteRight.success) { - res.status(inviteRight.status).json({ - error: inviteRight.message, - }) - logger.error(`incorrect role when inviting a ${role}`) - return - } const url = `${req.protocol}://${req.get('host')}` if (collectionId) { @@ -42,12 +30,12 @@ module.exports = models => async (req, res) => { if (reqUser.admin) return require('../controllers/inviteGlobalRole')( req.body, - models.User, + models, res, url, ) res.status(403).json({ - error: `${reqUser.roles} cannot invite a ${role} without a collection`, + error: `${reqUser.username} cannot invite a ${role} without a collection`, }) } diff --git a/packages/component-invite/src/tests/fixtures/teams.js b/packages/component-invite/src/tests/fixtures/teams.js index 1410dc9e2ce5475bd299847a0fa1db65dee57f66..28cdaf9a8e5bc9b914988f860a8b947f4f8f55ae 100644 --- a/packages/component-invite/src/tests/fixtures/teams.js +++ b/packages/component-invite/src/tests/fixtures/teams.js @@ -1,15 +1,51 @@ const users = require('./users') -module.exports = { - teamType: { - name: 'editorInChief', - permissions: 'editor', +const { editorInChief, handlingEditor, reviewer } = users +const teams = { + eicTeam: { + teamType: { + name: 'editorInChief', + permissions: 'editorInChief', + }, + group: 'editorInChief', + name: 'Editor in Chief', + object: { + type: 'collection', + id: '123', + }, + members: [editorInChief.id], + save: jest.fn(() => teams.eicTeam), + updateProperties: jest.fn(() => teams.eicTeam), }, - group: 'editor', - name: 'Editor in Chief', - object: { - type: 'collection', - id: '123', + heTeam: { + teamType: { + name: 'handlingEditor', + permissions: 'handlingEditor', + }, + group: 'handlingEditor', + name: 'HandlingEditor', + object: { + type: 'collection', + id: '123', + }, + members: [handlingEditor.id], + save: jest.fn(() => teams.heTeam), + updateProperties: jest.fn(() => teams.heTeam), + }, + reviewerTeam: { + teamType: { + name: 'reviewer', + permissions: 'reviewer', + }, + group: 'reviewer', + name: 'Reviewer', + object: { + type: 'collection', + id: '123', + }, + members: [reviewer.id], + save: jest.fn(() => teams.reviewerTeam), + updateProperties: jest.fn(() => teams.reviewerTeam), }, - members: [users.editorInChief.id], } +module.exports = teams diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index 3b4baec68d3297d8d5f64ca5baf94c56f6920b58..daef8ad168bbc95f704b71235ab4c375a96f91a2 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -1,6 +1,7 @@ const collections = require('./collections') const { standardCollection } = collections + const users = { admin: { type: 'user', @@ -17,14 +18,14 @@ const users = { password: 'test1234', admin: false, id: 'editor123', - roles: ['editorInChief'], passwordResetToken: 'token123', - firstName: 'vlad', - lastName: 'dracul', + firstName: 'john', + lastName: 'smith', affiliation: 'MIT', - title: 'prof', + title: 'Mr', save: jest.fn(() => users.editorInChief), isConfirmed: false, + editorInChief: true, }, handlingEditor: { type: 'user', @@ -33,7 +34,6 @@ const users = { password: 'test', admin: false, id: 'handling123', - roles: ['handlingEditor'], assignations: [ { type: 'handlingEditor', @@ -43,6 +43,7 @@ const users = { }, ], save: jest.fn(() => users.handlingEditor), + editorInChief: false, }, author: { type: 'user', @@ -51,15 +52,30 @@ const users = { password: 'test', admin: false, id: 'author123', - roles: ['author'], passwordResetToken: 'token123', firstName: 'leopold', lastName: 'smith', affiliation: 'MIT', - title: 'mr', + title: 'Mr', save: jest.fn(() => users.author), isConfirmed: false, }, + reviewer: { + type: 'user', + username: 'reviewer', + email: 'reviewer@example.com', + password: 'test', + admin: false, + id: 'reviewer123', + passwordResetToken: 'token123', + firstName: 'angela', + lastName: 'smith', + affiliation: 'MIT', + title: 'Ms', + save: jest.fn(() => users.reviewer), + isConfirmed: false, + teams: [], + }, } module.exports = users diff --git a/packages/component-invite/src/tests/postInvite.test.js b/packages/component-invite/src/tests/postInvite.test.js index 5a3bdc423ac6499fe156ff50ad5f1b32d92d95af..1e6de6eb5f8120039e12676ef52b82ec9115fb92 100644 --- a/packages/component-invite/src/tests/postInvite.test.js +++ b/packages/component-invite/src/tests/postInvite.test.js @@ -4,6 +4,7 @@ process.env.SUPPRESS_NO_CONFIG_WARNING = true const httpMocks = require('node-mocks-http') const random = require('lodash/random') const fixtures = require('./fixtures/fixtures') + const UserMock = require('./mocks/User') const Chance = require('chance') const TeamMock = require('./mocks/Team') @@ -16,7 +17,7 @@ const chance = new Chance() const globalRoles = ['editorInChief', 'author', 'admin'] const manuscriptRoles = ['handlingEditor', 'reviewer'] -const buildModels = (collection, findUser, emailUser) => { +const buildModels = (collection, findUser, emailUser, team) => { const models = { User: {}, Collection: { @@ -29,12 +30,18 @@ const buildModels = (collection, findUser, emailUser) => { }, Team: {}, } - UserMock.find = jest.fn( - () => - findUser instanceof Error - ? Promise.reject(findUser) - : Promise.resolve(findUser), - ) + UserMock.find = jest.fn(user => { + const foundUser = Object.values(fixtures.users).find( + fixUser => fixUser.id === user.id, + ) + + if (foundUser === undefined) { + return Promise.reject(findUser) + } + + return Promise.resolve(foundUser) + }) + UserMock.findByEmail = jest.fn( () => emailUser instanceof Error @@ -42,6 +49,12 @@ const buildModels = (collection, findUser, emailUser) => { : Promise.resolve(emailUser), ) + TeamMock.find = jest.fn( + () => + team instanceof Error ? Promise.reject(team) : Promise.resolve(team), + ) + TeamMock.all = jest.fn(() => Object.values(fixtures.teams)) + models.User = UserMock models.Team = TeamMock return models @@ -63,6 +76,7 @@ notFoundError.status = 404 const { admin, editorInChief, handlingEditor, author } = fixtures.users const { standardCollection } = fixtures.collections +const { heTeam } = fixtures.teams const postInvitePath = '../routes/postInvite' describe('Post invite route handler', () => { it('should return success when the admin invites a global role', async () => { @@ -76,7 +90,6 @@ describe('Post invite route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.roles[0]).toEqual(body.role) expect(data.firstName).toEqual(body.firstName) expect(data.email).toEqual(body.email) expect(data.admin).toEqual(body.admin) @@ -108,7 +121,9 @@ describe('Post invite route handler', () => { await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) - expect(data.error).toEqual(`admin cannot invite a ${body.role}`) + expect(data.error).toEqual( + `admin tried to invite an invalid role: ${body.role}`, + ) }) it('should return an error params are missing', async () => { delete body.email @@ -137,9 +152,7 @@ describe('Post invite route handler', () => { await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) - expect(data.error).toEqual( - `${req.user.roles[0]} cannot invite a ${body.role}`, - ) + expect(data.error).toEqual(`Role ${body.role} cannot be set on collections`) }) it('should return an error when an editorInChief invites a handlingEditor without a collection', async () => { body.role = 'handlingEditor' @@ -155,7 +168,7 @@ describe('Post invite route handler', () => { expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( - `${req.user.roles} cannot invite a ${body.role} without a collection`, + `${req.user.username} cannot invite a ${body.role} without a collection`, ) }) it('should return an error when an handlingEditor invites a reviewer without a collection', async () => { @@ -172,7 +185,7 @@ describe('Post invite route handler', () => { expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( - `${req.user.roles} cannot invite a ${body.role} without a collection`, + `${req.user.username} cannot invite a ${body.role} without a collection`, ) }) it('should return an error when inviting an existing user', async () => { @@ -201,12 +214,11 @@ describe('Post invite route handler', () => { req.user = editorInChief req.params.collectionId = '123' const res = httpMocks.createResponse() - const models = buildModels(standardCollection, editorInChief, author) + const models = buildModels(standardCollection, author, author) await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.roles).toContain(body.role) expect(data.email).toEqual(body.email) expect(data.assignations[0].collectionId).toEqual(req.params.collectionId) }) @@ -218,6 +230,7 @@ describe('Post invite route handler', () => { const req = httpMocks.createRequest({ body, }) + handlingEditor.teams = [heTeam.id] req.user = handlingEditor req.params.collectionId = '123' const res = httpMocks.createResponse() @@ -226,7 +239,6 @@ describe('Post invite route handler', () => { expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) - expect(data.roles).toContain(body.role) expect(data.email).toEqual(body.email) expect(data.assignations[0].collectionId).toEqual(req.params.collectionId) }) diff --git a/packages/xpub-faraday/config/mailer.js b/packages/xpub-faraday/config/mailer.js index b62cdc759efa43df07e795f8f6899b8e725902d9..61822e96e26af7b59b961d290431d1e67c9bc713 100644 --- a/packages/xpub-faraday/config/mailer.js +++ b/packages/xpub-faraday/config/mailer.js @@ -4,7 +4,7 @@ module.exports = { from: process.env.EMAIL_SENDER, transport: { SES: new AWS.SES({ - accessKeyId: process.env.AWS_SES_ACCESS_KEYs, + accessKeyId: process.env.AWS_SES_ACCESS_KEY, secretAccessKey: process.env.AWS_SES_SECRET_KEY, region: process.env.AWS_SES_REGION, }), diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 49323a02f1db61c4055a8908af6cd5496b8f8df1..7a30fb1cf1a0d6d51e9f75360ba6a538fab437dc 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -87,13 +87,14 @@ module.exports = { ], user: { name: Joi.string(), - roles: Joi.array(), isConfirmed: Joi.boolean(), firstName: Joi.string().allow(''), lastName: Joi.string().allow(''), affiliation: Joi.string().allow(''), title: Joi.string().allow(''), assignations: Joi.array(), + teams: Joi.array(), + editorInChief: Joi.boolean(), }, team: { group: Joi.string(),