diff --git a/apps/web/client/src/server/api/routers/project/helper.ts b/apps/web/client/src/server/api/routers/project/helper.ts index e2f6037378..b522900dd5 100644 --- a/apps/web/client/src/server/api/routers/project/helper.ts +++ b/apps/web/client/src/server/api/routers/project/helper.ts @@ -1,4 +1,8 @@ -import { type Frame } from "@onlook/db"; +import { eq } from "drizzle-orm"; +import { type Frame, projects, userProjects, type DrizzleDb } from "@onlook/db"; + +/** Type representing a db instance or transaction that has query capabilities */ +type DbOrTx = Pick; export function extractCsbPort(frames: Frame[]): number | null { if (!frames || frames.length === 0) return null; @@ -17,3 +21,32 @@ export function extractCsbPort(frames: Frame[]): number | null { } return null; } + +/** + * Verifies that a user has access to a project by checking the userProjects table. + * @throws Error if the user does not have access to the project or if it doesn't exist + * + * Note: This function intentionally returns the same error message whether the project + * doesn't exist or the user lacks access to prevent information disclosure about + * project existence. + * + * Accepts either a db instance or a transaction to support atomic authorization checks. + */ +export async function verifyProjectAccess( + db: DbOrTx, + userId: string, + projectId: string, +): Promise { + const project = await db.query.projects.findFirst({ + where: eq(projects.id, projectId), + with: { + userProjects: { + where: eq(userProjects.userId, userId), + }, + }, + }); + + if (!project || project.userProjects.length === 0) { + throw new Error('Unauthorized or not found'); + } +} diff --git a/apps/web/client/src/server/api/routers/project/project.ts b/apps/web/client/src/server/api/routers/project/project.ts index a110db5bbf..bfece724ee 100644 --- a/apps/web/client/src/server/api/routers/project/project.ts +++ b/apps/web/client/src/server/api/routers/project/project.ts @@ -37,7 +37,7 @@ import { and, eq, ne } from 'drizzle-orm'; import { z } from 'zod'; import { projectCreateRequestRouter } from './createRequest'; import { fork } from './fork'; -import { extractCsbPort } from './helper'; +import { extractCsbPort, verifyProjectAccess } from './helper'; export const projectRouter = createTRPCRouter({ hasAccess: protectedProcedure @@ -59,6 +59,7 @@ export const projectRouter = createTRPCRouter({ .input(z.object({ projectId: z.string() })) .mutation(async ({ ctx, input }) => { try { + await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId); if (!env.FIRECRAWL_API_KEY) { throw new Error('FIRECRAWL_API_KEY is not configured'); } @@ -349,8 +350,9 @@ export const projectRouter = createTRPCRouter({ .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { await ctx.db.transaction(async (tx) => { - await tx.delete(projects).where(eq(projects.id, input.id)); + await verifyProjectAccess(tx, ctx.user.id, input.id); await tx.delete(userProjects).where(eq(userProjects.projectId, input.id)); + await tx.delete(projects).where(eq(projects.id, input.id)); }); }), getPreviewProjects: protectedProcedure @@ -365,6 +367,7 @@ export const projectRouter = createTRPCRouter({ return projects.map((project) => fromDbProject(project.project)); }), update: protectedProcedure.input(projectUpdateSchema).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.id); const [updatedProject] = await ctx.db.update(projects).set({ ...input, updatedAt: new Date(), @@ -380,6 +383,7 @@ export const projectRouter = createTRPCRouter({ projectId: z.string(), tag: z.string(), })).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId); const project = await ctx.db.query.projects.findFirst({ where: eq(projects.id, input.projectId), }); @@ -404,6 +408,7 @@ export const projectRouter = createTRPCRouter({ projectId: z.string(), tag: z.string(), })).mutation(async ({ ctx, input }) => { + await verifyProjectAccess(ctx.db, ctx.user.id, input.projectId); const project = await ctx.db.query.projects.findFirst({ where: eq(projects.id, input.projectId), });