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

@ -122,6 +122,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -58,6 +58,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"

View file

@ -122,6 +122,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -65,6 +65,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"

View file

@ -105,6 +105,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

@ -49,6 +49,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"

View file

@ -122,6 +122,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -56,6 +56,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"

View file

@ -122,6 +122,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -55,6 +55,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"

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"

View file

@ -138,6 +138,46 @@ export const authStore = {
}
},
async enableTwoFactor(password: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available' };
return authService.enableTwoFactor(password);
},
async disableTwoFactor(password: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available' };
return authService.disableTwoFactor(password);
},
async verifyTwoFactor(code: string, trustDevice?: boolean) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available' };
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' };
const result = await authService.verifyBackupCode(code);
if (result.success) {
const userData = await authService.getUserFromToken();
user = userData;
}
return result;
},
async generateBackupCodes(password: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available' };
return authService.generateBackupCodes(password);
},
async registerPasskey(friendlyName?: string) {
const authService = getAuthService();
if (!authService) return { success: false, error: 'Auth not available' };

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { Button, Input, Card, PageHeader, GlobalSettingsSection } from '@manacore/shared-ui';
import { PasskeyManager } from '@manacore/shared-auth-ui';
import { PasskeyManager, TwoFactorSetup } from '@manacore/shared-auth-ui';
import { authStore } from '$lib/stores/auth.svelte';
import { creditsService } from '$lib/api/credits';
import type { CreditBalance } from '$lib/api/credits';
@ -294,6 +294,19 @@
</div>
</Card>
<!-- Two-Factor Authentication Section -->
<Card>
<div class="p-6">
<TwoFactorSetup
enabled={!!authStore.user?.twoFactorEnabled}
onEnable={(password) => authStore.enableTwoFactor(password)}
onDisable={(password) => authStore.disableTwoFactor(password)}
onGenerateBackupCodes={(password) => authStore.generateBackupCodes(password)}
primaryColor="#6366f1"
/>
</div>
</Card>
<!-- My Data & Danger Zone -->
<Card>
<div class="p-6">

View file

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

View file

@ -93,6 +93,24 @@ export const authStore = {
return true;
},
async verifyTwoFactor(code: string, trustDevice?: boolean) {
const result = await authService.verifyTwoFactor(code, trustDevice);
if (result.success) {
const userData = await authService.getUserFromToken();
user = toManaUser(userData);
}
return result;
},
async verifyBackupCode(code: string) {
const result = await authService.verifyBackupCode(code);
if (result.success) {
const userData = await authService.getUserFromToken();
user = toManaUser(userData);
}
return result;
},
isPasskeyAvailable(): boolean {
return authService.isPasskeyAvailable();
},

View file

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

View file

@ -107,6 +107,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

@ -52,6 +52,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"

View file

@ -120,6 +120,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -54,6 +54,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"

View file

@ -109,6 +109,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

@ -45,6 +45,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"

View file

@ -120,6 +120,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -38,6 +38,8 @@
onResendVerification={handleResendVerification}
passkeyAvailable={authStore.isPasskeyAvailable()}
onSignInWithPasskey={() => authStore.signInWithPasskey()}
onVerifyTwoFactor={(code, trust) => authStore.verifyTwoFactor(code, trust)}
onVerifyBackupCode={(code) => authStore.verifyBackupCode(code)}
{goto}
successRedirect="/app/gallery"
registerPath="/auth/signup"

View file

@ -115,6 +115,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

@ -54,6 +54,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"

View file

@ -84,6 +84,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

@ -36,6 +36,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"

View file

@ -119,6 +119,29 @@ export const auth = {
/**
* Check if passkeys are available in this browser
*/
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

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

View file

@ -113,6 +113,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

@ -60,6 +60,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"

View file

@ -117,6 +117,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

@ -60,6 +60,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"

View file

@ -118,6 +118,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -133,6 +133,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -57,6 +57,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"

View file

@ -122,6 +122,29 @@ export const authStore = {
/**
* Check if passkeys are available in this browser
*/
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

@ -55,6 +55,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"