Эх сурвалжийг харах

Merge pull request #15539 from overleaf/ab-schedule-sso-reminder

[web] Schedule Group SSO account linking reminder after joining the group

GitOrigin-RevId: 5586787fbd268446e441762fd7b4846821f849f6
Alexandre Bourdin 2 жил өмнө
parent
commit
2783e89bc3

+ 5 - 5
services/web/app/src/Features/Email/EmailBuilder.js

@@ -475,7 +475,7 @@ templates.inviteNewUserToJoinManagedUsers = ctaTemplate({
   },
   },
 })
 })
 
 
-templates.managedUsersEnabledSSO = ctaTemplate({
+templates.groupSSOLinkingInvite = ctaTemplate({
   subject(opts) {
   subject(opts) {
     const subjectPrefix = opts.reminder ? 'Reminder: ' : 'Action required: '
     const subjectPrefix = opts.reminder ? 'Reminder: ' : 'Action required: '
     return `${subjectPrefix}Authenticate your Overleaf account`
     return `${subjectPrefix}Authenticate your Overleaf account`
@@ -493,10 +493,10 @@ templates.managedUsersEnabledSSO = ctaTemplate({
       </br>
       </br>
       <div>
       <div>
         <strong>What does this mean for you?</strong>
         <strong>What does this mean for you?</strong>
-      </div> 
+      </div>
       </br>
       </br>
       <div>
       <div>
-        You won't need to remember a separate email address and password to sign in to Overleaf. 
+        You won't need to remember a separate email address and password to sign in to Overleaf.
         All you need to do is authenticate your existing Overleaf account with your SSO provider.
         All you need to do is authenticate your existing Overleaf account with your SSO provider.
       </div>
       </div>
       `,
       `,
@@ -516,7 +516,7 @@ templates.managedUsersEnabledSSO = ctaTemplate({
   },
   },
 })
 })
 
 
-templates.managedUsersDisabledSSO = ctaTemplate({
+templates.groupSSODisabled = ctaTemplate({
   subject(opts) {
   subject(opts) {
     return `Action required: Set your Overleaf password`
     return `Action required: Set your Overleaf password`
   },
   },
@@ -532,7 +532,7 @@ templates.managedUsersDisabledSSO = ctaTemplate({
       </br>
       </br>
       <div>
       <div>
         <strong>What does this mean for you?</strong>
         <strong>What does this mean for you?</strong>
-      </div> 
+      </div>
       </br>
       </br>
       <div>
       <div>
         You now need an email address and password to sign in to your Overleaf account.
         You now need an email address and password to sign in to your Overleaf account.

+ 12 - 0
services/web/app/src/Features/Subscription/TeamInvitesHandler.js

@@ -5,6 +5,7 @@ const settings = require('@overleaf/settings')
 const { ObjectId } = require('mongodb')
 const { ObjectId } = require('mongodb')
 
 
 const { Subscription } = require('../../models/Subscription')
 const { Subscription } = require('../../models/Subscription')
+const { SSOConfig } = require('../../models/SSOConfig')
 
 
 const UserGetter = require('../User/UserGetter')
 const UserGetter = require('../User/UserGetter')
 const SubscriptionLocator = require('./SubscriptionLocator')
 const SubscriptionLocator = require('./SubscriptionLocator')
@@ -21,6 +22,7 @@ const {
   callbackifyMultiResult,
   callbackifyMultiResult,
 } = require('@overleaf/promise-utils')
 } = require('@overleaf/promise-utils')
 const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
 const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
+const Modules = require('../../infrastructure/Modules')
 
 
 async function getInvite(token) {
 async function getInvite(token) {
   const subscription = await Subscription.findOne({
   const subscription = await Subscription.findOne({
@@ -77,6 +79,16 @@ async function acceptInvite(token, userId) {
       subscription
       subscription
     )
     )
   }
   }
+  if (subscription.ssoConfig) {
+    const ssoConfig = await SSOConfig.findById(subscription.ssoConfig)
+    if (ssoConfig?.enabled) {
+      await Modules.promises.hooks.fire(
+        'scheduleGroupSSOReminder',
+        userId,
+        subscription._id
+      )
+    }
+  }
 
 
   await _removeInviteFromTeam(subscription.id, invite.email)
   await _removeInviteFromTeam(subscription.id, invite.email)
 
 

+ 21 - 0
services/web/app/src/infrastructure/QueueWorkers.js

@@ -11,6 +11,7 @@ const {
 const EmailHandler = require('../Features/Email/EmailHandler')
 const EmailHandler = require('../Features/Email/EmailHandler')
 const logger = require('@overleaf/logger')
 const logger = require('@overleaf/logger')
 const OError = require('@overleaf/o-error')
 const OError = require('@overleaf/o-error')
+const Modules = require('./Modules')
 
 
 function start() {
 function start() {
   if (!Features.hasFeature('saas')) {
   if (!Features.hasFeature('saas')) {
@@ -79,6 +80,26 @@ function start() {
     }
     }
   })
   })
   registerCleanup(confirmInstitutionDomainQueue)
   registerCleanup(confirmInstitutionDomainQueue)
+
+  const groupSSOReminderQueue = Queues.getQueue('group-sso-reminder')
+  groupSSOReminderQueue.process(async job => {
+    const { userId, subscriptionId } = job.data
+    try {
+      await Modules.promises.hooks.fire(
+        'sendGroupSSOReminder',
+        userId,
+        subscriptionId
+      )
+    } catch (e) {
+      const error = OError.tag(
+        e,
+        'failed to send scheduled Group SSO account linking reminder'
+      )
+      logger.warn(error)
+      throw error
+    }
+  })
+  registerCleanup(groupSSOReminderQueue)
 }
 }
 
 
 function registerCleanup(queue) {
 function registerCleanup(queue) {

+ 4 - 0
services/web/app/src/infrastructure/Queues.js

@@ -37,6 +37,10 @@ const QUEUES_JOB_OPTIONS = {
     removeOnFail: MAX_FAILED_JOBS_RETAINED,
     removeOnFail: MAX_FAILED_JOBS_RETAINED,
     attempts: 3,
     attempts: 3,
   },
   },
+  'group-sso-reminder': {
+    removeOnFail: MAX_FAILED_JOBS_RETAINED,
+    attempts: 3,
+  },
 }
 }
 
 
 const QUEUE_OPTIONS = {
 const QUEUE_OPTIONS = {

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

@@ -51,11 +51,11 @@ export default function DropdownButton({
     isLoading: isResendingGroupInvite,
     isLoading: isResendingGroupInvite,
   } = useAsync<resendInviteResponse>()
   } = useAsync<resendInviteResponse>()
 
 
-  const userNotManaged =
-    !user.isEntityAdmin && !user.invite && !user.enrollment?.managedBy
-
   const userPending = user.invite
   const userPending = user.invite
 
 
+  const userNotManaged =
+    !user.isEntityAdmin && !userPending && !user.enrollment?.managedBy
+
   const handleResendManagedUserInvite = useCallback(
   const handleResendManagedUserInvite = useCallback(
     async user => {
     async user => {
       try {
       try {
@@ -219,7 +219,7 @@ export default function DropdownButton({
               ) : null}
               ) : null}
             </MenuItemButton>
             </MenuItemButton>
           ) : null}
           ) : null}
-          {ssoEnabledButNotAccepted && (
+          {!userPending && ssoEnabledButNotAccepted && (
             <MenuItemButton
             <MenuItemButton
               onClick={onResendSSOLinkInviteClick}
               onClick={onResendSSOLinkInviteClick}
               data-testid="resend-sso-link-invite-action"
               data-testid="resend-sso-link-invite-action"

+ 75 - 24
services/web/test/unit/src/Subscription/TeamInvitesHandlerTests.js

@@ -66,6 +66,10 @@ describe('TeamInvitesHandler', function () {
       updateOne: sinon.stub().resolves(),
       updateOne: sinon.stub().resolves(),
     }
     }
 
 
+    this.SSOConfig = {
+      findById: sinon.stub().resolves(),
+    }
+
     this.EmailHandler = {
     this.EmailHandler = {
       promises: {
       promises: {
         sendEmail: sinon.stub().resolves(null),
         sendEmail: sinon.stub().resolves(null),
@@ -118,6 +122,7 @@ describe('TeamInvitesHandler', function () {
         '@overleaf/settings': { siteUrl: 'http://example.com' },
         '@overleaf/settings': { siteUrl: 'http://example.com' },
         '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
         '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
         '../../models/Subscription': { Subscription: this.Subscription },
         '../../models/Subscription': { Subscription: this.Subscription },
+        '../../models/SSOConfig': { SSOConfig: this.SSOConfig },
         '../User/UserGetter': this.UserGetter,
         '../User/UserGetter': this.UserGetter,
         './SubscriptionLocator': this.SubscriptionLocator,
         './SubscriptionLocator': this.SubscriptionLocator,
         './SubscriptionUpdater': this.SubscriptionUpdater,
         './SubscriptionUpdater': this.SubscriptionUpdater,
@@ -125,6 +130,9 @@ describe('TeamInvitesHandler', function () {
         '../Email/EmailHandler': this.EmailHandler,
         '../Email/EmailHandler': this.EmailHandler,
         './ManagedUsersHandler': this.ManagedUsersHandler,
         './ManagedUsersHandler': this.ManagedUsersHandler,
         '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
         '../Notifications/NotificationsBuilder': this.NotificationsBuilder,
+        '../../infrastructure/Modules': (this.Modules = {
+          promises: { hooks: { fire: sinon.stub().resolves() } },
+        }),
       },
       },
     })
     })
   })
   })
@@ -334,39 +342,82 @@ describe('TeamInvitesHandler', function () {
       })
       })
     })
     })
 
 
-    it('adds the user to the team', function (done) {
-      this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
-        this.SubscriptionUpdater.promises.addUserToGroup
-          .calledWith(this.subscription._id, this.user.id)
-          .should.eq(true)
-        done()
+    describe('with standard group', function () {
+      it('adds the user to the team', function (done) {
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          this.SubscriptionUpdater.promises.addUserToGroup
+            .calledWith(this.subscription._id, this.user.id)
+            .should.eq(true)
+          done()
+        })
+      })
+
+      it('removes the invite from the subscription', function (done) {
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          this.Subscription.updateOne
+            .calledWith(
+              { _id: new ObjectId('55153a8014829a865bbf700d') },
+              { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
+            )
+            .should.eq(true)
+          done()
+        })
+      })
+
+      it('removes dashboard notification after they accepted group invitation', function (done) {
+        const managedUsersEnabled = false
+
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          sinon.assert.called(
+            this.NotificationsBuilder.promises.groupInvitation(
+              this.user.id,
+              this.subscription._id,
+              managedUsersEnabled
+            ).read
+          )
+          done()
+        })
+      })
+
+      it('should not schedule an SSO invite reminder', function (done) {
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          sinon.assert.notCalled(this.Modules.promises.hooks.fire)
+          done()
+        })
       })
       })
     })
     })
 
 
