From 5f0ca9660d832b30867fa1acd90bbc9219d70bd9 Mon Sep 17 00:00:00 2001 From: Alexandre Bourdin Date: Mon, 6 Nov 2023 15:26:22 +0100 Subject: [PATCH] Merge pull request #15539 from overleaf/ab-schedule-sso-reminder [web] Schedule Group SSO account linking reminder after joining the group GitOrigin-RevId: 5586787fbd268446e441762fd7b4846821f849f6 --- .../app/src/Features/Email/EmailBuilder.js | 10 +- .../Subscription/TeamInvitesHandler.js | 12 +++ .../app/src/infrastructure/QueueWorkers.js | 21 ++++ services/web/app/src/infrastructure/Queues.js | 4 + .../members-table/dropdown-button.tsx | 8 +- .../Subscription/TeamInvitesHandlerTests.js | 101 +++++++++++++----- 6 files changed, 122 insertions(+), 34 deletions(-) diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js index 6070f03cfa..41f0ba3847 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.js +++ b/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({
What does this mean for you? -
+
- 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.
`, @@ -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({
What does this mean for you? -
+
You now need an email address and password to sign in to your Overleaf account. diff --git a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js b/services/web/app/src/Features/Subscription/TeamInvitesHandler.js index ab6c620ea2..fde4842fa6 100644 --- a/services/web/app/src/Features/Subscription/TeamInvitesHandler.js +++ b/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) diff --git a/services/web/app/src/infrastructure/QueueWorkers.js b/services/web/app/src/infrastructure/QueueWorkers.js index 31921447a6..0c9ed4f1cb 100644 --- a/services/web/app/src/infrastructure/QueueWorkers.js +++ b/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) { diff --git a/services/web/app/src/infrastructure/Queues.js b/services/web/app/src/infrastructure/Queues.js index b6a0a19c44..6610528097 100644 --- a/services/web/app/src/infrastructure/Queues.js +++ b/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 = { diff --git a/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx b/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx index db914040c5..900ff39ff9 100644 --- a/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx +++ b/services/web/frontend/js/features/group-management/components/members-table/dropdown-button.tsx @@ -51,11 +51,11 @@ export default function DropdownButton({ isLoading: isResendingGroupInvite, } = useAsync() - 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} ) : null} - {ssoEnabledButNotAccepted && ( + {!userPending && ssoEnabledButNotAccepted && ( { - 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' } } } + 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 ) - .should.eq(true) - done() + 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 dashboard notification after they accepted group invitation', function (done) { - const managedUsersEnabled = false + 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.called( - this.NotificationsBuilder.promises.groupInvitation( + this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => { + sinon.assert.calledWith( + this.ManagedUsersHandler.promises.enrollInSubscription, this.user.id, - this.subscription._id, - managedUsersEnabled - ).read - ) - done() + this.subscription + ) + done() + }) + }) + }) + + 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.calledWith( + this.Modules.promises.hooks.fire, + 'scheduleGroupSSOReminder', + this.user.id, + this.subscription._id + ) + done() + }) }) }) })