| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- /**
- * Minimal frontend-facing API client for the Lieferscheine backend.
- *
- * Goals:
- * - Centralize fetch defaults (credentials + no-store) to match backend caching rules.
- * - Provide a single, predictable error shape via ApiClientError.
- * - Offer thin domain helpers (login/logout/me/branches/...).
- *
- * Notes:
- * - JavaScript only (no TypeScript). We use JSDoc for "typed-by-convention".
- * - This client is intentionally small. It is not an SDK.
- */
- /**
- * @typedef {Object} ApiErrorPayload
- * @property {{ message: string, code: string, details?: any }} error
- */
- export class ApiClientError extends Error {
- /**
- * @param {{
- * status: number,
- * code: string,
- * message: string,
- * details?: any,
- * url?: string,
- * method?: string,
- * cause?: any
- * }} input
- */
- constructor({ status, code, message, details, url, method, cause }) {
- super(message, cause ? { cause } : undefined);
- this.name = "ApiClientError";
- this.status = status;
- this.code = code;
- if (details !== undefined) this.details = details;
- if (url) this.url = url;
- if (method) this.method = method;
- }
- }
- const DEFAULT_HEADERS = { Accept: "application/json" };
- function resolveUrl(path, baseUrl) {
- if (/^https?:\/\//i.test(path)) return path;
- const base = (baseUrl || "").trim();
- if (!base) return path;
- return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
- }
- function isJsonResponse(response) {
- const ct = response.headers.get("content-type") || "";
- return ct.toLowerCase().includes("application/json");
- }
- export async function apiFetch(path, options = {}) {
- const {
- method = "GET",
- headers = {},
- body,
- baseUrl,
- fetchImpl = fetch,
- } = options;
- const url = resolveUrl(path, baseUrl);
- const init = {
- method,
- credentials: "include",
- cache: "no-store",
- headers: { ...DEFAULT_HEADERS, ...headers },
- };
- if (body !== undefined) {
- if (typeof body === "string") {
- init.body = body;
- } else {
- init.body = JSON.stringify(body);
- if (!init.headers["Content-Type"]) {
- init.headers["Content-Type"] = "application/json";
- }
- }
- }
- let response;
- try {
- response = await fetchImpl(url, init);
- } catch (err) {
- throw new ApiClientError({
- status: 0,
- code: "CLIENT_NETWORK_ERROR",
- message: "Network error",
- url,
- method,
- cause: err,
- });
- }
- if (response.status === 204) return null;
- if (isJsonResponse(response)) {
- let payload;
- try {
- payload = await response.json();
- } catch (err) {
- throw new ApiClientError({
- status: response.status,
- code: "CLIENT_INVALID_JSON",
- message: "Invalid JSON response",
- url,
- method,
- cause: err,
- });
- }
- if (response.ok) return payload;
- const maybeError = payload;
- if (maybeError?.error?.code && maybeError?.error?.message) {
- throw new ApiClientError({
- status: response.status,
- code: maybeError.error.code,
- message: maybeError.error.message,
- details: maybeError.error.details,
- url,
- method,
- });
- }
- throw new ApiClientError({
- status: response.status,
- code: "CLIENT_HTTP_ERROR",
- message: `Request failed with status ${response.status}`,
- details: payload,
- url,
- method,
- });
- }
- const text = await response.text().catch(() => "");
- if (response.ok) return text || null;
- throw new ApiClientError({
- status: response.status,
- code: "CLIENT_HTTP_ERROR",
- message: text || `Request failed with status ${response.status}`,
- url,
- method,
- });
- }
- /* -------------------------------------------------------------------------- */
- /* Domain helpers */
- /* -------------------------------------------------------------------------- */
- /**
- * @typedef {Object} MeUser
- * @property {string} userId
- * @property {string} role
- * @property {string|null} branchId
- * @property {string|null} email
- * @property {boolean} mustChangePassword
- */
- export function login(input, options) {
- return apiFetch("/api/auth/login", {
- method: "POST",
- body: input,
- ...options,
- });
- }
- export function logout(options) {
- return apiFetch("/api/auth/logout", { method: "GET", ...options });
- }
- /**
- * @returns {Promise<{ user: MeUser|null }>}
- */
- export function getMe(options) {
- return apiFetch("/api/auth/me", { method: "GET", ...options });
- }
- export function changePassword(input, options) {
- return apiFetch("/api/auth/change-password", {
- method: "POST",
- body: input,
- ...options,
- });
- }
- export function getBranches(options) {
- return apiFetch("/api/branches", { method: "GET", ...options });
- }
- export function getYears(branch, options) {
- return apiFetch(`/api/branches/${encodeURIComponent(branch)}/years`, {
- method: "GET",
- ...options,
- });
- }
- export function getMonths(branch, year, options) {
- return apiFetch(
- `/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(year)}/months`,
- { method: "GET", ...options },
- );
- }
- export function getDays(branch, year, month, options) {
- return apiFetch(
- `/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
- year,
- )}/${encodeURIComponent(month)}/days`,
- { method: "GET", ...options },
- );
- }
- export function getFiles(branch, year, month, day, options) {
- const qs = new URLSearchParams({
- branch: String(branch),
- year: String(year),
- month: String(month),
- day: String(day),
- });
- return apiFetch(`/api/files?${qs.toString()}`, { method: "GET", ...options });
- }
- export function search(input, options) {
- const { q, branch, scope, branches, from, to, limit, cursor } = input || {};
- const params = new URLSearchParams();
- if (typeof q === "string" && q.trim()) params.set("q", q.trim());
- if (typeof scope === "string" && scope.trim())
- params.set("scope", scope.trim());
- if (typeof branch === "string" && branch.trim())
- params.set("branch", branch.trim());
- if (Array.isArray(branches) && branches.length > 0) {
- const cleaned = branches.map((b) => String(b).trim()).filter(Boolean);
- if (cleaned.length > 0) params.set("branches", cleaned.join(","));
- }
- if (typeof from === "string" && from.trim()) params.set("from", from.trim());
- if (typeof to === "string" && to.trim()) params.set("to", to.trim());
- if (limit !== undefined && limit !== null) {
- const raw = String(limit).trim();
- if (raw) params.set("limit", raw);
- }
- if (typeof cursor === "string" && cursor.trim())
- params.set("cursor", cursor.trim());
- const qs = params.toString();
- return apiFetch(qs ? `/api/search?${qs}` : "/api/search", {
- method: "GET",
- ...options,
- });
- }
- export function adminListUsers(input, options) {
- const { q, role, branchId, limit, cursor } = input || {};
- const params = new URLSearchParams();
- if (typeof q === "string" && q.trim()) params.set("q", q.trim());
- if (typeof role === "string" && role.trim()) params.set("role", role.trim());
- if (typeof branchId === "string" && branchId.trim())
- params.set("branchId", branchId.trim());
- if (limit !== undefined && limit !== null) {
- const raw = String(limit).trim();
- if (raw) params.set("limit", raw);
- }
- if (typeof cursor === "string" && cursor.trim())
- params.set("cursor", cursor.trim());
- const qs = params.toString();
- return apiFetch(qs ? `/api/admin/users?${qs}` : "/api/admin/users", {
- method: "GET",
- ...options,
- });
- }
- export function adminCreateUser(input, options) {
- return apiFetch("/api/admin/users", {
- method: "POST",
- body: input,
- ...options,
- });
- }
- export function adminUpdateUser(userId, patch, options) {
- if (typeof userId !== "string" || !userId.trim()) {
- throw new Error("adminUpdateUser requires a userId string");
- }
- return apiFetch(`/api/admin/users/${encodeURIComponent(userId.trim())}`, {
- method: "PATCH",
- body: patch || {},
- ...options,
- });
- }
- export function adminDeleteUser(userId, options) {
- if (typeof userId !== "string" || !userId.trim()) {
- throw new Error("adminDeleteUser requires a userId string");
- }
- return apiFetch(`/api/admin/users/${encodeURIComponent(userId.trim())}`, {
- method: "DELETE",
- ...options,
- });
- }
|