mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 06:41:46 +00:00
3ee4da9775
Behind Cloudflare Zero Trust or Pangolin, cross-origin auth redirects on /api/* calls surface as CORS errors (error.response === undefined) that the existing 401 interceptor never catches, leaving the PWA stuck with network-error toasts instead of re-authenticating. New connectivity module probes /api/health every 30s using fetch with cache:no-store and inspects Content-Type to reliably detect whether the server is reachable vs intercepted by an upstream proxy. axios interceptor changes: - On !error.response + navigator.onLine: run probeNow(); if the health probe also fails (proxy is intercepting all requests), trigger a guarded window.location.reload() so the edge proxy can intercept the top-level navigation and run its auth flow (covers CF Access and Pangolin 302 mode) - On error.response status 401 with text/html body: same reload path, covering Pangolin header-auth extended compatibility mode which returns 401+HTML instead of a 302 redirect. TREK own 401s are always JSON so there is no collision with the existing AUTH_REQUIRED branch. - sessionStorage flag prevents reload loops; cleared on any successful response so the guard resets after re-auth. /api/health excluded from SW NetworkFirst cache (vite.config.js regex) and Cache-Control: no-store added server-side so probes always hit the network and cannot be served stale from the 24h api-data cache. LoginPage caches last-known appConfig in localStorage so the SSO button renders in OIDC+UN/PW dual mode even when the config fetch is intercepted by the proxy. Auto-redirect to IdP skipped when config comes from cache to avoid redirect loops while the proxy is challenging. Fixes discussion #836.
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
const PROBE_INTERVAL_MS = 30_000
|
|
const PROBE_TIMEOUT_MS = 1_500
|
|
|
|
let reachable = true
|
|
const listeners = new Set<(v: boolean) => void>()
|
|
|
|
function setReachable(v: boolean): void {
|
|
if (reachable === v) return
|
|
reachable = v
|
|
listeners.forEach(fn => fn(v))
|
|
}
|
|
|
|
async function probe(): Promise<void> {
|
|
if (!navigator.onLine) { setReachable(false); return }
|
|
try {
|
|
const ctrl = new AbortController()
|
|
const t = setTimeout(() => ctrl.abort(), PROBE_TIMEOUT_MS)
|
|
const res = await fetch('/api/health', {
|
|
method: 'GET',
|
|
credentials: 'include',
|
|
cache: 'no-store',
|
|
signal: ctrl.signal,
|
|
})
|
|
clearTimeout(t)
|
|
// /api/health returns JSON. CF Access / Pangolin will either return HTML
|
|
// (Pangolin 200 auth wall) or trigger a cross-origin redirect that throws
|
|
// below. Both proxy-auth scenarios resolve to reachable = false.
|
|
const ct = res.headers.get('content-type') || ''
|
|
setReachable(res.ok && ct.includes('application/json'))
|
|
} catch {
|
|
setReachable(false)
|
|
}
|
|
}
|
|
|
|
export function startConnectivityProbe(): void {
|
|
probe()
|
|
setInterval(probe, PROBE_INTERVAL_MS)
|
|
window.addEventListener('online', probe)
|
|
window.addEventListener('offline', () => setReachable(false))
|
|
}
|
|
|
|
export function isReachable(): boolean { return reachable }
|
|
export function probeNow(): Promise<void> { return probe() }
|
|
export function onChange(fn: (v: boolean) => void): () => void {
|
|
listeners.add(fn)
|
|
return () => listeners.delete(fn)
|
|
}
|