Skip to content

Commit 02f36a9

Browse files
committed
fix: TOTP login flow, disabled node filtering, add globalConfig editor
Fix TOTP 2FA login broken by NextAuth v5 error propagation — use CredentialsSignin subclasses so custom error codes reach the client. Also fix undefined string serialization in signIn credentials. Pass disabled flag through all server-side node mappings so disabled components are correctly filtered from generated YAML/TOML configs. Add Pipeline Settings panel (log level + globalConfig JSON editor) to the detail panel empty state; remove log level from toolbar.
1 parent 07b17d2 commit 02f36a9

8 files changed

Lines changed: 167 additions & 49 deletions

File tree

src/app/(auth)/login/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,16 @@ export default function LoginPage() {
5858
const result = await signIn("credentials", {
5959
email,
6060
password,
61-
totpCode: totpRequired ? totpCode : undefined,
61+
...(totpRequired && totpCode ? { totpCode } : {}),
6262
redirect: false,
6363
});
6464

65+
const resultWithCode = result as typeof result & { code?: string };
6566
if (result?.error) {
66-
if (result.error.includes("TOTP_REQUIRED")) {
67+
if (resultWithCode.code === "TOTP_REQUIRED") {
6768
setTotpRequired(true);
6869
setError(null);
69-
} else if (result.error.includes("Invalid verification code")) {
70+
} else if (resultWithCode.code === "INVALID_TOTP") {
7071
setError("Invalid verification code. Please try again.");
7172
setTotpCode("");
7273
} else {

src/app/api/agent/config/route.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export async function GET(request: Request) {
8888
componentDef: { type: n.componentType, kind: n.kind.toLowerCase() },
8989
componentKey: n.componentKey,
9090
config: withCerts,
91+
disabled: n.disabled,
9192
},
9293
};
9394
}),
@@ -120,6 +121,7 @@ export async function GET(request: Request) {
120121
n.componentType,
121122
(n.config as Record<string, unknown>) ?? {},
122123
),
124+
disabled: n.disabled,
123125
},
124126
}));
125127

src/auth.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import NextAuth from "next-auth";
1+
import NextAuth, { CredentialsSignin } from "next-auth";
22
import type { Provider } from "next-auth/providers";
33
import Credentials from "next-auth/providers/credentials";
44
import { PrismaAdapter } from "@auth/prisma-adapter";
@@ -8,6 +8,14 @@ import { encrypt, decrypt } from "@/server/services/crypto";
88
import { verifyTotpCode, verifyBackupCode } from "@/server/services/totp";
99
import { authConfig } from "@/auth.config";
1010

11+
class TotpRequiredError extends CredentialsSignin {
12+
code = "TOTP_REQUIRED";
13+
}
14+
15+
class InvalidVerificationCodeError extends CredentialsSignin {
16+
code = "INVALID_TOTP";
17+
}
18+
1119
/**
1220
* Load OIDC settings from the database.
1321
* Returns null if OIDC is not configured.
@@ -65,11 +73,11 @@ const credentialsProvider = Credentials({
6573

6674
// TOTP 2FA check
6775
if (user.totpEnabled && user.totpSecret) {
68-
const totpCode = credentials.totpCode as string | undefined;
76+
const raw = credentials.totpCode as string | undefined;
77+
const totpCode = raw && raw !== "undefined" ? raw.trim() : undefined;
6978

7079
if (!totpCode) {
71-
// Signal the client that TOTP is required
72-
throw new Error("TOTP_REQUIRED");
80+
throw new TotpRequiredError();
7381
}
7482

7583
const secret = decrypt(user.totpSecret);
@@ -90,7 +98,7 @@ const credentialsProvider = Credentials({
9098
}
9199

92100
if (!codeValid) {
93-
throw new Error("Invalid verification code");
101+
throw new InvalidVerificationCodeError();
94102
}
95103
}
96104

src/components/flow/detail-panel.tsx

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

3-
import { useCallback } from "react";
4-
import { Copy, Trash2 } from "lucide-react";
3+
import { useCallback, useState, useEffect } from "react";
4+
import { Copy, Trash2, ChevronRight } from "lucide-react";
55
import { useFlowStore } from "@/stores/flow-store";
66
import { SchemaForm } from "@/components/config-forms/schema-form";
77
import { VrlEditor } from "@/components/vrl-editor/vrl-editor";
@@ -12,6 +12,18 @@ import { Button } from "@/components/ui/button";
1212
import { Separator } from "@/components/ui/separator";
1313
import { Switch } from "@/components/ui/switch";
1414
import { Badge } from "@/components/ui/badge";
15+
import {
16+
Select,
17+
SelectContent,
18+
SelectItem,
19+
SelectTrigger,
20+
SelectValue,
21+
} from "@/components/ui/select";
22+
import {
23+
Collapsible,
24+
CollapsibleContent,
25+
CollapsibleTrigger,
26+
} from "@/components/ui/collapsible";
1527
import type { VectorComponentDef } from "@/lib/vector/types";
1628

1729
/* ------------------------------------------------------------------ */
@@ -59,6 +71,127 @@ function filterSchema(
5971
};
6072
}
6173

