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
10 changes: 2 additions & 8 deletions apps/sim/app/api/files/uploads/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
type InternalFileUploadSession,
internalFileUploadSessionSchema,
} from '@/lib/api/contracts/upload-sessions'
import { orchestrationFailureResponse } from '@/lib/api/server/orchestration-response'
import { getSession } from '@/lib/auth'
import { asOrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types'
import type { UploadSessionRecord } from '@/lib/uploads/upload-session/service'
import type { UploadActor, UploadPurposeResult } from '@/app/api/files/uploads/finalizers'

Expand All @@ -32,13 +32,7 @@ export async function requireUploadUser(): Promise<AuthenticatedUploadActor | Ne
}

export function uploadSessionErrorResponse(error: unknown): NextResponse | null {
const classified = asOrchestrationError(error)
return classified
? NextResponse.json(
{ error: classified.message },
{ status: statusForOrchestrationError(classified.code) }
)
: null
return orchestrationFailureResponse(error)
}

export function toInternalUploadSession(
Expand Down
30 changes: 14 additions & 16 deletions apps/sim/app/api/superuser/import-workflow/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ import { type NextRequest, NextResponse } from 'next/server'
import { importWorkflowAsSuperuserContract } from '@/lib/api/contracts/workflows'
import { parseRequest } from '@/lib/api/server'
import { getSession } from '@/lib/auth'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { asOrchestrationError } from '@/lib/core/orchestration/types'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { loadCopilotChatMessages } from '@/lib/mothership/chat/lifecycle'
import { appendCopilotChatMessages } from '@/lib/mothership/chat/messages-store'
import { verifyEffectiveSuperUser } from '@/lib/permissions/super-user'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { insertNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import {
loadWorkflowFromNormalizedTables,
saveWorkflowToNormalizedTables,
Expand Down Expand Up @@ -137,19 +137,17 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
null
)

await db.transaction(async (tx) => {
await tx.insert(workflow).values(
await buildNewWorkflowRow(tx, {
id: newWorkflowId,
userId: session.user.id,
workspaceId: targetWorkspaceId,
folderId: null,
name: dedupedName,
description: sourceWorkflow.description,
variables: sourceWorkflow.variables || {},
})
)
})
await db.transaction((tx) =>
insertNewWorkflowRow(tx, {
id: newWorkflowId,
userId: session.user.id,
workspaceId: targetWorkspaceId,
folderId: null,
name: dedupedName,
description: sourceWorkflow.description,
variables: sourceWorkflow.variables || {},
})
)

// Save using existing persistence logic
const saveResult = await saveWorkflowToNormalizedTables(newWorkflowId, importedData, {
Expand Down Expand Up @@ -228,7 +226,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
copilotChatsImported,
})
} catch (error) {
if (error instanceof OrchestrationError && error.code === 'not_found') {
if (asOrchestrationError(error)?.code === 'not_found') {
return NextResponse.json({ error: 'Target workspace not found' }, { status: 404 })
}
logger.error('Error importing workflow', error)
Expand Down
10 changes: 2 additions & 8 deletions apps/sim/app/api/table/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { createLogger } from '@sim/logger'
import { permissionSatisfies } from '@sim/platform-authz/workspace'
import { toError } from '@sim/utils/errors'
import { NextResponse } from 'next/server'
import { orchestrationFailureResponse } from '@/lib/api/server/orchestration-response'
import {
asOrchestrationError,
messageForOrchestrationError,
type OrchestrationErrorCode,
statusForOrchestrationError,
Expand Down Expand Up @@ -137,13 +137,7 @@ export function orchestrationErrorResponse(error: unknown): NextResponse | null
const lockResponse = tableLockErrorResponse(error)
if (lockResponse) return lockResponse

const classified = asOrchestrationError(error)
if (!classified) return null

return NextResponse.json(
{ error: classified.message },
{ status: statusForOrchestrationError(classified.code) }
)
return orchestrationFailureResponse(error)
}

/**
Expand Down
28 changes: 13 additions & 15 deletions apps/sim/app/api/v1/admin/workflows/import/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ import { and, eq, isNull } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { adminV1ImportWorkflowContract } from '@/lib/api/contracts/v1/admin'
import { parseRequest } from '@/lib/api/server'
import { OrchestrationError } from '@/lib/core/orchestration/types'
import { asOrchestrationError } from '@/lib/core/orchestration/types'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { parseWorkflowJson } from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { insertNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
Expand Down Expand Up @@ -116,18 +116,16 @@ export const POST = withRouteHandler(
const workflowId = generateId()
const dedupedName = await deduplicateWorkflowName(workflowName, workspaceId, folderId || null)

await db.transaction(async (tx) => {
await tx.insert(workflow).values(
await buildNewWorkflowRow(tx, {
id: workflowId,
userId: workspaceData.ownerId,
workspaceId,
folderId: folderId || null,
name: dedupedName,
description: workflowDescription,
})
)
})
await db.transaction((tx) =>
insertNewWorkflowRow(tx, {
id: workflowId,
userId: workspaceData.ownerId,
workspaceId,
folderId: folderId || null,
name: dedupedName,
description: workflowDescription,
})
)

/**
* Same normalization the editor and the v1 import API run, via the one
Expand Down Expand Up @@ -183,7 +181,7 @@ export const POST = withRouteHandler(

return NextResponse.json(response)
} catch (error) {
if (error instanceof OrchestrationError && error.code === 'not_found') {
if (asOrchestrationError(error)?.code === 'not_found') {
return notFoundResponse('Workspace')
}
if (error instanceof FolderNotFoundError) {
Expand Down
24 changes: 11 additions & 13 deletions apps/sim/app/api/v1/admin/workspaces/[id]/import/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
extractWorkflowsFromZip,
parseWorkflowJson,
} from '@/lib/workflows/operations/import-export'
import { buildNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { insertNewWorkflowRow } from '@/lib/workflows/persistence/new-workflow-row'
import { prepareWorkflowStateForPersistence } from '@/lib/workflows/persistence/prepare-state'
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/persistence/utils'
import { deduplicateWorkflowName } from '@/lib/workflows/utils'
Expand Down Expand Up @@ -350,18 +350,16 @@ async function importSingleWorkflow(
const workflowId = generateId()
const dedupedName = await deduplicateWorkflowName(workflowName, workspaceId, targetFolderId)

await db.transaction(async (tx) => {
await tx.insert(workflow).values(
await buildNewWorkflowRow(tx, {
id: workflowId,
userId: ownerId,
workspaceId,
folderId: targetFolderId,
name: dedupedName,
description: workflowData.metadata?.description || 'Imported via Admin API',
})
)
})
await db.transaction((tx) =>
insertNewWorkflowRow(tx, {
id: workflowId,
userId: ownerId,
workspaceId,
folderId: targetFolderId,
name: dedupedName,
description: workflowData.metadata?.description || 'Imported via Admin API',
})
)

/**
* Same normalization the editor, the v1 import API and the single-workflow
Expand Down
24 changes: 17 additions & 7 deletions apps/sim/app/api/workspaces/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { deleteWorkspaceBodySchema, updateWorkspaceContract } from '@/lib/api/contracts'
import { parseRequest, validationErrorResponse } from '@/lib/api/server'
import { orchestrationFailureResponse } from '@/lib/api/server/orchestration-response'
import { getSession } from '@/lib/auth'
import { changeWorkspaceStoragePayerInTx } from '@/lib/billing/storage/payer-transfer'
import { OrchestrationError, statusForOrchestrationError } from '@/lib/core/orchestration/types'
import { captureServerEvent } from '@/lib/posthog/server'
import { archiveWorkspace } from '@/lib/workspaces/lifecycle'

Expand Down Expand Up @@ -307,6 +307,20 @@ export const DELETE = withRouteHandler(
},
request,
})
if (archiveResult.archivedProject) {
recordAudit({
workspaceId,
actorId: session.user.id,
actorName: session.user.name,
actorEmail: session.user.email,
action: AuditAction.PROJECT_ARCHIVED,
resourceType: AuditResourceType.PROJECT,
resourceId: archiveResult.archivedProject.id,
resourceName: archiveResult.archivedProject.name,
description: `Archived Project "${archiveResult.archivedProject.name}" with its last active environment`,
request,
})
}

captureServerEvent(
session.user.id,
Expand All @@ -317,12 +331,8 @@ export const DELETE = withRouteHandler(

return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof OrchestrationError) {
return NextResponse.json(
{ error: error.message },
{ status: statusForOrchestrationError(error.code) }
)
}
const failure = orchestrationFailureResponse(error, 'Failed to delete workspace')
if (failure) return failure
logger.error(`Error deleting workspace ${workspaceId}:`, error)
return NextResponse.json({ error: 'Failed to delete workspace' }, { status: 500 })
}
Expand Down
20 changes: 10 additions & 10 deletions apps/sim/ee/access-control/components/group-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1784,16 +1784,6 @@ export function GroupDetail({

{configTab === 'platform' && (
<div className='flex flex-col gap-7'>
<ProjectIssueRestrictions
organizationId={organizationId}
value={editingConfig.deniedPartialAccessProjectIssues}
onChange={(value) =>
setEditingConfig((previous) => ({
...previous,
deniedPartialAccessProjectIssues: value,
}))
}
/>
<div className='flex items-center gap-2'>
<ChipInput
icon={Search}
Expand Down Expand Up @@ -1871,6 +1861,16 @@ export function GroupDetail({
</div>
</SettingsSection>
))}
<ProjectIssueRestrictions
organizationId={organizationId}
value={editingConfig.deniedPartialAccessProjectIssues}
onChange={(value) =>
setEditingConfig((previous) => ({
...previous,
deniedPartialAccessProjectIssues: value,
}))
}
/>
</div>
)}
</SettingsPanel>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use client'

import { Checkbox, Chip } from '@sim/emcn'
import { Checkbox, Chip, Info, OverflowText } from '@sim/emcn'
import { isApiClientError } from '@/lib/api/client/errors'
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
import { useProjects } from '@/hooks/queries/projects'

interface ProjectIssueRestrictionsProps {
Expand All @@ -10,7 +11,10 @@ interface ProjectIssueRestrictionsProps {
onChange: (value: string[]) => void
}

/** Project choices use the authorized inventory; policy remains enforced at the application boundary. */
/**
* Project choices use the authorized inventory; policy remains enforced at the
* application boundary.
*/
export function ProjectIssueRestrictions({
organizationId,
value,
Expand All @@ -20,44 +24,58 @@ export function ProjectIssueRestrictions({
if (projects.isPending || (isApiClientError(projects.error) && projects.error.status === 503)) {
return null
}
const choices = projects.data?.pages.flatMap((page) => page.projects) ?? []
if (!projects.error && choices.length === 0) return null
const selected = new Set(value)
return (
<div className='flex flex-col gap-2'>
<p className='text-small'>Restrict Issues for partial-access teammates</p>
<p className='text-[var(--text-muted)] text-small'>
For selected Projects, teammates governed by this group need access to every active
environment to use Issues.
</p>
<SettingsSection
label='Project Issues'
headerAccessory={
<Info side='top'>
For selected Projects, teammates governed by this group need access to every active
environment to use Issues.
</Info>
}
action={
projects.hasNextPage ? (
<Chip
disabled={projects.isFetchingNextPage}
onClick={() => void projects.fetchNextPage()}
>
{projects.isFetchingNextPage ? 'Loading…' : 'Load more'}
</Chip>
) : undefined
}
>
{projects.error && (
<p className='text-[var(--text-error)] text-small'>{projects.error.message}</p>
<p className='pl-2 text-[var(--text-error)] text-caption'>{projects.error.message}</p>
)}
{projects.data?.pages
.flatMap((page) => page.projects)
.map((project) => (
<label
htmlFor={`project-issues-${project.id}`}
<div className='flex flex-col gap-0.5'>
{choices.map((project) => (
<div
key={project.id}
className='flex items-center gap-2'
className='flex items-center gap-1.5 rounded-md pr-2 transition-colors hover-hover:bg-[var(--surface-active)]'
>
<Checkbox
id={`project-issues-${project.id}`}
checked={selected.has(project.id)}
onCheckedChange={(checked) =>
onChange(
checked === true
? [...new Set([...value, project.id])]
: value.filter((id) => id !== project.id)
)
}
/>
<span className='text-small'>{project.name}</span>
</label>
<label
htmlFor={`project-issues-${project.id}`}
className='flex min-w-0 flex-1 cursor-pointer items-center gap-2 py-[5px] pl-2'
>
<Checkbox
id={`project-issues-${project.id}`}
checked={selected.has(project.id)}
onCheckedChange={(checked) =>
onChange(
checked === true
? [...new Set([...value, project.id])]
: value.filter((id) => id !== project.id)
)
}
/>
<OverflowText label={project.name} className='text-sm' />
</label>
</div>
))}
{projects.hasNextPage && (
<Chip disabled={projects.isFetchingNextPage} onClick={() => void projects.fetchNextPage()}>
Load more
</Chip>
)}
</div>
</div>
</SettingsSection>
)
}
Loading
Loading