// 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 teams = () => call('GET', '/teams'); export const createTeam = (name) => call('POST', '/teams', { body: { name } }); export const renameTeam = (id, name) => call('PUT', `/teams/${id}`, { body: { name } }); export const deleteTeam = (id) => call('DELETE', `/teams/${id}`); // Administration. Every one of these is refused with 403 for anybody without // the flag, so the UI hides the section rather than guarding it. export const adminTeams = () => call('GET', '/admin/teams'); export const adminSettings = () => call('GET', '/admin/settings'); export const setAdminSettings = (body) => call('PUT', '/admin/settings', { body }); export const setUserAdmin = (id, isAdmin) => call('PUT', `/users/${id}/admin`, { body: { is_admin: isAdmin } }); export const setUserDisabled = (id, disabled) => call('PUT', `/users/${id}/disabled`, { body: { disabled } }); export const schedule = (teamID, from, to) => call('GET', `/teams/${teamID}/schedule`, { query: { from, to } }); // One entry per team the viewer belongs to, for the teams that have somebody // scheduled today. An empty array means nobody anywhere, which is a real answer // rather than an error — unlike the pre-teams endpoint, which 404ed. export const onCallNow = () => call('GET', '/schedule/current');