// Read CSRF token from cookie function getCsrfToken() { if (typeof document === 'undefined') return ''; const name = 'bt_csrf_token'; const match = document.cookie.match(new RegExp(name + '=([^;]+)')); return match ? match[1] : ''; } async function _fetch(method, path, body) { const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include' }; // Add CSRF token header for state-changing methods if (['POST', 'PUT', 'DELETE', 'PATCH'].includes(method)) { const csrfToken = getCsrfToken(); if (csrfToken) { opts.headers['x-csrf-token'] = csrfToken; } } if (body !== undefined) opts.body = JSON.stringify(body); const res = await fetch('/api' + path, opts); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; } const get = (path) => _fetch('GET', path); const post = (path, body) => _fetch('POST', path, body); const put = (path, body) => _fetch('PUT', path, body); const del = (path) => _fetch('DELETE', path); function filenameFromDisposition(value) { if (!value) return null; const match = value.match(/filename="?([^"]+)"?/i); return match ? match[1] : null; } export const api = { // Auth me: () => get('/auth/me'), authMode: () => get('/auth/mode'), login: (data) => post('/auth/login', data), logout: () => post('/auth/logout'), restoreMultiUserMode: () => post('/auth/restore-multi-user-mode'), changePassword: (data) => post('/auth/change-password', data), acknowledgePrivacy: () => post('/auth/acknowledge-privacy'), // Admin hasUsers: () => get('/admin/has-users'), adminUsers: () => get('/admin/users'), createUser: (data) => post('/admin/users', data), resetPassword: (id, data) => put(`/admin/users/${id}/password`, data), updateUserRole: (id, data) => put(`/admin/users/${id}/role`, data), updateUserActive: (id, data) => put(`/admin/users/${id}/active`, data), deleteUser: (id) => del(`/admin/users/${id}`), authModeConfig: () => get('/admin/auth-mode'), setAuthMode: (data) => put('/admin/auth-mode', data), testOidcConfig: (data) => post('/admin/auth-mode/oidc-test', data), adminBackups: () => get('/admin/backups'), createAdminBackup: () => post('/admin/backups'), deleteAdminBackup: (id) => del(`/admin/backups/${encodeURIComponent(id)}`), restoreAdminBackup: (id) => post(`/admin/backups/${encodeURIComponent(id)}/restore`), adminBackupSettings: () => get('/admin/backups/settings'), saveAdminBackupSettings: (data) => put('/admin/backups/settings', data), runScheduledBackupNow: () => post('/admin/backups/run-scheduled-now'), adminCleanup: () => get('/admin/cleanup'), saveAdminCleanup: (data) => put('/admin/cleanup', data), runAdminCleanup: () => post('/admin/cleanup/run'), seedDemoData: () => post('/user/seed-demo-data'), clearDemoData: () => post('/user/clear-demo-data'), seededStatus: () => get('/user/seeded-status'), downloadAdminBackup: async (id) => { const res = await fetch(`/api/admin/backups/${encodeURIComponent(id)}/download`, { credentials: 'include', }); if (!res.ok) { let data = {}; try { data = await res.json(); } catch {} throw new Error(data.error || `HTTP ${res.status}`); } return { blob: await res.blob(), filename: filenameFromDisposition(res.headers.get('Content-Disposition')) || id, }; }, importAdminBackup: async (file) => { const res = await fetch('/api/admin/backups/import', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream' }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, // Notifications (admin) notifAdmin: () => get('/notifications/admin'), saveNotifAdmin: (data) => put('/notifications/admin', data), testEmail: (data) => post('/notifications/test', data), notifMe: () => get('/notifications/me'), saveNotifMe: (data) => put('/notifications/me', data), // Profile profile: () => get('/profile'), updateProfile: (data) => _fetch('PATCH', '/profile', data), profileSettings: () => get('/profile/settings'), updateProfileSettings: (data) => _fetch('PATCH', '/profile/settings', data), changeProfilePassword: (data) => post('/profile/change-password', data), profileExports: () => get('/profile/exports'), profileImportHistory: () => get('/profile/import-history'), // Tracker tracker: (y, m) => get(`/tracker?year=${y}&month=${m}`), upcomingBills: (days = 30) => get(`/tracker/upcoming?days=${days}`), // Calendar calendar: (y, m) => get(`/calendar?year=${y}&month=${m}`), // Summary summary: (y, m) => get(`/summary?year=${y}&month=${m}`), saveSummaryIncome: (data) => put('/summary/income', data), getMonthlyStartingAmounts: (y, m) => get(`/monthly-starting-amounts?year=${y}&month=${m}`), updateMonthlyStartingAmounts: (data) => put('/monthly-starting-amounts', data), // Bills bills: () => get('/bills'), allBills: () => get('/bills?inactive=true'), bill: (id) => get(`/bills/${id}`), createBill: (data) => post('/bills', data), updateBill: (id, data) => put(`/bills/${id}`, data), deleteBill: (id) => del(`/bills/${id}`), togglePaid: (id, data) => post(`/bills/${id}/toggle-paid`, data), billPayments: (id, p, l) => get(`/bills/${id}/payments?page=${p||1}&limit=${l||20}`), billMonthlyState: (id, y, m) => get(`/bills/${id}/monthly-state?year=${y}&month=${m}`), saveBillMonthlyState: (id, data) => put(`/bills/${id}/monthly-state`, data), billHistoryRanges: (id) => get(`/bills/${id}/history-ranges`), createBillHistoryRange: (id, data) => post(`/bills/${id}/history-ranges`, data), updateBillHistoryRange: (id, rangeId, data) => put(`/bills/${id}/history-ranges/${rangeId}`, data), deleteBillHistoryRange: (id, rangeId) => del(`/bills/${id}/history-ranges/${rangeId}`), // Payments quickPay: (data) => post('/payments/quick', data), bulkPay: (items) => post('/payments/bulk', items), createPayment: (data) => post('/payments', data), updatePayment: (id, data) => put(`/payments/${id}`, data), deletePayment: (id) => del(`/payments/${id}`), restorePayment: (id) => post(`/payments/${id}/restore`), // Categories categories: () => get('/categories'), createCategory: (data) => post('/categories', data), updateCategory: (id, data) => put(`/categories/${id}`, data), deleteCategory: (id) => del(`/categories/${id}`), // Settings settings: () => get('/settings'), saveSettings: (data) => put('/settings', data), // Analytics analyticsSummary: (params = {}) => { const qs = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') qs.set(key, String(value)); }); const query = qs.toString(); return get(`/analytics/summary${query ? `?${query}` : ''}`); }, // Status status: () => get('/status'), // Version (public) about: () => get('/about'), aboutAdmin: () => get('/about-admin'), version: () => get('/version'), releaseHistory: () => get('/version/history'), // Export (returns a URL to navigate to, not a fetch) exportUrl: (year, fmt) => `/api/export?year=${year}&format=${fmt||'csv'}`, // Spreadsheet Import previewSpreadsheetImport: async (file, options = {}) => { const params = new URLSearchParams(); if (options.parseAllSheets) params.set('parse_all_sheets', 'true'); if (options.defaultYear) params.set('year', String(options.defaultYear)); if (options.defaultMonth) params.set('month', String(options.defaultMonth)); const qs = params.toString(); const res = await fetch(`/api/import/spreadsheet/preview${qs ? `?${qs}` : ''}`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream', ...(file.name ? { 'X-Filename': file.name } : {}), }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, applySpreadsheetImport: (data) => post('/import/spreadsheet/apply', data), importHistory: () => get('/import/history'), // User SQLite import previewUserDbImport: async (file) => { const res = await fetch('/api/import/user-db/preview', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/octet-stream', ...(file.name ? { 'X-Filename': file.name } : {}), }, body: file, }); const data = await res.json(); if (!res.ok) { const err = new Error(data.message || data.error || `HTTP ${res.status}`); err.status = res.status; err.data = data; err.details = data.details || []; err.code = data.code; throw err; } return data; }, applyUserDbImport: (data) => post('/import/user-db/apply', data), };