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

@ -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}
</button>
{/if}
{#if rateLimitCountdown > 0}
<p class="retry-countdown">Erneut versuchen in {rateLimitCountdown}s</p>
{/if}
</div>
</div>
{/if}
@ -652,7 +675,7 @@
<!-- Submit -->
<button
type="submit"
disabled={loading || showSuccess}
disabled={loading || showSuccess || rateLimitCountdown > 0}
class="submit-button"
style:background-color={showSuccess ? '#22c55e' : primaryColor + '60'}
style:border-color={showSuccess ? '#22c55e' : primaryColor}
@ -934,6 +957,11 @@
gap: 0.25rem;
}
.retry-countdown {
font-weight: 600;
margin-top: 0.25rem;
}
.resend-link {
background: none;
border: none;