feat(auth): add TOTP two-factor authentication across all apps

Uses Better Auth's built-in twoFactor plugin for TOTP + backup codes:

Backend (mana-core-auth):
- twoFactor plugin in better-auth.config.ts (issuer: ManaCore)
- twoFactorEnabled field on users table, backupCodes as encrypted text
- 2FA redirect detection in signIn flow
- Passthrough controller forwards /two-factor/* to Better Auth
- Security event types for 2FA operations

Client (shared-auth):
- enableTwoFactor, disableTwoFactor, verifyTwoFactor, verifyBackupCode,
  generateBackupCodes methods with session-to-token exchange

UI (shared-auth-ui):
- LoginPage: 2FA code input view after password login, backup code toggle
- TwoFactorSetup: settings component with enable/disable/QR code/backup codes

App integration:
- All 19 auth stores have verifyTwoFactor() and verifyBackupCode()
- All 19 login pages pass onVerifyTwoFactor and onVerifyBackupCode callbacks
- ManaCore settings page has TwoFactorSetup component

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 19:55:09 +01:00
parent 90e6135637
commit f5a9edcfb6
49 changed files with 1800 additions and 169 deletions

View file

@ -106,6 +106,28 @@ export const authStore = {
}
},
async verifyTwoFactor(code: string, trustDevice?: boolean) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
const result = await authService.verifyTwoFactor(code, trustDevice);
if (result.success) {
const userData = await authService.getUserFromToken();
user = userData;
}
return result;
},
async verifyBackupCode(code: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available on server' };
const result = await authService.verifyBackupCode(code);
if (result.success) {
const userData = await authService.getUserFromToken();
user = userData;
}
return result;
},
isPasskeyAvailable(): boolean {
const authService = getAuthService();
if (!authService) return false;

View file

@ -50,6 +50,8 @@
onResendVerification={handleResendVerification}
passkeyAvailable={authStore.isPasskeyAvailable()}
onSignInWithPasskey={() => authStore.signInWithPasskey()}
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
{goto}
successRedirect={redirectTo}
registerPath="/register"