mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #13164 from overleaf/msm-email-limit
[web] limit user email addresses to 10 GitOrigin-RevId: 038214cc921d86a407391e6c82fa9cd16a7f9646
This commit is contained in:
parent
d88e248178
commit
51223315e4
13 changed files with 133 additions and 5 deletions
|
@ -1,3 +1,4 @@
|
|||
const Settings = require('@overleaf/settings')
|
||||
const logger = require('@overleaf/logger')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const UserGetter = require('./UserGetter')
|
||||
|
@ -32,7 +33,14 @@ async function add(req, res, next) {
|
|||
if (!email) {
|
||||
return res.sendStatus(422)
|
||||
}
|
||||
const user = await UserGetter.promises.getUser(userId, { email: 1 })
|
||||
const user = await UserGetter.promises.getUser(userId, {
|
||||
email: 1,
|
||||
'emails.email': 1,
|
||||
})
|
||||
|
||||
if (user.emails.length >= Settings.emailAddressLimit) {
|
||||
return res.status(422).json({ message: 'secondary email limit exceeded' })
|
||||
}
|
||||
|
||||
const affiliationOptions = {
|
||||
university: req.body.university,
|
||||
|
|
|
@ -125,6 +125,7 @@ async function settingsPage(req, res) {
|
|||
projectSyncSuccessMessage,
|
||||
showPersonalAccessToken,
|
||||
personalAccessTokens,
|
||||
emailAddressLimit: Settings.emailAddressLimit,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ block append meta
|
|||
meta(name="ol-projectSyncSuccessMessage", content=projectSyncSuccessMessage)
|
||||
meta(name="ol-showPersonalAccessToken", data-type="boolean" content=showPersonalAccessToken)
|
||||
meta(name="ol-personalAccessTokens", data-type="json" content=personalAccessTokens)
|
||||
meta(name="ol-emailAddressLimit", data-type="json", content=emailAddressLimit)
|
||||
|
||||
block content
|
||||
main.content.content-alt#settings-page-root
|
||||
|
|
|
@ -684,6 +684,8 @@ module.exports = {
|
|||
emailConfirmationDisabled:
|
||||
process.env.EMAIL_CONFIRMATION_DISABLED === 'true' || false,
|
||||
|
||||
emailAddressLimit: intFromEnv('EMAIL_ADDRESS_LIMIT', 10),
|
||||
|
||||
enabledServices: (process.env.ENABLED_SERVICES || 'web,api')
|
||||
.split(',')
|
||||
.map(s => s.trim()),
|
||||
|
|
|
@ -257,6 +257,7 @@
|
|||
"educational_discount_for_groups_of_x_or_more": "",
|
||||
"educational_percent_discount_applied": "",
|
||||
"email": "",
|
||||
"email_limit_reached": "",
|
||||
"email_or_password_wrong_try_again": "",
|
||||
"emails_and_affiliations_explanation": "",
|
||||
"emails_and_affiliations_title": "",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { Col } from 'react-bootstrap'
|
||||
import Cell from './cell'
|
||||
import Layout from './add-email/layout'
|
||||
|
@ -15,6 +15,7 @@ import { postJSON } from '../../../../infrastructure/fetch-json'
|
|||
import { University } from '../../../../../../types/university'
|
||||
import { CountryCode } from '../../data/countries-list'
|
||||
import { isValidEmail } from '../../../../shared/utils/email'
|
||||
import getMeta from '../../../../utils/meta'
|
||||
|
||||
function AddEmail() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -38,6 +39,8 @@ function AddEmail() {
|
|||
getEmails,
|
||||
} = useUserEmailsContext()
|
||||
|
||||
const emailAddressLimit = getMeta('ol-emailAddressLimit', 10)
|
||||
|
||||
useEffect(() => {
|
||||
setUserEmailsContextLoading(isLoading)
|
||||
}, [setUserEmailsContextLoading, isLoading])
|
||||
|
@ -98,9 +101,21 @@ function AddEmail() {
|
|||
if (!isFormVisible) {
|
||||
return (
|
||||
<Layout isError={isError} error={error}>
|
||||
<Col md={4}>
|
||||
<Col md={12}>
|
||||
<Cell>
|
||||
{state.data.emailCount >= emailAddressLimit ? (
|
||||
<span className="small">
|
||||
<Trans
|
||||
i18nKey="email_limit_reached"
|
||||
values={{
|
||||
emailAddressLimit,
|
||||
}}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<AddAnotherEmailBtn onClick={handleShowAddEmailForm} />
|
||||
)}
|
||||
</Cell>
|
||||
</Col>
|
||||
</Layout>
|
||||
|
|
|
@ -65,6 +65,7 @@ export type State = {
|
|||
isLoading: boolean
|
||||
data: {
|
||||
byId: NormalizedObject<UserEmailData>
|
||||
emailCount: number
|
||||
linkedInstitutionIds: NonNullable<UserEmailData['samlProviderId']>[]
|
||||
emailAffiliationBeingEdited: Nullable<UserEmailData['email']>
|
||||
}
|
||||
|
@ -82,6 +83,7 @@ const setData = (state: State, action: ActionSetData) => {
|
|||
const normalized = normalize<UserEmailData>(action.payload, {
|
||||
idAttribute: 'email',
|
||||
})
|
||||
const emailCount = action.payload.length
|
||||
const byId = normalized || {}
|
||||
const linkedInstitutionIds = action.payload
|
||||
.filter(email => Boolean(email.samlProviderId))
|
||||
|
@ -94,6 +96,7 @@ const setData = (state: State, action: ActionSetData) => {
|
|||
data: {
|
||||
...initialState.data,
|
||||
byId,
|
||||
emailCount,
|
||||
linkedInstitutionIds,
|
||||
},
|
||||
}
|
||||
|
@ -132,6 +135,7 @@ const deleteEmailAction = (state: State, action: ActionDeleteEmail) => {
|
|||
...state,
|
||||
data: {
|
||||
...state.data,
|
||||
emailCount: state.data.emailCount - 1,
|
||||
byId,
|
||||
},
|
||||
}
|
||||
|
@ -191,6 +195,7 @@ const initialState: State = {
|
|||
isLoading: false,
|
||||
data: {
|
||||
byId: {},
|
||||
emailCount: 0,
|
||||
linkedInstitutionIds: [],
|
||||
emailAffiliationBeingEdited: null,
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
defaultSetupMocks,
|
||||
reconfirmationSetupMocks,
|
||||
errorsMocks,
|
||||
emailLimitSetupMocks,
|
||||
} from './helpers/emails'
|
||||
|
||||
export const EmailsList = args => {
|
||||
|
@ -15,6 +16,13 @@ export const EmailsList = args => {
|
|||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export const EmailLimitList = args => {
|
||||
useFetchMock(emailLimitSetupMocks)
|
||||
setDefaultMeta()
|
||||
|
||||
return <EmailsSection {...args} />
|
||||
}
|
||||
|
||||
export const ReconfirmationEmailsList = args => {
|
||||
useFetchMock(reconfirmationSetupMocks)
|
||||
setReconfirmationMeta()
|
||||
|
|
|
@ -176,6 +176,18 @@ export function reconfirmationSetupMocks(fetchMock) {
|
|||
})
|
||||
}
|
||||
|
||||
export function emailLimitSetupMocks(fetchMock) {
|
||||
const userData = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
userData.push({ email: `example${i}@overleaf.com` })
|
||||
}
|
||||
defaultSetupMocks(fetchMock)
|
||||
fetchMock.get(/\/user\/emails/, userData, {
|
||||
delay: MOCK_DELAY,
|
||||
overwriteRoutes: true,
|
||||
})
|
||||
}
|
||||
|
||||
export function errorsMocks(fetchMock) {
|
||||
fetchMock
|
||||
.get(/\/user\/emails/, fakeUsersData, { delay: MOCK_DELAY })
|
||||
|
|
|
@ -430,6 +430,7 @@
|
|||
"email_already_registered_secondary": "This email is already registered as a secondary email",
|
||||
"email_already_registered_sso": "This email is already registered. Please log in to your account another way and link your account to the new provider via your account settings.",
|
||||
"email_does_not_belong_to_university": "We don’t recognize that domain as being affiliated with your university. Please contact us to add the affiliation.",
|
||||
"email_limit_reached": "You can have a maximum of <0>__emailAddressLimit__ email addresses</0> on this account. To add another email address, please delete an existing one.",
|
||||
"email_link_expired": "Email link expired, please request a new one.",
|
||||
"email_or_password_wrong_try_again": "Your email or password is incorrect. Please try again.",
|
||||
"email_or_password_wrong_try_again_or_reset": "Your email or password is incorrect. Please try again, or <0>set or reset your password</0>.",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { expect } from 'chai'
|
|||
import fetchMock from 'fetch-mock'
|
||||
import { UserEmailData } from '../../../../../../types/user-email'
|
||||
import { Affiliation } from '../../../../../../types/affiliation'
|
||||
import withMarkup from '../../../../helpers/with-markup'
|
||||
|
||||
const userEmailData: UserEmailData & { affiliation: Affiliation } = {
|
||||
affiliation: {
|
||||
|
@ -149,6 +150,23 @@ describe('<EmailsSection />', function () {
|
|||
screen.getByRole('button', { name: /add new email/i })
|
||||
})
|
||||
|
||||
it('prevent users from adding new emails when the limit is reached', async function () {
|
||||
const emails = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
emails.push({ email: `bar${i}@overleaf.com` })
|
||||
}
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', emails)
|
||||
render(<EmailsSection />)
|
||||
|
||||
const findByTextWithMarkup = withMarkup(screen.findByText)
|
||||
await findByTextWithMarkup(
|
||||
'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('button', { name: /add another email/i })).to.not
|
||||
.exist
|
||||
})
|
||||
|
||||
it('adds new email address', async function () {
|
||||
fetchMock.get('/user/emails?ensureAffiliation=true', [])
|
||||
render(<EmailsSection />)
|
||||
|
|
30
services/web/test/frontend/helpers/with-markup.ts
Normal file
30
services/web/test/frontend/helpers/with-markup.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { MatcherFunction } from '@testing-library/react'
|
||||
|
||||
type Query = (f: MatcherFunction) => Element | Promise<Element>
|
||||
|
||||
/*
|
||||
Utility function to run testing-library queries over nodes that contain html tags, as in
|
||||
`<p>this includes some <strong>bold</strong> text</p>`.
|
||||
|
||||
Usage:
|
||||
const getByTextWithMarkup = withMarkup(screen.getByText)
|
||||
getByTextWithMarkup('this includes some bold text')
|
||||
|
||||
const findByTextWithMarkup = withMarkup(screen.findByText)
|
||||
await findByTextWithMarkup('this includes some bold text')
|
||||
*/
|
||||
const withMarkup =
|
||||
(query: Query) =>
|
||||
(text: string): Element | Promise<Element> =>
|
||||
query((content: string, node: Element | null) => {
|
||||
if (!node) {
|
||||
return false
|
||||
}
|
||||
const hasText = (node: Element) => node.textContent === text
|
||||
const childrenDontHaveText = Array.from(node.children).every(
|
||||
child => !hasText(child as HTMLElement)
|
||||
)
|
||||
return hasText(node) && childrenDontHaveText
|
||||
})
|
||||
|
||||
export default withMarkup
|
|
@ -14,7 +14,11 @@ describe('UserEmailsController', function () {
|
|||
this.req.sessionID = Math.random().toString()
|
||||
this.res = new MockResponse()
|
||||
this.next = sinon.stub()
|
||||
this.user = { _id: 'mock-user-id', email: 'example@overleaf.com' }
|
||||
this.user = {
|
||||
_id: 'mock-user-id',
|
||||
email: 'example@overleaf.com',
|
||||
emails: {},
|
||||
}
|
||||
|
||||
this.UserGetter = {
|
||||
getUserFullEmails: sinon.stub(),
|
||||
|
@ -234,6 +238,28 @@ describe('UserEmailsController', function () {
|
|||
})
|
||||
this.UserEmailsController.add(this.req, this.res, this.next)
|
||||
})
|
||||
|
||||
it('should fail to add new emails when the limit has been reached', function (done) {
|
||||
this.user.emails = []
|
||||
for (let i = 0; i < 10; i++) {
|
||||
this.user.emails.push({ email: `example${i}@overleaf.com` })
|
||||
}
|
||||
this.UserEmailsController.add(
|
||||
this.req,
|
||||
{
|
||||
status: code => {
|
||||
expect(code).to.equal(422)
|
||||
return {
|
||||
json: error => {
|
||||
expect(error.message).to.equal('secondary email limit exceeded')
|
||||
done()
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
this.next
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('remove', function () {
|
||||
|
|
Loading…
Reference in a new issue