Files
plainleaf/client/spaces_ui/api.ts
T

111 lines
3.2 KiB
TypeScript

import type { RuntimeAvailability } from "./runtime_availability.ts";
import type { FieldError, UserInfo } from "./types.ts";
export async function api(
method: string,
path: string,
body?: unknown,
): Promise<any> {
const resp = await fetch(path, {
method,
headers: body ? { "Content-Type": "application/json" } : {},
body: body ? JSON.stringify(body) : undefined,
});
// Only 401 — "no valid session" — means the browser should go log in.
// A 403 is "signed in, but not permitted": it falls through to the normal
// error path below so the caller renders a message. Redirecting on 403
// would loop, since the login screen bounces a valid session straight back.
if (resp.status === 401) throw { unauthorized: true };
const json = await resp.json().catch(() => ({}));
if (!resp.ok) {
const errors = json.errors ?? [
{
field: "",
message: resp.status === 403 ? "Forbidden" : `HTTP ${resp.status}`,
},
];
if (resp.status === 404) errors.notFound = true;
throw errors;
}
return json;
}
/**
* Admin-only endpoints live under `api/admin/`. Split from `api()` so a
* mistaken call to an admin route from a non-admin screen is visible at the
* call site rather than buried in a path string.
*/
export function adminApi(
method: string,
path: string,
body?: unknown,
): Promise<any> {
return api(method, `api/admin/${path}`, body);
}
export function formatApiError(e: unknown): string {
if (Array.isArray(e)) {
return e
.map((fe: FieldError) =>
fe.field ? `${fe.field}: ${fe.message}` : fe.message,
)
.join(", ");
}
return "Request failed";
}
/** Server-level facts for admin screens. See `RuntimeAvailability`. */
export function getServerInfo(): Promise<{ runtimeApi: RuntimeAvailability }> {
return adminApi("GET", "server-info");
}
// --- User management (backed by users.json via the admin API) ------------
export function listUsers(): Promise<Record<string, UserInfo>> {
return adminApi("GET", "users");
}
export function getUser(name: string): Promise<UserInfo> {
return adminApi("GET", `users/${encodeURIComponent(name)}`);
}
export function getSession(): Promise<{ username: string; admin: boolean }> {
return api("GET", "api/session");
}
export function createUser(
username: string,
password: string,
admin: boolean,
): Promise<void> {
return adminApi("POST", "users", { username, password, admin });
}
export function deleteUser(name: string): Promise<void> {
return adminApi("DELETE", `users/${encodeURIComponent(name)}`);
}
export function setUserPassword(name: string, password: string): Promise<void> {
return adminApi("POST", `users/${encodeURIComponent(name)}/password`, {
password,
});
}
export function setUserAdmin(name: string, admin: boolean): Promise<void> {
return adminApi("PUT", `users/${encodeURIComponent(name)}`, { admin });
}
export async function createToken(user: string, name: string): Promise<string> {
const r = await adminApi("POST", `users/${encodeURIComponent(user)}/tokens`, {
name,
});
return r.token;
}
export function deleteToken(user: string, name: string): Promise<void> {
return adminApi(
"DELETE",
`users/${encodeURIComponent(user)}/tokens/${encodeURIComponent(name)}`,
);
}