diff --git a/ui/sign-in/src/SignInPage.module.css b/ui/sign-in/src/SignInPage.module.css index 5fa956f..77a71a9 100644 --- a/ui/sign-in/src/SignInPage.module.css +++ b/ui/sign-in/src/SignInPage.module.css @@ -120,3 +120,33 @@ text-align: center; line-height: 1.5; } + +.backupCodeCard { + margin-top: 4px; + padding: 12px 16px; + background: var(--bg-surface, #f8f7f5); + border: 1px solid var(--border-default, #e8e0d4); + border-radius: 6px; + text-align: center; +} + +.backupCodeText { + font-size: 13px; + color: var(--text-muted); + margin: 0 0 6px; +} + +.backupCodeAction { + background: none; + border: none; + cursor: pointer; + color: var(--text-link, #C6820E); + font-size: 14px; + font-weight: 600; + padding: 4px 8px; + text-decoration: underline; +} + +.backupCodeAction:hover { + opacity: 0.8; +} diff --git a/ui/sign-in/src/SignInPage.tsx b/ui/sign-in/src/SignInPage.tsx index affaa4e..50cf923 100644 --- a/ui/sign-in/src/SignInPage.tsx +++ b/ui/sign-in/src/SignInPage.tsx @@ -121,6 +121,11 @@ export function SignInPage() { const redirectTo = await signIn(identifier, password); window.location.replace(redirectTo); } catch (err) { + if (err instanceof MfaRequiredError) { + setMode('mfaVerify'); + setLoading(false); + return; + } setError(err instanceof Error ? err.message : 'Sign-in failed'); setLoading(false); } @@ -221,6 +226,36 @@ export function SignInPage() { } }; + // --- MFA: TOTP verification --- + const handleMfaVerify = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const verificationId = await verifyTotp(code); + const redirectTo = await submitMfa(verificationId); + window.location.replace(redirectTo); + } catch (err) { + setError(err instanceof Error ? err.message : 'Verification failed'); + setLoading(false); + } + }; + + // --- MFA: backup code verification --- + const handleBackupCodeVerify = async (e: FormEvent) => { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const verificationId = await verifyBackupCode(code); + const redirectTo = await submitMfa(verificationId); + window.location.replace(redirectTo); + } catch (err) { + setError(err instanceof Error ? err.message : 'Verification failed'); + setLoading(false); + } + }; + const passwordToggle = ( + +
+

Lost your device?

+ +
+ + )} + + {/* --- MFA: backup code verification --- */} + {mode === 'mfaBackupCode' && ( +
+

+ Enter one of your 10 backup codes. +

+ + + setCode(e.target.value)} + placeholder="Enter backup code" + autoFocus + autoComplete="off" + disabled={loading} + /> + + + + +

+ +

+
+ )}