web/src/lib/api.ts
8.5 KB · sha256:2d76dd01939abad0946487e210ccd5e9028c6c75ebad510bf3df2da2ce81d477
import { getActiveToken, handleUnauthorized } from "./auth";
import type {
GroupBundleDetail,
GroupBundleSummary,
GroupDetail,
GroupSummary,
Overview,
PlayerDetail,
PlayerSummary,
TrackDetail,
TrackSummary,
} from "./types";
async function call<T>(
path: string,
init?: RequestInit & { json?: unknown },
): Promise<T> {
const opts: RequestInit = { ...init };
const headers = new Headers(init?.headers || {});
const token = getActiveToken();
if (token) headers.set("Authorization", `Bearer ${token}`);
if (init?.json !== undefined) {
headers.set("Content-Type", "application/json");
opts.body = JSON.stringify(init.json);
delete (opts as { json?: unknown }).json;
}
opts.headers = headers;
const res = await fetch(path, opts);
if (res.status === 401) {
handleUnauthorized();
throw new Error("session expired");
}
const text = await res.text();
const data = text ? JSON.parse(text) : null;
if (!res.ok) {
const msg =
(data && (data.error || data.message)) ||
`${res.status} ${res.statusText}`;
throw new Error(msg);
}
return data as T;
}
export const api = {
overview: () => call<Overview>("/api/overview"),
handlers: () => call<string[]>("/api/handlers"),
players: {
list: (q?: string) =>
call<PlayerSummary[]>("/api/players" + (q ? `?q=${encodeURIComponent(q)}` : "")),
get: (uuid: string) => call<PlayerDetail>(`/api/players/${uuid}`),
permissions: (uuid: string) =>
call<{ key: string; value: boolean }[]>(`/api/players/${uuid}/permissions`),
setPrimaryGroup: (uuid: string, group: string) =>
call(`/api/players/${uuid}/primary-group`, { method: "POST", json: { group } }),
addGroup: (uuid: string, group: string) =>
call(`/api/players/${uuid}/groups`, { method: "POST", json: { group } }),
removeGroup: (uuid: string, group: string) =>
call(`/api/players/${uuid}/groups/${group}`, { method: "DELETE" }),
setPrefix: (uuid: string, text: string, priority?: number) =>
call(`/api/players/${uuid}/prefix`, { method: "POST", json: { text, priority } }),
setSuffix: (uuid: string, text: string, priority?: number) =>
call(`/api/players/${uuid}/suffix`, { method: "POST", json: { text, priority } }),
grant: (uuid: string, permission: string, value?: boolean) =>
call(`/api/players/${uuid}/permissions`, {
method: "POST",
json: { permission, value },
}),
revoke: (uuid: string, perm: string) =>
call(`/api/players/${uuid}/permissions/${encodeURIComponent(perm)}`, {
method: "DELETE",
}),
promote: (uuid: string, track: string) =>
call<{ ok: boolean; from?: string | null; to?: string; reason?: string }>(
`/api/players/${uuid}/promote`,
{ method: "POST", json: { track } },
),
demote: (uuid: string, track: string) =>
call<{ ok: boolean; from?: string | null; to?: string; reason?: string }>(
`/api/players/${uuid}/demote`,
{ method: "POST", json: { track } },
),
clear: (uuid: string) =>
call(`/api/players/${uuid}/clear`, { method: "POST" }),
},
groups: {
list: () => call<GroupSummary[]>("/api/groups"),
get: (name: string) => call<GroupDetail>(`/api/groups/${name}`),
create: (name: string, weight?: number) =>
call<{ name: string }>("/api/groups", { method: "POST", json: { name, weight } }),
clone: (source: string, newName: string, opts?: { weight?: number; displayName?: string }) =>
call<{ name: string }>(`/api/groups/${source}/clone`, {
method: "POST",
json: { newName, ...opts },
}),
bulkCreate: (opts: {
names?: string[];
template?: string;
start?: number;
end?: number;
baseWeight?: number;
weightIncrement?: number;
cloneFrom?: string;
chainParents?: boolean;
inheritFrom?: string;
bundle?: { name: string; create?: boolean };
}) =>
call<{ created: string[]; failed: { name: string; reason: string }[] }>(
"/api/groups/bulk",
{ method: "POST", json: opts },
),
delete: (name: string) =>
call(`/api/groups/${name}`, { method: "DELETE" }),
setWeight: (name: string, weight: number) =>
call(`/api/groups/${name}/weight`, { method: "POST", json: { weight } }),
setPrefix: (name: string, text: string, priority?: number) =>
call(`/api/groups/${name}/prefix`, { method: "POST", json: { text, priority } }),
setSuffix: (name: string, text: string, priority?: number) =>
call(`/api/groups/${name}/suffix`, { method: "POST", json: { text, priority } }),
addParent: (name: string, parent: string) =>
call(`/api/groups/${name}/parents`, { method: "POST", json: { parent } }),
removeParent: (name: string, parent: string) =>
call(`/api/groups/${name}/parents/${parent}`, { method: "DELETE" }),
grant: (name: string, permission: string) =>
call(`/api/groups/${name}/permissions`, { method: "POST", json: { permission } }),
revoke: (name: string, perm: string) =>
call(`/api/groups/${name}/permissions/${encodeURIComponent(perm)}`, {
method: "DELETE",
}),
setDefault: (name: string) =>
call(`/api/groups/${name}/default`, { method: "POST", json: {} }),
},
bundles: {
list: () => call<GroupBundleSummary[]>("/api/bundles"),
get: (name: string) => call<GroupBundleDetail>(`/api/bundles/${name}`),
create: (
name: string,
opts?: { displayName?: string; groups?: string[] },
) =>
call<{ name: string }>("/api/bundles", {
method: "POST",
json: { name, ...opts },
}),
delete: (name: string) =>
call(`/api/bundles/${name}`, { method: "DELETE" }),
/** Replace the whole ordered list (used for reordering). */
setGroups: (name: string, groups: string[]) =>
call(`/api/bundles/${name}/groups`, {
method: "PUT",
json: { groups },
}),
addGroup: (name: string, group: string) =>
call(`/api/bundles/${name}/groups`, {
method: "POST",
json: { group },
}),
removeGroup: (name: string, group: string) =>
call(`/api/bundles/${name}/groups/${group}`, { method: "DELETE" }),
},
tracks: {
list: () => call<TrackSummary[]>("/api/tracks"),
get: (name: string) => call<TrackDetail>(`/api/tracks/${name}`),
create: (name: string) =>
call<{ name: string }>("/api/tracks", { method: "POST", json: { name } }),
delete: (name: string) =>
call(`/api/tracks/${name}`, { method: "DELETE" }),
addRung: (name: string, group: string, position?: number) =>
call(`/api/tracks/${name}/rungs`, {
method: "POST",
json: { group, position },
}),
/**
* Bulk-append rungs with per-row template expansion. Each requirement
* / cost-amount can use the `{}` DSL: bare PAPI placeholders stay
* literal; math expressions are evaluated against per-row vars
* `i` / `n` / `idx` plus anything passed in `vars`.
*/
bulkAppendRungs: (
name: string,
body: {
groups: string[];
requirements?: string[];
costs?: { handler: string; amount: number | string }[];
vars?: Record<string, number>;
autoPromote?: boolean;
position?: number | null;
},
) =>
call<{ created: number; position: number }>(
`/api/tracks/${name}/rungs/bulk`,
{ method: "POST", json: body },
),
removeRung: (name: string, group: string) =>
call(`/api/tracks/${name}/rungs/${group}`, { method: "DELETE" }),
addRequirement: (name: string, index: number, expression: string) =>
call(`/api/tracks/${name}/rungs/${index}/requirements`, {
method: "POST",
json: { expression },
}),
removeRequirement: (name: string, index: number, reqIndex: number) =>
call(`/api/tracks/${name}/rungs/${index}/requirements/${reqIndex}`, {
method: "DELETE",
}),
addCost: (name: string, index: number, handler: string, amount: number) =>
call(`/api/tracks/${name}/rungs/${index}/costs`, {
method: "POST",
json: { handler, amount },
}),
removeCost: (name: string, index: number, costIndex: number) =>
call(`/api/tracks/${name}/rungs/${index}/costs/${costIndex}`, {
method: "DELETE",
}),
setAutoPromote: (name: string, index: number, on: boolean) =>
call(`/api/tracks/${name}/rungs/${index}/autopromote`, {
method: "POST",
json: { on },
}),
setPollSeconds: (name: string, seconds: number) =>
call(`/api/tracks/${name}/poll-seconds`, {
method: "POST",
json: { seconds },
}),
},
};