From 07f507585f82ce0ef3af1d86c4d32f17073d65ff Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 26 Feb 2026 22:04:02 -0600 Subject: [PATCH] fix(api,web): resolve RLS context SQL error, workspace guard crash, and projects response unwrapping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three runtime bugs found during production site testing: 1. PrismaService.setWorkspaceContext used SET LOCAL with Prisma tagged templates, which produces parameterized SQL ($1) that PostgreSQL rejects in SET statements. Changed to set_config() which safely accepts parameterized values — matching the pattern already used in RlsContextInterceptor. 2. WorkspaceGuard.extractWorkspaceId accessed request.body.workspaceId without null-checking body, causing TypeError on GET requests where body is undefined. Added runtime type guard with explicit cast. 3. fetchProjects() cast the API response as Project[] but the backend returns { data: Project[], meta: {...} } paginated wrapper. Added response.data unwrapping to match the actual API contract. Co-Authored-By: Claude Opus 4.6 --- apps/api/src/common/guards/workspace.guard.ts | 8 ++++---- apps/api/src/prisma/prisma.service.ts | 11 +++++++---- apps/web/src/lib/api/projects.ts | 3 ++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/api/src/common/guards/workspace.guard.ts b/apps/api/src/common/guards/workspace.guard.ts index 75d065f..058441a 100644 --- a/apps/api/src/common/guards/workspace.guard.ts +++ b/apps/api/src/common/guards/workspace.guard.ts @@ -110,10 +110,10 @@ export class WorkspaceGuard implements CanActivate { return paramWorkspaceId; } - // 3. Check request body - const bodyWorkspaceId = request.body.workspaceId; - if (typeof bodyWorkspaceId === "string") { - return bodyWorkspaceId; + // 3. Check request body (body may be undefined for GET requests despite Express typings) + const body = request.body as Record | undefined; + if (body && typeof body.workspaceId === "string") { + return body.workspaceId; } // 4. Check query string (backward compatibility for existing clients) diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts index 66cfbfd..6721734 100644 --- a/apps/api/src/prisma/prisma.service.ts +++ b/apps/api/src/prisma/prisma.service.ts @@ -140,8 +140,11 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul workspaceId: string, client: PrismaClient = this ): Promise { - await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`; - await client.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`; + // Use set_config() instead of SET LOCAL so values are safely parameterized. + // SET LOCAL with Prisma's tagged template produces invalid SQL (bind parameter $1 + // is not supported in SET statements by PostgreSQL). + await client.$executeRaw`SELECT set_config('app.current_user_id', ${userId}, true)`; + await client.$executeRaw`SELECT set_config('app.current_workspace_id', ${workspaceId}, true)`; } /** @@ -151,8 +154,8 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul * @param client - Optional Prisma client (uses 'this' if not provided) */ async clearWorkspaceContext(client: PrismaClient = this): Promise { - await client.$executeRaw`SET LOCAL app.current_user_id = NULL`; - await client.$executeRaw`SET LOCAL app.current_workspace_id = NULL`; + await client.$executeRaw`SELECT set_config('app.current_user_id', '', true)`; + await client.$executeRaw`SELECT set_config('app.current_workspace_id', '', true)`; } /** diff --git a/apps/web/src/lib/api/projects.ts b/apps/web/src/lib/api/projects.ts index 89a11ca..8d68448 100644 --- a/apps/web/src/lib/api/projects.ts +++ b/apps/web/src/lib/api/projects.ts @@ -65,7 +65,8 @@ export interface UpdateProjectDto { * Fetch all projects for a workspace */ export async function fetchProjects(workspaceId?: string): Promise { - return apiGet("/api/projects", workspaceId); + const response = await apiGet<{ data: Project[]; meta?: unknown }>("/api/projects", workspaceId); + return response.data; } /**