Просмотр исходного кода

Merge pull request #27026 from overleaf/jel-domain-capture-check

[web] Check if domain is captured by group when using domain API

GitOrigin-RevId: 6e14df0a1701c33fc80f21f01bb3fbb446d7f074
Jessica Lawshe 1 год назад
Родитель
Сommit
1387466151

+ 39 - 2
services/web/frontend/js/features/settings/components/emails/add-email.tsx

@@ -20,6 +20,8 @@ import { useRecaptcha } from '../../../../shared/hooks/use-recaptcha'
 import OLCol from '@/features/ui/components/ol/ol-col'
 import OLCol from '@/features/ui/components/ol/ol-col'
 import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-email-form'
 import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-email-form'
 import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
 import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
+import SsoLinkingInfoGroup from './add-email/sso-linking-info-group'
+import Notification from '@/shared/components/notification'
 
 
 function AddEmail() {
 function AddEmail() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -240,9 +242,9 @@ function AddEmail() {
           <OLCol lg={12}>
           <OLCol lg={12}>
             <Cell>
             <Cell>
               <div className="affiliations-table-cell-tabbed">
               <div className="affiliations-table-cell-tabbed">
-                <SsoLinkingInfo
+                <AddEmailViaSSO
                   email={newEmail}
                   email={newEmail}
-                  domainInfo={newEmailMatchedDomain as DomainInfo}
+                  domainInfo={newEmailMatchedDomain}
                 />
                 />
               </div>
               </div>
             </Cell>
             </Cell>
@@ -254,4 +256,39 @@ function AddEmail() {
   )
   )
 }
 }
 
 
+function AddEmailViaSSO({
+  email,
+  domainInfo,
+}: {
+  email: string
+  domainInfo: DomainInfo
+}) {
+  if (domainInfo.university.ssoEnabled) {
+    // SSO for Commons institution
+    return <SsoLinkingInfo email={email} domainInfo={domainInfo} />
+  } else if (
+    domainInfo.group?.domainCaptureEnabled &&
+    domainInfo.group?.managedUsersEnabled
+  ) {
+    return (
+      <Notification
+        type="error"
+        ariaLive="polite"
+        content={
+          <>
+            Your company email address has been registered under a verified
+            domain, and cannot be added as a secondary email. Please create a
+            new <strong>Overleaf</strong> account linked to this email address.
+          </>
+        }
+      />
+    )
+  } else if (
+    domainInfo.group?.domainCaptureEnabled &&
+    domainInfo.group?.ssoConfig?.enabled
+  ) {
+    return <SsoLinkingInfoGroup domainInfo={domainInfo} />
+  }
+}
+
 export default AddEmail
 export default AddEmail

+ 9 - 0
services/web/frontend/js/features/settings/components/emails/add-email/input.tsx

@@ -34,6 +34,15 @@ export type DomainInfo = {
     ssoBeta?: boolean
     ssoBeta?: boolean
     departments?: string[]
     departments?: string[]
   }
   }
+  group: {
+    teamName?: string
+    managedUsersEnabled?: boolean
+    domainCaptureEnabled?: boolean
+    ssoConfig?: {
+      useUkamfSettings?: boolean
+      enabled: boolean
+    }
+  }
 }
 }
 
 
 let domainCache = new Map<string, DomainInfo>()
 let domainCache = new Map<string, DomainInfo>()

+ 33 - 0
services/web/frontend/js/features/settings/components/emails/add-email/sso-linking-info-group.tsx

@@ -0,0 +1,33 @@
+import { DomainInfo } from './input'
+import { Trans } from 'react-i18next'
+
+type SSOLinkingInfoProps = {
+  domainInfo: DomainInfo
+}
+
+function SsoLinkingInfoGroup({ domainInfo }: SSOLinkingInfoProps) {
+  if (!domainInfo.group.ssoConfig) {
+    return
+  }
+
+  const institutionName =
+    domainInfo.group.teamName || domainInfo.university.name
+
+  return (
+    <>
+      <p>
+        <Trans
+          i18nKey="to_add_email_accounts_need_to_be_linked_2"
+          components={[<strong />]} // eslint-disable-line react/jsx-key
+          values={{ institutionName }}
+          shouldUnescape
+          tOptions={{ interpolation: { escapeValue: true } }}
+        />
+      </p>
+
+      <p>This feature is currently unavailable.</p>
+    </>
+  )
+}
+
+export default SsoLinkingInfoGroup

+ 5 - 0
services/web/frontend/js/features/settings/utils/sso.ts

@@ -12,6 +12,11 @@ export const ssoAvailableForDomain = (
   if (domain.university.ssoEnabled) {
   if (domain.university.ssoEnabled) {
     return true
     return true
   }
   }
+
+  if (domain.group?.ssoConfig?.enabled) {
+    return true
+  }
+
   return Boolean(hasSamlBeta && domain.university.ssoBeta)
   return Boolean(hasSamlBeta && domain.university.ssoBeta)
 }
 }
 
 

+ 17 - 0
services/web/test/acceptance/src/mocks/MockV1Api.mjs

@@ -362,6 +362,23 @@ class MockV1Api extends AbstractMockApi {
             },
             },
           },
           },
         ])
         ])
+      } else if (req.query.hostname === 'sharelatex.com') {
+        res.json([
+          {
+            id: 44,
+            hostname: 'sharelatex.com',
+            department: 'test dept',
+            confirmed: true,
+            university: {
+              id: 5000,
+              name: 'Institution sharelatex',
+              departments: [],
+              ssoBeta: false,
+              ssoEnabled: false,
+              commons: false,
+            },
+          },
+        ])
       } else {
       } else {
         res.json([])
         res.json([])
       }
       }

