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

@ -818,6 +818,25 @@ export class AuthController {
}
}
// =========================================================================
// Security Events
// =========================================================================
/**
* Get user security events (audit log)
*
* Returns the authenticated user's security events ordered by most recent first.
*/
@Get('security-events')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Get user security events (audit log)' })
@ApiBearerAuth('JWT-auth')
@ApiResponse({ status: 200, description: 'Returns security events' })
@ApiResponse({ status: 401, description: 'Not authenticated' })
async getSecurityEvents(@CurrentUser() user: CurrentUserData, @Req() req: Request) {
return this.betterAuthService.getSecurityEvents(user.userId);
}
// =========================================================================
// Passkey (WebAuthn) Endpoints
// =========================================================================

View file

@ -2117,4 +2117,29 @@ export class BetterAuthService {
throw new UnauthorizedException('Failed to exchange session for tokens');
}
}
/**
* Get security events for a user (audit log)
*/
async getSecurityEvents(userId: string, limit = 50) {
const db = getDb(this.databaseUrl);
const { securityEvents } = await import('../../db/schema');
const { eq, desc } = await import('drizzle-orm');
const events = await db
.select({
id: securityEvents.id,
eventType: securityEvents.eventType,
ipAddress: securityEvents.ipAddress,
userAgent: securityEvents.userAgent,
metadata: securityEvents.metadata,
createdAt: securityEvents.createdAt,
})
.from(securityEvents)
.where(eq(securityEvents.userId, userId))
.orderBy(desc(securityEvents.createdAt))
.limit(limit);
return events;
}
}