export { createClient, resolveBaseUrl } from "./runtime/client"; export type { ApiClient, AuthHeaderResolver, CreateClientOptions, QueryParams, PathParams, } from "./runtime/client"; export { ApiError, isApiError } from "./runtime/client"; // Re-export API helpers export * from "./response-helpers"; // Import createClient for internal use import { createClient, ApiError } from "./runtime/client"; import { logger } from "@/lib/logger"; /** * Auth endpoints that should NOT trigger automatic logout on 401 * These are endpoints where 401 means "invalid credentials", not "session expired" */ const AUTH_ENDPOINTS = [ "/api/auth/login", "/api/auth/signup", "/api/auth/link-whmcs", "/api/auth/set-password", "/api/auth/reset-password", "/api/auth/check-password-needed", ]; /** * Check if a URL path is an auth endpoint */ function isAuthEndpoint(url: string): boolean { try { const urlPath = new URL(url).pathname; return AUTH_ENDPOINTS.some(endpoint => urlPath.endsWith(endpoint)); } catch { return AUTH_ENDPOINTS.some(endpoint => url.includes(endpoint)); } } /** * Extract error message from API error body * Handles both `{ message }` and `{ error: { message } }` formats */ function extractErrorMessage(body: unknown): string | null { if (!body || typeof body !== "object") { return null; } // Check for nested error.message format (standard API error response) const bodyWithError = body as { error?: { message?: unknown } }; if (bodyWithError.error && typeof bodyWithError.error === "object") { const errorMessage = bodyWithError.error.message; if (typeof errorMessage === "string") { return errorMessage; } } // Check for top-level message const bodyWithMessage = body as { message?: unknown }; if (typeof bodyWithMessage.message === "string") { return bodyWithMessage.message; } return null; } /** * Global error handler for API client * Handles authentication errors and triggers logout when needed */ async function handleApiError(response: Response): Promise { // Don't import useAuthStore at module level to avoid circular dependencies // We'll handle auth errors by dispatching a custom event that the auth system can listen to // Only dispatch logout event for 401s on non-auth endpoints // Auth endpoints (login, signup, etc.) return 401 for invalid credentials, // which should NOT trigger logout - just show the error message if (response.status === 401 && !isAuthEndpoint(response.url)) { logger.warn("Received 401 Unauthorized response - triggering logout"); // Dispatch a custom event that the auth system will listen to if (typeof window !== "undefined") { window.dispatchEvent( new CustomEvent("auth:unauthorized", { detail: { url: response.url, status: response.status }, }) ); } } // Still throw the error so the calling code can handle it let body: unknown; let message = response.statusText || `Request failed with status ${response.status}`; try { const cloned = response.clone(); const contentType = cloned.headers.get("content-type"); if (contentType?.includes("application/json")) { body = await cloned.json(); const extractedMessage = extractErrorMessage(body); if (extractedMessage) { message = extractedMessage; } } } catch { // Ignore body parse errors } throw new ApiError(message, response, body); } export const apiClient = createClient({ handleError: handleApiError, }); // Query keys for React Query - matching the expected structure export const queryKeys = { auth: { me: () => ["auth", "me"] as const, session: () => ["auth", "session"] as const, }, billing: { invoices: (params?: Record) => ["billing", "invoices", params] as const, invoice: (id: string) => ["billing", "invoice", id] as const, paymentMethods: () => ["billing", "payment-methods"] as const, }, subscriptions: { all: () => ["subscriptions"] as const, list: (params?: Record) => ["subscriptions", "list", params] as const, active: () => ["subscriptions", "active"] as const, stats: () => ["subscriptions", "stats"] as const, detail: (id: string) => ["subscriptions", "detail", id] as const, invoices: (id: number, params?: Record) => ["subscriptions", "invoices", id, params] as const, }, dashboard: { summary: () => ["dashboard", "summary"] as const, }, catalog: { products: () => ["catalog", "products"] as const, internet: { combined: () => ["catalog", "internet", "combined"] as const, }, sim: { combined: () => ["catalog", "sim", "combined"] as const, }, vpn: { combined: () => ["catalog", "vpn", "combined"] as const, }, }, orders: { list: () => ["orders", "list"] as const, detail: (id: string | number) => ["orders", "detail", String(id)] as const, }, support: { cases: (params?: Record) => ["support", "cases", params] as const, case: (id: string) => ["support", "case", id] as const, }, currency: { default: () => ["currency", "default"] as const, }, } as const;