Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 3 additions & 8 deletions apps/sim/app/api/organizations/[id]/workspaces/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { member, permissions, user, workspace, workspaceMember } from '@/db/schema'
import { member, permissions, user, workspace } from '@/db/schema'

const logger = createLogger('OrganizationWorkspacesAPI')

Expand Down Expand Up @@ -116,10 +116,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
id: workspace.id,
name: workspace.name,
ownerId: workspace.ownerId,
createdAt: workspace.createdAt,
isOwner: eq(workspace.ownerId, memberId),
permissionType: permissions.permissionType,
joinedAt: workspaceMember.joinedAt,
createdAt: permissions.createdAt,
Comment on lines 120 to +121
Copy link
Contributor

Choose a reason for hiding this comment

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

style: Consider adding an explicit alias for permissions.createdAt since it's used as both joinedAt and createdAt in the response

})
.from(workspace)
.leftJoin(
Expand All @@ -130,10 +129,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
eq(permissions.userId, memberId)
)
)
.leftJoin(
workspaceMember,
and(eq(workspaceMember.workspaceId, workspace.id), eq(workspaceMember.userId, memberId))
)
.where(
or(
// Member owns the workspace
Expand All @@ -148,7 +143,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
name: workspace.name,
isOwner: workspace.isOwner,
permission: workspace.permissionType,
joinedAt: workspace.joinedAt,
joinedAt: workspace.createdAt,
createdAt: workspace.createdAt,
}))

Expand Down
48 changes: 3 additions & 45 deletions apps/sim/app/api/organizations/invitations/accept/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { createLogger } from '@/lib/logs/console-logger'
import { db } from '@/db'
import { invitation, member, permissions, workspaceInvitation, workspaceMember } from '@/db/schema'
import { invitation, member, permissions, workspaceInvitation } from '@/db/schema'

const logger = createLogger('OrganizationInvitationAcceptance')

