Przeglądaj źródła

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

GitOrigin-RevId: 9641946e65ede2d52645caf8876d7587a24e7dfc
Davinder Singh 3 lat temu
rodzic
commit
f97689aa87

+ 4 - 2
services/web/app/src/Features/Email/EmailBuilder.js

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

+ 3 - 0
services/web/frontend/extracted-translations.json

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

+ 55 - 1
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 Icon from '../../../../shared/components/icon'
 import { ManagedUserAlert } from '../../utils/types'
 import { ManagedUserAlert } from '../../utils/types'
 import { useGroupMembersContext } from '../../context/group-members-context'
 import { useGroupMembersContext } from '../../context/group-members-context'
-
+import getMeta from '@/utils/meta'
 type resendInviteResponse = {
 type resendInviteResponse = {
   success: boolean
   success: boolean
 }
 }
@@ -42,6 +42,13 @@ export default function ManagedUserDropdownButton({
     isLoading: isResendingManagedUserInvite,
     isLoading: isResendingManagedUserInvite,
   } = useAsync<resendInviteResponse>()
   } = useAsync<resendInviteResponse>()
 
 
+  const groupSSOActive = getMeta('ol-groupSSOActive')
+  const ssoEnabledButNotAccepted = groupSSOActive && !user.enrollment?.sso
+  const {
+    runAsync: runResendLinkSSOInviteAsync,
+    isLoading: isResendingSSOLinkInvite,
+  } = useAsync<resendInviteResponse>()
+
   const {
   const {
     runAsync: runResendGroupInviteAsync,
     runAsync: runResendGroupInviteAsync,
     isLoading: isResendingGroupInvite,
     isLoading: isResendingGroupInvite,
@@ -87,6 +94,39 @@ export default function ManagedUserDropdownButton({
     [setManagedUserAlert, groupId, runResendManagedUserInviteAsync]
     [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(
   const handleResendGroupInvite = useCallback(
     async user => {
     async user => {
       try {
       try {
@@ -125,6 +165,9 @@ export default function ManagedUserDropdownButton({
   const onResendManagedUserInviteClick = () => {
   const onResendManagedUserInviteClick = () => {
     handleResendManagedUserInvite(user)
     handleResendManagedUserInvite(user)
   }
   }
+  const onResendSSOLinkInviteClick = () => {
+    handleResendLinkSSOInviteAsync(user)
+  }
 
 
   const onResendGroupInviteClick = () => {
   const onResendGroupInviteClick = () => {
     handleResendGroupInvite(user)
     handleResendGroupInvite(user)
@@ -179,6 +222,17 @@ export default function ManagedUserDropdownButton({
               ) : null}
               ) : null}
             </MenuItemButton>
             </MenuItemButton>
           ) : null}
           ) : 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 ? (
           {user.isEntityAdmin ? (
             <MenuItem data-testid="no-actions-available">
             <MenuItem data-testid="no-actions-available">
               <span className="text-muted">{t('no_actions')}</span>
               <span className="text-muted">{t('no_actions')}</span>

+ 57 - 0
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}
           invitedUserEmail={invitedUserEmail}
         />
         />
       )
       )
+    case 'resendSSOLinkInviteSuccess':
+      return (
+        <ResendSSOLinkInviteSuccess
+          onDismiss={onDismiss}
+          invitedUserEmail={invitedUserEmail}
+        />
+      )
     case 'resendManagedUserInviteFailed':
     case 'resendManagedUserInviteFailed':
       return (
       return (
         <FailedToResendManagedInvite
         <FailedToResendManagedInvite
@@ -29,6 +36,13 @@ export default function ManagedUsersListAlert({
           invitedUserEmail={invitedUserEmail}
           invitedUserEmail={invitedUserEmail}
         />
         />
       )
       )
+    case 'resendSSOLinkInviteFailed':
+      return (
+        <FailedToResendSSOLink
+          onDismiss={onDismiss}
+          invitedUserEmail={invitedUserEmail}
+        />
+      )
     case 'resendGroupInviteSuccess':
     case 'resendGroupInviteSuccess':
       return (
       return (
         <ResendGroupInviteSuccess
         <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({
 function FailedToResendManagedInvite({
   onDismiss,
   onDismiss,
   invitedUserEmail,
   invitedUserEmail,
@@ -101,6 +137,27 @@ function FailedToResendManagedInvite({
     </AlertComponent>
     </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({
 function ResendGroupInviteSuccess({
   onDismiss,
   onDismiss,

+ 2 - 0
services/web/frontend/js/features/group-management/utils/types.ts

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

+ 3 - 0
services/web/locales/en.json

@@ -522,6 +522,7 @@
   "export_project_to_github": "Export Project to GitHub",
   "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_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_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_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_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>.",
   "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_confirmation_email": "Resend confirmation email",
   "resend_email": "Resend email",
   "resend_email": "Resend email",
   "resend_group_invite": "Resend group invite",
   "resend_group_invite": "Resend group invite",
+  "resend_link_sso": "Resend SSO invite",
   "resend_managed_user_invite": "Resend managed user invite",
   "resend_managed_user_invite": "Resend managed user invite",
   "resending_confirmation_email": "Resending confirmation email",
   "resending_confirmation_email": "Resending confirmation email",
   "reset_password": "Reset Password",
   "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_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_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_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_linked": "SSO linked",
   "sso_logs": "SSO Logs",
   "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.",
   "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.",

+ 104 - 52
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 sinon from 'sinon'
 import ManagedUserDropdownButton from '../../../../../../frontend/js/features/group-management/components/managed-users/managed-user-dropdown-button'
 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 { 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>>) {
 function Wrapper({ children }: PropsWithChildren<Record<string, unknown>>) {
   return (
   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 () {
 describe('ManagedUserDropdownButton', function () {
   const subscriptionId = '123abc'
   const subscriptionId = '123abc'
 
 
@@ -38,17 +52,7 @@ describe('ManagedUserDropdownButton', function () {
       cy.window().then(win => {
       cy.window().then(win => {
         win.metaAttributesCache.set('ol-users', [user])
         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 () {
     it('should render dropdown button', function () {
@@ -84,16 +88,7 @@ describe('ManagedUserDropdownButton', function () {
         win.metaAttributesCache.set('ol-users', [user])
         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 () {
     it('should render dropdown button', function () {
@@ -133,16 +128,7 @@ describe('ManagedUserDropdownButton', function () {
         win.metaAttributesCache.set('ol-users', [user])
         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 () {
     it('should render dropdown button', function () {
@@ -181,17 +167,7 @@ describe('ManagedUserDropdownButton', function () {
       cy.window().then(win => {
       cy.window().then(win => {
         win.metaAttributesCache.set('ol-users', [user])
         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 () {
     it('should render dropdown button', function () {
@@ -227,16 +203,7 @@ describe('ManagedUserDropdownButton', function () {
         win.metaAttributesCache.set('ol-users', [user])
         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 () {
     it('should render the button', function () {
@@ -251,4 +218,89 @@ describe('ManagedUserDropdownButton', function () {
       cy.findByTestId('no-actions-available').should('exist')
       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')
+    })
+  })
 })
 })