|
|
@@ -0,0 +1,243 @@
|
|
|
+/**
|
|
|
+ * Unified API request utility
|
|
|
+ * Handles all HTTP requests with interceptors for authentication and error handling
|
|
|
+ */
|
|
|
+
|
|
|
+import { getToken } from "@/contexts/AuthContext";
|
|
|
+
|
|
|
+// API response structure
|
|
|
+export interface ApiResponse<T = unknown> {
|
|
|
+ success: boolean;
|
|
|
+ code: string; // API returns string type code
|
|
|
+ message: string;
|
|
|
+ data: T;
|
|
|
+}
|
|
|
+
|
|
|
+// Request configuration options
|
|
|
+export interface RequestOptions extends RequestInit {
|
|
|
+ params?: Record<string, unknown>;
|
|
|
+ skipAuth?: boolean;
|
|
|
+ timeout?: number;
|
|
|
+ returnFullResponse?: boolean; // Option to return full response instead of just data
|
|
|
+}
|
|
|
+
|
|
|
+// API base URL from environment variable
|
|
|
+const API_BASE_URL =
|
|
|
+ process.env.NEXT_PUBLIC_API_BASE_URL || "http://47.76.166.38:26001";
|
|
|
+
|
|
|
+/**
|
|
|
+ * Build URL with query parameters
|
|
|
+ */
|
|
|
+function buildUrl(url: string, params?: Record<string, unknown>): string {
|
|
|
+ const fullUrl = url.startsWith("http") ? url : `${API_BASE_URL}${url}`;
|
|
|
+
|
|
|
+ if (!params || Object.keys(params).length === 0) {
|
|
|
+ return fullUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ const searchParams = new URLSearchParams();
|
|
|
+ for (const [key, value] of Object.entries(params)) {
|
|
|
+ if (value !== undefined && value !== null) {
|
|
|
+ searchParams.append(key, String(value));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return `${fullUrl}?${searchParams.toString()}`;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Request timeout wrapper
|
|
|
+ */
|
|
|
+function fetchWithTimeout(
|
|
|
+ url: string,
|
|
|
+ options: RequestInit,
|
|
|
+ timeout: number = 30000
|
|
|
+): Promise<Response> {
|
|
|
+ return Promise.race([
|
|
|
+ fetch(url, options),
|
|
|
+ new Promise<Response>((_, reject) =>
|
|
|
+ setTimeout(() => reject(new Error("Request timeout")), timeout)
|
|
|
+ ),
|
|
|
+ ]);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Unified request function
|
|
|
+ */
|
|
|
+async function request<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ options: RequestOptions = {}
|
|
|
+): Promise<T> {
|
|
|
+ const {
|
|
|
+ params,
|
|
|
+ skipAuth = false,
|
|
|
+ timeout = 30000,
|
|
|
+ returnFullResponse = false,
|
|
|
+ headers = {},
|
|
|
+ ...restOptions
|
|
|
+ } = options;
|
|
|
+
|
|
|
+ // Build full URL with query parameters
|
|
|
+ const fullUrl = buildUrl(url, params);
|
|
|
+
|
|
|
+ // Prepare headers
|
|
|
+ const requestHeaders: Record<string, string> = {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ };
|
|
|
+
|
|
|
+ // Add custom headers
|
|
|
+ // X-Language: Language code from browser
|
|
|
+ const language =
|
|
|
+ typeof window !== "undefined" && navigator.language
|
|
|
+ ? navigator.language
|
|
|
+ : "en";
|
|
|
+ requestHeaders["X-Language"] = language;
|
|
|
+
|
|
|
+ // X-Client-Version: Client version number
|
|
|
+ const clientVersion = process.env.NEXT_PUBLIC_CLIENT_VERSION || "1.0.0";
|
|
|
+ requestHeaders["X-Client-Version"] = clientVersion;
|
|
|
+
|
|
|
+ // X-Client-Platform: Client Platform
|
|
|
+ const clientPlatform = "web";
|
|
|
+ requestHeaders["X-Client-Platform"] = clientPlatform;
|
|
|
+
|
|
|
+ // Merge provided headers
|
|
|
+ if (headers) {
|
|
|
+ Object.entries(headers).forEach(([key, value]) => {
|
|
|
+ if (typeof value === "string") {
|
|
|
+ requestHeaders[key] = value;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add authentication token if not skipped
|
|
|
+ if (!skipAuth) {
|
|
|
+ const token = getToken();
|
|
|
+ if (token) {
|
|
|
+ requestHeaders["X-Auth-Token"] = token;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Prepare fetch options
|
|
|
+ const fetchOptions: RequestInit = {
|
|
|
+ ...restOptions,
|
|
|
+ headers: requestHeaders,
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ // Make request with timeout
|
|
|
+ const response = await fetchWithTimeout(fullUrl, fetchOptions, timeout);
|
|
|
+
|
|
|
+ // Parse response
|
|
|
+ const result: ApiResponse<T> = await response.json();
|
|
|
+
|
|
|
+ // Handle HTTP errors
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(result.message || `HTTP Error: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle API business errors (code "1" means success)
|
|
|
+ if (result.code !== "1" && !result.success) {
|
|
|
+ throw new Error(result.message || "API Error");
|
|
|
+ }
|
|
|
+
|
|
|
+ // Return full response or just data based on option
|
|
|
+ return (returnFullResponse ? result : result.data) as T;
|
|
|
+ } catch (error) {
|
|
|
+ // Handle network errors
|
|
|
+ if (error instanceof Error) {
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ throw new Error("Network error");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * GET request
|
|
|
+ */
|
|
|
+export async function get<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ params?: Record<string, unknown>,
|
|
|
+ options?: RequestOptions
|
|
|
+): Promise<T> {
|
|
|
+ return request<T>(url, {
|
|
|
+ method: "GET",
|
|
|
+ params,
|
|
|
+ ...options,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * POST request
|
|
|
+ */
|
|
|
+export async function post<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ data?: unknown,
|
|
|
+ options?: RequestOptions
|
|
|
+): Promise<T> {
|
|
|
+ return request<T>(url, {
|
|
|
+ method: "POST",
|
|
|
+ body: JSON.stringify(data),
|
|
|
+ ...options,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * PUT request
|
|
|
+ */
|
|
|
+export async function put<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ data?: unknown,
|
|
|
+ options?: RequestOptions
|
|
|
+): Promise<T> {
|
|
|
+ return request<T>(url, {
|
|
|
+ method: "PUT",
|
|
|
+ body: JSON.stringify(data),
|
|
|
+ ...options,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * DELETE request
|
|
|
+ */
|
|
|
+export async function del<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ options?: RequestOptions
|
|
|
+): Promise<T> {
|
|
|
+ return request<T>(url, {
|
|
|
+ method: "DELETE",
|
|
|
+ ...options,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * PATCH request
|
|
|
+ */
|
|
|
+export async function patch<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ data?: unknown,
|
|
|
+ options?: RequestOptions
|
|
|
+): Promise<T> {
|
|
|
+ return request<T>(url, {
|
|
|
+ method: "PATCH",
|
|
|
+ body: JSON.stringify(data),
|
|
|
+ ...options,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Get full API response (including code, message, success, data)
|
|
|
+ * Use this when you need to access the complete response structure
|
|
|
+ */
|
|
|
+export async function getFullResponse<T = unknown>(
|
|
|
+ url: string,
|
|
|
+ options: RequestOptions = {}
|
|
|
+): Promise<ApiResponse<T>> {
|
|
|
+ return request<ApiResponse<T>>(url, {
|
|
|
+ ...options,
|
|
|
+ returnFullResponse: true,
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// Export default request function
|
|
|
+export default request;
|