TeamInvitesHandler.js 9.5 KB

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