Files
AIHostingTycoon/apps/web/src/hooks/useAuthGate.ts
T
josh 2ab097ec8a 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>
2026-04-27 19:55:50 -04:00

97 lines
2.6 KiB
TypeScript

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;
isAdmin: boolean;
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);
const init = useCallback(async () => {
setLoading(true);
setBackendError(null);
validateStoredToken();
try {
await api.health();
} catch (e) {
setBackendError(e instanceof Error ? e.message : 'Cannot connect to server');
setLoading(false);
return;
}
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();
if (payload) {
setAdmin(payload.role === 'admin');
setPasswordReset(payload.mustResetPassword);
}
}, []);
const handleSetPasswordReset = useCallback((value: boolean) => {
setPasswordReset(value);
}, []);
const needsInvite = !!(config?.requireInvite && !registered);
return {
loading,
backendError,
needsInvite,
needsPasswordReset: passwordReset,
registered,
isAdmin: admin,
config,
setRegistered: handleSetRegistered,
setNeedsPasswordReset: handleSetPasswordReset,
retry,
};
}