Merge pull request #13372 from overleaf/mj-captcha-add-email

[web] Add recaptcha to add-email

GitOrigin-RevId: 0540e0dbc3103dcaac87dd7fabeedbc5892c371c
This commit is contained in:
Mathias Jakobsen 2023-06-26 10:32:49 +02:00 committed by Copybot
parent 64ca8ce094
commit af76768eb7
14 changed files with 143 additions and 14 deletions

66
package-lock.json generated
View file

@ -12079,6 +12079,15 @@
"@types/react": "*"
}
},
"node_modules/@types/react-google-recaptcha": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-linkify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.0.tgz",
@ -32630,6 +32639,18 @@
"node": ">=0.10.0"
}
},
"node_modules/react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
"dependencies": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-bootstrap": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.33.1.tgz",
@ -32802,6 +32823,18 @@
"react": ">=16.13.1"
}
},
"node_modules/react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
"dependencies": {
"prop-types": "^15.5.0",
"react-async-script": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.4.1"
}
},
"node_modules/react-i18next": {
"version": "11.18.6",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",
@ -41330,6 +41363,7 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-error-boundary": "^2.3.1",
"react-google-recaptcha": "^3.1.0",
"react-i18next": "^11.18.6",
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
@ -41373,6 +41407,7 @@
"@types/react-bootstrap": "^0.32.29",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.13",
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-linkify": "^1.0.0",
"@types/recurly__recurly-js": "^4.22.0",
"@types/sinon-chai": "^3.2.8",
@ -50187,7 +50222,8 @@
"@types/react-bootstrap": "^0.32.29",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.13",
"@types/react-linkify": "1.0.0",
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-linkify": "^1.0.0",
"@types/recurly__recurly-js": "^4.22.0",
"@types/sinon-chai": "^3.2.8",
"@types/uuid": "^8.3.4",
@ -50353,6 +50389,7 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-error-boundary": "^2.3.1",
"react-google-recaptcha": "^3.1.0",
"react-i18next": "^11.18.6",
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
@ -54056,6 +54093,15 @@
"@types/react": "*"
}
},
"@types/react-google-recaptcha": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@types/react-google-recaptcha/-/react-google-recaptcha-2.1.5.tgz",
"integrity": "sha512-iWTjmVttlNgp0teyh7eBXqNOQzVq2RWNiFROWjraOptRnb1OcHJehQnji0sjqIRAk9K0z8stjyhU+OLpPb0N6w==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-linkify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/react-linkify/-/react-linkify-1.0.0.tgz",
@ -70114,6 +70160,15 @@
"object-assign": "^4.1.1"
}
},
"react-async-script": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/react-async-script/-/react-async-script-1.2.0.tgz",
"integrity": "sha512-bCpkbm9JiAuMGhkqoAiC0lLkb40DJ0HOEJIku+9JDjxX3Rcs+ztEOG13wbrOskt3n2DTrjshhaQ/iay+SnGg5Q==",
"requires": {
"hoist-non-react-statics": "^3.3.0",
"prop-types": "^15.5.0"
}
},
"react-bootstrap": {
"version": "0.33.1",
"resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.33.1.tgz",
@ -70248,6 +70303,15 @@
"@babel/runtime": "^7.11.2"
}
},
"react-google-recaptcha": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/react-google-recaptcha/-/react-google-recaptcha-3.1.0.tgz",
"integrity": "sha512-cYW2/DWas8nEKZGD7SCu9BSuVz8iOcOLHChHyi7upUuVhkpkhYG/6N3KDiTQ3XAiZ2UAZkfvYKMfAHOzBOcGEg==",
"requires": {
"prop-types": "^15.5.0",
"react-async-script": "^1.2.0"
}
},
"react-i18next": {
"version": "11.18.6",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-11.18.6.tgz",

View file

@ -393,10 +393,9 @@ module.exports = function (webRouter, privateApiRouter, publicApiRouter) {
emailConfirmationDisabled: Settings.emailConfirmationDisabled,
maxEntitiesPerProject: Settings.maxEntitiesPerProject,
maxUploadSize: Settings.maxUploadSize,
recaptchaSiteKeyV3:
Settings.recaptcha != null ? Settings.recaptcha.siteKeyV3 : undefined,
recaptchaDisabled:
Settings.recaptcha != null ? Settings.recaptcha.disabled : undefined,
recaptchaSiteKey: Settings.recaptcha?.siteKey,
recaptchaSiteKeyV3: Settings.recaptcha?.siteKeyV3,
recaptchaDisabled: Settings.recaptcha?.disabled,
textExtensions: Settings.textExtensions,
validRootDocExtensions: Settings.validRootDocExtensions,
sentryAllowedOriginRegex: Settings.sentry.allowedOriginRegex,

View file

@ -349,6 +349,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
'/user/emails',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit(rateLimiters.addEmail),
CaptchaMiddleware.validateCaptcha('addEmail'),
UserEmailsController.add
)
webRouter.post(

View file

@ -642,6 +642,7 @@ module.exports = {
login: true,
passwordReset: true,
register: true,
addEmail: true,
},
},

View file

@ -16,6 +16,8 @@ import { University } from '../../../../../../types/university'
import { CountryCode } from '../../data/countries-list'
import { isValidEmail } from '../../../../shared/utils/email'
import getMeta from '../../../../utils/meta'
import { ReCaptcha2 } from '../../../../shared/components/recaptcha-2'
import { useRecaptcha } from '../../../../shared/hooks/use-recaptcha'
function AddEmail() {
const { t } = useTranslation()
@ -40,6 +42,7 @@ function AddEmail() {
} = useUserEmailsContext()
const emailAddressLimit = getMeta('ol-emailAddressLimit', 10)
const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
useEffect(() => {
setUserEmailsContextLoading(isLoading)
@ -84,13 +87,17 @@ function AddEmail() {
}
runAsync(
postJSON('/user/emails', {
(async () => {
const token = await getReCaptchaToken()
await postJSON('/user/emails', {
body: {
email: newEmail,
...knownUniversityData,
...unknownUniversityData,
'g-recaptcha-response': token,
},
})
})()
)
.then(() => {
getEmails()
@ -137,6 +144,7 @@ function AddEmail() {
if (!isValidEmail(newEmail)) {
return (
<Layout isError={isError} error={error}>
<ReCaptcha2 page="addEmail" ref={recaptchaRef} />
<form>
<Col md={8}>
<Cell>
@ -161,6 +169,7 @@ function AddEmail() {
return (
<Layout isError={isError} error={error}>
<ReCaptcha2 page="addEmail" ref={recaptchaRef} />
<form>
<Col md={8}>
<Cell>

View file

@ -8,6 +8,10 @@ import ReactDOM from 'react-dom'
import SettingsPageRoot from '../../features/settings/components/root.tsx'
const element = document.getElementById('settings-page-root')
// For react-google-recaptcha
window.recaptchaOptions = {
enterprise: true,
}
if (element) {
ReactDOM.render(<SettingsPageRoot />, element)
}

View file

@ -0,0 +1,27 @@
import { forwardRef } from 'react'
import ReCAPTCHA from 'react-google-recaptcha'
const siteKey = window.ExposedSettings.recaptchaSiteKey
const recaptchaDisabled = window.ExposedSettings.recaptchaDisabled
type Page = keyof typeof recaptchaDisabled
export const ReCaptcha2 = forwardRef<
ReCAPTCHA,
{ page: Page; onChange?: (token: string | null) => void }
>(function ReCaptcha2({ page: site, onChange }, ref) {
if (!siteKey) {
return null
}
if (site && recaptchaDisabled[site]) {
return null
}
return (
<ReCAPTCHA
ref={ref}
size="invisible"
sitekey={siteKey}
onChange={onChange}
badge="inline"
/>
)
})

View file

@ -0,0 +1,13 @@
import { LegacyRef, createRef } from 'react'
import ReCAPTCHA from 'react-google-recaptcha'
export const useRecaptcha = () => {
const ref: LegacyRef<ReCAPTCHA> = createRef<ReCAPTCHA>()
const getReCaptchaToken = async (): Promise<string | null> => {
if (!ref.current) {
return null
}
return await ref.current.executeAsync()
}
return { ref, getReCaptchaToken }
}

View file

@ -149,6 +149,7 @@ const initialize = () => {
login: true,
passwordReset: true,
register: true,
addEmail: true,
},
sentryAllowedOriginRegex: '',
siteUrl: 'http://localhost',

View file

@ -229,6 +229,7 @@
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^17.0.2",
"react-error-boundary": "^2.3.1",
"react-google-recaptcha": "^3.1.0",
"react-i18next": "^11.18.6",
"react-linkify": "^1.0.0-alpha",
"react-refresh": "^0.14.0",
@ -272,6 +273,7 @@
"@types/react-bootstrap": "^0.32.29",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^17.0.13",
"@types/react-google-recaptcha": "^2.1.5",
"@types/react-linkify": "^1.0.0",
"@types/recurly__recurly-js": "^4.22.0",
"@types/sinon-chai": "^3.2.8",

View file

@ -251,6 +251,7 @@ module.exports = {
login: false,
passwordReset: true,
register: true,
addEmail: true,
},
},

View file

@ -349,7 +349,7 @@ describe('<EmailsSection />', function () {
const [[, request]] = fetchMock.calls(/\/user\/emails/)
expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.equal({
expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
email: userEmailData.email,
university: {
id: userEmailData.affiliation?.institution.id,
@ -494,7 +494,7 @@ describe('<EmailsSection />', function () {
const [[, request]] = fetchMock.calls(/\/user\/emails/)
expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.equal({
expect(JSON.parse(request?.body?.toString() || '{}')).to.deep.include({
email: userEmailData.email,
university: {
name: newUniversity,

View file

@ -27,8 +27,10 @@ export type ExposedSettings = {
login: boolean
passwordReset: boolean
register: boolean
addEmail: boolean
}
recaptchaSiteKeyV3?: string
recaptchaSiteKey?: string
samlInitPath?: string
sentryAllowedOriginRegex: string
sentryDsn?: string

View file

@ -35,5 +35,10 @@ declare global {
crypto: {
randomUUID: () => string
}
// For react-google-recaptcha
recaptchaOptions?: {
enterprise?: boolean
useRecaptchaNet?: boolean
}
}
}