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:
@@ -4,6 +4,7 @@ import { ZodError } from 'zod'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import type { Context } from './context'
|
||||
import type { UserRole } from '@prisma/client'
|
||||
import { checkRateLimit } from '@/lib/rate-limit'
|
||||
|
||||
/**
|
||||
* Initialize tRPC with context type and configuration
|
||||
@@ -298,16 +299,62 @@ const withErrorAudit = middleware(async ({ ctx, next, path, type, getRawInput })
|
||||
}
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Rate Limiting
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* General rate limiter: 100 mutations per minute per user.
|
||||
* Applied to all authenticated mutation procedures.
|
||||
*/
|
||||
const withRateLimit = middleware(async ({ ctx, next, type }) => {
|
||||
// Only rate-limit mutations — queries are read-only
|
||||
if (type !== 'mutation') return next()
|
||||
|
||||
const userId = ctx.session?.user?.id
|
||||
if (!userId) return next()
|
||||
|
||||
const result = checkRateLimit(`trpc:${userId}`, 100, 60_000)
|
||||
if (!result.success) {
|
||||
throw new TRPCError({
|
||||
code: 'TOO_MANY_REQUESTS',
|
||||
message: 'Too many requests. Please try again shortly.',
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
})
|
||||
|
||||
/**
|
||||
* Strict rate limiter for AI-triggering procedures: 5 per hour per user.
|
||||
* Protects against runaway OpenAI API costs.
|
||||
*/
|
||||
const withAIRateLimit = middleware(async ({ ctx, next }) => {
|
||||
const userId = ctx.session?.user?.id
|
||||
if (!userId) return next()
|
||||
|
||||
const result = checkRateLimit(`ai:${userId}`, 5, 60 * 60 * 1000)
|
||||
if (!result.success) {
|
||||
throw new TRPCError({
|
||||
code: 'TOO_MANY_REQUESTS',
|
||||
message: 'AI operation rate limit exceeded. Maximum 5 per hour.',
|
||||
})
|
||||
}
|
||||
|
||||
return next()
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// Procedure Types
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Protected procedure - requires authenticated user.
|
||||
* Mutations auto-audited, errors (FORBIDDEN/UNAUTHORIZED/NOT_FOUND) tracked.
|
||||
* Mutations rate-limited (100/min), auto-audited, errors tracked.
|
||||
*/
|
||||
export const protectedProcedure = t.procedure
|
||||
.use(isAuthenticated)
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -318,6 +365,7 @@ export const protectedProcedure = t.procedure
|
||||
*/
|
||||
export const adminProcedure = t.procedure
|
||||
.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN'))
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -335,6 +383,7 @@ export const superAdminProcedure = t.procedure
|
||||
*/
|
||||
export const juryProcedure = t.procedure
|
||||
.use(hasRole('JURY_MEMBER'))
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -344,6 +393,7 @@ export const juryProcedure = t.procedure
|
||||
*/
|
||||
export const mentorProcedure = t.procedure
|
||||
.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'MENTOR'))
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -353,6 +403,7 @@ export const mentorProcedure = t.procedure
|
||||
*/
|
||||
export const observerProcedure = t.procedure
|
||||
.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'))
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -362,6 +413,7 @@ export const observerProcedure = t.procedure
|
||||
*/
|
||||
export const awardMasterProcedure = t.procedure
|
||||
.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER'))
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
@@ -371,5 +423,12 @@ export const awardMasterProcedure = t.procedure
|
||||
*/
|
||||
export const audienceProcedure = t.procedure
|
||||
.use(isAuthenticated)
|
||||
.use(withRateLimit)
|
||||
.use(withErrorAudit)
|
||||
.use(withMutationAudit)
|
||||
|
||||
/**
|
||||
* AI rate limit middleware - apply to individual AI-triggering procedures.
|
||||
* 5 operations per hour per user to protect OpenAI API costs.
|
||||
*/
|
||||
export { withAIRateLimit }
|
||||
|
||||
Reference in New Issue
Block a user