TeamInvitesHandler.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  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 =
  106. await SubscriptionLocator.promises.getGroupsWithEmailInvite(email)
  107. return Promise.all(
  108. teams.map(team => createInvite(team.admin_id, team, email))
  109. )
  110. }
  111. async function _createInvite(subscription, email, inviter) {
  112. const { possible, reason } = await _checkIfInviteIsPossible(
  113. subscription,
  114. email
  115. )
  116. if (!possible) {
  117. throw reason
  118. }
  119. // don't send invites when inviting self; add user directly to the group
  120. const isInvitingSelf = inviter.emails.some(
  121. emailData => emailData.email === email
  122. )
  123. if (isInvitingSelf) {
  124. await SubscriptionUpdater.promises.addUserToGroup(
  125. subscription._id,
  126. inviter._id
  127. )
  128. // legacy: remove any invite that might have been created in the past
  129. await _removeInviteFromTeam(subscription._id, email)
  130. try {
  131. if (subscription.ssoConfig) {
  132. const ssoConfig = await SSOConfig.findById(
  133. subscription.ssoConfig._id || subscription.ssoConfig
  134. )
  135. if (ssoConfig?.enabled) {
  136. await Modules.promises.hooks.fire(
  137. 'sendGroupSSOReminder',
  138. inviter._id,
  139. subscription._id
  140. )
  141. }
  142. }
  143. } catch (error) {
  144. logger.error(
  145. { err: error, userId: inviter._id, subscriptionId: subscription._id },
  146. 'Failed to schedule Group SSO invite for group admin'
  147. )
  148. }
  149. return {
  150. email: inviter.email,
  151. first_name: inviter.first_name,
  152. last_name: inviter.last_name,
  153. invite: false,
  154. }
  155. }
  156. const inviterName = _getInviterName(inviter)
  157. let invite = subscription.teamInvites.find(invite => invite.email === email)
  158. if (invite) {
  159. invite = invite.toObject()
  160. invite.sentAt = new Date()
  161. } else {
  162. invite = {
  163. email,
  164. inviterName,
  165. token: crypto.randomBytes(32).toString('hex'),
  166. sentAt: new Date(),
  167. }
  168. subscription.teamInvites.push(invite)
  169. }
  170. try {
  171. await _sendNotificationToExistingUser(
  172. subscription,
  173. email,
  174. invite,
  175. subscription.managedUsersEnabled
  176. )
  177. } catch (err) {
  178. logger.error(
  179. { err },
  180. 'Failed to send notification to existing user when creating group invitation'
  181. )
  182. }
  183. await subscription.save()
  184. if (subscription.managedUsersEnabled) {
  185. let admin = {}
  186. try {
  187. admin = await SubscriptionLocator.promises.getAdminEmailAndName(
  188. subscription._id
  189. )
  190. } catch (err) {
  191. logger.error({ err }, 'error getting subscription admin email and name')
  192. }
  193. const user = await UserGetter.promises.getUserByAnyEmail(email)
  194. const opts = {
  195. to: email,
  196. admin,
  197. inviter,
  198. acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
  199. appName: settings.appName,
  200. }
  201. if (user) {
  202. await EmailHandler.promises.sendEmail(
  203. 'verifyEmailToJoinManagedUsers',
  204. opts
  205. )
  206. } else {
  207. await EmailHandler.promises.sendEmail(
  208. 'inviteNewUserToJoinManagedUsers',
  209. opts
  210. )
  211. }
  212. } else {
  213. const opts = {
  214. to: email,
  215. inviter,
  216. acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${invite.token}/`,
  217. appName: settings.appName,
  218. }
  219. await EmailHandler.promises.sendEmail('verifyEmailToJoinTeam', opts)
  220. }
  221. Object.assign(invite, { invite: true })
  222. return invite
  223. }
  224. async function _removeInviteFromTeam(subscriptionId, email, callback) {
  225. const searchConditions = { _id: new ObjectId(subscriptionId.toString()) }
  226. const removeInvite = { $pull: { teamInvites: { email } } }
  227. await Subscription.updateOne(searchConditions, removeInvite)
  228. await _removeLegacyInvite(subscriptionId, email)
  229. }
  230. async function _sendNotificationToExistingUser(
  231. subscription,
  232. email,
  233. invite,
  234. managedUsersEnabled
  235. ) {
  236. const user = await UserGetter.promises.getUserByMainEmail(email)
  237. if (!user) {
  238. return
  239. }
  240. await NotificationsBuilder.promises
  241. .groupInvitation(
  242. user._id.toString(),
  243. subscription._id.toString(),
  244. managedUsersEnabled
  245. )
  246. .create(invite)
  247. }
  248. async function _removeLegacyInvite(subscriptionId, email) {
  249. await Subscription.updateOne(
  250. {
  251. _id: new ObjectId(subscriptionId.toString()),
  252. },
  253. {
  254. $pull: {
  255. invited_emails: email,
  256. },
  257. }
  258. )
  259. }
  260. async function _checkIfInviteIsPossible(subscription, email) {
  261. if (!subscription.groupPlan) {
  262. logger.debug(
  263. { subscriptionId: subscription.id },
  264. 'can not add members to a subscription that is not in a group plan'
  265. )
  266. return { possible: false, reason: { wrongPlan: true } }
  267. }
  268. if (LimitationsManager.teamHasReachedMemberLimit(subscription)) {
  269. logger.debug(
  270. { subscriptionId: subscription.id },
  271. 'team has reached member limit'
  272. )
  273. return { possible: false, reason: { limitReached: true } }
  274. }
  275. const existingUser = await UserGetter.promises.getUserByAnyEmail(email)
  276. if (!existingUser) {
  277. return { possible: true }
  278. }
  279. const existingMember = subscription.member_ids.find(
  280. memberId => memberId.toString() === existingUser._id.toString()
  281. )
  282. if (existingMember) {
  283. logger.debug(
  284. { subscriptionId: subscription.id, email },
  285. 'user already in team'
  286. )
  287. return { possible: false, reason: { alreadyInTeam: true } }
  288. } else {
  289. return { possible: true }
  290. }
  291. }
  292. function _getInviterName(inviter) {
  293. let inviterName
  294. if (inviter.first_name && inviter.last_name) {
  295. inviterName = `${inviter.first_name} ${inviter.last_name} (${inviter.email})`
  296. } else {
  297. inviterName = inviter.email
  298. }
  299. return inviterName
  300. }
  301. module.exports = {
  302. getInvite: callbackifyMultiResult(getInvite, ['invite', 'subscription']),
  303. createInvite: callbackify(createInvite),
  304. importInvite: callbackify(importInvite),
  305. acceptInvite: callbackify(acceptInvite),
  306. revokeInvite: callbackify(revokeInvite),
  307. createTeamInvitesForLegacyInvitedEmail: callbackify(
  308. createTeamInvitesForLegacyInvitedEmail
  309. ),
  310. promises: {
  311. getInvite,
  312. createInvite,
  313. importInvite,
  314. acceptInvite,
  315. revokeInvite,
  316. createTeamInvitesForLegacyInvitedEmail,
  317. },
  318. }