Files
insar-management-system-v2/frontend/src/hooks/useAppAuthLifecycle.js
T

213 lines
6.0 KiB
JavaScript

import { useCallback, useEffect, useRef } from 'react';
import apiClient from '../api/client';
import { getHealth } from '../api/health';
const HEALTH_POLL_INTERVAL_MS = 5 * 60 * 1000;
const STARTUP_RETRY_DELAYS_MS = [750, 1500, 3000];
const sleep = (delayMs) => new Promise((resolve) => setTimeout(resolve, delayMs));
const getErrorStatus = (error) => Number(error?.response?.status || 0);
const isTransientApiError = (error) => {
const status = getErrorStatus(error);
return !status || status === 408 || status === 429 || status >= 500;
};
const requestWithTransientRetry = async (
requestFn,
{
delays = STARTUP_RETRY_DELAYS_MS,
shouldRetry = isTransientApiError,
} = {},
) => {
let lastError = null;
const retryDelays = Array.isArray(delays) ? delays : [];
for (let attempt = 0; attempt <= retryDelays.length; attempt += 1) {
try {
return await requestFn();
} catch (error) {
lastError = error;
if (attempt >= retryDelays.length || !shouldRetry(error)) {
throw error;
}
await sleep(retryDelays[attempt]);
}
}
throw lastError;
};
export default function useAppAuthLifecycle({
clearRadarSearchResults,
radarSearchRequestSeqRef,
aoeLayerRef,
activeLayersRef,
radarPreviewLayersRef,
setHasRadarSearched,
setCurrentUser,
setAuthChecked,
setPendingTaskIds,
setHealthLoading,
setHealthError,
setHealthStatus,
setAoiLayer,
setAllData,
setRadarPagination,
}) {
const authGenerationRef = useRef(0);
const fetchCurrentUser = useCallback(async (options = {}) => {
const { clearOnFailure = true } = options;
const requestGeneration = authGenerationRef.current;
try {
const response = await requestWithTransientRetry(() => (
apiClient.get('/auth/me', {
skipAuthReset: !clearOnFailure,
authGeneration: requestGeneration,
})
));
if (requestGeneration === authGenerationRef.current) {
setCurrentUser(response.data || null);
}
} catch (error) {
if (
clearOnFailure
&& getErrorStatus(error) === 401
&& requestGeneration === authGenerationRef.current
) {
setCurrentUser(null);
}
} finally {
if (requestGeneration === authGenerationRef.current || !clearOnFailure) {
setAuthChecked(true);
}
}
}, [setCurrentUser, setAuthChecked]);
const handleLoginSuccess = useCallback(async (userFromLogin = null) => {
authGenerationRef.current += 1;
if (userFromLogin) {
setCurrentUser(userFromLogin);
setAuthChecked(true);
void fetchCurrentUser({ clearOnFailure: false });
return;
}
await fetchCurrentUser({ clearOnFailure: !userFromLogin });
}, [fetchCurrentUser, setCurrentUser, setAuthChecked]);
const handleLogout = useCallback(async () => {
authGenerationRef.current += 1;
try {
await apiClient.post('/auth/logout');
} catch (error) {
console.error('Logout failed:', error);
} finally {
radarSearchRequestSeqRef.current += 1;
setHasRadarSearched(false);
clearRadarSearchResults();
setCurrentUser(null);
setPendingTaskIds([]);
setAuthChecked(true);
}
}, [
clearRadarSearchResults,
radarSearchRequestSeqRef,
setHasRadarSearched,
setCurrentUser,
setPendingTaskIds,
setAuthChecked,
]);
const fetchHealthStatus = useCallback(async (options = {}) => {
const { refresh = false, silent = false } = options;
try {
if (!silent) {
setHealthLoading(true);
}
setHealthError('');
const data = await requestWithTransientRetry(
() => getHealth(refresh ? { refresh: true } : {}),
{ delays: silent ? [] : STARTUP_RETRY_DELAYS_MS.slice(0, 2) },
);
setHealthStatus(data || null);
} catch (error) {
setHealthError(error.response?.data?.detail || '运维自检失败');
setHealthStatus(null);
} finally {
if (!silent) {
setHealthLoading(false);
}
}
}, [setHealthLoading, setHealthError, setHealthStatus]);
useEffect(() => {
const interceptorId = apiClient.interceptors.response.use(
(response) => response,
(error) => {
const requestGeneration = error?.config?.authGeneration;
const staleAuthFailure = typeof requestGeneration === 'number' && requestGeneration < authGenerationRef.current;
if (error?.response?.status === 401 && !error?.config?.skipAuthReset && !staleAuthFailure) {
radarSearchRequestSeqRef.current += 1;
setHasRadarSearched(false);
if (aoeLayerRef.current) {
aoeLayerRef.current.remove();
aoeLayerRef.current = null;
}
setAoiLayer(null);
Object.values(activeLayersRef.current).forEach((layer) => layer.remove());
activeLayersRef.current = {};
Object.values(radarPreviewLayersRef.current).forEach((layer) => layer.remove());
radarPreviewLayersRef.current = {};
setAllData([]);
setRadarPagination((prev) => ({
...prev,
total: 0,
offset: 0,
hasMore: false,
}));
setCurrentUser(null);
setAuthChecked(true);
}
return Promise.reject(error);
}
);
return () => {
apiClient.interceptors.response.eject(interceptorId);
};
}, [
radarSearchRequestSeqRef,
setHasRadarSearched,
aoeLayerRef,
setAoiLayer,
activeLayersRef,
radarPreviewLayersRef,
setAllData,
setRadarPagination,
setCurrentUser,
setAuthChecked,
]);
useEffect(() => {
fetchCurrentUser();
}, [fetchCurrentUser]);
useEffect(() => {
const startupTimer = setTimeout(() => {
void fetchHealthStatus({ silent: true });
}, 1500);
const interval = setInterval(() => {
void fetchHealthStatus({ silent: true });
}, HEALTH_POLL_INTERVAL_MS);
return () => {
clearTimeout(startupTimer);
clearInterval(interval);
};
}, [fetchHealthStatus]);
return {
handleLoginSuccess,
handleLogout,
fetchHealthStatus,
};
}