kirche-einfach/frontend/src/api.ts

214 lines
5.6 KiB
TypeScript

/* Typed client for the kirche-einfach API (served via vite /api proxy). */
export interface FlowDefinition {
id: string;
name: string;
description: string;
status: string;
source_path: string | null;
continuation_flow_id: string | null;
created_at: string;
}
export interface FlowVersion {
id: string;
flow_id: string;
version_no: number;
langflow_json: unknown;
lfx_flow_id: string | null;
deployed_at: string | null;
author: string;
created_at: string;
}
export interface RunOutput {
result?: string;
component?: string;
type?: string;
error?: string;
[key: string]: unknown;
}
export interface Run {
id: string;
version_id: string;
tenant_id: string;
status: string;
input: unknown;
output: RunOutput | null;
session_id: string | null;
created_at: string;
finished_at: string | null;
}
export interface ApprovalTask {
id: string;
run_id: string;
version_id: string;
tenant_id: string;
title: string;
payload: unknown;
continuation_flow_id: string | null;
approver: string | null;
status: string;
decision_note: string;
created_at: string;
decided_at: string | null;
}
export interface Decision {
task: ApprovalTask;
continuation_run: Run | null;
}
export interface SyncReport {
added: string[];
updated: string[];
deleted: string[];
skipped: string[];
pulled: string[];
errors: Record<string, string>;
}
export interface Me {
id: string;
email: string;
name: string;
role: string;
tenant_id: string;
}
export interface AccessRequest {
id: string;
name: string;
email: string;
reason: string;
status: string;
created_at: string;
decided_at: string | null;
decided_by: string | null;
}
export interface InputSchema {
flow_id: string;
version_no: number;
json_schema: Record<string, unknown>;
ui_schema: Record<string, unknown>;
fallback: string | null;
}
export class ApiError extends Error {
status: number;
constructor(status: number, message: string) {
super(message);
this.status = status;
}
}
async function req<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
// Session cookie (HttpOnly) must travel with every request.
credentials: "include",
headers: { "Content-Type": "application/json" },
...init,
});
if (!res.ok) {
let detail = res.statusText;
try {
const body: unknown = await res.json();
if (
typeof body === "object" &&
body !== null &&
"detail" in body &&
typeof (body as { detail: unknown }).detail === "string"
) {
detail = (body as { detail: string }).detail;
}
} catch {
/* keep statusText */
}
throw new ApiError(res.status, detail);
}
return (await res.json()) as T;
}
export const LANGFLOW_URL =
(import.meta.env.VITE_LANGFLOW_URL as string | undefined) ??
"http://localhost:7860";
export const api = {
listFlows: () => req<FlowDefinition[]>("/api/flows"),
listVersions: (flowId: string) =>
req<FlowVersion[]>(`/api/flows/${flowId}/versions`),
deployVersion: (flowId: string, versionNo: number) =>
req<FlowVersion>(`/api/flows/${flowId}/versions/${versionNo}/deploy`, {
method: "POST",
}),
runFlow: (flowId: string, input: unknown) =>
req<Run>(`/api/flows/${flowId}/run`, {
method: "POST",
// Tenant comes from the session server-side; no tenant box anymore.
body: JSON.stringify({ input }),
}),
getInputSchema: (flowId: string) =>
req<InputSchema>(`/api/flows/${flowId}/input-schema`),
listRuns: (flowId: string) => req<Run[]>(`/api/flows/${flowId}/runs`),
listApprovals: () => req<ApprovalTask[]>("/api/approvals"),
decide: (taskId: string, decision: "approve" | "reject", note: string) =>
req<Decision>(`/api/approvals/${taskId}/decision`, {
method: "POST",
// Identity comes from the session; the server records the user.
body: JSON.stringify({ decision, note }),
}),
importFlow: (langflowFlowId: string, name?: string) =>
req<{ definition: FlowDefinition; version: FlowVersion }>(
"/api/flows/import",
{
method: "POST",
body: JSON.stringify({
langflow_flow_id: langflowFlowId,
...(name ? { name } : {}),
}),
},
),
listLangflow: () =>
req<{ id: string; name: string }[]>("/api/flows/langflow/available"),
syncFlows: () =>
req<SyncReport>("/api/flows/sync", {
method: "POST",
}),
me: async (): Promise<Me | null> => {
try {
return await req<Me>("/api/auth/me");
} catch (e) {
if (e instanceof ApiError && e.status === 401) return null;
throw e;
}
},
consumeMagic: (token: string) =>
req<Me>("/api/auth/magic/consume", {
method: "POST",
body: JSON.stringify({ token }),
}),
requestMagicLink: (email: string) =>
req<{ message: string; email_sent: boolean }>("/api/auth/magic/request", {
method: "POST",
body: JSON.stringify({ email }),
}),
logout: () =>
req<{ message: string }>("/api/auth/logout", { method: "POST" }),
requestAccess: (name: string, email: string, reason: string) =>
req<AccessRequest>("/api/access/requests", {
method: "POST",
body: JSON.stringify({ name, email, reason }),
}),
listAccessRequests: (status?: string) =>
req<AccessRequest[]>(
`/api/access/requests${status ? `?status=${status}` : ""}`,
),
decideAccess: (id: string, approve: boolean) =>
req<{ request: AccessRequest; email_sent: boolean }>(
`/api/access/requests/${id}/${approve ? "approve" : "reject"}`,
{ method: "POST" },
),
};