mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #7424 from overleaf/jpa-captcha-error-handling
[web] double down on error handling in captcha process GitOrigin-RevId: 91692978a24b02e9fa1ca55193a462ca29f68a7e
This commit is contained in:
parent
8ce9330a00
commit
1f4c8d4ed9
1 changed files with 112 additions and 6 deletions
|
@ -3,9 +3,65 @@ import { postJSON } from '../../infrastructure/fetch-json'
|
|||
|
||||
const grecaptcha = window.grecaptcha
|
||||
|
||||
let recaptchaId
|
||||
let recaptchaId, canResetCaptcha, isFromReset, resetFailed
|
||||
const recaptchaCallbacks = []
|
||||
|
||||
function resetCaptcha() {
|
||||
if (!canResetCaptcha) return
|
||||
canResetCaptcha = false
|
||||
isFromReset = true
|
||||
grecaptcha.reset(recaptchaId)
|
||||
}
|
||||
|
||||
function handleAbortedCaptcha() {
|
||||
if (recaptchaCallbacks.length > 0) {
|
||||
// There is a pending captcha process and the user dismissed it by
|
||||
// clicking somewhere else on the page. Show it again.
|
||||
// But first clear the timeout to give the user more time to solve the
|
||||
// next one.
|
||||
recaptchaCallbacks.forEach(({ resetTimeout }) => resetTimeout())
|
||||
validateCaptchaV2().catch(() => {
|
||||
// The other callback is still there to pick up the result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function emitToken(token) {
|
||||
recaptchaCallbacks.splice(0).forEach(({ resolve, resetTimeout }) => {
|
||||
resetTimeout()
|
||||
resolve(token)
|
||||
})
|
||||
|
||||
// Happy path, let the user solve another one -- if needed.
|
||||
canResetCaptcha = true
|
||||
resetCaptcha()
|
||||
}
|
||||
|
||||
function getMessage(err) {
|
||||
return (err && err.message) || 'no details returned'
|
||||
}
|
||||
|
||||
function emitError(err, src) {
|
||||
if (isFromReset) {
|
||||
resetFailed = true
|
||||
}
|
||||
|
||||
err = new Error(
|
||||
`captcha check failed: ${getMessage(err)}, please retry again`
|
||||
)
|
||||
// Keep a record of this error. 2nd line might request a screenshot of it.
|
||||
console.error(err, src)
|
||||
|
||||
recaptchaCallbacks.splice(0).forEach(({ reject, resetTimeout }) => {
|
||||
resetTimeout()
|
||||
reject(err)
|
||||
})
|
||||
|
||||
// Unhappy path: Only reset if not failed before.
|
||||
// This could be a loop without human interaction: error -> reset -> error.
|
||||
resetCaptcha()
|
||||
}
|
||||
|
||||
export async function canSkipCaptcha(email) {
|
||||
let timer
|
||||
let canSkip
|
||||
|
@ -43,13 +99,63 @@ export async function validateCaptchaV2() {
|
|||
const el = document.getElementById('recaptcha')
|
||||
recaptchaId = grecaptcha.render(el, {
|
||||
callback: token => {
|
||||
recaptchaCallbacks.splice(0).forEach(cb => cb(token))
|
||||
grecaptcha.reset(recaptchaId)
|
||||
emitToken(token)
|
||||
},
|
||||
'error-callback': () => {
|
||||
emitError(
|
||||
new Error('recaptcha: something went wrong'),
|
||||
'error-callback'
|
||||
)
|
||||
},
|
||||
'expired-callback': () => {
|
||||
emitError(new Error('recaptcha: challenge expired'), 'expired-callback')
|
||||
},
|
||||
})
|
||||
// Attach abort handler once when setting up the captcha.
|
||||
document
|
||||
.querySelector('.content')
|
||||
.addEventListener('click', handleAbortedCaptcha)
|
||||
}
|
||||
return await new Promise(resolve => {
|
||||
recaptchaCallbacks.push(resolve)
|
||||
grecaptcha.execute(recaptchaId)
|
||||
|
||||
if (resetFailed) {
|
||||
throw new Error('captcha not available. try reloading the page')
|
||||
}
|
||||
|
||||
// This is likely a human making a submit action. Let them retry on error.
|
||||
canResetCaptcha = true
|
||||
isFromReset = false
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
// We triggered this error. Ensure that we can reset to captcha.
|
||||
canResetCaptcha = true
|
||||
|
||||
emitError(new Error('challenge expired'), 'timeout')
|
||||
|
||||
// The iframe title says it will expire after 2 min. Enforce that here.
|
||||
}, 120 * 1000)
|
||||
|
||||
recaptchaCallbacks.push({
|
||||
resolve,
|
||||
reject,
|
||||
resetTimeout: () => clearTimeout(timeout),
|
||||
})
|
||||
try {
|
||||
grecaptcha.execute(recaptchaId).catch(err => {
|
||||
emitError(new Error(`recaptcha: ${getMessage(err)}`), '.catch()')
|
||||
})
|
||||
} catch (err) {
|
||||
emitError(new Error(`recaptcha: ${getMessage(err)}`), 'try/catch')
|
||||
}
|
||||
|
||||
// Try to (re-)attach a handler to the backdrop element of the popup.
|
||||
for (const delay of [1, 10, 100, 1000]) {
|
||||
setTimeout(() => {
|
||||
const el = document.body.lastChild
|
||||
if (el.tagName !== 'DIV') return
|
||||
el.removeEventListener('click', handleAbortedCaptcha)
|
||||
el.addEventListener('click', handleAbortedCaptcha)
|
||||
}, delay)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue