|
@@ -16,13 +16,6 @@
|
|
|
* @property {{ message: string, code: string, details?: any }} error
|
|
* @property {{ message: string, code: string, details?: any }} error
|
|
|
*/
|
|
*/
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * A standardized client-side error type for API failures.
|
|
|
|
|
- * - status: HTTP status code (e.g. 401, 403, 404, 500)
|
|
|
|
|
- * - code: machine-readable error code (e.g. AUTH_UNAUTHENTICATED)
|
|
|
|
|
- * - message: human-readable message (safe to show in UI)
|
|
|
|
|
- * - details: optional structured payload (validation params etc.)
|
|
|
|
|
- */
|
|
|
|
|
export class ApiClientError extends Error {
|
|
export class ApiClientError extends Error {
|
|
|
/**
|
|
/**
|
|
|
* @param {{
|
|
* @param {{
|
|
@@ -36,88 +29,34 @@ export class ApiClientError extends Error {
|
|
|
* }} input
|
|
* }} input
|
|
|
*/
|
|
*/
|
|
|
constructor({ status, code, message, details, url, method, cause }) {
|
|
constructor({ status, code, message, details, url, method, cause }) {
|
|
|
- // We attach `cause` to preserve error chains (supported in modern Node).
|
|
|
|
|
super(message, cause ? { cause } : undefined);
|
|
super(message, cause ? { cause } : undefined);
|
|
|
|
|
|
|
|
this.name = "ApiClientError";
|
|
this.name = "ApiClientError";
|
|
|
this.status = status;
|
|
this.status = status;
|
|
|
this.code = code;
|
|
this.code = code;
|
|
|
|
|
|
|
|
- // Only attach optional properties when provided (keeps error objects clean).
|
|
|
|
|
if (details !== undefined) this.details = details;
|
|
if (details !== undefined) this.details = details;
|
|
|
if (url) this.url = url;
|
|
if (url) this.url = url;
|
|
|
if (method) this.method = method;
|
|
if (method) this.method = method;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const DEFAULT_HEADERS = {
|
|
|
|
|
- Accept: "application/json",
|
|
|
|
|
-};
|
|
|
|
|
|
|
+const DEFAULT_HEADERS = { Accept: "application/json" };
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Resolve a request URL.
|
|
|
|
|
- * - If `path` is absolute (http/https), return as-is.
|
|
|
|
|
- * - If `baseUrl` is provided, resolve relative to it.
|
|
|
|
|
- * - Otherwise return the relative path (browser-friendly: "/api/...").
|
|
|
|
|
- *
|
|
|
|
|
- * Why this exists:
|
|
|
|
|
- * - The same client can be used:
|
|
|
|
|
- * - in the browser (relative "/api/..." calls)
|
|
|
|
|
- * - in Node scripts (absolute baseUrl like "http://127.0.0.1:3000")
|
|
|
|
|
- *
|
|
|
|
|
- * @param {string} path
|
|
|
|
|
- * @param {string=} baseUrl
|
|
|
|
|
- * @returns {string}
|
|
|
|
|
- */
|
|
|
|
|
function resolveUrl(path, baseUrl) {
|
|
function resolveUrl(path, baseUrl) {
|
|
|
- // If someone passes a full URL, keep it unchanged.
|
|
|
|
|
if (/^https?:\/\//i.test(path)) return path;
|
|
if (/^https?:\/\//i.test(path)) return path;
|
|
|
|
|
|
|
|
const base = (baseUrl || "").trim();
|
|
const base = (baseUrl || "").trim();
|
|
|
-
|
|
|
|
|
- // Browser usage: baseUrl omitted -> use relative path.
|
|
|
|
|
if (!base) return path;
|
|
if (!base) return path;
|
|
|
|
|
|
|
|
- // Ensure baseUrl ends with a slash so URL() resolves correctly.
|
|
|
|
|
return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
|
|
return new URL(path, base.endsWith("/") ? base : `${base}/`).toString();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Best-effort detection if response is JSON.
|
|
|
|
|
- *
|
|
|
|
|
- * Why we need this:
|
|
|
|
|
- * - Our API is intended to always respond with JSON.
|
|
|
|
|
- * - But in error cases (misconfig, reverse proxy, 404 HTML), we might receive non-JSON.
|
|
|
|
|
- * - We want robust parsing behavior and a predictable client-side error.
|
|
|
|
|
- *
|
|
|
|
|
- * @param {Response} response
|
|
|
|
|
- * @returns {boolean}
|
|
|
|
|
- */
|
|
|
|
|
function isJsonResponse(response) {
|
|
function isJsonResponse(response) {
|
|
|
const ct = response.headers.get("content-type") || "";
|
|
const ct = response.headers.get("content-type") || "";
|
|
|
return ct.toLowerCase().includes("application/json");
|
|
return ct.toLowerCase().includes("application/json");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Core fetch helper with:
|
|
|
|
|
- * - credentials: "include" (cookie-based session)
|
|
|
|
|
- * - cache: "no-store" (match backend freshness strategy)
|
|
|
|
|
- * - standardized error mapping into ApiClientError
|
|
|
|
|
- *
|
|
|
|
|
- * Clean code rule:
|
|
|
|
|
- * - UI code should NOT call fetch directly.
|
|
|
|
|
- * - Instead, it should call domain helpers that route through apiFetch().
|
|
|
|
|
- *
|
|
|
|
|
- * @param {string} path
|
|
|
|
|
- * @param {{
|
|
|
|
|
- * method?: string,
|
|
|
|
|
- * headers?: Record<string, string>,
|
|
|
|
|
- * body?: any,
|
|
|
|
|
- * baseUrl?: string,
|
|
|
|
|
- * fetchImpl?: typeof fetch
|
|
|
|
|
- * }=} options
|
|
|
|
|
- * @returns {Promise<any>} parsed JSON payload (or null for empty responses)
|
|
|
|
|
- */
|
|
|
|
|
export async function apiFetch(path, options = {}) {
|
|
export async function apiFetch(path, options = {}) {
|
|
|
const {
|
|
const {
|
|
|
method = "GET",
|
|
method = "GET",
|
|
@@ -129,8 +68,6 @@ export async function apiFetch(path, options = {}) {
|
|
|
|
|
|
|
|
const url = resolveUrl(path, baseUrl);
|
|
const url = resolveUrl(path, baseUrl);
|
|
|
|
|
|
|
|
- // Build request init. We always set credentials + no-store.
|
|
|
|
|
- // For JSON bodies, we serialize and set Content-Type.
|
|
|
|
|
const init = {
|
|
const init = {
|
|
|
method,
|
|
method,
|
|
|
credentials: "include",
|
|
credentials: "include",
|
|
@@ -139,15 +76,10 @@ export async function apiFetch(path, options = {}) {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
if (body !== undefined) {
|
|
if (body !== undefined) {
|
|
|
- // If the caller passes a string, we assume it is already serialized.
|
|
|
|
|
- // Otherwise we serialize as JSON.
|
|
|
|
|
if (typeof body === "string") {
|
|
if (typeof body === "string") {
|
|
|
init.body = body;
|
|
init.body = body;
|
|
|
} else {
|
|
} else {
|
|
|
init.body = JSON.stringify(body);
|
|
init.body = JSON.stringify(body);
|
|
|
-
|
|
|
|
|
- // Only set Content-Type if caller didn't provide it.
|
|
|
|
|
- // This allows callers to send non-JSON payloads later if needed.
|
|
|
|
|
if (!init.headers["Content-Type"]) {
|
|
if (!init.headers["Content-Type"]) {
|
|
|
init.headers["Content-Type"] = "application/json";
|
|
init.headers["Content-Type"] = "application/json";
|
|
|
}
|
|
}
|
|
@@ -158,8 +90,6 @@ export async function apiFetch(path, options = {}) {
|
|
|
try {
|
|
try {
|
|
|
response = await fetchImpl(url, init);
|
|
response = await fetchImpl(url, init);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- // Network errors, DNS errors, connection refused, etc.
|
|
|
|
|
- // We use status=0 to indicate "no HTTP response".
|
|
|
|
|
throw new ApiClientError({
|
|
throw new ApiClientError({
|
|
|
status: 0,
|
|
status: 0,
|
|
|
code: "CLIENT_NETWORK_ERROR",
|
|
code: "CLIENT_NETWORK_ERROR",
|
|
@@ -170,17 +100,14 @@ export async function apiFetch(path, options = {}) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Handle empty responses explicitly (e.g. some endpoints might return 204 later).
|
|
|
|
|
if (response.status === 204) return null;
|
|
if (response.status === 204) return null;
|
|
|
|
|
|
|
|
- // Prefer JSON when the server says it's JSON.
|
|
|
|
|
if (isJsonResponse(response)) {
|
|
if (isJsonResponse(response)) {
|
|
|
let payload;
|
|
let payload;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
payload = await response.json();
|
|
payload = await response.json();
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
- // Server said JSON but response body was not parseable JSON.
|
|
|
|
|
throw new ApiClientError({
|
|
throw new ApiClientError({
|
|
|
status: response.status,
|
|
status: response.status,
|
|
|
code: "CLIENT_INVALID_JSON",
|
|
code: "CLIENT_INVALID_JSON",
|
|
@@ -191,13 +118,10 @@ export async function apiFetch(path, options = {}) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Happy path: return parsed JSON.
|
|
|
|
|
if (response.ok) return payload;
|
|
if (response.ok) return payload;
|
|
|
|
|
|
|
|
- /** @type {ApiErrorPayload|any} */
|
|
|
|
|
const maybeError = payload;
|
|
const maybeError = payload;
|
|
|
|
|
|
|
|
- // Map standardized backend errors into ApiClientError
|
|
|
|
|
if (maybeError?.error?.code && maybeError?.error?.message) {
|
|
if (maybeError?.error?.code && maybeError?.error?.message) {
|
|
|
throw new ApiClientError({
|
|
throw new ApiClientError({
|
|
|
status: response.status,
|
|
status: response.status,
|
|
@@ -209,7 +133,6 @@ export async function apiFetch(path, options = {}) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Fallback: error is JSON but not in our standardized shape.
|
|
|
|
|
throw new ApiClientError({
|
|
throw new ApiClientError({
|
|
|
status: response.status,
|
|
status: response.status,
|
|
|
code: "CLIENT_HTTP_ERROR",
|
|
code: "CLIENT_HTTP_ERROR",
|
|
@@ -220,9 +143,7 @@ export async function apiFetch(path, options = {}) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Non-JSON response fallback (should be rare for current endpoints)
|
|
|
|
|
const text = await response.text().catch(() => "");
|
|
const text = await response.text().catch(() => "");
|
|
|
-
|
|
|
|
|
if (response.ok) return text || null;
|
|
if (response.ok) return text || null;
|
|
|
|
|
|
|
|
throw new ApiClientError({
|
|
throw new ApiClientError({
|
|
@@ -235,17 +156,9 @@ export async function apiFetch(path, options = {}) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
-/* Domain helpers (thin wrappers) */
|
|
|
|
|
|
|
+/* Domain helpers */
|
|
|
/* -------------------------------------------------------------------------- */
|
|
/* -------------------------------------------------------------------------- */
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Login:
|
|
|
|
|
- * - Sends credentials to the backend.
|
|
|
|
|
- * - Backend sets an HTTP-only cookie when successful.
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{ username: string, password: string }} input
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function login(input, options) {
|
|
export function login(input, options) {
|
|
|
return apiFetch("/api/auth/login", {
|
|
return apiFetch("/api/auth/login", {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
@@ -254,40 +167,14 @@ export function login(input, options) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Logout:
|
|
|
|
|
- * - Clears the session cookie (idempotent).
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function logout(options) {
|
|
export function logout(options) {
|
|
|
return apiFetch("/api/auth/logout", { method: "GET", ...options });
|
|
return apiFetch("/api/auth/logout", { method: "GET", ...options });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Get current session identity (frontend-friendly):
|
|
|
|
|
- * - Always returns HTTP 200 with:
|
|
|
|
|
- * - { user: null } when unauthenticated
|
|
|
|
|
- * - { user: { userId, role, branchId } } when authenticated
|
|
|
|
|
- *
|
|
|
|
|
- * Why we want this:
|
|
|
|
|
- * - The UI should not use 401 as basic control-flow to determine "am I logged in?"
|
|
|
|
|
- * - This endpoint enables a clean "session check" UX (RHL-020 AuthGate).
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getMe(options) {
|
|
export function getMe(options) {
|
|
|
return apiFetch("/api/auth/me", { method: "GET", ...options });
|
|
return apiFetch("/api/auth/me", { method: "GET", ...options });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Change password (RHL-009):
|
|
|
|
|
- * - Requires an active session cookie.
|
|
|
|
|
- * - Body: { currentPassword, newPassword }
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{ currentPassword: string, newPassword: string }} input
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function changePassword(input, options) {
|
|
export function changePassword(input, options) {
|
|
|
return apiFetch("/api/auth/change-password", {
|
|
return apiFetch("/api/auth/change-password", {
|
|
|
method: "POST",
|
|
method: "POST",
|
|
@@ -296,19 +183,10 @@ export function changePassword(input, options) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * List branches visible to the current session (RBAC is enforced server-side).
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getBranches(options) {
|
|
export function getBranches(options) {
|
|
|
return apiFetch("/api/branches", { method: "GET", ...options });
|
|
return apiFetch("/api/branches", { method: "GET", ...options });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * @param {string} branch
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getYears(branch, options) {
|
|
export function getYears(branch, options) {
|
|
|
return apiFetch(`/api/branches/${encodeURIComponent(branch)}/years`, {
|
|
return apiFetch(`/api/branches/${encodeURIComponent(branch)}/years`, {
|
|
|
method: "GET",
|
|
method: "GET",
|
|
@@ -316,26 +194,13 @@ export function getYears(branch, options) {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * @param {string} branch
|
|
|
|
|
- * @param {string} year
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getMonths(branch, year, options) {
|
|
export function getMonths(branch, year, options) {
|
|
|
return apiFetch(
|
|
return apiFetch(
|
|
|
- `/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
|
|
|
|
|
- year,
|
|
|
|
|
- )}/months`,
|
|
|
|
|
|
|
+ `/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(year)}/months`,
|
|
|
{ method: "GET", ...options },
|
|
{ method: "GET", ...options },
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * @param {string} branch
|
|
|
|
|
- * @param {string} year
|
|
|
|
|
- * @param {string} month
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getDays(branch, year, month, options) {
|
|
export function getDays(branch, year, month, options) {
|
|
|
return apiFetch(
|
|
return apiFetch(
|
|
|
`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
|
|
`/api/branches/${encodeURIComponent(branch)}/${encodeURIComponent(
|
|
@@ -345,13 +210,6 @@ export function getDays(branch, year, month, options) {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * @param {string} branch
|
|
|
|
|
- * @param {string} year
|
|
|
|
|
- * @param {string} month
|
|
|
|
|
- * @param {string} day
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function getFiles(branch, year, month, day, options) {
|
|
export function getFiles(branch, year, month, day, options) {
|
|
|
const qs = new URLSearchParams({
|
|
const qs = new URLSearchParams({
|
|
|
branch: String(branch),
|
|
branch: String(branch),
|
|
@@ -363,31 +221,10 @@ export function getFiles(branch, year, month, day, options) {
|
|
|
return apiFetch(`/api/files?${qs.toString()}`, { method: "GET", ...options });
|
|
return apiFetch(`/api/files?${qs.toString()}`, { method: "GET", ...options });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/**
|
|
|
|
|
- * Search delivery notes (RHL-024).
|
|
|
|
|
- *
|
|
|
|
|
- * Notes:
|
|
|
|
|
- * - This endpoint is JSON and can be called via apiFetch.
|
|
|
|
|
- * - Cursor is intentionally not stored in shareable URLs by default; the UI can keep it in state.
|
|
|
|
|
- *
|
|
|
|
|
- * @param {{
|
|
|
|
|
- * q?: string|null,
|
|
|
|
|
- * branch?: string|null,
|
|
|
|
|
- * scope?: "branch"|"multi"|"all"|string|null,
|
|
|
|
|
- * branches?: string[]|null,
|
|
|
|
|
- * from?: string|null,
|
|
|
|
|
- * to?: string|null,
|
|
|
|
|
- * limit?: number|null,
|
|
|
|
|
- * cursor?: string|null
|
|
|
|
|
- * }} input
|
|
|
|
|
- * @param {{ baseUrl?: string, fetchImpl?: typeof fetch }=} options
|
|
|
|
|
- */
|
|
|
|
|
export function search(input, options) {
|
|
export function search(input, options) {
|
|
|
const { q, branch, scope, branches, from, to, limit, cursor } = input || {};
|
|
const { q, branch, scope, branches, from, to, limit, cursor } = input || {};
|
|
|
-
|
|
|
|
|
const params = new URLSearchParams();
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
|
|
- // Stable insertion order (helps debugging and makes URLs readable).
|
|
|
|
|
if (typeof q === "string" && q.trim()) params.set("q", q.trim());
|
|
if (typeof q === "string" && q.trim()) params.set("q", q.trim());
|
|
|
if (typeof scope === "string" && scope.trim())
|
|
if (typeof scope === "string" && scope.trim())
|
|
|
params.set("scope", scope.trim());
|
|
params.set("scope", scope.trim());
|
|
@@ -407,12 +244,67 @@ export function search(input, options) {
|
|
|
if (raw) params.set("limit", raw);
|
|
if (raw) params.set("limit", raw);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (typeof cursor === "string" && cursor.trim()) {
|
|
|
|
|
|
|
+ if (typeof cursor === "string" && cursor.trim())
|
|
|
params.set("cursor", 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();
|
|
const qs = params.toString();
|
|
|
- const path = qs ? `/api/search?${qs}` : "/api/search";
|
|
|
|
|
|
|
+ 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,
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- return apiFetch(path, { method: "GET", ...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,
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|