Eventbuzz_Web/src/api/client.ts

172 lines
5.2 KiB
TypeScript

const configuredApiBase = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim();
function resolveApiBaseUrl(): string {
if (configuredApiBase) {
// Relative /api only works in dev (Vite proxy). In production it hits the web host.
if (import.meta.env.PROD && configuredApiBase === '/api') {
return 'https://api.eventbuzz.live/api';
}
return configuredApiBase.replace(/\/$/, '');
}
return import.meta.env.DEV ? '/api' : 'https://api.eventbuzz.live/api';
}
export const API_BASE_URL = resolveApiBaseUrl();
export const PUBLIC_WEB_BASE =
(import.meta.env.VITE_PUBLIC_WEB_BASE as string | undefined)?.replace(/\/$/, '') ||
(typeof window !== 'undefined' ? window.location.origin : 'http://localhost:5175');
const STORAGE_KEYS = {
access: 'eventbuzz_access_token',
refresh: 'eventbuzz_refresh_token',
} as const;
let accessToken: string | null = null;
let refreshToken: string | null = null;
function restoreTokensFromStorage() {
try {
const access = window.localStorage.getItem(STORAGE_KEYS.access);
const refresh = window.localStorage.getItem(STORAGE_KEYS.refresh);
if (access) accessToken = access;
if (refresh) refreshToken = refresh;
} catch {
// ignore storage issues
}
}
restoreTokensFromStorage();
export function setTokens(access: string, refresh: string) {
accessToken = access;
refreshToken = refresh;
try {
window.localStorage.setItem(STORAGE_KEYS.access, access);
window.localStorage.setItem(STORAGE_KEYS.refresh, refresh);
} catch {
// ignore storage issues
}
}
export function clearTokens() {
accessToken = null;
refreshToken = null;
try {
window.localStorage.removeItem(STORAGE_KEYS.access);
window.localStorage.removeItem(STORAGE_KEYS.refresh);
} catch {
// ignore storage issues
}
}
async function doRefresh(): Promise<boolean> {
if (!refreshToken) return false;
const base = API_BASE_URL.replace(/\/api\/?$/, '');
try {
const res = await fetch(`${base}/api/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken }),
});
const body = await res.json().catch(() => null);
if (res.ok && body?.success && body?.data?.token) {
setTokens(body.data.token, refreshToken);
return true;
}
} catch {
// ignore refresh failures and proceed with clear state
}
clearTokens();
return false;
}
type ApiEnvelope<T> = {
success?: boolean;
data?: T;
error?: { message?: string; code?: string };
};
export async function apiGet<T>(path: string): Promise<T> {
const url = path.startsWith('http') ? path : `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
let res: Response;
try {
res = await fetch(url, {
headers: accessToken ? { Authorization: `Bearer ${accessToken}` } : undefined,
});
} catch {
throw new Error('Could not reach the API. If this persists, the server may be down or blocking cross-origin requests.');
}
let body: ApiEnvelope<T>;
try {
body = await res.json();
} catch {
if (res.status === 502 || res.status === 503 || res.status === 504) {
throw new Error('The API is temporarily unavailable. Please try again in a few minutes.');
}
throw new Error(`Unexpected server response (${res.status}).`);
}
if (!body.success || body.data === undefined) {
throw new Error(body.error?.message || 'Request failed');
}
return body.data;
}
export async function apiPost<T>(path: string, payload: unknown): Promise<T> {
const url = path.startsWith('http') ? path : `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
let res: Response;
try {
res = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),
},
body: JSON.stringify(payload),
});
} catch {
throw new Error('Could not reach the API. If this persists, the server may be down or blocking cross-origin requests.');
}
let body: ApiEnvelope<T>;
try {
body = await res.json();
} catch {
if (res.status === 502 || res.status === 503 || res.status === 504) {
throw new Error('The API is temporarily unavailable. Please try again in a few minutes.');
}
throw new Error(`Unexpected server response (${res.status}).`);
}
if (!body.success) {
throw new Error(body.error?.message || 'Request failed');
}
return body.data as T;
}
export async function fetchWithAuth(path: string, init: RequestInit = {}): Promise<Response> {
const url = path.startsWith('http') ? path : `${API_BASE_URL}${path.startsWith('/') ? path : `/${path}`}`;
const headers = new Headers(init.headers || {});
if (accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
}
if (typeof FormData !== 'undefined' && init.body instanceof FormData) {
headers.delete('Content-Type');
}
let res = await fetch(url, { ...init, headers });
if (res.status === 401 && refreshToken) {
const refreshed = await doRefresh();
if (refreshed && accessToken) {
headers.set('Authorization', `Bearer ${accessToken}`);
res = await fetch(url, { ...init, headers });
}
}
return res;
}