/** * Cliente HTTP centralizado para frontend-client. * Usa cookies HttpOnly (client_access_token) como fuente primaria de auth, * con Bearer token como complemento cuando está disponible en memoria. */ import { auth } from '$lib/stores/auth'; import { get } from 'svelte/store'; const API_BASE = '/api/v1'; interface RequestOptions extends RequestInit { params?: Record; } async function request(endpoint: string, options: RequestOptions = {}): Promise { const { params, ...init } = options; let url = `${API_BASE}${endpoint}`; if (params) { const filteredParams = Object.entries(params) .filter(([, value]) => value !== undefined && value !== null && value !== '') .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}); if (Object.keys(filteredParams).length > 0) { url += `?${new URLSearchParams(filteredParams).toString()}`; } } const authState = get(auth); const headers = new Headers(init.headers); // Bearer header cuando el token está en memoria (sesión activa sin reload) if (authState.token) { headers.set('Authorization', `Bearer ${authState.token}`); } if (authState.user?.tenant_id && !headers.has('X-Tenant-ID')) { headers.set('X-Tenant-ID', authState.user.tenant_id); } if (!headers.has('Content-Type')) { headers.set('Content-Type', 'application/json'); } // Identifica este frontend para que el backend use client_access_token headers.set('X-App', 'client'); const response = await fetch(url, { ...init, credentials: 'include', headers }); if (response.status === 401) { if (typeof window !== 'undefined') { window.location.href = '/login'; } throw new Error('Unauthorized'); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `API error: ${response.statusText}`); } if (response.status === 204) { return {} as T; } return response.json(); } async function downloadFile(endpoint: string, filename: string): Promise { const authState = get(auth); const headers = new Headers(); if (authState.token) { headers.set('Authorization', `Bearer ${authState.token}`); } if (authState.user?.tenant_id) { headers.set('X-Tenant-ID', authState.user.tenant_id); } headers.set('X-App', 'client'); const response = await fetch(`${API_BASE}${endpoint}`, { method: 'GET', credentials: 'include', headers }); if (response.status === 401) { if (typeof window !== 'undefined') window.location.href = '/login'; throw new Error('Unauthorized'); } if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new Error(errorData.detail || `Download error: ${response.statusText}`); } const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } export const api = { get: (endpoint: string, params?: Record) => request(endpoint, { method: 'GET', params }), post: (endpoint: string, body?: any) => request(endpoint, { method: 'POST', body: body !== undefined ? JSON.stringify(body) : undefined }), put: (endpoint: string, body?: any) => request(endpoint, { method: 'PUT', body: body !== undefined ? JSON.stringify(body) : undefined }), patch: (endpoint: string, body?: any) => request(endpoint, { method: 'PATCH', body: body !== undefined ? JSON.stringify(body) : undefined }), delete: (endpoint: string) => request(endpoint, { method: 'DELETE' }), downloadFile: (endpoint: string, filename: string) => downloadFile(endpoint, filename) };