diff --git a/src/server/routers/project.ts b/src/server/routers/project.ts index 7e48465..b725c36 100644 --- a/src/server/routers/project.ts +++ b/src/server/routers/project.ts @@ -177,14 +177,38 @@ export const projectRouter = router({ ] } - // Jury members can only see assigned projects (but not if they also have admin roles) - if ( - userHasRole(ctx.user, 'JURY_MEMBER') && - !userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN') - ) { - where.assignments = { - ...((where.assignments as Record) || {}), - some: { userId: ctx.user.id }, + // Per-role visibility filters. Admin / Observer / Award master see all + // (these roles are designed for cross-program oversight). Other roles + // are scoped to projects they have a relationship with. + const isAdmin = userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN') + const isObserverLevel = userHasRole(ctx.user, 'OBSERVER', 'AWARD_MASTER') + if (!isAdmin && !isObserverLevel) { + const orClauses: Array> = [] + if (userHasRole(ctx.user, 'JURY_MEMBER')) { + orClauses.push({ assignments: { some: { userId: ctx.user.id } } }) + } + if (userHasRole(ctx.user, 'MENTOR')) { + orClauses.push({ mentorAssignment: { mentorId: ctx.user.id } }) + } + if (userHasRole(ctx.user, 'APPLICANT')) { + orClauses.push({ teamMembers: { some: { userId: ctx.user.id } } }) + orClauses.push({ submittedByUserId: ctx.user.id }) + } + if (orClauses.length === 0) { + // No relationship-based access (e.g. AUDIENCE) — return nothing. + where.id = { in: [] } + } else if (orClauses.length === 1) { + Object.assign(where, orClauses[0]) + } else { + // Multiple roles — combine with the existing search OR if any. + // Compose with existing `where.OR` (from `search`) by AND-ing. + const existingOr = where.OR as Array> | undefined + if (existingOr) { + where.AND = [{ OR: existingOr }, { OR: orClauses }] + delete where.OR + } else { + where.OR = orClauses + } } } @@ -510,19 +534,42 @@ export const projectRouter = router({ // ProjectTag table may not exist yet } - // Check access for jury members (but not if they also have admin roles) - if (userHasRole(ctx.user, 'JURY_MEMBER') && !userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')) { - const assignment = await ctx.prisma.assignment.findFirst({ - where: { - projectId: input.id, - userId: ctx.user.id, - }, - }) - - if (!assignment) { + // Per-role access check. Admin / Observer / Award master can read any + // project. Jury / Mentor / Applicant must have a relationship to it. + const isAdmin = userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN') + const isObserverLevel = userHasRole(ctx.user, 'OBSERVER', 'AWARD_MASTER') + if (!isAdmin && !isObserverLevel) { + const checks: Array> = [] + if (userHasRole(ctx.user, 'JURY_MEMBER')) { + checks.push( + ctx.prisma.assignment.findFirst({ + where: { projectId: input.id, userId: ctx.user.id }, + select: { id: true }, + }), + ) + } + if (userHasRole(ctx.user, 'MENTOR')) { + checks.push( + ctx.prisma.mentorAssignment.findFirst({ + where: { projectId: input.id, mentorId: ctx.user.id }, + select: { id: true }, + }), + ) + } + if (userHasRole(ctx.user, 'APPLICANT')) { + checks.push( + ctx.prisma.teamMember.findFirst({ + where: { projectId: input.id, userId: ctx.user.id }, + select: { id: true }, + }), + ) + } + const results = await Promise.all(checks) + const hasAccess = results.some((r) => r !== null && r !== undefined) + if (!hasAccess) { throw new TRPCError({ code: 'FORBIDDEN', - message: 'You are not assigned to this project', + message: 'You do not have access to this project', }) } }