mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-03 14:19:01 +02:00
59dc237e53
[web] Create invite token when domain capture is disabled GitOrigin-RevId: 193b5b8567c3b593c98f432568b47cc4e9e17339
468 lines
12 KiB
JavaScript
468 lines
12 KiB
JavaScript
import logger from '@overleaf/logger'
|
|
import crypto from 'node:crypto'
|
|
import settings from '@overleaf/settings'
|
|
import Modules from '../../infrastructure/Modules.mjs'
|
|
import mongodb from 'mongodb-legacy'
|
|
import { Subscription } from '../../models/Subscription.mjs'
|
|
import { SSOConfig } from '../../models/SSOConfig.mjs'
|
|
import UserGetter from '../User/UserGetter.mjs'
|
|
import SubscriptionLocator from './SubscriptionLocator.mjs'
|
|
import SubscriptionUpdater from './SubscriptionUpdater.mjs'
|
|
import LimitationsManager from './LimitationsManager.mjs'
|
|
import EmailHandler from '../Email/EmailHandler.mjs'
|
|
import EmailHelper from '../Helpers/EmailHelper.mjs'
|
|
import Errors from '../Errors/Errors.js'
|
|
import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
|
|
import NotificationsBuilder from '../Notifications/NotificationsBuilder.mjs'
|
|
|
|
const { ObjectId } = mongodb
|
|
|
|
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 }
|
|
}
|
|
|
|
async function createInvite(teamManagerId, subscription, email, auditLog) {
|
|
email = EmailHelper.parseEmail(email)
|
|
if (!email) {
|
|
throw new Error('invalid email')
|
|
}
|
|
const teamManager = await UserGetter.promises.getUser(teamManagerId)
|
|
|
|
await _removeLegacyInvite(subscription.id, email)
|
|
return _createInvite(subscription, email, teamManager, auditLog)
|
|
}
|
|
|
|
async function importInvite(subscription, inviterName, email, token, sentAt) {
|
|
const { possible, reason } = await _checkIfInviteIsPossible(
|
|
subscription,
|
|
email
|
|
)
|
|
if (!possible) {
|
|
throw reason
|
|
}
|
|
subscription.teamInvites.push({
|
|
email,
|
|
inviterName,
|
|
token,
|
|
sentAt,
|
|
})
|
|
|
|
return subscription.save()
|
|
}
|
|
|
|
async function _deleteUserSubscription(subscription, userId, ipAddress) {
|
|
// Delete released user subscription to make it on a free plan
|
|
|
|
logger.debug(
|
|
{
|
|
subscriptionId: subscription._id,
|
|
},
|
|
'deleting user subscription'
|
|
)
|
|
|
|
const deleterData = {
|
|
id: userId,
|
|
ip: ipAddress,
|
|
}
|
|
await SubscriptionUpdater.promises.deleteSubscription(
|
|
subscription,
|
|
deleterData
|
|
)
|
|
|
|
try {
|
|
await Modules.promises.hooks.fire('terminateSubscription', subscription)
|
|
} catch (err) {
|
|
logger.error(
|
|
{ err, subscriptionId: subscription._id },
|
|
'terminating subscription failed'
|
|
)
|
|
}
|
|
}
|
|
|
|
async function removeTeamInviteAndNotification(subscriptionId, userId, email) {
|
|
await _removeInviteFromTeam(subscriptionId, email)
|
|
|
|
await NotificationsBuilder.promises
|
|
.groupInvitation(userId, subscriptionId, false)
|
|
.read()
|
|
}
|
|
|
|
async function acceptInvite(token, userId, ipAddress) {
|
|
const { invite, subscription } = await getInvite(token)
|
|
if (!invite) {
|
|
throw new Errors.NotFoundError('invite not found')
|
|
}
|
|
const auditLog = { initiatorId: userId, ipAddress }
|
|
|
|
await SubscriptionUpdater.promises.addUserToGroup(
|
|
subscription._id,
|
|
userId,
|
|
auditLog
|
|
)
|
|
|
|
if (subscription.managedUsersEnabled) {
|
|
// check if user has a personal subscription
|
|
const userSubscription =
|
|
await SubscriptionLocator.promises.getUsersSubscription(userId)
|
|
|
|
if (userSubscription) {
|
|
// if user has a personal subscription and joins a managed group, delete their personal subscription
|
|
// but make sure that it's not the same subscription as the group one.
|
|
if (!userSubscription._id.equals(subscription._id)) {
|
|
await _deleteUserSubscription(userSubscription, userId, ipAddress)
|
|
}
|
|
}
|
|
await Modules.promises.hooks.fire(
|
|
'enrollInManagedSubscription',
|
|
userId,
|
|
subscription,
|
|
auditLog
|
|
)
|
|
}
|
|
if (subscription.ssoConfig) {
|
|
const ssoConfig = await SSOConfig.findById(
|
|
subscription.ssoConfig._id || subscription.ssoConfig
|
|
)
|
|
if (ssoConfig?.enabled) {
|
|
await Modules.promises.hooks.fire(
|
|
'scheduleGroupSSOReminder',
|
|
userId,
|
|
subscription._id
|
|
)
|
|
}
|
|
}
|
|
|
|
await removeTeamInviteAndNotification(subscription._id, userId, invite.email)
|
|
|
|
return subscription
|
|
}
|
|
|
|
async function revokeInvite(teamManagerId, subscription, email) {
|
|
email = EmailHelper.parseEmail(email)
|
|
|
|
if (!email) {
|
|
throw new Error('invalid email')
|
|
}
|
|
|
|
await _removeInviteFromTeam(subscription.id, email)
|
|
|
|
// Remove group invitation dashboard notification if invitation is revoked before
|
|
// the invited user accepted the group invitation
|
|
const user = await UserGetter.promises.getUserByAnyEmail(email)
|
|
if (user) {
|
|
await NotificationsBuilder.promises
|
|
.groupInvitation(user._id, subscription._id, false)
|
|
.read()
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
async function createTeamInvitesForLegacyInvitedEmail(email) {
|
|
const teams =
|
|
await SubscriptionLocator.promises.getGroupsWithEmailInvite(email)
|
|
return Promise.all(
|
|
teams.map(team => createInvite(team.admin_id, team, email))
|
|
)
|
|
}
|
|
|
|
async function _createInvite(subscription, email, inviter, auditLog) {
|
|
const { domainCaptureEnabled, managedUsersEnabled } = subscription
|
|
const { possible, reason } = await _checkIfInviteIsPossible(
|
|
subscription,
|
|
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) {
|
|
const auditLog = { initiatorId: inviter._id }
|
|
await SubscriptionUpdater.promises.addUserToGroup(
|
|
subscription._id,
|
|
inviter._id,
|
|
auditLog
|
|
)
|
|
|
|
// legacy: remove any invite that might have been created in the past
|
|
await _removeInviteFromTeam(subscription._id, email)
|
|
|
|
try {
|
|
if (subscription.ssoConfig) {
|
|
const ssoConfig = await SSOConfig.findById(
|
|
subscription.ssoConfig._id || subscription.ssoConfig
|
|
)
|
|
if (ssoConfig?.enabled) {
|
|
await Modules.promises.hooks.fire(
|
|
'sendGroupSSOReminder',
|
|
inviter._id,
|
|
subscription._id
|
|
)
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
{ err: error, userId: inviter._id, subscriptionId: subscription._id },
|
|
'Failed to schedule Group SSO invite for group admin'
|
|
)
|
|
}
|
|
|
|
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.sentAt = new Date()
|
|
if (!invite.token && !domainCaptureEnabled) {
|
|
invite.token = crypto.randomBytes(32).toString('hex')
|
|
invite.domainCapture = false
|
|
}
|
|
} else {
|
|
if (domainCaptureEnabled) {
|
|
invite = {
|
|
email,
|
|
inviterName,
|
|
sentAt: new Date(),
|
|
domainCapture: true,
|
|
}
|
|
} else {
|
|
invite = {
|
|
email,
|
|
inviterName,
|
|
token: crypto.randomBytes(32).toString('hex'),
|
|
sentAt: new Date(),
|
|
}
|
|
}
|
|
|
|
subscription.teamInvites.push(invite)
|
|
}
|
|
|
|
if (!domainCaptureEnabled) {
|
|
// no need to create notification when domain capture is enabled since
|
|
// dash will show one on page load for non-managed groups, and for managed groups
|
|
// dash is not loadable until user joins the group
|
|
try {
|
|
await _sendNotificationToExistingUser(
|
|
subscription,
|
|
email,
|
|
invite,
|
|
subscription.managedUsersEnabled
|
|
)
|
|
} catch (err) {
|
|
logger.error(
|
|
{ err },
|
|
'Failed to send notification to existing user when creating group invitation'
|
|
)
|
|
}
|
|
}
|
|
|
|
await subscription.save()
|
|
|
|
if (subscription.managedUsersEnabled) {
|
|
const auditLogData = {
|
|
initiatorId: auditLog?.initiatorId,
|
|
ipAddress: auditLog?.ipAddress,
|
|
groupId: subscription._id,
|
|
operation: 'group-invite-sent',
|
|
info: { invitedEmail: email },
|
|
}
|
|
|
|
try {
|
|
await Modules.promises.hooks.fire('addGroupAuditLogEntry', auditLogData)
|
|
} catch (error) {
|
|
logger.error(
|
|
{ error, auditLog },
|
|
'Error adding group audit log entry for group-invite-sent'
|
|
)
|
|
}
|
|
}
|
|
|
|
let acceptInviteUrl
|
|
if (domainCaptureEnabled) {
|
|
const samlInitPath = (
|
|
await Modules.promises.hooks.fire(
|
|
'getGroupSSOInitPath',
|
|
subscription,
|
|
email
|
|
)
|
|
)?.[0]
|
|
acceptInviteUrl = `${settings.siteUrl}${samlInitPath}`
|
|
} else {
|
|
acceptInviteUrl = `${settings.siteUrl}/subscription/invites/${invite.token}/`
|
|
}
|
|
if (managedUsersEnabled) {
|
|
let admin = {}
|
|
try {
|
|
admin = await SubscriptionLocator.promises.getAdminEmailAndName(
|
|
subscription._id
|
|
)
|
|
} catch (err) {
|
|
logger.error({ err }, 'error getting subscription admin email and name')
|
|
}
|
|
|
|
const user = await UserGetter.promises.getUserByAnyEmail(email)
|
|
|
|
const opts = {
|
|
to: email,
|
|
admin,
|
|
inviter,
|
|
acceptInviteUrl,
|
|
appName: settings.appName,
|
|
}
|
|
|
|
if (user) {
|
|
await EmailHandler.promises.sendEmail(
|
|
'verifyEmailToJoinManagedUsers',
|
|
opts
|
|
)
|
|
} else {
|
|
await EmailHandler.promises.sendEmail(
|
|
'inviteNewUserToJoinManagedUsers',
|
|
opts
|
|
)
|
|
}
|
|
} else {
|
|
const opts = {
|
|
to: email,
|
|
inviter,
|
|
acceptInviteUrl,
|
|
appName: settings.appName,
|
|
}
|
|
await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts)
|
|
}
|
|
|
|
Object.assign(invite, { invite: true })
|
|
return invite
|
|
}
|
|
|
|
async function _removeInviteFromTeam(subscriptionId, email, callback) {
|
|
const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
|
|
const removeInvite = { $pull: { teamInvites: { email } } }
|
|
|
|
await Subscription.updateOne(searchConditions, removeInvite)
|
|
await _removeLegacyInvite(subscriptionId, email)
|
|
}
|
|
|
|
async function _sendNotificationToExistingUser(
|
|
subscription,
|
|
email,
|
|
invite,
|
|
managedUsersEnabled
|
|
) {
|
|
const user = await UserGetter.promises.getUserByMainEmail(email)
|
|
|
|
if (!user) {
|
|
return
|
|
}
|
|
|
|
await NotificationsBuilder.promises
|
|
.groupInvitation(
|
|
user._id.toString(),
|
|
subscription._id.toString(),
|
|
managedUsersEnabled
|
|
)
|
|
.create(invite)
|
|
}
|
|
|
|
async function _removeLegacyInvite(subscriptionId, email) {
|
|
await Subscription.updateOne(
|
|
{
|
|
_id: new ObjectId(subscriptionId.toString()),
|
|
},
|
|
{
|
|
$pull: {
|
|
invited_emails: email,
|
|
},
|
|
}
|
|
)
|
|
}
|
|
|
|
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 { possible: false, reason: { wrongPlan: true } }
|
|
}
|
|
|
|
if (LimitationsManager.teamHasReachedMemberLimit(subscription)) {
|
|
logger.debug(
|
|
{ subscriptionId: subscription.id },
|
|
'team has reached member limit'
|
|
)
|
|
return { possible: false, reason: { limitReached: 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()
|
|
)
|
|
|
|
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) {
|
|
let inviterName
|
|
if (inviter.first_name && inviter.last_name) {
|
|
inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})`
|
|
} else {
|
|
inviterName = inviter.email
|
|
}
|
|
|
|
return inviterName
|
|
}
|
|
|
|
export default {
|
|
getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']),
|
|
createInvite: callbackify(createInvite),
|
|
importInvite: callbackify(importInvite),
|
|
acceptInvite: callbackify(acceptInvite),
|
|
revokeInvite: callbackify(revokeInvite),
|
|
createTeamInvitesForLegacyInvitedEmail: callbackify(
|
|
createTeamInvitesForLegacyInvitedEmail
|
|
),
|
|
promises: {
|
|
getInvite,
|
|
createInvite,
|
|
importInvite,
|
|
acceptInvite,
|
|
removeTeamInviteAndNotification,
|
|
revokeInvite,
|
|
createTeamInvitesForLegacyInvitedEmail,
|
|
},
|
|
}
|