Skip to content

Commit 0abeac7

Browse files
improvement(platform): standardize perms, audit logging, lifecycle across admin, copilot, ui actions (#3858)
* improvement(platform): standardize perms, audit logging, lifecycle mgmt across admin, copilot, ui actions * address comments * improve error codes * address bugbot comments * fix test
1 parent e9c94fa commit 0abeac7

File tree

34 files changed

+1773
-2413
lines changed

34 files changed

+1773
-2413
lines changed

apps/sim/app/api/chat/manage/[id]/route.test.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@ const {
1515
mockLimit,
1616
mockUpdate,
1717
mockSet,
18-
mockDelete,
1918
mockCreateSuccessResponse,
2019
mockCreateErrorResponse,
2120
mockEncryptSecret,
2221
mockCheckChatAccess,
2322
mockDeployWorkflow,
23+
mockPerformChatUndeploy,
2424
mockLogger,
2525
} = vi.hoisted(() => {
2626
const logger = {
@@ -40,12 +40,12 @@ const {
4040
mockLimit: vi.fn(),
4141
mockUpdate: vi.fn(),
4242
mockSet: vi.fn(),
43-
mockDelete: vi.fn(),
4443
mockCreateSuccessResponse: vi.fn(),
4544
mockCreateErrorResponse: vi.fn(),
4645
mockEncryptSecret: vi.fn(),
4746
mockCheckChatAccess: vi.fn(),
4847
mockDeployWorkflow: vi.fn(),
48+
mockPerformChatUndeploy: vi.fn(),
4949
mockLogger: logger,
5050
}
5151
})
@@ -66,7 +66,6 @@ vi.mock('@sim/db', () => ({
6666
db: {
6767
select: mockSelect,
6868
update: mockUpdate,
69-
delete: mockDelete,
7069
},
7170
}))
7271
vi.mock('@sim/db/schema', () => ({
@@ -88,6 +87,9 @@ vi.mock('@/app/api/chat/utils', () => ({
8887
vi.mock('@/lib/workflows/persistence/utils', () => ({
8988
deployWorkflow: mockDeployWorkflow,
9089
}))
90+
vi.mock('@/lib/workflows/orchestration', () => ({
91+
performChatUndeploy: mockPerformChatUndeploy,
92+
}))
9193
vi.mock('drizzle-orm', () => ({
9294
and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),
9395
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -106,7 +108,7 @@ describe('Chat Edit API Route', () => {
106108
mockWhere.mockReturnValue({ limit: mockLimit })
107109
mockUpdate.mockReturnValue({ set: mockSet })
108110
mockSet.mockReturnValue({ where: mockWhere })
109-
mockDelete.mockReturnValue({ where: mockWhere })
111+
mockPerformChatUndeploy.mockResolvedValue({ success: true })
110112

111113
mockCreateSuccessResponse.mockImplementation((data) => {
112114
return new Response(JSON.stringify(data), {
@@ -428,7 +430,11 @@ describe('Chat Edit API Route', () => {
428430
const response = await DELETE(req, { params: Promise.resolve({ id: 'chat-123' }) })
429431

430432
expect(response.status).toBe(200)
431-
expect(mockDelete).toHaveBeenCalled()
433+
expect(mockPerformChatUndeploy).toHaveBeenCalledWith({
434+
chatId: 'chat-123',
435+
userId: 'user-id',
436+
workspaceId: 'workspace-123',
437+
})
432438
const data = await response.json()
433439
expect(data.message).toBe('Chat deployment deleted successfully')
434440
})
@@ -451,7 +457,11 @@ describe('Chat Edit API Route', () => {
451457

452458
expect(response.status).toBe(200)
453459
expect(mockCheckChatAccess).toHaveBeenCalledWith('chat-123', 'admin-user-id')
454-
expect(mockDelete).toHaveBeenCalled()
460+
expect(mockPerformChatUndeploy).toHaveBeenCalledWith({
461+
chatId: 'chat-123',
462+
userId: 'admin-user-id',
463+
workspaceId: 'workspace-123',
464+
})
455465
})
456466
})
457467
})

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { getSession } from '@/lib/auth'
99
import { isDev } from '@/lib/core/config/feature-flags'
1010
import { encryptSecret } from '@/lib/core/security/encryption'
1111
import { getEmailDomain } from '@/lib/core/utils/urls'
12+
import { performChatUndeploy } from '@/lib/workflows/orchestration'
1213
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
1314
import { checkChatAccess } from '@/app/api/chat/utils'
1415
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -270,33 +271,25 @@ export async function DELETE(
270271
return createErrorResponse('Unauthorized', 401)
271272
}
272273

273-
const {
274-
hasAccess,
275-
chat: chatRecord,
276-
workspaceId: chatWorkspaceId,
277-
} = await checkChatAccess(chatId, session.user.id)
274+
const { hasAccess, workspaceId: chatWorkspaceId } = await checkChatAccess(
275+
chatId,
276+
session.user.id
277+
)
278278

279279
if (!hasAccess) {
280280
return createErrorResponse('Chat not found or access denied', 404)
281281
}
282282

283-
await db.delete(chat).where(eq(chat.id, chatId))
284-
285-
logger.info(`Chat "${chatId}" deleted successfully`)
286-
287-
recordAudit({
288-
workspaceId: chatWorkspaceId || null,
289-
actorId: session.user.id,
290-
actorName: session.user.name,
291-
actorEmail: session.user.email,
292-
action: AuditAction.CHAT_DELETED,
293-
resourceType: AuditResourceType.CHAT,
294-
resourceId: chatId,
295-
resourceName: chatRecord?.title || chatId,
296-
description: `Deleted chat deployment "${chatRecord?.title || chatId}"`,
297-
request: _request,
283+
const result = await performChatUndeploy({
284+
chatId,
285+
userId: session.user.id,
286+
workspaceId: chatWorkspaceId,
298287
})
299288

289+
if (!result.success) {
290+
return createErrorResponse(result.error || 'Failed to delete chat', 500)
291+
}
292+
300293
return createSuccessResponse({
301294
message: 'Chat deployment deleted successfully',
302295
})

apps/sim/app/api/chat/route.test.ts

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*
44
* @vitest-environment node
55
*/
6-
import { auditMock, createEnvMock } from '@sim/testing'
6+
import { createEnvMock } from '@sim/testing'
77
import { NextRequest } from 'next/server'
88
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
99

@@ -12,66 +12,51 @@ const {
1212
mockFrom,
1313
mockWhere,
1414
mockLimit,
15-
mockInsert,
16-
mockValues,
17-
mockReturning,
1815
mockCreateSuccessResponse,
1916
mockCreateErrorResponse,
20-
mockEncryptSecret,
2117
mockCheckWorkflowAccessForChatCreation,
22-
mockDeployWorkflow,
18+
mockPerformChatDeploy,
2319
mockGetSession,
24-
mockUuidV4,
2520
} = vi.hoisted(() => ({
2621
mockSelect: vi.fn(),
2722
mockFrom: vi.fn(),
2823
mockWhere: vi.fn(),
2924
mockLimit: vi.fn(),
30-
mockInsert: vi.fn(),
31-
mockValues: vi.fn(),
32-
mockReturning: vi.fn(),
3325
mockCreateSuccessResponse: vi.fn(),
3426
mockCreateErrorResponse: vi.fn(),
35-
mockEncryptSecret: vi.fn(),
3627
mockCheckWorkflowAccessForChatCreation: vi.fn(),
37-
mockDeployWorkflow: vi.fn(),
28+
mockPerformChatDeploy: vi.fn(),
3829
mockGetSession: vi.fn(),
39-
mockUuidV4: vi.fn(),
4030
}))
4131

42-
vi.mock('@/lib/audit/log', () => auditMock)
43-
4432
vi.mock('@sim/db', () => ({
4533
db: {
4634
select: mockSelect,
47-
insert: mockInsert,
4835
},
4936
}))
5037

5138
vi.mock('@sim/db/schema', () => ({
52-
chat: { userId: 'userId', identifier: 'identifier' },
39+
chat: { userId: 'userId', identifier: 'identifier', archivedAt: 'archivedAt' },
5340
workflow: { id: 'id', userId: 'userId', isDeployed: 'isDeployed' },
5441
}))
5542

43+
vi.mock('drizzle-orm', () => ({
44+
and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })),
45+
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
46+
isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })),
47+
}))
48+
5649
vi.mock('@/app/api/workflows/utils', () => ({
5750
createSuccessResponse: mockCreateSuccessResponse,
5851
createErrorResponse: mockCreateErrorResponse,
5952
}))
6053

61-
vi.mock('@/lib/core/security/encryption', () => ({
62-
encryptSecret: mockEncryptSecret,
63-
}))
64-
65-
vi.mock('uuid', () => ({
66-
v4: mockUuidV4,
67-
}))
68-
6954
vi.mock('@/app/api/chat/utils', () => ({
7055
checkWorkflowAccessForChatCreation: mockCheckWorkflowAccessForChatCreation,
7156
}))
7257

73-
vi.mock('@/lib/workflows/persistence/utils', () => ({
74-
deployWorkflow: mockDeployWorkflow,
58+
vi.mock('@/lib/workflows/orchestration', () => ({
59+
performChatDeploy: mockPerformChatDeploy,
7560
}))
7661

7762
vi.mock('@/lib/auth', () => ({
@@ -94,10 +79,6 @@ describe('Chat API Route', () => {
9479
mockSelect.mockReturnValue({ from: mockFrom })
9580
mockFrom.mockReturnValue({ where: mockWhere })
9681
mockWhere.mockReturnValue({ limit: mockLimit })
97-
mockInsert.mockReturnValue({ values: mockValues })
98-
mockValues.mockReturnValue({ returning: mockReturning })
99-
100-
mockUuidV4.mockReturnValue('test-uuid')
10182

10283
mockCreateSuccessResponse.mockImplementation((data) => {
10384
return new Response(JSON.stringify(data), {
@@ -113,12 +94,10 @@ describe('Chat API Route', () => {
11394
})
11495
})
11596

116-
mockEncryptSecret.mockResolvedValue({ encrypted: 'encrypted-password' })
117-
118-
mockDeployWorkflow.mockResolvedValue({
97+
mockPerformChatDeploy.mockResolvedValue({
11998
success: true,
120-
version: 1,
121-
deployedAt: new Date(),
99+
chatId: 'test-uuid',
100+
chatUrl: 'http://localhost:3000/chat/test-chat',
122101
})
123102
})
124103

@@ -277,7 +256,6 @@ describe('Chat API Route', () => {
277256
hasAccess: true,
278257
workflow: { userId: 'user-id', workspaceId: null, isDeployed: true },
279258
})
280-
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
281259

282260
const req = new NextRequest('http://localhost:3000/api/chat', {
283261
method: 'POST',
@@ -287,6 +265,13 @@ describe('Chat API Route', () => {
287265

288266
expect(response.status).toBe(200)
289267
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
268+
expect(mockPerformChatDeploy).toHaveBeenCalledWith(
269+
expect.objectContaining({
270+
workflowId: 'workflow-123',
271+
userId: 'user-id',
272+
identifier: 'test-chat',
273+
})
274+
)
290275
})
291276

292277
it('should allow chat deployment when user has workspace admin permission', async () => {
@@ -309,7 +294,6 @@ describe('Chat API Route', () => {
309294
hasAccess: true,
310295
workflow: { userId: 'other-user-id', workspaceId: 'workspace-123', isDeployed: true },
311296
})
312-
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
313297

314298
const req = new NextRequest('http://localhost:3000/api/chat', {
315299
method: 'POST',
@@ -319,6 +303,12 @@ describe('Chat API Route', () => {
319303

320304
expect(response.status).toBe(200)
321305
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
306+
expect(mockPerformChatDeploy).toHaveBeenCalledWith(
307+
expect.objectContaining({
308+
workflowId: 'workflow-123',
309+
workspaceId: 'workspace-123',
310+
})
311+
)
322312
})
323313

324314
it('should reject when workflow is in workspace but user lacks admin permission', async () => {
@@ -383,7 +373,7 @@ describe('Chat API Route', () => {
383373
expect(mockCheckWorkflowAccessForChatCreation).toHaveBeenCalledWith('workflow-123', 'user-id')
384374
})
385375

386-
it('should auto-deploy workflow if not already deployed', async () => {
376+
it('should call performChatDeploy for undeployed workflow', async () => {
387377
mockGetSession.mockResolvedValue({
388378
user: { id: 'user-id', email: 'user@example.com' },
389379
})
@@ -403,7 +393,6 @@ describe('Chat API Route', () => {
403393
hasAccess: true,
404394
workflow: { userId: 'user-id', workspaceId: null, isDeployed: false },
405395
})
406-
mockReturning.mockResolvedValue([{ id: 'test-uuid' }])
407396

408397
const req = new NextRequest('http://localhost:3000/api/chat', {
409398
method: 'POST',
@@ -412,10 +401,12 @@ describe('Chat API Route', () => {
412401
const response = await POST(req)
413402

414403
expect(response.status).toBe(200)
415-
expect(mockDeployWorkflow).toHaveBeenCalledWith({
416-
workflowId: 'workflow-123',
417-
deployedBy: 'user-id',
418-
})
404+
expect(mockPerformChatDeploy).toHaveBeenCalledWith(
405+
expect.objectContaining({
406+
workflowId: 'workflow-123',
407+
userId: 'user-id',
408+
})
409+
)
419410
})
420411
})
421412
})

0 commit comments

Comments
 (0)