Skip to content

Commit 13b55d9

Browse files
committed
fix(workspaces): add admin authorization, audit log, and posthog event for workspace logo uploads
1 parent da185e6 commit 13b55d9

5 files changed

Lines changed: 88 additions & 14 deletions

File tree

apps/sim/app/api/files/upload/route.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import { createLogger } from '@sim/logger'
22
import { type NextRequest, NextResponse } from 'next/server'
33
import { sanitizeFileName } from '@/executor/constants'
44
import '@/lib/uploads/core/setup.server'
5+
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
56
import { getSession } from '@/lib/auth'
7+
import { captureServerEvent } from '@/lib/posthog/server'
68
import type { StorageContext } from '@/lib/uploads/config'
79
import { generateWorkspaceFileKey } from '@/lib/uploads/contexts/workspace/workspace-file-manager'
810
import { isImageFileType } from '@/lib/uploads/utils/file-utils'
@@ -294,6 +296,25 @@ export async function POST(request: NextRequest) {
294296
)
295297
}
296298

299+
if (context === 'workspace-logos') {
300+
if (!workspaceId) {
301+
throw new InvalidRequestError(
302+
'workspace-logos context requires workspaceId parameter'
303+
)
304+
}
305+
const permission = await getUserEntityPermissions(
306+
session.user.id,
307+
'workspace',
308+
workspaceId
309+
)
310+
if (permission !== 'admin') {
311+
return NextResponse.json(
312+
{ error: 'Admin access required for workspace logo uploads' },
313+
{ status: 403 }
314+
)
315+
}
316+
}
317+
297318
if (context === 'chat' && workspaceId) {
298319
const permission = await getUserEntityPermissions(
299320
session.user.id,
@@ -351,6 +372,33 @@ export async function POST(request: NextRequest) {
351372
}
352373

353374
logger.info(`Successfully uploaded ${context} file: ${fileInfo.key}`)
375+
376+
if (context === 'workspace-logos' && workspaceId) {
377+
recordAudit({
378+
workspaceId,
379+
actorId: session.user.id,
380+
actorName: session.user.name,
381+
actorEmail: session.user.email,
382+
action: AuditAction.FILE_UPLOADED,
383+
resourceType: AuditResourceType.WORKSPACE,
384+
resourceId: workspaceId,
385+
description: `Uploaded workspace logo "${originalName}"`,
386+
metadata: {
387+
fileName: originalName,
388+
fileKey: fileInfo.key,
389+
fileSize: buffer.length,
390+
fileType: file.type,
391+
},
392+
request,
393+
})
394+
395+
captureServerEvent(session.user.id, 'workspace_logo_uploaded', {
396+
workspace_id: workspaceId,
397+
file_name: originalName,
398+
file_size: buffer.length,
399+
})
400+
}
401+
354402
uploadResults.push(uploadResult)
355403
continue
356404
}

apps/sim/app/workspace/[workspaceId]/settings/hooks/use-profile-picture-upload.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ interface UseProfilePictureUploadProps {
1111
onError?: (error: string) => void
1212
currentImage?: string | null
1313
context?: StorageContext
14+
workspaceId?: string
1415
}
1516

1617
/**
@@ -22,6 +23,7 @@ export function useProfilePictureUpload({
2223
onError,
2324
currentImage,
2425
context = 'profile-pictures',
26+
workspaceId,
2527
}: UseProfilePictureUploadProps = {}) {
2628
const previewRef = useRef<string | null>(null)
2729
const fileInputRef = useRef<HTMLInputElement>(null)
@@ -57,6 +59,9 @@ export function useProfilePictureUpload({
5759
const formData = new FormData()
5860
formData.append('file', file)
5961
formData.append('context', context)
62+
if (workspaceId) {
63+
formData.append('workspaceId', workspaceId)
64+
}
6065

6166
const response = await fetch('/api/files/upload', {
6267
method: 'POST',
@@ -76,7 +81,7 @@ export function useProfilePictureUpload({
7681
throw new Error(error instanceof Error ? error.message : 'Failed to upload profile picture')
7782
}
7883
},
79-
[context]
84+
[context, workspaceId]
8085
)
8186

8287
const processFile = useCallback(

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workspace-logo-upload.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB
66
const ACCEPTED_IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml', 'image/webp']
77

88
interface UseWorkspaceLogoUploadProps {
9+
workspaceId?: string
910
currentLogoUrl?: string | null
1011
onUpload?: (url: string | null) => void
1112
onError?: (error: string) => void
@@ -16,6 +17,7 @@ interface UseWorkspaceLogoUploadProps {
1617
* Manages file validation, preview generation, and server upload.
1718
*/
1819
export function useWorkspaceLogoUpload({
20+
workspaceId,
1921
currentLogoUrl,
2022
onUpload,
2123
onError,
@@ -25,14 +27,16 @@ export function useWorkspaceLogoUpload({
2527
const onUploadRef = useRef(onUpload)
2628
const onErrorRef = useRef(onError)
2729
const currentLogoUrlRef = useRef(currentLogoUrl)
30+
const workspaceIdRef = useRef(workspaceId)
2831
const [previewUrl, setPreviewUrl] = useState<string | null>(currentLogoUrl || null)
2932
const [isUploading, setIsUploading] = useState(false)
3033

3134
useEffect(() => {
3235
onUploadRef.current = onUpload
3336
onErrorRef.current = onError
3437
currentLogoUrlRef.current = currentLogoUrl
35-
}, [onUpload, onError, currentLogoUrl])
38+
workspaceIdRef.current = workspaceId
39+
}, [onUpload, onError, currentLogoUrl, workspaceId])
3640

3741
useEffect(() => {
3842
if (previewRef.current && previewRef.current !== currentLogoUrl) {
@@ -56,6 +60,9 @@ export function useWorkspaceLogoUpload({
5660
const formData = new FormData()
5761
formData.append('file', file)
5862
formData.append('context', 'workspace-logos')
63+
if (workspaceIdRef.current) {
64+
formData.append('workspaceId', workspaceIdRef.current)
65+
}
5966

6067
const response = await fetch('/api/files/upload', {
6168
method: 'POST',
@@ -116,6 +123,10 @@ export function useWorkspaceLogoUpload({
116123
[processFile]
117124
)
118125

126+
const setTargetWorkspaceId = useCallback((id: string) => {
127+
workspaceIdRef.current = id
128+
}, [])
129+
119130
const handleRemove = useCallback(() => {
120131
if (previewRef.current) {
121132
URL.revokeObjectURL(previewRef.current)
@@ -141,6 +152,7 @@ export function useWorkspaceLogoUpload({
141152
fileInputRef,
142153
handleFileChange,
143154
handleRemove,
155+
setTargetWorkspaceId,
144156
isUploading,
145157
}
146158
}

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

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ import {
9696
useRenameTask,
9797
useTasks,
9898
} from '@/hooks/queries/tasks'
99-
import type { Workspace } from '@/hooks/queries/workspace'
10099
import { useUpdateWorkflow } from '@/hooks/queries/workflows'
100+
import type { Workspace } from '@/hooks/queries/workspace'
101101
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
102102
import { usePermissionConfig } from '@/hooks/use-permission-config'
103103
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
@@ -465,16 +465,20 @@ export const Sidebar = memo(function Sidebar() {
465465
const activeWorkspaceFull = workspaces.find((w) => w.id === workspaceId)
466466
const logoTargetWorkspaceIdRef = useRef<string>(workspaceId)
467467

468-
const { fileInputRef: logoFileInputRef, handleFileChange: handleLogoFileChange } =
469-
useWorkspaceLogoUpload({
470-
currentLogoUrl: activeWorkspaceFull?.logoUrl,
471-
onUpload: (url) => {
472-
updateWorkspace(logoTargetWorkspaceIdRef.current, { logoUrl: url })
473-
},
474-
onError: (error) => {
475-
logger.error('Workspace logo upload error:', error)
476-
},
477-
})
468+
const {
469+
fileInputRef: logoFileInputRef,
470+
handleFileChange: handleLogoFileChange,
471+
setTargetWorkspaceId: setLogoTargetWorkspaceId,
472+
} = useWorkspaceLogoUpload({
473+
workspaceId,
474+
currentLogoUrl: activeWorkspaceFull?.logoUrl,
475+
onUpload: (url) => {
476+
updateWorkspace(logoTargetWorkspaceIdRef.current, { logoUrl: url })
477+
},
478+
onError: (error) => {
479+
logger.error('Workspace logo upload error:', error)
480+
},
481+
})
478482

479483
const { handleMouseDown, isResizing } = useSidebarResize()
480484

@@ -1036,9 +1040,10 @@ export const Sidebar = memo(function Sidebar() {
10361040
const handleUploadLogo = useCallback(
10371041
(workspaceIdToUpdate: string) => {
10381042
logoTargetWorkspaceIdRef.current = workspaceIdToUpdate
1043+
setLogoTargetWorkspaceId(workspaceIdToUpdate)
10391044
logoFileInputRef.current?.click()
10401045
},
1041-
[logoFileInputRef]
1046+
[logoFileInputRef, setLogoTargetWorkspaceId]
10421047
)
10431048

10441049
const handleRemoveLogo = useCallback(

apps/sim/ee/whitelabeling/components/whitelabeling-settings.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useCallback, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { Loader2, X } from 'lucide-react'
66
import Image from 'next/image'
7+
import { useParams } from 'next/navigation'
78
import { Button, Input, Label } from '@/components/emcn'
89
import { useSession } from '@/lib/auth/auth-client'
910
import { getSubscriptionAccessState } from '@/lib/billing/client/utils'
@@ -146,6 +147,7 @@ function SectionTitle({ children }: { children: React.ReactNode }) {
146147
}
147148

148149
export function WhitelabelingSettings() {
150+
const params = useParams<{ workspaceId: string }>()
149151
const { data: session } = useSession()
150152
const { data: orgsData } = useOrganizations()
151153
const { data: subscriptionData } = useSubscriptionData()
@@ -198,13 +200,15 @@ export function WhitelabelingSettings() {
198200
onUpload: (url) => setLogoUrl(url),
199201
onError: (error) => setSaveError(error),
200202
context: 'workspace-logos',
203+
workspaceId: params.workspaceId,
201204
})
202205

203206
const wordmarkUpload = useProfilePictureUpload({
204207
currentImage: wordmarkUrl,
205208
onUpload: (url) => setWordmarkUrl(url),
206209
onError: (error) => setSaveError(error),
207210
context: 'workspace-logos',
211+
workspaceId: params.workspaceId,
208212
})
209213

210214
const handleSave = useCallback(async () => {

0 commit comments

Comments
 (0)