74+
/* ------------------------------------------------------------------ */
75+
/* Pipeline Settings (shown when no node is selected) */
76+
/* ------------------------------------------------------------------ */
77+
78+
function PipelineSettings() {
79+
const globalConfig = useFlowStore((s) => s.globalConfig);
80+
const updateGlobalConfig = useFlowStore((s) => s.updateGlobalConfig);
81+
const setGlobalConfig = useFlowStore((s) => s.setGlobalConfig);
82+
const currentLogLevel = (globalConfig?.log_level as string) || "info";
83+
84+
const [jsonOpen, setJsonOpen] = useState(false);
85+
const [jsonText, setJsonText] = useState("");
86+
const [jsonError, setJsonError] = useState<string | null>(null);
87+
88+
// Derive the config object minus log_level for the JSON editor
89+
useEffect(() => {
90+
const { log_level, ...rest } = globalConfig ?? {};
91+
setJsonText(
92+
Object.keys(rest).length > 0 ? JSON.stringify(rest, null, 2) : "",
93+
);
94+
setJsonError(null);
95+
}, [globalConfig]);
96+
97+
const handleApply = () => {
98+
const trimmed = jsonText.trim();
99+
if (trimmed === "") {
100+
// Clear everything except log_level
101+
if (currentLogLevel !== "info") {
102+
setGlobalConfig({ log_level: currentLogLevel });
103+
} else {
104+
setGlobalConfig(null);
105+
}
106+
setJsonError(null);
107+
return;
108+
}
109+
try {
110+
const parsed = JSON.parse(trimmed);
111+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
112+
setJsonError("Must be a JSON object");
113+
return;
114+
}
115+
// Merge back log_level if set
116+
const merged: Record<string, unknown> = { ...parsed };
117+
if (currentLogLevel !== "info") {
118+
merged.log_level = currentLogLevel;
119+
}
120+
setGlobalConfig(merged);
121+
setJsonError(null);
122+
} catch (e) {
123+
setJsonError(e instanceof Error ? e.message : "Invalid JSON");
124+
}
125+
};
126+
127+
const hasJsonContent = jsonText.trim().length > 0;
128+
129+
return (
130+
<div className="space-y-6 p-4">
131+
<h3 className="text-sm font-semibold">Pipeline Settings</h3>
132+
133+
{/* Log Level */}
134+
<div className="space-y-2">
135+
<Label htmlFor="log-level">Log Level</Label>
136+
<Select
137+
value={currentLogLevel}
138+
onValueChange={(value) =>
139+
updateGlobalConfig("log_level", value === "info" ? undefined : value)
140+
}
141+
>
142+
<SelectTrigger id="log-level" className="w-full">
143+
<SelectValue />
144+
</SelectTrigger>
145+
<SelectContent>
146+
{(["trace", "debug", "info", "warn", "error"] as const).map(
147+
(level) => (
148+
<SelectItem key={level} value={level}>
149+
{level}
150+
</SelectItem>
151+
),
152+
)}
153+
</SelectContent>
154+
</Select>
155+
</div>
156+
157+
<Separator />
158+
159+
{/* Global Configuration JSON */}
160+
<Collapsible open={jsonOpen} onOpenChange={setJsonOpen}>
161+
<CollapsibleTrigger className="flex w-full items-center gap-2 text-sm font-semibold">
162+
<ChevronRight
163+
className={`h-4 w-4 transition-transform ${jsonOpen ? "rotate-90" : ""}`}
164+
/>
165+
Global Configuration (JSON)
166+
{hasJsonContent && (
167+
<Badge variant="secondary" className="ml-auto text-[10px]">
168+
configured
169+
</Badge>
170+
)}
171+
</CollapsibleTrigger>
172+
<CollapsibleContent className="mt-3 space-y-3">
173+
<textarea
174+
className="min-h-[120px] w-full rounded-md border bg-muted/50 p-3 font-mono text-xs focus:outline-none focus:ring-2 focus:ring-ring"
175+
value={jsonText}
176+
onChange={(e) => {
177+
setJsonText(e.target.value);
178+
setJsonError(null);
179+
}}
180+
placeholder='{ "enrichment_tables": { ... } }'
181+
spellCheck={false}
182+
/>
183+
{jsonError && (
184+
<p className="text-xs text-destructive">{jsonError}</p>
185+
)}
186+
<Button size="sm" onClick={handleApply}>
187+
Apply
188+
</Button>
189+
</CollapsibleContent>
190+
</Collapsible>
191+
</div>
192+
);
193+
}
194+
62195
/* ------------------------------------------------------------------ */
63196
/* Component */
64197
/* ------------------------------------------------------------------ */
@@ -101,14 +234,12 @@ export function DetailPanel() {
101234
}
102235
}, [selectedNodeId, removeNode]);
103236

