Skip to content

Commit f52b669

Browse files
committed
feat(invite-workspace): members registries and variables loaded from workspace
1 parent 3c64622 commit f52b669

File tree

3 files changed

+156
-17
lines changed

3 files changed

+156
-17
lines changed

apps/sim/app/api/workflows/[id]/variables/route.ts

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { NextRequest, NextResponse } from 'next/server'
2-
import { eq } from 'drizzle-orm'
2+
import { and, eq } from 'drizzle-orm'
33
import { z } from 'zod'
44
import { getSession } from '@/lib/auth'
55
import { createLogger } from '@/lib/logs/console-logger'
66
import { Variable } from '@/stores/panel/variables/types'
77
import { db } from '@/db'
8-
import { workflow } from '@/db/schema'
8+
import { workflow, workspaceMember } from '@/db/schema'
99

1010
const logger = createLogger('WorkflowVariablesAPI')
1111

@@ -33,7 +33,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
3333
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
3434
}
3535

36-
// Check if the workflow belongs to the user
36+
// Get the workflow record
3737
const workflowRecord = await db
3838
.select()
3939
.from(workflow)
@@ -45,9 +45,31 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
4545
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
4646
}
4747

48-
if (workflowRecord[0].userId !== session.user.id) {
48+
const workflowData = workflowRecord[0]
49+
const workspaceId = workflowData.workspaceId
50+
51+
// Check authorization - either the user owns the workflow or is a member of the workspace
52+
let isAuthorized = workflowData.userId === session.user.id
53+
54+
// If not authorized by ownership and the workflow belongs to a workspace, check workspace membership
55+
if (!isAuthorized && workspaceId) {
56+
const membership = await db
57+
.select()
58+
.from(workspaceMember)
59+
.where(
60+
and(
61+
eq(workspaceMember.workspaceId, workspaceId),
62+
eq(workspaceMember.userId, session.user.id)
63+
)
64+
)
65+
.limit(1)
66+
67+
isAuthorized = membership.length > 0
68+
}
69+
70+
if (!isAuthorized) {
4971
logger.warn(
50-
`[${requestId}] User ${session.user.id} attempted to update variables for workflow ${workflowId} owned by ${workflowRecord[0].userId}`
72+
`[${requestId}] User ${session.user.id} attempted to update variables for workflow ${workflowId} without permission`
5173
)
5274
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
5375
}
@@ -115,7 +137,7 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
115137
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
116138
}
117139

118-
// Check if the workflow belongs to the user
140+
// Get the workflow record
119141
const workflowRecord = await db
120142
.select()
121143
.from(workflow)
@@ -127,15 +149,37 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
127149
return NextResponse.json({ error: 'Workflow not found' }, { status: 404 })
128150
}
129151

