From f97689aa87eaad762e76c738e422660923fc475a Mon Sep 17 00:00:00 2001 From: Davinder Singh Date: Wed, 25 Oct 2023 12:09:54 +0100 Subject: [PATCH] Group SSO - Adding the SSO invite link reminder button in the dropdown (#15289) GitOrigin-RevId: 9641946e65ede2d52645caf8876d7587a24e7dfc --- .../app/src/Features/Email/EmailBuilder.js | 6 +- .../web/frontend/extracted-translations.json | 3 + .../managed-user-dropdown-button.tsx | 56 ++++++- .../managed-users-list-alert.tsx | 57 +++++++ .../features/group-management/utils/types.ts | 2 + services/web/locales/en.json | 3 + .../managed-user-dropdown-button.spec.tsx | 156 ++++++++++++------ 7 files changed, 228 insertions(+), 55 deletions(-) diff --git a/services/web/app/src/Features/Email/EmailBuilder.js b/services/web/app/src/Features/Email/EmailBuilder.js index 092d96cbbd..7c28d9eb0d 100644 --- a/services/web/app/src/Features/Email/EmailBuilder.js +++ b/services/web/app/src/Features/Email/EmailBuilder.js @@ -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 [ diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 1ea6ec8bd7..9d7f8df9f9 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx index 21f002deec..95f29da009 100644 --- a/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button.tsx @@ -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() + const groupSSOActive = getMeta('ol-groupSSOActive') + const ssoEnabledButNotAccepted = groupSSOActive && !user.enrollment?.sso + const { + runAsync: runResendLinkSSOInviteAsync, + isLoading: isResendingSSOLinkInvite, + } = useAsync() + 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} ) : null} + {ssoEnabledButNotAccepted && ( + + {t('resend_link_sso')} + {isResendingSSOLinkInvite ? ( + + ) : null} + + )} {user.isEntityAdmin ? ( {t('no_actions')} diff --git a/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx index 018f2a59b6..86937dd842 100644 --- a/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx +++ b/services/web/frontend/js/features/group-management/components/managed-users/managed-users-list-alert.tsx @@ -22,6 +22,13 @@ export default function ManagedUsersListAlert({ invitedUserEmail={invitedUserEmail} /> ) + case 'resendSSOLinkInviteSuccess': + return ( + + ) case 'resendManagedUserInviteFailed': return ( ) + case 'resendSSOLinkInviteFailed': + return ( + + ) case 'resendGroupInviteSuccess': return ( + , + ]} + /> + + ) +} + function FailedToResendManagedInvite({ onDismiss, invitedUserEmail, @@ -101,6 +137,27 @@ function FailedToResendManagedInvite({ ) } +function FailedToResendSSOLink({ + onDismiss, + invitedUserEmail, +}: ManagedUsersListAlertComponentProps) { + return ( + + , + ]} + /> + + ) +} function ResendGroupInviteSuccess({ onDismiss, diff --git a/services/web/frontend/js/features/group-management/utils/types.ts b/services/web/frontend/js/features/group-management/utils/types.ts index 140d6f1de8..48b4f0bd4b 100644 --- a/services/web/frontend/js/features/group-management/utils/types.ts +++ b/services/web/frontend/js/features/group-management/utils/types.ts @@ -4,6 +4,8 @@ export type ManagedUserAlertVariant = | 'resendGroupInviteSuccess' | 'resendGroupInviteFailed' | 'resendInviteTooManyRequests' + | 'resendSSOLinkInviteSuccess' + | 'resendSSOLinkInviteFailed' export type ManagedUserAlert = | { diff --git a/services/web/locales/en.json b/services/web/locales/en.json index fb7247cad9..6262e39fa6 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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__. Please try again later.", "failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__. Please try again later.", + "failed_to_send_sso_link_invite_to_email": "Failed to send SSO invite reminder to <0>__email__. 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 and <1>how premium features work.", @@ -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 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__", "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.", diff --git a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx index f60a68df99..9d917ce932 100644 --- a/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx +++ b/services/web/test/frontend/features/group-management/components/managed-users/managed-user-dropdown-button.spec.tsx @@ -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>) { return ( @@ -16,6 +17,19 @@ function Wrapper({ children }: PropsWithChildren>) { ) } +function mountDropDownComponent(user: User, subscriptionId: string) { + cy.mount( + + + + ) +} + 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( - - - - ) + mountDropDownComponent(user, subscriptionId) }) it('should render dropdown button', function () { @@ -84,16 +88,7 @@ describe('ManagedUserDropdownButton', function () { win.metaAttributesCache.set('ol-users', [user]) }) - cy.mount( - - - - ) + mountDropDownComponent(user, subscriptionId) }) it('should render dropdown button', function () { @@ -133,16 +128,7 @@ describe('ManagedUserDropdownButton', function () { win.metaAttributesCache.set('ol-users', [user]) }) - cy.mount( - - - - ) + 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( - - - - ) + mountDropDownComponent(user, subscriptionId) }) it('should render dropdown button', function () { @@ -227,16 +203,7 @@ describe('ManagedUserDropdownButton', function () { win.metaAttributesCache.set('ol-users', [user]) }) - cy.mount( - - - - ) + 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') + }) + }) })