mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 22:23:42 +01:00
Add reCAPTCHA enterprise support and refactor invite flow (#35134)
GitOrigin-RevId: 4a4ebbd9e5ec55c3532a0cd3fa6c0feda3e2733c
This commit is contained in:
committed by
Copybot
parent
5a602e1b1d
commit
da7a2f0e72
@@ -0,0 +1,86 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import getMeta from '@/utils/meta'
|
||||
import { withCaptchaRetry, type FetchMethod } from './with-retry'
|
||||
import { OLModal, OLModalBody } from '@/shared/components/ol/ol-modal'
|
||||
import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
|
||||
import OLSpinner from '@/shared/components/ol/ol-spinner'
|
||||
|
||||
const { recaptchaEnterpriseSiteKey } = getMeta('ol-ExposedSettings')
|
||||
|
||||
function loadRecaptchaEnterprise(siteKey: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (window.grecaptcha?.enterprise) {
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
const script = document.createElement('script')
|
||||
script.src = `https://www.google.com/recaptcha/enterprise.js?render=${siteKey}`
|
||||
script.async = true
|
||||
script.onload = () => {
|
||||
window.grecaptcha!.enterprise!.ready(() => resolve())
|
||||
}
|
||||
script.onerror = () => reject(new Error('Failed to load reCAPTCHA'))
|
||||
document.head.appendChild(script)
|
||||
})
|
||||
}
|
||||
|
||||
async function getEnterpriseToken(
|
||||
siteKey: string,
|
||||
action: string
|
||||
): Promise<string> {
|
||||
await loadRecaptchaEnterprise(siteKey)
|
||||
return window.grecaptcha!.enterprise!.execute(siteKey, { action })
|
||||
}
|
||||
|
||||
export const useFetchWithRecaptcha = <R, M extends FetchMethod<R>>(
|
||||
method: M,
|
||||
{ action }: { action: string }
|
||||
) => {
|
||||
const [isRetrying, setIsRetrying] = useState(false)
|
||||
|
||||
const run = useCallback(
|
||||
async (...args: Parameters<M>): Promise<R> => {
|
||||
const result = await withCaptchaRetry<R, M>(method, ...args)
|
||||
if (result.status === 'ok') return result.data
|
||||
|
||||
// Without the ability to retry, we throw the original error
|
||||
if (!recaptchaEnterpriseSiteKey) throw result.error
|
||||
|
||||
setIsRetrying(true)
|
||||
|
||||
try {
|
||||
const token = await getEnterpriseToken(
|
||||
recaptchaEnterpriseSiteKey,
|
||||
action
|
||||
)
|
||||
const data = await result.retry(token)
|
||||
return data
|
||||
} finally {
|
||||
setIsRetrying(false)
|
||||
}
|
||||
},
|
||||
[method, action]
|
||||
)
|
||||
|
||||
const renderRecaptcha = useCallback(
|
||||
() =>
|
||||
isRetrying ? (
|
||||
<OLModal
|
||||
show
|
||||
size="sm"
|
||||
animation
|
||||
backdrop
|
||||
className="m-5"
|
||||
onHide={() => {}}
|
||||
>
|
||||
<OLSpinner size="lg" className="m-auto mt-3" />
|
||||
<OLModalBody>
|
||||
<RecaptchaConditions />
|
||||
</OLModalBody>
|
||||
</OLModal>
|
||||
) : null,
|
||||
[isRetrying]
|
||||
)
|
||||
|
||||
return { run, renderRecaptcha }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import {
|
||||
deleteJSON,
|
||||
getJSON,
|
||||
postJSON,
|
||||
putJSON,
|
||||
FetchError,
|
||||
} from '@/infrastructure/fetch-json'
|
||||
|
||||
const isCloudArmorReject = (response: Response) => {
|
||||
// 'x-served-by' is only sent by our backend, so if it's missing and we have a 403,
|
||||
// it's likely a Cloud Armor block page
|
||||
return response.status === 403 && !response.headers.get('x-served-by')
|
||||
}
|
||||
|
||||
export type FetchMethod<R> =
|
||||
| typeof getJSON<R>
|
||||
| typeof postJSON<R>
|
||||
| typeof deleteJSON<R>
|
||||
| typeof putJSON<R>
|
||||
|
||||
/**
|
||||
* Wraps a fetch method so it can be retried with a reCAPTCHA token if it fails due to Cloud Armor.
|
||||
*/
|
||||
export const withCaptchaRetry = async <R, M extends FetchMethod<R>>(
|
||||
method: M,
|
||||
...fetchArgs: Parameters<M>
|
||||
): Promise<
|
||||
| { status: 'ok'; data: R }
|
||||
| { status: 'retry'; retry: (token: string) => Promise<R>; error: FetchError }
|
||||
> => {
|
||||
try {
|
||||
// @ts-expect-error - typescript does not infer the args type correctly here
|
||||
const data = (await method(...fetchArgs)) satisfies R
|
||||
return { status: 'ok', data }
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof FetchError &&
|
||||
error.response &&
|
||||
isCloudArmorReject(error.response)
|
||||
) {
|
||||
const retry = async (token: string) => {
|
||||
const [path, options, ...rest] = fetchArgs
|
||||
const newOptions = {
|
||||
...options,
|
||||
headers: { ...(options?.headers || {}), 'x-recaptcha-token': token },
|
||||
}
|
||||
return await method(path, newOptions, ...rest)
|
||||
}
|
||||
return { status: 'retry', retry, error }
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useRef } from 'react'
|
||||
import ReCAPTCHA from 'react-google-recaptcha'
|
||||
import type ReCAPTCHA from 'react-google-recaptcha'
|
||||
|
||||
export const useRecaptcha = () => {
|
||||
const ref = useRef<ReCAPTCHA | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user