This commit is contained in:
2025-11-22 18:19:55 +01:00
parent d3c76bc303
commit 014f489857
7 changed files with 124 additions and 141 deletions
+7 -104
View File
@@ -1,28 +1,19 @@
import { useCallback, useEffect, useRef } from 'react';
import type { MutableRefObject } from 'react';
import type { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios';
import { AxiosHeaders } from 'axios';
import { clearAuthToken, logoutSession, refreshSession, setAuthToken } from '../../lib/apiClient';
type AppStatus = string;
type AppDispatch = (action: { type: string; [key: string]: unknown }) => void;
type NotifyApiError = (error: unknown, fallbackMessage: string, variant?: string) => void;
type SetStatusMessage = (message: string, variant?: string) => void;
type SetLoading = (state: boolean) => void;
interface RetryableAxiosRequestConfig extends InternalAxiosRequestConfig {
_retry?: boolean;
}
interface UseAuthManagerArgs {
apiClient: AxiosInstance;
token?: string | null;
appStatus: AppStatus;
appDispatch: AppDispatch;
notifyApiError: NotifyApiError;
setStatusMessage: SetStatusMessage;
setLoading: SetLoading;
}
@@ -33,40 +24,23 @@ interface UseAuthManagerResult {
handleLogout: () => Promise<void>;
}
const ensureAxiosHeaders = (
headers?: InternalAxiosRequestConfig['headers'],
): AxiosHeaders => {
if (headers instanceof AxiosHeaders) {
return headers;
}
return AxiosHeaders.from(headers || {});
};
const setHeaderAuthorization = (config: InternalAxiosRequestConfig, token: string): void => {
const headers = ensureAxiosHeaders(config.headers);
headers.set('Authorization', `Bearer ${token}`);
config.headers = headers;
};
const useAuthManager = ({
apiClient,
token,
appStatus,
appDispatch,
notifyApiError,
setStatusMessage,
setLoading,
}: UseAuthManagerArgs): UseAuthManagerResult => {
const tokenRef = useRef<string | null>(token);
const refreshPromiseRef = useRef<Promise<string> | null>(null);
const initialRefreshAttemptedRef = useRef(Boolean(token));
const refreshAccessToken = useCallback(async (): Promise<string> => {
console.log('[Auth] Attempting to refresh access token…');
appDispatch({ type: 'TOKEN_REFRESH_START' });
try {
const { data } = await apiClient.post<{ access_token?: string; tenant?: unknown }>('/auth/refresh');
const data = await refreshSession();
if (data?.access_token) {
setAuthToken(data.access_token);
appDispatch({
type: 'TOKEN_REFRESH_SUCCESS',
token: data.access_token,
@@ -81,7 +55,7 @@ const useAuthManager = ({
appDispatch({ type: 'TOKEN_REFRESH_FAILURE', error: (error as Error)?.message || null });
throw error;
}
}, [apiClient, appDispatch]);
}, [appDispatch]);
useEffect(() => {
tokenRef.current = token;
@@ -95,90 +69,19 @@ const useAuthManager = ({
}
}, [token, appStatus, refreshAccessToken]);
useEffect(() => {
const requestInterceptor = apiClient.interceptors.request.use((config) => {
const currentToken = tokenRef.current;
if (currentToken) {
const headers = ensureAxiosHeaders(config.headers);
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${currentToken}`);
}
config.headers = headers;
}
return config;
});
const responseInterceptor = apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const axiosError = error as AxiosError & { config?: RetryableAxiosRequestConfig };
const { response, config } = axiosError;
if (!response || !config) {
return Promise.reject(error);
}
const status = response.status;
const url = String(config?.url ?? '');
const isAuthRoute = url.includes('/auth/login') || url.includes('/auth/refresh');
if (status === 401 && !config._retry && !isAuthRoute) {
console.warn('[Auth] 401 received for', url, '- attempting token refresh');
if (!refreshPromiseRef.current) {
refreshPromiseRef.current = (async () => {
try {
return await refreshAccessToken();
} finally {
refreshPromiseRef.current = null;
}
})();
}
try {
const newToken = await refreshPromiseRef.current;
if (!newToken) {
throw new Error('No token returned from refresh');
}
config._retry = true;
setHeaderAuthorization(config, newToken);
console.log('[Auth] Retrying original request', url);
try {
return await apiClient(config);
} catch (retryError) {
if ((retryError as AxiosError)?.response?.status === 401) {
notifyApiError(retryError, 'Session expired. Please log in again.');
}
throw retryError;
}
} catch (refreshError) {
console.warn('[Auth] Refresh failed, clearing session');
notifyApiError(refreshError, 'Session expired. Please log in again.');
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
},
);
return () => {
apiClient.interceptors.request.eject(requestInterceptor);
apiClient.interceptors.response.eject(responseInterceptor);
};
}, [apiClient, notifyApiError, refreshAccessToken]);
const handleLogout = useCallback(async () => {
try {
setLoading(true);
await apiClient.post('/auth/logout');
await logoutSession();
} catch (error) {
console.warn('[Auth] Failed to revoke refresh token during logout', error);
} finally {
clearAuthToken();
setLoading(false);
appDispatch({ type: 'LOGOUT' });
setStatusMessage('Logged out.', 'info');
}
}, [apiClient, appDispatch, setLoading, setStatusMessage]);
}, [appDispatch, setLoading, setStatusMessage]);
return { tokenRef, refreshAccessToken, handleLogout };
};