TeamInvitesController.mjs 9.0 KB

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