Просмотр исходного кода

Merge pull request #19141 from overleaf/ii-add-email-account-settings

[web] Reset recaptcha every time

GitOrigin-RevId: 6db3571ad6dad3611c748a2d92dce47002a16a77
ilkin-overleaf 2 лет назад
Родитель
Сommit
687b56f18e

+ 2 - 2
services/web/frontend/js/features/settings/components/emails/add-email.tsx

@@ -151,7 +151,7 @@ function AddEmail() {
     return (
       <form>
         <Layout isError={isError} error={error}>
-          <ReCaptcha2 page="addEmail" ref={recaptchaRef} />
+          <ReCaptcha2 page="addEmail" recaptchaRef={recaptchaRef} />
           <OLCol lg={8}>
             <Cell>
               {InputComponent}
@@ -176,7 +176,7 @@ function AddEmail() {
   return (
     <form>
       <Layout isError={isError} error={error}>
-        <ReCaptcha2 page="addEmail" ref={recaptchaRef} />
+        <ReCaptcha2 page="addEmail" recaptchaRef={recaptchaRef} />
         <OLCol lg={8}>
           <Cell>
             {InputComponent}

+ 1 - 1
services/web/frontend/js/features/settings/components/emails/add-secondary-email-prompt.tsx

@@ -80,7 +80,7 @@ export function AddSecondaryEmailPrompt() {
     <>
       <Interstitial showLogo title={t('add_a_recovery_email_address')}>
         <form className="add-secondary-email" onSubmit={handleSubmit}>
-          <ReCaptcha2 page="addEmail" ref={recaptchaRef} />
+          <ReCaptcha2 page="addEmail" recaptchaRef={recaptchaRef} />
           <p>{t('keep_your_account_safe_add_another_email')}</p>
 
           <EmailInput

+ 12 - 8
services/web/frontend/js/shared/components/recaptcha-2.tsx

@@ -1,14 +1,18 @@
-import { forwardRef } from 'react'
 import ReCAPTCHA from 'react-google-recaptcha'
 import getMeta from '@/utils/meta'
 import { ExposedSettings } from '../../../../types/exposed-settings'
 
-type Page = keyof ExposedSettings['recaptchaDisabled']
+interface ReCaptcha2Props
+  extends Pick<React.ComponentProps<typeof ReCAPTCHA>, 'onChange'> {
+  page: keyof ExposedSettings['recaptchaDisabled']
+  recaptchaRef: React.LegacyRef<ReCAPTCHA>
+}
 
-export const ReCaptcha2 = forwardRef<
-  ReCAPTCHA,
-  { page: Page; onChange?: (token: string | null) => void }
->(function ReCaptcha2({ page: site, onChange }, ref) {
+export function ReCaptcha2({
+  page: site,
+  onChange,
+  recaptchaRef,
+}: ReCaptcha2Props) {
   const { recaptchaSiteKey, recaptchaDisabled } = getMeta('ol-ExposedSettings')
 
   if (!recaptchaSiteKey) {
@@ -19,11 +23,11 @@ export const ReCaptcha2 = forwardRef<
   }
   return (
     <ReCAPTCHA
-      ref={ref}
+      ref={recaptchaRef}
       size="invisible"
       sitekey={recaptchaSiteKey}
       onChange={onChange}
       badge="inline"
     />
   )
-})
+}

+ 10 - 3
services/web/frontend/js/shared/hooks/use-recaptcha.ts

@@ -1,13 +1,20 @@
-import { LegacyRef, createRef } from 'react'
+import { useRef } from 'react'
 import ReCAPTCHA from 'react-google-recaptcha'
 
 export const useRecaptcha = () => {
-  const ref: LegacyRef<ReCAPTCHA> = createRef<ReCAPTCHA>()
-  const getReCaptchaToken = async (): Promise<string | null> => {
+  const ref = useRef<ReCAPTCHA | null>(null)
+
+  const getReCaptchaToken = async (): Promise<
+    ReturnType<ReCAPTCHA['executeAsync']>
+  > => {
     if (!ref.current) {
       return null
     }
+    // Reset the reCAPTCHA before each submission.
+    // The reCAPTCHA token is meant to be used once per validation
+    ref.current.reset()
     return await ref.current.executeAsync()
   }
+
   return { ref, getReCaptchaToken }
 }

+ 37 - 0
services/web/test/frontend/shared/hooks/use-recaptcha.spec.tsx

@@ -0,0 +1,37 @@
+import { useRecaptcha } from '@/shared/hooks/use-recaptcha'
+import * as ReactGoogleRecaptcha from 'react-google-recaptcha'
+
+const ReCaptcha = () => {
+  const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
+
+  const handleClick = async () => {
+    await getReCaptchaToken()
+  }
+
+  return (
+    <>
+      <ReactGoogleRecaptcha.ReCAPTCHA
+        ref={recaptchaRef}
+        size="invisible"
+        sitekey="123456"
+        badge="inline"
+      />
+      <button onClick={handleClick}>Click</button>
+    </>
+  )
+}
+
+describe('useRecaptcha', function () {
+  it('should reset the captcha', function () {
+    cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'reset').as('resetSpy')
+    cy.spy(ReactGoogleRecaptcha.ReCAPTCHA.prototype, 'executeAsync').as(
+      'executeAsyncSpy'
+    )
+
+    cy.mount(<ReCaptcha />)
+
+    cy.findByRole('button', { name: /click/i }).click()
+    cy.get('@resetSpy').should('have.been.calledOnce')
+    cy.get('@executeAsyncSpy').should('have.been.calledOnce')
+  })
+})