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
This commit is contained in:
Jessica Lawshe
2026-09-17 12:02:03 +00:00
committed by Copybot
parent f62b8525f2
commit 49a00ef0e2
4 changed files with 167 additions and 4 deletions
@@ -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) {
<div className="small">
<strong>{t('unconfirmed')}.</strong>
<br />
{!ssoAvailable && (
{!mustConfirmViaSAML && (
<ResendConfirmationCodeModal
email={userEmailData.email}
setGroupLoading={setUserEmailsContextLoading}
@@ -0,0 +1,31 @@
import getMeta from '@/utils/meta'
import { Affiliation } from '../../../../../types/affiliation'
import { ssoAvailableForInstitution } from './sso'
export const emailMustBeConfirmedViaSAML = (
affiliation: Affiliation | null
): boolean => {
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
}
@@ -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('<EmailsSection />', 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()
@@ -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
})
})