Skip to content

Commit 4fae386

Browse files
committed
fix(workspace-files): audit fixes — transaction, status codes, contract refinements, guards
1 parent 98cf4d3 commit 4fae386

8 files changed

Lines changed: 71 additions & 57 deletions

File tree

apps/sim/app/api/workspaces/[id]/files/download/route.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export const GET = withRouteHandler(
9797
{
9898
error: `Too many files selected for download. Select ${MAX_ZIP_DOWNLOAD_FILES} or fewer files.`,
9999
},
100-
{ status: 413 }
100+
{ status: 400 }
101101
)
102102
}
103103

@@ -107,7 +107,7 @@ export const GET = withRouteHandler(
107107
{
108108
error: `Selected files total ${formatFileSize(totalBytes)}, which exceeds the ${formatFileSize(MAX_ZIP_DOWNLOAD_BYTES)} download limit.`,
109109
},
110-
{ status: 413 }
110+
{ status: 400 }
111111
)
112112
}
113113

apps/sim/app/workspace/[workspaceId]/files/components/action-bar/action-bar.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,10 +91,12 @@ export function FilesActionBar({
9191
align='center'
9292
className='max-h-[240px] overflow-y-auto'
9393
>
94-
<DropdownMenuItem onSelect={() => onMove(moveOptions[0].value)}>
95-
<Folder />
96-
{moveOptions[0].label}
97-
</DropdownMenuItem>
94+
{moveOptions.length > 0 && (
95+
<DropdownMenuItem onSelect={() => onMove(moveOptions[0].value)}>
96+
<Folder />
97+
{moveOptions[0].label}
98+
</DropdownMenuItem>
99+
)}
98100
{moveOptions.length > 1 && <DropdownMenuSeparator />}
99101
{moveOptions.slice(1).map((option) => renderMoveOption(option, onMove))}
100102
</DropdownMenuContent>

apps/sim/app/workspace/[workspaceId]/files/components/delete-confirm-modal/delete-confirm-modal.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,12 @@ export const DeleteConfirmModal = memo(function DeleteConfirmModal({
4848
<ModalBody>
4949
<ModalDescription className='text-[var(--text-secondary)]'>
5050
Are you sure you want to delete{' '}
51-
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>?{' '}
52-
{consequence}
51+
{fileName ? (
52+
<span className='font-medium text-[var(--text-primary)]'>{fileName}</span>
53+
) : (
54+
`${totalCount} item${totalCount === 1 ? '' : 's'}`
55+
)}
56+
? {consequence}
5357
</ModalDescription>
5458
</ModalBody>
5559
<ModalFooter>

apps/sim/app/workspace/[workspaceId]/files/components/files-list-context-menu/files-list-context-menu.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,8 @@ export const FilesListContextMenu = memo(function FilesListContextMenu({
3636
<DropdownMenu open={isOpen} onOpenChange={(open) => !open && onClose()} modal={false}>
3737
<DropdownMenuTrigger asChild>
3838
<div
39-
style={{
40-
position: 'fixed',
41-
left: `${position.x}px`,
42-
top: `${position.y}px`,
43-
width: '1px',
44-
height: '1px',
45-
pointerEvents: 'none',
46-
}}
39+
className='pointer-events-none fixed size-px'
40+
style={{ left: position.x, top: position.y }}
4741
tabIndex={-1}
4842
aria-hidden
4943
/>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ export const Sidebar = memo(function Sidebar() {
826826
id: f.id,
827827
name: f.name,
828828
href: `/workspace/${workspaceId}/files/${f.id}`,
829-
folderPath: f.folderPath ? f.folderPath.split('/') : undefined,
829+
folderPath: f.folderPath ? f.folderPath.split('/').filter(Boolean) : undefined,
830830
})),
831831
[fetchedFiles, workspaceId, permissionConfig.hideFilesTab]
832832
)

apps/sim/lib/api/contracts/tools/file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const fileManageGetBodySchema = z
2727
operation: z.literal('get'),
2828
workspaceId: z.string().min(1).optional(),
2929
fileId: z.string().min(1).optional(),
30-
fileInput: z.any().optional(),
30+
fileInput: z.unknown().optional(),
3131
})
3232
.refine((data) => data.fileId !== undefined || data.fileInput !== undefined, {
3333
message: 'Either fileId or fileInput is required for get operation',

apps/sim/lib/api/contracts/workspace-file-folders.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,24 @@ export const updateWorkspaceFileFolderBodySchema = z.object({
5454
sortOrder: z.number().int().optional(),
5555
})
5656

57-
export const moveWorkspaceFileItemsBodySchema = z.object({
58-
fileIds: z.array(z.string()).default([]),
59-
folderIds: z.array(z.string()).default([]),
60-
targetFolderId: z.string().nullable().optional(),
61-
})
57+
export const moveWorkspaceFileItemsBodySchema = z
58+
.object({
59+
fileIds: z.array(z.string()).default([]),
60+
folderIds: z.array(z.string()).default([]),
61+
targetFolderId: z.string().nullable().optional(),
62+
})
63+
.refine((body) => body.fileIds.length > 0 || body.folderIds.length > 0, {
64+
message: 'At least one file or folder must be selected',
65+
})
6266

63-
export const bulkArchiveWorkspaceFileItemsBodySchema = z.object({
64-
fileIds: z.array(z.string()).default([]),
65-
folderIds: z.array(z.string()).default([]),
66-
})
67+
export const bulkArchiveWorkspaceFileItemsBodySchema = z
68+
.object({
69+
fileIds: z.array(z.string()).default([]),
70+
folderIds: z.array(z.string()).default([]),
71+
})
72+
.refine((body) => body.fileIds.length > 0 || body.folderIds.length > 0, {
73+
message: 'At least one file or folder must be selected',
74+
})
6775

6876
const queryIdListSchema = z
6977
.union([z.string(), z.array(z.string())])

apps/sim/lib/uploads/contexts/workspace/workspace-file-folder-manager.ts

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -902,43 +902,49 @@ export async function restoreWorkspaceFileFolder(
902902
workspaceId: string,
903903
folderId: string
904904
): Promise<WorkspaceFileFolderRecord> {
905-
const raw = await db
906-
.select()
907-
.from(workspaceFileFolder)
908-
.where(
909-
and(eq(workspaceFileFolder.id, folderId), eq(workspaceFileFolder.workspaceId, workspaceId))
910-
)
911-
.limit(1)
912-
.then((rows) => rows[0] ?? null)
913-
914-
if (!raw) throw new Error('Folder not found')
915-
if (!raw.deletedAt) throw new Error('Folder is not archived')
905+
const restored = await db.transaction(async (tx) => {
906+
await acquireWorkspaceFileFolderMutationLock(tx, workspaceId)
916907

917-
// If the parent folder is still archived, restore to root so the folder
918-
// doesn't become an orphan (hidden under an archived parent).
919-
let resolvedParentId = raw.parentId
920-
if (resolvedParentId) {
921-
const parent = await db
922-
.select({ deletedAt: workspaceFileFolder.deletedAt })
908+
const raw = await tx
909+
.select()
923910
.from(workspaceFileFolder)
924911
.where(
925-
and(
926-
eq(workspaceFileFolder.id, resolvedParentId),
927-
eq(workspaceFileFolder.workspaceId, workspaceId)
928-
)
912+
and(eq(workspaceFileFolder.id, folderId), eq(workspaceFileFolder.workspaceId, workspaceId))
929913
)
930914
.limit(1)
931915
.then((rows) => rows[0] ?? null)
932-
if (!parent || parent.deletedAt) resolvedParentId = null
933-
}
934916

935-
const [restored] = await db
936-
.update(workspaceFileFolder)
937-
.set({ deletedAt: null, parentId: resolvedParentId, updatedAt: new Date() })
938-
.where(
939-
and(eq(workspaceFileFolder.id, folderId), eq(workspaceFileFolder.workspaceId, workspaceId))
940-
)
941-
.returning()
917+
if (!raw) throw new Error('Folder not found')
918+
if (!raw.deletedAt) throw new Error('Folder is not archived')
919+
920+
// If the parent folder is still archived, restore to root so the folder
921+
// doesn't become an orphan (hidden under an archived parent).
922+
let resolvedParentId = raw.parentId
923+
if (resolvedParentId) {
924+
const parent = await tx
925+
.select({ deletedAt: workspaceFileFolder.deletedAt })
926+
.from(workspaceFileFolder)
927+
.where(
928+
and(
929+
eq(workspaceFileFolder.id, resolvedParentId),
930+
eq(workspaceFileFolder.workspaceId, workspaceId)
931+
)
932+
)
933+
.limit(1)
934+
.then((rows) => rows[0] ?? null)
935+
if (!parent || parent.deletedAt) resolvedParentId = null
936+
}
937+
938+
const [row] = await tx
939+
.update(workspaceFileFolder)
940+
.set({ deletedAt: null, parentId: resolvedParentId, updatedAt: new Date() })
941+
.where(
942+
and(eq(workspaceFileFolder.id, folderId), eq(workspaceFileFolder.workspaceId, workspaceId))
943+
)
944+
.returning()
945+
946+
return row
947+
})
942948

943949
logger.info('Restored workspace file folder', { workspaceId, folderId })
944950

0 commit comments

Comments
 (0)