diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ae3880d67e25804af152863f06ac78b60d791ae0..8e602d0f47c8537e0439236574f4864cd5e9f480 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -66,5 +66,5 @@ demo:now: - npm i -g --unsafe-perm now - cd ${HOME}/now - echo "FROM ${IMAGE_ORG}/${IMAGE_NAME}:${CI_COMMIT_SHA}" >> Dockerfile - - now --public --docker --token $NOW_TOKEN -e AWS_S3_ACCESS_KEY=$AWS_S3_ACCESS_KEY -e AWS_S3_SECRET_KEY=$AWS_S3_SECRET_KEY -e AWS_S3_REGION=$AWS_S3_REGION -e AWS_S3_BUCKET=$AWS_S3_BUCKET -e AWS_SES_SECRET_KEY=$AWS_SES_SECRET_KEY -e AWS_SES_ACCESS_KEY=$AWS_SES_ACCESS_KEY -e AWS_SES_REGION=$AWS_SES_REGION -e EMAIL_SENDER=$EMAIL_SENDER -e secret=$SECRET + - now --public --docker --token $NOW_TOKEN -e AWS_S3_ACCESS_KEY=$AWS_S3_ACCESS_KEY -e AWS_S3_SECRET_KEY=$AWS_S3_SECRET_KEY -e AWS_S3_REGION=$AWS_S3_REGION -e AWS_S3_BUCKET=$AWS_S3_BUCKET -e AWS_SES_SECRET_KEY=$AWS_SES_SECRET_KEY -e AWS_SES_ACCESS_KEY=$AWS_SES_ACCESS_KEY -e AWS_SES_REGION=$AWS_SES_REGION -e EMAIL_SENDER=$EMAIL_SENDER -e secret=$SECRET -e DATABASE=$DATABASE -e DB_USER=$DB_USER -e DB_PASS=$DB_PASS -e DB_HOST=$DB_HOST - now alias $NOW_URL xpub-faraday --token $NOW_TOKEN \ No newline at end of file diff --git a/packages/component-invite/config/test.js b/packages/component-invite/config/test.js index 9046f749c74d8697f323149d26e1b4b9ba81ab2c..98b13174ed64eaeceacbe719fd8b4d20b53cb6d8 100644 --- a/packages/component-invite/config/test.js +++ b/packages/component-invite/config/test.js @@ -9,11 +9,12 @@ module.exports = { }, roles: { global: ['admin', 'editorInChief', 'author', 'handlingEditor'], - collection: ['handlingEditor', 'reviewer'], + collection: ['handlingEditor', 'reviewer', 'coAuthor'], inviteRights: { - admin: ['admin', 'editorInChief', 'author', 'handlingEditor'], + admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'coAuthor'], editorInChief: ['handlingEditor'], handlingEditor: ['reviewer'], + author: ['coAuthor'], }, }, } diff --git a/packages/component-invite/src/controllers/assignCollectionRole.js b/packages/component-invite/src/controllers/assignCollectionRole.js index febc1a39305c6c4a5f182437bf75e8a3053cb287..be8f7f8d644dbd47a791cdcec7e65f633bdf6bd6 100644 --- a/packages/component-invite/src/controllers/assignCollectionRole.js +++ b/packages/component-invite/src/controllers/assignCollectionRole.js @@ -5,6 +5,7 @@ const teamHelper = require('../helpers/Team') const mailService = require('pubsweet-component-mail-service') const inviteHelper = require('../helpers/Invitation') const collHelper = require('../helpers/Collection') +const userHelper = require('../helpers/User') const configRoles = config.get('roles') @@ -16,7 +17,7 @@ module.exports = async ( collectionId, models, url, - resend, + body, ) => { if (!configRoles.collection.includes(role)) { logger.error(`invitation has been attempted with invalid role: ${role}`) @@ -25,19 +26,20 @@ module.exports = async ( .json({ error: `Role ${role} cannot be set on collections` }) } - if (!reqUser.editorInChief && reqUser.teams === undefined && !reqUser.admin) { - return res - .status(403) - .json({ error: `User ${reqUser.username} is not part of any teams` }) - } else if (reqUser.editorInChief === false && reqUser.admin === false) { - const matchingTeams = await teamHelper.getMatchingTeams( + if (reqUser.handlingEditor === true) { + if (reqUser.teams === undefined) { + return res.status(403).json({ + error: `Handling Editor ${reqUser.email} is not part of any teams`, + }) + } + const heTeams = await teamHelper.getMatchingTeams( reqUser.teams, models.Team, collectionId, role, ) - if (matchingTeams.length === 0) { + if (heTeams.length === 0) { return res.status(403).json({ error: `User ${ reqUser.email @@ -65,7 +67,18 @@ module.exports = async ( collectionId, role, ) + // get updated user from DB user = await models.User.findByEmail(email) + if (role === 'coAuthor') { + try { + await mailService.setupAssignEmail(user.email, 'assign-coauthor', url) + + return res.status(200).json(user) + } catch (e) { + logger.error(e) + return res.status(500).json({ error: 'Email could not be sent.' }) + } + } if (user.invitations === undefined) { user = await inviteHelper.setupInvitation( @@ -76,11 +89,6 @@ module.exports = async ( ) await collHelper.addAssignedPeople(collection, user, role) } else { - // const matchingInvitation = inviteHelper.getMatchingInvitation( - // user.invitations, - // collectionId, - // role, - // ) const matchingInvitation = user.invitations.find( invitation => invitation.collectionId === collectionId && @@ -111,6 +119,17 @@ module.exports = async ( return res.status(500).json({ error: 'Mail could not be sent.' }) } } catch (e) { + if (e.name === 'NotFoundError' && role === 'coAuthor') { + return userHelper.setupNewUser( + body, + url, + res, + email, + role, + models.User, + 'invite-coauthor', + ) + } const notFoundError = await helpers.handleNotFoundError(e, 'user') return res.status(notFoundError.status).json({ error: notFoundError.message, diff --git a/packages/component-invite/src/controllers/inviteGlobalRole.js b/packages/component-invite/src/controllers/inviteGlobalRole.js index 4cf92182fd3a3b8a52abc9081d3a043a5ad589ed..43ec7b51c00c26c793ac87846f6390278d9c4e83 100644 --- a/packages/component-invite/src/controllers/inviteGlobalRole.js +++ b/packages/component-invite/src/controllers/inviteGlobalRole.js @@ -1,12 +1,11 @@ const logger = require('@pubsweet/logger') -const helpers = require('../helpers/helpers') -const mailService = require('pubsweet-component-mail-service') +const userHelper = require('../helpers/User') const config = require('config') const configRoles = config.get('roles') module.exports = async (body, models, res, url) => { - const { email, role, firstName, lastName, affiliation, title } = body + const { email, role } = body if (!configRoles.inviteRights.admin.includes(role)) { logger.error(`admin ${email} tried to invite a ${role}`) @@ -27,28 +26,14 @@ module.exports = async (body, models, res, url) => { return res.status(500).json({ error: e.details[0].message }) } - const newUser = await helpers.createNewUser( + return userHelper.setupNewUser( + body, + url, + res, email, - firstName, - lastName, - affiliation, - title, - models.User, role, + models.User, + 'invite', ) - - try { - await mailService.setupInviteEmail( - newUser.email, - 'invite', - newUser.passwordResetToken, - url, - ) - - return res.status(200).json(newUser) - } catch (e) { - logger.error(e.message) - return res.status(500).json({ error: 'Email could not be sent.' }) - } } } diff --git a/packages/component-invite/src/helpers/Team.js b/packages/component-invite/src/helpers/Team.js index 9c8d5175a15e0b50c876590394bcdffb0514cf52..9826378502ca6391d799106adec2792923f82c33 100644 --- a/packages/component-invite/src/helpers/Team.js +++ b/packages/component-invite/src/helpers/Team.js @@ -17,6 +17,11 @@ const createNewTeam = async (collectionId, role, userId, TeamModel) => { group = 'reviewer' name = 'Reviewer' break + case 'coAuthor': + permissions = 'coAuthor' + group = 'author' + name = 'author' + break default: break } @@ -87,7 +92,6 @@ const setupManuscriptTeam = async (models, user, collectionId, role) => { team.members.push(user.id) try { - // team = await team.updateProperties(team) team = await team.save() user.teams.push(team.id) await user.save() diff --git a/packages/component-invite/src/helpers/User.js b/packages/component-invite/src/helpers/User.js new file mode 100644 index 0000000000000000000000000000000000000000..4d1aa3a2917f6537b37a948392785e2ab87c3a77 --- /dev/null +++ b/packages/component-invite/src/helpers/User.js @@ -0,0 +1,40 @@ +const helpers = require('./helpers') +const mailService = require('pubsweet-component-mail-service') +const logger = require('@pubsweet/logger') + +module.exports = { + 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 { + await mailService.setupInviteEmail( + newUser.email, + invitationType, + newUser.passwordResetToken, + url, + ) + + return res.status(200).json(newUser) + } catch (e) { + logger.error(e.message) + return res.status(500).json({ error: 'Email could not be sent.' }) + } + }, +} diff --git a/packages/component-invite/src/routes/postInvite.js b/packages/component-invite/src/routes/postInvite.js index 99fad55167790b1aa97d802c6a4639bb5eebec95..b3b61a0db11910508d48a2052530bf60fb6d192a 100644 --- a/packages/component-invite/src/routes/postInvite.js +++ b/packages/component-invite/src/routes/postInvite.js @@ -38,6 +38,7 @@ module.exports = models => async (req, res) => { collectionId, models, url, + req.body, ) if (reqUser.admin) diff --git a/packages/component-invite/src/tests/fixtures/collections.js b/packages/component-invite/src/tests/fixtures/collections.js index 09b4b6eb797d61b15e529b247b5e76161736859c..cebc801fd407b4adad7b9dc62d453cd357eec5cf 100644 --- a/packages/component-invite/src/tests/fixtures/collections.js +++ b/packages/component-invite/src/tests/fixtures/collections.js @@ -1,5 +1,5 @@ const Chance = require('chance') -const { handlingEditor } = require('./userData') +const { handlingEditor, submittingAuthor } = require('./userData') const chance = new Chance() module.exports = { @@ -29,4 +29,12 @@ module.exports = { owners: [], save: jest.fn(), }, + authorsCollection: { + id: chance.guid(), + title: chance.sentence(), + type: 'collection', + fragments: [], + owners: [submittingAuthor.id], + save: jest.fn(), + }, } diff --git a/packages/component-invite/src/tests/fixtures/userData.js b/packages/component-invite/src/tests/fixtures/userData.js index a4a2327911e9969621d34315b0b4b0ad9176fdbf..8b03a81e7ff65007939e886448de70fe209a6a50 100644 --- a/packages/component-invite/src/tests/fixtures/userData.js +++ b/packages/component-invite/src/tests/fixtures/userData.js @@ -9,4 +9,10 @@ module.exports = { firstName: chance.first(), lastName: chance.last(), }, + submittingAuthor: { + id: chance.guid(), + email: chance.email(), + firstName: chance.first(), + lastName: chance.last(), + }, } diff --git a/packages/component-invite/src/tests/fixtures/users.js b/packages/component-invite/src/tests/fixtures/users.js index b2df443684be78b5c88a92fcde5bdd2fefd03a9a..ffd92ce7ad5c729b6c0d9e6081f74ce51cdb5187 100644 --- a/packages/component-invite/src/tests/fixtures/users.js +++ b/packages/component-invite/src/tests/fixtures/users.js @@ -1,7 +1,9 @@ const { standardCollection } = require('./collections') const { heTeamID, reviewerTeamID } = require('./teamIDs') -const { handlingEditor } = require('./userData') +const { handlingEditor, submittingAuthor } = require('./userData') +const Chance = require('chance') +const chance = new Chance() const users = { admin: { type: 'user', @@ -49,6 +51,7 @@ const users = { teams: [heTeamID], save: jest.fn(() => users.handlingEditor), editorInChief: false, + handlingEditor: true, }, author: { type: 'user', @@ -114,6 +117,21 @@ const users = { save: jest.fn(() => users.invitedHandlingEditor), editorInChief: false, }, + submittingAuthor: { + type: 'user', + username: 'sauthor', + email: submittingAuthor.email, + password: 'password', + admin: false, + id: submittingAuthor.id, + passwordResetToken: chance.hash(), + firstName: submittingAuthor.firstName, + lastName: submittingAuthor.lastName, + affiliation: chance.company(), + title: 'Mr', + save: jest.fn(() => users.submittingAuthor), + isConfirmed: false, + }, } module.exports = users diff --git a/packages/component-invite/src/tests/postInvite.test.js b/packages/component-invite/src/tests/postInvite.test.js index 76720c1e3049a0c42d2bc3456437bbe421098c11..72759d3340626c5fd37e57cd54948df38926a9c6 100644 --- a/packages/component-invite/src/tests/postInvite.test.js +++ b/packages/component-invite/src/tests/postInvite.test.js @@ -23,8 +23,8 @@ const body = { role: globalRoles[random(0, globalRoles.length - 1)], firstName: chance.first(), lastName: chance.last(), - title: 'professor', - affiliation: 'MIT', + title: 'Mr', + affiliation: chance.company(), } body.admin = body.role === 'admin' @@ -38,6 +38,7 @@ const { handlingEditor, author, invitedHandlingEditor, + submittingAuthor, } = fixtures.users const { standardCollection } = fixtures.collections const { heTeam } = fixtures.teams @@ -241,4 +242,59 @@ describe('Post invite route handler', () => { initialSize, ) }) + it('should return success when an author adds a new co author to a collection', async () => { + const body = { + email: chance.email(), + role: 'coAuthor', + } + const req = httpMocks.createRequest({ + body, + }) + req.user = submittingAuthor.id + req.params.collectionId = standardCollection.id + const res = httpMocks.createResponse() + await require(postInvitePath)(models)(req, res) + + expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + expect(data.email).toEqual(body.email) + expect(data.invitations).toBeUndefined() + }) + it('should return success when an author adds an existing user as co author to a collection', async () => { + const body = { + email: author.email, + role: 'coAuthor', + } + const req = httpMocks.createRequest({ + body, + }) + req.user = submittingAuthor.id + const initialSize = author.invitations.length + req.params.collectionId = standardCollection.id + const res = httpMocks.createResponse() + await require(postInvitePath)(models)(req, res) + + // expect(res.statusCode).toBe(200) + const data = JSON.parse(res._getData()) + // console.log(data) + expect(data.email).toEqual(body.email) + expect(data.invitations).toHaveLength(initialSize) + }) + it('should return an error when an handlingEditor without a team invites a reviewer on a collection', async () => { + body.role = 'reviewer' + body.admin = false + const req = httpMocks.createRequest({ + body, + }) + delete handlingEditor.teams + req.user = handlingEditor.id + req.params.collectionId = standardCollection.id + const res = httpMocks.createResponse() + await require(postInvitePath)(models)(req, res) + expect(res.statusCode).toBe(403) + const data = JSON.parse(res._getData()) + expect(data.error).toEqual( + `Handling Editor ${handlingEditor.email} is not part of any teams`, + ) + }) }) diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index 005b1a01ccffcdfc674ca97e5ce6725ecd1b25f7..1d7e188911406e53b03ab4ec340d39e02ad331df 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -8,28 +8,18 @@ const resetPath = config.get('invite-reset-password.url') module.exports = { setupInviteEmail: async (email, emailType, token, inviteUrl) => { - let subject - let replacements = {} - switch (emailType) { - case 'invite': - subject = 'Hindawi Invitation' - replacements = { - url: `${inviteUrl}${resetPath}?${querystring.encode({ - email, - token, - })}`, - } - break - default: - subject = 'Welcome to Hindawi!' - break + const replacements = { + url: `${inviteUrl}${resetPath}?${querystring.encode({ + email, + token, + })}`, } const { htmlBody, textBody } = getEmailBody(emailType, replacements) const mailData = { from: config.get('mailer.from'), to: email, - subject, + subject: 'Hindawi Invitation', text: textBody, html: htmlBody, } @@ -46,6 +36,12 @@ module.exports = { url: dashBoardUrl, } break + case 'assign-coauthor': + subject = 'Manuscript Assignment on Hindawi' + replacements = { + url: dashBoardUrl, + } + break default: subject = 'Welcome to Hindawi!' break diff --git a/packages/component-mail-service/src/templates/assign-coauthor.html b/packages/component-mail-service/src/templates/assign-coauthor.html new file mode 100644 index 0000000000000000000000000000000000000000..5df68870cc77e2d147c764eef37e9800e374a2f1 --- /dev/null +++ b/packages/component-mail-service/src/templates/assign-coauthor.html @@ -0,0 +1,234 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> + <!--[if !mso]><!--> + <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> + <!--<![endif]--> + <!--[if (gte mso 9)|(IE)]> + <xml> + <o:OfficeDocumentSettings> + <o:AllowPNG/> + <o:PixelsPerInch>96</o:PixelsPerInch> + </o:OfficeDocumentSettings> + </xml> + <![endif]--> + <!--[if (gte mso 9)|(IE)]> + <style type="text/css"> + body {width: 600px;margin: 0 auto;} + table {border-collapse: collapse;} + table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;} + img {-ms-interpolation-mode: bicubic;} + </style> + <![endif]--> + + <style type="text/css"> + body, + p, + div { + font-family: helvetica, arial, sans-serif; + font-size: 14px; + } + + body { + color: #626262; + } + + body a { + color: #0D78F2; + text-decoration: none; + } + + p { + margin: 0; + padding: 0; + } + + table.wrapper { + width: 100% !important; + table-layout: fixed; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + img.max-width { + max-width: 100% !important; + } + + .column.of-2 { + width: 50%; + } + + .column.of-3 { + width: 33.333%; + } + + .column.of-4 { + width: 25%; + } + + @media screen and (max-width:480px) { + .preheader .rightColumnContent, + .footer .rightColumnContent { + text-align: left !important; + } + .preheader .rightColumnContent div, + .preheader .rightColumnContent span, + .footer .rightColumnContent div, + .footer .rightColumnContent span { + text-align: left !important; + } + .preheader .rightColumnContent, + .preheader .leftColumnContent { + font-size: 80% !important; + padding: 5px 0; + } + table.wrapper-mobile { + width: 100% !important; + table-layout: fixed; + } + img.max-width { + height: auto !important; + max-width: 480px !important; + } + a.bulletproof-button { + display: block !important; + width: auto !important; + font-size: 80%; + padding-left: 0 !important; + padding-right: 0 !important; + } + .columns { + width: 100% !important; + } + .column { + display: block !important; + width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; + } + } + </style> + <!--user entered Head Start--> + + <!--End Head user entered--> +</head> + +<body> + <center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;"> + <div class="webkit"> + <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4"> + <tr> + <td valign="top" bgcolor="#F4F4F4" width="100%"> + <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td width="100%"> + <table width="100%" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td> + <!--[if mso]> + <center> + <table><tr><td width="600"> + <![endif]--> + <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center"> + <tr> + <td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%" + align="left"> + + <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" + width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> + <tr> + <td role="module-content"> + <p>author</p> + </td> + </tr> + </table> + + <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center"> + <img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;" + src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png" + alt="" width="60"> + </td> + </tr> + </table> + + <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> + <h1 style="text-align: center;">You have been assigned as an Author to a manuscript.</h1> + + <div style="text-align: center;">The manuscript will become visible on your dashboard once it's submitted.</div> + + <div style="text-align: center;">Please click on the link below to access your dashboard.</div> + + <div style="text-align: center;"> </div> + <div style="text-align: center;"> </div> + + + </td> + </tr> + </table> + <table border="0" cellPadding="0" cellSpacing="0" class="module" data-role="module-button" data-type="button" role="module" + style="table-layout:fixed" width="100%"> + <tbody> + <tr> + <td align="center" bgcolor="#FFFFFF" class="outer-td" style="padding:0px 0px 30px 0px;background-color:#FFFFFF"> + <table border="0" cellPadding="0" cellSpacing="0" class="button-css__deep-table___2OZyb wrapper-mobile" + style="text-align:center"> + <tbody> + <tr> + <td align="center" bgcolor="#0d78f2" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit"> + <a href="{{ url }}" style="background-color:#0d78f2;border:1px solid #333333;border-color:#0d78f2;border-radius:0px;border-width:1px;color:#ffffff;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none" + target="_blank">VIEW DASHBOARD</a> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <div data-role="module-unsubscribe" class="module unsubscribe-css__unsubscribe___2CDlR" role="module" + data-type="unsubscribe" style="color:#444444;font-size:12px;line-height:20px;padding:16px 16px 16px 16px;text-align:center"> + <div class="Unsubscribe--addressLine"> + <p class="Unsubscribe--senderName" style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">Hindawi Publishing Corporation</p> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <span class="Unsubscribe--senderAddress">315 Madison Ave, Third Floor, Suite 3070</span>, + <span class="Unsubscribe--senderCity">NEW YORK</span>, + <span class="Unsubscribe--senderState">NY</span> + <span class="Unsubscribe--senderZip">10017</span> + </p> + </div> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a> + </p> + </div> + </td> + </tr> + </table> + <!--[if mso]> + </td></tr></table> + </center> + <![endif]--> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </div> + </center> +</body> + +</html> \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/assign-coauthor.txt b/packages/component-mail-service/src/templates/assign-coauthor.txt new file mode 100644 index 0000000000000000000000000000000000000000..389fa6da83c0ac3b76d138163ddf85ca363a5f89 --- /dev/null +++ b/packages/component-mail-service/src/templates/assign-coauthor.txt @@ -0,0 +1,7 @@ +You have been assigned as an Author to a manuscript. +The manuscript will become visible on your dashboard once it's submitted. +Please click on the link below to access your dashboard +{{ url }} VIEW DASHBOARD +Hindawi Publishing Corporation +315 Madison Ave, Third Floor, Suite 307 +New York, NY 10017 \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/handling-editor-declined.html b/packages/component-mail-service/src/templates/handling-editor-declined.html index 0c7da04e3cbd2ad0e9fa52ae0919c08bb0ac7657..4923655d4abe1e48db179b88bbc2857fd6ffd4f0 100644 --- a/packages/component-mail-service/src/templates/handling-editor-declined.html +++ b/packages/component-mail-service/src/templates/handling-editor-declined.html @@ -168,6 +168,7 @@ <div style="text-align: center;">{{ name }} has declined to be Handling Editor on manuscript <span style="font-family:courier,monospace;">{{ collectionId }}</span>.</div> <div style="text-align: center;">{{ finalReason }}</div> <div style="text-align: center;"> </div> + <div style="text-align: center;"> </div> </td> </tr> diff --git a/packages/component-mail-service/src/templates/invite-coauthor.html b/packages/component-mail-service/src/templates/invite-coauthor.html new file mode 100644 index 0000000000000000000000000000000000000000..257a3850c43152c4e5e03348c607a1609ebe22fe --- /dev/null +++ b/packages/component-mail-service/src/templates/invite-coauthor.html @@ -0,0 +1,234 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html data-editor-version="2" class="sg-campaigns" xmlns="http://www.w3.org/1999/xhtml"> + +<head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1" /> + <!--[if !mso]><!--> + <meta http-equiv="X-UA-Compatible" content="IE=Edge" /> + <!--<![endif]--> + <!--[if (gte mso 9)|(IE)]> + <xml> + <o:OfficeDocumentSettings> + <o:AllowPNG/> + <o:PixelsPerInch>96</o:PixelsPerInch> + </o:OfficeDocumentSettings> + </xml> + <![endif]--> + <!--[if (gte mso 9)|(IE)]> + <style type="text/css"> + body {width: 600px;margin: 0 auto;} + table {border-collapse: collapse;} + table, td {mso-table-lspace: 0pt;mso-table-rspace: 0pt;} + img {-ms-interpolation-mode: bicubic;} + </style> + <![endif]--> + + <style type="text/css"> + body, + p, + div { + font-family: helvetica, arial, sans-serif; + font-size: 14px; + } + + body { + color: #626262; + } + + body a { + color: #0D78F2; + text-decoration: none; + } + + p { + margin: 0; + padding: 0; + } + + table.wrapper { + width: 100% !important; + table-layout: fixed; + -webkit-font-smoothing: antialiased; + -webkit-text-size-adjust: 100%; + -moz-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + img.max-width { + max-width: 100% !important; + } + + .column.of-2 { + width: 50%; + } + + .column.of-3 { + width: 33.333%; + } + + .column.of-4 { + width: 25%; + } + + @media screen and (max-width:480px) { + .preheader .rightColumnContent, + .footer .rightColumnContent { + text-align: left !important; + } + .preheader .rightColumnContent div, + .preheader .rightColumnContent span, + .footer .rightColumnContent div, + .footer .rightColumnContent span { + text-align: left !important; + } + .preheader .rightColumnContent, + .preheader .leftColumnContent { + font-size: 80% !important; + padding: 5px 0; + } + table.wrapper-mobile { + width: 100% !important; + table-layout: fixed; + } + img.max-width { + height: auto !important; + max-width: 480px !important; + } + a.bulletproof-button { + display: block !important; + width: auto !important; + font-size: 80%; + padding-left: 0 !important; + padding-right: 0 !important; + } + .columns { + width: 100% !important; + } + .column { + display: block !important; + width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; + } + } + </style> + <!--user entered Head Start--> + + <!--End Head user entered--> +</head> + +<body> + <center class="wrapper" data-link-color="#0D78F2" data-body-style="font-size: 14px; font-family: helvetica,arial,sans-serif; color: #626262; background-color: #F4F4F4;"> + <div class="webkit"> + <table cellpadding="0" cellspacing="0" border="0" width="100%" class="wrapper" bgcolor="#F4F4F4"> + <tr> + <td valign="top" bgcolor="#F4F4F4" width="100%"> + <table width="100%" role="content-container" class="outer" align="center" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td width="100%"> + <table width="100%" cellpadding="0" cellspacing="0" border="0"> + <tr> + <td> + <!--[if mso]> + <center> + <table><tr><td width="600"> + <![endif]--> + <table width="100%" cellpadding="0" cellspacing="0" border="0" style="width: 100%; max-width:600px;" align="center"> + <tr> + <td role="modules-container" style="padding: 0px 0px 0px 0px; color: #626262; text-align: left;" bgcolor="#F4F4F4" width="100%" + align="left"> + + <table class="module preheader preheader-hide" role="module" data-type="preheader" border="0" cellpadding="0" cellspacing="0" + width="100%" style="display: none !important; mso-hide: all; visibility: hidden; opacity: 0; color: transparent; height: 0; width: 0;"> + <tr> + <td role="module-content"> + <p>please confirm your Hindawi invitation</p> + </td> + </tr> + </table> + + <table class="wrapper" role="module" data-type="image" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="font-size:6px;line-height:10px;padding:20px 0px 20px 0px;" valign="top" align="center"> + <img class="max-width" border="0" style="display:block;color:#000000;text-decoration:none;font-family:Helvetica, arial, sans-serif;font-size:16px;max-width:10% !important;width:10%;height:auto !important;" + src="https://marketing-image-production.s3.amazonaws.com/uploads/bb39b20cf15e52c1c0933676e25f2b2402737c6560b8098c204ad6932b84eb2058804376dbc4db138c7a21dcaed9325bde36185648afac5bc97e3d73d4e12718.png" + alt="" width="60"> + </td> + </tr> + </table> + + <table class="module" role="module" data-type="text" border="0" cellpadding="0" cellspacing="0" width="100%" style="table-layout: fixed;"> + <tr> + <td style="padding:30px 23px 0px 23px;background-color:#ffffff;" height="100%" valign="top" bgcolor="#ffffff"> + <h1 style="text-align: center;">You have been invited as an Author to a manuscript.</h1> + + <div style="text-align: center;">The manuscript will be visible on your dashboard once it's submitted.</div> + + <div style="text-align: center;">Please confirm your account and set your account details by clicking on the link below.</div> + + <div style="text-align: center;"> </div> + + <div style="text-align: center;"> </div> + + </td> + </tr> + </table> + <table border="0" cellPadding="0" cellSpacing="0" class="module" data-role="module-button" data-type="button" role="module" + style="table-layout:fixed" width="100%"> + <tbody> + <tr> + <td align="center" bgcolor="#FFFFFF" class="outer-td" style="padding:0px 0px 30px 0px;background-color:#FFFFFF"> + <table border="0" cellPadding="0" cellSpacing="0" class="button-css__deep-table___2OZyb wrapper-mobile" + style="text-align:center"> + <tbody> + <tr> + <td align="center" bgcolor="#0d78f2" class="inner-td" style="border-radius:6px;font-size:16px;text-align:center;background-color:inherit"> + <a href="{{ url }}" style="background-color:#0d78f2;border:1px solid #333333;border-color:#0d78f2;border-radius:0px;border-width:1px;color:#ffffff;display:inline-block;font-family:arial,helvetica,sans-serif;font-size:16px;font-weight:normal;letter-spacing:0px;line-height:16px;padding:12px 18px 12px 18px;text-align:center;text-decoration:none" + target="_blank">CONFIRM</a> + </td> + </tr> + </tbody> + </table> + </td> + </tr> + </tbody> + </table> + <div data-role="module-unsubscribe" class="module unsubscribe-css__unsubscribe___2CDlR" role="module" + data-type="unsubscribe" style="color:#444444;font-size:12px;line-height:20px;padding:16px 16px 16px 16px;text-align:center"> + <div class="Unsubscribe--addressLine"> + <p class="Unsubscribe--senderName" style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px">Hindawi Publishing Corporation</p> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <span class="Unsubscribe--senderAddress">315 Madison Ave, Third Floor, Suite 3070</span>, + <span class="Unsubscribe--senderCity">NEW YORK</span>, + <span class="Unsubscribe--senderState">NY</span> + <span class="Unsubscribe--senderZip">10017</span> + </p> + </div> + <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> + <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a> + </p> + </div> + </td> + </tr> + </table> + <!--[if mso]> + </td></tr></table> + </center> + <![endif]--> + </td> + </tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </table> + </div> + </center> +</body> + +</html> \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/invite-coauthor.txt b/packages/component-mail-service/src/templates/invite-coauthor.txt new file mode 100644 index 0000000000000000000000000000000000000000..7d6c7eb14e2820222c64aab7e92ac797fcd07250 --- /dev/null +++ b/packages/component-mail-service/src/templates/invite-coauthor.txt @@ -0,0 +1,7 @@ +You have been invited as an Author to a manuscript. +The manuscript will be visible on your dashboard once it's submitted. +Please confirm your account and set your account details by clicking on the link below. +{{ url }} CONFIRM +Hindawi Publishing Corporation +315 Madison Ave, Third Floor, Suite 307 +New York, NY 10017 \ No newline at end of file diff --git a/packages/components-faraday/src/components/Dashboard/DashboardCard.js b/packages/components-faraday/src/components/Dashboard/DashboardCard.js index d95dc0c258b2bef26cec1de6e0b5ea6d15286e9d..02e08e23d5e4b773f8cd033f5e5d3bdff7f15d40 100644 --- a/packages/components-faraday/src/components/Dashboard/DashboardCard.js +++ b/packages/components-faraday/src/components/Dashboard/DashboardCard.js @@ -164,6 +164,7 @@ export default compose( const assignedHE = assignedPeople && assignedPeople.find(p => p.role === 'handlingEditor') + // this can be changed, but it works; cba if (isAdmin || isEic) { if (status === 'submitted' || status === 'he-invited') return <EditorInChiefActions project={project} /> diff --git a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js index 131ba412335e8bf36df1fb46527fd585d22c3ea8..34eb2d492ebd8482b075195f88b4aa128eac2d8c 100644 --- a/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js +++ b/packages/components-faraday/src/components/Dashboard/EditorInChiefActions.js @@ -26,12 +26,12 @@ const EditorInChiefActions = ({ const handlingEditor = getHandlingEditor() return ( <Root> - <HEActions> + <HEActions data-test="eic-assign"> {handlingEditor ? ( <HEActions> <HEName>{get(handlingEditor, 'name')}</HEName> {!handlingEditor.hasAnswer && ( - <HEActions> + <HEActions data-test="eic-after-assign"> <div onClick={showConfirmModal('resend')}> <Icon color={theme.colorPrimary}>refresh-cw</Icon> </div> @@ -180,9 +180,9 @@ const HEActions = styled.div` const AssignButton = styled(Button)` ${defaultText}; align-items: center; - background-color: ${th('colorPrimary')}; color: ${th('colorTextReverse')}; - text-align: center; + background-color: ${th('colorPrimary')}; height: calc(${th('subGridUnit')}*5); + text-align: center; ` // #endregion diff --git a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js index 6e5cfde77acffc5deb7badb04f00c4e1b6ede067..5064e6aef7cbe80d3b438340bb1c5775467f250d 100644 --- a/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js +++ b/packages/components-faraday/src/components/Dashboard/HandlingEditorActions.js @@ -1,8 +1,8 @@ import React from 'react' import { connect } from 'react-redux' -import styled from 'styled-components' -import { th, Button } from '@pubsweet/ui' import { actions } from 'pubsweet-client' +import { th, Button } from '@pubsweet/ui' +import styled, { css } from 'styled-components' import { withHandlers, compose, withState } from 'recompose' import { withModal, @@ -27,7 +27,7 @@ const DeclineModal = compose( placeholder="Decline reason (optional)" value={reason} /> - <div> + <div data-test="he-buttons"> <Button onClick={hideModal}>Cancel</Button> <Button onClick={onConfirm(reason)} primary> Decline @@ -45,10 +45,10 @@ const ModalComponent = ({ type, ...rest }) => const HandlingEditorActions = ({ showHEModal }) => ( <Root> - <Button onClick={showHEModal('decline')}>DECLINE</Button> - <Button onClick={showHEModal()} primary> + <DecisionButton onClick={showHEModal('decline')}>DECLINE</DecisionButton> + <DecisionButton onClick={showHEModal()} primary> AGREE - </Button> + </DecisionButton> </Root> ) @@ -111,6 +111,22 @@ export default compose( )(HandlingEditorActions) // #region styled-components +const defaultText = css` + font-family: ${th('fontReading')}; + font-size: ${th('fontSizeBaseSmall')}; +` + +const DecisionButton = styled(Button)` + ${defaultText}; + align-items: center; + color: ${({ primary }) => + primary ? th('colorTextReverse') : th('colorPrimary')}); + background-color: ${({ primary }) => + primary ? th('colorPrimary') : th('backgroundColorReverse')}; + height: calc(${th('subGridUnit')}*5); + text-align: center; +` + const DeclineRoot = styled.div` align-items: center; background-color: ${th('backgroundColor')}; diff --git a/packages/xpub-faraday/config/authsome.js b/packages/xpub-faraday/config/authsome.js index 978d988cac126e8195a1f26fc9eacc8cdce4f954..2adf7a0792e6d70a0e26f8a97b8f7feb7df758f1 100644 --- a/packages/xpub-faraday/config/authsome.js +++ b/packages/xpub-faraday/config/authsome.js @@ -3,29 +3,31 @@ const pickBy = require('lodash/pickBy') const omit = require('lodash/omit') async function teamPermissions(user, operation, object, context) { - const heTeamsProm = user.teams - .map(async teamId => { - const team = await context.models.Team.find(teamId) - if (team.teamType.permissions === 'handlingEditor') { - return team - } - return null - }) - .filter(Boolean) + const permissions = ['handlingEditor', 'coAuthor'] + const teams = Promise.all( + user.teams + .map(async teamId => { + const team = await context.models.Team.find(teamId) + if (permissions.includes(team.teamType.permissions)) { + return team + } + return null + }) + .filter(Boolean), + ) - const heTeams = await Promise.all(heTeamsProm) - const heCollections = heTeams.map(team => team.object.id) + const collIDs = teams.map(team => team.object.id) - if (heCollections.length > 0) { + if (collIDs.length > 0) { return { - filter: collections => { - if (collections.length > 0) { - const correctColl = collections.filter(coll => - heCollections.includes(coll.id), + filter: filterParam => { + if (filterParam.length > 0) { + const collections = filterParam.filter(coll => + collIDs.includes(coll.id), ) - return correctColl + return collections } - return collections + return filterParam }, } } diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index d6b8a057e65d92cac258106b300f973bcff68971..f279c3df72cfd495e162414d0527b0a842df446a 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -5,6 +5,20 @@ const logger = require('winston') // const environment = process.env.NODE_ENV || 'development' +const getDbConfig = () => { + if (process.env.DATABASE) { + return { + user: process.env.DB_USER, + password: process.env.DB_PASS, + database: process.env.DATABASE, + host: process.env.DB_HOST, + port: 5432, + ssl: true, + } + } + return {} +} + module.exports = { authsome: { mode: path.resolve(__dirname, 'authsome.js'), @@ -17,7 +31,7 @@ module.exports = { components, }, 'pubsweet-server': { - db: {}, + db: getDbConfig(), port: 3000, logger, uploads: 'uploads', @@ -49,11 +63,12 @@ module.exports = { }, roles: { global: ['admin', 'editorInChief', 'author', 'handlingEditor'], - collection: ['handlingEditor', 'reviewer'], + collection: ['handlingEditor', 'reviewer', 'coAuthor'], inviteRights: { - admin: ['admin', 'editorInChief', 'author', 'handlingEditor'], + admin: ['admin', 'editorInChief', 'author', 'handlingEditor', 'coAuthor'], editorInChief: ['handlingEditor'], handlingEditor: ['reviewer'], + author: ['coAuthor'], }, }, mailer: {