Files
inbuxa-admin/src/services/api.ts
T
2026-08-24 15:44:51 +02:00

137 lines
3.5 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
import { useAuthStore } from '../stores/authStore';
import { getBasePath } from '@/lib/basePath';
import { getOAuthClientId } from '@/lib/oauthClientId';
export function getApiBaseUrl(): string {
const envUrl = import.meta.env.VITE_API_BASE_URL as string | undefined;
if (envUrl && envUrl.length > 0) {
return envUrl.replace(/\/+$/, '');
}
return window.location.origin;
}
export class ApiError extends Error {
status: number;
statusText: string;
body: unknown;
constructor(status: number, statusText: string, body: unknown) {
super(`API error ${status}: ${statusText}`);
this.name = 'ApiError';
this.status = status;
this.statusText = statusText;
this.body = body;
}
}
let refreshPromise: Promise<void> | null = null;
export async function refreshAccessToken(): Promise<void> {
if (refreshPromise) {
return refreshPromise;
}
refreshPromise = (async () => {
const { refreshToken, tokenEndpoint, logout } = useAuthStore.getState();
if (!refreshToken || !tokenEndpoint) {
logout();
window.location.href = `${getBasePath()}/login`;
throw new Error('No refresh token or token endpoint available');
}
const clientId = getOAuthClientId();
try {
const response = await fetch(tokenEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: clientId,
}),
});
if (!response.ok) {
throw new Error(`Token refresh failed: ${response.status}`);
}
const data = await response.json();
useAuthStore
.getState()
.setTokens(data.access_token, data.refresh_token ?? refreshToken, data.expires_in, tokenEndpoint);
} catch (error) {
useAuthStore.getState().logout();
window.location.href = `${getBasePath()}/login`;
throw error;
}
})();
try {
await refreshPromise;
} finally {
refreshPromise = null;
}
}
export async function apiFetch(path: string, options?: RequestInit): Promise<Response> {
const store = useAuthStore.getState();
if (store.isTokenExpiringSoon() && store.refreshToken) {
await refreshAccessToken();
}
const makeRequest = async (): Promise<Response> => {
const { accessToken } = useAuthStore.getState();
const url = `${getApiBaseUrl()}${path}`;
const headers = new Headers(options?.headers);
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
}
const response = await fetch(url, {
...options,
headers,
redirect: 'follow',
});
return response;
};
let response = await makeRequest();
if (response.status === 401) {
if (useAuthStore.getState().refreshToken) {
try {
await refreshAccessToken();
response = await makeRequest();
} catch {
throw new ApiError(401, 'Unauthorized', null);
}
} else {
useAuthStore.getState().logout();
window.location.href = `${getBasePath()}/login`;
throw new ApiError(401, 'Unauthorized', null);
}
}
if (!response.ok) {
let body: unknown = null;
try {
body = await response.json();
// eslint-disable-next-line no-empty
} catch {}
throw new ApiError(response.status, response.statusText, body);
}
return response;
}