733 lines
21 KiB
TypeScript
733 lines
21 KiB
TypeScript
// Cross-origin API client. All requests send the session cookie (connect.sid)
|
|
// via credentials: "include". The API origin comes from NEXT_PUBLIC_API_ORIGIN.
|
|
|
|
import type {
|
|
AuthUser,
|
|
BalanceFilter,
|
|
BalanceListResponse,
|
|
BalanceSort,
|
|
BankCleared,
|
|
BankDirection,
|
|
BankFacets,
|
|
BankListResponse,
|
|
BankSort,
|
|
BankStats,
|
|
BankSummary,
|
|
BillingFacets,
|
|
BillingStats,
|
|
BusinessLine,
|
|
CreateBankMovementInput,
|
|
CreateMovementInput,
|
|
CustomerDetail,
|
|
CustomerInput,
|
|
CustomerListResponse,
|
|
CustomerStats,
|
|
LedgerCurrency,
|
|
LedgerDirection,
|
|
MovementListResponse,
|
|
MovementSort,
|
|
PolicyDetail,
|
|
PolicyFacets,
|
|
PolicyInput,
|
|
PolicyListResponse,
|
|
PolicySort,
|
|
PolicyStats,
|
|
PolicyStatus,
|
|
LookupsResponse,
|
|
OpsJob,
|
|
OpsJobKind,
|
|
IngestFile,
|
|
BackupFile,
|
|
PropertyDetail,
|
|
PropertyFacets,
|
|
PropertyInput,
|
|
PropertyListResponse,
|
|
PropertySort,
|
|
PropertyStats,
|
|
ServiceInput,
|
|
TrustInput,
|
|
Role,
|
|
ServiceKind,
|
|
Statement,
|
|
Transaction,
|
|
TransactionDomain,
|
|
TrustFilter,
|
|
UserRow,
|
|
} from "./types";
|
|
|
|
// Resolve the API origin at runtime, not build time. In the browser it comes
|
|
// from window.__API_ORIGIN__, injected server-side by the root layout from the
|
|
// deploy .env (API_ORIGIN) — so one built image serves any deployment. On the
|
|
// server (SSR) read process.env directly. NEXT_PUBLIC_API_ORIGIN stays as the
|
|
// dev/build fallback.
|
|
function resolveApiOrigin(): string {
|
|
if (typeof window !== "undefined") {
|
|
const injected = (window as { __API_ORIGIN__?: string }).__API_ORIGIN__;
|
|
if (injected) return injected;
|
|
}
|
|
return (
|
|
process.env.API_ORIGIN ??
|
|
process.env.NEXT_PUBLIC_API_ORIGIN ??
|
|
"http://localhost:3001"
|
|
);
|
|
}
|
|
|
|
export const API_ORIGIN = resolveApiOrigin();
|
|
|
|
export class ApiError extends Error {
|
|
status: number;
|
|
constructor(status: number, message: string) {
|
|
super(message);
|
|
this.status = status;
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
async function apiFetch<T>(
|
|
path: string,
|
|
init?: RequestInit,
|
|
): Promise<T> {
|
|
let res: Response;
|
|
try {
|
|
res = await fetch(`${API_ORIGIN}${path}`, {
|
|
credentials: "include",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(init?.headers ?? {}),
|
|
},
|
|
...init,
|
|
});
|
|
} catch (e) {
|
|
throw new ApiError(
|
|
0,
|
|
"No se pudo conectar con el servidor. Verifica tu conexión.",
|
|
);
|
|
}
|
|
|
|
if (!res.ok) {
|
|
let message = `Error ${res.status}`;
|
|
try {
|
|
const body = await res.json();
|
|
if (body?.message) message = body.message;
|
|
} catch {
|
|
/* ignore non-JSON error bodies */
|
|
}
|
|
throw new ApiError(res.status, message);
|
|
}
|
|
|
|
if (res.status === 204) return undefined as T;
|
|
return (await res.json()) as T;
|
|
}
|
|
|
|
export function login(email: string, password: string): Promise<AuthUser> {
|
|
return apiFetch<AuthUser>("/auth/login", {
|
|
method: "POST",
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
}
|
|
|
|
export function me(): Promise<AuthUser> {
|
|
return apiFetch<AuthUser>("/auth/me");
|
|
}
|
|
|
|
export function logout(): Promise<{ success: boolean }> {
|
|
return apiFetch<{ success: boolean }>("/auth/logout", { method: "POST" });
|
|
}
|
|
|
|
export function getStats(): Promise<CustomerStats> {
|
|
return apiFetch<CustomerStats>("/customers/stats");
|
|
}
|
|
|
|
export interface CustomerQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
line?: BusinessLine;
|
|
}
|
|
|
|
export function listCustomers(
|
|
q: CustomerQuery,
|
|
): Promise<CustomerListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.line) params.set("line", q.line);
|
|
const qs = params.toString();
|
|
return apiFetch<CustomerListResponse>(`/customers${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export function getCustomer(id: string): Promise<CustomerDetail> {
|
|
return apiFetch<CustomerDetail>(`/customers/${id}`);
|
|
}
|
|
|
|
export function createCustomer(input: CustomerInput): Promise<CustomerDetail> {
|
|
return apiFetch<CustomerDetail>("/customers", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function updateCustomer(
|
|
id: string,
|
|
input: Partial<CustomerInput>,
|
|
): Promise<CustomerDetail> {
|
|
return apiFetch<CustomerDetail>(`/customers/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function archiveCustomer(id: string): Promise<CustomerDetail> {
|
|
return apiFetch<CustomerDetail>(`/customers/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function restoreCustomer(id: string): Promise<CustomerDetail> {
|
|
return apiFetch<CustomerDetail>(`/customers/${id}/restore`, { method: "POST" });
|
|
}
|
|
|
|
/* ------------------------------------------------------ Policies module */
|
|
|
|
/** Renewal horizon in days, shared by the list, stats and detail calls so the
|
|
* "por vencer" bucket means the same thing everywhere. */
|
|
export const EXPIRY_WINDOW_DAYS = 30;
|
|
|
|
export interface PolicyQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
status?: PolicyStatus;
|
|
days?: number;
|
|
typeId?: string;
|
|
providerId?: string;
|
|
liquidated?: boolean;
|
|
sort?: PolicySort;
|
|
}
|
|
|
|
export function listPolicies(q: PolicyQuery): Promise<PolicyListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.status) params.set("status", q.status);
|
|
if (q.days) params.set("days", String(q.days));
|
|
if (q.typeId) params.set("typeId", q.typeId);
|
|
if (q.providerId) params.set("providerId", q.providerId);
|
|
if (q.liquidated !== undefined) params.set("liquidated", String(q.liquidated));
|
|
if (q.sort) params.set("sort", q.sort);
|
|
const qs = params.toString();
|
|
return apiFetch<PolicyListResponse>(`/policies${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export function getPolicyStats(
|
|
days: number = EXPIRY_WINDOW_DAYS,
|
|
): Promise<PolicyStats> {
|
|
return apiFetch<PolicyStats>(`/policies/stats?days=${days}`);
|
|
}
|
|
|
|
export function getPolicyFacets(): Promise<PolicyFacets> {
|
|
return apiFetch<PolicyFacets>("/policies/facets");
|
|
}
|
|
|
|
export function getPolicy(
|
|
id: string,
|
|
days: number = EXPIRY_WINDOW_DAYS,
|
|
): Promise<PolicyDetail> {
|
|
return apiFetch<PolicyDetail>(`/policies/${id}?days=${days}`);
|
|
}
|
|
|
|
export function createPolicy(input: PolicyInput): Promise<PolicyDetail> {
|
|
return apiFetch<PolicyDetail>("/policies", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function updatePolicy(
|
|
id: string,
|
|
input: Partial<PolicyInput>,
|
|
): Promise<PolicyDetail> {
|
|
return apiFetch<PolicyDetail>(`/policies/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function archivePolicy(id: string): Promise<PolicyDetail> {
|
|
return apiFetch<PolicyDetail>(`/policies/${id}`, { method: "DELETE" });
|
|
}
|
|
export function restorePolicy(id: string): Promise<PolicyDetail> {
|
|
return apiFetch<PolicyDetail>(`/policies/${id}/restore`, { method: "POST" });
|
|
}
|
|
|
|
// Generic policy-child CRUD. `kind` is the URL segment
|
|
// (installments|vehicles|drivers|beneficiaries|claims).
|
|
export function addPolicyChild<T>(
|
|
policyId: string,
|
|
kind: string,
|
|
input: T,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/policies/${policyId}/${kind}`, {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function updatePolicyChild<T>(
|
|
policyId: string,
|
|
kind: string,
|
|
childId: string,
|
|
input: T,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function removePolicyChild(
|
|
policyId: string,
|
|
kind: string,
|
|
childId: string,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/policies/${policyId}/${kind}/${childId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
/* ------------------------------------------------- Lookups (insurance ref) */
|
|
|
|
export function getLookups(): Promise<LookupsResponse> {
|
|
return apiFetch<LookupsResponse>("/lookups");
|
|
}
|
|
export function createLookup(kind: string, input: unknown): Promise<unknown> {
|
|
return apiFetch(`/lookups/${kind}`, { method: "POST", body: JSON.stringify(input) });
|
|
}
|
|
export function updateLookup(
|
|
kind: string,
|
|
id: string,
|
|
input: unknown,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/lookups/${kind}/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function removeLookup(kind: string, id: string): Promise<unknown> {
|
|
return apiFetch(`/lookups/${kind}/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
/* ----------------------------------------------------- Utilities module */
|
|
|
|
export interface PropertyQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
serviceKind?: ServiceKind;
|
|
municipality?: string;
|
|
bank?: string;
|
|
trust?: TrustFilter;
|
|
hasServices?: boolean;
|
|
customerId?: string;
|
|
days?: number;
|
|
sort?: PropertySort;
|
|
}
|
|
|
|
export function listProperties(q: PropertyQuery): Promise<PropertyListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.serviceKind) params.set("serviceKind", q.serviceKind);
|
|
if (q.municipality) params.set("municipality", q.municipality);
|
|
if (q.bank) params.set("bank", q.bank);
|
|
if (q.trust) params.set("trust", q.trust);
|
|
if (q.hasServices !== undefined)
|
|
params.set("hasServices", String(q.hasServices));
|
|
if (q.customerId) params.set("customerId", q.customerId);
|
|
if (q.days) params.set("days", String(q.days));
|
|
if (q.sort) params.set("sort", q.sort);
|
|
const qs = params.toString();
|
|
return apiFetch<PropertyListResponse>(`/properties${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export function getPropertyStats(
|
|
days: number = EXPIRY_WINDOW_DAYS,
|
|
): Promise<PropertyStats> {
|
|
return apiFetch<PropertyStats>(`/properties/stats?days=${days}`);
|
|
}
|
|
|
|
export function getPropertyFacets(): Promise<PropertyFacets> {
|
|
return apiFetch<PropertyFacets>("/properties/facets");
|
|
}
|
|
|
|
export function getProperty(
|
|
id: string,
|
|
days: number = EXPIRY_WINDOW_DAYS,
|
|
): Promise<PropertyDetail> {
|
|
return apiFetch<PropertyDetail>(`/properties/${id}?days=${days}`);
|
|
}
|
|
|
|
export function createProperty(input: PropertyInput): Promise<PropertyDetail> {
|
|
return apiFetch<PropertyDetail>("/properties", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function updateProperty(
|
|
id: string,
|
|
input: Partial<PropertyInput>,
|
|
): Promise<PropertyDetail> {
|
|
return apiFetch<PropertyDetail>(`/properties/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function archiveProperty(id: string): Promise<PropertyDetail> {
|
|
return apiFetch<PropertyDetail>(`/properties/${id}`, { method: "DELETE" });
|
|
}
|
|
export function restoreProperty(id: string): Promise<PropertyDetail> {
|
|
return apiFetch<PropertyDetail>(`/properties/${id}/restore`, { method: "POST" });
|
|
}
|
|
|
|
// Service child CRUD.
|
|
export function addService(propertyId: string, input: ServiceInput): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/services`, {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function updateService(
|
|
propertyId: string,
|
|
serviceId: string,
|
|
input: Partial<ServiceInput>,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/services/${serviceId}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function removeService(propertyId: string, serviceId: string): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/services/${serviceId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
// Trust account (1:1 upsert).
|
|
export function upsertTrust(propertyId: string, input: TrustInput): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/trust`, {
|
|
method: "PUT",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
export function removeTrust(propertyId: string): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/trust`, { method: "DELETE" });
|
|
}
|
|
|
|
export function removePropertyDocument(
|
|
propertyId: string,
|
|
documentId: string,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/properties/${propertyId}/documents/${documentId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export function propertyDocumentDownloadUrl(
|
|
propertyId: string,
|
|
documentId: string,
|
|
): string {
|
|
return `${API_ORIGIN}/properties/${propertyId}/documents/${documentId}/download`;
|
|
}
|
|
|
|
export function uploadPropertyDocument(
|
|
propertyId: string,
|
|
file: File,
|
|
type?: string,
|
|
): Promise<unknown> {
|
|
const q = type ? `?type=${encodeURIComponent(type)}` : "";
|
|
return uploadFile(`/properties/${propertyId}/documents${q}`, file);
|
|
}
|
|
|
|
export function removePolicyDocument(
|
|
policyId: string,
|
|
documentId: string,
|
|
): Promise<unknown> {
|
|
return apiFetch(`/policies/${policyId}/documents/${documentId}`, {
|
|
method: "DELETE",
|
|
});
|
|
}
|
|
|
|
export function policyDocumentDownloadUrl(
|
|
policyId: string,
|
|
documentId: string,
|
|
): string {
|
|
return `${API_ORIGIN}/policies/${policyId}/documents/${documentId}/download`;
|
|
}
|
|
|
|
export function uploadPolicyDocument(
|
|
policyId: string,
|
|
file: File,
|
|
type?: string,
|
|
): Promise<unknown> {
|
|
const q = type ? `?type=${encodeURIComponent(type)}` : "";
|
|
return uploadFile(`/policies/${policyId}/documents${q}`, file);
|
|
}
|
|
|
|
/* ------------------------------------------- Billing / statements module */
|
|
|
|
export interface MovementQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
domain?: TransactionDomain;
|
|
currency?: LedgerCurrency;
|
|
direction?: LedgerDirection;
|
|
typeId?: string;
|
|
source?: string;
|
|
customerId?: string;
|
|
/** `YYYY-MM-DD`, inclusive on both ends. */
|
|
from?: string;
|
|
to?: string;
|
|
sort?: MovementSort;
|
|
}
|
|
|
|
export function listMovements(q: MovementQuery): Promise<MovementListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.domain) params.set("domain", q.domain);
|
|
if (q.currency) params.set("currency", q.currency);
|
|
if (q.direction) params.set("direction", q.direction);
|
|
if (q.typeId) params.set("typeId", q.typeId);
|
|
if (q.source) params.set("source", q.source);
|
|
if (q.customerId) params.set("customerId", q.customerId);
|
|
if (q.from) params.set("from", q.from);
|
|
if (q.to) params.set("to", q.to);
|
|
if (q.sort) params.set("sort", q.sort);
|
|
const qs = params.toString();
|
|
return apiFetch<MovementListResponse>(`/billing${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export interface BalanceQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
currency?: LedgerCurrency;
|
|
balance?: BalanceFilter;
|
|
domain?: TransactionDomain;
|
|
sort?: BalanceSort;
|
|
}
|
|
|
|
export function listBalances(q: BalanceQuery): Promise<BalanceListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.currency) params.set("currency", q.currency);
|
|
if (q.balance) params.set("balance", q.balance);
|
|
if (q.domain) params.set("domain", q.domain);
|
|
if (q.sort) params.set("sort", q.sort);
|
|
const qs = params.toString();
|
|
return apiFetch<BalanceListResponse>(`/billing/balances${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export function getBillingStats(): Promise<BillingStats> {
|
|
return apiFetch<BillingStats>("/billing/stats");
|
|
}
|
|
|
|
export function getBillingFacets(): Promise<BillingFacets> {
|
|
return apiFetch<BillingFacets>("/billing/facets");
|
|
}
|
|
|
|
export function getStatement(customerId: string): Promise<Statement> {
|
|
return apiFetch<Statement>(`/billing/customers/${customerId}`);
|
|
}
|
|
|
|
/** Append a new ledger movement. Booked movements are never edited — fix
|
|
* mistakes with voidMovement + a fresh capture. */
|
|
export function createMovement(
|
|
input: CreateMovementInput,
|
|
): Promise<Transaction> {
|
|
return apiFetch<Transaction>("/billing", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Reverse a movement by marking it voided; totals drop it. 400 if already void. */
|
|
export function voidMovement(id: string): Promise<Transaction> {
|
|
return apiFetch<Transaction>(`/billing/${id}/void`, { method: "POST" });
|
|
}
|
|
|
|
/* ------------------------------------------------- Bank register (chequera) */
|
|
|
|
export interface BankQuery {
|
|
query?: string;
|
|
page?: number;
|
|
pageSize?: number;
|
|
direction?: BankDirection;
|
|
cleared?: BankCleared;
|
|
/** `YYYY-MM-DD`, inclusive on both ends. */
|
|
from?: string;
|
|
to?: string;
|
|
sort?: BankSort;
|
|
}
|
|
|
|
export function listBankMovements(q: BankQuery): Promise<BankListResponse> {
|
|
const params = new URLSearchParams();
|
|
if (q.query) params.set("query", q.query);
|
|
if (q.page) params.set("page", String(q.page));
|
|
if (q.pageSize) params.set("pageSize", String(q.pageSize));
|
|
if (q.direction) params.set("direction", q.direction);
|
|
if (q.cleared) params.set("cleared", q.cleared);
|
|
if (q.from) params.set("from", q.from);
|
|
if (q.to) params.set("to", q.to);
|
|
if (q.sort) params.set("sort", q.sort);
|
|
const qs = params.toString();
|
|
return apiFetch<BankListResponse>(`/bank${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
export function getBankStats(): Promise<BankStats> {
|
|
return apiFetch<BankStats>("/bank/stats");
|
|
}
|
|
|
|
export function getBankFacets(): Promise<BankFacets> {
|
|
return apiFetch<BankFacets>("/bank/facets");
|
|
}
|
|
|
|
export function getBankSummary(year?: number): Promise<BankSummary> {
|
|
return apiFetch<BankSummary>(`/bank/summary${year ? `?year=${year}` : ""}`);
|
|
}
|
|
|
|
/** Append a new chequera movement. Booked rows are never edited — fix mistakes
|
|
* with voidBankMovement + a fresh capture. */
|
|
export function createBankMovement(
|
|
input: CreateBankMovementInput,
|
|
): Promise<unknown> {
|
|
return apiFetch("/bank", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
/** Reverse a chequera movement by marking it voided; totals drop it. */
|
|
export function voidBankMovement(id: string): Promise<unknown> {
|
|
return apiFetch(`/bank/${id}/void`, { method: "POST" });
|
|
}
|
|
|
|
/* ------------------------------------------------- Users / administration */
|
|
|
|
export function listUsers(): Promise<UserRow[]> {
|
|
return apiFetch<UserRow[]>("/users");
|
|
}
|
|
|
|
export interface CreateUserInput {
|
|
name: string;
|
|
email: string;
|
|
password: string;
|
|
role: Role;
|
|
active?: boolean;
|
|
}
|
|
|
|
export function createUser(input: CreateUserInput): Promise<UserRow> {
|
|
return apiFetch<UserRow>("/users", {
|
|
method: "POST",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export interface UpdateUserInput {
|
|
name?: string;
|
|
email?: string;
|
|
role?: Role;
|
|
active?: boolean;
|
|
}
|
|
|
|
export function updateUser(id: string, input: UpdateUserInput): Promise<UserRow> {
|
|
return apiFetch<UserRow>(`/users/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(input),
|
|
});
|
|
}
|
|
|
|
export function resetUserPassword(id: string, password: string): Promise<UserRow> {
|
|
return apiFetch<UserRow>(`/users/${id}/reset-password`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ password }),
|
|
});
|
|
}
|
|
|
|
export function deleteUser(id: string): Promise<void> {
|
|
return apiFetch<void>(`/users/${id}`, { method: "DELETE" });
|
|
}
|
|
|
|
/* ------------------------------------------- DB operations (admin only) */
|
|
|
|
export function listIngest(): Promise<IngestFile[]> {
|
|
return apiFetch<IngestFile[]>("/ops/ingest");
|
|
}
|
|
|
|
/**
|
|
* Multipart upload — not JSON, so it bypasses apiFetch's Content-Type. `path`
|
|
* is API-relative (may include a query string); `filename` overrides the part
|
|
* name sent to the server.
|
|
*/
|
|
export async function uploadFile(
|
|
path: string,
|
|
file: File,
|
|
filename?: string,
|
|
): Promise<unknown> {
|
|
const body = new FormData();
|
|
body.append("file", file, filename ?? file.name);
|
|
const res = await fetch(`${API_ORIGIN}${path}`, {
|
|
method: "POST",
|
|
credentials: "include",
|
|
body,
|
|
});
|
|
if (!res.ok) {
|
|
let message = `Error ${res.status}`;
|
|
try {
|
|
const b = await res.json();
|
|
if (b?.message) message = b.message;
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
throw new ApiError(res.status, message);
|
|
}
|
|
return res.status === 204 ? undefined : res.json().catch(() => undefined);
|
|
}
|
|
|
|
export function uploadIngest(name: string, file: File): Promise<unknown> {
|
|
return uploadFile(`/ops/ingest/${encodeURIComponent(name)}`, file, name);
|
|
}
|
|
|
|
export function deleteIngest(name: string): Promise<unknown> {
|
|
return apiFetch(`/ops/ingest/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function listBackups(): Promise<BackupFile[]> {
|
|
return apiFetch<BackupFile[]>("/ops/backups");
|
|
}
|
|
|
|
export function backupDownloadUrl(name: string): string {
|
|
return `${API_ORIGIN}/ops/backups/${encodeURIComponent(name)}/download`;
|
|
}
|
|
|
|
export function deleteBackup(name: string): Promise<unknown> {
|
|
return apiFetch(`/ops/backups/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
}
|
|
|
|
export function listOpsJobs(): Promise<OpsJob[]> {
|
|
return apiFetch<OpsJob[]>("/ops/jobs");
|
|
}
|
|
|
|
export function getOpsJob(id: string): Promise<OpsJob> {
|
|
return apiFetch<OpsJob>(`/ops/jobs/${id}`);
|
|
}
|
|
|
|
/** Start a mutating op. `file` is required for RESTORE. 409 if one is running. */
|
|
export function startOpsJob(kind: OpsJobKind, file?: string): Promise<OpsJob> {
|
|
return apiFetch<OpsJob>("/ops/jobs", {
|
|
method: "POST",
|
|
body: JSON.stringify({ kind, file }),
|
|
});
|
|
}
|