// The terdut-server client. The page is served by the server itself, so every // call is same-origin and carries the session cookie. export class ApiError extends Error { constructor(status, message) { super(message); this.name = 'ApiError'; this.status = status; } } // Called whenever the server says the session is gone, so the app can put the // login form back up wherever the user happened to be. let onUnauthorized = () => {}; export function setUnauthorizedHandler(fn) { onUnauthorized = fn; } async function call(method, path, { query, body, signal } = {}) { const url = new URL('/api' + path, location.origin); for (const [k, v] of Object.entries(query || {})) { if (v === '' || v == null) continue; url.searchParams.set(k, v); } const headers = { Accept: 'application/json' }; if (body !== undefined) headers['Content-Type'] = 'application/json'; let resp; try { resp = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), credentials: 'same-origin', signal, }); } catch (err) { if (err.name === 'AbortError') throw err; throw new ApiError(0, 'Cannot reach the server.'); } if (resp.status === 204) return null; let data = null; try { data = await resp.json(); } catch { /* non-JSON body: keep null */ } if (!resp.ok) { if (resp.status === 401 && path !== '/login') onUnauthorized(); const message = (data && data.error) || `Server answered ${resp.status}.`; throw new ApiError(resp.status, message); } return data; } // session export const me = () => call('GET', '/me'); export const login = (username, password) => call('POST', '/login', { body: { username, password } }); export const logout = () => call('POST', '/logout'); export const setPassword = (userID, password, currentPassword) => call('PUT', `/users/${userID}/password`, { body: { password, current_password: currentPassword } }); // users export const users = () => call('GET', '/users'); // incidents export const incidents = (query, opts) => call('GET', '/incidents', { query, ...opts }); export const incident = (id) => call('GET', `/incidents/${id}`); export const timeline = (id) => call('GET', `/incidents/${id}/timeline`); export const acknowledge = (id) => call('POST', `/incidents/${id}/acknowledge`); export const unacknowledge = (id) => call('DELETE', `/incidents/${id}/acknowledge`); export const resolve = (id) => call('POST', `/incidents/${id}/resolve`); export const assign = (id, userID) => call('POST', `/incidents/${id}/assign`, { body: { user_id: userID } }); export const snooze = (id, spec) => call('POST', `/incidents/${id}/snooze`, { body: spec }); export const unsnooze = (id) => call('DELETE', `/incidents/${id}/snooze`); export const archive = (id) => call('POST', `/incidents/${id}/archive`); export const unarchive = (id) => call('DELETE', `/incidents/${id}/archive`); export const addNote = (id, content) => call('POST', `/incidents/${id}/notes`, { body: { content } }); export const deleteNote = (id, eventID) => call('DELETE', `/incidents/${id}/notes/${eventID}`); // alerts export const alerts = (query, opts) => call('GET', '/alerts', { query, ...opts }); // schedule export const schedule = (from, to) => call('GET', '/schedule', { query: { from, to } }); export async function onCallNow() { try { return await call('GET', '/schedule/current'); } catch (err) { if (err instanceof ApiError && err.status === 404) return null; throw err; } }