172 lines
5.2 KiB
TypeScript
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;
|
|
}
|