From 49a00ef0e25029b59869e9c337afe200c96727d9 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe <5312836+lawshe@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:01:13 -0500 Subject: [PATCH] Merge pull request #36310 from overleaf/jel-confirm-existing-email [web] Check if domain captured by group when showing confirm email UI GitOrigin-RevId: bb6add82e083497c8036739d877610a47e35ecfd --- .../settings/components/emails/email.tsx | 8 +- .../settings/utils/email-confirmation.ts | 31 +++++++ .../components/emails/emails-section.test.tsx | 41 +++++++++ .../settings/utils/email-confirmation.test.ts | 91 +++++++++++++++++++ 4 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 services/web/frontend/js/features/settings/utils/email-confirmation.ts create mode 100644 services/web/test/frontend/features/settings/utils/email-confirmation.test.ts diff --git a/services/web/frontend/js/features/settings/components/emails/email.tsx b/services/web/frontend/js/features/settings/components/emails/email.tsx index c0cf98e8bc..0a56d317ed 100644 --- a/services/web/frontend/js/features/settings/components/emails/email.tsx +++ b/services/web/frontend/js/features/settings/components/emails/email.tsx @@ -1,9 +1,9 @@ import { useTranslation } from 'react-i18next' import { UserEmailData } from '../../../../../../types/user-email' -import { ssoAvailableForInstitution } from '../../utils/sso' import OLBadge from '@/shared/components/ol/ol-badge' import ResendConfirmationCodeModal from '@/features/settings/components/emails/resend-confirmation-code-modal' import { useUserEmailsContext } from '@/features/settings/context/user-email-context' +import { emailMustBeConfirmedViaSAML } from '../../utils/email-confirmation' type EmailProps = { userEmailData: UserEmailData @@ -16,8 +16,8 @@ function Email({ userEmailData }: EmailProps) { setLoading: setUserEmailsContextLoading, getEmails, } = useUserEmailsContext() - const ssoAvailable = ssoAvailableForInstitution( - userEmailData.affiliation?.institution || null + const mustConfirmViaSAML = emailMustBeConfirmedViaSAML( + userEmailData.affiliation || null ) const isPrimary = userEmailData.default @@ -37,7 +37,7 @@ function Email({ userEmailData }: EmailProps) {
{t('unconfirmed')}.
- {!ssoAvailable && ( + {!mustConfirmViaSAML && ( { + const { hasSamlFeature } = getMeta('ol-ExposedSettings') + if ( + !hasSamlFeature || + !affiliation || + !affiliation.institution || + !affiliation.institution.confirmed + ) { + return false + } + // both groups and Commons have affiliation.institution, but only Commons has SAML + // information in affiliation.institution + // ssoAvailableForInstitution will only check affiliation.institution for Commons + // and there are different rules for whether SAML is enabled or not for groups with + // domain capture or Commons + + const samlViaCommons = ssoAvailableForInstitution(affiliation.institution) + + const samlViaGroup = + affiliation.domainCapturedByGroup && + affiliation.group?.domainCaptureEnabled && + affiliation.group?.managedUsersEnabled + + return samlViaCommons || samlViaGroup || false +} diff --git a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx index 5505d66ac3..bfe8fe0e1d 100644 --- a/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx +++ b/services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx @@ -9,6 +9,7 @@ import { import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section' import { expect } from 'chai' import fetchMock from 'fetch-mock' +import { cloneDeep } from 'lodash' import { confirmedUserData, fakeUsersData, @@ -111,6 +112,46 @@ describe('', function () { await screen.findByRole('button', { name: 'Send confirmation code' }) }) + describe('SAML emails', function () { + beforeEach(function () { + Object.assign(getMeta('ol-ExposedSettings'), { + hasSamlFeature: true, + }) + fetchMock.removeRoutes().clearHistory() + }) + + it('hides resend link when an unconfirmed affiliated email must be confirmed via Commons SAML', async function () { + const commonsSamlEmail = cloneDeep(professionalUserData) + delete commonsSamlEmail.confirmedAt + commonsSamlEmail.affiliation.institution.ssoEnabled = true + + fetchMock.get('/user/emails?ensureAffiliation=true', [commonsSamlEmail]) + renderEmailsSection() + + await screen.findByText(/unconfirmed/i) + expect(screen.queryByRole('button', { name: 'Send confirmation code' })) + .to.be.null + }) + + it('hides resend link when an unconfirmed affiliated email must be confirmed via managed group SAML', async function () { + const managedGroupEmail = cloneDeep(professionalUserData) + delete managedGroupEmail.confirmedAt + managedGroupEmail.affiliation.domainCapturedByGroup = true + managedGroupEmail.affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + + fetchMock.get('/user/emails?ensureAffiliation=true', [managedGroupEmail]) + renderEmailsSection() + + await screen.findByText(/unconfirmed/i) + expect(screen.queryByRole('button', { name: 'Send confirmation code' })) + .to.be.null + }) + }) + it('renders commons label', async function () { fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData]) renderEmailsSection() diff --git a/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts b/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts new file mode 100644 index 0000000000..c61cda0cec --- /dev/null +++ b/services/web/test/frontend/features/settings/utils/email-confirmation.test.ts @@ -0,0 +1,91 @@ +import { expect } from 'chai' +import { cloneDeep } from 'lodash' +import { professionalUserData } from '../fixtures/test-user-email-data' +import { Affiliation } from '../../../../../types/affiliation' +import { emailMustBeConfirmedViaSAML } from '../../../../../frontend/js/features/settings/utils/email-confirmation' + +describe('emailMustBeConfirmedViaSAML', function () { + beforeEach(function () { + window.metaAttributesCache.set('ol-ExposedSettings', { + hasSamlFeature: true, + hasSamlBeta: false, + }) + }) + + it('returns false when hasSamlFeature is false', function () { + window.metaAttributesCache.set('ol-ExposedSettings', { + hasSamlFeature: false, + hasSamlBeta: false, + }) + + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.domainCapturedByGroup = true + affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false + }) + + it('returns false when affiliation is null', function () { + expect(emailMustBeConfirmedViaSAML(null)).to.be.false + }) + + it('returns false when institution is not confirmed', function () { + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.institution.confirmed = false + affiliation.domainCapturedByGroup = true + affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false + }) + + it('returns true when Commons SSO is available for the institution', function () { + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.institution.ssoEnabled = true + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.true + }) + + it('returns true when the email is captured by a managed domain-capture group', function () { + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.domainCapturedByGroup = true + affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.true + }) + + it('returns false when the group is managed but the email domain is not captured by the group', function () { + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.domainCapturedByGroup = false + affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: true, + } + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false + }) + + it('returns false when the email is captured but managed users are disabled', function () { + const affiliation: Affiliation = cloneDeep(professionalUserData.affiliation) + affiliation.domainCapturedByGroup = true + affiliation.group = { + _id: 'group123', + domainCaptureEnabled: true, + managedUsersEnabled: false, + } + + expect(emailMustBeConfirmedViaSAML(affiliation)).to.be.false + }) +})