Skip to content

Commit ae2b57b

Browse files
committed
move more ui actions into orchestration dir
1 parent 10c66b7 commit ae2b57b

62 files changed

Lines changed: 4366 additions & 2672 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/credentials/[id]/route.ts

Lines changed: 34 additions & 241 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
1-
import { AuditAction, AuditResourceType, recordAudit } from '@sim/audit'
21
import { db } from '@sim/db'
3-
import { credential, credentialMember, environment, workspaceEnvironment } from '@sim/db/schema'
2+
import { credential, credentialMember } from '@sim/db/schema'
43
import { createLogger } from '@sim/logger'
5-
import { generateId } from '@sim/utils/id'
64
import { and, eq } from 'drizzle-orm'
75
import { type NextRequest, NextResponse } from 'next/server'
86
import { updateWorkspaceCredentialContract } from '@/lib/api/contracts/credentials'
97
import { getValidationErrorMessage, parseRequest } from '@/lib/api/server'
108
import { getSession } from '@/lib/auth'
11-
import { encryptSecret } from '@/lib/core/security/encryption'
129
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1310
import { getCredentialActorContext } from '@/lib/credentials/access'
14-
import { deleteCredential } from '@/lib/credentials/deletion'
15-
import {
16-
deleteWorkspaceEnvCredentials,
17-
syncPersonalEnvCredentialsForUser,
18-
} from '@/lib/credentials/environment'
19-
import { captureServerEvent } from '@/lib/posthog/server'
11+
import { performDeleteCredential, performUpdateCredential } from '@/lib/credentials/orchestration'
2012

2113
const logger = createLogger('CredentialByIdAPI')
2214

