// --------------------------------------------------------------------------- // Base API client for Casera web app // --------------------------------------------------------------------------- // All client-side requests go through Next.js API route handlers (proxy). // The proxy reads the httpOnly `casera-token` cookie and forwards it to the // Go API as an Authorization header. This avoids exposing the token to JS. // --------------------------------------------------------------------------- const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mycrib.treytartt.com/api'; /** * Server-only base URL. Falls back to the public one so that server * components / route handlers can reach the Go API directly. */ const SERVER_API_URL = process.env.API_URL || API_BASE_URL; // --------------------------------------------------------------------------- // Error class // --------------------------------------------------------------------------- export class ApiError extends Error { constructor( public status: number, message: string, public details?: Record, ) { super(message); this.name = 'ApiError'; } } // --------------------------------------------------------------------------- // Client-side fetcher (calls Next.js /api/proxy/... routes) // --------------------------------------------------------------------------- /** * Client-side authenticated fetch. Calls the Next.js catch-all proxy which * attaches the auth token from the httpOnly cookie before forwarding to Go. * * @param path API path *without* the `/api` prefix, e.g. `/tasks/` * @param options Standard RequestInit overrides */ export async function apiFetch( path: string, options: RequestInit = {}, ): Promise { // Ensure trailing slash (Go API requires it) const normalized = path.endsWith('/') ? path : `${path}/`; const url = `/api/proxy${normalized}`; const headers: Record = { 'Content-Type': 'application/json', 'X-Timezone': Intl.DateTimeFormat().resolvedOptions().timeZone, ...(options.headers as Record), }; // If the caller explicitly set Content-Type to something else (e.g. for // FormData uploads), remove our default so the browser can set the // multipart boundary automatically. if (options.body instanceof FormData) { delete headers['Content-Type']; } const res = await fetch(url, { ...options, headers }); if (!res.ok) { const body = await res.json().catch(() => ({ error: 'Unknown error' })); throw new ApiError( res.status, body.message || body.error || 'Request failed', body.details, ); } // Handle 204 No Content if (res.status === 204) return undefined as T; return res.json(); } // --------------------------------------------------------------------------- // Server-side fetcher (for server components & route handlers) // --------------------------------------------------------------------------- /** * Server-side fetch that reads the auth token from the `casera-token` cookie * and calls the Go API directly (no proxy hop). * * Only use this inside: * - `app/api/.../route.ts` handlers * - Server Components * - Server Actions */ export async function serverFetch( path: string, options: RequestInit = {}, ): Promise { // Dynamic import so this module can still be imported from client bundles // (the function itself should only be *called* on the server). const { cookies } = await import('next/headers'); const cookieStore = await cookies(); const token = cookieStore.get('casera-token')?.value; const normalized = path.endsWith('/') ? path : `${path}/`; const url = `${SERVER_API_URL}${normalized}`; const headers: Record = { 'Content-Type': 'application/json', 'X-Timezone': 'UTC', // Server-side default; client overrides via proxy ...(options.headers as Record), }; if (token) { headers['Authorization'] = `Token ${token}`; } if (options.body instanceof FormData) { delete headers['Content-Type']; } const res = await fetch(url, { ...options, headers, // Disable Next.js fetch cache for mutations; callers can override. cache: options.cache ?? 'no-store', }); if (!res.ok) { const body = await res.json().catch(() => ({ error: 'Unknown error' })); throw new ApiError( res.status, body.message || body.error || 'Request failed', body.details, ); } if (res.status === 204) return undefined as T; return res.json(); }