Files
FrenoCorp/server/trpc/router.ts
Michael Freno 0ba20e5b31 FRE-592: Fix 4 code review blockers (security + correctness)
- Add project ownership verification to relationship mutations (createRelationship,
  updateRelationship, deleteRelationship, getRelationshipsForCharacter)
- Add project ownership verification to getCharacter and getScene
- Add ownership check to projectProcedure middleware (hasProjectAccess)
- Fix searchCharacters filter combination bug (accumulate conditions instead of
  overwriting)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-24 10:13:49 -04:00

71 lines
2.3 KiB
TypeScript

import { initTRPC, TRPCError } from '@trpc/server';
import { z } from 'zod';
import { eq } from 'drizzle-orm';
import { projects } from '../../src/db/schema';
import type { TRPCContext } from './types';
// Initialize tRPC with context
const t = initTRPC.context<TRPCContext>().create();
// Middleware for authentication
const isAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
}
return next({ ctx: { ...ctx, userId: ctx.userId } });
});
// Middleware for database access
const hasDb = t.middleware(({ ctx, next }) => {
if (!ctx.db) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
}
return next({ ctx: { ...ctx, db: ctx.db } });
});
// Middleware for project ownership verification
const hasProjectAccess = t.middleware(async ({ ctx, next }) => {
if (!ctx.projectId) {
throw new TRPCError({ code: 'FORBIDDEN', message: 'Project access required' });
}
if (!ctx.userId) {
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
}
if (!ctx.db) {
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
}
const rows = await ctx.db.select({ id: projects.id, ownerId: projects.ownerId })
.from(projects)
.where(eq(projects.id, ctx.projectId));
const project = rows[0];
if (!project) {
throw new TRPCError({ code: 'NOT_FOUND', message: `Project ${ctx.projectId} not found` });
}
if (project.ownerId !== ctx.userId) {
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${ctx.projectId}` });
}
return next({ ctx: { ...ctx, projectId: ctx.projectId } });
});
// Base router
export const baseRouter = t.router;
// Procedure builders
export const publicProcedure = t.procedure.use(hasDb);
export const protectedProcedure = t.procedure.use(isAuthenticated).use(hasDb);
export const projectProcedure = t.procedure
.use(isAuthenticated)
.use(hasDb)
.use(hasProjectAccess);
// Validation middleware
export const validateInput = <T extends z.ZodTypeAny>(schema: T) => {
return t.middleware(({ input, next }) => {
const validated = schema.parse(input);
return next({ input: validated });
});
};
export { t, TRPCError };