Skip to content
This repository was archived by the owner on Apr 10, 2026. It is now read-only.
This repository was archived by the owner on Apr 10, 2026. It is now read-only.

feat(frontend): 2FA — TOTP verification step in login flow #53

@dajbelshaw

Description

@dajbelshaw

Part of #50.

Current login flow

LoginPage.tsxapi.login() → 200 → init E2EE key → render app.

New flow

api.login()202 → show TOTP input screen → api.verifyTotp() → 200 → init E2EE key → render app.

Changes required

api.ts

Add verifyTotp(mfaToken: string, code: string):

async verifyTotp(mfaToken: string, code: string): Promise<AuthUser & { keySalt: string }>
// POST /api/auth/totp — same response shape as login on success

Update login(): if response status is 202, return { mfaPending: true, mfaToken: string } instead of throwing.

LoginPage.tsx

Add a 'totp' view alongside existing 'login', 'register', 'forgot-password', 'reset-password'.

Critical — password retention across views: The E2EE key is derived from password + keySalt via cryptoService.initKey(). On normal login this happens immediately in api.login(). With 2FA, initKey must be called after api.verifyTotp() succeeds — at which point the server returns keySalt but the user's password is no longer being typed. The password must be retained in component state through the TOTP view. Do not clear the password field or reset password state on transition to 'totp'.

// On login 202 response — retain password, don't clear it:
setMfaToken(token);
setView('totp');
// password state stays untouched

// On TOTP success:
const { keySalt, ...user } = await api.verifyTotp(mfaToken, totpCode);
await cryptoService.initKey(password, keySalt); // password still in state
authService.setUser(user);

If the user clicks "Back to login" from the TOTP view, clear both mfaToken and password state and return to the login view.

State to add:

const [mfaToken, setMfaToken] = useState<string | null>(null);
const [totpCode, setTotpCode] = useState('');

TOTP view UI:

  • Heading: "Two-factor authentication"
  • Body copy: "Enter the 6-digit code from your authenticator app, or one of your backup codes."
  • Single input: numeric, inputMode="numeric", maxLength={8} (backup codes are longer than 6 digits), autoFocus, autoComplete="one-time-code"
  • Submit button: "Verify"
  • Link: "Back to login" (clears mfa state + password state, returns to login view)
  • Error state: inline below input (same pattern as existing error display)
  • Loading state: disable input + button while request is in flight

UX notes

  • Auto-submit when 6 digits are entered (skip button tap for TOTP codes; backup codes still need manual submit)
  • Clear the code input on error (don't preserve a failed code)
  • No "remember this device" for now (out of scope per feat: Add TOTP two-factor authentication #50)

Metadata

Metadata

Assignees

No one assigned

    Labels

    authAuthentication & sessionsenhancementNew feature or requestuxUser experience / interface

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions