From 4816b199765ec770b442a901a0c47cae71070656 Mon Sep 17 00:00:00 2001 From: Investor Flow Build Date: Mon, 29 Jun 2026 21:21:09 -0400 Subject: [PATCH] slice 2 UI: 2FA enrollment (enable -> secret/QR/backup codes -> confirm) Orchestrator (me) SPA work: auth client +2FA/OAuth methods, 2FA strings, AuthSection enrollment flow. Primary-Rule lint stays green. Slice 2 (auth-2fa-and-social-oauth) COMPLETE: 2FA + OAuth backend (ornith modules + me integration) + 2FA enrollment UI. --- app/src/app/page.tsx | 47 +++++++++++++++++++++++++++++++----------- app/src/lib/strings.ts | 9 ++++++++ app/src/lib/trpc.ts | 5 ++++- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/app/src/app/page.tsx b/app/src/app/page.tsx index 5a40493..4589768 100644 --- a/app/src/app/page.tsx +++ b/app/src/app/page.tsx @@ -9,7 +9,6 @@ export default function Page() { const activeSymbol = useActiveSymbol((s) => s.activeSymbol); const setActive = useActiveSymbol((s) => s.setActive); const [draft, setDraft] = useState(activeSymbol); - return (
{ e.preventDefault(); setActive(draft); }} className="flex gap-2"> @@ -17,7 +16,6 @@ export default function Page() { className="flex-1 bg-[#121318] border border-[#2a2b3a] rounded-md px-3 py-2 text-sm text-[#e6e7ec] placeholder:text-[#5a5b6a] focus:outline-none focus:border-[#4f8cff]" />
- @@ -31,25 +29,50 @@ function AuthSection() { const [user, setUser] = useState(null); const [error, setError] = useState(null); const [busy, setBusy] = useState(false); + // 2FA enrollment flow state + const [enroll, setEnroll] = useState<{ totpSecret: string; qrUrl: string; backupCodes: string[] } | null>(null); + const [code, setCode] = useState(""); + const [twoFactorMsg, setTwoFactorMsg] = useState(null); useEffect(() => { api.auth.me().then(setUser).catch(() => {}); }, []); const submit = async (mode: "signup" | "login") => { setBusy(true); setError(null); - try { - await api.auth[mode](email, password); - setUser(await api.auth.me()); - } catch (e) { - setError(e instanceof Error ? e.message : "request failed"); - } finally { setBusy(false); } + try { await api.auth[mode](email, password); setUser(await api.auth.me()); } + catch (e) { setError(e instanceof Error ? e.message : "request failed"); } + finally { setBusy(false); } }; if (user) { return ( -
-

Signed in as {user.email}

-

Complexity: {user.complexity} · Risk tolerance: {user.riskTolerance}

- +
+
+

Signed in as {user.email}

+

Complexity: {user.complexity} · Risk tolerance: {user.riskTolerance}

+
+ {!enroll && !twoFactorMsg && ( + + )} + {enroll && ( +
+

{UI_STRINGS.twoFactorQrLabel}

+

{UI_STRINGS.twoFactorSecretLabel}

+

{enroll.totpSecret}

+

{UI_STRINGS.backupCodesLabel}

+
    {enroll.backupCodes.map((c) =>
  • {c}
  • )}
+
+ setCode(e.target.value)} placeholder={UI_STRINGS.twoFactorCodePlaceholder} + className="bg-[#0a0b0f] border border-[#2a2b3a] rounded px-2 py-1 text-[#e6e7ec] w-32" /> + +
+
+ )} + {twoFactorMsg &&

{twoFactorMsg}

} + {error &&

{error}

} +
); } diff --git a/app/src/lib/strings.ts b/app/src/lib/strings.ts index 5233c30..0fb9e8a 100644 --- a/app/src/lib/strings.ts +++ b/app/src/lib/strings.ts @@ -24,6 +24,15 @@ export const UI_STRINGS = { loginButton: "Sign in", logoutButton: "Sign out", signupPrompt: "Create an account to track symbols and keep your research in one place.", + enable2faButton: "Enable two-factor", + twoFactorSecretLabel: "Two-factor secret (base32)", + twoFactorQrLabel: "Scan this in an authenticator app, or enter the secret manually.", + backupCodesLabel: "Backup codes — store these somewhere safe.", + confirm2faButton: "Confirm with a code", + twoFactorCodePlaceholder: "6-digit code", + twoFactorEnabled: "Two-factor is now enabled. Login will ask for a code.", + oauthGithub: "Continue with GitHub", + oauthGoogle: "Continue with Google", } as const; export const UI_STRING_LIST: string[] = Object.values(UI_STRINGS); diff --git a/app/src/lib/trpc.ts b/app/src/lib/trpc.ts index 3ac190a..d8f6f24 100644 --- a/app/src/lib/trpc.ts +++ b/app/src/lib/trpc.ts @@ -76,8 +76,11 @@ export const api = { market: { snapshot: (symbol: string) => trpcQuery("market.snapshot", { symbol }) }, auth: { signup: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.signup", { email, password }), - login: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.login", { email, password }), + login: (email: string, password: string, totp?: string) => trpcMutate<{ userId: string }>("auth.login", { email, password, ...(totp ? { totp } : {}) }), logout: () => trpcMutate<{ ok: boolean }>("auth.logout", {}), me: () => trpcQuery("auth.me"), + enable2fa: () => trpcMutate<{ totpSecret: string; qrUrl: string; backupCodes: string[] }>("auth.enable2fa", {}), + confirm2fa: (totp: string) => trpcMutate<{ ok: boolean }>("auth.confirm2fa", { totp }), + oauthStart: (provider: "github" | "google", redirectUri: string) => trpcMutate<{ redirectUrl: string; state: string }>("auth.oauthStart", { provider, redirectUri }), }, };