mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #13372 from overleaf/mj-captcha-add-email
[web] Add recaptcha to add-email GitOrigin-RevId: 0540e0dbc3103dcaac87dd7fabeedbc5892c371c
This commit is contained in:
parent
64ca8ce094
commit
af76768eb7
14 changed files with 143 additions and 14 deletions
66
package-lock.json
generated
66
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -349,6 +349,7 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
'/user/emails',
|
||||
AuthenticationController.requireLogin(),
|
||||
RateLimiterMiddleware.rateLimit(rateLimiters.addEmail),
|
||||
CaptchaMiddleware.validateCaptcha('addEmail'),
|
||||
UserEmailsController.add
|
||||
)
|
||||
webRouter.post(
|
||||
|
|
|
@ -642,6 +642,7 @@ module.exports = {
|
|||
login: true,
|
||||
passwordReset: true,
|
||||
register: true,
|
||||
addEmail: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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', {
|
||||
body: {
|
||||
email: newEmail,
|
||||
...knownUniversityData,
|
||||
...unknownUniversityData,
|
||||
},
|
||||
})
|
||||
(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>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
27
services/web/frontend/js/shared/components/recaptcha-2.tsx
Normal file
27
services/web/frontend/js/shared/components/recaptcha-2.tsx
Normal 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"
|
||||
/>
|
||||
)
|
||||
})
|
13
services/web/frontend/js/shared/hooks/use-recaptcha.ts
Normal file
13
services/web/frontend/js/shared/hooks/use-recaptcha.ts
Normal 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 }
|
||||
}
|
|
@ -149,6 +149,7 @@ const initialize = () => {
|
|||
login: true,
|
||||
passwordReset: true,
|
||||
register: true,
|
||||
addEmail: true,
|
||||
},
|
||||
sentryAllowedOriginRegex: '',
|
||||
siteUrl: 'http://localhost',
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -251,6 +251,7 @@ module.exports = {
|
|||
login: false,
|
||||
passwordReset: true,
|
||||
register: true,
|
||||
addEmail: true,
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -35,5 +35,10 @@ declare global {
|
|||
crypto: {
|
||||
randomUUID: () => string
|
||||
}
|
||||
// For react-google-recaptcha
|
||||
recaptchaOptions?: {
|
||||
enterprise?: boolean
|
||||
useRecaptchaNet?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue