TeamInvitesController.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. import settings from '@overleaf/settings'
  2. import logger from '@overleaf/logger'
  3. import OError from '@overleaf/o-error'
  4. import TeamInvitesHandler from './TeamInvitesHandler.js'
  5. import SessionManager from '../Authentication/SessionManager.js'
  6. import SubscriptionLocator from './SubscriptionLocator.js'
  7. import ErrorController from '../Errors/ErrorController.js'
  8. import EmailHelper from '../Helpers/EmailHelper.js'
  9. import UserGetter from '../User/UserGetter.js'
  10. import { expressify } from '@overleaf/promise-utils'
  11. import HttpErrorHandler from '../Errors/HttpErrorHandler.js'
  12. import PermissionsManager from '../Authorization/PermissionsManager.js'
  13. import EmailHandler from '../Email/EmailHandler.js'
  14. import { RateLimiter } from '../../infrastructure/RateLimiter.js'
  15. import Modules from '../../infrastructure/Modules.js'
  16. import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
  17. import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
  18. const rateLimiters = {
  19. resendGroupInvite: new RateLimiter('resend-group-invite', {
  20. points: 1,
  21. duration: 60 * 60,
  22. }),
  23. }
  24. async function createInvite(req, res, next) {
  25. const teamManagerId = SessionManager.getLoggedInUserId(req.session)
  26. const subscription = req.entity
  27. const email = EmailHelper.parseEmail(req.body.email)
  28. if (!email) {
  29. return res.status(422).json({
  30. error: {
  31. code: 'invalid_email',
  32. message: req.i18n.translate('invalid_email'),
  33. },
  34. })
  35. }
  36. try {
  37. const invitedUserData = await TeamInvitesHandler.promises.createInvite(
  38. teamManagerId,
  39. subscription,
  40. email
  41. )
  42. return res.json({ user: invitedUserData })
  43. } catch (err) {
  44. if (err.alreadyInTeam) {
  45. return res.status(400).json({
  46. error: {
  47. code: 'user_already_added',
  48. message: req.i18n.translate('user_already_added'),
  49. },
  50. })
  51. }
  52. if (err.limitReached) {
  53. return res.status(400).json({
  54. error: {
  55. code: 'group_full',
  56. message: req.i18n.translate('group_full'),
  57. },
  58. })
  59. }
  60. }
  61. }
  62. async function viewInvite(req, res, next) {
  63. const { token } = req.params
  64. const userId = SessionManager.getLoggedInUserId(req.session)
  65. const { invite, subscription } =
  66. await TeamInvitesHandler.promises.getInvite(token)
  67. await SplitTestHandler.promises.getAssignment(
  68. req,
  69. res,
  70. 'bootstrap-5-subscription'
  71. )
  72. if (!invite) {
  73. return ErrorController.notFound(req, res)
  74. }
  75. let validationStatus = new Map()
  76. if (userId) {
  77. const personalSubscription =
  78. await SubscriptionLocator.promises.getUsersSubscription(userId)
  79. const hasIndividualRecurlySubscription =
  80. personalSubscription &&
  81. personalSubscription.groupPlan === false &&
  82. personalSubscription.recurlyStatus?.state !== 'canceled' &&
  83. personalSubscription.recurlySubscription_id &&
  84. personalSubscription.recurlySubscription_id !== ''
  85. const groupSSOActive = (
  86. await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
  87. )?.[0]
  88. if (subscription?.groupPolicy) {
  89. if (!subscription.populated('groupPolicy')) {
  90. // eslint-disable-next-line no-restricted-syntax
  91. await subscription.populate('groupPolicy')
  92. }
  93. const user = await UserGetter.promises.getUser(userId)
  94. const isUserEnrolledInDifferentGroup =
  95. (
  96. await Modules.promises.hooks.fire(
  97. 'isUserEnrolledInDifferentGroup',
  98. user.enrollment,
  99. subscription._id
  100. )
  101. )?.[0] === true
  102. if (isUserEnrolledInDifferentGroup) {
  103. return HttpErrorHandler.forbidden(
  104. req,
  105. res,
  106. 'User is already enrolled in a different subscription'
  107. )
  108. }
  109. validationStatus =
  110. await PermissionsManager.promises.getUserValidationStatus({
  111. user,
  112. groupPolicy: subscription.groupPolicy,
  113. subscription,
  114. })
  115. let currentManagedUserAdminEmail
  116. try {
  117. currentManagedUserAdminEmail =
  118. await SubscriptionLocator.promises.getAdminEmail(subscription._id)
  119. } catch (err) {
  120. logger.error({ err }, 'error getting subscription admin email')
  121. }
  122. return res.render('subscriptions/team/invite-managed', {
  123. inviterName: invite.inviterName,
  124. inviteToken: invite.token,
  125. expired: req.query.expired,
  126. validationStatus: Object.fromEntries(validationStatus),
  127. currentManagedUserAdminEmail,
  128. groupSSOActive,
  129. subscriptionId: subscription._id.toString(),
  130. })
  131. } else {
  132. let currentManagedUserAdminEmail
  133. try {
  134. currentManagedUserAdminEmail =
  135. await SubscriptionLocator.promises.getAdminEmail(req.managedBy)
  136. } catch (err) {
  137. logger.error({ err }, 'error getting subscription admin email')
  138. }
  139. return res.render('subscriptions/team/invite', {
  140. inviterName: invite.inviterName,
  141. inviteToken: invite.token,
  142. hasIndividualRecurlySubscription,
  143. expired: req.query.expired,
  144. userRestrictions: Array.from(req.userRestrictions || []),
  145. currentManagedUserAdminEmail,
  146. groupSSOActive,
  147. subscriptionId: subscription._id.toString(),
  148. })
  149. }
  150. } else {
  151. const userByEmail = await UserGetter.promises.getUserByMainEmail(
  152. invite.email
  153. )
  154. return res.render('subscriptions/team/invite_logged_out', {
  155. inviterName: invite.inviterName,
  156. inviteToken: invite.token,
  157. appName: settings.appName,
  158. accountExists: userByEmail != null,
  159. emailAddress: invite.email,
  160. })
  161. }
  162. }
  163. async function viewInvites(req, res, next) {
  164. const userId = SessionManager.getLoggedInUserId(req.session)
  165. const userEmail = await UserGetter.promises.getUserEmail(userId)
  166. const groupSubscriptions =
  167. await SubscriptionLocator.promises.getGroupsWithTeamInvitesEmail(userEmail)
  168. const teamInvites = groupSubscriptions.map(groupSubscription =>
  169. groupSubscription.teamInvites.find(invite => invite.email === userEmail)
  170. )
  171. await SplitTestHandler.promises.getAssignment(
  172. req,
  173. res,
  174. 'bootstrap-5-subscription'
  175. )
  176. return res.render('subscriptions/team/group-invites', {
  177. teamInvites,
  178. })
  179. }
  180. async function acceptInvite(req, res, next) {
  181. const { token } = req.params
  182. const userId = SessionManager.getLoggedInUserId(req.session)
  183. const subscription = await TeamInvitesHandler.promises.acceptInvite(
  184. token,
  185. userId
  186. )
  187. const groupSSOActive = (
  188. await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
  189. )?.[0]
  190. try {
  191. await UserAuditLogHandler.promises.addEntry(
  192. userId,
  193. 'accept-group-invitation',
  194. userId,
  195. req.ip,
  196. { subscriptionId: subscription._id }
  197. )
  198. } catch (e) {
  199. logger.error(
  200. { err: e, userId, subscriptionId: subscription._id },
  201. 'error adding audit log entry'
  202. )
  203. }
  204. res.json({ groupSSOActive })
  205. }
  206. function revokeInvite(req, res, next) {
  207. const subscription = req.entity
  208. const email = EmailHelper.parseEmail(req.params.email)
  209. const teamManagerId = SessionManager.getLoggedInUserId(req.session)
  210. if (!email) {
  211. return res.sendStatus(400)
  212. }
  213. TeamInvitesHandler.revokeInvite(
  214. teamManagerId,
  215. subscription,
  216. email,
  217. function (err, results) {
  218. if (err) {
  219. return next(err)
  220. }
  221. res.sendStatus(204)
  222. }
  223. )
  224. }
  225. async function resendInvite(req, res, next) {
  226. const { entity: subscription } = req
  227. const userEmail = EmailHelper.parseEmail(req.body.email)
  228. await subscription.populate('admin_id', ['email', 'first_name', 'last_name'])
  229. if (!userEmail) {
  230. throw new Error('invalid email')
  231. }
  232. const currentInvite = subscription.teamInvites.find(
  233. invite => invite?.email === userEmail
  234. )
  235. if (!currentInvite) {
  236. return await createInvite(req, res)
  237. }
  238. const opts = {
  239. to: userEmail,
  240. admin: subscription.admin_id,
  241. inviter: currentInvite.inviterName,
  242. acceptInviteUrl: `${settings.siteUrl}/subscription/invites/${currentInvite.token}/`,
  243. reminder: true,
  244. }
  245. try {
  246. await rateLimiters.resendGroupInvite.consume(userEmail, 1, {
  247. method: 'email',
  248. })
  249. const existingUser = await UserGetter.promises.getUserByAnyEmail(userEmail)
  250. let emailTemplate
  251. if (subscription.managedUsersEnabled) {
  252. if (existingUser) {
  253. emailTemplate = 'verifyEmailToJoinManagedUsers'
  254. } else {
  255. emailTemplate = 'inviteNewUserToJoinManagedUsers'
  256. }
  257. } else {
  258. emailTemplate = 'verifyEmailToJoinTeam'
  259. }
  260. EmailHandler.sendDeferredEmail(emailTemplate, opts)
  261. } catch (err) {
  262. if (err?.remainingPoints === 0) {
  263. return res.sendStatus(429)
  264. } else {
  265. throw OError.tag(err, 'Failed to resend group invite email')
  266. }
  267. }
  268. return res.status(200).json({ success: true })
  269. }
  270. export default {
  271. createInvite: expressify(createInvite),
  272. viewInvite: expressify(viewInvite),
  273. viewInvites: expressify(viewInvites),
  274. acceptInvite: expressify(acceptInvite),
  275. revokeInvite,
  276. resendInvite: expressify(resendInvite),
  277. }