feat: error audit middleware, impersonation attribution, account lockout logging
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m13s

- Add withErrorAudit middleware tracking FORBIDDEN/UNAUTHORIZED/NOT_FOUND per user
- Fix impersonation attribution: log real admin ID, prefix IMPERSONATED_ on actions
- Add ACCOUNT_LOCKED audit events on login lockout (distinct from LOGIN_FAILED)
- Audit export of assignments and audit logs (meta-audit gap)
- Update audit page UI with new security event types and colors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 18:28:56 +01:00
parent c8c26beed2
commit 13f125af28
4 changed files with 214 additions and 50 deletions

View File

@@ -155,7 +155,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
// Track failed attempt (don't reveal whether user exists)
const current = failedAttempts.get(email) || { count: 0, lockedUntil: 0 }
current.count++
if (current.count >= MAX_LOGIN_ATTEMPTS) {
const wasLocked = current.count >= MAX_LOGIN_ATTEMPTS
if (wasLocked) {
current.lockedUntil = Date.now() + LOCKOUT_DURATION_MS
current.count = 0
}
@@ -171,6 +172,22 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
}).catch(() => {})
// Log account lockout as a distinct security event
if (wasLocked) {
await prisma.auditLog.create({
data: {
userId: null,
action: 'ACCOUNT_LOCKED',
entityType: 'User',
detailsJson: {
email,
reason: 'max_failed_attempts',
lockoutDurationMinutes: LOCKOUT_DURATION_MS / 60000,
},
},
}).catch(() => {})
}
return null
}
@@ -185,7 +202,8 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
// Track failed attempt
const current = failedAttempts.get(email) || { count: 0, lockedUntil: 0 }
current.count++
if (current.count >= MAX_LOGIN_ATTEMPTS) {
const wasLocked = current.count >= MAX_LOGIN_ATTEMPTS
if (wasLocked) {
current.lockedUntil = Date.now() + LOCKOUT_DURATION_MS
current.count = 0
}
@@ -202,6 +220,23 @@ export const { handlers, auth, signIn, signOut } = NextAuth({
},
}).catch(() => {})
// Log account lockout as a distinct security event
if (wasLocked) {
await prisma.auditLog.create({
data: {
userId: user.id,
action: 'ACCOUNT_LOCKED',
entityType: 'User',
entityId: user.id,
detailsJson: {
email,
reason: 'max_failed_attempts',
lockoutDurationMinutes: LOCKOUT_DURATION_MS / 60000,
},
},
}).catch(() => {})
}
return null
}