Add backend health check, fetch timeouts, stale token cleanup, and error screen
Frontend now checks /health before starting auth flow. Shows a clear "Cannot Connect to Server" screen with retry button when backend is unreachable. Stale non-JWT tokens in localStorage are detected and cleared automatically. All API calls have a 10s timeout via AbortController. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { api, getTokenPayload, isRegistered as checkRegistered, needsPasswordReset as checkNeedsReset, setAuthToken } from '@/lib/api';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { api, getTokenPayload, isRegistered as checkRegistered, needsPasswordReset as checkNeedsReset, validateStoredToken } from '@/lib/api';
|
||||
import { ensureAuth } from './useCloudSave';
|
||||
|
||||
interface AuthGateState {
|
||||
loading: boolean;
|
||||
backendError: string | null;
|
||||
needsInvite: boolean;
|
||||
needsPasswordReset: boolean;
|
||||
registered: boolean;
|
||||
@@ -11,46 +12,60 @@ interface AuthGateState {
|
||||
config: { requireInvite: boolean; userInvitations: number } | null;
|
||||
setRegistered: (value: boolean) => void;
|
||||
setNeedsPasswordReset: (value: boolean) => void;
|
||||
retry: () => void;
|
||||
}
|
||||
|
||||
export function useAuthGate(): AuthGateState {
|
||||
const [config, setConfig] = useState<{ requireInvite: boolean; userInvitations: number } | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [backendError, setBackendError] = useState<string | null>(null);
|
||||
const [registered, setRegistered] = useState(false);
|
||||
const [passwordReset, setPasswordReset] = useState(false);
|
||||
const [admin, setAdmin] = useState(false);
|
||||
const [initCount, setInitCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const init = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setBackendError(null);
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const [cfg] = await Promise.all([
|
||||
api.config.get(),
|
||||
ensureAuth(),
|
||||
]);
|
||||
validateStoredToken();
|
||||
|
||||
if (cancelled) return;
|
||||
|
||||
setConfig(cfg);
|
||||
|
||||
const payload = getTokenPayload();
|
||||
const reg = checkRegistered();
|
||||
setRegistered(reg);
|
||||
setPasswordReset(checkNeedsReset());
|
||||
setAdmin(payload?.role === 'admin');
|
||||
} catch {
|
||||
// Config fetch failed — allow game to load (fail open)
|
||||
setConfig({ requireInvite: false, userInvitations: 0 });
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false);
|
||||
}
|
||||
try {
|
||||
await api.health();
|
||||
} catch (e) {
|
||||
setBackendError(e instanceof Error ? e.message : 'Cannot connect to server');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
init();
|
||||
return () => { cancelled = true; };
|
||||
try {
|
||||
const cfg = await api.config.get();
|
||||
setConfig(cfg);
|
||||
} catch {
|
||||
setConfig({ requireInvite: false, userInvitations: 0 });
|
||||
}
|
||||
|
||||
try {
|
||||
await ensureAuth();
|
||||
} catch {
|
||||
// auth failed — will show as unregistered
|
||||
}
|
||||
|
||||
const payload = getTokenPayload();
|
||||
setRegistered(checkRegistered());
|
||||
setPasswordReset(checkNeedsReset());
|
||||
setAdmin(payload?.role === 'admin');
|
||||
setLoading(false);
|
||||
}, []);
|
||||
|
||||
// Run init on mount and on retry
|
||||
useState(() => { init(); });
|
||||
|
||||
const retry = useCallback(() => {
|
||||
setInitCount(c => c + 1);
|
||||
init();
|
||||
}, [init]);
|
||||
|
||||
const handleSetRegistered = useCallback((value: boolean) => {
|
||||
setRegistered(value);
|
||||
const payload = getTokenPayload();
|
||||
@@ -68,6 +83,7 @@ export function useAuthGate(): AuthGateState {
|
||||
|
||||
return {
|
||||
loading,
|
||||
backendError,
|
||||
needsInvite,
|
||||
needsPasswordReset: passwordReset,
|
||||
registered,
|
||||
@@ -75,5 +91,6 @@ export function useAuthGate(): AuthGateState {
|
||||
config,
|
||||
setRegistered: handleSetRegistered,
|
||||
setNeedsPasswordReset: handleSetPasswordReset,
|
||||
retry,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user