mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 13:03:42 -05:00
Group SSO - Adding the SSO invite link reminder button in the dropdown (#15289)
GitOrigin-RevId: 9641946e65ede2d52645caf8876d7587a24e7dfc
This commit is contained in:
parent
a388c1f414
commit
f97689aa87
7 changed files with 228 additions and 55 deletions
|
@ -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 [
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -4,6 +4,8 @@ export type ManagedUserAlertVariant =
|
|||
| 'resendGroupInviteSuccess'
|
||||
| 'resendGroupInviteFailed'
|
||||
| 'resendInviteTooManyRequests'
|
||||
| 'resendSSOLinkInviteSuccess'
|
||||
| 'resendSSOLinkInviteFailed'
|
||||
|
||||
export type ManagedUserAlert =
|
||||
| {
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue