Platform review round 2: audit logging migration, nav unification, DB indexes, and UI polish
- Migrate ~41 inline audit log calls to shared logAudit() utility across all routers - Add transaction-aware prisma parameter to logAudit() for atomic operations - Unify jury/mentor/observer navigation into shared RoleNav component - Add composite DB indexes (Evaluation, GracePeriod, AuditLog) for query performance - Fix profile page: consolidate dual save buttons, proper useEffect initialization - Enhance auth error page with MOPC branding and navigation - Improve observer dashboard with prominent read-only badge - Fix DI-3: fetch projects before bulk status update for accurate notifications - Remove unused aiBoost field from smart-assignment scoring - Add shared image-upload utility and structured logger module Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
notifyRoundJury,
|
||||
NotificationTypes,
|
||||
} from '../services/in-app-notification'
|
||||
import { logAudit } from '@/server/utils/audit'
|
||||
|
||||
export const roundRouter = router({
|
||||
/**
|
||||
@@ -114,40 +115,43 @@ export const roundRouter = router({
|
||||
const now = new Date()
|
||||
const shouldAutoActivate = input.votingStartAt && input.votingStartAt <= now
|
||||
|
||||
const round = await ctx.prisma.round.create({
|
||||
data: {
|
||||
...rest,
|
||||
sortOrder,
|
||||
status: shouldAutoActivate ? 'ACTIVE' : 'DRAFT',
|
||||
settingsJson: settingsJson as Prisma.InputJsonValue ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
// For FILTERING rounds, automatically move all projects from the program to this round
|
||||
if (input.roundType === 'FILTERING') {
|
||||
await ctx.prisma.project.updateMany({
|
||||
where: {
|
||||
round: { programId: input.programId },
|
||||
roundId: { not: round.id },
|
||||
},
|
||||
const round = await ctx.prisma.$transaction(async (tx) => {
|
||||
const created = await tx.round.create({
|
||||
data: {
|
||||
roundId: round.id,
|
||||
status: 'SUBMITTED',
|
||||
...rest,
|
||||
sortOrder,
|
||||
status: shouldAutoActivate ? 'ACTIVE' : 'DRAFT',
|
||||
settingsJson: settingsJson as Prisma.InputJsonValue ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
// For FILTERING rounds, automatically move all projects from the program to this round
|
||||
if (input.roundType === 'FILTERING') {
|
||||
await tx.project.updateMany({
|
||||
where: {
|
||||
round: { programId: input.programId },
|
||||
roundId: { not: created.id },
|
||||
},
|
||||
data: {
|
||||
roundId: created.id,
|
||||
status: 'SUBMITTED',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Audit log
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'Round',
|
||||
entityId: round.id,
|
||||
detailsJson: { ...rest, settingsJson } as Prisma.InputJsonValue,
|
||||
entityId: created.id,
|
||||
detailsJson: { ...rest, settingsJson } as Record<string, unknown>,
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
return created
|
||||
})
|
||||
|
||||
return round
|
||||
@@ -215,26 +219,28 @@ export const roundRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
const round = await ctx.prisma.round.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(autoActivate && { status: 'ACTIVE' }),
|
||||
settingsJson: settingsJson as Prisma.InputJsonValue ?? undefined,
|
||||
},
|
||||
})
|
||||
const round = await ctx.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.round.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...data,
|
||||
...(autoActivate && { status: 'ACTIVE' }),
|
||||
settingsJson: settingsJson as Prisma.InputJsonValue ?? undefined,
|
||||
},
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'UPDATE',
|
||||
entityType: 'Round',
|
||||
entityId: id,
|
||||
detailsJson: { ...data, settingsJson } as Prisma.InputJsonValue,
|
||||
detailsJson: { ...data, settingsJson } as Record<string, unknown>,
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
return round
|
||||
@@ -275,11 +281,6 @@ export const roundRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
const round = await ctx.prisma.round.update({
|
||||
where: { id: input.id },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
// Map status to specific action name
|
||||
const statusActionMap: Record<string, string> = {
|
||||
ACTIVE: 'ROUND_ACTIVATED',
|
||||
@@ -288,9 +289,14 @@ export const roundRouter = router({
|
||||
}
|
||||
const action = statusActionMap[input.status] || 'UPDATE_STATUS'
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
const round = await ctx.prisma.$transaction(async (tx) => {
|
||||
const updated = await tx.round.update({
|
||||
where: { id: input.id },
|
||||
data: updateData,
|
||||
})
|
||||
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action,
|
||||
entityType: 'Round',
|
||||
@@ -306,7 +312,9 @@ export const roundRouter = router({
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
return updated
|
||||
})
|
||||
|
||||
// Notify jury members when round is activated
|
||||
@@ -485,16 +493,15 @@ export const roundRouter = router({
|
||||
}
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'UPDATE_EVALUATION_FORM',
|
||||
entityType: 'EvaluationForm',
|
||||
entityId: form.id,
|
||||
detailsJson: { roundId, criteriaCount: criteria.length },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'UPDATE_EVALUATION_FORM',
|
||||
entityType: 'EvaluationForm',
|
||||
entityId: form.id,
|
||||
detailsJson: { roundId, criteriaCount: criteria.length },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return form
|
||||
@@ -525,13 +532,9 @@ export const roundRouter = router({
|
||||
},
|
||||
})
|
||||
|
||||
await ctx.prisma.round.delete({
|
||||
where: { id: input.id },
|
||||
})
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
await ctx.prisma.$transaction(async (tx) => {
|
||||
await logAudit({
|
||||
prisma: tx,
|
||||
userId: ctx.user.id,
|
||||
action: 'DELETE',
|
||||
entityType: 'Round',
|
||||
@@ -544,7 +547,11 @@ export const roundRouter = router({
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
})
|
||||
|
||||
await tx.round.delete({
|
||||
where: { id: input.id },
|
||||
})
|
||||
})
|
||||
|
||||
return round
|
||||
@@ -601,16 +608,15 @@ export const roundRouter = router({
|
||||
}
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'ASSIGN_PROJECTS_TO_ROUND',
|
||||
entityType: 'Round',
|
||||
entityId: input.roundId,
|
||||
detailsJson: { projectCount: updated.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'ASSIGN_PROJECTS_TO_ROUND',
|
||||
entityType: 'Round',
|
||||
entityId: input.roundId,
|
||||
detailsJson: { projectCount: updated.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { assigned: updated.count }
|
||||
@@ -640,16 +646,15 @@ export const roundRouter = router({
|
||||
const deleted = { count: updated.count }
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'REMOVE_PROJECTS_FROM_ROUND',
|
||||
entityType: 'Round',
|
||||
entityId: input.roundId,
|
||||
detailsJson: { projectCount: deleted.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'REMOVE_PROJECTS_FROM_ROUND',
|
||||
entityType: 'Round',
|
||||
entityId: input.roundId,
|
||||
detailsJson: { projectCount: deleted.count },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { removed: deleted.count }
|
||||
@@ -711,20 +716,19 @@ export const roundRouter = router({
|
||||
const created = { count: updated.count }
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'ADVANCE_PROJECTS',
|
||||
entityType: 'Round',
|
||||
entityId: input.toRoundId,
|
||||
detailsJson: {
|
||||
fromRoundId: input.fromRoundId,
|
||||
toRoundId: input.toRoundId,
|
||||
projectCount: created.count,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'ADVANCE_PROJECTS',
|
||||
entityType: 'Round',
|
||||
entityId: input.toRoundId,
|
||||
detailsJson: {
|
||||
fromRoundId: input.fromRoundId,
|
||||
toRoundId: input.toRoundId,
|
||||
projectCount: created.count,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { advanced: created.count }
|
||||
@@ -752,16 +756,15 @@ export const roundRouter = router({
|
||||
)
|
||||
|
||||
// Audit log
|
||||
await ctx.prisma.auditLog.create({
|
||||
data: {
|
||||
userId: ctx.user.id,
|
||||
action: 'REORDER_ROUNDS',
|
||||
entityType: 'Program',
|
||||
entityId: input.programId,
|
||||
detailsJson: { roundIds: input.roundIds },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
},
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'REORDER_ROUNDS',
|
||||
entityType: 'Program',
|
||||
entityId: input.programId,
|
||||
detailsJson: { roundIds: input.roundIds },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
|
||||
Reference in New Issue
Block a user