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
This commit is contained in:
Jessica Lawshe
2025-07-22 09:28:07 -05:00
committed by Copybot
parent d34b38b6e6
commit 1387466151
6 changed files with 243 additions and 2 deletions

View File

@@ -20,6 +20,8 @@ import { useRecaptcha } from '../../../../shared/hooks/use-recaptcha'
import OLCol from '@/features/ui/components/ol/ol-col'
import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-email-form'
import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
import SsoLinkingInfoGroup from './add-email/sso-linking-info-group'
import Notification from '@/shared/components/notification'
function AddEmail() {
const { t } = useTranslation()
@@ -240,9 +242,9 @@ function AddEmail() {
<OLCol lg={12}>
<Cell>
<div className="affiliations-table-cell-tabbed">
<SsoLinkingInfo
<AddEmailViaSSO
email={newEmail}
domainInfo={newEmailMatchedDomain as DomainInfo}
domainInfo={newEmailMatchedDomain}
/>
</div>
</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

View File

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

View File

@@ -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

View File

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

View File

@@ -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 {
res.json([])
}

View File

@@ -3,6 +3,7 @@ import {
screen,
fireEvent,
waitForElementToBeRemoved,
within,
} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
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 withMarkup from '../../../../helpers/with-markup'
import getMeta from '@/utils/meta'
import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
const userEmailData: UserEmailData & { affiliation: Affiliation } = {
affiliation: {
@@ -87,6 +89,7 @@ describe('<EmailsSection />', function () {
afterEach(function () {
resetFetchMock()
clearDomainCache()
})
it('renders "add another email" button', async function () {
@@ -709,4 +712,141 @@ describe('<EmailsSection />', function () {
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',
})
})
})
})
})