diff --git a/packages/component-fixture-manager/src/fixtures/fragments.js b/packages/component-fixture-manager/src/fixtures/fragments.js index 2ee8c78db32d02ae01404cabaaae3ae88aecb113..10fcf908ed077f7ecda22e87cb745af8b0a539f1 100644 --- a/packages/component-fixture-manager/src/fixtures/fragments.js +++ b/packages/component-fixture-manager/src/fixtures/fragments.js @@ -86,6 +86,7 @@ const fragments = { ], authors: [ { + email: chance.email(), id: submittingAuthor.id, isSubmitting: true, isCorresponding: false, @@ -124,6 +125,7 @@ const fragments = { }, authors: [ { + email: chance.email(), id: submittingAuthor.id, isSubmitting: true, isCorresponding: false, @@ -162,6 +164,7 @@ const fragments = { }, authors: [ { + email: chance.email(), id: submittingAuthor.id, isSubmitting: true, isCorresponding: false, diff --git a/packages/component-helper-service/src/services/Email.js b/packages/component-helper-service/src/services/Email.js index 7241792473c594738d65210ccfe98b4e0f08e91f..15d06ecb278aac32ee6928c6eced4c1bf74bc2aa 100644 --- a/packages/component-helper-service/src/services/Email.js +++ b/packages/component-helper-service/src/services/Email.js @@ -347,6 +347,30 @@ class Email { }, }) } + + async setupManuscriptSubmittedEmail() { + const { + baseUrl, + UserModel, + collection, + parsedFragment: { id, title }, + authors: { submittingAuthor: { firstName = '', lastName = '' } }, + } = this + + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + + mailService.sendSimpleEmail({ + toEmail: eic.email, + emailType: 'manuscript-submitted', + dashboardUrl: baseUrl, + meta: { + collection, + fragment: { id, authorName: `${firstName} ${lastName}`, title }, + eicName: `${eic.firstName} ${eic.lastName}`, + }, + }) + } } const getSubject = recommendation => diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index d40d99d1f2803362450ef95ec51552948ce6e791..39cff7d046e3704ae6abdb3a3cc728672bb1fe38 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -7,10 +7,10 @@ const resetPath = config.get('invite-reset-password.url') module.exports = { sendSimpleEmail: async ({ - toEmail, - user, - emailType, - dashboardUrl, + toEmail = '', + user = {}, + emailType = '', + dashboardUrl = '', meta = {}, }) => { let subject, textBody @@ -117,6 +117,26 @@ module.exports = { textBody = `${replacements.headline}` emailTemplate = 'noCTA' break + case 'manuscript-submitted': + subject = `${meta.collection.customId}: Manuscript Submitted` + replacements.previewText = 'A new manuscript has been submitted' + replacements.headline = `A new manuscript has been submitted.` + replacements.paragraph = `You can view the full manuscript titled "${ + meta.fragment.title + }" by ${ + meta.fragment.authorName + } and take further actions by clicking on the following link:` + replacements.buttonText = 'MANUSCRIPT DETAILS' + replacements.url = helpers.createUrl( + dashboardUrl, + `/projects/${meta.collection.id}/versions/${ + meta.fragment.id + }/details`, + ) + textBody = `${replacements.headline} ${replacements.paragraph} ${ + replacements.url + } ${replacements.buttonText}` + break default: subject = 'Welcome to Hindawi!' break diff --git a/packages/component-manuscript-manager/src/Fragments.js b/packages/component-manuscript-manager/src/Fragments.js index 9e3569166d7dd7e7e76f9082412ab3bc8219c16a..01e11a93582bdc5ff37497d2bb7a7c6a980b68c0 100644 --- a/packages/component-manuscript-manager/src/Fragments.js +++ b/packages/component-manuscript-manager/src/Fragments.js @@ -9,7 +9,7 @@ const Fragments = app => { }) /** * @api {patch} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a revision for a manuscript - * @apiGroup FragmentsRecommendations + * @apiGroup Fragments * @apiParam {collectionId} collectionId Collection id * @apiParam {fragmentId} fragmentId Fragment id * @apiSuccessExample {json} Success @@ -28,6 +28,27 @@ const Fragments = app => { authBearer, require(`${routePath}/patch`)(app.locals.models), ) + /** + * @api {post} /api/collections/:collectionId/fragments/:fragmentId/submit Submit a manuscript + * @apiGroup Fragments + * @apiParam {collectionId} collectionId Collection id + * @apiParam {fragmentId} fragmentId Fragment id + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * + * } + * @apiErrorExample {json} Invite user errors + * HTTP/1.1 403 Forbidden + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + * HTTP/1.1 500 Internal Server Error + */ + app.post( + `${basePath}`, + authBearer, + require(`${routePath}/post`)(app.locals.models), + ) } module.exports = Fragments diff --git a/packages/component-manuscript-manager/src/routes/fragments/post.js b/packages/component-manuscript-manager/src/routes/fragments/post.js new file mode 100644 index 0000000000000000000000000000000000000000..d0b5b18f6d4592f681bcd14298c8cedd5c194c0d --- /dev/null +++ b/packages/component-manuscript-manager/src/routes/fragments/post.js @@ -0,0 +1,58 @@ +const { + Email, + Fragment, + services, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { collectionId, fragmentId } = req.params + let collection, fragment + + try { + collection = await models.Collection.find(collectionId) + if (!collection.fragments.includes(fragmentId)) + return res.status(400).json({ + error: `Collection and fragment do not match.`, + }) + fragment = await models.Fragment.find(fragmentId) + + const authsome = authsomeHelper.getAuthsome(models) + const target = { + fragment, + path: req.route.path, + } + const canPost = await authsome.can(req.user, 'POST', target) + if (!canPost) + return res.status(403).json({ + error: 'Unauthorized.', + }) + + fragment.submitted = Date.now() + fragment = await fragment.save() + + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const authors = await fragmentHelper.getAuthorData({ + UserModel: models.User, + }) + + const email = new Email({ + authors, + collection, + parsedFragment, + UserModel: models.User, + baseUrl: services.getBaseUrl(req), + }) + email.setupManuscriptSubmittedEmail() + + return res.status(200).json(fragment) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'Item') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/component-manuscript-manager/src/tests/fragments/post.test.js b/packages/component-manuscript-manager/src/tests/fragments/post.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1da5c72f731cdc11512706c2709d014dd2ed1930 --- /dev/null +++ b/packages/component-manuscript-manager/src/tests/fragments/post.test.js @@ -0,0 +1,86 @@ +process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' +process.env.SUPPRESS_NO_CONFIG_WARNING = true + +const cloneDeep = require('lodash/cloneDeep') +const fixturesService = require('pubsweet-component-fixture-service') +const requests = require('../requests') + +const { Model, fixtures } = fixturesService +jest.mock('pubsweet-component-mail-service', () => ({ + sendNotificationEmail: jest.fn(), +})) +const reqBody = {} + +const path = '../routes/fragments/post' +const route = { + path: '/api/collections/:collectionId/fragments/:fragmentId/submit', +} +describe('Post fragments route handler', () => { + let testFixtures = {} + let body = {} + let models + beforeEach(() => { + testFixtures = cloneDeep(fixtures) + body = cloneDeep(reqBody) + models = Model.build(testFixtures) + }) + it('should return success when the parameters are correct', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { fragment } = testFixtures.fragments + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(200) + }) + it('should return an error when the fragmentId does not match the collectionId', async () => { + const { user } = testFixtures.users + const { collection } = testFixtures.collections + const { noParentFragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: collection.id, + fragmentId: noParentFragment.id, + }, + }) + + expect(res.statusCode).toBe(400) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Collection and fragment do not match.') + }) + it('should return an error when the collection does not exist', async () => { + const { user } = testFixtures.users + const { fragment } = testFixtures.fragments + + const res = await requests.sendRequest({ + body, + userId: user.id, + models, + route, + path, + params: { + collectionId: 'invalid-id', + fragmentId: fragment.id, + }, + }) + + expect(res.statusCode).toBe(404) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual('Item not found') + }) +}) diff --git a/packages/xpub-faraday/config/authsome-helpers.js b/packages/xpub-faraday/config/authsome-helpers.js index ad75355dd14fd8f26a70e2f75bb2f8c74a87054d..8b5a135a30aa8f7bc660d518664db6b32001017a 100644 --- a/packages/xpub-faraday/config/authsome-helpers.js +++ b/packages/xpub-faraday/config/authsome-helpers.js @@ -4,7 +4,9 @@ const config = require('config') const statuses = config.get('statuses') +const keysToOmit = ['email', 'id'] const publicStatusesPermissions = ['author', 'reviewer'] +const authorAllowedStatuses = ['revisionRequested', 'rejected', 'accepted'] const parseAuthorsData = (coll, matchingCollPerm) => { if (['reviewer'].includes(matchingCollPerm.permission)) { @@ -124,6 +126,55 @@ const hasFragmentInDraft = async ({ object, Fragment }) => { return isInDraft(fragment) } +const filterAuthorRecommendationData = recommendation => { + const { comments } = recommendation + return { + ...recommendation, + comments: comments ? comments.filter(c => c.public) : [], + } +} + +const stripeCollectionByRole = (coll = {}, role = '') => { + if (role === 'author') { + const { handlingEditor } = coll + + if (!authorAllowedStatuses.includes(coll.status)) { + coll = { + ...coll, + handlingEditor: handlingEditor && { + ...omit(handlingEditor, keysToOmit), + name: 'Assigned', + }, + } + } + } + return coll +} + +const stripeFragmentByRole = (fragment = {}, role = '', user = {}) => { + const { recommendations, files, authors } = fragment + switch (role) { + case 'author': + return { + ...fragment, + recommendations: recommendations + ? recommendations.map(filterAuthorRecommendationData) + : [], + } + case 'reviewer': + return { + ...fragment, + files: omit(files, ['coverLetter']), + authors: authors.map(a => omit(a, ['email'])), + recommendations: recommendations + ? recommendations.filter(r => r.userId === user.id) + : [], + } + default: + return fragment + } +} + module.exports = { filterObjectData, parseAuthorsData, @@ -137,4 +188,6 @@ module.exports = { hasPermissionForObject, isInDraft, hasFragmentInDraft, + stripeCollectionByRole, + stripeFragmentByRole, } diff --git a/packages/xpub-faraday/config/authsome-mode.js b/packages/xpub-faraday/config/authsome-mode.js index 960e0bede30325e9d0dec6bc33e6762880b77e8e..2aada0e12aef4d742f83a5847ae150a6a58079fa 100644 --- a/packages/xpub-faraday/config/authsome-mode.js +++ b/packages/xpub-faraday/config/authsome-mode.js @@ -1,5 +1,5 @@ const config = require('config') -const { get, pickBy, omit } = require('lodash') +const { get, pickBy } = require('lodash') const statuses = config.get('statuses') const helpers = require('./authsome-helpers') @@ -103,9 +103,13 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { collection.fragments.includes(p.objectId), ) const visibleStatus = get(statuses, `${status}.${role}.label`) + const parsedCollection = helpers.stripeCollectionByRole( + collection, + role, + ) return { - ...collection, + ...parsedCollection, visibleStatus, } }, @@ -113,11 +117,7 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { } if (get(object, 'type') === 'fragment') { - if (helpers.isOwner({ user, object })) { - return true - } - - if (helpers.isInDraft(object)) { + if (helpers.isInDraft(object) && !helpers.isOwner({ user, object })) { return false } @@ -133,17 +133,8 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { if (!permission) return false return { - filter: fragment => { - // handle other roles - if (permission.role === 'reviewer') { - fragment.files = omit(fragment.files, ['coverLetter']) - fragment.authors = fragment.authors.map(a => omit(a, ['email'])) - fragment.recommendations = fragment.recommendations - ? fragment.recommendations.filter(r => r.userId === user.id) - : [] - } - return fragment - }, + filter: fragment => + helpers.stripeFragmentByRole(fragment, permission.role, user), } } @@ -167,7 +158,7 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { } if (operation === 'POST') { - // allow everytone to create manuscripts and versions + // allow everyone to create manuscripts and versions if (createPaths.includes(object.path)) { return true } @@ -192,6 +183,14 @@ async function applyAuthenticatedUserPolicy(user, operation, object, context) { roles: ['reviewer', 'handlingEditor'], }) } + + // allow owner to submit a manuscript + if ( + get(object, 'path') === + '/api/collections/:collectionId/fragments/:fragmentId/submit' + ) { + return helpers.isOwner({ user, object: object.fragment }) + } } if (operation === 'PATCH') { diff --git a/packages/xpub-faraday/config/test.js b/packages/xpub-faraday/config/test.js new file mode 100644 index 0000000000000000000000000000000000000000..9950c9b354fa8710a4f043a07ac2b86a3cf6bd2f --- /dev/null +++ b/packages/xpub-faraday/config/test.js @@ -0,0 +1,3 @@ +const defaultConfig = require('xpub-faraday/config/default') + +module.exports = defaultConfig diff --git a/packages/xpub-faraday/package.json b/packages/xpub-faraday/package.json index 2460482894ba362cf6d2356471ae669c9c9c71b8..e4e829deab3a05be7c02ee7c4bd237ed68d18ca0 100644 --- a/packages/xpub-faraday/package.json +++ b/packages/xpub-faraday/package.json @@ -63,6 +63,7 @@ "file-loader": "^1.1.5", "html-webpack-plugin": "^2.24.0", "joi-browser": "^10.0.6", + "jest": "^22.1.1", "react-hot-loader": "^3.1.1", "string-replace-loader": "^1.3.0", "style-loader": "^0.19.0", @@ -71,6 +72,10 @@ "webpack-dev-middleware": "^1.12.0", "webpack-hot-middleware": "^2.20.0" }, + "jest": { + "verbose": true, + "testRegex": "/tests/.*.test.js$" + }, "scripts": { "setupdb": "pubsweet setupdb ./", "start": "pubsweet start", @@ -79,6 +84,8 @@ "start-now": "echo $secret > config/local-development.json && npm run server", "build": "NODE_ENV=production pubsweet build", - "clean": "rm -rf node_modules" + "clean": "rm -rf node_modules", + "debug": "pgrep -f startup/start.js | xargs kill -sigusr1", + "test": "jest" } } diff --git a/packages/xpub-faraday/tests/authsome-helpers.test.js b/packages/xpub-faraday/tests/authsome-helpers.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8999b892b2b80a1ef5b92ffa35f8f823d84f9580 --- /dev/null +++ b/packages/xpub-faraday/tests/authsome-helpers.test.js @@ -0,0 +1,117 @@ +const { cloneDeep, get } = require('lodash') +const fixturesService = require('pubsweet-component-fixture-service') +const ah = require('../config/authsome-helpers') + +describe('Authsome Helpers', () => { + let testFixtures = {} + beforeEach(() => { + testFixtures = cloneDeep(fixturesService.fixtures) + }) + it('stripeCollection - should return collection', () => { + const { collection } = testFixtures.collections + const result = ah.stripeCollectionByRole(collection) + expect(result).toBeTruthy() + }) + it('stripeFragment - should return fragment', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole(fragment) + expect(result).toBeTruthy() + }) + + it('stripeCollection - author should not see HE name before recommendation made', () => { + const { collection } = testFixtures.collections + collection.status = 'underReview' + + const result = ah.stripeCollectionByRole(collection, 'author') + const { handlingEditor = {} } = result + + expect(handlingEditor.email).toBeFalsy() + expect(handlingEditor.name).toEqual('Assigned') + }) + + it('stripeCollection - author should see HE name after recommendation made', () => { + const { collection } = testFixtures.collections + collection.status = 'revisionRequested' + + const result = ah.stripeCollectionByRole(collection, 'author') + const { handlingEditor = {} } = result + + expect(handlingEditor.name).not.toEqual('Assigned') + }) + + it('stripeCollection - other user than author should see HE name before recommendation made', () => { + const { collection } = testFixtures.collections + collection.status = 'underReview' + + const result = ah.stripeCollectionByRole(collection, 'admin') + const { handlingEditor = {} } = result + + expect(handlingEditor.name).not.toEqual('Assigned') + }) + + it('stripeCollection - other user than author should see HE name after recommendation made', () => { + const { collection } = testFixtures.collections + collection.status = 'revisionRequested' + + const result = ah.stripeCollectionByRole(collection, 'admin') + const { handlingEditor = {} } = result + + expect(handlingEditor.name).not.toEqual('Assigned') + }) + + it('stripeCollection - returns if collection does not have HE', () => { + const { collection } = testFixtures.collections + delete collection.handlingEditor + + const result = ah.stripeCollectionByRole(collection, 'admin') + expect(result.handlingEditor).toBeFalsy() + }) + + it('stripeFragment - reviewer should not see authors email', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole(fragment, 'reviewer') + const { authors = [] } = result + expect(authors[0].email).toBeFalsy() + }) + it('stripeFragment - other roles than reviewer should see authors emails', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole(fragment, 'author') + const { authors = [] } = result + + expect(authors[0].email).toBeTruthy() + }) + + it('stripeFragment - reviewer should not see cover letter', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole(fragment, 'reviewer') + const { files = {} } = result + expect(files.coverLetter).toBeFalsy() + }) + it('stripeFragment - reviewer should not see others reviews', () => { + const { fragment } = testFixtures.fragments + const result = ah.stripeFragmentByRole(fragment, 'reviewer') + const { recommendations } = result + expect(recommendations).toEqual([]) + }) + + it('stripeFragment - author should not see private recommendations comments', () => { + const { fragment } = testFixtures.fragments + fragment.recommendations = [ + { + comments: [ + { + content: 'private', + public: false, + }, + { + content: 'public', + public: true, + }, + ], + }, + ] + const result = ah.stripeFragmentByRole(fragment, 'author') + const privateComments = get(result, 'recommendations[0].comments') + expect(privateComments).toHaveLength(1) + }) +})