import { getDeviceId } from '../lib/device'; export type UploadError = Error & { code?: string; status?: number; meta?: Record; }; function getCsrfToken(): string | null { // Method 1: Meta tag (preferred for SPA) const metaToken = document.querySelector('meta[name="csrf-token"]'); if (metaToken) { return (metaToken as HTMLMetaElement).getAttribute('content') || null; } // Method 2: XSRF-TOKEN cookie (Sanctum fallback) const name = 'XSRF-TOKEN='; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(';'); for (let i = 0; i < ca.length; i++) { const c = ca[i].trimStart(); if (c.startsWith(name)) { const token = c.substring(name.length); try { // Decode base64 if needed return decodeURIComponent(atob(token)); } catch { return token; } } } console.warn('No CSRF token found - API requests may fail'); return null; } function getCsrfHeaders(): Record { const token = getCsrfToken(); const headers: Record = { 'X-Device-Id': getDeviceId(), 'Accept': 'application/json', }; if (token) { headers['X-CSRF-TOKEN'] = token; headers['X-XSRF-TOKEN'] = token; } return headers; } export async function likePhoto(id: number): Promise { const headers = getCsrfHeaders(); const res = await fetch(`/api/v1/photos/${id}/like`, { method: 'POST', credentials: 'include', headers: { ...headers, 'Content-Type': 'application/json', }, }); if (!res.ok) { let payload: any = null; try { payload = await res.clone().json(); } catch {} if (res.status === 419) { const error: UploadError = new Error('CSRF token mismatch. Please refresh the page and try again.'); error.code = 'csrf_mismatch'; error.status = res.status; throw error; } const error: UploadError = new Error( payload?.error?.message ?? `Like failed: ${res.status}` ); error.code = payload?.error?.code ?? 'like_failed'; error.status = res.status; if (payload?.error?.meta) { error.meta = payload.error.meta as Record; } throw error; } const json = await res.json(); return json.likes_count ?? json.data?.likes_count ?? 0; } export async function uploadPhoto(eventToken: string, file: File, taskId?: number, emotionSlug?: string): Promise { const formData = new FormData(); formData.append('photo', file, `photo-${Date.now()}.jpg`); if (taskId) formData.append('task_id', taskId.toString()); if (emotionSlug) formData.append('emotion_slug', emotionSlug); formData.append('device_id', getDeviceId()); const res = await fetch(`/api/v1/events/${encodeURIComponent(eventToken)}/upload`, { method: 'POST', credentials: 'include', body: formData, // Don't set Content-Type for FormData - let browser handle it with boundary }); if (!res.ok) { let payload: any = null; try { payload = await res.clone().json(); } catch {} if (res.status === 419) { const csrfError: UploadError = new Error( 'CSRF token mismatch during upload. Please refresh the page and try again.' ); csrfError.code = 'csrf_mismatch'; csrfError.status = res.status; throw csrfError; } const error: UploadError = new Error( payload?.error?.message ?? `Upload failed: ${res.status}` ); error.code = payload?.error?.code ?? 'upload_failed'; error.status = res.status; if (payload?.error?.meta) { error.meta = payload.error.meta as Record; } throw error; } const json = await res.json(); return json.photo_id ?? json.id ?? json.data?.id ?? 0; }