LimitationsManager.mjs 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. // @ts-check
  2. import logger from '@overleaf/logger'
  3. import ProjectGetter from '../Project/ProjectGetter.mjs'
  4. import UserGetter from '../User/UserGetter.mjs'
  5. import SubscriptionLocator from './SubscriptionLocator.mjs'
  6. import Settings from '@overleaf/settings'
  7. import CollaboratorsGetter from '../Collaborators/CollaboratorsGetter.mjs'
  8. import CollaboratorsInvitesGetter from '../Collaborators/CollaboratorsInviteGetter.mjs'
  9. import PrivilegeLevels from '../Authorization/PrivilegeLevels.js'
  10. import { callbackify, callbackifyMultiResult } from '@overleaf/promise-utils'
  11. async function allowedNumberOfCollaboratorsInProject(projectId) {
  12. const project = await ProjectGetter.promises.getProject(projectId, {
  13. owner_ref: true,
  14. })
  15. return await allowedNumberOfCollaboratorsForUser(project.owner_ref)
  16. }
  17. async function allowedNumberOfCollaboratorsForUser(userId) {
  18. const user = await UserGetter.promises.getUser(userId, { features: 1 })
  19. if (user.features && user.features.collaborators) {
  20. return user.features.collaborators
  21. } else {
  22. return Settings.defaultFeatures.collaborators
  23. }
  24. }
  25. async function canAcceptEditCollaboratorInvite(projectId) {
  26. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  27. if (allowedNumber < 0) {
  28. return true // -1 means unlimited
  29. }
  30. const currentEditors =
  31. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  32. projectId
  33. )
  34. return currentEditors + 1 <= allowedNumber
  35. }
  36. async function canAddXEditCollaborators(
  37. projectId,
  38. numberOfNewEditCollaborators
  39. ) {
  40. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  41. if (allowedNumber < 0) {
  42. return true // -1 means unlimited
  43. }
  44. const currentEditors =
  45. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  46. projectId
  47. )
  48. const editInviteCount =
  49. await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
  50. return (
  51. currentEditors + editInviteCount + numberOfNewEditCollaborators <=
  52. allowedNumber
  53. )
  54. }
  55. /**
  56. * Check whether a collaborator can be switched to the given privilege level
  57. *
  58. * @param {string} projectId
  59. * @param {string} userId
  60. * @param {'readOnly' | 'review' | 'readAndWrite'} privilegeLevel
  61. * @return {Promise<boolean>}
  62. */
  63. async function canChangeCollaboratorPrivilegeLevel(
  64. projectId,
  65. userId,
  66. privilegeLevel
  67. ) {
  68. if (privilegeLevel === PrivilegeLevels.READ_ONLY) {
  69. return true
  70. }
  71. const currentPrivilegeLevel =
  72. await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
  73. userId,
  74. projectId
  75. )
  76. if (
  77. currentPrivilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
  78. currentPrivilegeLevel === PrivilegeLevels.REVIEW
  79. ) {
  80. // Current collaborator already takes a slot, so changing the privilege
  81. // level won't increase the collaborator count
  82. return true
  83. }
  84. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  85. if (allowedNumber < 0) {
  86. // -1 means unlimited
  87. return true
  88. }
  89. const slotsTaken =
  90. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  91. projectId
  92. )
  93. const inviteCount =
  94. await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
  95. return slotsTaken + inviteCount < allowedNumber
  96. }
  97. async function hasPaidSubscription(user) {
  98. const { hasSubscription, subscription } = await userHasSubscription(user)
  99. const { isMember } = await userIsMemberOfGroupSubscription(user)
  100. return {
  101. hasPaidSubscription: hasSubscription || isMember,
  102. subscription,
  103. }
  104. }
  105. // alias for backward-compatibility with modules. Use `haspaidsubscription` instead
  106. async function userHasSubscriptionOrIsGroupMember(user) {
  107. return await hasPaidSubscription(user)
  108. }
  109. async function userHasSubscription(user) {
  110. const subscription = await SubscriptionLocator.promises.getUsersSubscription(
  111. user._id
  112. )
  113. let hasValidSubscription = false
  114. if (subscription) {
  115. if (
  116. subscription.recurlySubscription_id ||
  117. subscription.paymentProvider?.subscriptionId ||
  118. subscription.customAccount
  119. ) {
  120. hasValidSubscription = true
  121. }
  122. }
  123. return {
  124. hasSubscription: hasValidSubscription,
  125. subscription,
  126. }
  127. }
  128. async function userIsMemberOfGroupSubscription(user) {
  129. const subscriptions =
  130. (await SubscriptionLocator.promises.getMemberSubscriptions(user._id)) || []
  131. return { isMember: subscriptions.length > 0, subscriptions }
  132. }
  133. function teamHasReachedMemberLimit(subscription) {
  134. const currentTotal =
  135. (subscription.member_ids || []).length +
  136. (subscription.teamInvites || []).length +
  137. (subscription.invited_emails || []).length
  138. return currentTotal >= subscription.membersLimit
  139. }
  140. async function hasGroupMembersLimitReached(subscriptionId, callback) {
  141. const subscription =
  142. await SubscriptionLocator.promises.getSubscription(subscriptionId)
  143. if (!subscription) {
  144. logger.warn({ subscriptionId }, 'no subscription found')
  145. throw new Error('no subscription found')
  146. }
  147. const limitReached = teamHasReachedMemberLimit(subscription)
  148. return { limitReached, subscription }
  149. }
  150. const LimitationsManager = {
  151. allowedNumberOfCollaboratorsInProject: callbackify(
  152. allowedNumberOfCollaboratorsInProject
  153. ),
  154. allowedNumberOfCollaboratorsForUser: callbackify(
  155. allowedNumberOfCollaboratorsForUser
  156. ),
  157. canAddXEditCollaborators: callbackify(canAddXEditCollaborators),
  158. canChangeCollaboratorPrivilegeLevel: callbackify(
  159. canChangeCollaboratorPrivilegeLevel
  160. ),
  161. hasPaidSubscription: callbackifyMultiResult(hasPaidSubscription, [
  162. 'hasPaidSubscription',
  163. 'subscription',
  164. ]),
  165. userHasSubscriptionOrIsGroupMember: callbackifyMultiResult(
  166. userHasSubscriptionOrIsGroupMember,
  167. ['hasPaidSubscription', 'subscription']
  168. ),
  169. userHasSubscription: callbackifyMultiResult(userHasSubscription, [
  170. 'hasSubscription',
  171. 'subscription',
  172. ]),
  173. userIsMemberOfGroupSubscription: callbackifyMultiResult(
  174. userIsMemberOfGroupSubscription,
  175. ['isMember', 'subscriptions']
  176. ),
  177. hasGroupMembersLimitReached: callbackifyMultiResult(
  178. hasGroupMembersLimitReached,
  179. ['limitReached', 'subscription']
  180. ),
  181. teamHasReachedMemberLimit,
  182. promises: {
  183. allowedNumberOfCollaboratorsInProject,
  184. allowedNumberOfCollaboratorsForUser,
  185. canAcceptEditCollaboratorInvite,
  186. canAddXEditCollaborators,
  187. canChangeCollaboratorPrivilegeLevel,
  188. hasPaidSubscription,
  189. userHasSubscriptionOrIsGroupMember,
  190. userHasSubscription,
  191. userIsMemberOfGroupSubscription,
  192. hasGroupMembersLimitReached,
  193. },
  194. }
  195. export default LimitationsManager