Expand Down Expand Up @@ -135,18 +135,6 @@ export async function GET(req: NextRequest) {
wsInvitation.expiresAt &&
new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
) {
// Check if user isn't already a member of the workspace
const existingWorkspaceMember = await tx
.select()
.from(workspaceMember)
.where(
and(
eq(workspaceMember.workspaceId, wsInvitation.workspaceId),
eq(workspaceMember.userId, session.user.id)
)
)
.limit(1)

// Check if user doesn't already have permissions on the workspace
const existingPermission = await tx
.select()
Expand All @@ -160,17 +148,7 @@ export async function GET(req: NextRequest) {
)
.limit(1)

if (existingWorkspaceMember.length === 0 && existingPermission.length === 0) {
// Add user as workspace member
await tx.insert(workspaceMember).values({
id: randomUUID(),
workspaceId: wsInvitation.workspaceId,
userId: session.user.id,
role: wsInvitation.role,
joinedAt: new Date(),
updatedAt: new Date(),
})

if (existingPermission.length === 0) {
// Add workspace permissions
await tx.insert(permissions).values({
id: randomUUID(),
Expand Down Expand Up @@ -311,17 +289,6 @@ export async function POST(req: NextRequest) {
wsInvitation.expiresAt &&
new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
) {
Comment on lines 289 to 291
Copy link
Contributor

Choose a reason for hiding this comment

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

style: same date comparison issue here - use native Date objects instead of string conversion

Suggested change
wsInvitation.expiresAt &&
new Date().toISOString() <= wsInvitation.expiresAt.toISOString()
) {
wsInvitation.expiresAt &&
new Date() <= wsInvitation.expiresAt
) {

const existingWorkspaceMember = await tx
.select()
.from(workspaceMember)
.where(
and(
eq(workspaceMember.workspaceId, wsInvitation.workspaceId),
eq(workspaceMember.userId, session.user.id)
)
)
.limit(1)

const existingPermission = await tx
.select()
.from(permissions)
Expand All @@ -334,16 +301,7 @@ export async function POST(req: NextRequest) {
)
.limit(1)

if (existingWorkspaceMember.length === 0 && existingPermission.length === 0) {
await tx.insert(workspaceMember).values({
id: randomUUID(),
workspaceId: wsInvitation.workspaceId,
userId: session.user.id,
role: wsInvitation.role,
joinedAt: new Date(),
updatedAt: new Date(),
})

if (existingPermission.length === 0) {
await tx.insert(permissions).values({
id: randomUUID(),
userId: session.user.id,
Expand Down
13 changes: 13 additions & 0 deletions apps/sim/app/api/schedules/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
generateCronExpression,
getScheduleTimeValues,
getSubBlockValue,
validateCronExpression,
} from '@/lib/schedules/utils'
import { db } from '@/db'
import { workflowSchedule } from '@/db/schema'
Expand Down Expand Up @@ -192,6 +193,18 @@ export async function POST(req: NextRequest) {

cronExpression = generateCronExpression(defaultScheduleType, scheduleValues)

// Additional validation for custom cron expressions
if (defaultScheduleType === 'custom' && cronExpression) {
const validation = validateCronExpression(cronExpression)
if (!validation.isValid) {
logger.error(`[${requestId}] Invalid cron expression: ${validation.error}`)
return NextResponse.json(
{ error: `Invalid cron expression: ${validation.error}` },
{ status: 400 }
)
}
}
Comment on lines +196 to +206
Copy link
Contributor

Choose a reason for hiding this comment

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

logic: The validation check could fail silently if cronExpression is null. Consider adding null check or handling this case explicitly.


nextRunAt = calculateNextRunTime(defaultScheduleType, scheduleValues)

logger.debug(
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/templates/[id]/use/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
name: templates.name,
description: templates.description,
state: templates.state,
color: templates.color,
})
.from(templates)
.where(eq(templates.id, id))
Expand Down Expand Up @@ -80,6 +81,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
name: `${templateData.name} (copy)`,
description: templateData.description,
state: templateData.state,
color: templateData.color,
userId: session.user.id,
createdAt: now,
updatedAt: now,
Expand Down
62 changes: 47 additions & 15 deletions apps/sim/app/api/workflows/[id]/duplicate/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import crypto from 'crypto'
import { and, eq } from 'drizzle-orm'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
import type { LoopConfig, ParallelConfig, WorkflowState } from '@/stores/workflows/workflow/types'
Expand All @@ -24,15 +25,13 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
const requestId = crypto.randomUUID().slice(0, 8)
const startTime = Date.now()

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

try {
const body = await req.json()
const { name, description, color, workspaceId, folderId } = DuplicateRequestSchema.parse(body)

Expand All @@ -46,19 +45,43 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:

// Duplicate workflow and all related data in a transaction
const result = await db.transaction(async (tx) => {
// First verify the source workflow exists and user has access
// First verify the source workflow exists
const sourceWorkflow = await tx
.select()
.from(workflow)
.where(and(eq(workflow.id, sourceWorkflowId), eq(workflow.userId, session.user.id)))
.where(eq(workflow.id, sourceWorkflowId))
.limit(1)

if (sourceWorkflow.length === 0) {
throw new Error('Source workflow not found or access denied')
throw new Error('Source workflow not found')
}

const source = sourceWorkflow[0]

// Check if user has permission to access the source workflow
let canAccessSource = false

// Case 1: User owns the workflow
if (source.userId === session.user.id) {
canAccessSource = true
}

// Case 2: User has admin or write permission in the source workspace
if (!canAccessSource && source.workspaceId) {
const userPermission = await getUserEntityPermissions(
session.user.id,
'workspace',
source.workspaceId
)
if (userPermission === 'admin' || userPermission === 'write') {
canAccessSource = true
}
}

if (!canAccessSource) {
throw new Error('Source workflow not found or access denied')
}

// Create the new workflow first (required for foreign key constraints)
await tx.insert(workflow).values({
id: newWorkflowId,
Expand Down Expand Up @@ -346,9 +369,18 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:

return NextResponse.json(result, { status: 201 })
} catch (error) {
if (error instanceof Error && error.message === 'Source workflow not found or access denied') {
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found or access denied`)
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
if (error instanceof Error) {
if (error.message === 'Source workflow not found') {
logger.warn(`[${requestId}] Source workflow ${sourceWorkflowId} not found`)
return NextResponse.json({ error: 'Source workflow not found' }, { status: 404 })
}

if (error.message === 'Source workflow not found or access denied') {
logger.warn(
`[${requestId}] User ${session.user.id} denied access to source workflow ${sourceWorkflowId}`
)
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
}
}

if (error instanceof z.ZodError) {
Expand Down
14 changes: 8 additions & 6 deletions apps/sim/app/api/workspaces/[id]/permissions/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import crypto from 'crypto'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getUsersWithPermissions, hasWorkspaceAdminAccess } from '@/lib/permissions/utils'
import { db } from '@/db'
import { permissions, type permissionTypeEnum, workspaceMember } from '@/db/schema'
import { permissions, type permissionTypeEnum } from '@/db/schema'

type PermissionType = (typeof permissionTypeEnum.enumValues)[number]

Expand Down Expand Up @@ -33,18 +34,19 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
}

// Verify the current user has access to this workspace
const userMembership = await db
const userPermission = await db
.select()
.from(workspaceMember)
.from(permissions)
.where(
and(
eq(workspaceMember.workspaceId, workspaceId),
eq(workspaceMember.userId, session.user.id)
eq(permissions.entityId, workspaceId),
eq(permissions.entityType, 'workspace'),
eq(permissions.userId, session.user.id)
)
)
.limit(1)

if (userMembership.length === 0) {
if (userPermission.length === 0) {
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
}

Expand Down
5 changes: 1 addition & 4 deletions apps/sim/app/api/workspaces/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console-logger'
import { workflow, workspaceMember } from '@/db/schema'
import { workflow } from '@/db/schema'

const logger = createLogger('WorkspaceByIdAPI')

Expand Down Expand Up @@ -126,9 +126,6 @@ export async function DELETE(
// workflow_schedule, webhook, marketplace, chat, and memory records
await tx.delete(workflow).where(eq(workflow.workspaceId, workspaceId))

// Delete workspace members
await tx.delete(workspaceMember).where(eq(workspaceMember.workspaceId, workspaceId))

// Delete all permissions associated with this workspace
await tx
.delete(permissions)
Expand Down
Loading