Part of #50.
Location
Add a "Two-factor authentication" section to SettingsPanel.tsx (Security section, below the password change area if one exists, or as a new Security group).
API additions (api.ts)
setupTotp(): Promise<{ otpauthUrl: string; secret: string }>
// POST /api/user/totp/setup
confirmTotp(code: string): Promise<{ backupCodes: string[] }>
// POST /api/user/totp/confirm
disableTotp(code: string): Promise<void>
// DELETE /api/user/totp
Also add totpEnabled: boolean to AuthUser interface (returned by /api/auth/me).
UI states
State A — 2FA disabled
Two-factor authentication [Enable]
Not enabled
Clicking Enable → starts setup flow (State B).
State B — Setup flow (multi-step within settings)
Step 1: Scan QR code
- Render QR code from
otpauthUrl (use qrcode npm package in the frontend, or a <canvas>-based renderer — keep it client-side, no external services)
- Show the base32 secret as a fallback for manual entry (monospace, copy button)
- "Can't scan? Enter this code manually:
XXXX XXXX XXXX XXXX"
- [Continue] button → Step 2
Step 2: Verify setup
- "Enter the 6-digit code from your authenticator app to confirm setup."
- TOTP code input (same as login TOTP step)
- [Confirm] / [Back]
- On success → Step 3
Step 3: Save your backup codes
- Display 8 backup codes in a 2×4 grid, monospace font
- [Download codes] button — triggers download of a
.txt file: taskdial-backup-codes.txt
- Warning: "These codes will not be shown again. Store them somewhere safe."
- [Done] → State C
State C — 2FA enabled
Two-factor authentication [Disable]
Enabled ✓
Backup codes: 8 remaining [Regenerate]
backupCodesCount returned from /api/auth/me or a separate endpoint.
Clicking Disable → prompt for current TOTP code (or backup code) → api.disableTotp(code) → State A.
Clicking Regenerate → same confirm step as setup (verify TOTP first) → new backup codes shown once.
Component structure
Keep the setup flow as local state within the settings panel — no new routes needed. Use the same card/section styling as existing settings groups.
AuthUser type change
export interface AuthUser {
id: string;
email: string;
role: string;
onboardingComplete?: boolean;
totpEnabled: boolean; // new
backupCodesCount?: number; // new — only meaningful when totpEnabled
}
Server /api/auth/me must return these fields.
Part of #50.
Location
Add a "Two-factor authentication" section to
SettingsPanel.tsx(Security section, below the password change area if one exists, or as a new Security group).API additions (
api.ts)Also add
totpEnabled: booleantoAuthUserinterface (returned by/api/auth/me).UI states
State A — 2FA disabled
Clicking Enable → starts setup flow (State B).
State B — Setup flow (multi-step within settings)
Step 1: Scan QR code
otpauthUrl(useqrcodenpm package in the frontend, or a<canvas>-based renderer — keep it client-side, no external services)XXXX XXXX XXXX XXXX"Step 2: Verify setup
Step 3: Save your backup codes
.txtfile:taskdial-backup-codes.txtState C — 2FA enabled
backupCodesCountreturned from/api/auth/meor a separate endpoint.Clicking Disable → prompt for current TOTP code (or backup code) →
api.disableTotp(code)→ State A.Clicking Regenerate → same confirm step as setup (verify TOTP first) → new backup codes shown once.
Component structure
Keep the setup flow as local state within the settings panel — no new routes needed. Use the same card/section styling as existing settings groups.
AuthUsertype changeServer
/api/auth/memust return these fields.