|
|
@@ -1,6 +1,5 @@
|
|
|
const logger = require('@overleaf/logger')
|
|
|
const crypto = require('crypto')
|
|
|
-const async = require('async')
|
|
|
|
|
|
const settings = require('@overleaf/settings')
|
|
|
const { ObjectId } = require('mongodb')
|
|
|
@@ -11,220 +10,164 @@ const UserGetter = require('../User/UserGetter')
|
|
|
const SubscriptionLocator = require('./SubscriptionLocator')
|
|
|
const SubscriptionUpdater = require('./SubscriptionUpdater')
|
|
|
const LimitationsManager = require('./LimitationsManager')
|
|
|
+const ManagedUsersHandler = require('./ManagedUsersHandler')
|
|
|
|
|
|
const EmailHandler = require('../Email/EmailHandler')
|
|
|
const EmailHelper = require('../Helpers/EmailHelper')
|
|
|
|
|
|
const Errors = require('../Errors/Errors')
|
|
|
-const { promisifyMultiResult, promisify } = require('../../util/promises')
|
|
|
-
|
|
|
-function getInvite(token, callback) {
|
|
|
- Subscription.findOne(
|
|
|
- { 'teamInvites.token': token },
|
|
|
- function (err, subscription) {
|
|
|
- if (err) {
|
|
|
- return callback(err)
|
|
|
- }
|
|
|
- if (!subscription) {
|
|
|
- return callback(new Errors.NotFoundError('team not found'))
|
|
|
- }
|
|
|
-
|
|
|
- const invite = subscription.teamInvites.find(i => i.token === token)
|
|
|
- callback(null, invite, subscription)
|
|
|
- }
|
|
|
- )
|
|
|
+const { callbackify, callbackifyMultiResult } = require('../../util/promises')
|
|
|
+
|
|
|
+async function getInvite(token) {
|
|
|
+ const subscription = await Subscription.findOne({
|
|
|
+ 'teamInvites.token': token,
|
|
|
+ })
|
|
|
+ if (!subscription) {
|
|
|
+ throw new Errors.NotFoundError('team not found')
|
|
|
+ }
|
|
|
+
|
|
|
+ const invite = subscription.teamInvites.find(i => i.token === token)
|
|
|
+ return { invite, subscription }
|
|
|
}
|
|
|
|
|
|
-function createInvite(teamManagerId, subscription, email, callback) {
|
|
|
+async function createInvite(teamManagerId, subscription, email) {
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
if (!email) {
|
|
|
- return callback(new Error('invalid email'))
|
|
|
+ throw new Error('invalid email')
|
|
|
}
|
|
|
- UserGetter.getUser(teamManagerId, function (error, teamManager) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
+ const teamManager = await UserGetter.promises.getUser(teamManagerId)
|
|
|
|
|
|
- _removeLegacyInvite(subscription.id, email, function (error) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
- _createInvite(subscription, email, teamManager, callback)
|
|
|
- })
|
|
|
- })
|
|
|
+ await _removeLegacyInvite(subscription.id, email)
|
|
|
+ return _createInvite(subscription, email, teamManager)
|
|
|
}
|
|
|
|
|
|
-function importInvite(
|
|
|
- subscription,
|
|
|
- inviterName,
|
|
|
- email,
|
|
|
- token,
|
|
|
- sentAt,
|
|
|
- callback
|
|
|
-) {
|
|
|
- _checkIfInviteIsPossible(
|
|
|
+async function importInvite(subscription, inviterName, email, token, sentAt) {
|
|
|
+ const { possible, reason } = await _checkIfInviteIsPossible(
|
|
|
subscription,
|
|
|
- email,
|
|
|
- function (error, possible, reason) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
- if (!possible) {
|
|
|
- return callback(reason)
|
|
|
- }
|
|
|
-
|
|
|
- subscription.teamInvites.push({
|
|
|
- email,
|
|
|
- inviterName,
|
|
|
- token,
|
|
|
- sentAt,
|
|
|
- })
|
|
|
-
|
|
|
- subscription.save(callback)
|
|
|
- }
|
|
|
+ email
|
|
|
)
|
|
|
+ if (!possible) {
|
|
|
+ throw reason
|
|
|
+ }
|
|
|
+ subscription.teamInvites.push({
|
|
|
+ email,
|
|
|
+ inviterName,
|
|
|
+ token,
|
|
|
+ sentAt,
|
|
|
+ })
|
|
|
+
|
|
|
+ return subscription.save()
|
|
|
}
|
|
|
|
|
|
-function acceptInvite(token, userId, callback) {
|
|
|
- getInvite(token, function (err, invite, subscription) {
|
|
|
- if (err) {
|
|
|
- return callback(err)
|
|
|
- }
|
|
|
- if (!invite) {
|
|
|
- return callback(new Errors.NotFoundError('invite not found'))
|
|
|
- }
|
|
|
+async function acceptInvite(token, userId) {
|
|
|
+ const { invite, subscription } = await getInvite(token)
|
|
|
+ if (!invite) {
|
|
|
+ throw new Errors.NotFoundError('invite not found')
|
|
|
+ }
|
|
|
|
|
|
- SubscriptionUpdater.addUserToGroup(
|
|
|
- subscription._id,
|
|
|
- userId,
|
|
|
- function (err) {
|
|
|
- if (err) {
|
|
|
- return callback(err)
|
|
|
- }
|
|
|
+ await SubscriptionUpdater.promises.addUserToGroup(subscription._id, userId)
|
|
|
|
|
|
- _removeInviteFromTeam(subscription.id, invite.email, callback)
|
|
|
- }
|
|
|
+ if (subscription.groupPolicy) {
|
|
|
+ await ManagedUsersHandler.promises.enrollInSubscription(
|
|
|
+ userId,
|
|
|
+ subscription
|
|
|
)
|
|
|
- })
|
|
|
+ }
|
|
|
+
|
|
|
+ await _removeInviteFromTeam(subscription.id, invite.email)
|
|
|
}
|
|
|
|
|
|
-function revokeInvite(teamManagerId, subscription, email, callback) {
|
|
|
+async function revokeInvite(teamManagerId, subscription, email) {
|
|
|
email = EmailHelper.parseEmail(email)
|
|
|
if (!email) {
|
|
|
- return callback(new Error('invalid email'))
|
|
|
+ throw new Error('invalid email')
|
|
|
}
|
|
|
- _removeInviteFromTeam(subscription.id, email, callback)
|
|
|
+ await _removeInviteFromTeam(subscription.id, email)
|
|
|
}
|
|
|
|
|
|
// Legacy method to allow a user to receive a confirmation email if their
|
|
|
// email is in Subscription.invited_emails when they join. We'll remove this
|
|
|
// after a short while.
|
|
|
-function createTeamInvitesForLegacyInvitedEmail(email, callback) {
|
|
|
- SubscriptionLocator.getGroupsWithEmailInvite(email, function (err, teams) {
|
|
|
- if (err) {
|
|
|
- return callback(err)
|
|
|
- }
|
|
|
+async function createTeamInvitesForLegacyInvitedEmail(email) {
|
|
|
+ const teams = await SubscriptionLocator.promises.getGroupsWithEmailInvite(
|
|
|
+ email
|
|
|
+ )
|
|
|
|
|
|
- async.map(
|
|
|
- teams,
|
|
|
- (team, cb) => createInvite(team.admin_id, team, email, cb),
|
|
|
- callback
|
|
|
- )
|
|
|
- })
|
|
|
+ return Promise.all(
|
|
|
+ teams.map(team => createInvite(team.admin_id, team, email))
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
-function _createInvite(subscription, email, inviter, callback) {
|
|
|
- _checkIfInviteIsPossible(
|
|
|
+async function _createInvite(subscription, email, inviter) {
|
|
|
+ const { possible, reason } = await _checkIfInviteIsPossible(
|
|
|
subscription,
|
|
|
- email,
|
|
|
- function (error, possible, reason) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
- if (!possible) {
|
|
|
- return callback(reason)
|
|
|
- }
|
|
|
-
|
|
|
- // don't send invites when inviting self; add user directly to the group
|
|
|
- const isInvitingSelf = inviter.emails.some(
|
|
|
- emailData => emailData.email === email
|
|
|
- )
|
|
|
- if (isInvitingSelf) {
|
|
|
- return SubscriptionUpdater.addUserToGroup(
|
|
|
- subscription._id,
|
|
|
- inviter._id,
|
|
|
- err => {
|
|
|
- if (err) {
|
|
|
- return callback(err)
|
|
|
- }
|
|
|
-
|
|
|
- // legacy: remove any invite that might have been created in the past
|
|
|
- _removeInviteFromTeam(subscription._id, email, error => {
|
|
|
- const inviteUserData = {
|
|
|
- email: inviter.email,
|
|
|
- first_name: inviter.first_name,
|
|
|
- last_name: inviter.last_name,
|
|
|
- invite: false,
|
|
|
- }
|
|
|
- callback(error, inviteUserData)
|
|
|
- })
|
|
|
- }
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- const inviterName = _getInviterName(inviter)
|
|
|
- let invite = subscription.teamInvites.find(
|
|
|
- invite => invite.email === email
|
|
|
- )
|
|
|
-
|
|
|
- if (invite) {
|
|
|
- invite = invite.toObject()
|
|
|
- invite.sentAt = new Date()
|
|
|
- } else {
|
|
|
- invite = {
|
|
|
- email,
|
|
|
- inviterName,
|
|
|
- token: crypto.randomBytes(32).toString('hex'),
|
|
|
- sentAt: new Date(),
|
|
|
- }
|
|
|
- subscription.teamInvites.push(invite)
|
|
|
- }
|
|
|
-
|
|
|
- subscription.save(function (error) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
-
|
|
|
- const opts = {
|
|
|
- to: email,
|
|
|
- inviter,
|
|
|
- acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
|
|
|
- appName: settings.appName,
|
|
|
- }
|
|
|
- EmailHandler.sendEmail('verifyEmailToJoinTeam', opts, error => {
|
|
|
- Object.assign(invite, { invite: true })
|
|
|
- callback(error, invite)
|
|
|
- })
|
|
|
- })
|
|
|
- }
|
|
|
+ email
|
|
|
)
|
|
|
+
|
|
|
+ if (!possible) {
|
|
|
+ throw reason
|
|
|
+ }
|
|
|
+
|
|
|
+ // don't send invites when inviting self; add user directly to the group
|
|
|
+ const isInvitingSelf = inviter.emails.some(
|
|
|
+ emailData => emailData.email === email
|
|
|
+ )
|
|
|
+ if (isInvitingSelf) {
|
|
|
+ await SubscriptionUpdater.promises.addUserToGroup(
|
|
|
+ subscription._id,
|
|
|
+ inviter._id
|
|
|
+ )
|
|
|
+
|
|
|
+ // legacy: remove any invite that might have been created in the past
|
|
|
+ await _removeInviteFromTeam(subscription._id, email)
|
|
|
+
|
|
|
+ return {
|
|
|
+ email: inviter.email,
|
|
|
+ first_name: inviter.first_name,
|
|
|
+ last_name: inviter.last_name,
|
|
|
+ invite: false,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const inviterName = _getInviterName(inviter)
|
|
|
+ let invite = subscription.teamInvites.find(invite => invite.email === email)
|
|
|
+
|
|
|
+ if (invite) {
|
|
|
+ invite = invite.toObject()
|
|
|
+ invite.sentAt = new Date()
|
|
|
+ } else {
|
|
|
+ invite = {
|
|
|
+ email,
|
|
|
+ inviterName,
|
|
|
+ token: crypto.randomBytes(32).toString('hex'),
|
|
|
+ sentAt: new Date(),
|
|
|
+ }
|
|
|
+ subscription.teamInvites.push(invite)
|
|
|
+ }
|
|
|
+
|
|
|
+ await subscription.save()
|
|
|
+
|
|
|
+ const opts = {
|
|
|
+ to: email,
|
|
|
+ inviter,
|
|
|
+ acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
|
|
|
+ appName: settings.appName,
|
|
|
+ }
|
|
|
+ await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts)
|
|
|
+ Object.assign(invite, { invite: true })
|
|
|
+ return invite
|
|
|
}
|
|
|
|
|
|
-function _removeInviteFromTeam(subscriptionId, email, callback) {
|
|
|
+async function _removeInviteFromTeam(subscriptionId, email, callback) {
|
|
|
const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
|
|
|
const removeInvite = { $pull: { teamInvites: { email } } }
|
|
|
|
|
|
- async.series(
|
|
|
- [
|
|
|
- cb => Subscription.updateOne(searchConditions, removeInvite, cb),
|
|
|
- cb => _removeLegacyInvite(subscriptionId, email, cb),
|
|
|
- ],
|
|
|
- callback
|
|
|
- )
|
|
|
+ await Subscription.updateOne(searchConditions, removeInvite)
|
|
|
+ await _removeLegacyInvite(subscriptionId, email)
|
|
|
}
|
|
|
|
|
|
-const _removeLegacyInvite = (subscriptionId, email, callback) =>
|
|
|
- Subscription.updateOne(
|
|
|
+async function _removeLegacyInvite(subscriptionId, email) {
|
|
|
+ await Subscription.updateOne(
|
|
|
{
|
|
|
_id: new ObjectId(subscriptionId.toString()),
|
|
|
},
|
|
|
@@ -232,17 +175,17 @@ const _removeLegacyInvite = (subscriptionId, email, callback) =>
|
|
|
$pull: {
|
|
|
invited_emails: email,
|
|
|
},
|
|
|
- },
|
|
|
- callback
|
|
|
+ }
|
|
|
)
|
|
|
+}
|
|
|
|
|
|
-function _checkIfInviteIsPossible(subscription, email, callback) {
|
|
|
+async function _checkIfInviteIsPossible(subscription, email) {
|
|
|
if (!subscription.groupPlan) {
|
|
|
logger.debug(
|
|
|
{ subscriptionId: subscription.id },
|
|
|
'can not add members to a subscription that is not in a group plan'
|
|
|
)
|
|
|
- return callback(null, false, { wrongPlan: true })
|
|
|
+ return { possible: false, reason: { wrongPlan: true } }
|
|
|
}
|
|
|
|
|
|
if (LimitationsManager.teamHasReachedMemberLimit(subscription)) {
|
|
|
@@ -250,31 +193,27 @@ function _checkIfInviteIsPossible(subscription, email, callback) {
|
|
|
{ subscriptionId: subscription.id },
|
|
|
'team has reached member limit'
|
|
|
)
|
|
|
- return callback(null, false, { limitReached: true })
|
|
|
+ return { possible: false, reason: { limitReached: true } }
|
|
|
}
|
|
|
|
|
|
- UserGetter.getUserByAnyEmail(email, function (error, existingUser) {
|
|
|
- if (error) {
|
|
|
- return callback(error)
|
|
|
- }
|
|
|
- if (!existingUser) {
|
|
|
- return callback(null, true)
|
|
|
- }
|
|
|
+ const existingUser = await UserGetter.promises.getUserByAnyEmail(email)
|
|
|
+ if (!existingUser) {
|
|
|
+ return { possible: true }
|
|
|
+ }
|
|
|
|
|
|
- const existingMember = subscription.member_ids.find(
|
|
|
- memberId => memberId.toString() === existingUser._id.toString()
|
|
|
- )
|
|
|
+ const existingMember = subscription.member_ids.find(
|
|
|
+ memberId => memberId.toString() === existingUser._id.toString()
|
|
|
+ )
|
|
|
|
|
|
- if (existingMember) {
|
|
|
- logger.debug(
|
|
|
- { subscriptionId: subscription.id, email },
|
|
|
- 'user already in team'
|
|
|
- )
|
|
|
- callback(null, false, { alreadyInTeam: true })
|
|
|
- } else {
|
|
|
- callback(null, true)
|
|
|
- }
|
|
|
- })
|
|
|
+ if (existingMember) {
|
|
|
+ logger.debug(
|
|
|
+ { subscriptionId: subscription.id, email },
|
|
|
+ 'user already in team'
|
|
|
+ )
|
|
|
+ return { possible: false, reason: { alreadyInTeam: true } }
|
|
|
+ } else {
|
|
|
+ return { possible: true }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
function _getInviterName(inviter) {
|
|
|
@@ -289,20 +228,20 @@ function _getInviterName(inviter) {
|
|
|
}
|
|
|
|
|
|
module.exports = {
|
|
|
- getInvite,
|
|
|
- createInvite,
|
|
|
- importInvite,
|
|
|
- acceptInvite,
|
|
|
- revokeInvite,
|
|
|
- createTeamInvitesForLegacyInvitedEmail,
|
|
|
+ getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']),
|
|
|
+ createInvite: callbackify(createInvite),
|
|
|
+ importInvite: callbackify(importInvite),
|
|
|
+ acceptInvite: callbackify(acceptInvite),
|
|
|
+ revokeInvite: callbackify(revokeInvite),
|
|
|
+ createTeamInvitesForLegacyInvitedEmail: callbackify(
|
|
|
+ createTeamInvitesForLegacyInvitedEmail
|
|
|
+ ),
|
|
|
promises: {
|
|
|
- getInvite: promisifyMultiResult(getInvite, ['invite', 'subscription']),
|
|
|
- createInvite: promisify(createInvite),
|
|
|
- importInvite: promisify(importInvite),
|
|
|
- acceptInvite: promisify(acceptInvite),
|
|
|
- revokeInvite: promisify(revokeInvite),
|
|
|
- createTeamInvitesForLegacyInvitedEmail: promisify(
|
|
|
- createTeamInvitesForLegacyInvitedEmail
|
|
|
- ),
|
|
|
+ getInvite,
|
|
|
+ createInvite,
|
|
|
+ importInvite,
|
|
|
+ acceptInvite,
|
|
|
+ revokeInvite,
|
|
|
+ createTeamInvitesForLegacyInvitedEmail,
|
|
|
},
|
|
|
}
|