104-
// ---- Empty state ----
237+
// ---- Empty state → Pipeline Settings ----
105238
if (!selectedNode) {
106239
return (
107240
<div className="flex h-full w-80 shrink-0 flex-col border-l bg-muted/30">
108-
<div className="flex flex-1 items-center justify-center p-6">
109-
<p className="text-sm text-muted-foreground">
110-
Select a node to edit its configuration
111-
</p>
241+
<div className="min-h-0 flex-1 overflow-y-auto">
242+
<PipelineSettings />
112243
</div>
113244
</div>
114245
);

src/components/flow/flow-toolbar.tsx

Lines changed: 0 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,6 @@ import {
3333
DropdownMenuItem,
3434
DropdownMenuTrigger,
3535
} from "@/components/ui/dropdown-menu";
36-
import {
37-
Select,
38-
SelectContent,
39-
SelectItem,
40-
SelectTrigger,
41-
SelectValue,
42-
} from "@/components/ui/select";
4336
import { useFlowStore } from "@/stores/flow-store";
4437
import { generateVectorYaml, generateVectorToml, importVectorConfig } from "@/lib/config-generator";
4538
import { useTRPC } from "@/trpc/client";
@@ -85,8 +78,6 @@ export function FlowToolbar({
8578
onToggleMetrics,
8679
}: FlowToolbarProps) {
8780
const globalConfig = useFlowStore((s) => s.globalConfig);
88-
const updateGlobalConfig = useFlowStore((s) => s.updateGlobalConfig);
89-
const currentLogLevel = (globalConfig?.log_level as string) || "info";
9081
const canUndo = useFlowStore((s) => s.canUndo);
9182
const canRedo = useFlowStore((s) => s.canRedo);
9283
const undo = useFlowStore((s) => s.undo);
@@ -301,31 +292,6 @@ export function FlowToolbar({
301292
</Tooltip>
302293
)}
303294

304-
<Tooltip>
305-
<TooltipTrigger asChild>
306-
<div>
307-
<Select
308-
value={currentLogLevel}
309-
onValueChange={(value) =>
310-
updateGlobalConfig("log_level", value === "info" ? undefined : value)
311-
}
312-
>
313-
<SelectTrigger className="h-7 w-[5.5rem] gap-1 border-none bg-transparent px-2 text-xs shadow-none hover:bg-accent">
314-
<SelectValue />
315-
</SelectTrigger>
316-
<SelectContent>
317-
{(["trace", "debug", "info", "warn", "error"] as const).map((level) => (
318-
<SelectItem key={level} value={level}>
319-
{level}
320-
</SelectItem>
321-
))}
322-
</SelectContent>
323-
</Select>
324-
</div>
325-
</TooltipTrigger>
326-
<TooltipContent>Pipeline log level</TooltipContent>
327-
</Tooltip>
328-
329295
<Separator orientation="vertical" className="mx-1 h-5" />
330296

331297
{/* Deploy state buttons */}

src/server/routers/deploy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export const deployRouter = router({
3333
componentDef: { type: n.componentType, kind: n.kind.toLowerCase() },
3434
componentKey: n.componentKey,
3535
config: decryptNodeConfig(n.componentType, (n.config as Record<string, unknown>) ?? {}),
36+
disabled: n.disabled,
3637
},
3738
}));
3839

src/server/services/deploy-agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export async function deployAgent(
4747
n.componentType,
4848
(n.config as Record<string, unknown>) ?? {},
4949
),
50+
disabled: n.disabled,
5051
},
5152
}));
5253

src/stores/flow-store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ export interface FlowState {
7171

7272
// Global config
7373
updateGlobalConfig: (key: string, value: unknown) => void;
74+
setGlobalConfig: (config: Record<string, unknown> | null) => void;
7475

7576
// Copy / Paste
7677
copyNode: (id: string) => void;
@@ -347,6 +348,13 @@ export const useFlowStore = create<InternalState>()((set, get) => ({
347348
});
348349
},
349350

351+
setGlobalConfig: (config) => {
352+
set({
353+
globalConfig: config && Object.keys(config).length > 0 ? config : null,
354+
isDirty: true,
355+
});
356+
},
357+
350358
/* ---- Copy / Paste ---- */
351359

352360
copyNode: (id) => {

0 commit comments

Comments
 (0)