Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
756d07a
remove local storage usage
Jun 14, 2025
5236784
remove migration for last active workspace id
Jun 15, 2025
45c19f5
Update apps/sim/app/w/[id]/components/workflow-block/components/sub-b…
icecrasher321 Jun 15, 2025
29cab60
add url builder util
Jun 15, 2025
c50bf8e
merge
Jun 15, 2025
beccbd0
fi
Jun 15, 2025
db21043
fix lint
Jun 16, 2025
da3761e
Merge branch 'main' into improvement/local-storage
icecrasher321 Jun 16, 2025
1882d1e
lint
Jun 16, 2025
ef8b17f
modify pre commit hook
Jun 16, 2025
549b0f8
fix oauth
Jun 16, 2025
de8da66
get last active workspace working again
Jun 16, 2025
79029df
new workspace logic works
Jun 16, 2025
f1ce4fc
fetch locks
Jun 17, 2025
379bbb5
works now
Jun 17, 2025
36e35e2
remove empty useEffect
Jun 17, 2025
deb8fef
fix loading issue
Jun 17, 2025
771d673
skip empty workflow syncs
Jun 17, 2025
5ddbf00
use isWorkspace in transition flag
Jun 17, 2025
75192aa
add logging
Jun 17, 2025
51b0c70
add data initialized flag
Jun 17, 2025
db836de
fix lint
Jun 17, 2025
fa87f0e
fix: build error by create a server-side utils
emir-karabeg Jun 17, 2025
9970239
remove migration snapshots
Jun 17, 2025
ca5a445
Merge branch 'improvement/local-storage' of github.com:simstudioai/si…
Jun 17, 2025
9ef67a7
reverse search for workspace based on workflow id
Jun 17, 2025
0f0ce83
Merge branch 'main' into improvement/local-storage
icecrasher321 Jun 17, 2025
243958a
fix lint
Jun 17, 2025
91853e1
Merge branch 'improvement/local-storage' of github.com:simstudioai/si…
Jun 17, 2025
141af66
improvement: loading check and animation
emir-karabeg Jun 17, 2025
65747c5
remove unused utils
Jun 17, 2025
597cc07
remove console logs
Jun 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bunx lint-staged
bun lint
2 changes: 1 addition & 1 deletion apps/sim/app/api/chat/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { chat, environment as envTable, userStats, workflow } from '@/db/schema'
import { Executor } from '@/executor'
import type { BlockLog } from '@/executor/types'
import { Serializer } from '@/serializer'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { mergeSubblockState } from '@/stores/workflows/server-utils'
import type { WorkflowState } from '@/stores/workflows/workflow/types'

