TeamInvitesController.mjs 8.8 KB

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