375 lines
11 KiB
TypeScript
375 lines
11 KiB
TypeScript
// ============================================================================
|
|
// API Client with Mock Support
|
|
// ============================================================================
|
|
|
|
import { API_URL, USE_MOCKS, STORAGE_KEYS } from "@/lib/utils/constants";
|
|
import { mockApiHandlers } from "@/lib/mocks/handlers";
|
|
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) {
|
|
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);
|
|
localStorage.setItem(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
|
|
};
|
|
|
|
// ============================================================================
|
|
// Mock Router
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Route request to appropriate mock handler
|
|
*/
|
|
const routeMockRequest = async (
|
|
endpoint: string,
|
|
options: RequestOptions
|
|
): Promise<any> => {
|
|
const method = options.method || "GET";
|
|
const params = options.params || {};
|
|
const body = options.body ? JSON.parse(options.body as string) : undefined;
|
|
|
|
console.log(`[MOCK API] ${method} ${endpoint}`, { params, body });
|
|
|
|
// Auth endpoints
|
|
if (endpoint === "/auth/init") {
|
|
return mockApiHandlers.authInit();
|
|
}
|
|
if (endpoint.startsWith("/auth/complete")) {
|
|
// Extract token from query params
|
|
const token =
|
|
params.token ||
|
|
(endpoint.includes("?")
|
|
? new URLSearchParams(endpoint.split("?")[1]).get("token")
|
|
: null);
|
|
return mockApiHandlers.authComplete({ token });
|
|
}
|
|
if (endpoint === "/auth/refresh") {
|
|
return mockApiHandlers.authRefresh(body);
|
|
}
|
|
if (endpoint === "/auth/me") {
|
|
return mockApiHandlers.authMe();
|
|
}
|
|
|
|
// Target Channels
|
|
if (endpoint === "/target_channels" && method === "GET") {
|
|
return mockApiHandlers.getTargetChannels();
|
|
}
|
|
if (endpoint.startsWith("/target_channels/") && method === "DELETE") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.deleteTargetChannel(id);
|
|
}
|
|
|
|
// External Channels
|
|
if (endpoint.startsWith("/external_channels/target/") && method === "GET") {
|
|
const targetChannelId = endpoint.split("/")[3];
|
|
return mockApiHandlers.getExternalChannels(targetChannelId);
|
|
}
|
|
if (endpoint === "/external_channels" && method === "POST") {
|
|
return mockApiHandlers.createExternalChannel(body);
|
|
}
|
|
if (
|
|
endpoint.startsWith("/external_channels/") &&
|
|
!endpoint.includes("/target/") &&
|
|
!endpoint.includes("/links") &&
|
|
method === "GET"
|
|
) {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.getExternalChannel(id);
|
|
}
|
|
if (
|
|
endpoint.startsWith("/external_channels/") &&
|
|
endpoint.includes("/links") &&
|
|
method === "PATCH"
|
|
) {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.updateExternalChannelLinks(id, body);
|
|
}
|
|
if (
|
|
endpoint.startsWith("/external_channels/") &&
|
|
!endpoint.includes("/links") &&
|
|
method === "PATCH"
|
|
) {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.updateExternalChannel(id, body);
|
|
}
|
|
if (endpoint.startsWith("/external_channels/") && method === "DELETE") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.deleteExternalChannel(id);
|
|
}
|
|
|
|
// Creatives
|
|
if (endpoint === "/creatives" && method === "GET") {
|
|
return mockApiHandlers.getCreatives(params);
|
|
}
|
|
if (endpoint === "/creatives" && method === "POST") {
|
|
return mockApiHandlers.createCreative(body);
|
|
}
|
|
if (endpoint.startsWith("/creatives/") && method === "GET") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.getCreative(id);
|
|
}
|
|
if (endpoint.startsWith("/creatives/") && method === "PATCH") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.updateCreative(id, body);
|
|
}
|
|
if (endpoint.startsWith("/creatives/") && method === "DELETE") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.deleteCreative(id);
|
|
}
|
|
|
|
// Placements
|
|
if (endpoint === "/placements" && method === "GET") {
|
|
return mockApiHandlers.getPlacements(params);
|
|
}
|
|
if (endpoint === "/placements" && method === "POST") {
|
|
return mockApiHandlers.createPlacement(body);
|
|
}
|
|
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "GET") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.getPlacement(id);
|
|
}
|
|
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "PATCH") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.updatePlacement(id, body);
|
|
}
|
|
if (endpoint.match(/^\/placements\/[^\/]+$/) && method === "DELETE") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.deletePlacement(id);
|
|
}
|
|
if (endpoint.includes("/views/fetch") && method === "POST") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.fetchPlacementViews(id);
|
|
}
|
|
if (endpoint.includes("/views/history") && method === "GET") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.getPlacementViewsHistory(id, params);
|
|
}
|
|
if (endpoint.includes("/views/manual") && method === "POST") {
|
|
const id = endpoint.split("/")[2];
|
|
return mockApiHandlers.setPlacementViewsManually(id, params?.views_count);
|
|
}
|
|
|
|
// Analytics
|
|
if (endpoint === "/analytics/overview") {
|
|
return mockApiHandlers.getAnalyticsOverview(params);
|
|
}
|
|
if (endpoint === "/analytics/costs") {
|
|
return mockApiHandlers.getAnalyticsCosts(params);
|
|
}
|
|
if (endpoint === "/analytics/channels") {
|
|
return mockApiHandlers.getAnalyticsChannels(params);
|
|
}
|
|
if (endpoint === "/analytics/creatives") {
|
|
return mockApiHandlers.getAnalyticsCreatives(params);
|
|
}
|
|
|
|
throw {
|
|
error: {
|
|
code: "NOT_IMPLEMENTED",
|
|
message: `Mock handler not implemented for ${method} ${endpoint}`,
|
|
},
|
|
};
|
|
};
|
|
|
|
// ============================================================================
|
|
// API Client
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Main API request function with mock support
|
|
*/
|
|
export const apiRequest = async <T = any>(
|
|
endpoint: string,
|
|
options: RequestOptions = {}
|
|
): Promise<T> => {
|
|
const { params, requireAuth = true, ...fetchOptions } = options;
|
|
|
|
// Use mocks if enabled
|
|
if (USE_MOCKS) {
|
|
try {
|
|
const result = await routeMockRequest(endpoint, options);
|
|
return result as T;
|
|
} catch (error) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Real API request
|
|
const url = buildUrl(`${API_URL}${endpoint}`, params);
|
|
|
|
const headers: HeadersInit = {
|
|
"Content-Type": "application/json",
|
|
...fetchOptions.headers,
|
|
};
|
|
|
|
// Add auth token if required
|
|
if (requireAuth) {
|
|
const token = getAuthToken();
|
|
if (token) {
|
|
(headers as Record<string, string>)["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...fetchOptions,
|
|
headers,
|
|
});
|
|
|
|
// Parse response
|
|
const data = await response.json();
|
|
|
|
// Handle errors
|
|
if (!response.ok) {
|
|
// FastAPI returns errors in format { "detail": "..." }
|
|
// Transform to our ApiError format
|
|
if (data.detail) {
|
|
throw {
|
|
error: {
|
|
code:
|
|
response.status === 401
|
|
? "UNAUTHORIZED"
|
|
: response.status === 403
|
|
? "FORBIDDEN"
|
|
: response.status === 404
|
|
? "NOT_FOUND"
|
|
: response.status === 422
|
|
? "VALIDATION_ERROR"
|
|
: "API_ERROR",
|
|
message: data.detail,
|
|
},
|
|
detail: data.detail,
|
|
};
|
|
}
|
|
// If it's already in our format, throw as is
|
|
const error: ApiError = data;
|
|
throw error;
|
|
}
|
|
|
|
return data as T;
|
|
} catch (error: any) {
|
|
// Network error or parsing error
|
|
if (!error.error) {
|
|
throw {
|
|
error: {
|
|
code: "NETWORK_ERROR",
|
|
message: 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: JSON.stringify(body),
|
|
}),
|
|
|
|
patch: <T = any>(endpoint: string, body?: any, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, {
|
|
...options,
|
|
method: "PATCH",
|
|
body: JSON.stringify(body),
|
|
}),
|
|
|
|
delete: <T = any>(endpoint: string, options?: RequestOptions) =>
|
|
apiRequest<T>(endpoint, { ...options, method: "DELETE" }),
|
|
|
|
// Special method for file upload
|
|
upload: async <T = any>(
|
|
endpoint: string,
|
|
formData: FormData,
|
|
options?: RequestOptions
|
|
): Promise<T> => {
|
|
// File import is now done on client side, no need for mock handling
|
|
const url = `${API_URL}${endpoint}`;
|
|
const token = getAuthToken();
|
|
|
|
const headers: HeadersInit = {};
|
|
if (token) {
|
|
headers["Authorization"] = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch(url, {
|
|
method: "POST",
|
|
headers,
|
|
body: formData,
|
|
...options,
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
throw data as ApiError;
|
|
}
|
|
|
|
return data as T;
|
|
},
|
|
};
|