2ab097ec8a
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>
97 lines
2.6 KiB
TypeScript
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,
|
|
};
|
|
}
|