+ 140 - 0
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

@@ -3,6 +3,7 @@ import {
   screen,
   screen,
   fireEvent,
   fireEvent,
   waitForElementToBeRemoved,
   waitForElementToBeRemoved,
+  within,
 } from '@testing-library/react'
 } from '@testing-library/react'
 import userEvent from '@testing-library/user-event'
 import userEvent from '@testing-library/user-event'
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
@@ -12,6 +13,7 @@ import { UserEmailData } from '../../../../../../types/user-email'
 import { Affiliation } from '../../../../../../types/affiliation'
 import { Affiliation } from '../../../../../../types/affiliation'
 import withMarkup from '../../../../helpers/with-markup'
 import withMarkup from '../../../../helpers/with-markup'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
+import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
 
 
 const userEmailData: UserEmailData & { affiliation: Affiliation } = {
 const userEmailData: UserEmailData & { affiliation: Affiliation } = {
   affiliation: {
   affiliation: {
@@ -87,6 +89,7 @@ describe('<EmailsSection />', function () {
 
 
   afterEach(function () {
   afterEach(function () {
     resetFetchMock()
     resetFetchMock()
+    clearDomainCache()
   })
   })
 
 
   it('renders "add another email" button', async function () {
   it('renders "add another email" button', async function () {
@@ -709,4 +712,141 @@ describe('<EmailsSection />', function () {
       exact: false,
       exact: false,
     })
     })
   })
   })
+
+  describe('when domain is captured by a group', function () {
+    describe('and managed users is not enabled', function () {
+      beforeEach(async function () {
+        await fetchMock.callHistory.flush(true)
+        fetchMock.removeRoutes().clearHistory()
+        const institution = {
+          university: {
+            id: 1234,
+            ssoEnabled: false,
+            name: 'Auto Complete University',
+          },
+          hostname: 'autocomplete.edu',
+          confirmed: true,
+          group: {
+            domainCaptureEnabled: true,
+            ssoConfig: {
+              enabled: true,
+            },
+          },
+        }
+
+        fetchMock.get('express:/institutions/domains', [institution])
+      })
+
+      it('can add email address via SSO', async function () {
+        // note: this UI is a WIP
+        fetchMock.get('/user/emails?ensureAffiliation=true', [])
+        render(<EmailsSection />)
+
+        const button = await screen.findByRole<HTMLButtonElement>('button', {
+          name: /add another email/i,
+        })
+
+        await userEvent.click(button)
+
+        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        fireEvent.change(input, {
+          target: { value: 'user@autocomplete.edu' },
+        })
+        await screen.findByText('This feature is currently unavailable.')
+      })
+    })
+
+    describe('and managed users is enabled', function () {
+      beforeEach(async function () {
+        await fetchMock.callHistory.flush(true)
+        fetchMock.removeRoutes().clearHistory()
+        const institution = {
+          university: {
+            id: 1234,
+            ssoEnabled: false,
+            name: 'Auto Complete University',
+          },
+          hostname: 'autocomplete.edu',
+          confirmed: true,
+          group: {
+            domainCaptureEnabled: true,
+            managedUsersEnabled: true,
+            ssoConfig: {
+              enabled: true,
+            },
+          },
+        }
+
+        fetchMock.get('express:/institutions/domains', [institution])
+      })
+
+      it('renders error', async function () {
+        // note: this UI is a WIP
+        fetchMock.get('/user/emails?ensureAffiliation=true', [])
+        render(<EmailsSection />)
+
+        const button = await screen.findByRole<HTMLButtonElement>('button', {
+          name: /add another email/i,
+        })
+
+        await userEvent.click(button)
+
+        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        fireEvent.change(input, {
+          target: { value: 'user@autocomplete.edu' },
+        })
+
+        const notification = await screen.findByRole('alert')
+        within(notification).getByText(
+          'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
+          { exact: false }
+        )
+      })
+    })
+
+    describe('if Commons SSO then enabled, that takes priority over group UI', function () {
+      // we shouldn't have SSO config in v1 and in v2 but adding test to ensure Commons takes priority
+      beforeEach(async function () {
+        await fetchMock.callHistory.flush(true)
+        fetchMock.removeRoutes().clearHistory()
+        const institution = {
+          university: {
+            id: 1234,
+            ssoEnabled: true,
+            name: 'Auto Complete University',
+          },
+          hostname: 'autocomplete.edu',
+          confirmed: true,
+          group: {
+            domainCaptureEnabled: true,
+            ssoConfig: {
+              enabled: true,
+            },
+          },
+        }
+
+        fetchMock.get('express:/institutions/domains', [institution])
+      })
+
+      it('renders Commons UI', async function () {
+        fetchMock.get('/user/emails?ensureAffiliation=true', [])
+        render(<EmailsSection />)
+
+        const button = await screen.findByRole<HTMLButtonElement>('button', {
+          name: /add another email/i,
+        })
+
+        await userEvent.click(button)
+
+        const input = screen.getByLabelText(/email/i, { selector: 'input' })
+        fireEvent.change(input, {
+          target: { value: 'user@autocomplete.edu' },
+        })
+
+        await screen.findByRole('button', {
+          name: 'Link accounts and add email',
+        })
+      })
+    })
+  })
 })
 })