From 04c54b6794ad5d1a813e3ce8e88441ab2e24cbac Mon Sep 17 00:00:00 2001 From: Matt Date: Tue, 17 Feb 2026 22:23:16 +0100 Subject: [PATCH] =?UTF-8?q?Fix=20FK=20constraint=20error=20on=20filtering?= =?UTF-8?q?=20override=20=E2=80=94=20verify=20user=20exists?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overriddenBy FK to User was failing when the session contained a stale user ID (e.g. after database reseed). Added ensureUserExists() guard to all override/reinstate mutations and switched single-record updates to use Prisma connect syntax for safer FK resolution. Co-Authored-By: Claude Opus 4.6 --- src/server/routers/filtering.ts | 48 ++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/server/routers/filtering.ts b/src/server/routers/filtering.ts index 43bd179..fdb4ce8 100644 --- a/src/server/routers/filtering.ts +++ b/src/server/routers/filtering.ts @@ -12,6 +12,24 @@ import { NotificationTypes, } from '../services/in-app-notification' +/** + * Verify the current session user exists in the database. + * Guards against stale JWT sessions (e.g., after database reseed). + */ +async function ensureUserExists(db: PrismaClient, userId: string): Promise { + const user = await db.user.findUnique({ + where: { id: userId }, + select: { id: true }, + }) + if (!user) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'Your session refers to a user that no longer exists. Please log out and log back in.', + }) + } + return user.id +} + /** * Extract a numeric confidence/quality score from aiScreeningJson. * Looks for common keys: overallScore, confidenceScore, score, qualityScore. @@ -908,18 +926,20 @@ export const filteringRouter = router({ }) ) .mutation(async ({ ctx, input }) => { + const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id) + const result = await ctx.prisma.filteringResult.update({ where: { id: input.id }, data: { finalOutcome: input.finalOutcome, - overriddenBy: ctx.user.id, + overriddenByUser: { connect: { id: verifiedUserId } }, overriddenAt: new Date(), overrideReason: input.reason, }, }) await logAudit({ - userId: ctx.user.id, + userId: verifiedUserId, action: 'UPDATE', entityType: 'FilteringResult', entityId: input.id, @@ -946,18 +966,20 @@ export const filteringRouter = router({ }) ) .mutation(async ({ ctx, input }) => { + const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id) + await ctx.prisma.filteringResult.updateMany({ where: { id: { in: input.ids } }, data: { finalOutcome: input.finalOutcome, - overriddenBy: ctx.user.id, + overriddenBy: verifiedUserId, overriddenAt: new Date(), overrideReason: input.reason, }, }) await logAudit({ - userId: ctx.user.id, + userId: verifiedUserId, action: 'BULK_UPDATE_STATUS', entityType: 'FilteringResult', detailsJson: { @@ -983,6 +1005,8 @@ export const filteringRouter = router({ }) ) .mutation(async ({ ctx, input }) => { + const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id) + const currentRound = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.roundId }, select: { id: true, competitionId: true, sortOrder: true, name: true }, @@ -1101,7 +1125,7 @@ export const filteringRouter = router({ where: { id: { in: demotedIds } }, data: { finalOutcome: 'FLAGGED', - overriddenBy: ctx.user.id, + overriddenBy: verifiedUserId, overriddenAt: new Date(), overrideReason: 'Demoted by category target enforcement', }, @@ -1112,7 +1136,7 @@ export const filteringRouter = router({ await ctx.prisma.$transaction(operations) await logAudit({ - userId: ctx.user.id, + userId: verifiedUserId, action: 'UPDATE', entityType: 'Stage', entityId: input.roundId, @@ -1149,6 +1173,8 @@ export const filteringRouter = router({ }) ) .mutation(async ({ ctx, input }) => { + const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id) + await ctx.prisma.filteringResult.update({ where: { roundId_projectId: { @@ -1158,7 +1184,7 @@ export const filteringRouter = router({ }, data: { finalOutcome: 'PASSED', - overriddenBy: ctx.user.id, + overriddenByUser: { connect: { id: verifiedUserId } }, overriddenAt: new Date(), overrideReason: 'Reinstated by admin', }, @@ -1170,7 +1196,7 @@ export const filteringRouter = router({ }) await logAudit({ - userId: ctx.user.id, + userId: verifiedUserId, action: 'UPDATE', entityType: 'FilteringResult', detailsJson: { @@ -1192,6 +1218,8 @@ export const filteringRouter = router({ }) ) .mutation(async ({ ctx, input }) => { + const verifiedUserId = await ensureUserExists(ctx.prisma, ctx.user.id) + await ctx.prisma.$transaction([ ...input.projectIds.map((projectId) => ctx.prisma.filteringResult.update({ @@ -1203,7 +1231,7 @@ export const filteringRouter = router({ }, data: { finalOutcome: 'PASSED', - overriddenBy: ctx.user.id, + overriddenByUser: { connect: { id: verifiedUserId } }, overriddenAt: new Date(), overrideReason: 'Bulk reinstated by admin', }, @@ -1216,7 +1244,7 @@ export const filteringRouter = router({ ]) await logAudit({ - userId: ctx.user.id, + userId: verifiedUserId, action: 'BULK_UPDATE_STATUS', entityType: 'FilteringResult', detailsJson: {