@@ -93,93 +85,33 @@ export const PUT = withRouteHandler(
9385
const { id } = parsed.data.params
9486
const body = parsed.data.body
9587

96-
const access = await getCredentialActorContext(id, session.user.id)
97-
if (!access.credential) {
98-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
99-
}
100-
if (!access.hasWorkspaceAccess || !access.isAdmin) {
101-
return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 })
102-
}
103-
104-
const updates: Record<string, unknown> = {}
105-
106-
if (body.description !== undefined) {
107-
updates.description = body.description ?? null
108-
}
109-
110-
if (
111-
body.displayName !== undefined &&
112-
(access.credential.type === 'oauth' || access.credential.type === 'service_account')
113-
) {
114-
updates.displayName = body.displayName
115-
}
116-
117-
if (body.serviceAccountJson !== undefined && access.credential.type === 'service_account') {
118-
let parsedJson: Record<string, unknown>
119-
try {
120-
parsedJson = JSON.parse(body.serviceAccountJson)
121-
} catch {
122-
return NextResponse.json({ error: 'Invalid JSON format' }, { status: 400 })
123-
}
124-
if (
125-
parsedJson.type !== 'service_account' ||
126-
typeof parsedJson.client_email !== 'string' ||
127-
typeof parsedJson.private_key !== 'string' ||
128-
typeof parsedJson.project_id !== 'string'
129-
) {
130-
return NextResponse.json({ error: 'Invalid service account JSON key' }, { status: 400 })
131-
}
132-
const { encrypted } = await encryptSecret(body.serviceAccountJson)
133-
updates.encryptedServiceAccountKey = encrypted
134-
}
135-
136-
if (Object.keys(updates).length === 0) {
137-
if (access.credential.type === 'oauth' || access.credential.type === 'service_account') {
138-
return NextResponse.json(
139-
{
140-
error: 'No updatable fields provided.',
141-
},
142-
{ status: 400 }
143-
)
144-
}
145-
return NextResponse.json(
146-
{
147-
error:
148-
'Environment credentials cannot be updated via this endpoint. Use the environment value editor in credentials settings.',
149-
},
150-
{ status: 400 }
151-
)
152-
}
153-
154-
updates.updatedAt = new Date()
155-
await db.update(credential).set(updates).where(eq(credential.id, id))
156-
157-
recordAudit({
158-
workspaceId: access.credential.workspaceId,
159-
actorId: session.user.id,
88+
const result = await performUpdateCredential({
89+
credentialId: id,
90+
userId: session.user.id,
16091
actorName: session.user.name,
16192
actorEmail: session.user.email,
162-
action: AuditAction.CREDENTIAL_UPDATED,
163-
resourceType: AuditResourceType.CREDENTIAL,
164-
resourceId: id,
165-
resourceName: access.credential.displayName,
166-
description: `Updated ${access.credential.type} credential "${access.credential.displayName}"`,
167-
metadata: {
168-
credentialType: access.credential.type,
169-
updatedFields: Object.keys(updates).filter((k) => k !== 'updatedAt'),
170-
},
93+
displayName: body.displayName,
94+
description: body.description,
95+
serviceAccountJson: body.serviceAccountJson,
17196
request,
17297
})
98+
if (!result.success) {
99+
const status =
100+
result.errorCode === 'not_found'
101+
? 404
102+
: result.errorCode === 'forbidden'
103+
? 403
104+
: result.errorCode === 'conflict'
105+
? 409
106+
: result.errorCode === 'validation'
107+
? 400
108+
: 500
109+
return NextResponse.json({ error: result.error }, { status })
110+
}
173111

174112
const row = await getCredentialResponse(id, session.user.id)
175113
return NextResponse.json({ credential: row }, { status: 200 })
176114
} catch (error) {
177-
if (error instanceof Error && error.message.includes('unique')) {
178-
return NextResponse.json(
179-
{ error: 'A service account credential with this name already exists in the workspace' },
180-
{ status: 409 }
181-
)
182-
}
183115
logger.error('Failed to update credential', error)
184116
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
185117
}
@@ -196,163 +128,24 @@ export const DELETE = withRouteHandler(
196128
const { id } = await params
197129

198130
try {
199-
const access = await getCredentialActorContext(id, session.user.id)
200-
if (!access.credential) {
201-
return NextResponse.json({ error: 'Credential not found' }, { status: 404 })
202-
}
203-
if (!access.hasWorkspaceAccess || !access.isAdmin) {
204-
return NextResponse.json({ error: 'Credential admin permission required' }, { status: 403 })
205-
}
206-
207-
if (access.credential.type === 'env_personal' && access.credential.envKey) {
208-
const ownerUserId = access.credential.envOwnerUserId
209-
if (!ownerUserId) {
210-
return NextResponse.json({ error: 'Invalid personal secret owner' }, { status: 400 })
211-
}
212-
213-
const [personalRow] = await db
214-
.select({ variables: environment.variables })
215-
.from(environment)
216-
.where(eq(environment.userId, ownerUserId))
217-
.limit(1)
218-
219-
const current = ((personalRow?.variables as Record<string, string> | null) ?? {}) as Record<
220-
string,
221-
string
222-
>
223-
if (access.credential.envKey in current) {
224-
delete current[access.credential.envKey]
225-
}
226-
227-
await db
228-
.insert(environment)
229-
.values({
230-
id: ownerUserId,
231-
userId: ownerUserId,
232-
variables: current,
233-
updatedAt: new Date(),
234-
})
235-
.onConflictDoUpdate({
236-
target: [environment.userId],
237-
set: { variables: current, updatedAt: new Date() },
238-
})
239-
240-
await syncPersonalEnvCredentialsForUser({
241-
userId: ownerUserId,
242-
envKeys: Object.keys(current),
243-
})
244-
245-
captureServerEvent(
246-
session.user.id,
247-
'credential_deleted',
248-
{
249-
credential_type: 'env_personal',
250-
provider_id: access.credential.envKey,
251-
workspace_id: access.credential.workspaceId,
252-
},
253-
{ groups: { workspace: access.credential.workspaceId } }
254-
)
255-
256-
recordAudit({
257-
workspaceId: access.credential.workspaceId,
258-
actorId: session.user.id,
259-
actorName: session.user.name,
260-
actorEmail: session.user.email,
261-
action: AuditAction.CREDENTIAL_DELETED,
262-
resourceType: AuditResourceType.CREDENTIAL,
263-
resourceId: id,
264-
resourceName: access.credential.displayName,
265-
description: `Deleted personal env credential "${access.credential.envKey}"`,
266-
metadata: { credentialType: 'env_personal', envKey: access.credential.envKey },
267-
request,
268-
})
269-
270-
return NextResponse.json({ success: true }, { status: 200 })
271-
}
272-
273-
if (access.credential.type === 'env_workspace' && access.credential.envKey) {
274-
const [workspaceRow] = await db
275-
.select({
276-
id: workspaceEnvironment.id,
277-
createdAt: workspaceEnvironment.createdAt,
278-
variables: workspaceEnvironment.variables,
279-
})
280-
.from(workspaceEnvironment)
281-
.where(eq(workspaceEnvironment.workspaceId, access.credential.workspaceId))
282-
.limit(1)
283-
284-
const current = ((workspaceRow?.variables as Record<string, string> | null) ??
285-
{}) as Record<string, string>
286-
if (access.credential.envKey in current) {
287-
delete current[access.credential.envKey]
288-
}
289-
290-
await db
291-
.insert(workspaceEnvironment)
292-
.values({
293-
id: workspaceRow?.id || generateId(),
294-
workspaceId: access.credential.workspaceId,
295-
variables: current,
296-
createdAt: workspaceRow?.createdAt || new Date(),
297-
updatedAt: new Date(),
298-
})
299-
.onConflictDoUpdate({
300-
target: [workspaceEnvironment.workspaceId],
301-
set: { variables: current, updatedAt: new Date() },
302-
})
303-
304-
await deleteWorkspaceEnvCredentials({
305-
workspaceId: access.credential.workspaceId,
306-
removedKeys: [access.credential.envKey],
307-
})
308-
309-
captureServerEvent(
310-
session.user.id,
311-
'credential_deleted',
312-
{
313-
credential_type: 'env_workspace',
314-
provider_id: access.credential.envKey,
315-
workspace_id: access.credential.workspaceId,
316-
},
317-
{ groups: { workspace: access.credential.workspaceId } }
318-
)
319-
320-
recordAudit({
321-
workspaceId: access.credential.workspaceId,
322-
actorId: session.user.id,
323-
actorName: session.user.name,
324-
actorEmail: session.user.email,
325-
action: AuditAction.CREDENTIAL_DELETED,
326-
resourceType: AuditResourceType.CREDENTIAL,
327-
resourceId: id,
328-
resourceName: access.credential.displayName,
329-
description: `Deleted workspace env credential "${access.credential.envKey}"`,
330-
metadata: { credentialType: 'env_workspace', envKey: access.credential.envKey },
331-
request,
332-
})
333-
334-
return NextResponse.json({ success: true }, { status: 200 })
335-
}
336-
337-
await deleteCredential({
131+
const result = await performDeleteCredential({
338132
credentialId: id,
339-
actorId: session.user.id,
133+
userId: session.user.id,
340134
actorName: session.user.name,
341135
actorEmail: session.user.email,
342-
reason: 'user_delete',
343136
request,
344137
})
345-
346-
captureServerEvent(
347-
session.user.id,
348-
'credential_deleted',
349-
{
350-
credential_type: access.credential.type as 'oauth' | 'service_account',
351-
provider_id: access.credential.providerId ?? id,
352-
workspace_id: access.credential.workspaceId,
353-
},
354-
{ groups: { workspace: access.credential.workspaceId } }
355-
)
138+
if (!result.success) {
139+
const status =
140+
result.errorCode === 'not_found'
141+
? 404
142+
: result.errorCode === 'forbidden'
143+
? 403
144+
: result.errorCode === 'validation'
145+
? 400
146+
: 500
147+
return NextResponse.json({ error: result.error }, { status })
148+
}
356149

357150
return NextResponse.json({ success: true }, { status: 200 })
358151
} catch (error) {

apps/sim/app/api/folders/[id]/route.ts

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,7 @@ import { parseRequest } from '@/lib/api/server'
99
import { getSession } from '@/lib/auth'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
1111
import { captureServerEvent } from '@/lib/posthog/server'
12-
import { performDeleteFolder } from '@/lib/workflows/orchestration'
13-
import { checkForCircularReference } from '@/lib/workflows/utils'
12+
import { performDeleteFolder, performUpdateFolder } from '@/lib/workflows/orchestration'
1413
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1514

1615
const logger = createLogger('FoldersIDAPI')
@@ -81,39 +80,27 @@ export const PUT = withRouteHandler(
8180
await assertFolderMutable(parentId)
8281
}
8382

84-
// Prevent setting a folder as its own parent or creating circular references
85-
if (parentId && parentId === id) {
86-
return NextResponse.json({ error: 'Folder cannot be its own parent' }, { status: 400 })
87-
}
83+
const result = await performUpdateFolder({
84+
folderId: id,
85+
workspaceId: existingFolder.workspaceId,
86+
userId: session.user.id,
87+
name,
88+
color,
89+
isExpanded,
90+
locked,
91+
parentId,
92+
sortOrder,
93+
})
8894

89-
// Check for circular references if parentId is provided
90-
if (parentId) {
91-
const wouldCreateCycle = await checkForCircularReference(id, parentId)
92-
if (wouldCreateCycle) {
93-
return NextResponse.json(
94-
{ error: 'Cannot create circular folder reference' },
95-
{ status: 400 }
96-
)
97-
}
95+
if (!result.success || !result.folder) {
96+
const status =
97+
result.errorCode === 'not_found' ? 404 : result.errorCode === 'validation' ? 400 : 500
98+
return NextResponse.json({ error: result.error }, { status })
9899
}
99100

100-
const updates: Record<string, unknown> = { updatedAt: new Date() }
101-
if (name !== undefined) updates.name = name.trim()
102-
if (color !== undefined) updates.color = color
103-
if (isExpanded !== undefined) updates.isExpanded = isExpanded
104-
if (locked !== undefined) updates.locked = locked
105-
if (parentId !== undefined) updates.parentId = parentId || null
106-
if (sortOrder !== undefined) updates.sortOrder = sortOrder
107-
108-
const [updatedFolder] = await db
109-
.update(workflowFolder)
110-
.set(updates)
111-
.where(eq(workflowFolder.id, id))
112-
.returning()
113-
114-
logger.info('Updated folder:', { id, updates })
101+
logger.info('Updated folder:', { id, updates: parsed.data.body })
115102

116-
return NextResponse.json({ folder: updatedFolder })
103+
return NextResponse.json({ folder: result.folder })
117104
} catch (error) {
118105
if (error instanceof FolderLockedError) {
119106
return NextResponse.json({ error: error.message }, { status: error.status })

0 commit comments

Comments
 (0)