H1: Add verifyScriptAccess/verifyRevisionAccess to all 14 revisions endpoints H2: Add verifyProjectAccess to listScripts and searchScripts M2: Add cascade delete for projectMembers on project deletion M4: Replace plain Error throws with TRPCError for consistent error handling M5: Use crypto.randomUUID for team ID generation (was Date.now + Math.random) L1: Add 100KB content size limit on revision content L2: Add unique constraint to script slug column L3: Update hasProjectAccess middleware to check project membership
98 lines
3.3 KiB
TypeScript
98 lines
3.3 KiB
TypeScript
import { initTRPC, TRPCError } from '@trpc/server';
|
|
import { z } from 'zod';
|
|
import { eq, and } from 'drizzle-orm';
|
|
import { projects, projectMembers } 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.clerkUserId) {
|
|
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
|
|
}
|
|
return next({ ctx: { ...ctx, clerkUserId: ctx.clerkUserId } });
|
|
});
|
|
|
|
// Middleware for database access and user lookup
|
|
const hasDb = t.middleware(async ({ ctx, next }) => {
|
|
if (!ctx.db) {
|
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
|
|
}
|
|
|
|
let userId: number | undefined;
|
|
if (ctx.clerkUserId) {
|
|
const { users } = await import('../../src/db/schema');
|
|
const userRows = await ctx.db.select({ id: users.id })
|
|
.from(users)
|
|
.where(eq(users.clerkId, ctx.clerkUserId));
|
|
if (userRows.length > 0) {
|
|
userId = userRows[0].id;
|
|
}
|
|
}
|
|
|
|
return next({ ctx: { ...ctx, db: ctx.db, userId } });
|
|
});
|
|
|
|
// 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.clerkUserId) {
|
|
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'User not authenticated' });
|
|
}
|
|
if (!ctx.db) {
|
|
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: 'Database not available' });
|
|
}
|
|
const { users } = await import('../../src/db/schema');
|
|
const userRows = await ctx.db.select({ id: users.id, clerkId: users.clerkId })
|
|
.from(users)
|
|
.where(eq(users.clerkId, ctx.clerkUserId));
|
|
const dbUser = userRows[0];
|
|
if (!dbUser) {
|
|
throw new TRPCError({ code: 'FORBIDDEN', message: 'User mapping not found' });
|
|
}
|
|
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 === dbUser.id) {
|
|
return next({ ctx: { ...ctx, projectId: ctx.projectId, userId: dbUser.id } });
|
|
}
|
|
// L3 fix: also check project membership
|
|
const memberRows = await ctx.db.select()
|
|
.from(projectMembers)
|
|
.where(and(eq(projectMembers.projectId, ctx.projectId), eq(projectMembers.userId, dbUser.id)));
|
|
if (memberRows.length === 0) {
|
|
throw new TRPCError({ code: 'FORBIDDEN', message: `You do not have access to project ${ctx.projectId}` });
|
|
}
|
|
return next({ ctx: { ...ctx, projectId: ctx.projectId, userId: dbUser.id } });
|
|
});
|
|
|
|
// 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 };
|