SubscriptionGroupController.mjs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. // ts-check
  2. import SubscriptionGroupHandler from './SubscriptionGroupHandler.js'
  3. import OError from '@overleaf/o-error'
  4. import logger from '@overleaf/logger'
  5. import SubscriptionLocator from './SubscriptionLocator.js'
  6. import SessionManager from '../Authentication/SessionManager.js'
  7. import UserAuditLogHandler from '../User/UserAuditLogHandler.js'
  8. import { expressify } from '@overleaf/promise-utils'
  9. import Modules from '../../infrastructure/Modules.js'
  10. import SplitTestHandler from '../SplitTests/SplitTestHandler.js'
  11. import ErrorController from '../Errors/ErrorController.js'
  12. import UserGetter from '../User/UserGetter.js'
  13. import { Subscription } from '../../models/Subscription.js'
  14. import { isProfessionalGroupPlan } from './PlansHelper.mjs'
  15. import { MissingBillingInfoError } from './Errors.js'
  16. import RecurlyClient from './RecurlyClient.js'
  17. /**
  18. * @import { Subscription } from "../../../../types/subscription/dashboard/subscription.js"
  19. */
  20. /**
  21. * @param {import("express").Request} req
  22. * @param {import("express").Response} res
  23. * @returns {Promise<void>}
  24. */
  25. async function removeUserFromGroup(req, res) {
  26. const subscription = req.entity
  27. const userToRemoveId = req.params.user_id
  28. const loggedInUserId = SessionManager.getLoggedInUserId(req.session)
  29. const subscriptionId = subscription._id
  30. logger.debug(
  31. { subscriptionId, userToRemoveId },
  32. 'removing user from group subscription'
  33. )
  34. await _removeUserFromGroup(req, res, {
  35. userToRemoveId,
  36. loggedInUserId,
  37. subscription,
  38. })
  39. }
  40. /**
  41. * @param {import("express").Request} req
  42. * @param {import("express").Response} res
  43. * @returns {Promise<void>}
  44. */
  45. async function removeSelfFromGroup(req, res) {
  46. const userToRemoveId = SessionManager.getLoggedInUserId(req.session)
  47. const subscription = await SubscriptionLocator.promises.getSubscription(
  48. req.query.subscriptionId
  49. )
  50. await _removeUserFromGroup(req, res, {
  51. userToRemoveId,
  52. loggedInUserId: userToRemoveId,
  53. subscription,
  54. })
  55. }
  56. /**
  57. * @param {import("express").Request} req
  58. * @param {import("express").Response} res
  59. * @param {string} userToRemoveId
  60. * @param {string} loggedInUserId
  61. * @param {Subscription} subscription
  62. * @returns {Promise<void>}
  63. * @private
  64. */
  65. async function _removeUserFromGroup(
  66. req,
  67. res,
  68. { userToRemoveId, loggedInUserId, subscription }
  69. ) {
  70. const subscriptionId = subscription._id
  71. const groupSSOActive = (
  72. await Modules.promises.hooks.fire('hasGroupSSOEnabled', subscription)
  73. )?.[0]
  74. if (groupSSOActive) {
  75. await Modules.promises.hooks.fire(
  76. 'unlinkUserFromGroupSSO',
  77. userToRemoveId,
  78. subscriptionId
  79. )
  80. }
  81. try {
  82. await UserAuditLogHandler.promises.addEntry(
  83. userToRemoveId,
  84. 'remove-from-group-subscription',
  85. loggedInUserId,
  86. req.ip,
  87. { subscriptionId }
  88. )
  89. } catch (auditLogError) {
  90. throw OError.tag(auditLogError, 'error adding audit log entry', {
  91. userToRemoveId,
  92. subscriptionId,
  93. })
  94. }
  95. try {
  96. await SubscriptionGroupHandler.promises.removeUserFromGroup(
  97. subscriptionId,
  98. userToRemoveId
  99. )
  100. } catch (error) {
  101. logger.err(
  102. { err: error, userToRemoveId, subscriptionId },
  103. 'error removing self from group'
  104. )
  105. return res.sendStatus(500)
  106. }
  107. res.sendStatus(200)
  108. }
  109. /**
  110. * @param {import("express").Request} req
  111. * @param {import("express").Response} res
  112. * @returns {Promise<void>}
  113. */
  114. async function addSeatsToGroupSubscription(req, res) {
  115. try {
  116. const userId = SessionManager.getLoggedInUserId(req.session)
  117. const { subscription, plan } =
  118. await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
  119. userId
  120. )
  121. await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
  122. // Check if the user has missing billing details
  123. await RecurlyClient.promises.getPaymentMethod(userId)
  124. res.render('subscriptions/add-seats', {
  125. subscriptionId: subscription._id,
  126. groupName: subscription.teamName,
  127. totalLicenses: subscription.membersLimit,
  128. isProfessional: isProfessionalGroupPlan(subscription),
  129. })
  130. } catch (error) {
  131. logger.err(
  132. { error },
  133. 'error while getting users group subscription details'
  134. )
  135. if (error instanceof MissingBillingInfoError) {
  136. return res.redirect(
  137. '/user/subscription/group/missing-billing-information'
  138. )
  139. }
  140. return res.redirect('/user/subscription')
  141. }
  142. }
  143. /**
  144. * @param {import("express").Request} req
  145. * @param {import("express").Response} res
  146. * @returns {Promise<void>}
  147. */
  148. async function previewAddSeatsSubscriptionChange(req, res) {
  149. try {
  150. const userId = SessionManager.getLoggedInUserId(req.session)
  151. const preview =
  152. await SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange(
  153. userId,
  154. req.body.adding
  155. )
  156. res.json(preview)
  157. } catch (error) {
  158. logger.err(
  159. { error },
  160. 'error trying to preview "add seats" subscription change'
  161. )
  162. return res.status(400).end()
  163. }
  164. }
  165. /**
  166. * @param {import("express").Request} req
  167. * @param {import("express").Response} res
  168. * @returns {Promise<void>}
  169. */
  170. async function createAddSeatsSubscriptionChange(req, res) {
  171. try {
  172. const userId = SessionManager.getLoggedInUserId(req.session)
  173. const create =
  174. await SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange(
  175. userId,
  176. req.body.adding
  177. )
  178. res.json(create)
  179. } catch (error) {
  180. logger.err(
  181. { error },
  182. 'error trying to create "add seats" subscription change'
  183. )
  184. return res.status(400).end()
  185. }
  186. }
  187. async function submitForm(req, res) {
  188. const userId = SessionManager.getLoggedInUserId(req.session)
  189. const userEmail = await UserGetter.promises.getUserEmail(userId)
  190. const { adding } = req.body
  191. const messageLines = [`\n**Overleaf Sales Contact Form:**`]
  192. messageLines.push('**Subject:** Self-Serve Group User Increase Request')
  193. messageLines.push(`**Estimated Number of Users:** ${adding}`)
  194. messageLines.push(
  195. `**Message:** This email has been generated on behalf of user with email **${userEmail}** ` +
  196. 'to request an increase in the total number of users for their subscription.'
  197. )
  198. const messageFormatted = messageLines.join('\n\n')
  199. const data = {
  200. email: userEmail,
  201. subject: 'Sales Contact Form',
  202. message: messageFormatted,
  203. inbox: 'sales',
  204. }
  205. await Modules.promises.hooks.fire('sendSupportRequest', data)
  206. res.sendStatus(204)
  207. }
  208. async function flexibleLicensingSplitTest(req, res, next) {
  209. const { variant } = await SplitTestHandler.promises.getAssignment(
  210. req,
  211. res,
  212. 'flexible-group-licensing'
  213. )
  214. if (variant !== 'enabled') {
  215. return ErrorController.notFound(req, res)
  216. }
  217. next()
  218. }
  219. async function subscriptionUpgradePage(req, res) {
  220. try {
  221. const userId = SessionManager.getLoggedInUserId(req.session)
  222. const changePreview =
  223. await SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview(userId)
  224. const olSubscription = await Subscription.findOne({
  225. admin_id: userId,
  226. }).exec()
  227. res.render('subscriptions/upgrade-group-subscription-react', {
  228. changePreview,
  229. totalLicenses: olSubscription.membersLimit,
  230. groupName: olSubscription.teamName,
  231. })
  232. } catch (error) {
  233. logger.err({ error }, 'error loading upgrade subscription page')
  234. if (error instanceof MissingBillingInfoError) {
  235. return res.redirect(
  236. '/user/subscription/group/missing-billing-information'
  237. )
  238. }
  239. return res.redirect('/user/subscription')
  240. }
  241. }
  242. async function upgradeSubscription(req, res) {
  243. try {
  244. const userId = SessionManager.getLoggedInUserId(req.session)
  245. await SubscriptionGroupHandler.promises.upgradeGroupPlan(userId)
  246. return res.sendStatus(200)
  247. } catch (error) {
  248. logger.err({ error }, 'error trying to upgrade subscription')
  249. return res.sendStatus(500)
  250. }
  251. }
  252. async function missingBillingInformation(req, res) {
  253. try {
  254. const userId = SessionManager.getLoggedInUserId(req.session)
  255. const subscription =
  256. await SubscriptionLocator.promises.getUsersSubscription(userId)
  257. res.render('subscriptions/missing-billing-information', {
  258. groupName: subscription.teamName,
  259. })
  260. } catch (error) {
  261. logger.err(
  262. { error },
  263. 'error trying to render missing billing information page'
  264. )
  265. return res.render('/user/subscription')
  266. }
  267. }
  268. export default {
  269. removeUserFromGroup: expressify(removeUserFromGroup),
  270. removeSelfFromGroup: expressify(removeSelfFromGroup),
  271. addSeatsToGroupSubscription: expressify(addSeatsToGroupSubscription),
  272. submitForm: expressify(submitForm),
  273. flexibleLicensingSplitTest: expressify(flexibleLicensingSplitTest),
  274. previewAddSeatsSubscriptionChange: expressify(
  275. previewAddSeatsSubscriptionChange
  276. ),
  277. createAddSeatsSubscriptionChange: expressify(
  278. createAddSeatsSubscriptionChange
  279. ),
  280. subscriptionUpgradePage: expressify(subscriptionUpgradePage),
  281. upgradeSubscription: expressify(upgradeSubscription),
  282. missingBillingInformation: expressify(missingBillingInformation),
  283. }