130-
if (workflowRecord[0].userId !== session.user.id) {
152+
const workflowData = workflowRecord[0]
153+
const workspaceId = workflowData.workspaceId
154+
155+
// Check authorization - either the user owns the workflow or is a member of the workspace
156+
let isAuthorized = workflowData.userId === session.user.id
157+
158+
// If not authorized by ownership and the workflow belongs to a workspace, check workspace membership
159+
if (!isAuthorized && workspaceId) {
160+
const membership = await db
161+
.select()
162+
.from(workspaceMember)
163+
.where(
164+
and(
165+
eq(workspaceMember.workspaceId, workspaceId),
166+
eq(workspaceMember.userId, session.user.id)
167+
)
168+
)
169+
.limit(1)
170+
171+
isAuthorized = membership.length > 0
172+
}
173+
174+
if (!isAuthorized) {
131175
logger.warn(
132-
`[${requestId}] User ${session.user.id} attempted to access variables for workflow ${workflowId} owned by ${workflowRecord[0].userId}`
176+
`[${requestId}] User ${session.user.id} attempted to access variables for workflow ${workflowId} without permission`
133177
)
134178
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
135179
}
136180

137181
// Return variables if they exist
138-
const variables = (workflowRecord[0].variables as Record<string, Variable>) || {}
182+
const variables = (workflowData.variables as Record<string, Variable>) || {}
139183

140184
// Add cache headers to prevent frequent reloading
141185
const headers = new Headers({

apps/sim/app/api/workflows/sync/route.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { z } from 'zod'
44
import { getSession } from '@/lib/auth'
55
import { createLogger } from '@/lib/logs/console-logger'
66
import { db } from '@/db'
7-
import { workflow, workspace } from '@/db/schema'
7+
import { workflow, workspace, workspaceMember } from '@/db/schema'
88

99
const logger = createLogger('WorkflowAPI')
1010

@@ -80,6 +80,26 @@ export async function GET(request: Request) {
8080
)
8181
}
8282

83+
// Verify the user is a member of the workspace
84+
const isMember = await db
85+
.select({ id: workspaceMember.id })
86+
.from(workspaceMember)
87+
.where(and(
88+
eq(workspaceMember.workspaceId, workspaceId),
89+
eq(workspaceMember.userId, userId)
90+
))
91+
.then((rows) => rows.length > 0)
92+
93+
if (!isMember) {
94+
logger.warn(
95+
`[${requestId}] User ${userId} attempted to access workspace ${workspaceId} without membership`
96+
)
97+
return NextResponse.json(
98+
{ error: 'Access denied to this workspace', code: 'WORKSPACE_ACCESS_DENIED' },
99+
{ status: 403 }
100+
)
101+
}
102+
83103
// Migrate any orphaned workflows to this workspace
84104
await migrateOrphanedWorkflows(userId, workspaceId)
85105
}
@@ -88,11 +108,12 @@ export async function GET(request: Request) {
88108
let workflows
89109

90110
if (workspaceId) {
91-
// Filter by user ID and workspace ID
111+
// Filter by workspace ID only, not user ID
112+
// This allows sharing workflows across workspace members
92113
workflows = await db
93114
.select()
94115
.from(workflow)
95-
.where(and(eq(workflow.userId, userId), eq(workflow.workspaceId, workspaceId)))
116+
.where(eq(workflow.workspaceId, workspaceId))
96117
} else {
97118
// Filter by user ID only, including workflows without workspace IDs
98119
workflows = await db.select().from(workflow).where(eq(workflow.userId, userId))
@@ -186,7 +207,9 @@ export async function POST(req: NextRequest) {
186207
}
187208
}
188209

189-
// Validate that the workspace exists if one is specified
210+
// Validate workspace membership and permissions
211+
let userRole: string | null = null;
212+
190213
if (workspaceId) {
191214
const workspaceExists = await db
192215
.select({ id: workspace.id })
@@ -206,17 +229,40 @@ export async function POST(req: NextRequest) {
206229
{ status: 404 }
207230
)
208231
}
232+
233+
// Verify the user is a member of the workspace
234+
const membership = await db
235+
.select({ role: workspaceMember.role })
236+
.from(workspaceMember)
237+
.where(and(
238+
eq(workspaceMember.workspaceId, workspaceId),
239+
eq(workspaceMember.userId, session.user.id)
240+
))
241+
.then((rows) => rows[0])
242+
243+
if (!membership) {
244+
logger.warn(
245+
`[${requestId}] User ${session.user.id} attempted to sync to workspace ${workspaceId} without membership`
246+
)
247+
return NextResponse.json(
248+
{ error: 'Access denied to this workspace', code: 'WORKSPACE_ACCESS_DENIED' },
249+
{ status: 403 }
250+
)
251+
}
252+
253+
// Store user's role for permission checks later
254+
userRole = membership.role;
209255
}
210256

211-
// Get all workflows for the user from the database
257+
// Get all workflows for the workspace from the database
212258
// If workspaceId is provided, only get workflows for that workspace
213259
let dbWorkflows
214260

215261
if (workspaceId) {
216262
dbWorkflows = await db
217263
.select()
218264
.from(workflow)
219-
.where(and(eq(workflow.userId, session.user.id), eq(workflow.workspaceId, workspaceId)))
265+
.where(eq(workflow.workspaceId, workspaceId))
220266
} else {
221267
dbWorkflows = await db.select().from(workflow).where(eq(workflow.userId, session.user.id))
222268
}
@@ -260,6 +306,17 @@ export async function POST(req: NextRequest) {
260306
})
261307
)
262308
} else {
309+
// Check if user has permission to update this workflow
310+
const canUpdate = dbWorkflow.userId === session.user.id ||
311+
(workspaceId && (userRole === 'owner' || userRole === 'admin' || userRole === 'member'));
312+
313+
if (!canUpdate) {
314+
logger.warn(
315+
`[${requestId}] User ${session.user.id} attempted to update workflow ${id} without permission`
316+
)
317+
continue; // Skip this workflow update and move to the next one
318+
}
319+
263320
// Existing workflow - update if needed
264321
const needsUpdate =
265322
JSON.stringify(dbWorkflow.state) !== JSON.stringify(clientWorkflow.state) ||
@@ -291,13 +348,24 @@ export async function POST(req: NextRequest) {
291348
}
292349

293350
// Handle deletions - workflows in DB but not in client
294-
// Only delete workflows for the current workspace!
351+
// Only delete workflows for the current workspace and only those the user can modify
295352
for (const dbWorkflow of dbWorkflows) {
296353
if (
297354
!processedIds.has(dbWorkflow.id) &&
298355
(!workspaceId || dbWorkflow.workspaceId === workspaceId)
299356
) {
300-
operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id)))
357+
// Check if the user has permission to delete this workflow
358+
// Users can delete their own workflows, or any workflow if they're a workspace owner/admin
359+
const canDelete = dbWorkflow.userId === session.user.id ||
360+
(workspaceId && (userRole === 'owner' || userRole === 'admin' || userRole === 'member'));
361+
362+
if (canDelete) {
363+
operations.push(db.delete(workflow).where(eq(workflow.id, dbWorkflow.id)))
364+
} else {
365+
logger.warn(
366+
`[${requestId}] User ${session.user.id} attempted to delete workflow ${dbWorkflow.id} without permission`
367+
)
368+
}
301369
}
302370
}
303371

apps/sim/stores/panel/variables/store.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,33 @@ export const useVariablesStore = create<VariablesStore>()(
494494
return
495495
}
496496

497+
// Handle unauthorized (401) or forbidden (403) gracefully
498+
if (response.status === 401 || response.status === 403) {
499+
logger.warn(`No permission to access variables for workflow ${workflowId}`)
500+
set((state) => {
501+
// Keep variables from other workflows
502+
const otherVariables = Object.values(state.variables).reduce(
503+
(acc, variable) => {
504+
if (variable.workflowId !== workflowId) {
505+
acc[variable.id] = variable
506+
}
507+
return acc
508+
},
509+
{} as Record<string, Variable>
510+
)
511+
512+
// Mark this workflow as loaded but with access issues
513+
loadedWorkflows.add(workflowId)
514+
515+
return {
516+
variables: otherVariables,
517+
isLoading: false,
518+
error: 'You do not have permission to access these variables',
519+
}
520+
})
521+
return
522+
}
523+
497524
if (!response.ok) {
498525
throw new Error(`Failed to load workflow variables: ${response.statusText}`)
499526
}

0 commit comments

Comments
 (0)