-    it('removes the invite from the subscription', function (done) {
-      this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
-        this.Subscription.updateOne
-          .calledWith(
-            { _id: new ObjectId('55153a8014829a865bbf700d') },
-            { $pull: { teamInvites: { email: 'john.snow@example.com' } } }
+    describe('with managed group', function () {
+      it('should enroll the group member', function (done) {
+        this.subscription.managedUsersEnabled = true
+
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          sinon.assert.calledWith(
+            this.ManagedUsersHandler.promises.enrollInSubscription,
+            this.user.id,
+            this.subscription
           )
           )
-          .should.eq(true)
-        done()
+          done()
+        })
       })
       })
     })
     })
 
 
-    it('removes dashboard notification after they accepted group invitation', function (done) {
-      const managedUsersEnabled = false
+    describe('with group SSO enabled', function () {
+      it('should schedule an SSO invite reminder', function (done) {
+        this.subscription.ssoConfig = 'ssoconfig1'
+        this.SSOConfig.findById
+          .withArgs('ssoconfig1')
+          .resolves({ enabled: true })
 
 
-      this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
-        sinon.assert.called(
-          this.NotificationsBuilder.promises.groupInvitation(
+        this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
+          sinon.assert.calledWith(
+            this.Modules.promises.hooks.fire,
+            'scheduleGroupSSOReminder',
             this.user.id,
             this.user.id,
-            this.subscription._id,
-            managedUsersEnabled
-          ).read
-        )
-        done()
+            this.subscription._id
+          )
+          done()
+        })
       })
       })
     })
     })
   })
   })