diff --git a/packages/component-invite/index.js b/packages/component-invite/index.js index 1bdf5faede2f8361802cef49db4e1efb763998ed..0fb36961e5814466bbcd2b3c145d094abe3b25bd 100644 --- a/packages/component-invite/index.js +++ b/packages/component-invite/index.js @@ -1,3 +1,6 @@ module.exports = { - backend: () => app => require('./src/Invite')(app), + backend: () => app => { + require('./src/Invite')(app) + require('./src/Assignation')(app) + }, } diff --git a/packages/component-invite/src/Assignation.js b/packages/component-invite/src/Assignation.js new file mode 100644 index 0000000000000000000000000000000000000000..c5f7e83aa8bfa41966a4e4c36dbec72dcf3c06cb --- /dev/null +++ b/packages/component-invite/src/Assignation.js @@ -0,0 +1,15 @@ +const bodyParser = require('body-parser') + +const Assignation = app => { + app.use(bodyParser.json()) + const authBearer = app.locals.passport.authenticate('bearer', { + session: false, + }) + app.post( + '/api/collections/:collectionId/users', + authBearer, + require('./routes/postAssignation')(app.locals.models), + ) +} + +module.exports = Assignation diff --git a/packages/component-invite/src/Invite.js b/packages/component-invite/src/Invite.js index 7e41bb5b719785d43a403d6920f09be67154a0c7..d618d82e9ba97b5daa780bd6ffeddb703473fb8a 100644 --- a/packages/component-invite/src/Invite.js +++ b/packages/component-invite/src/Invite.js @@ -8,13 +8,16 @@ const Invite = app => { app.post( '/api/users/invite/:collectionId?', authBearer, - require('./routes/post')(app.locals.models), + require('./routes/postInvite')(app.locals.models), + ) + app.get( + '/api/users/invite', + require('./routes/getInviteDetails')(app.locals.models), ) - app.get('/api/users/invite', require('./routes/get')(app.locals.models)) app.post( '/api/users/invite/password/reset', bodyParser.json(), - require('./routes/reset')(app.locals.models), + require('./routes/resetPassword')(app.locals.models), ) } diff --git a/packages/component-invite/src/controllers/inviteCollectionRole.js b/packages/component-invite/src/controllers/assignCollectionRole.js similarity index 80% rename from packages/component-invite/src/controllers/inviteCollectionRole.js rename to packages/component-invite/src/controllers/assignCollectionRole.js index 0c670970f4979ad9461ca354ebfe1499fb5514cc..cccf36662b9113cdcffd9b4bd2ccfd2feb0d637a 100644 --- a/packages/component-invite/src/controllers/inviteCollectionRole.js +++ b/packages/component-invite/src/controllers/assignCollectionRole.js @@ -12,7 +12,7 @@ module.exports = async ( res, collectionId, models, - dashboardUrl, + url, ) => { if (reqUser.admin) { logger.error(`admin tried to invite a ${role} to a collection`) @@ -39,17 +39,24 @@ module.exports = async ( } try { - const user = await models.User.findByEmail(email) + let user = await models.User.findByEmail(email) user.roles.push(role) + const assignation = { + type: role, + hasAnswer: false, + isAccepted: false, + collectionId, + } + user.assignation = assignation + user = await user.save() await mailService.setupAssignEmail( user.email, 'assign-handling-editor', - dashboardUrl, + url, ) - // await helpers.createNewTeam(collection.id, user, models.Team) + // TODO: create a team and add the team id to the user's teams array - // add team to collection.teams 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 107da28af222bd0c6df78b8a68fff6a635e2727d..ea05a8d5c09bf0c2913a9a0c7c55a63b0f33c469 100644 --- a/packages/component-invite/src/controllers/inviteGlobalRole.js +++ b/packages/component-invite/src/controllers/inviteGlobalRole.js @@ -2,7 +2,7 @@ const logger = require('@pubsweet/logger') const helpers = require('../helpers/helpers') const mailService = require('pubsweet-component-mail-service') -module.exports = async (body, UserModel, res) => { +module.exports = async (body, UserModel, res, url) => { const { email, role, firstName, lastName, affiliation, title } = body try { @@ -32,6 +32,7 @@ module.exports = async (body, UserModel, res) => { newUser.email, 'invite', newUser.passwordResetToken, + url, ) return res.status(200).json(newUser) diff --git a/packages/component-invite/src/routes/get.js b/packages/component-invite/src/routes/getInviteDetails.js similarity index 100% rename from packages/component-invite/src/routes/get.js rename to packages/component-invite/src/routes/getInviteDetails.js diff --git a/packages/component-invite/src/routes/postAssignation.js b/packages/component-invite/src/routes/postAssignation.js new file mode 100644 index 0000000000000000000000000000000000000000..6a47dfd0460962be4e75d2c9c09b591c62560e3f --- /dev/null +++ b/packages/component-invite/src/routes/postAssignation.js @@ -0,0 +1,60 @@ +const logger = require('@pubsweet/logger') +const helpers = require('../helpers/helpers') + +module.exports = models => async (req, res) => { + const { type, accept } = req.body + + if (!helpers.checkForUndefinedParams(type, accept)) { + res.status(400).json({ error: 'Type and accept are required' }) + logger.error('some parameters are missing') + return + } + + const user = await models.User.find(req.user) + if (!user.assignation) { + res.status(400).json({ error: 'The user has no assignation' }) + logger.error('The request user does not have any assignation') + return + } + const { collectionId } = req.params + if (collectionId !== user.assignation.collectionId) { + res.status(400).json({ + error: 'User collection and provided collection do not match', + }) + logger.error( + `Param ${collectionId} does not match user collection: ${ + user.assignation.collectionId + }`, + ) + return + } + + if (type !== user.assignation.type) { + res.status(400).json({ + error: 'User assignation type and provided type do not match', + }) + logger.error( + `Param ${type} does not match user assignation type: ${ + user.assignation.type + }`, + ) + return + } + + try { + await models.Collection.find(collectionId) + // TODO: create a team and add the team id to the user's teams array + user.assignation.hasAnswer = true + if (accept) { + user.assignation.isAccepted = true + } + await user.save() + res.status(204).json() + return + } catch (e) { + const notFoundError = await helpers.handleNotFoundError(e, 'collection') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-invite/src/routes/post.js b/packages/component-invite/src/routes/postInvite.js similarity index 90% rename from packages/component-invite/src/routes/post.js rename to packages/component-invite/src/routes/postInvite.js index e3c8b16c4e0eb6df64acbc904d1ef97a661a6c52..40050fad233caed4334c8a7a1379c89ac43659b5 100644 --- a/packages/component-invite/src/routes/post.js +++ b/packages/component-invite/src/routes/postInvite.js @@ -26,15 +26,16 @@ module.exports = models => async (req, res) => { return } + const url = `${req.protocol}://${req.get('host')}` if (collectionId) { - return require('../controllers/inviteCollectionRole')( + return require('../controllers/assignCollectionRole')( email, role, reqUser, res, collectionId, models, - `${req.protocol}://${req.get('host')}`, + url, ) } @@ -43,6 +44,7 @@ module.exports = models => async (req, res) => { req.body, models.User, res, + url, ) res.status(403).json({ diff --git a/packages/component-invite/src/routes/reset.js b/packages/component-invite/src/routes/resetPassword.js similarity index 100% rename from packages/component-invite/src/routes/reset.js rename to packages/component-invite/src/routes/resetPassword.js diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index d592b48ae39b88055c7c95dc59e11ed0be1a03e5..012d67453bf27075f7e643e09d995d76f7b8ffd8 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -31,6 +31,12 @@ const users = { admin: false, id: 'handling123', roles: ['handlingEditor'], + assignation: { + type: 'handlingEditor', + hasAnswer: false, + isAccepted: false, + }, + save: jest.fn(() => users.handlingEditor), }, author: { type: 'user', @@ -45,7 +51,7 @@ const users = { lastName: 'smith', affiliation: 'MIT', title: 'mr', - save: jest.fn(() => users.authorUser), + save: jest.fn(() => users.author), isConfirmed: false, }, } diff --git a/packages/component-invite/src/tests/get.test.js b/packages/component-invite/src/tests/getInviteDetails.test.js similarity index 88% rename from packages/component-invite/src/tests/get.test.js rename to packages/component-invite/src/tests/getInviteDetails.test.js index db3b68af7effa3400e2655e0b4762af92f7b63d1..a566d50c8f3db6308acc8f901ff12e873bba2590 100644 --- a/packages/component-invite/src/tests/get.test.js +++ b/packages/component-invite/src/tests/getInviteDetails.test.js @@ -21,13 +21,14 @@ const query = { email: user.email, token: user.passwordResetToken, } +const getInviteDetailsPath = '../routes/getInviteDetails' describe('Get inivte data route handler', () => { it('should return success email and token are correct', async () => { const req = httpMocks.createRequest() req.query = query const res = httpMocks.createResponse() const models = buildModels(user) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -40,7 +41,7 @@ describe('Get inivte data route handler', () => { const res = httpMocks.createResponse() const models = buildModels(user) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('missing required params') @@ -53,7 +54,7 @@ describe('Get inivte data route handler', () => { const res = httpMocks.createResponse() const models = buildModels(user) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('invalid request') @@ -67,7 +68,7 @@ describe('Get inivte data route handler', () => { error.status = 404 const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('user not found') @@ -82,7 +83,7 @@ describe('Get inivte data route handler', () => { error.details.push({ message: 'validation error' }) const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('validation error') @@ -95,7 +96,7 @@ describe('Get inivte data route handler', () => { error.details.push({ message: 'internal server error' }) const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/get')(models)(req, res) + await require(getInviteDetailsPath)(models)(req, res) expect(res.statusCode).toBe(500) const data = JSON.parse(res._getData()) expect(data.error).toEqual('internal server error') diff --git a/packages/component-invite/src/tests/postAssignation.test.js b/packages/component-invite/src/tests/postAssignation.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c8033b793a9601ee625cb5e68cbf080ef65bd721 --- /dev/null +++ b/packages/component-invite/src/tests/postAssignation.test.js @@ -0,0 +1,57 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const httpMocks = require('node-mocks-http') +const fixtures = require('./fixtures/fixtures') +const UserMock = require('./mocks/User') + +jest.mock('pubsweet-component-mail-service', () => ({ + setupAssignEmail: jest.fn(), +})) + +const buildModels = (collection, user) => { + const models = { + User: {}, + Collection: { + find: jest.fn( + () => + collection instanceof Error + ? Promise.reject(collection) + : Promise.resolve(collection), + ), + }, + } + UserMock.find = jest.fn( + () => + user instanceof Error ? Promise.reject(user) : Promise.resolve(user), + ) + models.User = UserMock + return models +} + +const notFoundError = new Error() +notFoundError.name = 'NotFoundError' +notFoundError.status = 404 + +const { handlingEditor } = fixtures.users +const { standardCollection } = fixtures.collections +const postAssignationPath = '../routes/postAssignation' +describe('Post assignation route handler', () => { + it('should return success when the handling editor accepts work on a collection', async () => { + const body = { + type: 'handlingEditor', + accept: true, + } + const req = httpMocks.createRequest({ + body, + }) + req.user = handlingEditor + const res = httpMocks.createResponse() + const models = buildModels(standardCollection, handlingEditor) + await require(postAssignationPath)(models)(req, res) + + expect(res.statusCode).toBe(204) + expect(handlingEditor.assignation.hasAnswer).toBeTruthy() + expect(handlingEditor.assignation.isAccepted).toBeTruthy() + }) +}) diff --git a/packages/component-invite/src/tests/post.test.js b/packages/component-invite/src/tests/postInvite.test.js similarity index 90% rename from packages/component-invite/src/tests/post.test.js rename to packages/component-invite/src/tests/postInvite.test.js index 03db53ddd2cb0b12d90d82bb70b1e3aedb052bd2..d03f3ebc8f0dc8758bea0f9d49a10abeee3485d5 100644 --- a/packages/component-invite/src/tests/post.test.js +++ b/packages/component-invite/src/tests/postInvite.test.js @@ -63,6 +63,7 @@ notFoundError.status = 404 const { admin, editorInChief, handlingEditor, author } = fixtures.users const { standardCollection } = fixtures.collections +const postInvitePath = '../routes/postInvite' describe('Post invite route handler', () => { it('should return success when the admin invites a global role', async () => { const req = httpMocks.createRequest({ @@ -71,7 +72,7 @@ describe('Post invite route handler', () => { req.user = admin const res = httpMocks.createResponse() const models = buildModels(notFoundError, admin, notFoundError) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -88,7 +89,7 @@ describe('Post invite route handler', () => { req.params.collectionId = '123' const res = httpMocks.createResponse() const models = buildModels(notFoundError, admin) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -104,7 +105,7 @@ describe('Post invite route handler', () => { req.user = admin const res = httpMocks.createResponse() const models = buildModels(notFoundError, admin) - await require('../routes/post')(models)(req, res) + 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}`) @@ -117,7 +118,7 @@ describe('Post invite route handler', () => { req.user = admin const res = httpMocks.createResponse() const models = buildModels(notFoundError, admin) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('Email and role are required') @@ -133,7 +134,7 @@ describe('Post invite route handler', () => { req.params.collectionId = '123' const res = httpMocks.createResponse() const models = buildModels(notFoundError, editorInChief) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -150,7 +151,7 @@ describe('Post invite route handler', () => { const res = httpMocks.createResponse() const models = buildModels(notFoundError, editorInChief) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -167,7 +168,7 @@ describe('Post invite route handler', () => { const res = httpMocks.createResponse() const models = buildModels(notFoundError, handlingEditor) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(403) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -183,7 +184,7 @@ describe('Post invite route handler', () => { req.user = admin const res = httpMocks.createResponse() const models = buildModels(notFoundError, admin, editorInChief) - await require('../routes/post')(models)(req, res) + await require(postInvitePath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) @@ -201,12 +202,13 @@ describe('Post invite route handler', () => { req.params.collectionId = '123' const res = httpMocks.createResponse() const models = buildModels(standardCollection, editorInChief, author) - await require('../routes/post')(models)(req, res) + 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.assignation.collectionId).toEqual(req.params.collectionId) }) it('should return success when the handlingEditor invites a reviewer with a collection', async () => { const body = { @@ -220,11 +222,12 @@ describe('Post invite route handler', () => { req.params.collectionId = '123' const res = httpMocks.createResponse() const models = buildModels(standardCollection, handlingEditor, author) - await require('../routes/post')(models)(req, res) + 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.assignation.collectionId).toEqual(req.params.collectionId) }) }) diff --git a/packages/component-invite/src/tests/reset.test.js b/packages/component-invite/src/tests/resetPassword.test.js similarity index 90% rename from packages/component-invite/src/tests/reset.test.js rename to packages/component-invite/src/tests/resetPassword.test.js index b2ea34c15e50105b1e904834bb9c58fb695ce1c8..93f8438b630e3a656841596b9b5df5a2eb9735b3 100644 --- a/packages/component-invite/src/tests/reset.test.js +++ b/packages/component-invite/src/tests/resetPassword.test.js @@ -33,13 +33,13 @@ const body = { const notFoundError = new Error() notFoundError.name = 'NotFoundError' notFoundError.status = 404 - +const resetPasswordPath = '../routes/resetPassword' describe('Password reset after invite route handler', () => { it('should return success when the body is correct', async () => { const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() const models = buildModels(editorInChiefUser) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(200) const data = JSON.parse(res._getData()) @@ -53,7 +53,7 @@ describe('Password reset after invite route handler', () => { const res = httpMocks.createResponse() const models = buildModels(editorInChiefUser) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('missing required params') @@ -65,7 +65,7 @@ describe('Password reset after invite route handler', () => { const res = httpMocks.createResponse() const models = buildModels(editorInChiefUser) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual( @@ -80,7 +80,7 @@ describe('Password reset after invite route handler', () => { error.status = 404 const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(404) const data = JSON.parse(res._getData()) expect(data.error).toEqual('user not found') @@ -94,7 +94,7 @@ describe('Password reset after invite route handler', () => { error.details.push({ message: 'validation error' }) const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('validation error') @@ -106,7 +106,7 @@ describe('Password reset after invite route handler', () => { error.details.push({ message: 'internal server error' }) const res = httpMocks.createResponse() const models = buildModels(error) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(500) const data = JSON.parse(res._getData()) expect(data.error).toEqual('internal server error') @@ -116,7 +116,7 @@ describe('Password reset after invite route handler', () => { const req = httpMocks.createRequest({ body }) const res = httpMocks.createResponse() const models = buildModels(editorInChiefUser) - await require('../routes/reset')(models)(req, res) + await require(resetPasswordPath)(models)(req, res) expect(res.statusCode).toBe(400) const data = JSON.parse(res._getData()) expect(data.error).toEqual('User is already confirmed') diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index 246ef41f325b651774a02053a7d6cd4f090ad8db..4db1b9e17f84226e732e7f154a89fc940ebe1d25 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -4,17 +4,17 @@ const querystring = require('querystring') const Email = require('@pubsweet/component-send-email') const config = require('config') -const resetUrl = config.get('invite-reset-password.url') +const resetPath = config.get('invite-reset-password.url') module.exports = { - setupInviteEmail: async (email, emailType, token) => { + setupInviteEmail: async (email, emailType, token, inviteUrl) => { let subject let replacements = {} switch (emailType) { case 'invite': subject = 'Hindawi Invitation' replacements = { - url: `${resetUrl}?${querystring.encode({ + url: `${inviteUrl}${resetPath}?${querystring.encode({ email, token, })}`, diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 942018750708dd302aabad1b6567e2b97dfcb2e0..4a753517200ef603bd276a194767b62915db1048 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -93,6 +93,7 @@ module.exports = { lastName: Joi.string().allow(''), affiliation: Joi.string().allow(''), title: Joi.string().allow(''), + assignation: Joi.object(), }, team: { group: Joi.string(),