2026-01-30 13:41:32 +01:00
|
|
|
import { initTRPC, TRPCError } from '@trpc/server'
|
|
|
|
|
import superjson from 'superjson'
|
|
|
|
|
import { ZodError } from 'zod'
|
|
|
|
|
import type { Context } from './context'
|
|
|
|
|
import type { UserRole } from '@prisma/client'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Initialize tRPC with context type and configuration
|
|
|
|
|
*/
|
|
|
|
|
const t = initTRPC.context<Context>().create({
|
|
|
|
|
transformer: superjson,
|
|
|
|
|
errorFormatter({ shape, error }) {
|
|
|
|
|
return {
|
|
|
|
|
...shape,
|
|
|
|
|
data: {
|
|
|
|
|
...shape.data,
|
|
|
|
|
zodError:
|
|
|
|
|
error.cause instanceof ZodError ? error.cause.flatten() : null,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Export reusable router and procedure helpers
|
|
|
|
|
*/
|
|
|
|
|
export const router = t.router
|
|
|
|
|
export const publicProcedure = t.procedure
|
|
|
|
|
export const middleware = t.middleware
|
|
|
|
|
export const createCallerFactory = t.createCallerFactory
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Middleware
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware to require authenticated user
|
|
|
|
|
*/
|
|
|
|
|
const isAuthenticated = middleware(async ({ ctx, next }) => {
|
|
|
|
|
if (!ctx.session?.user) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'UNAUTHORIZED',
|
|
|
|
|
message: 'You must be logged in to perform this action',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return next({
|
|
|
|
|
ctx: {
|
|
|
|
|
...ctx,
|
|
|
|
|
user: ctx.session.user,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware to require specific role(s)
|
|
|
|
|
*/
|
|
|
|
|
const hasRole = (...roles: UserRole[]) =>
|
|
|
|
|
middleware(async ({ ctx, next }) => {
|
|
|
|
|
if (!ctx.session?.user) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'UNAUTHORIZED',
|
|
|
|
|
message: 'You must be logged in to perform this action',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!roles.includes(ctx.session.user.role)) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'FORBIDDEN',
|
|
|
|
|
message: 'You do not have permission to perform this action',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return next({
|
|
|
|
|
ctx: {
|
|
|
|
|
...ctx,
|
|
|
|
|
user: ctx.session.user,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Middleware for audit logging
|
|
|
|
|
*/
|
|
|
|
|
const withAuditLog = middleware(async ({ ctx, next, path }) => {
|
|
|
|
|
const result = await next()
|
|
|
|
|
|
|
|
|
|
// Log successful mutations
|
|
|
|
|
if (result.ok && path.includes('.')) {
|
|
|
|
|
const [, action] = path.split('.')
|
|
|
|
|
const mutationActions = ['create', 'update', 'delete', 'import', 'submit', 'grant', 'revoke']
|
|
|
|
|
|
|
|
|
|
if (mutationActions.some((a) => action?.toLowerCase().includes(a))) {
|
|
|
|
|
// Audit logging would happen here
|
|
|
|
|
// We'll implement this in the audit service
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// =============================================================================
|
|
|
|
|
// Procedure Types
|
|
|
|
|
// =============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Protected procedure - requires authenticated user
|
|
|
|
|
*/
|
|
|
|
|
export const protectedProcedure = t.procedure.use(isAuthenticated)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Admin procedure - requires SUPER_ADMIN or PROGRAM_ADMIN role
|
|
|
|
|
*/
|
|
|
|
|
export const adminProcedure = t.procedure.use(
|
|
|
|
|
hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Super admin procedure - requires SUPER_ADMIN role
|
|
|
|
|
*/
|
|
|
|
|
export const superAdminProcedure = t.procedure.use(hasRole('SUPER_ADMIN'))
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Jury procedure - requires JURY_MEMBER role
|
|
|
|
|
*/
|
|
|
|
|
export const juryProcedure = t.procedure.use(hasRole('JURY_MEMBER'))
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mentor procedure - requires MENTOR role (or admin)
|
|
|
|
|
*/
|
|
|
|
|
export const mentorProcedure = t.procedure.use(
|
|
|
|
|
hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'MENTOR')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Observer procedure - requires OBSERVER role (read-only access)
|
|
|
|
|
*/
|
|
|
|
|
export const observerProcedure = t.procedure.use(
|
|
|
|
|
hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER')
|
|
|
|
|
)
|
|
|
|
|
|
Round system redesign: Phases 1-7 complete
Full pipeline/track/stage architecture replacing the legacy round system.
Schema: 11 new models (Pipeline, Track, Stage, StageTransition,
ProjectStageState, RoutingRule, Cohort, CohortProject, LiveProgressCursor,
OverrideAction, AudienceVoter) + 8 new enums.
Backend: 9 new routers (pipeline, stage, routing, stageFiltering,
stageAssignment, cohort, live, decision, award) + 6 new services
(stage-engine, routing-engine, stage-filtering, stage-assignment,
stage-notifications, live-control).
Frontend: Pipeline wizard (17 components), jury stage pages (7),
applicant pipeline pages (3), public stage pages (2), admin pipeline
pages (5), shared stage components (3), SSE route, live hook.
Phase 6 refit: 23 routers/services migrated from roundId to stageId,
all frontend components refitted. Deleted round.ts (985 lines),
roundTemplate.ts, round-helpers.ts, round-settings.ts, round-type-settings.tsx,
10 legacy admin pages, 7 legacy jury pages, 3 legacy dialogs.
Phase 7 validation: 36 tests (10 unit + 8 integration files) all passing,
TypeScript 0 errors, Next.js build succeeds, 13 integrity checks,
legacy symbol sweep clean, auto-seed on first Docker startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 13:57:09 +01:00
|
|
|
/**
|
|
|
|
|
* Award master procedure - requires AWARD_MASTER role (or admin)
|
|
|
|
|
*/
|
|
|
|
|
export const awardMasterProcedure = t.procedure.use(
|
|
|
|
|
hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER')
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Audience procedure - requires any authenticated user
|
|
|
|
|
*/
|
|
|
|
|
export const audienceProcedure = t.procedure.use(isAuthenticated)
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
/**
|
|
|
|
|
* Protected procedure with audit logging
|
|
|
|
|
*/
|
|
|
|
|
export const auditedProcedure = t.procedure
|
|
|
|
|
.use(isAuthenticated)
|
|
|
|
|
.use(withAuditLog)
|