From 0dfd603892c0a92f88e3d91e8ddfc66406b1160c Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 26 Mar 2026 21:58:56 +0100 Subject: [PATCH] feat(auth): rate limit feedback, audit log UI, and E2E tests Rate-limiting feedback: - LoginPage detects 429/account-locked errors and shows countdown timer - Submit button disabled during cooldown period Audit log: - GET /auth/security-events endpoint (JWT-protected) in auth controller - getSecurityEvents() in BetterAuthService + shared-auth client - AuditLog component with event type labels, relative dates, UA parsing - Integrated in ManaCore settings page E2E tests (passkey-2fa.e2e-spec.ts): - Passkey registration/authentication flow tests - Auth guard enforcement (protected vs public endpoints) - 2FA passthrough route existence tests - Edge cases (cross-user access, missing fields, token shape) CSRF note: Already covered by Better Auth (SameSite + HttpOnly + Trusted Origins). Token refresh already has 4-retry + offline detection. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/stores/auth.svelte.ts | 6 + .../src/routes/(app)/settings/+page.svelte | 26 +- .../src/components/AuditLog.svelte | 414 ++++++++++++++ packages/shared-auth-ui/src/index.ts | 1 + .../shared-auth-ui/src/pages/LoginPage.svelte | 30 +- packages/shared-auth/src/core/authService.ts | 19 + .../src/auth/auth.controller.ts | 19 + .../src/auth/services/better-auth.service.ts | 25 + .../test/e2e/passkey-2fa.e2e-spec.ts | 523 ++++++++++++++++++ 9 files changed, 1061 insertions(+), 2 deletions(-) create mode 100644 packages/shared-auth-ui/src/components/AuditLog.svelte create mode 100644 services/mana-core-auth/test/e2e/passkey-2fa.e2e-spec.ts diff --git a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts index f0c50b504..91af0fb40 100644 --- a/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/manacore/apps/web/src/lib/stores/auth.svelte.ts @@ -202,6 +202,12 @@ export const authStore = { return authService.renamePasskey(passkeyId, friendlyName); }, + async getSecurityEvents() { + const authService = getAuthService(); + if (!authService) return []; + return authService.getSecurityEvents(); + }, + /** * Sign in with email and password */ diff --git a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte index d351fe3df..fa69256a1 100644 --- a/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/manacore/apps/web/src/routes/(app)/settings/+page.svelte @@ -1,7 +1,7 @@ + +
+
+
+
+ + + +
+
+

Sicherheitsprotokoll

+

Letzte Aktivitäten deines Kontos

+
+
+ +
+ + {#if loading && events.length === 0} +
+
+
+ {:else if events.length === 0} +

Keine Sicherheitsereignisse vorhanden.

+ {:else} +
+ {#each events as event (event.id)} + {@const info = getEventInfo(event.eventType)} +
+
+
+
+ {info.label} + {#if info.badgeText} + {info.badgeText} + {/if} +
+
+ {formatDate(event.createdAt)} + {#if event.ipAddress} + · + {event.ipAddress} + {/if} +
+ {#if parseUserAgent(event.userAgent)} +
+ {parseUserAgent(event.userAgent)} +
+ {/if} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/packages/shared-auth-ui/src/index.ts b/packages/shared-auth-ui/src/index.ts index 099f78261..ae5115c51 100644 --- a/packages/shared-auth-ui/src/index.ts +++ b/packages/shared-auth-ui/src/index.ts @@ -12,6 +12,7 @@ export { default as PasskeyManager } from './components/PasskeyManager.svelte'; export { default as TwoFactorSetup } from './components/TwoFactorSetup.svelte'; export { default as SecurityOnboarding } from './components/SecurityOnboarding.svelte'; export { default as ChangePassword } from './components/ChangePassword.svelte'; +export { default as AuditLog } from './components/AuditLog.svelte'; // Utilities export { diff --git a/packages/shared-auth-ui/src/pages/LoginPage.svelte b/packages/shared-auth-ui/src/pages/LoginPage.svelte index 1a763a623..ee057c564 100644 --- a/packages/shared-auth-ui/src/pages/LoginPage.svelte +++ b/packages/shared-auth-ui/src/pages/LoginPage.svelte @@ -145,6 +145,16 @@ let twoFactorCode = $state(''); let useBackupCode = $state(false); let trustDevice = $state(false); + let rateLimitCountdown = $state(0); + + $effect(() => { + if (rateLimitCountdown > 0) { + const timer = setTimeout(() => { + rateLimitCountdown--; + }, 1000); + return () => clearTimeout(timer); + } + }); // Theme state - can be toggled manually, defaults to system preference let userThemePreference = $state<'light' | 'dark' | null>(null); @@ -252,6 +262,16 @@ setError(t.emailNotVerified || 'Email not verified.', 'general'); } else { setError(result.error || t.signInFailed, 'general'); + + // Detect rate limiting + if (result.error?.includes('Too Many') || result.error?.includes('rate limit')) { + rateLimitCountdown = 60; // 1 minute cooldown + } else if ( + result.error?.includes('temporarily locked') || + result.error === 'ACCOUNT_LOCKED' + ) { + rateLimitCountdown = (result as any).retryAfter || 300; // 5 min default + } } } @@ -568,6 +588,9 @@ {resendingVerification ? t.resendingVerification : t.resendVerification} {/if} + {#if rateLimitCountdown > 0} +

Erneut versuchen in {rateLimitCountdown}s

+ {/if} {/if} @@ -652,7 +675,7 @@