diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 1cc3f49..d15faa1 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -66,6 +66,7 @@ const siteGateEnabled = siteGateSetting == null const staticDataBase = (import.meta.env.VITE_STATIC_DATA_BASE || '/data').replace(/\/+$/, '') const staticDataEnabled = String(import.meta.env.VITE_STATIC_DATA || 'false').toLowerCase() === 'true' const missingStaticDataPaths = new Set() +const turnstileRequiredEvent = 'tssbot:turnstile-required' const BLOCKED_PLAYER_UIDS = new Set(['165569402', '86157459', '33536334', '41808996', '3651161']) const BLOCKED_TEAM_NAMES = new Set(['TPC']) @@ -135,6 +136,15 @@ function dataSource(apiPath, staticPath = null) { return { apiPath, staticPath } } +function turnstileSessionRequiredMessage(error) { + return error === 'Turnstile session required' || error === 'Site session required' +} + +function promptTurnstileSession(detail) { + if (typeof window === 'undefined' || !turnstileSiteKey) return + window.dispatchEvent(new CustomEvent(turnstileRequiredEvent, { detail })) +} + const publicDataSources = { teams: dataSource(apiEndpoints.teams, staticDataPath('leaderboard-teams.json')), players: dataSource(apiEndpoints.players, staticDataPath('leaderboard-players.json')), @@ -161,6 +171,10 @@ async function fetchJson(path, signal) { const error = new Error(body?.error || `Request failed with ${response.status}`) error.status = response.status error.path = path + error.detail = body?.detail + if (response.status === 403 && turnstileSessionRequiredMessage(body?.error)) { + promptTurnstileSession({ path, error: body?.error, detail: body?.detail }) + } throw error } @@ -1307,10 +1321,10 @@ function SiteGate({ onVerified }) { >

- Verifying you are human + Start a protected session

- This quick check protects the site from automated abuse. It usually clears itself. + Complete this Turnstile check to continue using protected site data.

- return + if (!turnstileSiteKey) return + return } -function GatedAppContent() { +function GatedAppContent({ enforceInitialGate }) { const embeddedGateState = document .querySelector('meta[name="tss-turnstile-session"]') ?.getAttribute('content') - const initialGateState = turnstileSiteKey + const initialGateState = enforceInitialGate ? ['verified', 'required'].includes(embeddedGateState) ? embeddedGateState : 'checking' @@ -1346,7 +1360,7 @@ function GatedAppContent() { const [gateState, setGateState] = useState(initialGateState) useEffect(() => { - if (!turnstileSiteKey || gateState !== 'checking') return undefined + if (!enforceInitialGate || gateState !== 'checking') return undefined let cancelled = false fetch('/api/turnstile/session', { headers: { Accept: 'application/json' } }) .then((response) => (response.ok ? response.json() : { verified: false })) @@ -1360,7 +1374,16 @@ function GatedAppContent() { return () => { cancelled = true } - }, [gateState]) + }, [enforceInitialGate, gateState]) + + useEffect(() => { + function handleTurnstileRequired() { + setGateState('required') + } + + window.addEventListener(turnstileRequiredEvent, handleTurnstileRequired) + return () => window.removeEventListener(turnstileRequiredEvent, handleTurnstileRequired) + }, []) if (gateState === 'checking') { return