184 lines
5.0 KiB
TypeScript
184 lines
5.0 KiB
TypeScript
// ============================================================================
|
|
// API Client
|
|
// ============================================================================
|
|
|
|
import { API_URL, STORAGE_KEYS } from "@/lib/utils/constants";
|
|
import type { ApiError } from "@/lib/types/api";
|
|
|
|
// ============================================================================
|
|
// Types
|
|
// ============================================================================
|
|
|
|
interface RequestOptions extends RequestInit {
|
|
params?: Record<string, any>;
|
|
requireAuth?: boolean;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Helper Functions
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Build URL with query parameters
|
|
*/
|
|
const buildUrl = (endpoint: string, params?: Record<string, any>): string => {
|
|
if (!params || Object.keys(params).length === 0) {
|
|
return endpoint;
|
|
}
|
|
|
|
const searchParams = new URLSearchParams();
|
|
Object.entries(params).forEach(([key, value]) => {
|
|
if (value !== undefined && value !== null) {
|
|
if (Array.isArray(value)) {
|
|
value.forEach((v) => searchParams.append(key, String(v)));
|
|
} else {
|
|
searchParams.append(key, String(value));
|
|
}
|
|
}
|
|
});
|
|
|
|
const queryString = searchParams.toString();
|
|
return queryString ? `${endpoint}?${queryString}` : endpoint;
|
|
};
|
|
|
|
/**
|
|
* Get auth token from storage
|
|
*/
|
|
const getAuthToken = (): string | null => {
|
|
if (typeof window === "undefined") return null;
|
|
return localStorage.getItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
};
|
|
|
|
/**
|
|
* Clear auth tokens from storage
|
|
*/
|
|
export const clearAuthTokens = (): void => {
|
|
if (typeof window === "undefined") return;
|
|
localStorage.removeItem(STORAGE_KEYS.ACCESS_TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.REFRESH_TOKEN);
|
|
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
};
|
|
|
|
/**
|
|
* Save auth tokens to storage
|
|
*/
|
|
export const saveAuthTokens = (
|
|
accessToken: string,
|
|
refreshToken?: string
|
|
): void => {
|
|
if (typeof window === "undefined") return;
|
|
localStorage.setItem(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
|
|
if (refreshToken) {
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// API Client
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Main API request function
|
|
*/
|
|
export const apiRequest = async <T = any>(
|
|
endpoint: string,
|
|
options: RequestOptions = {}
|
|
): Promise<T> => {
|
|
const { params, requireAuth = true, ...fetchOptions } = options;
|
|
const body = fetchOptions.body;
|
|
|
|
// Build URL
|
|
const url = buildUrl(`${API_URL}${endpoint}`, params);
|
|
|
|
const headers: HeadersInit = {
|
|
...fetchOptions.headers,
|
|
};
|
|
|
|
// Don't set Content-Type for FormData - browser will set it with boundary
|
|
if (!(body instanceof FormData)) {
|
|
(headers as Record<string, string>)["Content-Type"] = "application/json";
|
|
}
|
|
|
|
// Add auth token if required
|
|
if (requireAuth) {
|
|
const token = getAuthToken();
|
|
if (token) {
|
|
(headers as Record<string, string>)["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Serialize body to JSON if it's an object (not FormData or string)
|
|
let serializedBody = body;
|
|
if (body && typeof body === 'object' && !(body instanceof FormData) && !(body instanceof String)) {
|
|
serializedBody = JSON.stringify(body);
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
...fetchOptions,
|
|
headers,
|
|
body: serializedBody,
|
|
});
|
|
|
|
// Handle 204 No Content
|
|
if (response.status === 204) {
|
|
return undefined as T;
|
|
}
|
|
|
|
// Parse response
|
|
const data = await response.json();
|
|
|
|
// Handle errors
|
|
if (!response.ok) {
|
|
// FastAPI returns errors in format { "detail": "..." }
|
|
const error: ApiError = {
|
|
detail: data.detail || "An error occurred",
|
|
};
|
|
throw error;
|
|
}
|
|
|
|
return data as T;
|
|
} catch (error: any) {
|
|
// Network error or parsing error
|
|
if (!error.detail) {
|
|
throw {
|
|
detail: error.message || "Network error occurred",
|
|
} as ApiError;
|
|
}
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
// ============================================================================
|
|
// Convenience Methods
|
|
// ============================================================================
|
|
|
|
export const api = {
|
|
get: <T = any>(endpoint: string, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, { ...options, method: "GET" }),
|
|
|
|
post: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, {
|
|
...options,
|
|
method: "POST",
|
|
body,
|
|
}),
|
|
|
|
put: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, {
|
|
...options,
|
|
method: "PUT",
|
|
body,
|
|
}),
|
|
|
|
patch: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, {
|
|
...options,
|
|
method: "PATCH",
|
|
body,
|
|
}),
|
|
|
|
delete: <T = any>(endpoint: string, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, { ...options, method: "DELETE" }),
|
|
};
|