// ===== API CLIENT ===== // Single place that talks to the backend. Has two modes: // • MOCK (default in this preview) — no network; callers keep their existing // localStorage behavior. Lets the demo work with no server. // • LIVE — real fetch() calls to the NRSA backend (see /backend). // // Mode is LIVE when an API base URL is configured, otherwise MOCK. // Configure by setting window.NRSA_CONFIG.apiBase in index.html, OR in a Vite // build via import.meta.env.VITE_API_BASE_URL (read at build time and injected). // // You can force a mode for testing from the console: // localStorage.setItem('nrsa_api_mode','live') // or 'mock' const API_BASE = (window.NRSA_CONFIG && window.NRSA_CONFIG.apiBase) || ''; function apiMode() { const forced = localStorage.getItem('nrsa_api_mode'); if (forced === 'live' || forced === 'mock') return forced; return API_BASE ? 'live' : 'mock'; } async function http(path, { method = 'GET', body } = {}) { const res = await fetch(`${API_BASE}${path}`, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, credentials: 'include', // send/receive the httpOnly session cookie body: body ? JSON.stringify(body) : undefined, }); let data = null; try { data = await res.json(); } catch (e) { /* no body */ } if (!res.ok) { const msg = (data && data.error) || `Request failed (${res.status})`; const err = new Error(msg); err.status = res.status; err.details = data && data.details; throw err; } return data; } const NRSAApi = { base: API_BASE, isLive: () => apiMode() === 'live', isMock: () => apiMode() === 'mock', // ---------- Auth ---------- // Returns { ok, message } in live mode. In mock mode the caller handles the code. requestOtp: (phone) => http('/api/auth/otp/request', { method: 'POST', body: { phone } }), verifyOtp: (phone, code) => http('/api/auth/otp/verify', { method: 'POST', body: { phone, code } }), socialLogin: (provider, token) => http('/api/auth/social', { method: 'POST', body: { provider, token } }), adminLogin: (email, password) => http('/api/auth/admin/login', { method: 'POST', body: { email, password } }), refresh: () => http('/api/auth/refresh', { method: 'POST' }), logout: () => http('/api/auth/logout', { method: 'POST' }), me: () => http('/api/auth/me'), // ---------- Profile & consent ---------- updateProfile: (fields) => http('/api/me', { method: 'PATCH', body: fields }), submitIdentity: (docType, storageKey) => http('/api/me/identity', { method: 'POST', body: { docType, storageKey } }), applyResponder: (payload) => http('/api/me/responder-application', { method: 'POST', body: payload }), recordConsent: (payload) => http('/api/consent', { method: 'POST', body: payload }), exportMyData: () => http('/api/me/data-export'), deleteMe: () => http('/api/me', { method: 'DELETE' }), // ---------- Crash reports ---------- createCrashReport: (payload) => http('/api/crash-reports', { method: 'POST', body: payload }), listCrashReports: (status) => http(`/api/crash-reports${status ? `?status=${status}` : ''}`), updateCrashStatus: (id, status) => http(`/api/crash-reports/${id}/status`, { method: 'PATCH', body: { status } }), // ---------- Donations ---------- createDonationIntent: (payload) => http('/api/donations/intent', { method: 'POST', body: payload }), // ---------- Admin ---------- adminStats: () => http('/api/admin/stats'), adminListResponders: (status = 'pending') => http(`/api/admin/responders?status=${status}`), adminApproveResponder: (id) => http(`/api/admin/responders/${id}/verify`, { method: 'POST' }), adminRejectResponder: (id) => http(`/api/admin/responders/${id}/reject`, { method: 'POST' }), adminListIdentity: (status = 'pending') => http(`/api/admin/identity?status=${status}`), adminReviewIdentity: (id, decision) => http(`/api/admin/identity/${id}/${decision}`, { method: 'POST' }), }; Object.assign(window, { NRSAApi });