mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
[Settings] Link SSO email (#7778)
* [Settings] Link SSO email GitOrigin-RevId: c46ed1709ceedd74df52e02a87f22614d024afea
This commit is contained in:
parent
8472ac6fc6
commit
23d1bfd11e
8 changed files with 241 additions and 77 deletions
|
@ -87,6 +87,7 @@
|
|||
"dismiss": "",
|
||||
"dismiss_error_popup": "",
|
||||
"doesnt_match": "",
|
||||
"doing_this_will_verify_affiliation_and_allow_log_in_2": "",
|
||||
"done": "",
|
||||
"download": "",
|
||||
"download_pdf": "",
|
||||
|
@ -120,6 +121,7 @@
|
|||
"file_name_in_this_project": "",
|
||||
"file_outline": "",
|
||||
"files_cannot_include_invalid_characters": "",
|
||||
"find_out_more_about_institution_login": "",
|
||||
"find_out_more_about_the_file_outline": "",
|
||||
"find_the_symbols_you_need_with_premium": "",
|
||||
"first_name": "",
|
||||
|
@ -216,6 +218,7 @@
|
|||
"learn_more_about_the_symbol_palette": "",
|
||||
"let_us_know": "",
|
||||
"link": "",
|
||||
"link_accounts_and_add_email": "",
|
||||
"link_sharing_is_off": "",
|
||||
"link_sharing_is_on": "",
|
||||
"link_to_github": "",
|
||||
|
@ -431,6 +434,7 @@
|
|||
"this_project_is_public_read_only": "",
|
||||
"this_project_will_appear_in_your_dropbox_folder_at": "",
|
||||
"timedout": "",
|
||||
"to_add_email_accounts_need_to_be_linked_2": "",
|
||||
"to_add_more_collaborators": "",
|
||||
"to_change_access_permissions": "",
|
||||
"toggle_compile_options_menu": "",
|
||||
|
|
|
@ -19,7 +19,16 @@ function matchLocalAndDomain(emailHint: string) {
|
|||
}
|
||||
}
|
||||
|
||||
type InstitutionInfo = { hostname: string; university: { id: number } }
|
||||
export type InstitutionInfo = {
|
||||
hostname: string
|
||||
confirmed?: boolean
|
||||
university: {
|
||||
id: number
|
||||
name: string
|
||||
ssoEnabled?: boolean
|
||||
ssoBeta?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
let domainCache = new Map<string, InstitutionInfo>()
|
||||
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { InstitutionInfo } from './add-email-input'
|
||||
import { ExposedSettings } from '../../../../../../types/exposed-settings'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
|
||||
type AddEmailSSOLinkingInfoProps = {
|
||||
institutionInfo: InstitutionInfo
|
||||
email: string
|
||||
}
|
||||
|
||||
export function AddEmailSSOLinkingInfo({
|
||||
institutionInfo,
|
||||
email,
|
||||
}: AddEmailSSOLinkingInfoProps) {
|
||||
const { samlInitPath } = getMeta('ol-ExposedSettings') as ExposedSettings
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className="affiliations-table-label">
|
||||
{institutionInfo.university.name}
|
||||
</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="to_add_email_accounts_need_to_be_linked_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institutionInfo.university.name }}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="doing_this_will_verify_affiliation_and_allow_log_in_2"
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
values={{ institutionName: institutionInfo.university.name }}
|
||||
/>{' '}
|
||||
<a
|
||||
href="/learn/how-to/Institutional_Login"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t('find_out_more_about_institution_login')}.
|
||||
</a>
|
||||
</p>
|
||||
<a
|
||||
className="btn-sm btn btn-primary btn-link-accounts"
|
||||
href={`${samlInitPath}?university_id=${institutionInfo.university.id}&auto=/user/settings&email=${email}`}
|
||||
>
|
||||
{t('link_accounts_and_add_email')}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -5,7 +5,7 @@ import Cell from './cell'
|
|||
import Icon from '../../../../shared/components/icon'
|
||||
import DownshiftInput from './downshift-input'
|
||||
import CountryInput from './country-input'
|
||||
import { AddEmailInput } from './add-email-input'
|
||||
import { AddEmailInput, InstitutionInfo } from './add-email-input'
|
||||
import useAsync from '../../../../shared/hooks/use-async'
|
||||
import { useUserEmailsContext } from '../../context/user-email-context'
|
||||
import { getJSON, postJSON } from '../../../../infrastructure/fetch-json'
|
||||
|
@ -13,17 +13,35 @@ import { defaults as roles } from '../../roles'
|
|||
import { defaults as departments } from '../../departments'
|
||||
import { University } from '../../../../../../types/university'
|
||||
import { CountryCode } from '../../../../../../types/country'
|
||||
import { ExposedSettings } from '../../../../../../types/exposed-settings'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
import { AddEmailSSOLinkingInfo } from './add-email-sso-linking-info'
|
||||
|
||||
const isValidEmail = (email: string) => {
|
||||
return Boolean(email)
|
||||
}
|
||||
|
||||
const ssoAvailableForDomain = (domain: InstitutionInfo | null) => {
|
||||
const { hasSamlBeta, hasSamlFeature } = getMeta(
|
||||
'ol-ExposedSettings'
|
||||
) as ExposedSettings
|
||||
if (!hasSamlFeature || !domain || !domain.confirmed || !domain.university) {
|
||||
return false
|
||||
}
|
||||
if (domain.university.ssoEnabled) {
|
||||
return true
|
||||
}
|
||||
return hasSamlBeta && domain.university.ssoBeta
|
||||
}
|
||||
|
||||
function AddEmail() {
|
||||
const { t } = useTranslation()
|
||||
const [isFormVisible, setIsFormVisible] = useState(
|
||||
() => window.location.hash === '#add-email'
|
||||
)
|
||||
const [newEmail, setNewEmail] = useState('')
|
||||
const [newEmailMatchedInstitution, setNewEmailMatchedInstitution] =
|
||||
useState<InstitutionInfo | null>(null)
|
||||
const [countryCode, setCountryCode] = useState<CountryCode | null>(null)
|
||||
const [universities, setUniversities] = useState<
|
||||
Partial<Record<CountryCode, University[]>>
|
||||
|
@ -77,8 +95,9 @@ function AddEmail() {
|
|||
setIsInstitutionFieldsVisible(true)
|
||||
}
|
||||
|
||||
const handleEmailChange = (value: string) => {
|
||||
const handleEmailChange = (value: string, institution?: InstitutionInfo) => {
|
||||
setNewEmail(value)
|
||||
setNewEmailMatchedInstitution(institution || null)
|
||||
}
|
||||
|
||||
const handleAddNewEmail = () => {
|
||||
|
@ -118,6 +137,7 @@ function AddEmail() {
|
|||
getEmails()
|
||||
setIsFormVisible(false)
|
||||
setNewEmail('')
|
||||
setNewEmailMatchedInstitution(null)
|
||||
setCountryCode(null)
|
||||
setIsUniversityDirty(false)
|
||||
setUniversity('')
|
||||
|
@ -136,6 +156,10 @@ function AddEmail() {
|
|||
return universities[countryCode]?.map(({ name }) => name) ?? []
|
||||
}
|
||||
|
||||
const ssoAvailable =
|
||||
newEmailMatchedInstitution &&
|
||||
ssoAvailableForDomain(newEmailMatchedInstitution)
|
||||
|
||||
return (
|
||||
<div className="affiliations-table-row--highlighted">
|
||||
<Row>
|
||||
|
@ -160,83 +184,100 @@ function AddEmail() {
|
|||
<AddEmailInput onChange={handleEmailChange} />
|
||||
</Cell>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<Cell>
|
||||
{isInstitutionFieldsVisible ? (
|
||||
<>
|
||||
<div className="form-group mb-2">
|
||||
<CountryInput
|
||||
id="new-email-country-input"
|
||||
setValue={setCountryCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-2">
|
||||
<DownshiftInput
|
||||
items={getUniversityItems()}
|
||||
inputValue={university}
|
||||
placeholder={t('university')}
|
||||
label={t('university')}
|
||||
setValue={setUniversity}
|
||||
disabled={!countryCode}
|
||||
/>
|
||||
</div>
|
||||
{isUniversityDirty && (
|
||||
|
||||
{ssoAvailable && (
|
||||
<Col md={8}>
|
||||
<Cell>
|
||||
<AddEmailSSOLinkingInfo
|
||||
email={newEmail}
|
||||
institutionInfo={newEmailMatchedInstitution}
|
||||
/>
|
||||
</Cell>
|
||||
</Col>
|
||||
)}
|
||||
|
||||
{!ssoAvailable && (
|
||||
<>
|
||||
<Col md={4}>
|
||||
<Cell>
|
||||
{isInstitutionFieldsVisible ? (
|
||||
<>
|
||||
<div className="form-group mb-2">
|
||||
<DownshiftInput
|
||||
items={roles}
|
||||
inputValue={role}
|
||||
placeholder={t('role')}
|
||||
label={t('role')}
|
||||
setValue={setRole}
|
||||
<CountryInput
|
||||
id="new-email-country-input"
|
||||
setValue={setCountryCode}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-0">
|
||||
<div className="form-group mb-2">
|
||||
<DownshiftInput
|
||||
items={departments}
|
||||
inputValue={department}
|
||||
placeholder={t('department')}
|
||||
label={t('department')}
|
||||
setValue={setDepartment}
|
||||
items={getUniversityItems()}
|
||||
inputValue={university}
|
||||
placeholder={t('university')}
|
||||
label={t('university')}
|
||||
setValue={setUniversity}
|
||||
disabled={!countryCode}
|
||||
/>
|
||||
</div>
|
||||
{isUniversityDirty && (
|
||||
<>
|
||||
<div className="form-group mb-2">
|
||||
<DownshiftInput
|
||||
items={roles}
|
||||
inputValue={role}
|
||||
placeholder={t('role')}
|
||||
label={t('role')}
|
||||
setValue={setRole}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group mb-0">
|
||||
<DownshiftInput
|
||||
items={departments}
|
||||
inputValue={department}
|
||||
placeholder={t('department')}
|
||||
label={t('department')}
|
||||
setValue={setDepartment}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-1">
|
||||
{t('is_email_affiliated')}
|
||||
<br />
|
||||
<Button
|
||||
className="btn-inline-link"
|
||||
onClick={handleShowInstitutionFields}
|
||||
>
|
||||
{t('let_us_know')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-1">
|
||||
{t('is_email_affiliated')}
|
||||
<br />
|
||||
</Cell>
|
||||
</Col>
|
||||
|
||||
<Col md={4}>
|
||||
<Cell className="text-md-right">
|
||||
<Button
|
||||
className="btn-inline-link"
|
||||
onClick={handleShowInstitutionFields}
|
||||
bsSize="small"
|
||||
bsStyle="success"
|
||||
disabled={
|
||||
!isValidEmail(newEmail) || isLoading || state.isLoading
|
||||
}
|
||||
onClick={handleAddNewEmail}
|
||||
>
|
||||
{t('let_us_know')}
|
||||
{t('add_new_email')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Cell>
|
||||
</Col>
|
||||
<Col md={4}>
|
||||
<Cell className="text-md-right">
|
||||
<Button
|
||||
bsSize="small"
|
||||
bsStyle="success"
|
||||
disabled={
|
||||
!isValidEmail(newEmail) || isLoading || state.isLoading
|
||||
}
|
||||
onClick={handleAddNewEmail}
|
||||
>
|
||||
{t('add_new_email')}
|
||||
</Button>
|
||||
{isError && (
|
||||
<div className="text-danger small">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('error_performing_request')}
|
||||
</div>
|
||||
)}
|
||||
</Cell>
|
||||
</Col>
|
||||
{isError && (
|
||||
<div className="text-danger small">
|
||||
<Icon type="exclamation-triangle" fw />{' '}
|
||||
{t('error_performing_request')}
|
||||
</div>
|
||||
)}
|
||||
</Cell>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Row>
|
||||
|
|
|
@ -4,7 +4,10 @@ import { AddEmailInput } from '../../js/features/settings/components/emails/add-
|
|||
export const EmailInput = args => {
|
||||
useFetchMock(fetchMock =>
|
||||
fetchMock.get(/\/institutions\/domains/, [
|
||||
{ hostname: 'autocomplete.edu', id: 123 },
|
||||
{
|
||||
hostname: 'autocomplete.edu',
|
||||
university: { id: 123, name: 'Auto Complete University' },
|
||||
},
|
||||
])
|
||||
)
|
||||
return (
|
||||
|
|
|
@ -41,10 +41,23 @@ const fakeInstitutions = [
|
|||
{ id: 9326, name: 'Unknown', country_code: 'al', departments: [] },
|
||||
]
|
||||
|
||||
const fakeInstitutionDomain = [
|
||||
{
|
||||
university: {
|
||||
id: 1234,
|
||||
ssoEnabled: true,
|
||||
name: 'Auto Complete University',
|
||||
},
|
||||
hostname: 'autocomplete.edu',
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
|
||||
export function defaultSetupMocks(fetchMock) {
|
||||
fetchMock
|
||||
.get(/\/user\/emails/, fakeUsersData, { delay: MOCK_DELAY })
|
||||
.get(/\/institutions\/list/, fakeInstitutions, { delay: MOCK_DELAY })
|
||||
.get(/\/institutions\/domains/, fakeInstitutionDomain)
|
||||
.post(/\/user\/emails\/*/, 200, {
|
||||
delay: MOCK_DELAY,
|
||||
})
|
||||
|
@ -60,5 +73,7 @@ export function setDefaultMeta() {
|
|||
window.metaAttributesCache = window.metaAttributesCache || new Map()
|
||||
window.metaAttributesCache.set('ol-ExposedSettings', {
|
||||
hasAffiliationsFeature: true,
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: 'saml/init',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -169,6 +169,7 @@
|
|||
"this_grants_access_to_features": "This grants you access to <b>__appName__</b> <b>__featureType__</b> features.",
|
||||
"this_grants_access_to_features_2": "This grants you access to <0>__appName__</0> <0>__featureType__</0> features.",
|
||||
"to_add_email_accounts_need_to_be_linked": "To add this email, your <b>__appName__</b> and <b>__institutionName__</b> accounts will need to be linked.",
|
||||
"to_add_email_accounts_need_to_be_linked_2": "To add this email, your <0>__appName__</0> and <0>__institutionName__</0> accounts will need to be linked.",
|
||||
"tried_to_log_in_with_email": "You’ve tried to login with <b>__email__</b>.",
|
||||
"tried_to_register_with_email": "You’ve tried to register with <b>__email__</b>, which is already registered with <b>__appName__</b> as an institutional account.",
|
||||
"log_in_with_email": "Log in with __email__",
|
||||
|
@ -208,6 +209,7 @@
|
|||
"institutional": "Institutional",
|
||||
"doing_this_allow_log_in_through_institution": "Doing this will allow you to log in to <b>__appName__</b> through your institution portal and will reconfirm your institutional email address.",
|
||||
"doing_this_will_verify_affiliation_and_allow_log_in": "Doing this will verify your affiliation with <b>__institutionName__</b> and will allow you to log in to <b>__appName__</b> through your institution.",
|
||||
"doing_this_will_verify_affiliation_and_allow_log_in_2": "Doing this will verify your affiliation with <0>__institutionName__</0> and will allow you to log in to <0>__appName__</0> through your institution.",
|
||||
"email_already_associated_with": "The <b>__email1__</b> email is already associated with the <b>__email2__</b> <b>__appName__</b> account.",
|
||||
"enter_institution_email_to_log_in": "Enter your institutional email to log in through your institution.",
|
||||
"find_out_more_about_institution_login": "Find out more about institutional login",
|
||||
|
|
|
@ -36,17 +36,35 @@ const userEmailData: UserEmailData = {
|
|||
default: false,
|
||||
}
|
||||
|
||||
const institutionDomainData = [
|
||||
{
|
||||
university: {
|
||||
id: 1234,
|
||||
ssoEnabled: true,
|
||||
name: 'Auto Complete University',
|
||||
},
|
||||
hostname: 'autocomplete.edu',
|
||||
confirmed: true,
|
||||
},
|
||||
]
|
||||
|
||||
function resetFetchMock() {
|
||||
fetchMock.reset()
|
||||
fetchMock.get('express:/institutions/domains', [])
|
||||
}
|
||||
|
||||
describe('<EmailsSection />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-ExposedSettings', {
|
||||
hasAffiliationsFeature: true,
|
||||
hasSamlFeature: true,
|
||||
samlInitPath: 'saml/init',
|
||||
})
|
||||
fetchMock.reset()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
})
|
||||
|
||||
it('renders "add another email" button', function () {
|
||||
|
@ -85,7 +103,7 @@ describe('<EmailsSection />', function () {
|
|||
render(<EmailsSection />)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [userEmailData])
|
||||
.post('/user/emails', 200)
|
||||
|
@ -125,7 +143,7 @@ describe('<EmailsSection />', function () {
|
|||
render(<EmailsSection />)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
fetchMock
|
||||
.get('/user/emails?ensureAffiliation=true', [])
|
||||
.post('/user/emails', 500)
|
||||
|
@ -158,13 +176,33 @@ describe('<EmailsSection />', function () {
|
|||
expect(submitBtn.disabled).to.be.false
|
||||
})
|
||||
|
||||
it('can link email address to an existing SSO institution', async function () {
|
||||
fetchMock.reset()
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
fetchMock.get('express:/institutions/domains', institutionDomainData)
|
||||
render(<EmailsSection />)
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
name: /add another email/i,
|
||||
})
|
||||
)
|
||||
|
||||
const input = screen.getByLabelText(/email/i)
|
||||
fireEvent.change(input, {
|
||||
target: { value: 'user@autocomplete.edu' },
|
||||
})
|
||||
|
||||
await screen.findByRole('link', { name: 'Link Accounts and Add Email' })
|
||||
})
|
||||
|
||||
it('adds new email address with existing institution', async function () {
|
||||
const country = 'Germany'
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
|
@ -203,7 +241,7 @@ describe('<EmailsSection />', function () {
|
|||
expect(universityInput.disabled).to.be.false
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
|
||||
// Select the university from dropdown
|
||||
await userEvent.click(universityInput)
|
||||
|
@ -251,7 +289,7 @@ describe('<EmailsSection />', function () {
|
|||
render(<EmailsSection />)
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
|
||||
await userEvent.click(
|
||||
screen.getByRole('button', {
|
||||
|
@ -290,7 +328,7 @@ describe('<EmailsSection />', function () {
|
|||
expect(universityInput.disabled).to.be.false
|
||||
|
||||
await fetchMock.flush(true)
|
||||
fetchMock.reset()
|
||||
resetFetchMock()
|
||||
|
||||
// Enter the university manually
|
||||
await userEvent.type(universityInput, newUniversity)
|
||||
|
|
Loading…
Reference in a new issue