feat: router.back() navigation, read-only evaluation view, auth audit logging
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m53s

- Convert all Back buttons platform-wide (38 files) to use router.back()
  for natural browser-back behavior regardless of entry point
- Add read-only view for submitted evaluations in closed rounds with
  blue banner, disabled inputs, and contextual back navigation
- Add auth audit logs: MAGIC_LINK_SENT, PASSWORD_RESET_LINK_CLICKED,
  PASSWORD_RESET_LINK_EXPIRED, PASSWORD_RESET_LINK_INVALID
- Learning Hub links navigate in same window for all roles
- Update settings descriptions to reflect all-user scope

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 14:25:56 +01:00
parent a556732b46
commit a1e758bc39
44 changed files with 398 additions and 384 deletions

View File

@@ -1628,14 +1628,44 @@ export const userRouter = router({
})
if (!user) {
await logAudit({
prisma: ctx.prisma,
userId: null,
action: 'PASSWORD_RESET_LINK_INVALID',
entityType: 'User',
entityId: 'unknown',
detailsJson: { reason: 'token_not_found', timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Invalid or expired reset link. Please request a new one.' })
}
if (user.status === 'SUSPENDED') {
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_INVALID',
entityType: 'User',
entityId: user.id,
detailsJson: { reason: 'account_suspended', email: user.email, timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
throw new TRPCError({ code: 'FORBIDDEN', message: 'This account has been suspended.' })
}
if (user.passwordResetExpiresAt && user.passwordResetExpiresAt < new Date()) {
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_EXPIRED',
entityType: 'User',
entityId: user.id,
detailsJson: { email: user.email, expiredAt: user.passwordResetExpiresAt.toISOString(), timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
// Clear expired token
await ctx.prisma.user.update({
where: { id: user.id },
@@ -1644,6 +1674,18 @@ export const userRouter = router({
throw new TRPCError({ code: 'BAD_REQUEST', message: 'This reset link has expired. Please request a new one.' })
}
// Audit: reset link clicked and valid
await logAudit({
prisma: ctx.prisma,
userId: user.id,
action: 'PASSWORD_RESET_LINK_CLICKED',
entityType: 'User',
entityId: user.id,
detailsJson: { email: user.email, timestamp: new Date().toISOString() },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
}).catch(() => {})
// Hash and save new password, clear reset token
const passwordHash = await hashPassword(input.password)
await ctx.prisma.user.update({