This commit is contained in:
2025-11-09 13:03:44 +01:00
parent 4bc7357e41
commit bc33f68209
24 changed files with 5665 additions and 5261 deletions
@@ -0,0 +1,137 @@
import { useCallback, useEffect, useRef } from 'react';
const useAuthManager = ({
apiClient,
token,
appStatus,
appDispatch,
notifyApiError,
setStatusMessage,
setLoading,
}) => {
const tokenRef = useRef(token);
const refreshPromiseRef = useRef(null);
const initialRefreshAttemptedRef = useRef(Boolean(token));
const refreshAccessToken = useCallback(async () => {
console.log('[Auth] Attempting to refresh access token…');
appDispatch({ type: 'TOKEN_REFRESH_START' });
try {
const { data } = await apiClient.post('/auth/refresh');
if (data?.access_token) {
appDispatch({
type: 'TOKEN_REFRESH_SUCCESS',
token: data.access_token,
tenant: data.tenant || null,
});
console.log('[Auth] Access token refreshed at', new Date().toISOString());
return data.access_token;
}
throw new Error('Missing access token in refresh response');
} catch (error) {
console.warn('[Auth] Failed to refresh access token', error);
appDispatch({ type: 'TOKEN_REFRESH_FAILURE', error: error?.message || null });
throw error;
}
}, [apiClient, appDispatch]);
useEffect(() => {
tokenRef.current = token;
}, [token]);
useEffect(() => {
if (!token && !initialRefreshAttemptedRef.current && appStatus === 'logged-out') {
initialRefreshAttemptedRef.current = true;
console.log('[Auth] Attempting refresh at startup');
refreshAccessToken().catch(() => {});
}
}, [token, appStatus, refreshAccessToken]);
useEffect(() => {
const requestInterceptor = apiClient.interceptors.request.use((config) => {
const currentToken = tokenRef.current;
if (currentToken) {
config.headers = config.headers || {};
if (!config.headers.Authorization) {
config.headers.Authorization = `Bearer ${currentToken}`;
}
}
return config;
});
const responseInterceptor = apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const { response, config } = error;
if (!response || !config) {
return Promise.reject(error);
}
const status = response.status;
const url = typeof config.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;
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${newToken}`;
console.log('[Auth] Retrying original request', url);
try {
return await apiClient(config);
} catch (retryError) {
if (retryError?.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');
} catch (error) {
console.warn('[Auth] Failed to revoke refresh token during logout', error);
} finally {
setLoading(false);
appDispatch({ type: 'LOGOUT' });
setStatusMessage('Logged out.', 'info');
}
}, [apiClient, appDispatch, setLoading, setStatusMessage]);
return { tokenRef, refreshAccessToken, handleLogout };
};
export default useAuthManager;