LimitationsManager.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. // @ts-check
  2. const logger = require('@overleaf/logger')
  3. const ProjectGetter = require('../Project/ProjectGetter')
  4. const UserGetter = require('../User/UserGetter')
  5. const SubscriptionLocator = require('./SubscriptionLocator')
  6. const Settings = require('@overleaf/settings')
  7. const CollaboratorsGetter = require('../Collaborators/CollaboratorsGetter')
  8. const CollaboratorsInvitesGetter = require('../Collaborators/CollaboratorsInviteGetter')
  9. const PrivilegeLevels = require('../Authorization/PrivilegeLevels')
  10. const {
  11. callbackify,
  12. callbackifyMultiResult,
  13. } = require('@overleaf/promise-utils')
  14. async function allowedNumberOfCollaboratorsInProject(projectId) {
  15. const project = await ProjectGetter.promises.getProject(projectId, {
  16. owner_ref: true,
  17. })
  18. return await allowedNumberOfCollaboratorsForUser(project.owner_ref)
  19. }
  20. async function allowedNumberOfCollaboratorsForUser(userId) {
  21. const user = await UserGetter.promises.getUser(userId, { features: 1 })
  22. if (user.features && user.features.collaborators) {
  23. return user.features.collaborators
  24. } else {
  25. return Settings.defaultFeatures.collaborators
  26. }
  27. }
  28. async function canAcceptEditCollaboratorInvite(projectId) {
  29. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  30. if (allowedNumber < 0) {
  31. return true // -1 means unlimited
  32. }
  33. const currentEditors =
  34. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  35. projectId
  36. )
  37. return currentEditors + 1 <= allowedNumber
  38. }
  39. async function canAddXEditCollaborators(
  40. projectId,
  41. numberOfNewEditCollaborators
  42. ) {
  43. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  44. if (allowedNumber < 0) {
  45. return true // -1 means unlimited
  46. }
  47. const currentEditors =
  48. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  49. projectId
  50. )
  51. const editInviteCount =
  52. await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
  53. return (
  54. currentEditors + editInviteCount + numberOfNewEditCollaborators <=
  55. allowedNumber
  56. )
  57. }
  58. /**
  59. * Check whether a collaborator can be switched to the given privilege level
  60. *
  61. * @param {string} projectId
  62. * @param {string} userId
  63. * @param {'readOnly' | 'review' | 'readAndWrite'} privilegeLevel
  64. * @return {Promise<boolean>}
  65. */
  66. async function canChangeCollaboratorPrivilegeLevel(
  67. projectId,
  68. userId,
  69. privilegeLevel
  70. ) {
  71. if (privilegeLevel === PrivilegeLevels.READ_ONLY) {
  72. return true
  73. }
  74. const currentPrivilegeLevel =
  75. await CollaboratorsGetter.promises.getMemberIdPrivilegeLevel(
  76. userId,
  77. projectId
  78. )
  79. if (
  80. [PrivilegeLevels.READ_AND_WRITE, PrivilegeLevels.REVIEW].includes(
  81. currentPrivilegeLevel
  82. )
  83. ) {
  84. // Current collaborator already takes a slot, so changing the privilege
  85. // level won't increase the collaborator count
  86. return true
  87. }
  88. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  89. if (allowedNumber < 0) {
  90. // -1 means unlimited
  91. return true
  92. }
  93. const slotsTaken =
  94. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  95. projectId
  96. )
  97. const inviteCount =
  98. await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
  99. return slotsTaken + inviteCount < allowedNumber
  100. }
  101. async function hasPaidSubscription(user) {
  102. const { hasSubscription, subscription } = await userHasSubscription(user)
  103. const { isMember } = await userIsMemberOfGroupSubscription(user)
  104. return {
  105. hasPaidSubscription: hasSubscription || isMember,
  106. subscription,
  107. }
  108. }
  109. // alias for backward-compatibility with modules. Use `haspaidsubscription` instead
  110. async function userHasSubscriptionOrIsGroupMember(user) {
  111. return await hasPaidSubscription(user)
  112. }
  113. async function userHasSubscription(user) {
  114. const subscription = await SubscriptionLocator.promises.getUsersSubscription(
  115. user._id
  116. )
  117. let hasValidSubscription = false
  118. if (subscription) {
  119. if (subscription.recurlySubscription_id || subscription.customAccount) {
  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. module.exports = LimitationsManager