13e12d97ae
Adds api.put helper, ListenBrainz API module with query/mutation helpers, a /settings route with token + enable/disable UI, and 5 tests. Adds Settings to the Shell nav. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
53 lines
1.6 KiB
TypeScript
53 lines
1.6 KiB
TypeScript
export type ApiError = {
|
|
code: string;
|
|
message: string;
|
|
status: number;
|
|
};
|
|
|
|
export type User = {
|
|
id: string;
|
|
username: string;
|
|
is_admin: boolean;
|
|
};
|
|
|
|
export type LoginResponse = {
|
|
token: string;
|
|
user: User;
|
|
};
|
|
|
|
export async function apiFetch(path: string, init?: RequestInit): Promise<unknown> {
|
|
const res = await fetch(path, {
|
|
credentials: 'same-origin',
|
|
headers: { 'Content-Type': 'application/json', ...(init?.headers ?? {}) },
|
|
...init
|
|
});
|
|
const body = res.status === 204 ? null : await res.json().catch(() => null);
|
|
if (!res.ok) {
|
|
if (res.status === 401) {
|
|
// Lazy import: auth/store imports from this file, so a top-level
|
|
// import would be circular. By the time any 401 actually happens
|
|
// at runtime, both modules have finished loading.
|
|
const { logout } = await import('$lib/auth/store.svelte');
|
|
await logout({ silent: true });
|
|
}
|
|
const envelope = body && (body as { error?: { code?: string; message?: string } }).error;
|
|
const err: ApiError = {
|
|
code: envelope?.code ?? 'unknown',
|
|
message: envelope?.message ?? res.statusText,
|
|
status: res.status
|
|
};
|
|
throw err;
|
|
}
|
|
return body;
|
|
}
|
|
|
|
export const api = {
|
|
get: <T>(path: string): Promise<T> => apiFetch(path) as Promise<T>,
|
|
post: <T>(path: string, body: unknown): Promise<T> =>
|
|
apiFetch(path, { method: 'POST', body: JSON.stringify(body) }) as Promise<T>,
|
|
put: <T>(path: string, body: unknown): Promise<T> =>
|
|
apiFetch(path, { method: 'PUT', body: JSON.stringify(body) }) as Promise<T>,
|
|
del: (path: string): Promise<null> =>
|
|
apiFetch(path, { method: 'DELETE' }) as Promise<null>
|
|
};
|