Explorar o código

Merge pull request #25983 from overleaf/ii-managed-users-make-unmanaged-roles-access

[web] Prevent managers from removing/deleting themselves

GitOrigin-RevId: 9287dc06bab8024bf03fecff678a4118a9456919
ilkin-overleaf hai 1 ano
pai
achega
cfc6ff0759

+ 39 - 0
services/web/app/src/Features/Subscription/SubscriptionLocator.js

@@ -162,6 +162,45 @@ const SubscriptionLocator = {
         }
       : null
   },
+
+  async getUserSubscriptionStatus(userId) {
+    let usersSubscription = { personal: false, group: false }
+
+    if (!userId) {
+      return usersSubscription
+    }
+
+    const memberSubscriptions =
+      await SubscriptionLocator.getMemberSubscriptions(userId)
+
+    const hasActiveGroupSubscription = memberSubscriptions.some(
+      subscription =>
+        subscription.recurlyStatus?.state === 'active' && subscription.groupPlan
+    )
+    if (hasActiveGroupSubscription) {
+      // Member of a group plan
+      usersSubscription = { ...usersSubscription, group: true }
+    }
+
+    const personalSubscription =
+      await SubscriptionLocator.getUsersSubscription(userId)
+
+    if (personalSubscription) {
+      const hasActivePersonalSubscription =
+        personalSubscription.recurlyStatus?.state === 'active'
+      if (hasActivePersonalSubscription) {
+        if (personalSubscription.groupPlan) {
+          // Owner of a group plan
+          usersSubscription = { ...usersSubscription, group: true }
+        } else {
+          // Owner of an individual plan
+          usersSubscription = { ...usersSubscription, personal: true }
+        }
+      }
+    }
+
+    return usersSubscription
+  },
 }
 
 module.exports = {

+ 4 - 0
services/web/app/src/Features/Subscription/TeamInvitesController.mjs

@@ -132,6 +132,9 @@ async function viewInvite(req, res, next) {
         logger.error({ err }, 'error getting subscription admin email')
       }
 
+      const usersSubscription =
+        await SubscriptionLocator.promises.getUserSubscriptionStatus(userId)
+
       return res.render('subscriptions/team/invite-managed', {
         inviterName: invite.inviterName,
         inviteToken: invite.token,
@@ -141,6 +144,7 @@ async function viewInvite(req, res, next) {
         groupSSOActive,
         subscriptionId: subscription._id.toString(),
         user: sessionUser,
+        usersSubscription,
       })
     } else {
       let currentManagedUserAdminEmail

+ 5 - 1
services/web/app/src/Features/UserMembership/UserMembershipController.mjs

@@ -31,8 +31,11 @@ async function manageGroupMembers(req, res, next) {
   )
   const ssoConfig = await SSOConfig.findById(subscription.ssoConfig).exec()
   const plan = PlansLocator.findLocalPlanInSettings(subscription.planCode)
-  const userId = SessionManager.getLoggedInUserId(req.session)
+  const userId = SessionManager.getLoggedInUserId(req.session)?.toString()
   const isAdmin = subscription.admin_id.toString() === userId
+  const isUserGroupManager =
+    Boolean(subscription.manager_ids?.some(id => id.toString() === userId)) &&
+    !isAdmin
   const recurlySubscription = subscription.recurlySubscription_id
     ? await RecurlyClient.promises.getSubscription(
         subscription.recurlySubscription_id
@@ -51,6 +54,7 @@ async function manageGroupMembers(req, res, next) {
     users,
     groupSize: subscription.membersLimit,
     managedUsersActive: subscription.managedUsersEnabled,
+    isUserGroupManager,
     groupSSOActive: ssoConfig?.enabled,
     canUseFlexibleLicensing: plan?.canUseFlexibleLicensing,
     canUseAddSeatsFeature,

+ 1 - 0
services/web/app/views/subscriptions/team/invite-managed.pug

@@ -13,6 +13,7 @@ block append meta
 	meta(name="ol-groupSSOActive" data-type="boolean" content=groupSSOActive)
 	meta(name="ol-subscriptionId" data-type="string" content=subscriptionId)
 	meta(name="ol-user" data-type="json" content=user)
+	meta(name="ol-usersSubscription" data-type="json" content=usersSubscription)
 
 block content
 	main.content.content-alt.team-invite#invite-managed-root

+ 1 - 0
services/web/app/views/user_membership/group-members-react.pug

@@ -10,6 +10,7 @@ block append meta
 	meta(name="ol-groupName", data-type="string", content=name)
 	meta(name="ol-groupSize", data-type="json", content=groupSize)
 	meta(name="ol-managedUsersActive", data-type="boolean", content=managedUsersActive)
+	meta(name="ol-isUserGroupManager", data-type="boolean", content=isUserGroupManager)
 	meta(name="ol-groupSSOActive", data-type="boolean", content=groupSSOActive)
 	meta(name="ol-canUseFlexibleLicensing", data-type="boolean", content=canUseFlexibleLicensing)
 	meta(name="ol-canUseAddSeatsFeature", data-type="boolean", content=canUseAddSeatsFeature)

+ 3 - 1
services/web/frontend/extracted-translations.json

@@ -201,6 +201,8 @@
   "can_view_content": "",
   "cancel": "",
   "cancel_add_on": "",
+  "cancel_any_existing_subscriptions": "",
+  "cancel_any_existing_subscriptions_and_leave_any_group_subscriptions": "",
   "cancel_anytime": "",
   "cancel_my_account": "",
   "cancel_my_subscription": "",
@@ -1142,7 +1144,7 @@
   "only_group_admin_or_managers_can_delete_your_account_2": "",
   "only_group_admin_or_managers_can_delete_your_account_3": "",
   "only_group_admin_or_managers_can_delete_your_account_4": "",
-  "only_group_admin_or_managers_can_delete_your_account_5": "",
+  "only_group_admin_or_managers_can_delete_your_account_8": "",
   "only_importer_can_refresh": "",
   "open_action_menu": "",
   "open_advanced_reference_search": "",

+ 8 - 3
services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx

@@ -60,7 +60,8 @@ export default function DropdownButton({
 
   const managedUsersActive = getMeta('ol-managedUsersActive')
   const groupSSOActive = getMeta('ol-groupSSOActive')
-
+  const userId = getMeta('ol-user_id')
+  const isUserGroupManager = getMeta('ol-isUserGroupManager')
   const userPending = user.invite
   const isGroupSSOLinked =
     !userPending && user.enrollment?.sso?.some(sso => sso.groupId === groupId)
@@ -238,7 +239,11 @@ export default function DropdownButton({
       </MenuItemButton>
     )
   }
-  if (isUserManaged && !user.isEntityAdmin) {
+  if (
+    isUserManaged &&
+    !user.isEntityAdmin &&
+    (!isUserGroupManager || userId !== user._id)
+  ) {
     buttons.push(
       <MenuItemButton
         key="delete-user-action"
@@ -272,7 +277,7 @@ export default function DropdownButton({
 
   if (buttons.length === 0) {
     buttons.push(
-      <DropdownListItem>
+      <DropdownListItem key="no-actions-available">
         <DropdownItem
           as="button"
           tabIndex={-1}

+ 2 - 0
services/web/frontend/js/utils/meta.ts

@@ -147,6 +147,7 @@ export interface Meta {
   'ol-isRegisteredViaGoogle': boolean
   'ol-isRestrictedTokenMember': boolean
   'ol-isSaas': boolean
+  'ol-isUserGroupManager': boolean
   'ol-itm_campaign': string
   'ol-itm_content': string
   'ol-itm_referrer': string
@@ -268,6 +269,7 @@ export interface Meta {
   'ol-users': ManagedUser[]
   'ol-usersBestSubscription': ProjectDashboardSubscription | undefined
   'ol-usersEmail': string | undefined
+  'ol-usersSubscription': { personal: boolean; group: boolean }
   'ol-validationStatus': ValidationStatus
   'ol-wikiEnabled': boolean
   'ol-writefullCssUrl': string

+ 3 - 1
services/web/locales/en.json

@@ -263,6 +263,8 @@
   "can_view_content": "Can view content",
   "cancel": "Cancel",
   "cancel_add_on": "Cancel add-on",
+  "cancel_any_existing_subscriptions": "Cancel any existing subscriptions. <0>This can be managed from the Subscription page.</0>",
+  "cancel_any_existing_subscriptions_and_leave_any_group_subscriptions": "Cancel any existing subscriptions, and leave any group subscriptions other than the one managing your account. <0>This can be managed from the Subscription page.</0>",
   "cancel_anytime": "We’re confident that you’ll love __appName__, but if not, you can cancel anytime and request your money back, hassle free, within 30 days.",
   "cancel_my_account": "Cancel my subscription",
   "cancel_my_subscription": "Cancel my subscription",
@@ -1498,7 +1500,7 @@
   "only_group_admin_or_managers_can_delete_your_account_2": "Only your group admin or group managers will be able to delete your account.",
   "only_group_admin_or_managers_can_delete_your_account_3": "Your group admin and group managers will be able to reassign ownership of your projects to another group member.",
   "only_group_admin_or_managers_can_delete_your_account_4": "Once you have become a managed user, you cannot change back. <0>Learn more about managed Overleaf accounts.</0>",
-  "only_group_admin_or_managers_can_delete_your_account_5": "For more information, see the \"Managed Accounts\" section in our terms of use, which you agree to by clicking Accept invitation",
+  "only_group_admin_or_managers_can_delete_your_account_8": "We’ll cancel the renewal of your subscription, reach out to Support to request a pro-rata refund. Your individual subscription will be terminated when your account becomes managed.",
   "only_importer_can_refresh": "Only the person who originally imported this __provider__ file can refresh it.",
   "open_action_menu": "Open __name__ action menu",
   "open_advanced_reference_search": "Open advanced reference search",

+ 3 - 0
services/web/test/frontend/features/group-management/components/members-table/dropdown-button.spec.tsx

@@ -175,6 +175,7 @@ describe('DropdownButton', function () {
       beforeEach(function () {
         cy.window().then(win => {
           win.metaAttributesCache.set('ol-users', [user])
+          win.metaAttributesCache.set('ol-isUserGroupManager', true)
         })
         mountDropDownComponent(user, subscriptionId)
       })
@@ -637,6 +638,7 @@ describe('DropdownButton', function () {
       beforeEach(function () {
         cy.window().then(win => {
           win.metaAttributesCache.set('ol-users', [user])
+          win.metaAttributesCache.set('ol-isUserGroupManager', true)
         })
         mountDropDownComponent(user, subscriptionId)
       })
@@ -687,6 +689,7 @@ describe('DropdownButton', function () {
       beforeEach(function () {
         cy.window().then(win => {
           win.metaAttributesCache.set('ol-users', [user])
+          win.metaAttributesCache.set('ol-isUserGroupManager', true)
         })
         mountDropDownComponent(user, subscriptionId)
       })

+ 111 - 0
services/web/test/unit/src/Subscription/SubscriptionLocatorTests.js

@@ -13,6 +13,11 @@ describe('Subscription Locator Tests', function () {
         exec: sinon.stub().resolves(),
       }),
       find: sinon.stub().returns({
+        populate: sinon.stub().returns({
+          populate: sinon.stub().returns({
+            exec: sinon.stub().resolves([]),
+          }),
+        }),
         exec: sinon.stub().resolves(),
       }),
     }
@@ -77,4 +82,110 @@ describe('Subscription Locator Tests', function () {
       subscription.should.equal(this.subscription)
     })
   })
+
+  describe('getUserSubscriptionStatus', function () {
+    it('should return no active personal or group subscription when no user is passed', async function () {
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          undefined
+        )
+      expect(subscriptionStatus).to.deep.equal({
+        personal: false,
+        group: false,
+      })
+    })
+
+    it('should return no active personal or group subscription when the user has no subscription', async function () {
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          this.user._id
+        )
+      expect(subscriptionStatus).to.deep.equal({
+        personal: false,
+        group: false,
+      })
+    })
+
+    it('should return active personal subscription', async function () {
+      this.Subscription.findOne.returns({
+        exec: sinon.stub().resolves({
+          recurlyStatus: {
+            state: 'active',
+          },
+        }),
+      })
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          this.user._id
+        )
+      expect(subscriptionStatus).to.deep.equal({ personal: true, group: false })
+    })
+
+    it('should return active group subscription when member of a group plan', async function () {
+      this.Subscription.find.returns({
+        populate: sinon.stub().returns({
+          populate: sinon.stub().returns({
+            exec: sinon.stub().resolves([
+              {
+                recurlyStatus: {
+                  state: 'active',
+                },
+                groupPlan: true,
+              },
+            ]),
+          }),
+        }),
+      })
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          this.user._id
+        )
+      expect(subscriptionStatus).to.deep.equal({ personal: false, group: true })
+    })
+
+    it('should return active group subscription when owner of a group plan', async function () {
+      this.Subscription.findOne.returns({
+        exec: sinon.stub().resolves({
+          recurlyStatus: {
+            state: 'active',
+          },
+          groupPlan: true,
+        }),
+      })
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          this.user._id
+        )
+      expect(subscriptionStatus).to.deep.equal({ personal: false, group: true })
+    })
+
+    it('should return active personal and group subscription when has personal subscription and member of a group', async function () {
+      this.Subscription.find.returns({
+        populate: sinon.stub().returns({
+          populate: sinon.stub().returns({
+            exec: sinon.stub().resolves([
+              {
+                recurlyStatus: {
+                  state: 'active',
+                },
+                groupPlan: true,
+              },
+            ]),
+          }),
+        }),
+      })
+      this.Subscription.findOne.returns({
+        exec: sinon.stub().resolves({
+          recurlyStatus: {
+            state: 'active',
+          },
+        }),
+      })
+      const subscriptionStatus =
+        await this.SubscriptionLocator.promises.getUserSubscriptionStatus(
+          this.user._id
+        )
+      expect(subscriptionStatus).to.deep.equal({ personal: true, group: true })
+    })
+  })
 })

+ 1 - 0
services/web/test/unit/src/UserMembership/UserMembershipController.test.mjs

@@ -184,6 +184,7 @@ describe('UserMembershipController', function () {
           expect(viewParams.users).to.deep.equal(ctx.users)
           expect(viewParams.groupSize).to.equal(ctx.subscription.membersLimit)
           expect(viewParams.managedUsersActive).to.equal(true)
+          expect(viewParams.isUserGroupManager).to.equal(false)
         },
       })
     })