From b30e8e577b33527d279fbff08cb7287561498a5c Mon Sep 17 00:00:00 2001 From: megaproxy Date: Thu, 23 Apr 2026 23:46:27 +0000 Subject: [PATCH] Block 2FA setup in demo mode at all entry points The dashboard had a 'Set up 2FA' banner link to /security/totp that bypassed the settings button guard entirely. Three fixes: - Dashboard: hide the 2FA nudge banner completely in demo mode - TwoFactorSetupPage: redirect to /settings on mount if isDemo, and disable the setup query so no API call fires even briefly - This covers both the UI entry point and direct URL navigation Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/auth/TwoFactorSetup.tsx | 9 ++++++++- frontend/src/pages/dashboard/Dashboard.tsx | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/frontend/src/pages/auth/TwoFactorSetup.tsx b/frontend/src/pages/auth/TwoFactorSetup.tsx index 46a378b..1551545 100644 --- a/frontend/src/pages/auth/TwoFactorSetup.tsx +++ b/frontend/src/pages/auth/TwoFactorSetup.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; @@ -7,6 +7,7 @@ import { getTotpSetup, enableTotp } from "@/api/auth"; import { useAuthStore } from "@/store/authStore"; import { useQuery, useMutation } from "@tanstack/react-query"; import { ShieldCheck, Copy, CheckCircle, Loader2 } from "lucide-react"; +import { useDemoMode } from "@/hooks/useDemoMode"; const schema = z.object({ code: z.string().length(6, "6-digit code required") }); type Form = z.infer; @@ -14,6 +15,11 @@ type Form = z.infer; export default function TwoFactorSetupPage() { const navigate = useNavigate(); const { setTotpEnabled } = useAuthStore(); + const isDemo = useDemoMode(); + + useEffect(() => { + if (isDemo) navigate("/settings", { replace: true }); + }, [isDemo, navigate]); const [copied, setCopied] = useState(false); const [secret, setSecret] = useState(null); const [error, setError] = useState(null); @@ -25,6 +31,7 @@ export default function TwoFactorSetupPage() { setSecret(res.secret); return res; }, + enabled: !isDemo, }); const { register, handleSubmit, formState } = useForm
({ diff --git a/frontend/src/pages/dashboard/Dashboard.tsx b/frontend/src/pages/dashboard/Dashboard.tsx index 5e6015a..755bd87 100644 --- a/frontend/src/pages/dashboard/Dashboard.tsx +++ b/frontend/src/pages/dashboard/Dashboard.tsx @@ -16,6 +16,7 @@ import { } from "recharts"; import { Link } from "react-router-dom"; import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme"; +import { useDemoMode } from "@/hooks/useDemoMode"; const COLORS = ["#6366f1","#22c55e","#f97316","#ec4899","#14b8a6","#f59e0b","#8b5cf6","#ef4444"]; @@ -29,6 +30,7 @@ const TYPE_COLORS: Record = { export default function Dashboard() { const displayName = useAuthStore((s) => s.displayName); const totpEnabled = useAuthStore((s) => s.totpEnabled); + const isDemo = useDemoMode(); const { data: nw } = useQuery({ queryKey: ["net-worth"], queryFn: getNetWorth }); const { data: accounts = [] } = useQuery({ queryKey: ["accounts"], queryFn: getAccounts }); @@ -52,8 +54,8 @@ export default function Dashboard() {

Here's your financial overview

- {/* 2FA nudge */} - {!totpEnabled && ( + {/* 2FA nudge — hidden in demo mode */} + {!totpEnabled && !isDemo && (