208 lines
8.6 KiB
JavaScript
208 lines
8.6 KiB
JavaScript
|
|
async function _fetch(method, path, body) {
|
||
|
|
const opts = { method, headers: { 'Content-Type': 'application/json' }, credentials: 'include' };
|
||
|
|
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),
|
||
|
|
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'),
|
||
|
|
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}`),
|
||
|
|
|
||
|
|
// 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}`),
|
||
|
|
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),
|
||
|
|
|
||
|
|
// Status
|
||
|
|
status: () => get('/status'),
|
||
|
|
|
||
|
|
// Version (public)
|
||
|
|
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),
|
||
|
|
};
|