diff --git a/packages/component-helper-service/.gitignore b/packages/component-helper-service/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18 --- /dev/null +++ b/packages/component-helper-service/.gitignore @@ -0,0 +1,8 @@ +_build/ +api/ +logs/ +node_modules/ +uploads/ +.env.* +.env +config/local*.* \ No newline at end of file diff --git a/packages/component-helper-service/README.md b/packages/component-helper-service/README.md new file mode 100644 index 0000000000000000000000000000000000000000..a0ac7afbf2c8255c8a94f4f2ff525dd245cc3c93 --- /dev/null +++ b/packages/component-helper-service/README.md @@ -0,0 +1,2 @@ +# Helper Service + diff --git a/packages/component-helper-service/index.js b/packages/component-helper-service/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e3c50cf86934045799e812f2851110d36ba7cfec --- /dev/null +++ b/packages/component-helper-service/index.js @@ -0,0 +1 @@ +module.exports = require('./src/Helper') diff --git a/packages/component-helper-service/package.json b/packages/component-helper-service/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e4aa40e13b33df0be7f7a47e1c00a8ef7019a8b1 --- /dev/null +++ b/packages/component-helper-service/package.json @@ -0,0 +1,25 @@ +{ + "name": "pubsweet-component-helper-service", + "version": "0.0.1", + "description": "helper service component for pubsweet", + "license": "MIT", + "author": "Collaborative Knowledge Foundation", + "files": [ + "src" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://gitlab.coko.foundation/xpub/xpub-faraday", + "path": "component-helper-service" + }, + "dependencies": { + }, + "peerDependencies": { + "@pubsweet/logger": "^0.0.1", + "pubsweet-server": "^1.0.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/component-helper-service/src/Helper.js b/packages/component-helper-service/src/Helper.js new file mode 100644 index 0000000000000000000000000000000000000000..0037b50c55ed4e8f5af9baccf34702df12b77731 --- /dev/null +++ b/packages/component-helper-service/src/Helper.js @@ -0,0 +1,19 @@ +const Email = require('./services/Email') +const Collection = require('./services/Collection') +const Fragment = require('./services/Fragment') +const services = require('./services/services') +const authsome = require('./services/authsome') +const User = require('./services/User') +const Team = require('./services/Team') +const Invitation = require('./services/Invitation') + +module.exports = { + Email, + Collection, + Fragment, + services, + authsome, + User, + Team, + Invitation, +} diff --git a/packages/component-helper-service/src/services/Collection.js b/packages/component-helper-service/src/services/Collection.js new file mode 100644 index 0000000000000000000000000000000000000000..6e502735b7ae9ce777bbda52be940b4e06abf953 --- /dev/null +++ b/packages/component-helper-service/src/services/Collection.js @@ -0,0 +1,107 @@ +const config = require('config') + +const statuses = config.get('statuses') + +class Collection { + constructor({ collection = {} }) { + this.collection = collection + } + + async updateStatusByRecommendation({ recommendation }) { + let newStatus = 'pendingApproval' + if (['minor', 'major'].includes(recommendation)) + newStatus = 'revisionRequested' + + await this.updateStatus({ newStatus }) + } + + async updateFinalStatusByRecommendation({ recommendation }) { + let newStatus + switch (recommendation) { + case 'reject': + newStatus = 'rejected' + break + case 'publish': + newStatus = 'published' + break + case 'return-to-handling-editor': + newStatus = 'reviewCompleted' + break + default: + break + } + + await this.updateStatus({ newStatus }) + } + + async updateStatus({ newStatus }) { + this.collection.status = newStatus + this.collection.visibleStatus = statuses[this.collection.status].private + await this.collection.save() + } + + async getAuthorData({ UserModel }) { + const { collection: { authors } } = this + const submittingAuthorData = authors.find( + author => author.isSubmitting === true, + ) + const submittingAuthor = await UserModel.find(submittingAuthorData.userId) + const authorsPromises = authors.map(async author => { + const user = await UserModel.find(author.userId) + return `${user.firstName} ${user.lastName}` + }) + const authorsList = await Promise.all(authorsPromises) + + return { + authorsList, + submittingAuthor, + } + } + + getReviewerInvitations({ agree = true }) { + const { collection: { invitations } } = this + return agree + ? invitations.filter( + inv => + inv.role === 'reviewer' && + inv.hasAnswer === true && + inv.isAccepted === true, + ) + : invitations.filter( + inv => inv.role === 'reviewer' && inv.hasAnswer === false, + ) + } + + async addHandlingEditor({ user, invitation }) { + this.collection.handlingEditor = { + id: user.id, + name: `${user.firstName} ${user.lastName}`, + invitedOn: invitation.invitedOn, + respondedOn: invitation.respondedOn, + email: user.email, + hasAnswer: invitation.hasAnswer, + isAccepted: invitation.isAccepted, + } + await this.updateStatus({ newStatus: 'heInvited' }) + } + + async updateHandlingEditor({ isAccepted }) { + const { collection: { handlingEditor } } = this + handlingEditor.hasAnswer = true + handlingEditor.isAccepted = isAccepted + handlingEditor.respondedOn = Date.now() + let status + isAccepted ? (status = 'heAssigned') : (status = 'submitted') + await this.updateStatus({ newStatus: status }) + } + + async updateStatusByNumberOfReviewers() { + const reviewerInvitations = this.collection.invitations.filter( + inv => inv.role === 'reviewer', + ) + if (reviewerInvitations.length === 0) + await this.updateStatus({ newStatus: 'heAssigned' }) + } +} + +module.exports = Collection diff --git a/packages/component-manuscript-manager/src/helpers/Email.js b/packages/component-helper-service/src/services/Email.js similarity index 68% rename from packages/component-manuscript-manager/src/helpers/Email.js rename to packages/component-helper-service/src/services/Email.js index 044dc1797d147d1fa7f1909c1395b6e6e694fb25..49a106d8d72305dcf029accd2bd0a094a52cab45 100644 --- a/packages/component-manuscript-manager/src/helpers/Email.js +++ b/packages/component-helper-service/src/services/Email.js @@ -1,4 +1,5 @@ -const collectionHelper = require('./Collection') +const Collection = require('./Collection') +const User = require('./User') const get = require('lodash/get') const config = require('config') const mailService = require('pubsweet-component-mail-service') @@ -31,19 +32,14 @@ class Email { }) { const { UserModel, + collection, parsedFragment: { recommendations, title, type }, authors: { submittingAuthor: { firstName = '', lastName = '' } }, - collection: { - customId, - invitations = [], - handlingEditor: { name: heName }, - }, } = this - - const reviewerInvitations = collectionHelper.getReviewerInvitations( - invitations, + const collectionHelper = new Collection({ collection }) + const reviewerInvitations = collectionHelper.getReviewerInvitations({ agree, - ) + }) const hasReview = invUserId => rec => rec.recommendationType === 'review' && @@ -59,15 +55,18 @@ class Email { }) let emailType = 'agreed-reviewers-after-recommendation' let emailText, subject, manuscriptType - const eic = await getEditorInChief(UserModel) - const editorName = isSubmitted ? `${eic.firstName} ${eic.lastName}` : heName + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + const editorName = isSubmitted + ? `${eic.firstName} ${eic.lastName}` + : collection.handlingEditor.name let reviewers = await Promise.all(reviewerPromises) reviewers = reviewers.filter(Boolean) if (agree) { subject = isSubmitted - ? `${customId}: Manuscript Decision` - : `${customId}: Manuscript ${getSubject(recommendation)}` + ? `${collection.customId}: Manuscript Decision` + : `${collection.customId}: Manuscript ${getSubject(recommendation)}` if (isSubmitted) { emailType = 'submitting-reviewers-after-decision' @@ -75,7 +74,7 @@ class Email { if (recommendation === 'publish') emailText = 'will now be published' } } else { - subject = `${customId}: Reviewer Unassigned` + subject = `${collection.customId}: Reviewer Unassigned` manuscriptType = manuscriptTypes[type] emailType = 'no-response-reviewers-after-recommendation' } @@ -161,7 +160,8 @@ class Email { parsedFragment: { title, id }, authors: { submittingAuthor: { firstName = '', lastName = '' } }, } = this - const eic = await getEditorInChief(UserModel) + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() const toEmail = collection.handlingEditor.email let emailType = publish ? 'he-manuscript-published' @@ -222,7 +222,8 @@ class Email { throw new Error('undefined HE recommentation type') } - const eic = await getEditorInChief(UserModel) + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() const toEmail = eic.email mailService.sendNotificationEmail({ toEmail, @@ -237,6 +238,103 @@ class Email { }, }) } + + setupReviewerInvitationEmail({ + user, + invitationId, + timestamp, + resend = false, + authorName, + }) { + const { + baseUrl, + collection, + parsedFragment: { id, title, abstract }, + authors, + } = this + const params = { + user, + baseUrl, + subject: `${collection.customId}: Review Requested`, + toEmail: user.email, + meta: { + fragment: { + id, + title, + abstract, + authors, + }, + invitation: { + id: invitationId, + timestamp, + }, + collection: { + id: collection.id, + authorName, + handlingEditor: collection.handlingEditor, + }, + }, + emailType: resend ? 'resend-reviewer' : 'invite-reviewer', + } + mailService.sendReviewerInvitationEmail(params) + } + + async setupReviewerDecisionEmail({ + user, + agree = false, + timestamp = Date.now(), + authorName = '', + }) { + const { + baseUrl, + UserModel, + collection, + parsedFragment: { id, title }, + } = this + const userHelper = new User({ UserModel }) + const eic = await userHelper.getEditorInChief() + const toEmail = collection.handlingEditor.email + mailService.sendNotificationEmail({ + toEmail, + user, + emailType: agree ? 'reviewer-agreed' : 'reviewer-declined', + meta: { + collection: { customId: collection.customId, id: collection.id }, + fragment: { id, title, authorName }, + handlingEditorName: collection.handlingEditor.name, + baseUrl, + eicName: `${eic.firstName} ${eic.lastName}`, + timestamp, + }, + }) + if (agree) + mailService.sendNotificationEmail({ + toEmail: user.email, + user, + emailType: 'reviewer-thank-you', + meta: { + collection: { customId: collection.customId, id: collection.id }, + fragment: { id, title, authorName }, + handlingEditorName: collection.handlingEditor.name, + baseUrl, + }, + }) + } + + async setupReviewerUnassignEmail({ user, authorName }) { + const { collection, fragment: { title } } = this + + await mailService.sendNotificationEmail({ + toEmail: user.email, + user, + emailType: 'unassign-reviewer', + meta: { + collection: { customId: collection.customId }, + fragment: { title, authorName }, + handlingEditorName: collection.handlingEditor.name, + }, + }) + } } const getSubject = recommendation => @@ -252,10 +350,4 @@ const getHeRecommendation = recommendation => { return heRecommendation } -const getEditorInChief = async UserModel => { - const users = await UserModel.all() - const eic = users.find(user => user.editorInChief || user.admin) - return eic -} - module.exports = Email diff --git a/packages/component-helper-service/src/services/Fragment.js b/packages/component-helper-service/src/services/Fragment.js new file mode 100644 index 0000000000000000000000000000000000000000..d4be48eec23c41fd8e08e2d5695a34a705454fb1 --- /dev/null +++ b/packages/component-helper-service/src/services/Fragment.js @@ -0,0 +1,26 @@ +class Fragment { + constructor({ fragment }) { + this.fragment = fragment + } + async getFragmentData({ handlingEditor = {} }) { + const { fragment: { metadata, recommendations = [], id } } = this + const heRecommendation = recommendations.find( + rec => rec.userId === handlingEditor.id, + ) + let { title, abstract } = metadata + const { type } = metadata + title = title.replace(/<(.|\n)*?>/g, '') + abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : '' + + return { + id, + type, + title, + abstract, + recommendations, + heRecommendation, + } + } +} + +module.exports = Fragment diff --git a/packages/component-helper-service/src/services/Invitation.js b/packages/component-helper-service/src/services/Invitation.js new file mode 100644 index 0000000000000000000000000000000000000000..ac595dabf0ee0f4880a343f29c8c4376c171a1e6 --- /dev/null +++ b/packages/component-helper-service/src/services/Invitation.js @@ -0,0 +1,61 @@ +const uuid = require('uuid') +const logger = require('@pubsweet/logger') + +class Invitation { + constructor({ userId, role }) { + this.userId = userId + this.role = role + } + + set _userId(newUserId) { + this.userId = newUserId + } + + getInvitationsData({ invitations = [] }) { + const { userId, role } = this + const matchingInvitation = invitations.find( + inv => inv.role === role && inv.userId === userId, + ) + if (!matchingInvitation) { + logger.error( + `There should be at least one matching invitation between User ${userId} and Role ${role}`, + ) + throw Error('no matching invitation') + } + let status = 'pending' + if (matchingInvitation.isAccepted) { + status = 'accepted' + } else if (matchingInvitation.hasAnswer) { + status = 'declined' + } + + const { invitedOn, respondedOn, id } = matchingInvitation + return { invitedOn, respondedOn, status, id } + } + + async setupInvitation({ collection }) { + const { userId, role } = this + const invitation = { + role, + hasAnswer: false, + isAccepted: false, + invitedOn: Date.now(), + id: uuid.v4(), + userId, + respondedOn: null, + } + collection.invitations = collection.invitations || [] + collection.invitations.push(invitation) + collection = await collection.save() + return invitation + } + + getInvitation({ invitations = [] }) { + return invitations.find( + invitation => + invitation.userId === this.userId && invitation.role === this.role, + ) + } +} + +module.exports = Invitation diff --git a/packages/component-helper-service/src/services/Team.js b/packages/component-helper-service/src/services/Team.js new file mode 100644 index 0000000000000000000000000000000000000000..b5b70ee1eb1f4ecb467f985ada697602f1247811 --- /dev/null +++ b/packages/component-helper-service/src/services/Team.js @@ -0,0 +1,133 @@ +const logger = require('@pubsweet/logger') +const get = require('lodash/get') + +class Team { + constructor({ TeamModel = {}, collectionId = '' }) { + this.TeamModel = TeamModel + this.collectionId = collectionId + } + + async createNewTeam({ role, userId }) { + const { collectionId, TeamModel } = this + 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 + case 'author': + permissions = 'author' + group = 'author' + name = 'author' + break + default: + break + } + + const teamBody = { + teamType: { + name: role, + permissions, + }, + group, + name, + object: { + type: 'collection', + id: collectionId, + }, + members: [userId], + } + let team = new TeamModel(teamBody) + team = await team.save() + return team + } + + async setupManuscriptTeam({ user, role }) { + const { TeamModel, collectionId } = this + const teams = await TeamModel.all() + user.teams = user.teams || [] + let foundTeam = teams.find( + team => + team.group === role && + team.object.type === 'collection' && + team.object.id === collectionId, + ) + + if (foundTeam !== undefined) { + if (!foundTeam.members.includes(user.id)) { + foundTeam.members.push(user.id) + } + + try { + foundTeam = await foundTeam.save() + if (!user.teams.includes(foundTeam.id)) { + user.teams.push(foundTeam.id) + await user.save() + } + return foundTeam + } catch (e) { + logger.error(e) + } + } else { + const team = await this.createNewTeam({ role, userId: user.id }) + user.teams.push(team.id) + await user.save() + return team + } + } + + async removeTeamMember({ teamId, userId }) { + const { TeamModel } = this + const team = await TeamModel.find(teamId) + const members = team.members.filter(member => member !== userId) + team.members = members + await team.save() + } + + async getTeamMembersByCollection({ role }) { + const { TeamModel, collectionId } = this + + const teams = await TeamModel.all() + const members = get( + teams.find( + team => + team.group === role && + team.object.type === 'collection' && + team.object.id === collectionId, + ), + 'members', + ) + + return members + } + + async getTeamByGroupAndCollection({ role }) { + const { TeamModel, collectionId } = this + const teams = await TeamModel.all() + return teams.find( + team => + team.group === role && + team.object.type === 'collection' && + team.object.id === collectionId, + ) + } + + async updateHETeam({ collection, role, user }) { + const team = await this.getTeamByGroupAndCollection({ + role, + }) + delete collection.handlingEditor + await this.removeTeamMember({ teamId: team.id, userId: user.id }) + user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) + await user.save() + await collection.save() + } +} + +module.exports = Team diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js new file mode 100644 index 0000000000000000000000000000000000000000..ed69cd655e6acd1445dacab855d2ddbde74dd7b9 --- /dev/null +++ b/packages/component-helper-service/src/services/User.js @@ -0,0 +1,70 @@ +const uuid = require('uuid') +const crypto = require('crypto') +const logger = require('@pubsweet/logger') +const mailService = require('pubsweet-component-mail-service') + +class User { + constructor({ UserModel = {} }) { + this.UserModel = UserModel + } + + async createUser({ role, body }) { + const { UserModel } = this + const { email, firstName, lastName, affiliation, title } = body + const username = email + const password = uuid.v4() + const userBody = { + username, + email, + password, + passwordResetToken: crypto.randomBytes(32).toString('hex'), + isConfirmed: false, + firstName, + lastName, + affiliation, + title, + editorInChief: role === 'editorInChief', + admin: role === 'admin', + handlingEditor: role === 'handlingEditor', + invitationToken: role === 'reviewer' ? uuid.v4() : '', + } + + let newUser = new UserModel(userBody) + + try { + newUser = await newUser.save() + return newUser + } catch (e) { + logger.error(e) + } + } + + async setupNewUser({ url, role, invitationType, body = {} }) { + const newUser = await this.createUser({ role, body }) + + try { + if (role !== 'reviewer') { + mailService.sendSimpleEmail({ + toEmail: newUser.email, + user: newUser, + emailType: invitationType, + dashboardUrl: url, + }) + } + + return newUser + } catch (e) { + logger.error(e.message) + return { status: 500, error: 'Email could not be sent.' } + } + } + + async getEditorInChief() { + const { UserModel } = this + const users = await UserModel.all() + const eic = users.find(user => user.editorInChief || user.admin) + return eic + } +} + +module.exports = User diff --git a/packages/component-invite/src/helpers/authsome.js b/packages/component-helper-service/src/services/authsome.js similarity index 100% rename from packages/component-invite/src/helpers/authsome.js rename to packages/component-helper-service/src/services/authsome.js diff --git a/packages/component-manuscript-manager/src/helpers/helpers.js b/packages/component-helper-service/src/services/services.js similarity index 96% rename from packages/component-manuscript-manager/src/helpers/helpers.js rename to packages/component-helper-service/src/services/services.js index a37f7504aaa1efd3fbacdc94734fc0807cfa5734..cb56e8ec7edd3c811f3f77f11f66567c70d4a898 100644 --- a/packages/component-manuscript-manager/src/helpers/helpers.js +++ b/packages/component-helper-service/src/services/services.js @@ -8,7 +8,7 @@ const checkForUndefinedParams = (...params) => { return true } -const validateEmailAndToken = async (email, token, userModel) => { +const validateEmailAndToken = async ({ email, token, userModel }) => { try { const user = await userModel.findByEmail(email) if (user) { diff --git a/packages/component-invite/config/default.js b/packages/component-invite/config/default.js index 7afbb2f22c26d8ae4d9f314989bc4a02189360f7..4609e678ed2d63b591dde366b05826f85b78d835 100644 --- a/packages/component-invite/config/default.js +++ b/packages/component-invite/config/default.js @@ -55,4 +55,16 @@ module.exports = { private: 'Under Review', }, }, + 'manuscript-types': { + research: 'Research', + review: 'Review', + 'clinical-study': 'Clinical Study', + 'case-report': 'Case Report', + 'letter-to-editor': 'Letter to the Editor', + editorial: 'Editorial', + corrigendum: 'Corrigendum', + erratum: 'Erratum', + 'expression-of-concern': 'Expression of Concern', + retraction: 'Retraction', + }, } diff --git a/packages/component-invite/config/test.js b/packages/component-invite/config/test.js index 0eb54780a931f66f1b611d9a4d51552908ece2c3..e622200e004081d3a576664b5547179e87fede3c 100644 --- a/packages/component-invite/config/test.js +++ b/packages/component-invite/config/test.js @@ -56,4 +56,16 @@ module.exports = { private: 'Under Review', }, }, + 'manuscript-types': { + research: 'Research', + review: 'Review', + 'clinical-study': 'Clinical Study', + 'case-report': 'Case Report', + 'letter-to-editor': 'Letter to the Editor', + editorial: 'Editorial', + corrigendum: 'Corrigendum', + erratum: 'Erratum', + 'expression-of-concern': 'Expression of Concern', + retraction: 'Retraction', + }, } diff --git a/packages/component-invite/src/helpers/Collection.js b/packages/component-invite/src/helpers/Collection.js deleted file mode 100644 index d04841fc244fa3b7619bacd064d3685267d42ccd..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/helpers/Collection.js +++ /dev/null @@ -1,72 +0,0 @@ -const config = require('config') -const last = require('lodash/last') - -const statuses = config.get('statuses') - -const addHandlingEditor = async (collection, user, invitation) => { - collection.handlingEditor = { - id: user.id, - name: `${user.firstName} ${user.lastName}`, - invitedOn: invitation.invitedOn, - respondedOn: invitation.respondedOn, - email: user.email, - hasAnswer: invitation.hasAnswer, - isAccepted: invitation.isAccepted, - } - await updateStatus(collection, 'heInvited') -} - -const updateHandlingEditor = async (collection, isAccepted) => { - collection.handlingEditor.hasAnswer = true - collection.handlingEditor.isAccepted = isAccepted - collection.handlingEditor.respondedOn = Date.now() - let status - isAccepted ? (status = 'heAssigned') : (status = 'submitted') - await updateStatus(collection, status) -} - -const getFragmentAndAuthorData = async ({ - UserModel, - FragmentModel, - collection: { fragments, authors }, -}) => { - const fragment = await FragmentModel.find(last(fragments)) - let { title, abstract } = fragment.metadata - title = title.replace(/<(.|\n)*?>/g, '') - abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : '' - - const submittingAuthorData = authors.find( - author => author.isSubmitting === true, - ) - const author = await UserModel.find(submittingAuthorData.userId) - const authorName = `${author.firstName} ${author.lastName}` - const authorsPromises = authors.map(async author => { - const user = await UserModel.find(author.userId) - return ` ${user.firstName} ${user.lastName}` - }) - const authorsList = await Promise.all(authorsPromises) - const { id } = fragment - return { title, authorName, id, abstract, authorsList } -} - -const updateReviewerCollectionStatus = async collection => { - const reviewerInvitations = collection.invitations.filter( - inv => inv.role === 'reviewer', - ) - if (reviewerInvitations.length === 0) - await updateStatus(collection, 'heAssigned') -} - -const updateStatus = async (collection, newStatus) => { - collection.status = newStatus - collection.visibleStatus = statuses[collection.status].private - await collection.save() -} - -module.exports = { - addHandlingEditor, - updateHandlingEditor, - getFragmentAndAuthorData, - updateReviewerCollectionStatus, - updateStatus, -} diff --git a/packages/component-invite/src/helpers/Invitation.js b/packages/component-invite/src/helpers/Invitation.js deleted file mode 100644 index c543f1f5435d0822a649c96144f14bfa5f93ea1b..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/helpers/Invitation.js +++ /dev/null @@ -1,95 +0,0 @@ -const uuid = require('uuid') -const collectionHelper = require('./Collection') - -const getInvitationData = (invitations, userId, role) => { - const matchingInvitation = invitations.find( - inv => inv.role === role && inv.userId === userId, - ) - let status = 'pending' - if (matchingInvitation.isAccepted) { - status = 'accepted' - } else if (matchingInvitation.hasAnswer) { - status = 'declined' - } - - const { invitedOn, respondedOn, id } = matchingInvitation - return { invitedOn, respondedOn, status, id } -} - -const setupInvitation = async (userId, role, collection) => { - const invitation = { - role, - hasAnswer: false, - isAccepted: false, - invitedOn: Date.now(), - id: uuid.v4(), - userId, - respondedOn: null, - } - collection.invitations = collection.invitations || [] - collection.invitations.push(invitation) - collection = await collection.save() - return invitation -} - -const setupReviewerInvitation = async ({ - baseUrl, - FragmentModel, - UserModel, - collection, - user, - mailService, - invitationId, - timestamp, - resend = false, -}) => { - const { - title, - authorName, - id, - abstract, - authorsList, - } = await collectionHelper.getFragmentAndAuthorData({ - UserModel, - FragmentModel, - collection, - }) - - const params = { - user, - baseUrl, - subject: `${collection.customId}: Review Requested`, - toEmail: user.email, - meta: { - fragment: { - id, - title, - abstract, - authors: authorsList, - }, - invitation: { - id: invitationId, - timestamp, - }, - collection: { - id: collection.id, - authorName, - handlingEditor: collection.handlingEditor, - }, - }, - emailType: resend ? 'resend-reviewer' : 'invite-reviewer', - } - await mailService.sendReviewerInvitationEmail(params) -} - -const getInvitation = (invitations = [], userId, role) => - invitations.find( - invitation => invitation.userId === userId && invitation.role === role, - ) - -module.exports = { - getInvitationData, - setupInvitation, - setupReviewerInvitation, - getInvitation, -} diff --git a/packages/component-invite/src/helpers/Team.js b/packages/component-invite/src/helpers/Team.js deleted file mode 100644 index 8092336a8cdd94720eea378cd4dfae14a3453f7f..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/helpers/Team.js +++ /dev/null @@ -1,125 +0,0 @@ -const logger = require('@pubsweet/logger') -const get = require('lodash/get') - -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 - case 'author': - permissions = 'author' - group = 'author' - name = 'author' - break - default: - break - } - - const teamBody = { - teamType: { - name: role, - permissions, - }, - group, - name, - object: { - type: 'collection', - id: collectionId, - }, - members: [userId], - } - let team = new TeamModel(teamBody) - team = await team.save() - return team -} - -const setupManuscriptTeam = async (models, user, collectionId, role) => { - const teams = await models.Team.all() - user.teams = user.teams || [] - let foundTeam = teams.find( - team => - team.group === role && - team.object.type === 'collection' && - team.object.id === collectionId, - ) - - if (foundTeam !== undefined) { - if (!foundTeam.members.includes(user.id)) { - foundTeam.members.push(user.id) - } - - try { - foundTeam = await foundTeam.save() - if (!user.teams.includes(foundTeam.id)) { - user.teams.push(foundTeam.id) - await user.save() - } - return foundTeam - } catch (e) { - logger.error(e) - } - } else { - const team = await createNewTeam(collectionId, role, user.id, models.Team) - user.teams.push(team.id) - await user.save() - return team - } -} - -const removeTeamMember = async (teamId, userId, TeamModel) => { - const team = await TeamModel.find(teamId) - const members = team.members.filter(member => member !== userId) - team.members = members - await team.save() -} - -const getTeamMembersByCollection = async (collectionId, role, TeamModel) => { - const teams = await TeamModel.all() - const members = get( - teams.find( - team => - team.group === role && - team.object.type === 'collection' && - team.object.id === collectionId, - ), - 'members', - ) - - return members -} - -const getTeamByGroupAndCollection = async (collectionId, role, TeamModel) => { - const teams = await TeamModel.all() - return teams.find( - team => - team.group === role && - team.object.type === 'collection' && - team.object.id === collectionId, - ) -} - -const updateHETeam = async (collection, role, TeamModel, user) => { - const team = await getTeamByGroupAndCollection(collection.id, role, TeamModel) - delete collection.handlingEditor - await removeTeamMember(team.id, user.id, TeamModel) - user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) - await user.save() - await collection.save() -} - -module.exports = { - createNewTeam, - setupManuscriptTeam, - removeTeamMember, - getTeamMembersByCollection, - getTeamByGroupAndCollection, - updateHETeam, -} diff --git a/packages/component-invite/src/helpers/User.js b/packages/component-invite/src/helpers/User.js deleted file mode 100644 index 4b293a2ad476f3ccf4269869234985b7b6dc0c05..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/helpers/User.js +++ /dev/null @@ -1,129 +0,0 @@ -const helpers = require('./helpers') -const mailService = require('pubsweet-component-mail-service') -const logger = require('@pubsweet/logger') -const collectionHelper = require('./Collection') - -const setupNewUser = async ( - body, - url, - res, - email, - role, - UserModel, - invitationType, -) => { - const { firstName, lastName, affiliation, title } = body - const newUser = await helpers.createNewUser( - email, - firstName, - lastName, - affiliation, - title, - UserModel, - role, - ) - - try { - if (role !== 'reviewer') { - await mailService.sendSimpleEmail({ - toEmail: newUser.email, - user: newUser, - emailType: invitationType, - dashboardUrl: url, - }) - } - - return newUser - } catch (e) { - logger.error(e.message) - return { status: 500, error: 'Email could not be sent.' } - } -} - -const getEditorInChief = async UserModel => { - const users = await UserModel.all() - const eic = users.find(user => user.editorInChief || user.admin) - return eic -} - -const setupReviewerDecisionEmailData = async ({ - baseUrl, - UserModel, - FragmentModel, - collection, - user, - mailService, - agree, - timestamp = Date.now(), -}) => { - const { - title, - authorName, - id, - } = await collectionHelper.getFragmentAndAuthorData({ - UserModel, - FragmentModel, - collection, - }) - const eic = await getEditorInChief(UserModel) - const toEmail = collection.handlingEditor.email - await mailService.sendNotificationEmail({ - toEmail, - user, - emailType: agree ? 'reviewer-agreed' : 'reviewer-declined', - meta: { - collection: { customId: collection.customId, id: collection.id }, - fragment: { id, title, authorName }, - handlingEditorName: collection.handlingEditor.name, - baseUrl, - eicName: `${eic.firstName} ${eic.lastName}`, - timestamp, - }, - }) - if (agree) - await mailService.sendNotificationEmail({ - toEmail: user.email, - user, - emailType: 'reviewer-thank-you', - meta: { - collection: { customId: collection.customId, id: collection.id }, - fragment: { id, title, authorName }, - handlingEditorName: collection.handlingEditor.name, - baseUrl, - }, - }) -} - -const setupReviewerUnassignEmail = async ({ - UserModel, - FragmentModel, - collection, - user, - mailService, -}) => { - const { title, authorName } = await collectionHelper.getFragmentAndAuthorData( - { - UserModel, - FragmentModel, - collection, - }, - ) - - await mailService.sendNotificationEmail({ - toEmail: user.email, - user, - emailType: 'unassign-reviewer', - meta: { - collection: { customId: collection.customId }, - fragment: { title, authorName }, - handlingEditorName: collection.handlingEditor.name, - }, - }) -} - -module.exports = { - setupNewUser, - getEditorInChief, - setupReviewerDecisionEmailData, - setupReviewerUnassignEmail, -} diff --git a/packages/component-invite/src/helpers/helpers.js b/packages/component-invite/src/helpers/helpers.js deleted file mode 100644 index b484721469b65e466195b899a64c3a0c40bed8da..0000000000000000000000000000000000000000 --- a/packages/component-invite/src/helpers/helpers.js +++ /dev/null @@ -1,123 +0,0 @@ -const logger = require('@pubsweet/logger') -const uuid = require('uuid') -const crypto = require('crypto') - -const checkForUndefinedParams = (...params) => { - if (params.includes(undefined)) { - return false - } - - return true -} - -const validateEmailAndToken = async (email, token, userModel) => { - try { - const user = await userModel.findByEmail(email) - if (user) { - if (token !== user.passwordResetToken) { - logger.error( - `invite pw reset tokens do not match: REQ ${token} vs. DB ${ - user.passwordResetToken - }`, - ) - return { - success: false, - status: 400, - message: 'invalid request', - } - } - return { success: true, user } - } - } catch (e) { - if (e.name === 'NotFoundError') { - logger.error('invite pw reset on non-existing user') - return { - success: false, - status: 404, - message: 'user not found', - } - } else if (e.name === 'ValidationError') { - logger.error('invite pw reset validation error') - return { - success: false, - status: 400, - message: e.details[0].message, - } - } - logger.error('internal server error') - return { - success: false, - status: 500, - message: e.details[0].message, - } - } - return { - success: false, - status: 500, - message: 'something went wrong', - } -} - -const handleNotFoundError = async (error, item) => { - const response = { - success: false, - status: 500, - message: 'Something went wrong', - } - if (error.name === 'NotFoundError') { - logger.error(`invalid ${item} id`) - response.status = 404 - response.message = `${item} not found` - return response - } - - logger.error(error) - return response -} - -const createNewUser = async ( - email, - firstName, - lastName, - affiliation, - title, - UserModel, - role, -) => { - const username = email - const password = uuid.v4() - const userBody = { - username, - email, - password, - passwordResetToken: crypto.randomBytes(32).toString('hex'), - isConfirmed: false, - firstName, - lastName, - affiliation, - title, - editorInChief: role === 'editorInChief', - admin: role === 'admin', - handlingEditor: role === 'handlingEditor', - invitationToken: role === 'reviewer' ? uuid.v4() : '', - } - - let newUser = new UserModel(userBody) - - try { - newUser = await newUser.save() - return newUser - } catch (e) { - logger.error(e) - } -} - -const getBaseUrl = req => `${req.protocol}://${req.get('host')}` - -module.exports = { - checkForUndefinedParams, - validateEmailAndToken, - handleNotFoundError, - createNewUser, - getBaseUrl, -} diff --git a/packages/component-invite/src/routes/collectionsInvitations/decline.js b/packages/component-invite/src/routes/collectionsInvitations/decline.js index fc88ffe4f4b69da2bf0481db895e247b90864b49..b049a4a0aeb43f096b0ec673b338d91202440cc4 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/decline.js +++ b/packages/component-invite/src/routes/collectionsInvitations/decline.js @@ -1,16 +1,22 @@ -const helpers = require('../../helpers/helpers') -const mailService = require('pubsweet-component-mail-service') -const userHelper = require('../../helpers/User') +const last = require('lodash/last') + +const { + services, + Email, + Fragment, + Collection, +} = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const { invitationToken } = req.body - if (!helpers.checkForUndefinedParams(invitationToken)) + if (!services.checkForUndefinedParams(invitationToken)) return res.status(400).json({ error: 'Token is required' }) + const UserModel = models.User try { - const user = await models.User.findOneByField( + const user = await UserModel.findOneByField( 'invitationToken', invitationToken, ) @@ -37,18 +43,32 @@ module.exports = models => async (req, res) => { invitation.hasAnswer = true invitation.isAccepted = false await collection.save() - return await userHelper.setupReviewerDecisionEmailData({ - baseUrl: helpers.getBaseUrl(req), - UserModel: models.User, - FragmentModel: models.Fragment, + const collectionHelper = new Collection({ collection }) + const fragment = await models.Fragment.find(last(collection.fragments)) + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await collectionHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, collection, - reviewerName: `${user.firstName} ${user.lastName}`, - mailService, + parsedFragment, + baseUrl, + authors, + }) + emailHelper.setupReviewerDecisionEmail({ + authorName: `${submittingAuthor.firstName} ${submittingAuthor.lastName}`, agree: false, user, }) + return res.status(200).json({}) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'item') + const notFoundError = await services.handleNotFoundError(e, 'item') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/delete.js b/packages/component-invite/src/routes/collectionsInvitations/delete.js index d1640d5d78afd6aab05dbd8aa74c13f3c150bbeb..e3e69c0ecc26e772b0428004df9fa9134010d141 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/delete.js +++ b/packages/component-invite/src/routes/collectionsInvitations/delete.js @@ -1,17 +1,25 @@ -const helpers = require('../../helpers/helpers') -const teamHelper = require('../../helpers/Team') -const mailService = require('pubsweet-component-mail-service') -const logger = require('@pubsweet/logger') const config = require('config') -const userHelper = require('../../helpers/User') -const collectionHelper = require('../../helpers/Collection') -const authsomeHelper = require('../../helpers/authsome') +const logger = require('@pubsweet/logger') +const last = require('lodash/last') +const mailService = require('pubsweet-component-mail-service') + +const { + services, + Team, + Email, + Fragment, + Collection, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') const statuses = config.get('statuses') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params + const teamHelper = new Team({ TeamModel: models.Team, collectionId }) + try { const collection = await models.Collection.find(collectionId) + const collectionHelper = new Collection({ collection }) const authsome = authsomeHelper.getAuthsome(models) const target = { collection, @@ -31,11 +39,10 @@ module.exports = models => async (req, res) => { }) return } - const team = await teamHelper.getTeamByGroupAndCollection( - collectionId, - invitation.role, - models.Team, - ) + + const team = await teamHelper.getTeamByGroupAndCollection({ + role: invitation.role, + }) collection.invitations = collection.invitations.filter( inv => inv.id !== invitation.id, @@ -45,11 +52,15 @@ module.exports = models => async (req, res) => { collection.visibleStatus = statuses[collection.status].private delete collection.handlingEditor } else if (invitation.role === 'reviewer') { - await collectionHelper.updateReviewerCollectionStatus(collection) + await collectionHelper.updateStatusByNumberOfReviewers() } await collection.save() - await teamHelper.removeTeamMember(team.id, invitation.userId, models.Team) - const user = await models.User.find(invitation.userId) + await teamHelper.removeTeamMember({ + teamId: team.id, + userId: invitation.userId, + }) + const UserModel = models.User + const user = await UserModel.find(invitation.userId) user.teams = user.teams.filter(userTeamId => team.id !== userTeamId) await user.save() try { @@ -59,12 +70,29 @@ module.exports = models => async (req, res) => { emailType: 'revoke-handling-editor', }) } else if (invitation.role === 'reviewer') { - await userHelper.setupReviewerUnassignEmail({ - UserModel: models.User, - FragmentModel: models.Fragment, + const collectionHelper = new Collection({ collection }) + const fragment = await models.Fragment.find(last(collection.fragments)) + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await collectionHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, collection, + parsedFragment, + baseUrl, + authors, + }) + emailHelper.setupReviewerUnassignEmail({ user, - mailService, + authorName: `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }`, }) } @@ -74,7 +102,7 @@ module.exports = models => async (req, res) => { return res.status(500).json({ error: 'Email could not be sent.' }) } } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/get.js b/packages/component-invite/src/routes/collectionsInvitations/get.js index 308a57670a9030a82833f445fef7855083077edb..18e1e0eb7a8a8c08a0fb5f3c34e220c91c1678c7 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/get.js +++ b/packages/component-invite/src/routes/collectionsInvitations/get.js @@ -1,13 +1,16 @@ -const helpers = require('../../helpers/helpers') -const teamHelper = require('../../helpers/Team') const config = require('config') -const invitationHelper = require('../../helpers/Invitation') -const authsomeHelper = require('../../helpers/authsome') +const { + services, + Team, + Invitation, + authsome: authsomeHelper, +} = require('pubsweet-component-helper-service') const configRoles = config.get('roles') + module.exports = models => async (req, res) => { const { role } = req.query - if (!helpers.checkForUndefinedParams(role)) { + if (!services.checkForUndefinedParams(role)) { res.status(400).json({ error: 'Role is required' }) return } @@ -16,7 +19,10 @@ module.exports = models => async (req, res) => { res.status(400).json({ error: `Role ${role} is invalid` }) return } + const { collectionId } = req.params + const teamHelper = new Team({ TeamModel: models.Team, collectionId }) + try { const collection = await models.Collection.find(collectionId) const authsome = authsomeHelper.getAuthsome(models) @@ -25,31 +31,31 @@ module.exports = models => async (req, res) => { path: req.route.path, } const canGet = await authsome.can(req.user, 'GET', target) + if (!canGet) return res.status(403).json({ error: 'Unauthorized.', }) - const members = await teamHelper.getTeamMembersByCollection( - collectionId, - role, - models.Team, - ) + const members = await teamHelper.getTeamMembersByCollection({ + role, + }) if (members === undefined) return res.status(200).json([]) // TO DO: handle case for when the invitationID is provided const membersData = members.map(async member => { const user = await models.User.find(member) + const invitationHelper = new Invitation({ userId: user.id, role }) + const { invitedOn, respondedOn, status, id, - } = invitationHelper.getInvitationData( - collection.invitations, - user.id, - role, - ) + } = invitationHelper.getInvitationsData({ + invitations: collection.invitations, + }) + return { name: `${user.firstName} ${user.lastName}`, invitedOn, @@ -64,7 +70,7 @@ module.exports = models => async (req, res) => { const resBody = await Promise.all(membersData) res.status(200).json(resBody) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/patch.js b/packages/component-invite/src/routes/collectionsInvitations/patch.js index 657ba2794843e4e5df353e2b52f26de5b85161f1..69d6302f6184d69ead39341d5867748bec23477d 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/patch.js +++ b/packages/component-invite/src/routes/collectionsInvitations/patch.js @@ -1,20 +1,27 @@ const logger = require('@pubsweet/logger') -const helpers = require('../../helpers/helpers') -const teamHelper = require('../../helpers/Team') const mailService = require('pubsweet-component-mail-service') -const userHelper = require('../../helpers/User') -const collectionHelper = require('../../helpers/Collection') +const last = require('lodash/last') + +const { + Email, + services, + Fragment, + Collection, + Team, + User, +} = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { const { collectionId, invitationId } = req.params const { isAccepted, reason } = req.body - if (!helpers.checkForUndefinedParams(isAccepted)) { + if (!services.checkForUndefinedParams(isAccepted)) { res.status(400).json({ error: 'Missing parameters.' }) logger.error('some parameters are missing') return } - let user = await models.User.find(req.user) + const UserModel = models.User + const user = await UserModel.find(req.user) try { const collection = await models.Collection.find(collectionId) const invitation = await collection.invitations.find( @@ -33,19 +40,32 @@ module.exports = models => async (req, res) => { error: `User is not allowed to modify this invitation.`, }) - const params = { - baseUrl: helpers.getBaseUrl(req), - UserModel: models.User, - FragmentModel: models.Fragment, + const collectionHelper = new Collection({ collection }) + const fragment = await models.Fragment.find(last(collection.fragments)) + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor: collection.handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await collectionHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, collection, - reviewerName: `${user.firstName} ${user.lastName}`, - mailService, - } + parsedFragment, + baseUrl, + authors, + }) + const teamHelper = new Team({ TeamModel: models.Team, collectionId }) + const userHelper = new User({ UserModel }) + if (invitation.role === 'handlingEditor') - await collectionHelper.updateHandlingEditor(collection, isAccepted) + await collectionHelper.updateHandlingEditor({ isAccepted }) invitation.respondedOn = Date.now() invitation.hasAnswer = true - const eic = await userHelper.getEditorInChief(models.User) + const eic = await userHelper.getEditorInChief() const toEmail = eic.email if (isAccepted === true) { invitation.isAccepted = true @@ -53,7 +73,7 @@ module.exports = models => async (req, res) => { invitation.role === 'reviewer' && collection.status === 'reviewersInvited' ) - await collectionHelper.updateStatus(collection, 'underReview') + await collectionHelper.updateStatus({ newStatus: 'underReview' }) await collection.save() try { @@ -62,17 +82,19 @@ module.exports = models => async (req, res) => { toEmail, user, emailType: 'handling-editor-agreed', - dashboardUrl: `${req.protocol}://${req.get('host')}`, + dashboardUrl: baseUrl, meta: { collectionId: collection.customId, }, }) if (invitation.role === 'reviewer') - await userHelper.setupReviewerDecisionEmailData({ - ...params, + emailHelper.setupReviewerDecisionEmail({ agree: true, timestamp: invitation.respondedOn, user, + authorName: `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }`, }) return res.status(200).json(invitation) } catch (e) { @@ -83,12 +105,11 @@ module.exports = models => async (req, res) => { invitation.isAccepted = false if (invitation.role === 'handlingEditor') - await teamHelper.updateHETeam( + await teamHelper.updateHETeam({ collection, - invitation.role, - models.Team, + role: invitation.role, user, - ) + }) if (reason !== undefined) { invitation.reason = reason } @@ -106,9 +127,8 @@ module.exports = models => async (req, res) => { }, }) } else if (invitation.role === 'reviewer') { - await collectionHelper.updateReviewerCollectionStatus(collection) - await userHelper.setupReviewerDecisionEmailData({ - ...params, + collectionHelper.updateStatusByNumberOfReviewers() + emailHelper.setupReviewerDecisionEmail({ agree: false, user, }) @@ -118,10 +138,10 @@ module.exports = models => async (req, res) => { return res.status(500).json({ error: 'Email could not be sent.' }) } } - user = await user.save() + user.save() return res.status(200).json(invitation) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/routes/collectionsInvitations/post.js b/packages/component-invite/src/routes/collectionsInvitations/post.js index 8f21d2c11d3ce636a6998febd9d71a6da31e5d40..3bafaf954c6276637c2195b93ff0651ab53cdb05 100644 --- a/packages/component-invite/src/routes/collectionsInvitations/post.js +++ b/packages/component-invite/src/routes/collectionsInvitations/post.js @@ -1,20 +1,25 @@ -const logger = require('@pubsweet/logger') -const get = require('lodash/get') -const helpers = require('../../helpers/helpers') -const collectionHelper = require('../../helpers/Collection') -const teamHelper = require('../../helpers/Team') const config = require('config') +const get = require('lodash/get') +const last = require('lodash/last') +const logger = require('@pubsweet/logger') const mailService = require('pubsweet-component-mail-service') -const userHelper = require('../../helpers/User') -const invitationHelper = require('../../helpers/Invitation') -const authsomeHelper = require('../../helpers/authsome') +const { + Email, + services, + authsome: authsomeHelper, + Fragment, + Collection, + Team, + Invitation, + User, +} = require('pubsweet-component-helper-service') const configRoles = config.get('roles') module.exports = models => async (req, res) => { const { email, role } = req.body - if (!helpers.checkForUndefinedParams(email, role)) { + if (!services.checkForUndefinedParams(email, role)) { res.status(400).json({ error: 'Email and role are required' }) logger.error('User ID and role are missing') return @@ -25,7 +30,8 @@ module.exports = models => async (req, res) => { logger.error(`invitation attempted on invalid role ${role}`) return } - const reqUser = await models.User.find(req.user) + const UserModel = models.User + const reqUser = await UserModel.find(req.user) if (email === reqUser.email && !reqUser.admin) { logger.error(`${reqUser.email} tried to invite his own email`) @@ -37,7 +43,7 @@ module.exports = models => async (req, res) => { try { collection = await models.Collection.find(collectionId) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'collection') + const notFoundError = await services.handleNotFoundError(e, 'collection') return res.status(notFoundError.status).json({ error: notFoundError.message, }) @@ -53,25 +59,36 @@ module.exports = models => async (req, res) => { return res.status(403).json({ error: 'Unauthorized.', }) - const baseUrl = `${req.protocol}://${req.get('host')}` - const params = { - baseUrl, - FragmentModel: models.Fragment, - UserModel: models.User, + + const collectionHelper = new Collection({ collection }) + const fragment = await models.Fragment.find(last(collection.fragments)) + const fragmentHelper = new Fragment({ fragment }) + const handlingEditor = collection.handlingEditor || {} + const parsedFragment = await fragmentHelper.getFragmentData({ + handlingEditor, + }) + const baseUrl = services.getBaseUrl(req) + const { + authorsList: authors, + submittingAuthor, + } = await collectionHelper.getAuthorData({ UserModel }) + const emailHelper = new Email({ + UserModel, collection, - mailService, - resend: false, - } + parsedFragment, + baseUrl, + authors, + }) + const teamHelper = new Team({ TeamModel: models.Team, collectionId }) + const invitationHelper = new Invitation({ role }) try { - const user = await models.User.findByEmail(email) - - await teamHelper.setupManuscriptTeam(models, user, collectionId, role) - let invitation = invitationHelper.getInvitation( - collection.invitations, - user.id, - role, - ) + const user = await UserModel.findByEmail(email) + await teamHelper.setupManuscriptTeam({ user, role }) + invitationHelper.userId = user.id + let invitation = invitationHelper.getInvitation({ + invitations: collection.invitations, + }) let resend = false if (invitation !== undefined) { @@ -83,32 +100,32 @@ module.exports = models => async (req, res) => { await collection.save() resend = true } else { - invitation = await invitationHelper.setupInvitation( - user.id, - role, + invitation = await invitationHelper.setupInvitation({ collection, - ) + }) } try { if (role === 'reviewer') { if (collection.status === 'heAssigned') - await collectionHelper.updateStatus(collection, 'reviewersInvited') + await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) - await invitationHelper.setupReviewerInvitation({ - ...params, + await emailHelper.setupReviewerInvitationEmail({ user, invitationId: invitation.id, timestamp: invitation.invitedOn, resend, + authorName: `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }`, }) } if (role === 'handlingEditor') { invitation.invitedOn = Date.now() await collection.save() - await collectionHelper.addHandlingEditor(collection, user, invitation) - await mailService.sendSimpleEmail({ + await collectionHelper.addHandlingEditor({ user, invitation }) + mailService.sendSimpleEmail({ toEmail: user.email, user, emailType: 'assign-handling-editor', @@ -121,39 +138,38 @@ module.exports = models => async (req, res) => { return res.status(500).json({ error: 'Email could not be sent.' }) } } catch (e) { + const userHelper = new User({ UserModel }) if (role === 'reviewer') { - const newUser = await userHelper.setupNewUser( - req.body, - baseUrl, - res, - email, + const newUser = await userHelper.setupNewUser({ + url: baseUrl, role, - models.User, - 'invite', - ) + invitationType: 'invite', + body: req.body, + }) if (newUser.error !== undefined) { return res.status(newUser.status).json({ error: newUser.message, }) } if (collection.status === 'heAssigned') - await collectionHelper.updateStatus(collection, 'reviewersInvited') - await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role) - const invitation = await invitationHelper.setupInvitation( - newUser.id, - role, + await collectionHelper.updateStatus({ newStatus: 'reviewersInvited' }) + await teamHelper.setupManuscriptTeam({ user: newUser, role }) + invitationHelper.userId = newUser.id + const invitation = await invitationHelper.setupInvitation({ collection, - ) + }) - await invitationHelper.setupReviewerInvitation({ - ...params, + await emailHelper.setupReviewerInvitationEmail({ user: newUser, invitationId: invitation.id, timestamp: invitation.invitedOn, + authorName: `${submittingAuthor.firstName} ${ + submittingAuthor.lastName + }`, }) return res.status(200).json(invitation) } - const notFoundError = await helpers.handleNotFoundError(e, 'user') + const notFoundError = await services.handleNotFoundError(e, 'user') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-invite/src/tests/collectionsInvitations/get.test.js b/packages/component-invite/src/tests/collectionsInvitations/get.test.js index 105187c0a834d5c5614b9346318ccdbb715ac8c0..7cf8a64115d3cfd58db7db627638ee37d296f43d 100644 --- a/packages/component-invite/src/tests/collectionsInvitations/get.test.js +++ b/packages/component-invite/src/tests/collectionsInvitations/get.test.js @@ -6,6 +6,11 @@ const Model = require('./../helpers/Model') const cloneDeep = require('lodash/cloneDeep') const requests = require('./../helpers/requests') +jest.mock('pubsweet-component-mail-service', () => ({ + sendSimpleEmail: jest.fn(), + sendNotificationEmail: jest.fn(), + sendReviewerInvitationEmail: jest.fn(), +})) const path = '../../routes/collectionsInvitations/get' const route = { path: '/api/collections/:collectionId/invitations/:invitationId?', diff --git a/packages/component-invite/src/tests/fixtures/fragments.js b/packages/component-invite/src/tests/fixtures/fragments.js index 32379359150362143172390427a5113121ad9233..08d0eedf3a531c317cbfb79e58a6d1b019413564 100644 --- a/packages/component-invite/src/tests/fixtures/fragments.js +++ b/packages/component-invite/src/tests/fixtures/fragments.js @@ -8,6 +8,7 @@ const fragments = { title: chance.sentence(), abstract: chance.paragraph(), }, + recommendations: [], }, } diff --git a/packages/component-manuscript-manager/package.json b/packages/component-manuscript-manager/package.json index 9d40f80a6ae8f8706e7e0fc235e9940a444c8b6b..55ffd9d800fbc02181a01ef7821f74637be8296b 100644 --- a/packages/component-manuscript-manager/package.json +++ b/packages/component-manuscript-manager/package.json @@ -25,7 +25,8 @@ "peerDependencies": { "@pubsweet/logger": "^0.0.1", "pubsweet-component-mail-service": "0.0.1", - "pubsweet-server": "^1.0.1" + "pubsweet-server": "^1.0.1", + "component-helper-service": "0.0.1" }, "devDependencies": { "apidoc": "^0.17.6", diff --git a/packages/component-manuscript-manager/src/helpers/Collection.js b/packages/component-manuscript-manager/src/helpers/Collection.js deleted file mode 100644 index 7868b5b9481903203443d0a63c8fc0a716c95d65..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/helpers/Collection.js +++ /dev/null @@ -1,98 +0,0 @@ -const config = require('config') - -const statuses = config.get('statuses') - -const updateStatusByRecommendation = async (collection, recommendation) => { - let newStatus = 'pendingApproval' - if (['minor', 'major'].includes(recommendation)) - newStatus = 'revisionRequested' - collection.status = newStatus - collection.visibleStatus = statuses[collection.status].private - await collection.save() -} - -const updateFinalStatusByRecommendation = async ( - collection, - recommendation, -) => { - let newStatus - switch (recommendation) { - case 'reject': - newStatus = 'rejected' - break - case 'publish': - newStatus = 'published' - break - case 'return-to-handling-editor': - newStatus = 'reviewCompleted' - break - default: - break - } - collection.status = newStatus - collection.visibleStatus = statuses[collection.status].private - await collection.save() -} - -const updateStatus = async (collection, newStatus) => { - collection.status = newStatus - collection.visibleStatus = statuses[collection.status].private - await collection.save() -} - -const getFragmentData = async ({ fragment, handlingEditor }) => { - const heRecommendation = fragment.recommendations.find( - rec => rec.userId === handlingEditor.id, - ) - let { title, abstract } = fragment.metadata - const { type } = fragment.metadata - title = title.replace(/<(.|\n)*?>/g, '') - abstract = abstract ? abstract.replace(/<(.|\n)*?>/g, '') : '' - - return { - title, - abstract, - type, - heRecommendation, - recommendations: fragment.recommendations, - id: fragment.id, - } -} - -const getAuthorData = async ({ authors, UserModel }) => { - const submittingAuthorData = authors.find( - author => author.isSubmitting === true, - ) - const submittingAuthor = await UserModel.find(submittingAuthorData.userId) - const authorsPromises = authors.map(async author => { - const user = await UserModel.find(author.userId) - return `${user.firstName} ${user.lastName}` - }) - const authorsList = await Promise.all(authorsPromises) - - return { - authorsList, - submittingAuthor, - } -} - -const getReviewerInvitations = (invitations = [], agree = true) => - agree - ? invitations.filter( - inv => - inv.role === 'reviewer' && - inv.hasAnswer === true && - inv.isAccepted === true, - ) - : invitations.filter( - inv => inv.role === 'reviewer' && inv.hasAnswer === false, - ) - -module.exports = { - updateStatusByRecommendation, - getFragmentData, - getAuthorData, - getReviewerInvitations, - updateStatus, - updateFinalStatusByRecommendation, -} diff --git a/packages/component-manuscript-manager/src/helpers/authsome.js b/packages/component-manuscript-manager/src/helpers/authsome.js deleted file mode 100644 index 212cee2a3ea23a424b1f77dde2f7dd4cf888b2a0..0000000000000000000000000000000000000000 --- a/packages/component-manuscript-manager/src/helpers/authsome.js +++ /dev/null @@ -1,29 +0,0 @@ -const config = require('config') -const Authsome = require('authsome') - -const mode = require(config.get('authsome.mode')) - -const getAuthsome = models => - new Authsome( - { ...config.authsome, mode }, - { - // restrict methods passed to mode since these have to be shimmed on client - // any changes here should be reflected in the `withAuthsome` component of `pubsweet-client` - models: { - Collection: { - find: id => models.Collection.find(id), - }, - Fragment: { - find: id => models.Fragment.find(id), - }, - User: { - find: id => models.User.find(id), - }, - Team: { - find: id => models.Team.find(id), - }, - }, - }, - ) - -module.exports = { getAuthsome } diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js index e629034244dd04c370ad6fde7d3bbef8797f8bd8..fc771b67cbee2448a50bbad958be986c0c7b96d7 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/patch.js @@ -1,17 +1,25 @@ -const helpers = require('../../helpers/helpers') -const authsomeHelper = require('../../helpers/authsome') -const collectionHelper = require('../../helpers/Collection') -const Email = require('../../helpers/Email') +const { + Email, + services, + authsome: authsomeHelper, + Fragment, + Collection, +} = require('pubsweet-component-helper-service') +const logger = require('@pubsweet/logger') module.exports = models => async (req, res) => { const { collectionId, fragmentId, recommendationId } = req.params let collection, fragment try { collection = await models.Collection.find(collectionId) - if (!collection.fragments.includes(fragmentId)) + if (!collection.fragments.includes(fragmentId)) { + logger.error( + `Collection ${collectionId} does not contain fragment ${fragmentId}`, + ) return res.status(400).json({ error: `Collection and fragment do not match.`, }) + } const authsome = authsomeHelper.getAuthsome(models) const target = { collection, @@ -20,10 +28,15 @@ module.exports = models => async (req, res) => { const UserModel = models.User const user = await UserModel.find(req.user) const canPatch = await authsome.can(req.user, 'PATCH', target) - if (!canPatch) + if (!canPatch) { + logger.error( + `User ${req.user} is not allowed to access Collection ${collectionId}`, + ) return res.status(403).json({ error: 'Unauthorized.', }) + } + fragment = await models.Fragment.find(fragmentId) const recommendation = fragment.recommendations.find( rec => rec.id === recommendationId, @@ -37,13 +50,13 @@ module.exports = models => async (req, res) => { Object.assign(recommendation, req.body) recommendation.updatedOn = Date.now() if (req.body.submittedOn) { - const parsedFragment = await collectionHelper.getFragmentData({ - fragment, + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, }) - const baseUrl = helpers.getBaseUrl(req) + const baseUrl = services.getBaseUrl(req) + const collectionHelper = new Collection({ collection }) const authors = await collectionHelper.getAuthorData({ - authors: collection.authors, UserModel, }) const email = new Email({ @@ -58,12 +71,12 @@ module.exports = models => async (req, res) => { reviewerName: `${user.firstName} ${user.lastName}`, }) if (!['pendingApproval', 'revisionRequested'].includes(collection.status)) - collectionHelper.updateStatus(collection, 'reviewCompleted') + collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) } await fragment.save() return res.status(200).json(recommendation) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'Item') + const notFoundError = await services.handleNotFoundError(e, 'Item') return res.status(notFoundError.status).json({ error: notFoundError.message, }) diff --git a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js index c56ec1408a965228941ae92a2bf0dec22fe23729..d5a4a500385cc43e771d01758a78137236320ad7 100644 --- a/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js +++ b/packages/component-manuscript-manager/src/routes/fragmentsRecommendations/post.js @@ -1,13 +1,15 @@ const uuid = require('uuid') - -const helpers = require('../../helpers/helpers') -const Email = require('../../helpers/Email') -const authsomeHelper = require('../../helpers/authsome') -const collectionHelper = require('../../helpers/Collection') +const { + Email, + services, + authsome: authsomeHelper, + Fragment, + Collection, +} = require('pubsweet-component-helper-service') module.exports = models => async (req, res) => { const { recommendation, comments, recommendationType } = req.body - if (!helpers.checkForUndefinedParams(recommendationType)) + if (!services.checkForUndefinedParams(recommendationType)) return res.status(400).json({ error: 'Recommendation type is required.' }) const reqUser = await models.User.find(req.user) @@ -24,7 +26,7 @@ module.exports = models => async (req, res) => { fragment = await models.Fragment.find(fragmentId) } catch (e) { - const notFoundError = await helpers.handleNotFoundError(e, 'Item') + const notFoundError = await services.handleNotFoundError(e, 'Item') return res.status(notFoundError.status).json({ error: notFoundError.message, }) @@ -51,15 +53,13 @@ module.exports = models => async (req, res) => { newRecommendation.recommendation = recommendation || undefined newRecommendation.comments = comments || undefined const UserModel = models.User - const parsedFragment = await collectionHelper.getFragmentData({ - fragment, + const collectionHelper = new Collection({ collection }) + const fragmentHelper = new Fragment({ fragment }) + const parsedFragment = await fragmentHelper.getFragmentData({ handlingEditor: collection.handlingEditor, }) - const baseUrl = helpers.getBaseUrl(req) - const authors = await collectionHelper.getAuthorData({ - authors: collection.authors, - UserModel, - }) + const baseUrl = services.getBaseUrl(req) + const authors = await collectionHelper.getAuthorData({ UserModel }) const email = new Email({ UserModel, collection, @@ -70,12 +70,11 @@ module.exports = models => async (req, res) => { if (reqUser.editorInChief || reqUser.admin) { if (recommendation === 'return-to-handling-editor') - collectionHelper.updateStatus(collection, 'reviewCompleted') + collectionHelper.updateStatus({ newStatus: 'reviewCompleted' }) else { - collectionHelper.updateFinalStatusByRecommendation( - collection, + collectionHelper.updateFinalStatusByRecommendation({ recommendation, - ) + }) email.setupAuthorsEmail({ requestToRevision: false, publish: recommendation === 'publish', @@ -87,10 +86,11 @@ module.exports = models => async (req, res) => { email.setupReviewersEmail({ recommendation, isSubmitted: true, + agree: true, }) } } else if (recommendationType === 'editorRecommendation') { - collectionHelper.updateStatusByRecommendation(collection, recommendation) + collectionHelper.updateStatusByRecommendation({ recommendation }) email.setupReviewersEmail({ recommendation, agree: true, diff --git a/packages/component-user-manager/package.json b/packages/component-user-manager/package.json index 0d58cca77bc2e2b3c6162fae78e292c2d0a60e08..c65b0553de09100b8c477dfdab49531406d95536 100644 --- a/packages/component-user-manager/package.json +++ b/packages/component-user-manager/package.json @@ -25,7 +25,8 @@ "peerDependencies": { "@pubsweet/logger": "^0.0.1", "pubsweet-component-mail-service": "0.0.1", - "pubsweet-server": "^1.0.1" + "pubsweet-server": "^1.0.1", + "pubsweet-component-helper-service": "0.0.1" }, "devDependencies": { "apidoc": "^0.17.6",