fix(api): fix RLS context, DTO validation, and error handling
- Wrap SET LOCAL in transactions for proper connection pooling - Make workspaceId optional in query DTOs (derived from guards) - Replace Error throws with UnauthorizedException in activity controller - Update workspace guard to remove RLS context setting - Document that services should use withUserContext/withUserTransaction
This commit is contained in:
@@ -22,51 +22,58 @@ function getPrismaInstance(): PrismaClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current user ID for RLS policies.
|
||||
* Sets the current user ID for RLS policies within a transaction context.
|
||||
* Must be called before executing any queries that rely on RLS.
|
||||
*
|
||||
* Note: SET LOCAL must be used within a transaction to ensure it's scoped
|
||||
* correctly with connection pooling. This is a low-level function - prefer
|
||||
* using withUserContext or withUserTransaction for most use cases.
|
||||
*
|
||||
* @param userId - The UUID of the current user
|
||||
* @param client - Optional Prisma client (defaults to global prisma)
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await setCurrentUser(userId);
|
||||
* const tasks = await prisma.task.findMany(); // Automatically filtered by RLS
|
||||
* await prisma.$transaction(async (tx) => {
|
||||
* await setCurrentUser(userId, tx);
|
||||
* const tasks = await tx.task.findMany(); // Automatically filtered by RLS
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export async function setCurrentUser(
|
||||
userId: string,
|
||||
client?: PrismaClient
|
||||
client: PrismaClient
|
||||
): Promise<void> {
|
||||
const prismaClient = client || getPrismaInstance();
|
||||
await prismaClient.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = ${userId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current user context.
|
||||
* Clears the current user context within a transaction.
|
||||
* Use this to reset the session or when switching users.
|
||||
*
|
||||
* @param client - Optional Prisma client (defaults to global prisma)
|
||||
* Note: SET LOCAL is automatically cleared at transaction end,
|
||||
* so explicit clearing is typically unnecessary.
|
||||
*
|
||||
* @param client - Prisma client (required - must be a transaction client)
|
||||
*/
|
||||
export async function clearCurrentUser(
|
||||
client?: PrismaClient
|
||||
client: PrismaClient
|
||||
): Promise<void> {
|
||||
const prismaClient = client || getPrismaInstance();
|
||||
await prismaClient.$executeRaw`SET LOCAL app.current_user_id = NULL`;
|
||||
await client.$executeRaw`SET LOCAL app.current_user_id = NULL`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a function with the current user context set.
|
||||
* Automatically sets and clears the user context.
|
||||
* Executes a function with the current user context set within a transaction.
|
||||
* Automatically sets the user context and ensures it's properly scoped.
|
||||
*
|
||||
* @param userId - The UUID of the current user
|
||||
* @param fn - The function to execute with user context
|
||||
* @param fn - The function to execute with user context (receives transaction client)
|
||||
* @returns The result of the function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const tasks = await withUserContext(userId, async () => {
|
||||
* return prisma.task.findMany({
|
||||
* const tasks = await withUserContext(userId, async (tx) => {
|
||||
* return tx.task.findMany({
|
||||
* where: { workspaceId }
|
||||
* });
|
||||
* });
|
||||
@@ -74,16 +81,13 @@ export async function clearCurrentUser(
|
||||
*/
|
||||
export async function withUserContext<T>(
|
||||
userId: string,
|
||||
fn: () => Promise<T>
|
||||
fn: (tx: any) => Promise<T>
|
||||
): Promise<T> {
|
||||
await setCurrentUser(userId);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
// Note: LOCAL settings are automatically cleared at transaction end
|
||||
// but we explicitly clear here for consistency
|
||||
await clearCurrentUser();
|
||||
}
|
||||
const prismaClient = getPrismaInstance();
|
||||
return prismaClient.$transaction(async (tx) => {
|
||||
await setCurrentUser(userId, tx as PrismaClient);
|
||||
return fn(tx);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,9 +172,8 @@ export async function verifyWorkspaceAccess(
|
||||
userId: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const prismaClient = getPrismaInstance();
|
||||
return withUserContext(userId, async () => {
|
||||
const member = await prismaClient.workspaceMember.findUnique({
|
||||
return withUserContext(userId, async (tx) => {
|
||||
const member = await tx.workspaceMember.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
@@ -195,9 +198,8 @@ export async function verifyWorkspaceAccess(
|
||||
* ```
|
||||
*/
|
||||
export async function getUserWorkspaces(userId: string) {
|
||||
const prismaClient = getPrismaInstance();
|
||||
return withUserContext(userId, async () => {
|
||||
return prismaClient.workspace.findMany({
|
||||
return withUserContext(userId, async (tx) => {
|
||||
return tx.workspace.findMany({
|
||||
include: {
|
||||
members: {
|
||||
where: { userId },
|
||||
@@ -219,9 +221,8 @@ export async function isWorkspaceAdmin(
|
||||
userId: string,
|
||||
workspaceId: string
|
||||
): Promise<boolean> {
|
||||
const prismaClient = getPrismaInstance();
|
||||
return withUserContext(userId, async () => {
|
||||
const member = await prismaClient.workspaceMember.findUnique({
|
||||
return withUserContext(userId, async (tx) => {
|
||||
const member = await tx.workspaceMember.findUnique({
|
||||
where: {
|
||||
workspaceId_userId: {
|
||||
workspaceId,
|
||||
|
||||
Reference in New Issue
Block a user