diff --git a/packages/component-admin/.gitignore b/packages/component-admin/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18 --- /dev/null +++ b/packages/component-admin/.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-admin/README.md b/packages/component-admin/README.md new file mode 100644 index 0000000000000000000000000000000000000000..6107c057207697467a41159bcc3f0eee270ae181 --- /dev/null +++ b/packages/component-admin/README.md @@ -0,0 +1,42 @@ +# AWS SES + +In order to use `component-aws-ses` you first need to have a `.env` file containing AWS data in the root folder of the starting point of your application. + +The `.env` file contain the following data: +```bash +AWS_SES_SECRET_KEY = <secretKey> +AWS_SES_ACCESS_KEY = <accessKey> +EMAIL_SENDER = verified_ses_sender@domain.com +AWS_SES_REGION = region-name +``` + +Then, as soon as possible in your app you should add the `dotenv` package: +```js +require('dotenv').config() +``` + +# `component-aws-ses` API +A list of endpoints that help you upload, download and delete S3 files. + +## Send an email [POST] +#### Request +`POST /api/email` +#### Request body + +All parameters are `required` +```json +{ + "email": "to_email@domain.com", + "subject": "Example subject", + "textBody": "this is an email", + "htmlBody": "<p><b>This</b> is an <i>email</i>" +} +``` +#### Response +```json +HTTP/1.1 204 +``` + + + + diff --git a/packages/component-admin/index.js b/packages/component-admin/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f569b273abd8938c1a205c421dff09fc32e824ec --- /dev/null +++ b/packages/component-admin/index.js @@ -0,0 +1,3 @@ +module.exports = { + backend: () => app => require('./src/User')(app), +} diff --git a/packages/component-admin/package.json b/packages/component-admin/package.json new file mode 100644 index 0000000000000000000000000000000000000000..12bc8f99650bf87df42887c5d9d350090b7d8771 --- /dev/null +++ b/packages/component-admin/package.json @@ -0,0 +1,29 @@ +{ + "name": "pubsweet-component-admin", + "version": "0.0.1", + "description": "admin component for pubsweet", + "license": "MIT", + "files": [ + "src" + ], + "scripts": { + "test": "jest" + }, + "repository": { + "type": "git", + "url": "https://gitlab.coko.foundation/xpub/xpub" + }, + "dependencies": { + "body-parser": "^1.17.2" + }, + "peerDependencies": { + "@pubsweet/logger": "^0.0.1", + "pubsweet": "^1.1.1", + "pubsweet-client": "^1.1.1", + "pubsweet-server": "^1.0.1" + }, + "devDependencies": { + "jest": "^22.1.1", + "supertest": "^3.0.0" + } +} diff --git a/packages/component-admin/src/User.js b/packages/component-admin/src/User.js new file mode 100644 index 0000000000000000000000000000000000000000..f72f01649615103c983a34407c02ebd314a4596e --- /dev/null +++ b/packages/component-admin/src/User.js @@ -0,0 +1,150 @@ +const bodyParser = require('body-parser') +const logger = require('@pubsweet/logger') +const uuid = require('uuid') +const crypto = require('crypto') +const mailService = require('pubsweet-component-mail-service') + +const User = app => { + app.use(bodyParser.json()) + const authBearer = app.locals.passport.authenticate('bearer', { + session: false, + }) + app.post('/api/admin/users', authBearer, async (req, res) => { + const reqUser = await app.locals.models.User.find(req.user) + if (reqUser.admin !== true) { + res.status(403).json({ error: 'Unauthorized' }) + logger.error('unauthorized request') + return + } + const { email, role } = req.body + if (email === undefined || role === undefined) { + res.status(400).json({ error: 'all parameters are required' }) + logger.error('some parameters are missing') + return + } + + try { + const user = await app.locals.models.User.findByEmail(email) + if (user) { + res.status(400).json({ error: 'User already exists' }) + logger.error('admin tried to invite existing user') + return + } + } catch (e) { + if (e.name === 'NotFoundError') { + const userBody = { + username: uuid.v4().slice(0, 7), + email, + password: uuid.v4(), + roles: { role }, + passwordResetToken: crypto.randomBytes(32).toString('hex'), + isConfirmed: false, + } + let newUser = new app.locals.models.User(userBody) + newUser = await newUser.save() + let emailType + switch (newUser.roles.role) { + case 'editorInChief': + emailType = 'invite-editor-in-chief' + break + case 'handlingEditor': + emailType = 'invite-handling-editor' + break + case 'reviewer': + emailType = 'invite-reviewer' + break + default: + break + } + await mailService.setupEmail( + newUser.email, + emailType, + newUser.passwordResetToken, + ) + } + } + + res.status(204).json() + }) + app.post( + '/api/admin/users/password-reset', + bodyParser.json(), + async (req, res) => { + const { + token, + password, + email, + firstName, + lastName, + username, + middleName, + affiliation, + position, + title, + } = req.body + + if ( + !checkForUndefinedParams( + token, + password, + email, + firstName, + lastName, + username, + ) + ) { + res.status(400).json({ error: 'missing required params' }) + return + } + + const updateFields = { + password, + firstName, + lastName, + username, + middleName, + affiliation, + position, + title, + isConfirmed: true, + } + + try { + const user = await app.locals.models.User.findByEmail(email) + if (user) { + if (token !== user.passwordResetToken) { + res.status(400).json({ error: 'invalid request' }) + logger.error('admin pw reset tokens do not match') + return + } + + let newUser = Object.assign(user, updateFields, user) + delete newUser.passwordResetToken + + newUser = await newUser.save() + res.status(200).json(newUser) + } + } catch (e) { + if (e.name === 'NotFoundError') { + res.status(404).json({ error: 'user not found' }) + logger.error('admin pw reset on non-existing user') + } else if (e.name === 'ValidationError') { + res.status(400).json({ error: e.details[0].message }) + logger.error('admin pw reset validation error') + } + res.status(400).json({ error: e }) + logger.error(e) + } + }, + ) +} + +const checkForUndefinedParams = (...params) => { + if (params.includes(undefined)) { + return false + } + + return true +} + +module.exports = User diff --git a/packages/component-aws-ses/index.js b/packages/component-aws-ses/index.js index 8b8d52ceca5a0d380e5e5a3a66681030ccfad18a..b54639e30fd890a999ba73bcf5ab581788aeaa82 100644 --- a/packages/component-aws-ses/index.js +++ b/packages/component-aws-ses/index.js @@ -1,5 +1,3 @@ require('dotenv').config() -module.exports = { - backend: () => app => require('./src/EmailBackend')(app), -} +module.exports = require('./src/EmailBackend') diff --git a/packages/component-aws-ses/package.json b/packages/component-aws-ses/package.json index 6267c1f10a75287b9b72fe5e0c2f369e2f6b8190..21500be0578aca6d2dfa147f4e3e5ddee0829738 100644 --- a/packages/component-aws-ses/package.json +++ b/packages/component-aws-ses/package.json @@ -1,6 +1,6 @@ { "name": "pubsweet-components-aws-ses", - "version": "0.0.1", + "version": "0.2.0", "description": "xpub aws ses configured for faraday", "license": "MIT", "files": [ diff --git a/packages/component-aws-ses/src/EmailBackend.js b/packages/component-aws-ses/src/EmailBackend.js index 33a52e0f83f0b42874701cfdfece5ff793a5805a..3cef31414720ec8cb8caf745fd84f3df4d22fa5c 100644 --- a/packages/component-aws-ses/src/EmailBackend.js +++ b/packages/component-aws-ses/src/EmailBackend.js @@ -1,5 +1,4 @@ const nodemailer = require('nodemailer') -const bodyParser = require('body-parser') const AWS = require('aws-sdk') const config = require('config') const _ = require('lodash') @@ -7,36 +6,21 @@ const logger = require('@pubsweet/logger') const sesConfig = _.get(config, 'pubsweet-component-aws-ses') -const EmailBackend = app => { - app.use(bodyParser.json()) - const authBearer = app.locals.passport.authenticate('bearer', { - session: false, - }) - app.post('/api/email', authBearer, async (req, res) => { +module.exports = { + sendEmail: (toEmail, subject, textBody, htmlBody) => { AWS.config.update({ secretAccessKey: sesConfig.secretAccessKey, accessKeyId: sesConfig.accessKeyId, region: sesConfig.region, }) - const { email, subject, textBody, htmlBody } = req.body - if ( - email === undefined || - subject === undefined || - textBody === undefined || - htmlBody === undefined - ) { - res.status(400).json({ error: 'all parameters are required' }) - logger.error('some parameters are missing') - return - } const transporter = nodemailer.createTransport({ SES: new AWS.SES(), }) transporter.sendMail( { from: sesConfig.sender, - to: email, + to: toEmail, subject, text: textBody, html: htmlBody, @@ -48,8 +32,5 @@ const EmailBackend = app => { logger.debug(info) }, ) - res.status(204).json() - }) + }, } - -module.exports = EmailBackend diff --git a/packages/component-mail-service/.gitignore b/packages/component-mail-service/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3614a810088d89d9ccaa28d82401545634874a18 --- /dev/null +++ b/packages/component-mail-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-mail-service/README.md b/packages/component-mail-service/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/component-mail-service/index.js b/packages/component-mail-service/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3d0b3c14535e01c151f895accdfd5e0ccfc1294b --- /dev/null +++ b/packages/component-mail-service/index.js @@ -0,0 +1 @@ +module.exports = require('./src/Mail') diff --git a/packages/component-mail-service/package.json b/packages/component-mail-service/package.json new file mode 100644 index 0000000000000000000000000000000000000000..09ca4ea72eafe7a7507fef3b6cd682216dad9850 --- /dev/null +++ b/packages/component-mail-service/package.json @@ -0,0 +1,30 @@ +{ + "name": "pubsweet-component-mail-service", + "version": "0.0.1", + "description": "mail service component for pubsweet", + "license": "MIT", + "files": [ + "src" + ], + "scripts": { + "test": "jest" + }, + "repository": { + "type": "git", + "url": "https://gitlab.coko.foundation/xpub/xpub" + }, + "dependencies": { + "body-parser": "^1.17.2", + "handlebars": "^4.0.11" + }, + "peerDependencies": { + "@pubsweet/logger": "^0.0.1", + "pubsweet": "^1.1.1", + "pubsweet-client": "^1.1.1", + "pubsweet-server": "^1.0.1" + }, + "devDependencies": { + "jest": "^22.1.1", + "supertest": "^3.0.0" + } +} diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js new file mode 100644 index 0000000000000000000000000000000000000000..23644ec833d62036e394e8f933810fad077ae486 --- /dev/null +++ b/packages/component-mail-service/src/Mail.js @@ -0,0 +1,50 @@ +const fs = require('fs') +const handlebars = require('handlebars') +const querystring = require('querystring') +const SES = require('pubsweet-components-aws-ses') +const config = require('config') + +const resetUrl = config.get('admin-reset-password.url') + +module.exports = { + setupEmail: async (email, emailType, token, comment = '') => { + let subject + const htmlFile = readFile(`${__dirname}/templates/${emailType}.html`) + const textFile = readFile(`${__dirname}/templates/${emailType}.txt`) + let replacements = {} + const htmlTemplate = handlebars.compile(htmlFile) + const textTemplate = handlebars.compile(textFile) + + switch (emailType) { + case 'invite-editor-in-chief': + subject = 'Hindawi Invitation' + replacements = { + url: `${resetUrl}?${querystring.encode({ + email, + token, + })}`, + } + break + default: + subject = 'Welcome to Hindawi!' + break + } + + const htmlBody = htmlTemplate(replacements) + const textBody = textTemplate(replacements) + + SES.sendEmail(email, subject, textBody, htmlBody) + }, +} + +const readFile = path => { + const file = fs.readFileSync(path, { encoding: 'utf-8' }, (err, file) => { + if (err) { + throw err + } else { + return file + } + }) + + return file +} diff --git a/packages/component-mail-service/src/templates/invite-editor-in-chief.html b/packages/component-mail-service/src/templates/invite-editor-in-chief.html new file mode 100644 index 0000000000000000000000000000000000000000..58abc88115fe94ea3601b861c1703316e49f0446 --- /dev/null +++ b/packages/component-mail-service/src/templates/invite-editor-in-chief.html @@ -0,0 +1,2 @@ +<p>You have been invited</p> +<p>Here is your url <a href="{{ url }}">PW RESET</a></p> \ No newline at end of file diff --git a/packages/component-mail-service/src/templates/invite-editor-in-chief.txt b/packages/component-mail-service/src/templates/invite-editor-in-chief.txt new file mode 100644 index 0000000000000000000000000000000000000000..d249e30cc14554c2a87ba079439bbba7d2b2d976 --- /dev/null +++ b/packages/component-mail-service/src/templates/invite-editor-in-chief.txt @@ -0,0 +1,2 @@ +You have been invited +Here is your url PW REST {{ url }} \ No newline at end of file diff --git a/packages/xpub-faraday/config/components.json b/packages/xpub-faraday/config/components.json index 8c34645a85924f65fc2e2d8475db8b2d05cba90c..d64253bf25516eebd779a3f72a6f728e37ef63c6 100644 --- a/packages/xpub-faraday/config/components.json +++ b/packages/xpub-faraday/config/components.json @@ -7,5 +7,5 @@ "pubsweet-component-wizard", "pubsweet-components-faraday", "pubsweet-components-aws-s3", - "pubsweet-components-aws-ses" + "pubsweet-component-admin" ] diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index 3cafabc6a12877337794cf1647cc29330eca4d59..245f1dbe7b3d34ab7c80d7399c2d000240341f51 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -60,6 +60,11 @@ module.exports = { region: process.env.AWS_SES_REGION, sender: process.env.EMAIL_SENDER, }, + 'admin-reset-password': { + url: + process.env.PUBSWEET_ADMIN_PASSWORD_RESET_URL || + 'http://localhost:3000/admin/password-reset', + }, publicKeys: [ 'pubsweet-client', 'authsome', diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index f7d617d3d72c0ead1b9bc4390c10a45445006929..8b98883df175275ecb57a268cf4f549d065a92c3 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -88,6 +88,13 @@ module.exports = { user: { name: Joi.string(), // TODO: add "name" to the login form roles: Joi.object(), + isConfirmed: Joi.boolean(), + firstName: Joi.string().allow(''), + lastName: Joi.string().allow(''), + middleName: Joi.string().allow(''), + affiliation: Joi.string().allow(''), + position: Joi.string().allow(''), + title: Joi.string().allow(''), }, team: { group: Joi.string(), diff --git a/packages/xpub-faraday/package.json b/packages/xpub-faraday/package.json index fabc887cc97882eb924de656b768f103fbf52326..e3b80d41b71900568e6bb0eb5d821f06df2765f7 100644 --- a/packages/xpub-faraday/package.json +++ b/packages/xpub-faraday/package.json @@ -31,7 +31,7 @@ "pubsweet-component-xpub-submit": "^0.0.2", "pubsweet-server": "1.0.5", "pubsweet-components-aws-s3": "^0.0.1", - "pubsweet-components-aws-ses": "^0.0.1", + "pubsweet-component-admin": "^0.0.1", "react": "^15.6.1", "react-dnd": "^2.5.4", "react-dnd-html5-backend": "^2.5.4", diff --git a/yarn.lock b/yarn.lock index eaeb4eea2fab94726a5a42f59a381f52f3ad1025..76654745dcb4355df780499d9a0f89f31cf8640e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4324,7 +4324,7 @@ handle-thing@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4" -handlebars@^4.0.2, handlebars@^4.0.3: +handlebars@^4.0.11, handlebars@^4.0.2, handlebars@^4.0.3: version "4.0.11" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc" dependencies: