From 05d9d1962cf0120d3e8620e6ca99bfbfe7e95b50 Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 18:48:00 +0200 Subject: [PATCH] fix(shared-auth): proxy passkey/2FA/session methods through ManaAuthStore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The settings page in mana/web (and any future consumer that wants to manage passkeys, 2FA, or sessions from the UI) was calling 11 methods on `authStore` that the wrapper had never exposed: listPasskeys, registerPasskey, deletePasskey, renamePasskey, listSessions, revokeSession, getSecurityEvents, enableTwoFactor, disableTwoFactor, generateBackupCodes — all of which DO exist on the underlying AuthServiceInterface but were silently dropped by createManaAuthStore. Result: 17 type errors on settings/+page.svelte and a complete dead-end for anyone trying to wire up the UI. Fix: add thin passthrough wrappers in createManaAuthStore that delegate to authService. Each handles the SSR/no-service case the same way the existing methods do (return empty array or {success:false} with a stable error message). enableTwoFactor and disableTwoFactor additionally refresh the local user snapshot after success because the JWT issued post-enrollment carries the new flag and downstream UI gates on it. Type fixes that fell out of touching settings/+page.svelte: - UserData.twoFactorEnabled?: boolean — optional flag on the public user shape. The TwoFactorSetup component reads it via `authStore.user?.twoFactorEnabled` to gate the enable/disable button; without the type the call site coerced through `any`. - CreditBalance.{freeCreditsRemaining,dailyFreeCredits}?: number — daily-free accounting fields the backend already returns but the local type was missing. Optional because not every backend deployment turns them on. - settings/+page.svelte: `authStore.user?.sub` → `?.id`. The public UserData shape uses `id`; `sub` is the raw JWT claim name and never made it onto the consumer type. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/mana/apps/web/src/lib/api/credits.ts | 8 +++ .../src/routes/(app)/settings/+page.svelte | 2 +- .../src/stores/createManaAuthStore.svelte.ts | 65 +++++++++++++++++++ packages/shared-auth/src/types/index.ts | 7 ++ 4 files changed, 81 insertions(+), 1 deletion(-) diff --git a/apps/mana/apps/web/src/lib/api/credits.ts b/apps/mana/apps/web/src/lib/api/credits.ts index 3daf227c5..b1425caf9 100644 --- a/apps/mana/apps/web/src/lib/api/credits.ts +++ b/apps/mana/apps/web/src/lib/api/credits.ts @@ -11,6 +11,14 @@ export interface CreditBalance { balance: number; totalEarned: number; totalSpent: number; + /** + * Daily-free-credit accounting. Optional because the backend only + * returns these fields when the user has a free-tier allowance + * configured (paying users get them too but with `dailyFreeCredits = 0`). + * Settings UIs render the "free today" tile only when both are present. + */ + freeCreditsRemaining?: number; + dailyFreeCredits?: number; } export interface CreditTransaction { diff --git a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte index 0c8ac30ab..720c4f05a 100644 --- a/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/settings/+page.svelte @@ -274,7 +274,7 @@

Deine eindeutige Kennung

- {authStore.user?.sub?.slice(0, 8) || '...'}... + {authStore.user?.id?.slice(0, 8) || '...'}... diff --git a/packages/shared-auth-ui/src/stores/createManaAuthStore.svelte.ts b/packages/shared-auth-ui/src/stores/createManaAuthStore.svelte.ts index 0ec2436d8..db6606983 100644 --- a/packages/shared-auth-ui/src/stores/createManaAuthStore.svelte.ts +++ b/packages/shared-auth-ui/src/stores/createManaAuthStore.svelte.ts @@ -314,6 +314,71 @@ export function createManaAuthStore(config: ManaAuthStoreConfig = {}) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; } }, + + // Passkey CRUD — thin passthroughs to authService. The settings page + // (and any other consumer) needs these to render the PasskeyManager + // UI. Each method swallows the SSR/no-service case the same way the + // rest of the wrapper does. + async listPasskeys() { + const authService = getAuthService(); + if (!authService) return [] as unknown[]; + return authService.listPasskeys(); + }, + async registerPasskey(friendlyName?: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + return authService.registerPasskey(friendlyName); + }, + async deletePasskey(passkeyId: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + return authService.deletePasskey(passkeyId); + }, + async renamePasskey(passkeyId: string, friendlyName: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + return authService.renamePasskey(passkeyId, friendlyName); + }, + + // Two-factor passthroughs. enableTwoFactor refreshes the local user + // snapshot on success because the JWT issued post-enrollment carries + // the new flag and downstream UI gates on it. + async enableTwoFactor(password: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + const result = await authService.enableTwoFactor(password); + if (result.success) user = await authService.getUserFromToken(); + return result; + }, + async disableTwoFactor(password: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + const result = await authService.disableTwoFactor(password); + if (result.success) user = await authService.getUserFromToken(); + return result; + }, + async generateBackupCodes(password: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + return authService.generateBackupCodes(password); + }, + + // Sessions + audit log passthroughs. + async listSessions() { + const authService = getAuthService(); + if (!authService) return [] as unknown[]; + return authService.listSessions(); + }, + async revokeSession(sessionId: string) { + const authService = getAuthService(); + if (!authService) return { success: false, error: 'Auth not available on server' }; + return authService.revokeSession(sessionId); + }, + async getSecurityEvents(limit?: number) { + const authService = getAuthService(); + if (!authService) return [] as unknown[]; + return authService.getSecurityEvents(limit); + }, }; } diff --git a/packages/shared-auth/src/types/index.ts b/packages/shared-auth/src/types/index.ts index f57bf412b..515ca0c82 100644 --- a/packages/shared-auth/src/types/index.ts +++ b/packages/shared-auth/src/types/index.ts @@ -52,6 +52,13 @@ export interface UserData { email: string; role: string; tier: string; + /** + * Whether 2FA is enrolled. Optional because the field is only present + * when the JWT was minted with the `twofa` claim — guest tokens and + * legacy sessions omit it. Settings UIs that gate the "enable 2FA" + * button on this should default to `false` when undefined. + */ + twoFactorEnabled?: boolean; } /**