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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-26 21:58:56 +01:00
parent 11ab265d55
commit 0dfd603892
9 changed files with 1061 additions and 2 deletions

View file

@ -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
*/

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, TwoFactorSetup } from '@manacore/shared-auth-ui';
import { PasskeyManager, TwoFactorSetup, AuditLog } 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';
@ -23,6 +23,10 @@
// Credits data
let creditBalance = $state<CreditBalance | null>(null);
// Security events
let securityEvents = $state<any[]>([]);
let securityEventsLoading = $state(false);
onMount(async () => {
if (authStore.isAuthenticated) {
try {
@ -30,6 +34,10 @@
passkeys = await authStore.listPasskeys();
// Load user settings from server
await userSettings.load();
// Load security events
securityEventsLoading = true;
securityEvents = await authStore.getSecurityEvents();
securityEventsLoading = false;
} catch (e) {
console.error('Failed to load data:', e);
}
@ -307,6 +315,22 @@
</div>
</Card>
<!-- Security Log Section -->
<Card>
<div class="p-6">
<AuditLog
events={securityEvents}
loading={securityEventsLoading}
onRefresh={async () => {
securityEventsLoading = true;
securityEvents = await authStore.getSecurityEvents();
securityEventsLoading = false;
}}
primaryColor="#6366f1"
/>
</div>
</Card>
<!-- My Data & Danger Zone -->
<Card>
<div class="p-6">