138 lines
4.5 KiB
JavaScript
138 lines
4.5 KiB
JavaScript
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;
|