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
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:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user