feat: router with RJSF workflows and access management UI

This commit is contained in:
Jonas Miederer 2026-09-12 07:36:05 +02:00
parent 46ecab00db
commit 62d01b5097
15 changed files with 948 additions and 144 deletions

View file

@ -69,6 +69,33 @@ export interface SyncReport {
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) {
@ -79,6 +106,8 @@ export class ApiError extends Error {
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,
});
@ -114,11 +143,14 @@ export const api = {
req<FlowVersion>(`/api/flows/${flowId}/versions/${versionNo}/deploy`, {
method: "POST",
}),
runFlow: (flowId: string, input: unknown, tenantId: string) =>
runFlow: (flowId: string, input: unknown) =>
req<Run>(`/api/flows/${flowId}/run`, {
method: "POST",
body: JSON.stringify({ input, tenant_id: tenantId }),
// 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: (
@ -148,4 +180,33 @@ export const api = {
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 }),
}),
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" },
),
};