mirror of
https://github.com/overleaf/overleaf.git
synced 2025-03-20 01:13:16 +00:00
Merge pull request #14989 from overleaf/em-promisify-collaborators-invite-controller
Promisify CollaboratorsInviteController GitOrigin-RevId: 007b77823a9332464123c11da55dda8571803095
This commit is contained in:
parent
aafb0d377c
commit
0f473d9a9f
2 changed files with 591 additions and 665 deletions
|
@ -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),
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue