Skip to content
Snippets Groups Projects
Commit 9772f841 authored by Alexandru Munteanu's avatar Alexandru Munteanu
Browse files

Merge branch 'refactor-component-invite' into 'master'

Refactor component invite

See merge request !5
parents 52e17190 bbfe1981
No related branches found
No related tags found
1 merge request!5Refactor component invite
Showing
with 415 additions and 285 deletions
_build/
api/
logs/
node_modules/
uploads/
.env.*
.env
config/local*.*
public/apidoc
{
"name": "Component Email API documentation",
"version": "0.0.1",
"description": "A list of APIs for the Email Component",
"template": {
"forceLanguage": "en"
}
}
module.exports = {
backend: () => app => {
require('./src/Emails')(app)
},
}
{
"name": "pubsweet-component-email",
"version": "0.0.1",
"description": "email component for faraday",
"license": "MIT",
"author": "Collaborative Knowledge Foundation",
"files": [
"src"
],
"main": "index.js",
"scripts": {
"test": "jest",
"docs": "./node_modules/.bin/apidoc -e \"(node_modules|public)\" -o public/apidoc",
"open-docs": "open public/apidoc/index.html"
},
"repository": {
"type": "git",
"url": "https://gitlab.coko.foundation/xpub/xpub-faraday",
"path": "component-user-manager"
},
"dependencies": {
"body-parser": "^1.17.2",
"chance": "^1.0.13"
},
"peerDependencies": {
"@pubsweet/logger": "^0.0.1",
"pubsweet-component-mail-service": "0.0.1",
"pubsweet-server": "^1.0.1"
},
"devDependencies": {
"apidoc": "^0.17.6",
"jest": "^22.1.1",
"supertest": "^3.0.0"
},
"jest": {
"verbose": true,
"testRegex": "/src/.*.test.js$"
},
"publishConfig": {
"access": "public"
}
}
const bodyParser = require('body-parser')
const CollectionsInvitations = app => {
app.use(bodyParser.json())
const basePath = '/api/emails'
const routePath = './routes/emails'
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
/**
* @api {post} /api/emails Send a new email
* @apiGroup Emails
* @apiParamExample {json} Body
* {
* "email": "email@example.com",
* "type": "invite", [acceptedValues: invite, assign]
* "role": "editorInChief" [acceptedValues: editorInChief, admin, author, handlingEditor]
* }
* @apiSuccessExample {json} Success
* HTTP/1.1 200 OK {}
* @apiErrorExample {json} Send email errors
* HTTP/1.1 403 Forbidden
* HTTP/1.1 400 Bad Request
* HTTP/1.1 500 Internal Server Error
*/
app.post(
basePath,
authBearer,
require(`${routePath}/post`)(app.locals.models),
)
}
module.exports = CollectionsInvitations
const mailService = require('pubsweet-component-mail-service')
const logger = require('@pubsweet/logger')
const helpers = require('./helpers')
module.exports = {
setupAssignEmail: async (req, email, res, role) => {
const url = `${req.protocol}://${req.get('host')}`
let emailType
switch (role) {
case 'handlingEditor':
emailType = 'assign-handling-editor'
break
case 'author':
emailType = 'add-author'
break
default:
return res
.status(400)
.json({ error: `Role ${role} cannot be used with an assigned email` })
}
try {
await mailService.setupAssignEmail(email, emailType, url)
return res.status(200).json({})
} catch (e) {
logger.error(e)
return res.status(500).json({ error: 'Email could not be sent.' })
}
},
setupNewUserEmail: async (req, res, email, role, UserModel) => {
let user
try {
user = await UserModel.findByEmail(email)
} catch (e) {
const notFoundError = await helpers.handleNotFoundError(e, 'user')
return res.status(notFoundError.status).json({
error: notFoundError.message,
})
}
if (user.passwordResetToken === undefined) {
return res
.status(400)
.json({ error: 'User does not have a password reset token' })
}
const url = `${req.protocol}://${req.get('host')}`
let emailType
switch (role) {
case 'handlingEditor':
case 'editorInChief':
case 'admin':
emailType = 'invite'
break
case 'author':
emailType = 'invite-author'
break
default:
return res.status(400).json({
error: `Role ${role} cannot be used with a password reset email`,
})
}
try {
await mailService.setupInviteEmail(
email,
emailType,
user.passwordResetToken,
url,
)
return res.status(200).json({})
} catch (e) {
logger.error(e)
return res.status(500).json({ error: 'Email could not be sent.' })
}
},
}
const logger = require('@pubsweet/logger')
const checkForUndefinedParams = (...params) => {
if (params.includes(undefined)) {
return false
}
return true
}
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
}
module.exports = {
checkForUndefinedParams,
handleNotFoundError,
}
const logger = require('@pubsweet/logger')
const helpers = require('../../helpers/helpers')
const emailHelper = require('../../helpers/Email')
module.exports = models => async (req, res) => {
const { email, type, role } = req.body
if (!helpers.checkForUndefinedParams(email, type, role)) {
res.status(400).json({ error: 'Email and type are required' })
logger.error('User ID and role are missing')
return
}
switch (type) {
case 'invite':
return emailHelper.setupNewUserEmail(req, res, email, role, models.User)
case 'assign':
return emailHelper.setupAssignEmail(req, email, res, role)
default:
return res
.status(400)
.json({ error: `Email type ${type} is not defined` })
}
}
describe('Emails to be tested later', () => {
it('works', () => {
expect(true).toBeTruthy()
})
})
......@@ -5,4 +5,5 @@ node_modules/
uploads/
.env.*
.env
config/local*.*
\ No newline at end of file
config/local*.*
public/apidoc
\ No newline at end of file
{
"name": "Component Invite API documentation",
"version": "0.0.1",
"description": "A list of APIs for the Invite Component",
"template": {
"forceLanguage": "en"
}
}
module.exports = {
backend: () => app => {
require('./src/Invite')(app)
require('./src/HandleInvitation')(app)
require('./src/CollectionsInvitations')(app)
},
}
......@@ -4,10 +4,14 @@
"description": "invite component for pubsweet",
"license": "MIT",
"author": "Collaborative Knowledge Foundation",
"files": ["src"],
"files": [
"src"
],
"main": "index.js",
"scripts": {
"test": "jest"
"test": "jest",
"docs": "./node_modules/.bin/apidoc -e \"(node_modules|public)\" -o public/apidoc",
"open-docs": "open public/apidoc/index.html"
},
"repository": {
"type": "git",
......@@ -20,10 +24,11 @@
},
"peerDependencies": {
"@pubsweet/logger": "^0.0.1",
"pubsweet-server": "^1.0.1",
"pubsweet-component-mail-service": "0.0.1"
"pubsweet-component-mail-service": "0.0.1",
"pubsweet-server": "^1.0.1"
},
"devDependencies": {
"apidoc": "^0.17.6",
"jest": "^22.1.1",
"supertest": "^3.0.0"
},
......
const bodyParser = require('body-parser')
const CollectionsInvitations = app => {
app.use(bodyParser.json())
const basePath = '/api/collections/:collectionId/invitations'
const routePath = './routes/collectionsInvitations'
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
/**
* @api {post} /api/collections/:collectionId/invitations Invite a user to a collection
* @apiGroup CollectionsInvitations
* @apiParam {collectionId} collectionId Collection id
* @apiParamExample {json} Body
* {
* "email": "email@example.com",
* "role": "handlingEditor", [acceptedValues: handlingEditor]
* }
* @apiSuccessExample {json} Success
* HTTP/1.1 200 OK
* {
* "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
* "type": "user",
* "admin": false,
* "email": "email@example.com",
* "teams": [
* "c576695a-7cda-4e27-8e9c-31f3a0e9d592"
* ],
* "username": "email@example.com",
* "fragments": [],
* "collections": [],
* "isConfirmed": false,
* "editorInChief": false,
* "handlingEditor": true,
* "passwordResetToken": "04590a2b7f6c1f37cb84881d529e516fa6fc309c205a07f1341b2bfaa6f2b46c"
* }
* @apiErrorExample {json} Invite user errors
* HTTP/1.1 403 Forbidden
* HTTP/1.1 400 Bad Request
* HTTP/1.1 404 Not Found
* HTTP/1.1 500 Internal Server Error
*/
app.post(
basePath,
authBearer,
require(`${routePath}/post`)(app.locals.models),
)
/**
* @api {get} /api/collections/:collectionId/invitations List collections invitations
* @apiGroup CollectionsInvitations
* @apiParam {collectionId} id Collection id
* @apiSuccessExample {json} Success
* HTTP/1.1 200 OK
* [{
* "name": "John Smith",
* "timestamp": "123223121",
* "email": "email@example.com",
* "status": "pending",
* }]
* @apiErrorExample {json} List errors
* HTTP/1.1 403 Forbidden
* HTTP/1.1 400 Bad Request
* HTTP/1.1 404 Not Found
*/
app.get(
`${basePath}/:invitationId?`,
authBearer,
require(`${routePath}/get`)(app.locals.models),
)
/**
* @api {delete} /api/collections/:collectionId/invitations/:invitationId Delete invitation
* @apiGroup CollectionsInvitations
* @apiParam {collectionId} collectionId Collection id
* @apiParam {invitationId} invitationId Invitation id
* @apiSuccessExample {json} Success
* HTTP/1.1 204 No Content
* @apiErrorExample {json} Delete errors
* HTTP/1.1 403 Forbidden
* HTTP/1.1 404 Not Found
* HTTP/1.1 500 Internal Server Error
*/
app.delete(
`${basePath}/:invitationId`,
authBearer,
require(`${routePath}/delete`)(app.locals.models),
)
/**
* @api {patch} /api/collections/:collectionId/invitations/:invitationId Update an invitation
* @apiGroup CollectionsInvitations
* @apiParam {collectionId} collectionId Collection id
* @apiParam {invitationId} invitationId Invitation id
* @apiParamExample {json} Body
* {
* "isAccepted": false,
* "reason": "I am not ready" [optional]
* }
* @apiSuccessExample {json} Success
* HTTP/1.1 200 OK
* {
* "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b",
* "type": "user",
* "admin": false,
* "email": "email@example.com",
* "teams": [],
* "username": "email@example.com",
* "fragments": [],
* "collections": [],
* "isConfirmed": false,
* "editorInChief": false,
* "handlingEditor": true,
* "passwordResetToken": "04590a2b7f6c1f37cb84881d529e516fa6fc309c205a07f1341b2bfaa6f2b46c"
* }
* @apiErrorExample {json} Update invitations errors
* HTTP/1.1 403 Forbidden
* HTTP/1.1 400 Bad Request
* HTTP/1.1 404 Not Found
* HTTP/1.1 500 Internal Server Error
*/
app.patch(
`${basePath}/:invitationId`,
authBearer,
require(`${routePath}/patch`)(app.locals.models),
)
}
module.exports = CollectionsInvitations
const bodyParser = require('body-parser')
const HandleInvitation = app => {
app.use(bodyParser.json())
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
app.post(
'/api/collections/:collectionId/users',
authBearer,
require('./routes/postHandleInvitation')(app.locals.models),
)
app.get(
'/api/collections/:collectionId/users',
authBearer,
require('./routes/getCollectionUsers')(app.locals.models),
)
app.delete(
'/api/collections/:collectionId/users/:userId',
authBearer,
require('./routes/deleteInvitation')(app.locals.models),
)
}
module.exports = HandleInvitation
const bodyParser = require('body-parser')
const Invite = app => {
app.use(bodyParser.json())
const authBearer = app.locals.passport.authenticate('bearer', {
session: false,
})
app.post(
'/api/users/invite/:collectionId?',
authBearer,
require('./routes/postInvite')(app.locals.models),
)
app.get(
'/api/users/invite',
require('./routes/getInviteDetails')(app.locals.models),
)
app.post(
'/api/users/invite/password/reset',
bodyParser.json(),
require('./routes/resetPassword')(app.locals.models),
)
}
module.exports = Invite
const logger = require('@pubsweet/logger')
const config = require('config')
const helpers = require('../helpers/helpers')
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')
module.exports = async (
email,
role,
reqUser,
res,
collectionId,
models,
url,
body,
) => {
if (!configRoles.collection.includes(role)) {
logger.error(`invitation has been attempted with invalid role: ${role}`)
return res
.status(403)
.json({ error: `Role ${role} cannot be set on collections` })
}
if (reqUser.handlingEditor === true) {
if (reqUser.email === email) {
logger.error(`${reqUser.email} tried to invite his own email`)
return res.status(400).json({ error: 'Cannot invite yourself' })
}
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 (heTeams.length === 0) {
return res.status(403).json({
error: `User ${
reqUser.email
} cannot invite a ${role} to ${collectionId}`,
})
}
}
if (reqUser.editorInChief === true && email === reqUser.email) {
logger.error(`${reqUser.email} tried to invite his own email`)
return res.status(400).json({ error: 'Cannot invite yourself' })
}
let collection
try {
collection = await models.Collection.find(collectionId)
} catch (e) {
const notFoundError = await helpers.handleNotFoundError(e, 'collection')
return res.status(notFoundError.status).json({
error: notFoundError.message,
})
}
try {
let user = await models.User.findByEmail(email)
const team = await teamHelper.setupManuscriptTeam(
models,
user,
collectionId,
role,
)
// get updated user from DB
user = await models.User.findByEmail(email)
if (role === 'author') {
if (collection.owners[0].id === user.id) {
return res.status(200).json(user)
}
try {
await mailService.setupAssignEmail(user.email, 'assign-author', 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(
user,
role,
collectionId,
team.id,
)
await collHelper.addAssignedPeople(collection, user, role)
} else {
const matchingInvitation = user.invitations.find(
invitation =>
invitation.collectionId === collectionId &&
invitation.role === role &&
invitation.hasAnswer === false,
)
if (matchingInvitation === undefined) {
user = await inviteHelper.setupInvitation(
user,
role,
collectionId,
team.id,
)
await collHelper.addAssignedPeople(collection, user, role)
}
}
try {
await mailService.setupAssignEmail(
user.email,
'assign-handling-editor',
url,
)
return res.status(200).json(user)
} catch (e) {
logger.error(e)
return res.status(500).json({ error: 'Mail could not be sent.' })
}
} catch (e) {
if (e.name === 'NotFoundError' && role === 'author') {
const newUser = await userHelper.setupNewUser(
body,
url,
res,
email,
role,
models.User,
'invite-author',
)
if (newUser.error !== undefined) {
return res.status(newUser.status).json({
error: newUser.message,
})
}
await teamHelper.setupManuscriptTeam(models, newUser, collectionId, role)
return res.status(200).json(newUser)
}
const notFoundError = await helpers.handleNotFoundError(e, 'user')
return res.status(notFoundError.status).json({
error: notFoundError.message,
})
}
}
const logger = require('@pubsweet/logger')
const userHelper = require('../helpers/User')
const config = require('config')
const configRoles = config.get('roles')
module.exports = async (body, models, res, url) => {
const { email, role } = body
if (!configRoles.inviteRights.admin.includes(role)) {
logger.error(`admin ${email} tried to invite a ${role}`)
return res
.status(403)
.json({ error: `admin tried to invite an invalid role: ${role}` })
}
try {
const user = await models.User.findByEmail(email)
if (user) {
logger.error(`admin tried to invite existing user: ${email}`)
return res.status(400).json({ error: 'User already exists' })
}
} catch (e) {
if (e.name !== 'NotFoundError') {
logger.error(e)
return res.status(500).json({ error: e.details[0].message })
}
const newUser = await userHelper.setupNewUser(
body,
url,
res,
email,
role,
models.User,
'invite',
)
if (newUser.error !== undefined) {
return res.status(newUser.status).json({
error: newUser.message,
})
}
return res.status(200).json(newUser)
}
}
const invitationHelper = require('./Invitation')
const mailService = require('pubsweet-component-mail-service')
const logger = require('@pubsweet/logger')
module.exports = {
addAssignedPeople: async (collection, user, role) => {
collection.assignedPeople = collection.assignedPeople || []
const matchingInvitation = invitationHelper.getMatchingInvitation(
user.invitations,
collection.id,
role,
addInvitation: async (collection, userId, role) => {
collection.invitations = collection.invitations || []
let matchingInvitation = collection.invitations.find(
invitation =>
invitation.userId === userId &&
invitation.role === role &&
invitation.hasAnswer === false,
)
const assignedPerson = {
if (matchingInvitation === undefined) {
matchingInvitation = await invitationHelper.setupInvitation(
userId,
role,
collection,
)
}
return matchingInvitation
},
addAuthor: async (collection, user, res, url) => {
if (collection.owners.includes(user.id)) {
return res.status(200).json(user)
}
try {
await mailService.setupAssignEmail(user.email, 'assign-author', url)
return res.status(200).json(user)
} catch (e) {
logger.error(e)
return res.status(500).json({ error: 'Email could not be sent.' })
}
},
addHandlingEditor: async (collection, user, invitation) => {
collection.handlingEditor = {
id: user.id,
email: user.email,
name: `${user.firstName} ${user.lastName}`,
role,
hasAnswer: matchingInvitation.hasAnswer,
isAccepted: matchingInvitation.isAccepted,
timestamp: invitation.timestamp,
email: user.email,
hasAnswer: invitation.hasAnswer,
isAccepted: invitation.isAccepted,
}
collection.assignedPeople.push(assignedPerson)
await collection.save()
},
removeAssignedPeople: async (collection, email) => {
const assignedPeople =
collection.assignedPeople &&
collection.assignedPeople.filter(person => person.email !== email)
collection.assignedPeople = assignedPeople || []
await collection.save()
},
updateAssignedPeople: async (collection, email) => {
const assignedPerson = collection.assignedPeople.find(
person => person.email === email,
)
assignedPerson.hasAnswer = true
assignedPerson.isAccepted = true
updateHandlingEditor: async (collection, isAccepted) => {
collection.handlingEditor.hasAnswer = true
collection.handlingEditor.isAccepted = isAccepted
await collection.save()
},
}
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment