LimitationsManager.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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. currentPrivilegeLevel === PrivilegeLevels.READ_AND_WRITE ||
  81. currentPrivilegeLevel === PrivilegeLevels.REVIEW
  82. ) {
  83. // Current collaborator already takes a slot, so changing the privilege
  84. // level won't increase the collaborator count
  85. return true
  86. }
  87. const allowedNumber = await allowedNumberOfCollaboratorsInProject(projectId)
  88. if (allowedNumber < 0) {
  89. // -1 means unlimited
  90. return true
  91. }
  92. const slotsTaken =
  93. await CollaboratorsGetter.promises.getInvitedEditCollaboratorCount(
  94. projectId
  95. )
  96. const inviteCount =
  97. await CollaboratorsInvitesGetter.promises.getEditInviteCount(projectId)
  98. return slotsTaken + inviteCount < allowedNumber
  99. }
  100. async function hasPaidSubscription(user) {
  101. const { hasSubscription, subscription } = await userHasSubscription(user)
  102. const { isMember } = await userIsMemberOfGroupSubscription(user)
  103. return {
  104. hasPaidSubscription: hasSubscription || isMember,
  105. subscription,
  106. }
  107. }
  108. // alias for backward-compatibility with modules. Use `haspaidsubscription` instead
  109. async function userHasSubscriptionOrIsGroupMember(user) {
  110. return await hasPaidSubscription(user)
  111. }
  112. async function userHasSubscription(user) {
  113. const subscription = await SubscriptionLocator.promises.getUsersSubscription(
  114. user._id
  115. )
  116. let hasValidSubscription = false
  117. if (subscription) {
  118. if (subscription.recurlySubscription_id || subscription.customAccount) {
  119. hasValidSubscription = true
  120. }
  121. }
  122. return {
  123. hasSubscription: hasValidSubscription,
  124. subscription,
  125. }
  126. }
  127. async function userIsMemberOfGroupSubscription(user) {
  128. const subscriptions =
  129. (await SubscriptionLocator.promises.getMemberSubscriptions(user._id)) || []
  130. return { isMember: subscriptions.length > 0, subscriptions }
  131. }
  132. function teamHasReachedMemberLimit(subscription) {
  133. const currentTotal =
  134. (subscription.member_ids || []).length +
  135. (subscription.teamInvites || []).length +
  136. (subscription.invited_emails || []).length
  137. return currentTotal >= subscription.membersLimit
  138. }
  139. async function hasGroupMembersLimitReached(subscriptionId, callback) {
  140. const subscription =
  141. await SubscriptionLocator.promises.getSubscription(subscriptionId)
  142. if (!subscription) {
  143. logger.warn({ subscriptionId }, 'no subscription found')
  144. throw new Error('no subscription found')
  145. }
  146. const limitReached = teamHasReachedMemberLimit(subscription)
  147. return { limitReached, subscription }
  148. }
  149. const LimitationsManager = {
  150. allowedNumberOfCollaboratorsInProject: callbackify(
  151. allowedNumberOfCollaboratorsInProject
  152. ),
  153. allowedNumberOfCollaboratorsForUser: callbackify(
  154. allowedNumberOfCollaboratorsForUser
  155. ),
  156. canAddXEditCollaborators: callbackify(canAddXEditCollaborators),
  157. canChangeCollaboratorPrivilegeLevel: callbackify(
  158. canChangeCollaboratorPrivilegeLevel
  159. ),
  160. hasPaidSubscription: callbackifyMultiResult(hasPaidSubscription, [
  161. 'hasPaidSubscription',
  162. 'subscription',
  163. ]),
  164. userHasSubscriptionOrIsGroupMember: callbackifyMultiResult(
  165. userHasSubscriptionOrIsGroupMember,
  166. ['hasPaidSubscription', 'subscription']
  167. ),
  168. userHasSubscription: callbackifyMultiResult(userHasSubscription, [
  169. 'hasSubscription',
  170. 'subscription',
  171. ]),
  172. userIsMemberOfGroupSubscription: callbackifyMultiResult(
  173. userIsMemberOfGroupSubscription,
  174. ['isMember', 'subscriptions']
  175. ),
  176. hasGroupMembersLimitReached: callbackifyMultiResult(
  177. hasGroupMembersLimitReached,
  178. ['limitReached', 'subscription']
  179. ),
  180. teamHasReachedMemberLimit,
  181. promises: {
  182. allowedNumberOfCollaboratorsInProject,
  183. allowedNumberOfCollaboratorsForUser,
  184. canAcceptEditCollaboratorInvite,
  185. canAddXEditCollaborators,
  186. canChangeCollaboratorPrivilegeLevel,
  187. hasPaidSubscription,
  188. userHasSubscriptionOrIsGroupMember,
  189. userHasSubscription,
  190. userIsMemberOfGroupSubscription,
  191. hasGroupMembersLimitReached,
  192. },
  193. }
  194. module.exports = LimitationsManager