Skip to content

Commit 8f7b11f

Browse files
authored
feat(account): added user profile pictures in settings (#1297)
* update infra and remove railway * feat(account): add profile pictures * Revert "update infra and remove railway" This reverts commit e3f0c49. * ack PR comments, use brandConfig logo URL as default
1 parent ae670a7 commit 8f7b11f

File tree

6 files changed

+371
-47
lines changed

6 files changed

+371
-47
lines changed

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

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {
1212
BLOB_CONFIG,
1313
BLOB_COPILOT_CONFIG,
1414
BLOB_KB_CONFIG,
15+
BLOB_PROFILE_PICTURES_CONFIG,
1516
S3_CHAT_CONFIG,
1617
S3_CONFIG,
1718
S3_COPILOT_CONFIG,
1819
S3_KB_CONFIG,
20+
S3_PROFILE_PICTURES_CONFIG,
1921
} from '@/lib/uploads/setup'
2022
import { validateFileType } from '@/lib/uploads/validation'
2123
import { createErrorResponse, createOptionsResponse } from '@/app/api/files/utils'
@@ -30,7 +32,7 @@ interface PresignedUrlRequest {
3032
chatId?: string
3133
}
3234

33-
type UploadType = 'general' | 'knowledge-base' | 'chat' | 'copilot'
35+
type UploadType = 'general' | 'knowledge-base' | 'chat' | 'copilot' | 'profile-pictures'
3436

3537
class PresignedUrlError extends Error {
3638
constructor(
@@ -96,7 +98,9 @@ export async function POST(request: NextRequest) {
9698
? 'chat'
9799
: uploadTypeParam === 'copilot'
98100
? 'copilot'
99-
: 'general'
101+
: uploadTypeParam === 'profile-pictures'
102+
? 'profile-pictures'
103+
: 'general'
100104

101105
if (uploadType === 'knowledge-base') {
102106
const fileValidationError = validateFileType(fileName, contentType)
@@ -121,6 +125,21 @@ export async function POST(request: NextRequest) {
121125
}
122126
}
123127

128+
// Validate profile picture requirements
129+
if (uploadType === 'profile-pictures') {
130+
if (!sessionUserId?.trim()) {
131+
throw new ValidationError(
132+
'Authenticated user session is required for profile picture uploads'
133+
)
134+
}
135+
// Only allow image uploads for profile pictures
136+
if (!isImageFileType(contentType)) {
137+
throw new ValidationError(
138+
'Only image files (JPEG, PNG, GIF, WebP, SVG) are allowed for profile picture uploads'
139+
)
140+
}
141+
}
142+
124143
if (!isUsingCloudStorage()) {
125144
throw new StorageConfigError(
126145
'Direct uploads are only available when cloud storage is enabled'
@@ -185,7 +204,9 @@ async function handleS3PresignedUrl(
185204
? S3_CHAT_CONFIG
186205
: uploadType === 'copilot'
187206
? S3_COPILOT_CONFIG
188-
: S3_CONFIG
207+
: uploadType === 'profile-pictures'
208+
? S3_PROFILE_PICTURES_CONFIG
209+
: S3_CONFIG
189210

190211
if (!config.bucket || !config.region) {
191212
throw new StorageConfigError(`S3 configuration missing for ${uploadType} uploads`)
@@ -200,6 +221,8 @@ async function handleS3PresignedUrl(
200221
prefix = 'chat/'
201222
} else if (uploadType === 'copilot') {
202223
prefix = `${userId}/`
224+
} else if (uploadType === 'profile-pictures') {
225+
prefix = `${userId}/`
203226
}
204227

205228
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
@@ -219,6 +242,9 @@ async function handleS3PresignedUrl(
219242
} else if (uploadType === 'copilot') {
220243
metadata.purpose = 'copilot'
221244
metadata.userId = userId || ''
245+
} else if (uploadType === 'profile-pictures') {
246+
metadata.purpose = 'profile-pictures'
247+
metadata.userId = userId || ''
222248
}
223249

224250
const command = new PutObjectCommand({
@@ -239,9 +265,9 @@ async function handleS3PresignedUrl(
239265
)
240266
}
241267

242-
// For chat images and knowledge base files, use direct URLs since they need to be accessible by external services
268+
// For chat images, knowledge base files, and profile pictures, use direct URLs since they need to be accessible by external services
243269
const finalPath =
244-
uploadType === 'chat' || uploadType === 'knowledge-base'
270+
uploadType === 'chat' || uploadType === 'knowledge-base' || uploadType === 'profile-pictures'
245271
? `https://${config.bucket}.s3.${config.region}.amazonaws.com/${uniqueKey}`
246272
: `/api/files/serve/s3/${encodeURIComponent(uniqueKey)}`
247273

@@ -285,7 +311,9 @@ async function handleBlobPresignedUrl(
285311
? BLOB_CHAT_CONFIG
286312
: uploadType === 'copilot'
287313
? BLOB_COPILOT_CONFIG
288-
: BLOB_CONFIG
314+
: uploadType === 'profile-pictures'
315+
? BLOB_PROFILE_PICTURES_CONFIG
316+
: BLOB_CONFIG
289317

290318
if (
291319
!config.accountName ||
@@ -304,6 +332,8 @@ async function handleBlobPresignedUrl(
304332
prefix = 'chat/'
305333
} else if (uploadType === 'copilot') {
306334
prefix = `${userId}/`
335+
} else if (uploadType === 'profile-pictures') {
336+
prefix = `${userId}/`
307337
}
308338

309339
const uniqueKey = `${prefix}${uuidv4()}-${safeFileName}`
@@ -339,10 +369,10 @@ async function handleBlobPresignedUrl(
339369

340370
const presignedUrl = `${blockBlobClient.url}?${sasToken}`
341371

342-
// For chat images, use direct Blob URLs since they need to be permanently accessible
372+
// For chat images and profile pictures, use direct Blob URLs since they need to be permanently accessible
343373
// For other files, use serve path for access control
344374
const finalPath =
345-
uploadType === 'chat'
375+
uploadType === 'chat' || uploadType === 'profile-pictures'
346376
? blockBlobClient.url
347377
: `/api/files/serve/blob/${encodeURIComponent(uniqueKey)}`
348378

@@ -362,6 +392,9 @@ async function handleBlobPresignedUrl(
362392
} else if (uploadType === 'copilot') {
363393
uploadHeaders['x-ms-meta-purpose'] = 'copilot'
364394
uploadHeaders['x-ms-meta-userid'] = encodeURIComponent(userId || '')
395+
} else if (uploadType === 'profile-pictures') {
396+
uploadHeaders['x-ms-meta-purpose'] = 'profile-pictures'
397+
uploadHeaders['x-ms-meta-userid'] = encodeURIComponent(userId || '')
365398
}
366399

367400
return NextResponse.json({

apps/sim/app/api/users/me/profile/route.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ const logger = createLogger('UpdateUserProfileAPI')
1212
const UpdateProfileSchema = z
1313
.object({
1414
name: z.string().min(1, 'Name is required').optional(),
15+
image: z.string().url('Invalid image URL').optional(),
1516
})
16-
.refine((data) => data.name !== undefined, {
17-
message: 'Name field must be provided',
17+
.refine((data) => data.name !== undefined || data.image !== undefined, {
18+
message: 'At least one field (name or image) must be provided',
1819
})
1920

21+
interface UpdateData {
22+
updatedAt: Date
23+
name?: string
24+
image?: string | null
25+
}
26+
2027
export const dynamic = 'force-dynamic'
2128

2229
export async function PATCH(request: NextRequest) {
@@ -36,8 +43,9 @@ export async function PATCH(request: NextRequest) {
3643
const validatedData = UpdateProfileSchema.parse(body)
3744

3845
// Build update object
39-
const updateData: any = { updatedAt: new Date() }
46+
const updateData: UpdateData = { updatedAt: new Date() }
4047
if (validatedData.name !== undefined) updateData.name = validatedData.name
48+
if (validatedData.image !== undefined) updateData.image = validatedData.image
4149

4250
// Update user profile
4351
const [updatedUser] = await db

0 commit comments

Comments
 (0)