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

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

GitOrigin-RevId: 5586787fbd268446e441762fd7b4846821f849f6
This commit is contained in:
Alexandre Bourdin 2023-11-06 15:26:22 +01:00 committed by Copybot
parent c3afce73c1
commit 2783e89bc3
6 changed files with 122 additions and 34 deletions

View file

@ -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`
@ -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`
}, },

View file

@ -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)

View file

@ -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) {

View file

@ -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 = {

View file

@ -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"

View file

@ -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,6 +342,7 @@ describe('TeamInvitesHandler', function () {
}) })
}) })
describe('with standard group', function () {
it('adds the user to the team', function (done) { it('adds the user to the team', function (done) {
this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => { this.TeamInvitesHandler.acceptInvite('dddddddd', this.user.id, () => {
this.SubscriptionUpdater.promises.addUserToGroup this.SubscriptionUpdater.promises.addUserToGroup
@ -369,6 +378,48 @@ describe('TeamInvitesHandler', function () {
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()
})
})
})
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
)
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()
})
})
})
}) })
describe('revokeInvite', function () { describe('revokeInvite', function () {