diff --git a/services/web/frontend/js/features/settings/components/emails/add-email.tsx b/services/web/frontend/js/features/settings/components/emails/add-email.tsx index df66c21f9f..83039a3006 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-email.tsx +++ b/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 { 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() {
-
@@ -254,4 +256,39 @@ function AddEmail() { ) } +function AddEmailViaSSO({ + email, + domainInfo, +}: { + email: string + domainInfo: DomainInfo +}) { + if (domainInfo.university.ssoEnabled) { + // SSO for Commons institution + return + } else if ( + domainInfo.group?.domainCaptureEnabled && + domainInfo.group?.managedUsersEnabled + ) { + return ( + + Your company email address has been registered under a verified + domain, and cannot be added as a secondary email. Please create a + new Overleaf account linked to this email address. + + } + /> + ) + } else if ( + domainInfo.group?.domainCaptureEnabled && + domainInfo.group?.ssoConfig?.enabled + ) { + return + } +} + export default AddEmail diff --git a/services/web/frontend/js/features/settings/components/emails/add-email/input.tsx b/services/web/frontend/js/features/settings/components/emails/add-email/input.tsx index c5884dbd9c..45adcebe98 100644 --- a/services/web/frontend/js/features/settings/components/emails/add-email/input.tsx +++ b/services/web/frontend/js/features/settings/components/emails/add-email/input.tsx @@ -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() diff --git a/services/web/frontend/js/features/settings/components/emails/add-email/sso-linking-info-group.tsx b/services/web/frontend/js/features/settings/components/emails/add-email/sso-linking-info-group.tsx new file mode 100644 index 0000000000..314afe67e0 --- /dev/null +++ b/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 ( + <> +

+ ]} // eslint-disable-line react/jsx-key + values={{ institutionName }} + shouldUnescape + tOptions={{ interpolation: { escapeValue: true } }} + /> +

+ +

This feature is currently unavailable.

+ + ) +} + +export default SsoLinkingInfoGroup diff --git a/services/web/frontend/js/features/settings/utils/sso.ts b/services/web/frontend/js/features/settings/utils/sso.ts index f667d5976e..29764c3883 100644 --- a/services/web/frontend/js/features/settings/utils/sso.ts +++ b/services/web/frontend/js/features/settings/utils/sso.ts @@ -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) } diff --git a/services/web/test/acceptance/src/mocks/MockV1Api.mjs b/services/web/test/acceptance/src/mocks/MockV1Api.mjs index 70740d656e..982bf4b6c7 100644 --- a/services/web/test/acceptance/src/mocks/MockV1Api.mjs +++ b/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 { res.json([]) } diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx index 63021ca26e..1fd4542f76 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx @@ -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('', function () { afterEach(function () { resetFetchMock() + clearDomainCache() }) it('renders "add another email" button', async function () { @@ -709,4 +712,141 @@ describe('', 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() + + const button = await screen.findByRole('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() + + const button = await screen.findByRole('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() + + const button = await screen.findByRole('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', + }) + }) + }) + }) })