Просмотр исходного кода

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 лет назад
Родитель
Сommit
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) {
     const subjectPrefix = opts.reminder ? 'Reminder: ' : 'Action required: '
     return `${subjectPrefix}Authenticate your Overleaf account`
@@ -493,10 +493,10 @@ templates.managedUsersEnabledSSO = ctaTemplate({
       </br>
       <div>
         <strong>What does this mean for you?</strong>
-      </div> 
+      </div>
       </br>
       <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.
       </div>
       `,
@@ -516,7 +516,7 @@ templates.managedUsersEnabledSSO = ctaTemplate({
   },
 })
 
-templates.managedUsersDisabledSSO = ctaTemplate({
+templates.groupSSODisabled = ctaTemplate({
   subject(opts) {
     return `Action required: Set your Overleaf password`
   },
@@ -532,7 +532,7 @@ templates.managedUsersDisabledSSO = ctaTemplate({
       </br>
       <div>
         <strong>What does this mean for you?</strong>
-      </div> 
+      </div>
       </br>
       <div>
         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 { Subscription } = require('../../models/Subscription')
+const { SSOConfig } = require('../../models/SSOConfig')
 
 const UserGetter = require('../User/UserGetter')
 const SubscriptionLocator = require('./SubscriptionLocator')
@@ -21,6 +22,7 @@ const {
   callbackifyMultiResult,
 } = require('@overleaf/promise-utils')
 const NotificationsBuilder = require('../Notifications/NotificationsBuilder')
+const Modules = require('../../infrastructure/Modules')
 
 async function getInvite(token) {
   const subscription = await Subscription.findOne({
@@ -77,6 +79,16 @@ async function acceptInvite(token, userId) {
       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)
 

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

@@ -11,6 +11,7 @@ const {
 const EmailHandler = require('../Features/Email/EmailHandler')
 const logger = require('@overleaf/logger')
 const OError = require('@overleaf/o-error')
+const Modules = require('./Modules')
 
 function start() {
   if (!Features.hasFeature('saas')) {
@@ -79,6 +80,26 @@ function start() {
     }
   })
   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) {

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

@@ -37,6 +37,10 @@ const QUEUES_JOB_OPTIONS = {
     removeOnFail: MAX_FAILED_JOBS_RETAINED,
     attempts: 3,
   },
+  'group-sso-reminder': {
+    removeOnFail: MAX_FAILED_JOBS_RETAINED,
+    attempts: 3,
+  },
 }
 
 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,
   } = useAsync<resendInviteResponse>()
 
-  const userNotManaged =
-    !user.isEntityAdmin && !user.invite && !user.enrollment?.managedBy
-
   const userPending = user.invite
 
+  const userNotManaged =
+    !user.isEntityAdmin && !userPending && !user.enrollment?.managedBy
+
   const handleResendManagedUserInvite = useCallback(
     async user => {
       try {
@@ -219,7 +219,7 @@ export default function DropdownButton({
               ) : null}
             </MenuItemButton>
           ) : null}
-          {ssoEnabledButNotAccepted && (
+          {!userPending && ssoEnabledButNotAccepted && (
             <MenuItemButton
               onClick={onResendSSOLinkInviteClick}
               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(),
     }
 
+    this.SSOConfig = {
+      findById: sinon.stub().resolves(),
+    }
+
     this.EmailHandler = {
       promises: {
         sendEmail: sinon.stub().resolves(null),
@@ -118,6 +122,7 @@ describe('TeamInvitesHandler', function () {
         '@overleaf/settings': { siteUrl: 'http://example.com' },
         '../../models/TeamInvite': { TeamInvite: (this.TeamInvite = {}) },
         '../../models/Subscription': { Subscription: this.Subscription },
+        '../../models/SSOConfig': { SSOConfig: this.SSOConfig },
         '../User/UserGetter': this.UserGetter,
         './SubscriptionLocator': this.SubscriptionLocator,
         './SubscriptionUpdater': this.SubscriptionUpdater,
@@ -125,6 +130,9 @@ describe('TeamInvitesHandler', function () {
         '../Email/EmailHandler': this.EmailHandler,
         './ManagedUsersHandler': this.ManagedUsersHandler,
         '../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.subscription._id,
-            managedUsersEnabled
-          ).read
-        )
-        done()
+            this.subscription._id
+          )
+          done()
+        })
       })
     })
   })