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 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-23 23:46:27 +00:00
parent bc1ed8372d
commit b30e8e577b
2 changed files with 12 additions and 3 deletions

View file

@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -7,6 +7,7 @@ import { getTotpSetup, enableTotp } from "@/api/auth";
import { useAuthStore } from "@/store/authStore"; import { useAuthStore } from "@/store/authStore";
import { useQuery, useMutation } from "@tanstack/react-query"; import { useQuery, useMutation } from "@tanstack/react-query";
import { ShieldCheck, Copy, CheckCircle, Loader2 } from "lucide-react"; 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") }); const schema = z.object({ code: z.string().length(6, "6-digit code required") });
type Form = z.infer<typeof schema>; type Form = z.infer<typeof schema>;
@ -14,6 +15,11 @@ type Form = z.infer<typeof schema>;
export default function TwoFactorSetupPage() { export default function TwoFactorSetupPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const { setTotpEnabled } = useAuthStore(); const { setTotpEnabled } = useAuthStore();
const isDemo = useDemoMode();
useEffect(() => {
if (isDemo) navigate("/settings", { replace: true });
}, [isDemo, navigate]);
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [secret, setSecret] = useState<string | null>(null); const [secret, setSecret] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -25,6 +31,7 @@ export default function TwoFactorSetupPage() {
setSecret(res.secret); setSecret(res.secret);
return res; return res;
}, },
enabled: !isDemo,
}); });
const { register, handleSubmit, formState } = useForm<Form>({ const { register, handleSubmit, formState } = useForm<Form>({

View file

@ -16,6 +16,7 @@ import {
} from "recharts"; } from "recharts";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme"; import { TOOLTIP_STYLE, ACTIVE_DOT } from "@/utils/chartTheme";
import { useDemoMode } from "@/hooks/useDemoMode";
const COLORS = ["#6366f1","#22c55e","#f97316","#ec4899","#14b8a6","#f59e0b","#8b5cf6","#ef4444"]; const COLORS = ["#6366f1","#22c55e","#f97316","#ec4899","#14b8a6","#f59e0b","#8b5cf6","#ef4444"];
@ -29,6 +30,7 @@ const TYPE_COLORS: Record<string, string> = {
export default function Dashboard() { export default function Dashboard() {
const displayName = useAuthStore((s) => s.displayName); const displayName = useAuthStore((s) => s.displayName);
const totpEnabled = useAuthStore((s) => s.totpEnabled); const totpEnabled = useAuthStore((s) => s.totpEnabled);
const isDemo = useDemoMode();
const { data: nw } = useQuery({ queryKey: ["net-worth"], queryFn: getNetWorth }); const { data: nw } = useQuery({ queryKey: ["net-worth"], queryFn: getNetWorth });
const { data: accounts = [] } = useQuery({ queryKey: ["accounts"], queryFn: getAccounts }); const { data: accounts = [] } = useQuery({ queryKey: ["accounts"], queryFn: getAccounts });
@ -52,8 +54,8 @@ export default function Dashboard() {
<p className="text-muted-foreground text-sm mt-1">Here's your financial overview</p> <p className="text-muted-foreground text-sm mt-1">Here's your financial overview</p>
</div> </div>
{/* 2FA nudge */} {/* 2FA nudge — hidden in demo mode */}
{!totpEnabled && ( {!totpEnabled && !isDemo && (
<div className="flex items-center gap-3 bg-warning/10 border border-warning/30 rounded-xl px-4 py-3"> <div className="flex items-center gap-3 bg-warning/10 border border-warning/30 rounded-xl px-4 py-3">
<ShieldAlert className="w-5 h-5 text-warning shrink-0" /> <ShieldAlert className="w-5 h-5 text-warning shrink-0" />
<p className="flex-1 text-sm"> <p className="flex-1 text-sm">