fix(api,web): resolve RLS context SQL error, workspace guard crash, and projects response unwrapping
All checks were successful
ci/woodpecker/push/api Pipeline was successful
ci/woodpecker/push/web Pipeline was successful

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 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 22:04:02 -06:00
parent 21bf7e050f
commit 07f507585f
3 changed files with 13 additions and 9 deletions

View File

@@ -110,10 +110,10 @@ export class WorkspaceGuard implements CanActivate {
return paramWorkspaceId; return paramWorkspaceId;
} }
// 3. Check request body // 3. Check request body (body may be undefined for GET requests despite Express typings)
const bodyWorkspaceId = request.body.workspaceId; const body = request.body as Record<string, unknown> | undefined;
if (typeof bodyWorkspaceId === "string") { if (body && typeof body.workspaceId === "string") {
return bodyWorkspaceId; return body.workspaceId;
} }
// 4. Check query string (backward compatibility for existing clients) // 4. Check query string (backward compatibility for existing clients)

View File

@@ -140,8 +140,11 @@ export class PrismaService extends PrismaClient implements OnModuleInit, OnModul
workspaceId: string, workspaceId: string,
client: PrismaClient = this client: PrismaClient = this
): Promise<void> { ): Promise<void> {
await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`; // Use set_config() instead of SET LOCAL so values are safely parameterized.
await client.$executeRaw`SET LOCAL app.current_workspace_id = ${workspaceId}`; // 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) * @param client - Optional Prisma client (uses 'this' if not provided)
*/ */
async clearWorkspaceContext(client: PrismaClient = this): Promise<void> { async clearWorkspaceContext(client: PrismaClient = this): Promise<void> {
await client.$executeRaw`SET LOCAL app.current_user_id = NULL`; await client.$executeRaw`SELECT set_config('app.current_user_id', '', true)`;
await client.$executeRaw`SET LOCAL app.current_workspace_id = NULL`; await client.$executeRaw`SELECT set_config('app.current_workspace_id', '', true)`;
} }
/** /**

View File

@@ -65,7 +65,8 @@ export interface UpdateProjectDto {
* Fetch all projects for a workspace * Fetch all projects for a workspace
*/ */
export async function fetchProjects(workspaceId?: string): Promise<Project[]> { export async function fetchProjects(workspaceId?: string): Promise<Project[]> {
return apiGet<Project[]>("/api/projects", workspaceId); const response = await apiGet<{ data: Project[]; meta?: unknown }>("/api/projects", workspaceId);
return response.data;
} }
/** /**