TeamInvitesHandler.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import logger from '@overleaf/logger'
  2. import crypto from 'node:crypto'
  3. import settings from '@overleaf/settings'
  4. import Modules from '../../infrastructure/Modules.js'
  5. import mongodb from 'mongodb-legacy'
  6. import { Subscription } from '../../models/Subscription.mjs'
  7. import { SSOConfig } from '../../models/SSOConfig.mjs'
  8. import UserGetter from '../User/UserGetter.mjs'
  9. import SubscriptionLocator from './SubscriptionLocator.mjs'
  10. import SubscriptionUpdater from './SubscriptionUpdater.mjs'
  11. import LimitationsManager from './LimitationsManager.mjs'
  12. import EmailHandler from '../Email/EmailHandler.mjs'
  13. import EmailHelper from '../Helpers/EmailHelper.mjs'
  14. import Errors from '../Errors/Errors.js'
  15. import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
  16. import NotificationsBuilder from '../Notifications/NotificationsBuilder.mjs'
  17. const { ObjectId } = mongodb
  18. async function getInvite(token) {
  19. const subscription = await Subscription.findOne({
  20. 'teamInvites.token': token,
  21. })
  22. if (!subscription) {
  23. throw new Errors.NotFoundError('team not found')
  24. }
  25. const invite = subscription.teamInvites.find(i => i.token === token)
  26. return { invite, subscription }
  27. }
  28. async function createInvite(teamManagerId, subscription, email) {
  29. email = EmailHelper.parseEmail(email)
  30. if (!email) {
  31. throw new Error('invalid email')
  32. }
  33. const teamManager = await UserGetter.promises.getUser(teamManagerId)
  34. await _removeLegacyInvite(subscription.id, email)
  35. return _createInvite(subscription, email, teamManager)
  36. }
  37. async function importInvite(subscription, inviterName, email, token, sentAt) {
  38. const { possible, reason } = await _checkIfInviteIsPossible(
  39. subscription,
  40. email
  41. )
  42. if (!possible) {
  43. throw reason
  44. }
  45. subscription.teamInvites.push({
  46. email,
  47. inviterName,
  48. token,
  49. sentAt,
  50. })
  51. return subscription.save()
  52. }
  53. async function _deleteUserSubscription(subscription, userId, ipAddress) {
  54. // Delete released user subscription to make it on a free plan
  55. logger.debug(
  56. {
  57. subscriptionId: subscription._id,
  58. },
  59. 'deleting user subscription'
  60. )
  61. const deleterData = {
  62. id: userId,
  63. ip: ipAddress,
  64. }
  65. await SubscriptionUpdater.promises.deleteSubscription(
  66. subscription,
  67. deleterData
  68. )
  69. try {
  70. await Modules.promises.hooks.fire('terminateSubscription', subscription)
  71. } catch (err) {
  72. logger.error(
  73. { err, subscriptionId: subscription._id },
  74. 'terminating subscription failed'
  75. )
  76. }
  77. }
  78. async function acceptInvite(token, userId, ipAddress) {
  79. const { invite, subscription } = await getInvite(token)
  80. if (!invite) {
  81. throw new Errors.NotFoundError('invite not found')
  82. }
  83. const auditLog = { initiatorId: userId, ipAddress }
  84. await SubscriptionUpdater.promises.addUserToGroup(
  85. subscription._id,
  86. userId,
  87. auditLog
  88. )
  89. if (subscription.managedUsersEnabled) {
  90. // check if user has a personal subscription
  91. const userSubscription =
  92. await SubscriptionLocator.promises.getUsersSubscription(userId)
  93. if (userSubscription) {
  94. // if user has a personal subscription and joins a managed group, delete their personal subscription
  95. // but make sure that it's not the same subscription as the group one.
  96. if (!userSubscription._id.equals(subscription._id)) {
  97. await _deleteUserSubscription(userSubscription, userId, ipAddress)
  98. }
  99. }
  100. await Modules.promises.hooks.fire(
  101. 'enrollInManagedSubscription',
  102. userId,
  103. subscription,
  104. auditLog
  105. )
  106. }
  107. if (subscription.ssoConfig) {
  108. const ssoConfig = await SSOConfig.findById(
  109. subscription.ssoConfig._id || subscription.ssoConfig
  110. )
  111. if (ssoConfig?.enabled) {
  112. await Modules.promises.hooks.fire(
  113. 'scheduleGroupSSOReminder',
  114. userId,
  115. subscription._id
  116. )
  117. }
  118. }
  119. await _removeInviteFromTeam(subscription.id, invite.email)
  120. await NotificationsBuilder.promises
  121. .groupInvitation(userId, subscription._id, false)
  122. .read()
  123. return subscription
  124. }
  125. async function revokeInvite(teamManagerId, subscription, email) {
  126. email = EmailHelper.parseEmail(email)
  127. if (!email) {
  128. throw new Error('invalid email')
  129. }
  130. await _removeInviteFromTeam(subscription.id, email)
  131. // Remove group invitation dashboard notification if invitation is revoked before
  132. // the invited user accepted the group invitation
  133. const user = await UserGetter.promises.getUserByAnyEmail(email)
  134. if (user) {
  135. await NotificationsBuilder.promises
  136. .groupInvitation(user._id, subscription._id, false)
  137. .read()
  138. }
  139. }
  140. // Legacy method to allow a user to receive a confirmation email if their
  141. // email is in Subscription.invited_emails when they join. We'll remove this
  142. // after a short while.
  143. async function createTeamInvitesForLegacyInvitedEmail(email) {
  144. const teams =
  145. await SubscriptionLocator.promises.getGroupsWithEmailInvite(email)
  146. return Promise.all(
  147. teams.map(team => createInvite(team.admin_id, team, email))
  148. )
  149. }
  150. async function _createInvite(subscription, email, inviter) {
  151. const { possible, reason } = await _checkIfInviteIsPossible(
  152. subscription,
  153. email
  154. )
  155. if (!possible) {
  156. throw reason
  157. }
  158. // don't send invites when inviting self; add user directly to the group
  159. const isInvitingSelf = inviter.emails.some(
  160. emailData => emailData.email === email
  161. )
  162. if (isInvitingSelf) {
  163. const auditLog = { initiatorId: inviter._id }
  164. await SubscriptionUpdater.promises.addUserToGroup(
  165. subscription._id,
  166. inviter._id,
  167. auditLog
  168. )
  169. // legacy: remove any invite that might have been created in the past
  170. await _removeInviteFromTeam(subscription._id, email)
  171. try {
  172. if (subscription.ssoConfig) {
  173. const ssoConfig = await SSOConfig.findById(
  174. subscription.ssoConfig._id || subscription.ssoConfig
  175. )
  176. if (ssoConfig?.enabled) {
  177. await Modules.promises.hooks.fire(
  178. 'sendGroupSSOReminder',
  179. inviter._id,
  180. subscription._id
  181. )
  182. }
  183. }
  184. } catch (error) {
  185. logger.error(
  186. { err: error, userId: inviter._id, subscriptionId: subscription._id },
  187. 'Failed to schedule Group SSO invite for group admin'
  188. )
  189. }
  190. return {
  191. email: inviter.email,
  192. first_name: inviter.first_name,
  193. last_name: inviter.last_name,
  194. invite: false,
  195. }
  196. }
  197. const inviterName = _getInviterName(inviter)
  198. let invite = subscription.teamInvites.find(invite => invite.email === email)
  199. if (invite) {
  200. invite = invite.toObject()
  201. invite.sentAt = new Date()
  202. } else {
  203. invite = {
  204. email,
  205. inviterName,
  206. token: crypto.randomBytes(32).toString('hex'),
  207. sentAt: new Date(),
  208. }
  209. subscription.teamInvites.push(invite)
  210. }
  211. try {
  212. await _sendNotificationToExistingUser(
  213. subscription,
  214. email,
  215. invite,
  216. subscription.managedUsersEnabled
  217. )
  218. } catch (err) {
  219. logger.error(
  220. { err },
  221. 'Failed to send notification to existing user when creating group invitation'
  222. )
  223. }
  224. await subscription.save()
  225. if (subscription.managedUsersEnabled) {
  226. let admin = {}
  227. try {
  228. admin = await SubscriptionLocator.promises.getAdminEmailAndName(
  229. subscription._id
  230. )
  231. } catch (err) {
  232. logger.error({ err }, 'error getting subscription admin email and name')
  233. }
  234. const user = await UserGetter.promises.getUserByAnyEmail(email)
  235. const opts = {
  236. to: email,
  237. admin,
  238. inviter,
  239. acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
  240. appName: settings.appName,
  241. }
  242. if (user) {
  243. await EmailHandler.promises.sendEmail(
  244. 'verifyEmailToJoinManagedUsers',
  245. opts
  246. )
  247. } else {
  248. await EmailHandler.promises.sendEmail(
  249. 'inviteNewUserToJoinManagedUsers',
  250. opts
  251. )
  252. }
  253. } else {
  254. const opts = {
  255. to: email,
  256. inviter,
  257. acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
  258. appName: settings.appName,
  259. }
  260. await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts)
  261. }
  262. Object.assign(invite, { invite: true })
  263. return invite
  264. }
  265. async function _removeInviteFromTeam(subscriptionId, email, callback) {
  266. const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
  267. const removeInvite = { $pull: { teamInvites: { email } } }
  268. await Subscription.updateOne(searchConditions, removeInvite)
  269. await _removeLegacyInvite(subscriptionId, email)
  270. }
  271. async function _sendNotificationToExistingUser(
  272. subscription,
  273. email,
  274. invite,
  275. managedUsersEnabled
  276. ) {
  277. const user = await UserGetter.promises.getUserByMainEmail(email)
  278. if (!user) {
  279. return
  280. }
  281. await NotificationsBuilder.promises
  282. .groupInvitation(
  283. user._id.toString(),
  284. subscription._id.toString(),
  285. managedUsersEnabled
  286. )
  287. .create(invite)
  288. }
  289. async function _removeLegacyInvite(subscriptionId, email) {
  290. await Subscription.updateOne(
  291. {
  292. _id: new ObjectId(subscriptionId.toString()),
  293. },
  294. {
  295. $pull: {
  296. invited_emails: email,
  297. },
  298. }
  299. )
  300. }
  301. async function _checkIfInviteIsPossible(subscription, email) {
  302. if (!subscription.groupPlan) {
  303. logger.debug(
  304. { subscriptionId: subscription.id },
  305. 'can not add members to a subscription that is not in a group plan'
  306. )
  307. return { possible: false, reason: { wrongPlan: true } }
  308. }
  309. if (LimitationsManager.teamHasReachedMemberLimit(subscription)) {
  310. logger.debug(
  311. { subscriptionId: subscription.id },
  312. 'team has reached member limit'
  313. )
  314. return { possible: false, reason: { limitReached: true } }
  315. }
  316. const existingUser = await UserGetter.promises.getUserByAnyEmail(email)
  317. if (!existingUser) {
  318. return { possible: true }
  319. }
  320. const existingMember = subscription.member_ids.find(
  321. memberId => memberId.toString() === existingUser._id.toString()
  322. )
  323. if (existingMember) {
  324. logger.debug(
  325. { subscriptionId: subscription.id, email },
  326. 'user already in team'
  327. )
  328. return { possible: false, reason: { alreadyInTeam: true } }
  329. } else {
  330. return { possible: true }
  331. }
  332. }
  333. function _getInviterName(inviter) {
  334. let inviterName
  335. if (inviter.first_name && inviter.last_name) {
  336. inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})`
  337. } else {
  338. inviterName = inviter.email
  339. }
  340. return inviterName
  341. }
  342. export default {
  343. getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']),
  344. createInvite: callbackify(createInvite),
  345. importInvite: callbackify(importInvite),
  346. acceptInvite: callbackify(acceptInvite),
  347. revokeInvite: callbackify(revokeInvite),
  348. createTeamInvitesForLegacyInvitedEmail: callbackify(
  349. createTeamInvitesForLegacyInvitedEmail
  350. ),
  351. promises: {
  352. getInvite,
  353. createInvite,
  354. importInvite,
  355. acceptInvite,
  356. revokeInvite,
  357. createTeamInvitesForLegacyInvitedEmail,
  358. },
  359. }