From 22a02aa33ae0b612ddacfce82e3890d680e1fd5c Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 9 May 2026 14:29:57 -0700 Subject: [PATCH 1/2] fix(uploads): write workspaceFiles row when issuing presigned URL --- .../sim/app/api/files/presigned/route.test.ts | 101 ++++++++++++++++++ apps/sim/app/api/files/presigned/route.ts | 31 ++++++ 2 files changed, 132 insertions(+) diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts index 7c4893dc44..95964c012c 100644 --- a/apps/sim/app/api/files/presigned/route.test.ts +++ b/apps/sim/app/api/files/presigned/route.test.ts @@ -25,6 +25,7 @@ const { mockGetUserEntityPermissions, mockGenerateWorkspaceFileKey, mockGenerateExecutionFileKey, + mockInsertFileMetadata, } = vi.hoisted(() => ({ mockVerifyFileAccess: vi.fn().mockResolvedValue(true), mockVerifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true), @@ -50,6 +51,7 @@ const { (ctx: { workspaceId: string; workflowId: string; executionId: string }, fileName: string) => `execution/${ctx.workspaceId}/${ctx.workflowId}/${ctx.executionId}/${fileName}` ), + mockInsertFileMetadata: vi.fn().mockResolvedValue({ id: 'wf_test' }), })) vi.mock('@/app/api/files/authorization', () => ({ @@ -89,6 +91,10 @@ vi.mock('@/lib/uploads/contexts/execution/utils', () => ({ generateExecutionFileKey: mockGenerateExecutionFileKey, })) +vi.mock('@/lib/uploads/server/metadata', () => ({ + insertFileMetadata: mockInsertFileMetadata, +})) + vi.mock('@/lib/uploads/utils/file-utils', () => ({ isImageFileType: mockIsImageFileType, })) @@ -614,6 +620,37 @@ describe('/api/files/presigned', () => { const response = await POST(request) expect(response.status).toBe(403) }) + + it('inserts a workspaceFiles row with context=mothership so previews authorize', async () => { + setupFileApiMocks({ cloudEnabled: true, storageProvider: 's3' }) + + const request = new NextRequest( + 'http://localhost:3000/api/files/presigned?type=mothership&workspaceId=ws-1', + { + method: 'POST', + body: JSON.stringify({ + fileName: 'screenshot.png', + contentType: 'image/png', + fileSize: 4096, + }), + } + ) + + const response = await POST(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(mockInsertFileMetadata).toHaveBeenCalledTimes(1) + expect(mockInsertFileMetadata).toHaveBeenCalledWith({ + key: data.fileInfo.key, + userId: 'test-user-id', + workspaceId: 'ws-1', + context: 'mothership', + originalName: 'screenshot.png', + contentType: 'image/png', + size: 4096, + }) + }) }) describe('execution uploads', () => { @@ -682,6 +719,70 @@ describe('/api/files/presigned', () => { const response = await POST(request) expect(response.status).toBe(400) }) + + it('inserts a workspaceFiles row with context=execution so previews authorize', async () => { + setupFileApiMocks({ cloudEnabled: true, storageProvider: 's3' }) + + const request = new NextRequest( + 'http://localhost:3000/api/files/presigned?type=execution&workspaceId=ws-1&workflowId=wf-1&executionId=exec-1', + { + method: 'POST', + body: JSON.stringify({ + fileName: 'output.mp4', + contentType: 'video/mp4', + fileSize: 4096, + }), + } + ) + + const response = await POST(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(mockInsertFileMetadata).toHaveBeenCalledTimes(1) + expect(mockInsertFileMetadata).toHaveBeenCalledWith({ + key: data.fileInfo.key, + userId: 'test-user-id', + workspaceId: 'ws-1', + context: 'execution', + originalName: 'output.mp4', + contentType: 'video/mp4', + size: 4096, + }) + }) + }) + + describe('workspace-logos uploads', () => { + it('inserts a workspaceFiles row with context=workspace-logos so logos authorize', async () => { + setupFileApiMocks({ cloudEnabled: true, storageProvider: 's3' }) + + const request = new NextRequest( + 'http://localhost:3000/api/files/presigned?type=workspace-logos&workspaceId=ws-1', + { + method: 'POST', + body: JSON.stringify({ + fileName: 'logo.png', + contentType: 'image/png', + fileSize: 4096, + }), + } + ) + + const response = await POST(request) + const data = await response.json() + + expect(response.status).toBe(200) + expect(mockInsertFileMetadata).toHaveBeenCalledTimes(1) + expect(mockInsertFileMetadata).toHaveBeenCalledWith({ + key: data.fileInfo.key, + userId: 'test-user-id', + workspaceId: 'ws-1', + context: 'workspace-logos', + originalName: 'logo.png', + contentType: 'image/png', + size: 4096, + }) + }) }) describe('knowledge-base uploads', () => { diff --git a/apps/sim/app/api/files/presigned/route.ts b/apps/sim/app/api/files/presigned/route.ts index 7c4bb01ec6..8c3eda979d 100644 --- a/apps/sim/app/api/files/presigned/route.ts +++ b/apps/sim/app/api/files/presigned/route.ts @@ -10,6 +10,7 @@ import { USE_BLOB_STORAGE } from '@/lib/uploads/config' import { generateExecutionFileKey } from '@/lib/uploads/contexts/execution/utils' import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { generatePresignedUploadUrl, hasCloudStorage } from '@/lib/uploads/core/storage-service' +import { insertFileMetadata } from '@/lib/uploads/server/metadata' import { isImageFileType } from '@/lib/uploads/utils/file-utils' import { validateAttachmentFileType, validateFileType } from '@/lib/uploads/utils/validation' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -157,6 +158,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { expirationSeconds: 3600, metadata: { workspaceId }, }) + + await insertFileMetadata({ + key: presignedUrlResponse.key, + userId: sessionUserId, + workspaceId, + context: 'mothership', + originalName: fileName, + contentType, + size: fileSize, + }) } else if (uploadType === 'execution') { const workflowId = request.nextUrl.searchParams.get('workflowId') const executionId = request.nextUrl.searchParams.get('executionId') @@ -191,6 +202,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { expirationSeconds: 3600, metadata: { workspaceId, workflowId, executionId }, }) + + await insertFileMetadata({ + key: presignedUrlResponse.key, + userId: sessionUserId, + workspaceId, + context: 'execution', + originalName: fileName, + contentType, + size: fileSize, + }) } else if (uploadType === 'workspace-logos') { const workspaceId = request.nextUrl.searchParams.get('workspaceId') if (!workspaceId?.trim()) { @@ -222,6 +243,16 @@ export const POST = withRouteHandler(async (request: NextRequest) => { expirationSeconds: 3600, metadata: { workspaceId }, }) + + await insertFileMetadata({ + key: presignedUrlResponse.key, + userId: sessionUserId, + workspaceId, + context: 'workspace-logos', + originalName: fileName, + contentType, + size: fileSize, + }) } else { if (uploadType === 'profile-pictures') { if (!sessionUserId?.trim()) { From c89ef33384d80f20175a5a2a8082e0c6b069b4eb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sat, 9 May 2026 14:36:14 -0700 Subject: [PATCH 2/2] test(uploads): cover insertFileMetadata failure path in presigned route --- .../sim/app/api/files/presigned/route.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts index 95964c012c..724aab5d06 100644 --- a/apps/sim/app/api/files/presigned/route.test.ts +++ b/apps/sim/app/api/files/presigned/route.test.ts @@ -651,6 +651,26 @@ describe('/api/files/presigned', () => { size: 4096, }) }) + + it('returns 500 when insertFileMetadata fails so callers do not get an unauthorizable URL', async () => { + setupFileApiMocks({ cloudEnabled: true, storageProvider: 's3' }) + mockInsertFileMetadata.mockRejectedValueOnce(new Error('DB connection lost')) + + const request = new NextRequest( + 'http://localhost:3000/api/files/presigned?type=mothership&workspaceId=ws-1', + { + method: 'POST', + body: JSON.stringify({ + fileName: 'screenshot.png', + contentType: 'image/png', + fileSize: 4096, + }), + } + ) + + const response = await POST(request) + expect(response.status).toBe(500) + }) }) describe('execution uploads', () => {