Merge pull request #14989 from overleaf/em-promisify-collaborators-invite-controller

Promisify CollaboratorsInviteController

GitOrigin-RevId: 007b77823a9332464123c11da55dda8571803095
This commit is contained in:
Eric Mc Sween 2023-10-03 08:05:52 -04:00 committed by Copybot
parent aafb0d377c
commit 0f473d9a9f
2 changed files with 591 additions and 665 deletions

View file

@ -1,5 +1,4 @@
let CollaboratorsInviteController
const OError = require('@overleaf/o-error')
const { callbackify } = require('util')
const ProjectGetter = require('../Project/ProjectGetter')
const LimitationsManager = require('../Subscription/LimitationsManager')
const UserGetter = require('../User/UserGetter')
@ -12,6 +11,7 @@ const EditorRealTimeController = require('../Editor/EditorRealTimeController')
const AnalyticsManager = require('../Analytics/AnalyticsManager')
const SessionManager = require('../Authentication/SessionManager')
const { RateLimiter } = require('../../infrastructure/RateLimiter')
const { expressify } = require('../../util/promises')
// This rate limiter allows a different number of requests depending on the
// number of callaborators a user is allowed. This is implemented by providing
@ -28,76 +28,62 @@ const rateLimiter = new RateLimiter('invite-to-project-by-user-id', {
duration: 60 * 30,
})
module.exports = CollaboratorsInviteController = {
getAllInvites(req, res, next) {
const CollaboratorsInviteController = {
async getAllInvites(req, res) {
const projectId = req.params.Project_id
logger.debug({ projectId }, 'getting all active invites for project')
CollaboratorsInviteHandler.getAllInvites(
projectId,
function (err, invites) {
if (err) {
OError.tag(err, 'error getting invites for project', {
projectId,
})
return next(err)
}
res.json({ invites })
}
const invites = await CollaboratorsInviteHandler.promises.getAllInvites(
projectId
)
res.json({ invites })
},
_checkShouldInviteEmail(email, callback) {
async _checkShouldInviteEmail(email) {
if (Settings.restrictInvitesToExistingAccounts === true) {
logger.debug({ email }, 'checking if user exists with this email')
UserGetter.getUserByAnyEmail(email, { _id: 1 }, function (err, user) {
if (err) {
return callback(err)
}
const userExists = user?._id != null
callback(null, userExists)
const user = await UserGetter.promises.getUserByAnyEmail(email, {
_id: 1,
})
const userExists = user?._id != null
return userExists
} else {
callback(null, true)
return true
}
},
_checkRateLimit(userId, callback) {
LimitationsManager.allowedNumberOfCollaboratorsForUser(
userId,
(err, collabLimit) => {
if (err) {
return callback(err)
}
if (collabLimit == null || collabLimit === 0) {
collabLimit = 1
} else if (collabLimit < 0 || collabLimit > 20) {
collabLimit = 20
}
async _checkRateLimit(userId) {
let collabLimit =
await LimitationsManager.promises.allowedNumberOfCollaboratorsForUser(
userId
)
// Consume enough points to hit the rate limit at 10 * collabLimit
const maxRequests = 10 * collabLimit
const points = Math.floor(RATE_LIMIT_POINTS / maxRequests)
rateLimiter
.consume(userId, points)
.then(() => {
callback(null, true)
})
.catch(err => {
if (err instanceof Error) {
callback(err)
} else {
callback(null, false)
}
})
if (collabLimit == null || collabLimit === 0) {
collabLimit = 1
} else if (collabLimit < 0 || collabLimit > 20) {
collabLimit = 20
}
// Consume enough points to hit the rate limit at 10 * collabLimit
const maxRequests = 10 * collabLimit
const points = Math.floor(RATE_LIMIT_POINTS / maxRequests)
try {
await rateLimiter.consume(userId, points)
} catch (err) {
if (err instanceof Error) {
throw err
} else {
return false
}
)
}
return true
},
inviteToProject(req, res, next) {
async inviteToProject(req, res) {
const projectId = req.params.Project_id
let { email } = req.body
let { email, privileges } = req.body
const sendingUser = SessionManager.getSessionUser(req.session)
const sendingUserId = sendingUser._id
if (email === sendingUser.email) {
logger.debug(
{ projectId, email, sendingUserId },
@ -105,243 +91,163 @@ module.exports = CollaboratorsInviteController = {
)
return res.json({ invite: null, error: 'cannot_invite_self' })
}
logger.debug({ projectId, email, sendingUserId }, 'inviting to project')
LimitationsManager.canAddXCollaborators(projectId, 1, (error, allowed) => {
let privileges
if (error) {
return next(error)
}
if (!allowed) {
logger.debug(
{ projectId, email, sendingUserId },
'not allowed to invite more users to project'
)
return res.json({ invite: null })
}
;({ email, privileges } = req.body)
email = EmailHelper.parseEmail(email, true)
if (email == null || email === '') {
logger.debug(
{ projectId, email, sendingUserId },
'invalid email address'
)
return res.status(400).json({ errorReason: 'invalid_email' })
}
CollaboratorsInviteController._checkRateLimit(
sendingUserId,
function (error, underRateLimit) {
if (error) {
return next(error)
}
if (!underRateLimit) {
return res.sendStatus(429)
}
CollaboratorsInviteController._checkShouldInviteEmail(
email,
function (err, shouldAllowInvite) {
if (err) {
OError.tag(
err,
'error checking if we can invite this email address',
{
email,
projectId,
sendingUserId,
}
)
return next(err)
}
if (!shouldAllowInvite) {
logger.debug(
{ email, projectId, sendingUserId },
'not allowed to send an invite to this email address'
)
return res.json({
invite: null,
error: 'cannot_invite_non_user',
})
}
CollaboratorsInviteHandler.inviteToProject(
projectId,
sendingUser,
email,
privileges,
function (err, invite) {
if (err) {
OError.tag(err, 'error creating project invite', {
projectId,
email,
sendingUserId,
})
return next(err)
}
logger.debug(
{ projectId, email, sendingUserId },
'invite created'
)
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true }
)
res.json({ invite })
}
)
}
)
}
)
})
},
revokeInvite(req, res, next) {
const projectId = req.params.Project_id
const inviteId = req.params.invite_id
logger.debug({ projectId, inviteId }, 'revoking invite')
CollaboratorsInviteHandler.revokeInvite(
const allowed = await LimitationsManager.promises.canAddXCollaborators(
projectId,
inviteId,
function (err) {
if (err) {
OError.tag(err, 'error revoking invite', {
projectId,
inviteId,
})
return next(err)
}
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true }
)
res.sendStatus(201)
}
1
)
if (!allowed) {
logger.debug(
{ projectId, email, sendingUserId },
'not allowed to invite more users to project'
)
return res.json({ invite: null })
}
email = EmailHelper.parseEmail(email, true)
if (email == null || email === '') {
logger.debug({ projectId, email, sendingUserId }, 'invalid email address')
return res.status(400).json({ errorReason: 'invalid_email' })
}
const underRateLimit = await CollaboratorsInviteController._checkRateLimit(
sendingUserId
)
if (!underRateLimit) {
return res.sendStatus(429)
}
const shouldAllowInvite =
await CollaboratorsInviteController._checkShouldInviteEmail(email)
if (!shouldAllowInvite) {
logger.debug(
{ email, projectId, sendingUserId },
'not allowed to send an invite to this email address'
)
return res.json({
invite: null,
error: 'cannot_invite_non_user',
})
}
const invite = await CollaboratorsInviteHandler.promises.inviteToProject(
projectId,
sendingUser,
email,
privileges
)
logger.debug({ projectId, email, sendingUserId }, 'invite created')
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true }
)
res.json({ invite })
},
resendInvite(req, res, next) {
async revokeInvite(req, res) {
const projectId = req.params.Project_id
const inviteId = req.params.invite_id
logger.debug({ projectId, inviteId }, 'revoking invite')
await CollaboratorsInviteHandler.promises.revokeInvite(projectId, inviteId)
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true }
)
res.sendStatus(201)
},
async resendInvite(req, res) {
const projectId = req.params.Project_id
const inviteId = req.params.invite_id
logger.debug({ projectId, inviteId }, 'resending invite')
const sendingUser = SessionManager.getSessionUser(req.session)
CollaboratorsInviteController._checkRateLimit(
sendingUser._id,
function (error, underRateLimit) {
if (error) {
return next(error)
}
if (!underRateLimit) {
return res.sendStatus(429)
}
CollaboratorsInviteHandler.resendInvite(
projectId,
sendingUser,
inviteId,
function (err) {
if (err) {
OError.tag(err, 'error resending invite', {
projectId,
inviteId,
})
return next(err)
}
res.sendStatus(201)
}
)
}
const underRateLimit = await CollaboratorsInviteController._checkRateLimit(
sendingUser._id
)
if (!underRateLimit) {
return res.sendStatus(429)
}
await CollaboratorsInviteHandler.promises.resendInvite(
projectId,
sendingUser,
inviteId
)
res.sendStatus(201)
},
viewInvite(req, res, next) {
async viewInvite(req, res) {
const projectId = req.params.Project_id
const { token } = req.params
const _renderInvalidPage = function () {
logger.debug({ projectId }, 'invite not valid, rendering not-valid page')
res.render('project/invite/not-valid', { title: 'Invalid Invite' })
}
// check if the user is already a member of the project
const currentUser = SessionManager.getSessionUser(req.session)
CollaboratorsGetter.isUserInvitedMemberOfProject(
currentUser._id,
const isMember =
await CollaboratorsGetter.promises.isUserInvitedMemberOfProject(
currentUser._id,
projectId
)
if (isMember) {
logger.debug(
{ projectId, userId: currentUser._id },
'user is already a member of this project, redirecting'
)
return res.redirect(`/project/${projectId}`)
}
// get the invite
const invite = await CollaboratorsInviteHandler.promises.getInviteByToken(
projectId,
function (err, isMember) {
if (err) {
OError.tag(err, 'error checking if user is member of project', {
projectId,
})
return next(err)
}
if (isMember) {
logger.debug(
{ projectId, userId: currentUser._id },
'user is already a member of this project, redirecting'
)
return res.redirect(`/project/${projectId}`)
}
// get the invite
CollaboratorsInviteHandler.getInviteByToken(
projectId,
token,
function (err, invite) {
if (err) {
OError.tag(err, 'error getting invite by token', {
projectId,
})
return next(err)
}
// check if invite is gone, or otherwise non-existent
if (invite == null) {
logger.debug({ projectId }, 'no invite found for this token')
return _renderInvalidPage()
}
// check the user who sent the invite exists
UserGetter.getUser(
{ _id: invite.sendingUserId },
{ email: 1, first_name: 1, last_name: 1 },
function (err, owner) {
if (err) {
OError.tag(err, 'error getting project owner', {
projectId,
})
return next(err)
}
if (owner == null) {
logger.debug({ projectId }, 'no project owner found')
return _renderInvalidPage()
}
// fetch the project name
ProjectGetter.getProject(
projectId,
{},
function (err, project) {
if (err) {
OError.tag(err, 'error getting project', {
projectId,
})
return next(err)
}
if (project == null) {
logger.debug({ projectId }, 'no project found')
return _renderInvalidPage()
}
// finally render the invite
res.render('project/invite/show', {
invite,
project,
owner,
title: 'Project Invite',
})
}
)
}
)
}
)
}
token
)
// check if invite is gone, or otherwise non-existent
if (invite == null) {
logger.debug({ projectId }, 'no invite found for this token')
return _renderInvalidPage()
}
// check the user who sent the invite exists
const owner = await UserGetter.promises.getUser(
{ _id: invite.sendingUserId },
{ email: 1, first_name: 1, last_name: 1 }
)
if (owner == null) {
logger.debug({ projectId }, 'no project owner found')
return _renderInvalidPage()
}
// fetch the project name
const project = await ProjectGetter.promises.getProject(projectId, {
name: 1,
})
if (project == null) {
logger.debug({ projectId }, 'no project found')
return _renderInvalidPage()
}
// finally render the invite
res.render('project/invite/show', {
invite,
project,
owner,
title: 'Project Invite',
})
},
acceptInvite(req, res, next) {
async acceptInvite(req, res) {
const projectId = req.params.Project_id
const { token } = req.params
const currentUser = SessionManager.getSessionUser(req.session)
@ -349,36 +255,44 @@ module.exports = CollaboratorsInviteController = {
{ projectId, userId: currentUser._id },
'got request to accept invite'
)
CollaboratorsInviteHandler.acceptInvite(
await CollaboratorsInviteHandler.promises.acceptInvite(
projectId,
token,
currentUser,
function (err) {
if (err) {
OError.tag(err, 'error accepting invite by token', {
projectId,
token,
})
return next(err)
}
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true, members: true }
)
AnalyticsManager.recordEventForUser(
currentUser._id,
'project-invite-accept',
{
projectId,
}
)
if (req.xhr) {
res.sendStatus(204) // Done async via project page notification
} else {
res.redirect(`/project/${projectId}`)
}
currentUser
)
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ invites: true, members: true }
)
AnalyticsManager.recordEventForUser(
currentUser._id,
'project-invite-accept',
{
projectId,
}
)
if (req.xhr) {
res.sendStatus(204) // Done async via project page notification
} else {
res.redirect(`/project/${projectId}`)
}
},
}
module.exports = {
promises: CollaboratorsInviteController,
getAllInvites: expressify(CollaboratorsInviteController.getAllInvites),
inviteToProject: expressify(CollaboratorsInviteController.inviteToProject),
revokeInvite: expressify(CollaboratorsInviteController.revokeInvite),
resendInvite: expressify(CollaboratorsInviteController.resendInvite),
viewInvite: expressify(CollaboratorsInviteController.viewInvite),
acceptInvite: expressify(CollaboratorsInviteController.acceptInvite),
_checkShouldInviteEmail: callbackify(
CollaboratorsInviteController._checkShouldInviteEmail
),
_checkRateLimit: callbackify(CollaboratorsInviteController._checkRateLimit),
}