// API client and shared application state. import { toast } from "./ui.js"; export const state = { me: null, // whoami response csrf: "", catalog: null, }; export class APIError extends Error { constructor(status, body) { const e = (body && body.error) || {}; super(e.message || ("HTTP " + status)); this.status = status; this.code = e.code || ""; this.details = e; } } let onUnauthorized = () => {}; export function setUnauthorizedHandler(fn) { onUnauthorized = fn; } // onChange is told about every applied change (the probation banner). let onChange = () => {}; export function setChangeHandler(fn) { onChange = fn; } // api calls the REST API. opts: {body, query}. export async function api(method, path, opts = {}) { const headers = { "Accept": "application/json" }; let body; if (opts.body !== undefined) { headers["Content-Type"] = "application/json"; body = JSON.stringify(opts.body); } if (method !== "GET" && state.csrf) headers["X-CSRF-Token"] = state.csrf; let url = "/api/v1" + path; if (opts.query) { const q = new URLSearchParams(); for (const [k, v] of Object.entries(opts.query)) if (v !== undefined && v !== null && v !== "") q.set(k, v); const s = q.toString(); if (s) url += (url.includes("?") ? "&" : "?") + s; } const resp = await fetch(url, { method, headers, body, credentials: "same-origin" }); if (resp.status === 204) return null; const ct = resp.headers.get("Content-Type") || ""; const data = ct.includes("application/json") ? await resp.json() : await resp.text(); if (!resp.ok) { if (resp.status === 401 && !path.startsWith("/auth/login")) onUnauthorized(); throw new APIError(resp.status, data); } return data; } export const get = (path, query) => api("GET", path, { query }); // change sends a configuration change and reports it: a toast with the // history entry (to undo it from History) and one per new warning. It // returns the response. export async function change(method, path, body, opts = {}) { const r = await api(method, path, { body, query: opts.query }); if (!opts.quiet) report(r); return r; } export function report(r) { if (!r) return; if (r.dry_run) toast("Checked by Housecarl and nftables: nothing was changed"); else if (r.applied && r.confirm_by) toast("Applied on probation (history #" + r.history_id + "): confirm it in the banner", "warn", 6000); else if (r.applied) toast("Applied and saved (history #" + r.history_id + ")"); else if (!(r.changes || []).length) toast("Nothing to change"); for (const w of r.warnings || []) toast(w.message + (w.object ? " (" + w.object + ")" : ""), "warn", 10000); if (r.applied) onChange(); } // can reports whether the user may read (or write) a resource. export function can(resource, write) { const me = state.me; if (!me) return false; if (me.admin) return true; const perms = me.permissions || []; return perms.includes(resource + ":write") || (!write && perms.includes(resource + ":read")); } // catalog returns the option catalog (cached). export async function catalog() { if (!state.catalog) state.catalog = await get("/catalog"); return state.catalog; }