From b0a5659c8261965e4a9e0c79eda15858d3a351b8 Mon Sep 17 00:00:00 2001 From: Sebastian <sebastian.mihalache@thinslices.com> Date: Fri, 23 Feb 2018 16:12:59 +0200 Subject: [PATCH] feat(component-invite): add accept/refuse work on a manuscript endpoint --- packages/component-invite/index.js | 5 +- packages/component-invite/src/Assignation.js | 15 +++++ packages/component-invite/src/Invite.js | 9 ++- ...lectionRole.js => assignCollectionRole.js} | 17 ++++-- .../src/controllers/inviteGlobalRole.js | 3 +- .../routes/{get.js => getInviteDetails.js} | 0 .../src/routes/postAssignation.js | 60 +++++++++++++++++++ .../src/routes/{post.js => postInvite.js} | 6 +- .../src/routes/{reset.js => resetPassword.js} | 0 .../src/tests/fixtures/users.js | 8 ++- .../{get.test.js => getInviteDetails.test.js} | 13 ++-- .../src/tests/postAssignation.test.js | 57 ++++++++++++++++++ .../{post.test.js => postInvite.test.js} | 23 +++---- .../{reset.test.js => resetPassword.test.js} | 16 ++--- packages/component-mail-service/src/Mail.js | 6 +- packages/xpub-faraday/config/validations.js | 1 + 16 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 packages/component-invite/src/Assignation.js rename packages/component-invite/src/controllers/{inviteCollectionRole.js => assignCollectionRole.js} (80%) rename packages/component-invite/src/routes/{get.js => getInviteDetails.js} (100%) create mode 100644 packages/component-invite/src/routes/postAssignation.js rename packages/component-invite/src/routes/{post.js => postInvite.js} (90%) rename packages/component-invite/src/routes/{reset.js => resetPassword.js} (100%) rename packages/component-invite/src/tests/{get.test.js => getInviteDetails.test.js} (88%) create mode 100644 packages/component-invite/src/tests/postAssignation.test.js rename packages/component-invite/src/tests/{post.test.js => postInvite.test.js} (90%) rename packages/component-invite/src/tests/{reset.test.js => resetPassword.test.js} (90%) diff --git a/packages/component-invite/index.js b/packages/component-invite/index.js index 1bdf5faed..0fb36961e 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 000000000..c5f7e83aa --- /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 7e41bb5b7..d618d82e9 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 0c670970f..cccf36662 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 107da28af..ea05a8d5c 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 000000000..6a47dfd04 --- /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 e3c8b16c4..40050fad2 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 d592b48ae..012d67453 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 db3b68af7..a566d50c8 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 000000000..c8033b793 --- /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 03db53ddd..d03f3ebc8 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 b2ea34c15..93f8438b6 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 246ef41f3..4db1b9e17 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 942018750..4a7535172 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(), -- GitLab