fix: authorize flow management, gate runs, token-gate callbacks, strict tenants

This commit is contained in:
Jonas Miederer 2026-09-14 20:12:17 +02:00
parent cfcf0e8bd6
commit 94e5e53092
21 changed files with 662 additions and 160 deletions

View file

@ -1,4 +1,4 @@
import { Link, Route, Routes, useNavigate } from "react-router-dom";
import { Link, Navigate, Route, Routes, useNavigate } from "react-router-dom";
import {
Anchor,
AppShell,
@ -10,6 +10,7 @@ import {
Text,
Title,
} from "@mantine/core";
import type { ReactNode } from "react";
import { AuthProvider, useAuth } from "./auth";
import SchemeToggle from "./components/SchemeToggle";
import StartPage from "./pages/StartPage";
@ -66,6 +67,21 @@ function Nav() {
);
}
function RequireAuth({ children }: { children: ReactNode }) {
const { me, loading } = useAuth();
if (loading) {
return (
<Center mt="xl">
<Loader />
</Center>
);
}
// Runs, run history and approvals are login-gated server-side;
// anonymous visitors go to the magic-link login.
if (!me) return <Navigate to="/login" replace />;
return <>{children}</>;
}
function Shell() {
const { loading } = useAuth();
@ -103,9 +119,30 @@ function Shell() {
<Route path="/" element={<StartPage />} />
<Route path="/access" element={<AccessPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/app/workflows" element={<WorkflowsPage />} />
<Route path="/app/tasks" element={<TasksPage />} />
<Route path="/app/admin" element={<AdminPage />} />
<Route
path="/app/workflows"
element={
<RequireAuth>
<WorkflowsPage />
</RequireAuth>
}
/>
<Route
path="/app/tasks"
element={
<RequireAuth>
<TasksPage />
</RequireAuth>
}
/>
<Route
path="/app/admin"
element={
<RequireAuth>
<AdminPage />
</RequireAuth>
}
/>
<Route path="*" element={<StartPage />} />
</Routes>
)}

View file

@ -153,15 +153,11 @@ export const api = {
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",
approver: string,
note: string,
) =>
decide: (taskId: string, decision: "approve" | "reject", note: string) =>
req<Decision>(`/api/approvals/${taskId}/decision`, {
method: "POST",
body: JSON.stringify({ decision, approver, note }),
// 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 }>(

View file

@ -12,11 +12,12 @@ import {
} from "@mantine/core";
import { IconAlertCircle, IconCheck, IconRefresh, IconX } from "@tabler/icons-react";
import { ApiError, api, type ApprovalTask } from "../api";
import { useAuth } from "../auth";
import StatusBadge from "./StatusBadge";
export default function ApprovalsTab() {
const { me } = useAuth();
const [tasks, setTasks] = useState<ApprovalTask[]>([]);
const [approver, setApprover] = useState("");
const [note, setNote] = useState("");
const [error, setError] = useState("");
const [busyId, setBusyId] = useState("");
@ -37,7 +38,7 @@ export default function ApprovalsTab() {
setBusyId(taskId);
setError("");
try {
const result = await api.decide(taskId, decision, approver, note);
const result = await api.decide(taskId, decision, note);
setTasks((prev) =>
prev.map((t) => (t.id === taskId ? result.task : t)),
);
@ -61,12 +62,11 @@ export default function ApprovalsTab() {
{error}
</Alert>
)}
<Text size="sm" c="dimmed" mb="md">
Angemeldet als {me?.email ?? "–"} — Entscheidungen werden diesem
Konto zugeordnet.
</Text>
<Group mb="md" align="flex-end">
<TextInput
label="Name (Bearbeiter)"
value={approver}
onChange={(e) => setApprover(e.target.value)}
/>
<TextInput
label="Notiz"
value={note}

View file

@ -15,11 +15,9 @@ import {
} from "@mantine/core";
import { IconAlertCircle, IconPlayerPlay } from "@tabler/icons-react";
import { ApiError, api, type FlowDefinition, type InputSchema, type Run } from "../api";
import { useAuth } from "../auth";
import StatusBadge from "../components/StatusBadge";
export default function WorkflowsPage() {
const { me } = useAuth();
const [flows, setFlows] = useState<FlowDefinition[]>([]);
const [flowId, setFlowId] = useState("");
const [schema, setSchema] = useState<InputSchema | null>(null);
@ -86,11 +84,6 @@ export default function WorkflowsPage() {
<Title order={2} mb="md">
Workflows
</Title>
{!me && (
<Alert color="blue" mb="md">
Sie sind nicht angemeldet — Läufe nutzen den Standard-Mandanten.
</Alert>
)}
{error && (
<Alert color="red" icon={<IconAlertCircle size={18} />} mb="md">
{error}