/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * 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 | null = null; export async function refreshAccessToken(): Promise { 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 { const store = useAuthStore.getState(); if (store.isTokenExpiringSoon() && store.refreshToken) { await refreshAccessToken(); } const makeRequest = async (): Promise => { 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; }