diff --git a/apps/sim/socket/database/operations.ts b/apps/sim/socket/database/operations.ts index 2aae11de3..7cef59a19 100644 --- a/apps/sim/socket/database/operations.ts +++ b/apps/sim/socket/database/operations.ts @@ -625,44 +625,74 @@ async function handleBlocksOperationTx( logger.info(`Batch removing ${ids.length} blocks from workflow ${workflowId}`) + // Fetch all blocks to check lock status and filter out protected blocks + const allBlocks = await tx + .select({ + id: workflowBlocks.id, + type: workflowBlocks.type, + locked: workflowBlocks.locked, + data: workflowBlocks.data, + }) + .from(workflowBlocks) + .where(eq(workflowBlocks.workflowId, workflowId)) + + type BlockRecord = (typeof allBlocks)[number] + const blocksById: Record = Object.fromEntries( + allBlocks.map((b: BlockRecord) => [b.id, b]) + ) + + // Helper to check if a block is protected (locked or inside locked parent) + const isProtected = (blockId: string): boolean => { + const block = blocksById[blockId] + if (!block) return false + if (block.locked) return true + const parentId = (block.data as Record | null)?.parentId as + | string + | undefined + if (parentId && blocksById[parentId]?.locked) return true + return false + } + + // Filter out protected blocks from deletion request + const deletableIds = ids.filter((id) => !isProtected(id)) + if (deletableIds.length === 0) { + logger.info('All requested blocks are protected, skipping deletion') + return + } + + if (deletableIds.length < ids.length) { + logger.info( + `Filtered out ${ids.length - deletableIds.length} protected blocks from deletion` + ) + } + // Collect all block IDs including children of subflows - const allBlocksToDelete = new Set(ids) + const allBlocksToDelete = new Set(deletableIds) - for (const id of ids) { - const blockToRemove = await tx - .select({ type: workflowBlocks.type }) - .from(workflowBlocks) - .where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId))) - .limit(1) - - if (blockToRemove.length > 0 && isSubflowBlockType(blockToRemove[0].type)) { - const childBlocks = await tx - .select({ id: workflowBlocks.id }) - .from(workflowBlocks) - .where( - and( - eq(workflowBlocks.workflowId, workflowId), - sql`${workflowBlocks.data}->>'parentId' = ${id}` - ) - ) - - childBlocks.forEach((child: { id: string }) => allBlocksToDelete.add(child.id)) + for (const id of deletableIds) { + const block = blocksById[id] + if (block && isSubflowBlockType(block.type)) { + // Include all children of the subflow (they should be deleted with parent) + for (const b of allBlocks) { + const parentId = (b.data as Record | null)?.parentId + if (parentId === id) { + allBlocksToDelete.add(b.id) + } + } } } const blockIdsArray = Array.from(allBlocksToDelete) - // Collect parent IDs BEFORE deleting blocks + // Collect parent IDs BEFORE deleting blocks (use blocksById, already fetched) const parentIds = new Set() - for (const id of ids) { - const parentInfo = await tx - .select({ parentId: sql`${workflowBlocks.data}->>'parentId'` }) - .from(workflowBlocks) - .where(and(eq(workflowBlocks.id, id), eq(workflowBlocks.workflowId, workflowId))) - .limit(1) - - if (parentInfo.length > 0 && parentInfo[0].parentId) { - parentIds.add(parentInfo[0].parentId) + for (const id of deletableIds) { + const block = blocksById[id] + const parentId = (block?.data as Record | null)?.parentId as + | string + | undefined + if (parentId) { + parentIds.add(parentId) } } diff --git a/apps/sim/socket/middleware/permissions.ts b/apps/sim/socket/middleware/permissions.ts index 6bae2bb93..b160a061b 100644 --- a/apps/sim/socket/middleware/permissions.ts +++ b/apps/sim/socket/middleware/permissions.ts @@ -14,7 +14,10 @@ import { const logger = createLogger('SocketPermissions') -// All write operations (admin and write roles have same permissions) +// Admin-only operations (require admin role) +const ADMIN_ONLY_OPERATIONS: string[] = [BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED] + +// Write operations (admin and write roles both have these permissions) const WRITE_OPERATIONS: string[] = [ // Block operations BLOCK_OPERATIONS.UPDATE_POSITION, @@ -30,7 +33,6 @@ const WRITE_OPERATIONS: string[] = [ BLOCKS_OPERATIONS.BATCH_REMOVE_BLOCKS, BLOCKS_OPERATIONS.BATCH_TOGGLE_ENABLED, BLOCKS_OPERATIONS.BATCH_TOGGLE_HANDLES, - BLOCKS_OPERATIONS.BATCH_TOGGLE_LOCKED, BLOCKS_OPERATIONS.BATCH_UPDATE_PARENT, // Edge operations EDGE_OPERATIONS.ADD, @@ -52,7 +54,7 @@ const READ_OPERATIONS: string[] = [ // Define operation permissions based on role const ROLE_PERMISSIONS: Record = { - admin: WRITE_OPERATIONS, + admin: [...ADMIN_ONLY_OPERATIONS, ...WRITE_OPERATIONS], write: WRITE_OPERATIONS, read: READ_OPERATIONS, } diff --git a/apps/sim/stores/workflows/utils.ts b/apps/sim/stores/workflows/utils.ts index 18bf38fb1..03039b7f8 100644 --- a/apps/sim/stores/workflows/utils.ts +++ b/apps/sim/stores/workflows/utils.ts @@ -520,7 +520,6 @@ export function regenerateBlockIds( } } else { // Parent doesn't exist anywhere OR parent is locked - clear the relationship - // Parent doesn't exist anywhere - clear the relationship block.data = { ...block.data, parentId: undefined, extent: undefined } } }