declare global {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/schedules/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { db } from '@/db'
import { environment, userStats, workflow, workflowSchedule } from '@/db/schema'
import { Executor } from '@/executor'
import { Serializer } from '@/serializer'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { mergeSubblockState } from '@/stores/workflows/server-utils'
import type { WorkflowState } from '@/stores/workflows/workflow/types'

// Add dynamic export to prevent caching
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/workflows/[id]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { db } from '@/db'
import { environment, userStats } from '@/db/schema'
import { Executor } from '@/executor'
import { Serializer } from '@/serializer'
import { mergeSubblockState } from '@/stores/workflows/utils'
import { mergeSubblockState } from '@/stores/workflows/server-utils'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
import { validateWorkflowAccess } from '../../middleware'
import { createErrorResponse, createSuccessResponse } from '../../utils'
Expand Down
145 changes: 114 additions & 31 deletions apps/sim/app/api/workflows/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
import { and, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { workflow, workspaceMember } from '@/db/schema'

const logger = createLogger('WorkflowDetailAPI')
const logger = createLogger('WorkflowByIdAPI')

export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
/**
* GET /api/workflows/[id]
* Fetch a single workflow by ID
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const requestId = crypto.randomUUID().slice(0, 8)
const startTime = Date.now()
const { id: workflowId } = await params

try {
// Get the session
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized workflow access attempt`)
logger.warn(`[${requestId}] Unauthorized access attempt for workflow ${workflowId}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const { id: workflowId } = await params
const userId = session.user.id

if (!workflowId) {
return NextResponse.json({ error: 'Workflow ID is required' }, { status: 400 })
}

// Fetch the workflow from database
// Fetch the workflow
const workflowData = await db
.select()
.from(workflow)
Expand All @@ -38,42 +39,124 @@ export async function GET(request: Request, { params }: { params: Promise<{ id:
}

// Check if user has access to this workflow
// User can access if they own it OR if it's in a workspace they're part of
const canAccess = workflowData.userId === session.user.id
let hasAccess = false

// Case 1: User owns the workflow
if (workflowData.userId === userId) {
hasAccess = true
}

if (!canAccess && workflowData.workspaceId) {
// Check workspace membership
// Case 2: Workflow belongs to a workspace the user is a member of
if (!hasAccess && workflowData.workspaceId) {
const membership = await db
.select()
.select({ id: workspaceMember.id })
.from(workspaceMember)
.where(
and(
eq(workspaceMember.workspaceId, workflowData.workspaceId),
eq(workspaceMember.userId, session.user.id)
eq(workspaceMember.userId, userId)
)
)
.then((rows) => rows[0])

if (membership) {
// User is a member of the workspace, allow access
const elapsed = Date.now() - startTime
logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`)
return NextResponse.json({ data: workflowData }, { status: 200 })
hasAccess = true
}
}

if (!hasAccess) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine we're going to have a lot of the same logic to verify if a user can modify a workflow. We should make a generic access control to see if the user can delete or view a workflow based on ownership or membership of a workspace.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, makes sense. Will add it along with the generic permissions system we need to make.

logger.warn(`[${requestId}] User ${userId} denied access to workflow ${workflowId}`)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}

const elapsed = Date.now() - startTime
logger.info(`[${requestId}] Successfully fetched workflow ${workflowId} in ${elapsed}ms`)

return NextResponse.json({ data: workflowData }, { status: 200 })
} catch (error: any) {
const elapsed = Date.now() - startTime
logger.error(`[${requestId}] Error fetching workflow ${workflowId} after ${elapsed}ms`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}

/**
* DELETE /api/workflows/[id]
* Delete a workflow by ID
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const requestId = crypto.randomUUID().slice(0, 8)
const startTime = Date.now()
const { id: workflowId } = await params

try {
// Get the session
const session = await getSession()
if (!session?.user?.id) {
logger.warn(`[${requestId}] Unauthorized deletion attempt for workflow ${workflowId}`)
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const userId = session.user.id

// Fetch the workflow to check ownership/access
const workflowData = await db
.select()
.from(workflow)
.where(eq(workflow.id, workflowId))
.then((rows) => rows[0])

if (!workflowData) {
logger.warn(`[${requestId}] Workflow ${workflowId} not found for deletion`)
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
}

// Check if user has permission to delete this workflow
let canDelete = false

// Case 1: User owns the workflow
if (workflowData.userId === userId) {
canDelete = true
}

// Case 2: Workflow belongs to a workspace and user has admin/owner role
if (!canDelete && workflowData.workspaceId) {
const membership = await db
.select({ role: workspaceMember.role })
.from(workspaceMember)
.where(
and(
eq(workspaceMember.workspaceId, workflowData.workspaceId),
eq(workspaceMember.userId, userId)
)
)
.then((rows) => rows[0])

if (membership && (membership.role === 'owner' || membership.role === 'admin')) {
canDelete = true
}
} else if (canAccess) {
// User owns the workflow, allow access
const elapsed = Date.now() - startTime
logger.info(`[${requestId}] Workflow ${workflowId} fetched in ${elapsed}ms`)
return NextResponse.json({ data: workflowData }, { status: 200 })
}

logger.warn(
`[${requestId}] User ${session.user.id} attempted to access workflow ${workflowId} without permission`
)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
if (!canDelete) {
logger.warn(
`[${requestId}] User ${userId} denied permission to delete workflow ${workflowId}`
)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}

// Delete the workflow
await db.delete(workflow).where(eq(workflow.id, workflowId))

const elapsed = Date.now() - startTime
logger.info(`[${requestId}] Successfully deleted workflow ${workflowId} in ${elapsed}ms`)

return NextResponse.json({ success: true }, { status: 200 })
} catch (error: any) {
const elapsed = Date.now() - startTime
logger.error(`[${requestId}] Error fetching workflow after ${elapsed}ms:`, error)
return NextResponse.json({ error: 'Failed to fetch workflow' }, { status: 500 })
logger.error(`[${requestId}] Error deleting workflow ${workflowId} after ${elapsed}ms`, error)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}
94 changes: 45 additions & 49 deletions apps/sim/app/api/workflows/sync/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,17 +377,17 @@ export async function POST(req: NextRequest) {
processedIds.add(id)
const dbWorkflow = dbWorkflowMap.get(id)

// Handle legacy published workflows migration
// Handle legacy published workflows migration (only if state is provided)
// If client workflow has isPublished but no marketplaceData, create marketplaceData with owner status
if (clientWorkflow.state.isPublished && !clientWorkflow.marketplaceData) {
if (clientWorkflow.state?.isPublished && !clientWorkflow.marketplaceData) {
clientWorkflow.marketplaceData = { id: clientWorkflow.id, status: 'owner' }
}

// Ensure the workflow has the correct workspaceId
const effectiveWorkspaceId = clientWorkflow.workspaceId || workspaceId

if (!dbWorkflow) {
// New workflow - create
// New workflow - create (state is required by schema)
operations.push(
db.insert(workflow).values({
id: clientWorkflow.id,
Expand Down Expand Up @@ -417,61 +417,57 @@ export async function POST(req: NextRequest) {
continue // Skip this workflow update and move to the next one
}

// Existing workflow - update if needed
const needsUpdate =
JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state) ||
dbWorkflow.name !== clientWorkflow.name ||
dbWorkflow.description !== clientWorkflow.description ||
dbWorkflow.color !== clientWorkflow.color ||
dbWorkflow.workspaceId !== effectiveWorkspaceId ||
dbWorkflow.folderId !== (clientWorkflow.folderId || null) ||
// For existing workflows, determine what needs updating
let needsUpdate = false
const updateData: any = {}

Comment thread
icecrasher321 marked this conversation as resolved.
// Check metadata changes
if (dbWorkflow.name !== clientWorkflow.name) {
updateData.name = clientWorkflow.name
needsUpdate = true
}
if (dbWorkflow.description !== clientWorkflow.description) {
updateData.description = clientWorkflow.description
needsUpdate = true
}
if (dbWorkflow.color !== clientWorkflow.color) {
updateData.color = clientWorkflow.color
needsUpdate = true
}
if (dbWorkflow.workspaceId !== effectiveWorkspaceId) {
updateData.workspaceId = effectiveWorkspaceId
needsUpdate = true
}
if (dbWorkflow.folderId !== (clientWorkflow.folderId || null)) {
updateData.folderId = clientWorkflow.folderId || null
needsUpdate = true
}
if (
JSON.stringify(dbWorkflow.marketplaceData) !==
JSON.stringify(clientWorkflow.marketplaceData)
JSON.stringify(clientWorkflow.marketplaceData)
) {
updateData.marketplaceData = clientWorkflow.marketplaceData || null
needsUpdate = true
}

if (needsUpdate) {
operations.push(
db
.update(workflow)
.set({
name: clientWorkflow.name,
description: clientWorkflow.description,
color: clientWorkflow.color,
workspaceId: effectiveWorkspaceId,
folderId: clientWorkflow.folderId || null,
state: clientWorkflow.state,
marketplaceData: clientWorkflow.marketplaceData || null,
lastSynced: now,
updatedAt: now,
})
.where(eq(workflow.id, id))
)
// Always update state since we only sync the active workflow with valid state
if (JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state)) {
updateData.state = clientWorkflow.state
needsUpdate = true
}
}
}

// Handle deletions - workflows in DB but not in client
// Only delete workflows for the current workspace and only those the user can modify
for (const dbWorkflow of dbWorkflows) {
if (
!processedIds.has(dbWorkflow.id) &&
(!workspaceId || dbWorkflow.workspaceId === workspaceId)
) {
// Check if the user has permission to delete this workflow
// Users can delete their own workflows, or any workflow if they're a workspace owner/admin
const canDelete =
dbWorkflow.userId === session.user.id ||
(workspaceId && (userRole === 'owner' || userRole === 'admin' || userRole === 'member'))
if (needsUpdate) {
updateData.lastSynced = now
updateData.updatedAt = now

if (canDelete) {
operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id)))
} else {
logger.warn(
`[${requestId}] User ${session.user.id} attempted to delete workflow ${dbWorkflow.id} without permission`
)
operations.push(db.update(workflow).set(updateData).where(eq(workflow.id, id)))
}
}
}

// NOTE: We don't delete workflows here since we're only syncing the active workflow
// Other workflows remain untouched in the database

// Execute all operations in parallel
await Promise.all(operations)

Expand Down
14 changes: 9 additions & 5 deletions apps/sim/app/w/[id]/components/control-bar/control-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,21 +409,25 @@ export function ControlBar() {
/**
* Workflow deletion handler
*/
const handleDeleteWorkflow = () => {
const handleDeleteWorkflow = async () => {
if (!activeWorkflowId) return

// Get remaining workflow IDs
// Navigate to another workflow first
const remainingIds = Object.keys(workflows).filter((id) => id !== activeWorkflowId)

// Navigate before removing the workflow to avoid any state inconsistencies
if (remainingIds.length > 0) {
router.push(`/w/${remainingIds[0]}`)
} else {
router.push('/')
}

// Remove the workflow from the registry
removeWorkflow(activeWorkflowId)
// Remove the workflow from the registry (now async)
try {
await removeWorkflow(activeWorkflowId)
} catch (error) {
// Handle error gracefully - could show user notification instead
logger.error('Failed to delete workflow:', error)
}
}

// /**
Expand Down
Loading