This commit is contained in:
2025-11-23 13:22:00 +01:00
parent 85afff2c19
commit 71f9330c67
6 changed files with 92 additions and 12 deletions
+71 -2
View File
@@ -12,7 +12,7 @@ import type {
TenantSnippet,
TagResponse,
} from './apiTypes';
import type { AxiosInstance } from 'axios';
import type { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosRequestConfig } from 'axios';
export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete' | 'defaults'> = {
get: api.get.bind(api),
@@ -22,6 +22,22 @@ export const httpClient: Pick<AxiosInstance, 'get' | 'post' | 'patch' | 'delete'
defaults: api.defaults,
};
type AuthAwareRequestConfig = InternalAxiosRequestConfig & {
_retry?: boolean;
skipAuthRefresh?: boolean;
};
type AuthRequestConfig = AxiosRequestConfig & {
skipAuthRefresh?: boolean;
};
type AuthRefreshHandlers = {
onRefreshSuccess?: (token: string, payload?: { tenant?: unknown }) => void;
onRefreshFailure?: (error: unknown) => void;
};
let refreshPromise: Promise<string> | null = null;
let authRefreshHandlers: AuthRefreshHandlers = {};
const normalizeNumber = (value: unknown): number | undefined => {
const n = Number(value);
return Number.isFinite(n) ? n : undefined;
@@ -168,7 +184,7 @@ export const performLogin = async (payload: Record<string, unknown>): Promise<un
};
export const refreshSession = async (): Promise<{ access_token?: string; tenant?: unknown }> => {
const { data } = await api.post('/auth/refresh');
const { data } = await api.post('/auth/refresh', undefined, { skipAuthRefresh: true } as AuthRequestConfig);
return data as { access_token?: string; tenant?: unknown };
};
@@ -277,6 +293,59 @@ export const clearAuthToken = () => {
delete api.defaults.headers.common.Authorization;
};
export const setAuthRefreshHandlers = (handlers: AuthRefreshHandlers) => {
authRefreshHandlers = handlers;
};
const performTokenRefresh = async (): Promise<string> => {
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = refreshSession()
.then((data) => {
const token = data?.access_token;
if (!token) {
throw new Error('Missing access token in refresh response');
}
setAuthToken(token);
authRefreshHandlers.onRefreshSuccess?.(token, { tenant: data?.tenant });
return token;
})
.catch((error) => {
authRefreshHandlers.onRefreshFailure?.(error);
throw error;
})
.finally(() => {
refreshPromise = null;
});
return refreshPromise;
};
api.interceptors.response.use(
(response) => response,
async (error: AxiosError) => {
const response = error.response;
const config = (error.config || {}) as AuthAwareRequestConfig;
if (!response || response.status !== 401 || config._retry || config.skipAuthRefresh) {
return Promise.reject(error);
}
config._retry = true;
try {
const token = await performTokenRefresh();
const headers = (config.headers ?? {}) as Record<string, unknown>;
headers.Authorization = `Bearer ${token}`;
config.headers = headers as AuthAwareRequestConfig['headers'];
return api(config);
} catch (refreshError) {
return Promise.reject(refreshError);
}
},
);
export type {
DownloadLink,
DocumentResponse,