fix: security hardening + performance refactoring (code review batch 1)
- IDOR fix: deliberation vote now verifies juryMemberId === ctx.user.id - Rate limiting: tRPC middleware (100/min), AI endpoints (5/hr), auth IP-based (10/15min) - 6 compound indexes added to Prisma schema - N+1 eliminated in processRoundClose (batch updateMany/createMany) - N+1 eliminated in batchCheckRequirementsAndTransition (3 batch queries) - Service extraction: juror-reassignment.ts (578 lines) - Dead code removed: award.ts, cohort.ts, decision.ts (680 lines) - 35 bare catch blocks replaced across 16 files - Fire-and-forget async calls fixed - Notification false positive bug fixed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -941,11 +941,95 @@ export async function batchCheckRequirementsAndTransition(
|
||||
actorId: string,
|
||||
prisma: PrismaClient | any,
|
||||
): Promise<{ transitionedCount: number; projectIds: string[] }> {
|
||||
const transitioned: string[] = []
|
||||
if (projectIds.length === 0) return { transitionedCount: 0, projectIds: [] }
|
||||
|
||||
// Pre-load all requirements for this round in batch (avoids per-project queries)
|
||||
const [requirements, round] = await Promise.all([
|
||||
prisma.fileRequirement.findMany({
|
||||
where: { roundId, isRequired: true },
|
||||
select: { id: true },
|
||||
}),
|
||||
prisma.round.findUnique({
|
||||
where: { id: roundId },
|
||||
select: { submissionWindowId: true },
|
||||
}),
|
||||
])
|
||||
|
||||
let submissionRequirements: Array<{ id: string }> = []
|
||||
if (round?.submissionWindowId) {
|
||||
submissionRequirements = await prisma.submissionFileRequirement.findMany({
|
||||
where: { submissionWindowId: round.submissionWindowId, required: true },
|
||||
select: { id: true },
|
||||
})
|
||||
}
|
||||
|
||||
// If no requirements at all, nothing to check
|
||||
if (requirements.length === 0 && submissionRequirements.length === 0) {
|
||||
return { transitionedCount: 0, projectIds: [] }
|
||||
}
|
||||
|
||||
// Pre-load all project files and current states in batch
|
||||
type FileRow = { projectId: string; requirementId: string | null; submissionFileRequirementId: string | null }
|
||||
type StateRow = { projectId: string; state: string }
|
||||
|
||||
const [allFiles, allStates] = await Promise.all([
|
||||
prisma.projectFile.findMany({
|
||||
where: {
|
||||
projectId: { in: projectIds },
|
||||
roundId,
|
||||
},
|
||||
select: { projectId: true, requirementId: true, submissionFileRequirementId: true },
|
||||
}) as Promise<FileRow[]>,
|
||||
prisma.projectRoundState.findMany({
|
||||
where: { roundId, projectId: { in: projectIds } },
|
||||
select: { projectId: true, state: true },
|
||||
}) as Promise<StateRow[]>,
|
||||
])
|
||||
|
||||
// Build per-project lookup maps
|
||||
const filesByProject = new Map<string, FileRow[]>()
|
||||
for (const f of allFiles) {
|
||||
const arr = filesByProject.get(f.projectId) ?? []
|
||||
arr.push(f)
|
||||
filesByProject.set(f.projectId, arr)
|
||||
}
|
||||
const stateByProject = new Map(allStates.map((s) => [s.projectId, s.state]))
|
||||
|
||||
// Determine which projects have all requirements met and are eligible for transition
|
||||
const eligibleStates = ['PENDING', 'IN_PROGRESS']
|
||||
const toTransition: string[] = []
|
||||
|
||||
for (const projectId of projectIds) {
|
||||
const result = await checkRequirementsAndTransition(projectId, roundId, actorId, prisma)
|
||||
if (result.transitioned) {
|
||||
const currentState = stateByProject.get(projectId)
|
||||
if (!currentState || !eligibleStates.includes(currentState)) continue
|
||||
|
||||
const files = filesByProject.get(projectId) ?? []
|
||||
|
||||
// Check legacy requirements
|
||||
if (requirements.length > 0) {
|
||||
const fulfilledIds = new Set(files.map((f) => f.requirementId).filter(Boolean))
|
||||
if (!requirements.every((r: { id: string }) => fulfilledIds.has(r.id))) continue
|
||||
}
|
||||
|
||||
// Check submission requirements
|
||||
if (submissionRequirements.length > 0) {
|
||||
const fulfilledSubIds = new Set(files.map((f) => f.submissionFileRequirementId).filter(Boolean))
|
||||
if (!submissionRequirements.every((r: { id: string }) => fulfilledSubIds.has(r.id))) continue
|
||||
}
|
||||
|
||||
toTransition.push(projectId)
|
||||
}
|
||||
|
||||
// Transition eligible projects (still uses transitionProject for state machine correctness)
|
||||
const transitioned: string[] = []
|
||||
for (const projectId of toTransition) {
|
||||
const currentState = stateByProject.get(projectId)
|
||||
// If PENDING, first move to IN_PROGRESS
|
||||
if (currentState === 'PENDING') {
|
||||
await triggerInProgressOnActivity(projectId, roundId, actorId, prisma)
|
||||
}
|
||||
const result = await transitionProject(projectId, roundId, 'COMPLETED' as ProjectRoundStateValue, actorId, prisma)
|
||||
if (result.success) {
|
||||
transitioned.push(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user