Group SSO - Adding the SSO invite link reminder button in the dropdown (#15289)

GitOrigin-RevId: 9641946e65ede2d52645caf8876d7587a24e7dfc
This commit is contained in:
Davinder Singh 2023-10-25 12:09:54 +01:00 committed by Copybot
parent a388c1f414
commit f97689aa87
7 changed files with 228 additions and 55 deletions

View file

@ -447,10 +447,12 @@ templates.inviteNewUserToJoinManagedUsers = ctaTemplate({
templates.managedUsersEnabledSSO = ctaTemplate({
subject(opts) {
return `Action required: Authenticate your Overleaf account`
const subjectPrefix = opts.reminder ? 'Reminder: ' : 'Action required: '
return `${subjectPrefix}Authenticate your Overleaf account`
},
title(opts) {
return `Single sign-on enabled`
const titlePrefix = opts.reminder ? 'Reminder: ' : ''
return `${titlePrefix}Single sign-on enabled`
},
message(opts) {
return [

View file

@ -339,6 +339,7 @@
"export_project_to_github": "",
"failed_to_send_group_invite_to_email": "",
"failed_to_send_managed_user_invite_to_email": "",
"failed_to_send_sso_link_invite_to_email": "",
"fast": "",
"faster_compiles_feedback_question": "",
"faster_compiles_feedback_seems_faster": "",
@ -934,6 +935,7 @@
"resend_confirmation_email": "",
"resend_email": "",
"resend_group_invite": "",
"resend_link_sso": "",
"resend_managed_user_invite": "",
"resending_confirmation_email": "",
"resize": "",
@ -1076,6 +1078,7 @@
"sso_is_enabled_explanation_1": "",
"sso_is_enabled_explanation_2": "",
"sso_link_error": "",
"sso_link_invite_has_been_sent_to_email": "",
"sso_linked": "",
"sso_logs": "",
"sso_unlinked": "",

View file

@ -16,7 +16,7 @@ import {
import Icon from '../../../../shared/components/icon'
import { ManagedUserAlert } from '../../utils/types'
import { useGroupMembersContext } from '../../context/group-members-context'
import getMeta from '@/utils/meta'
type resendInviteResponse = {
success: boolean
}
@ -42,6 +42,13 @@ export default function ManagedUserDropdownButton({
isLoading: isResendingManagedUserInvite,
} = useAsync<resendInviteResponse>()
const groupSSOActive = getMeta('ol-groupSSOActive')
const ssoEnabledButNotAccepted = groupSSOActive && !user.enrollment?.sso
const {
runAsync: runResendLinkSSOInviteAsync,
isLoading: isResendingSSOLinkInvite,
} = useAsync<resendInviteResponse>()
const {
runAsync: runResendGroupInviteAsync,
isLoading: isResendingGroupInvite,
@ -87,6 +94,39 @@ export default function ManagedUserDropdownButton({
[setManagedUserAlert, groupId, runResendManagedUserInviteAsync]
)
const handleResendLinkSSOInviteAsync = useCallback(
async user => {
try {
const result = await runResendLinkSSOInviteAsync(
postJSON(`/manage/groups/${groupId}/resendSSOLinkInvite/${user._id}`)
)
if (result.success) {
setManagedUserAlert({
variant: 'resendSSOLinkInviteSuccess',
email: user.email,
})
setIsOpened(false)
}
} catch (err) {
if ((err as FetchError)?.response?.status === 429) {
setManagedUserAlert({
variant: 'resendInviteTooManyRequests',
email: user.email,
})
} else {
setManagedUserAlert({
variant: 'resendSSOLinkInviteFailed',
email: user.email,
})
}
setIsOpened(false)
}
},
[setManagedUserAlert, groupId, runResendLinkSSOInviteAsync]
)
const handleResendGroupInvite = useCallback(
async user => {
try {
@ -125,6 +165,9 @@ export default function ManagedUserDropdownButton({
const onResendManagedUserInviteClick = () => {
handleResendManagedUserInvite(user)
}
const onResendSSOLinkInviteClick = () => {
handleResendLinkSSOInviteAsync(user)
}
const onResendGroupInviteClick = () => {
handleResendGroupInvite(user)
@ -179,6 +222,17 @@ export default function ManagedUserDropdownButton({
) : null}
</MenuItemButton>
) : null}
{ssoEnabledButNotAccepted && (
<MenuItemButton
onClick={onResendSSOLinkInviteClick}
data-testid="resend-sso-link-invite-action"
>
{t('resend_link_sso')}
{isResendingSSOLinkInvite ? (
<Icon type="spinner" spin style={{ marginLeft: '5px' }} />
) : null}
</MenuItemButton>
)}
{user.isEntityAdmin ? (
<MenuItem data-testid="no-actions-available">
<span className="text-muted">{t('no_actions')}</span>

View file

@ -22,6 +22,13 @@ export default function ManagedUsersListAlert({
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendSSOLinkInviteSuccess':
return (
<ResendSSOLinkInviteSuccess
onDismiss={onDismiss}
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendManagedUserInviteFailed':
return (
<FailedToResendManagedInvite
@ -29,6 +36,13 @@ export default function ManagedUsersListAlert({
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendSSOLinkInviteFailed':
return (
<FailedToResendSSOLink
onDismiss={onDismiss}
invitedUserEmail={invitedUserEmail}
/>
)
case 'resendGroupInviteSuccess':
return (
<ResendGroupInviteSuccess
@ -80,6 +94,28 @@ function ResendManagedUserInviteSuccess({
)
}
function ResendSSOLinkInviteSuccess({
onDismiss,
invitedUserEmail,
}: ManagedUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="success" onDismiss={onDismiss}>
<Trans
i18nKey="sso_link_invite_has_been_sent_to_email"
values={{
email: invitedUserEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
)
}
function FailedToResendManagedInvite({
onDismiss,
invitedUserEmail,
@ -101,6 +137,27 @@ function FailedToResendManagedInvite({
</AlertComponent>
)
}
function FailedToResendSSOLink({
onDismiss,
invitedUserEmail,
}: ManagedUsersListAlertComponentProps) {
return (
<AlertComponent bsStyle="danger" onDismiss={onDismiss}>
<Trans
i18nKey="failed_to_send_sso_link_invite_to_email"
values={{
email: invitedUserEmail,
}}
shouldUnescape
tOptions={{ interpolation: { escapeValue: true } }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</AlertComponent>
)
}
function ResendGroupInviteSuccess({
onDismiss,

View file

@ -4,6 +4,8 @@ export type ManagedUserAlertVariant =
| 'resendGroupInviteSuccess'
| 'resendGroupInviteFailed'
| 'resendInviteTooManyRequests'
| 'resendSSOLinkInviteSuccess'
| 'resendSSOLinkInviteFailed'
export type ManagedUserAlert =
| {

View file

@ -522,6 +522,7 @@
"export_project_to_github": "Export Project to GitHub",
"failed_to_send_group_invite_to_email": "Failed to send Group invite to <0>__email__</0>. Please try again later.",
"failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__</0>. Please try again later.",
"failed_to_send_sso_link_invite_to_email": "Failed to send SSO invite reminder to <0>__email__</0>. Please try again later.",
"faq_change_plans_or_cancel_answer": "Yes, you can do this at any time via your subscription settings. You can change plans, switch between monthly and annual billing options, or cancel to downgrade to the free plan. When cancelling, your subscription will continue until the end of the billing period. If your account temporarily does not have a subscription, the only change will be to the features available to you. Your projects will always be available on your account.",
"faq_change_plans_or_cancel_question": "Can I change plans or cancel later?",
"faq_do_collab_need_on_paid_plan_answer": "No, they can be on any plan, including the free plan. If you are on a premium plan, some premium features will be available to your collaborators in projects that you have created, even if those collaborators are on the free plan. For more information, read about <0>account and subscriptions</0> and <1>how premium features work</1>.",
@ -1461,6 +1462,7 @@
"resend_confirmation_email": "Resend confirmation email",
"resend_email": "Resend email",
"resend_group_invite": "Resend group invite",
"resend_link_sso": "Resend SSO invite",
"resend_managed_user_invite": "Resend managed user invite",
"resending_confirmation_email": "Resending confirmation email",
"reset_password": "Reset Password",
@ -1649,6 +1651,7 @@
"sso_is_enabled_explanation_1": "Group members will <0>only</0> be able to sign in via SSO",
"sso_is_enabled_explanation_2": "If there are any problems with the configuration, only you (as the group administrator) will be able to disable SSO.",
"sso_link_error": "Error linking account",
"sso_link_invite_has_been_sent_to_email": "An SSO invite reminder has been sent to <0>__email__</0>",
"sso_linked": "SSO linked",
"sso_logs": "SSO Logs",
"sso_not_linked": "You have not linked your account to __provider__. Please log in to your account another way and link your __provider__ account via your account settings.",

View file

@ -2,6 +2,7 @@ import type { PropsWithChildren } from 'react'
import sinon from 'sinon'
import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
import { GroupMembersProvider } from '../../../../../../frontend/js/features/group-management/context/group-members-context'
import { User } from '../../../../../../types/group-management/user'
function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
return (
@ -16,6 +17,19 @@ function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
)
}
function mountDropDownComponent(user: User, subscriptionId: string) {
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
}
describe('ManagedUserDropdownButton', function () {
const subscriptionId = '123abc'
@ -38,17 +52,7 @@ describe('ManagedUserDropdownButton', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
mountDropDownComponent(user, subscriptionId)
})
it('should render dropdown button', function () {
@ -84,16 +88,7 @@ describe('ManagedUserDropdownButton', function () {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
mountDropDownComponent(user, subscriptionId)
})
it('should render dropdown button', function () {
@ -133,16 +128,7 @@ describe('ManagedUserDropdownButton', function () {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
mountDropDownComponent(user, subscriptionId)
})
it('should render dropdown button', function () {
@ -181,17 +167,7 @@ describe('ManagedUserDropdownButton', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
mountDropDownComponent(user, subscriptionId)
})
it('should render dropdown button', function () {
@ -227,16 +203,7 @@ describe('ManagedUserDropdownButton', function () {
win.metaAttributesCache.set('ol-users', [user])
})
cy.mount(
<Wrapper>
<ManagedUserDropdownButton
user={user}
openOffboardingModalForUser={sinon.stub()}
groupId={subscriptionId}
setManagedUserAlert={sinon.stub()}
/>
</Wrapper>
)
mountDropDownComponent(user, subscriptionId)
})
it('should render the button', function () {
@ -251,4 +218,89 @@ describe('ManagedUserDropdownButton', function () {
cy.findByTestId('no-actions-available').should('exist')
})
})
describe('sending SSO invite reminder', function () {
const user = {
_id: 'some-user',
email: 'some.user@example.com',
first_name: 'Some',
last_name: 'User',
invite: false,
last_active_at: new Date(),
enrollment: {
managedBy: 'some-group',
enrolledAt: new Date(),
},
isEntityAdmin: undefined,
}
beforeEach(function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-users', [user])
})
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
})
it('should show resend invite when user is admin', function () {
mountDropDownComponent({ ...user, isEntityAdmin: true }, '123abc')
cy.get('.action-btn').click()
cy.findByTestId('resend-sso-link-invite-action').should('exist')
})
it('should not show resend invite when SSO is disabled', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountDropDownComponent(user, '123abc')
cy.get('.action-btn').click()
cy.findByTestId('resend-sso-link-invite-action').should('not.exist')
})
it('should not show resend invite when user has accepted SSO already', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', false)
})
mountDropDownComponent(
{
...user,
enrollment: {
managedBy: 'some-group',
enrolledAt: new Date(),
sso: {
providerId: '123',
externalId: '123',
},
},
},
'123abc'
)
cy.get('.action-btn').click()
cy.findByTestId('resend-sso-link-invite-action').should('not.exist')
})
it('should show the resend SSO invite option when dropdown button is clicked', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
mountDropDownComponent(user, '123abc')
cy.get('.action-btn').click()
cy.findByTestId('resend-sso-link-invite-action').should('exist')
cy.findByTestId('resend-sso-link-invite-action').then($el => {
Cypress.dom.isVisible($el)
})
})
it('should make the correct post request when resend SSO invite is clicked ', function () {
cy.window().then(win => {
win.metaAttributesCache.set('ol-groupSSOActive', true)
})
cy.intercept(
'POST',
'/manage/groups/123abc/resendSSOLinkInvite/some-user',
{ success: true }
).as('resendInviteRequest')
mountDropDownComponent(user, '123abc')
cy.get('.action-btn').click()
cy.findByTestId('resend-sso-link-invite-action')
.should('exist')
.as('resendInvite')
cy.get('@resendInvite').click()
cy.wait('@resendInviteRequest')
})
})
})