From 78334676d092905ce6c012ae62a0ff8117cd92a7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 5 Mar 2026 02:11:16 +0100 Subject: [PATCH] fix: avatar/logo display diagnostics and upload error handling Add error logging to silent catch blocks in avatar/logo URL generation, show user avatar on admin member detail page, and surface specific error messages for upload failures (CORS/network issues) instead of generic errors. Co-Authored-By: Claude Opus 4.6 --- src/app/(admin)/admin/members/[id]/page.tsx | 22 ++++++++++-------- src/components/shared/avatar-upload.tsx | 23 ++++++++++++------- src/components/shared/logo-upload.tsx | 23 ++++++++++++------- src/components/shared/project-logo-upload.tsx | 19 ++++++++++----- src/server/routers/user.ts | 5 ++-- src/server/utils/avatar-url.ts | 3 ++- src/server/utils/project-logo-url.ts | 3 ++- 7 files changed, 63 insertions(+), 35 deletions(-) diff --git a/src/app/(admin)/admin/members/[id]/page.tsx b/src/app/(admin)/admin/members/[id]/page.tsx index 73cd8ef..b83fc42 100644 --- a/src/app/(admin)/admin/members/[id]/page.tsx +++ b/src/app/(admin)/admin/members/[id]/page.tsx @@ -47,6 +47,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' +import { UserAvatar } from '@/components/shared/user-avatar' import { ArrowLeft, Save, @@ -196,15 +197,18 @@ export default function MemberDetailPage() {
-
-

- {user.name || 'Unnamed Member'} -

-
-

{user.email}

- - {user.status === 'NONE' ? 'Not Invited' : user.status} - +
+ +
+

+ {user.name || 'Unnamed Member'} +

+
+

{user.email}

+ + {user.status === 'NONE' ? 'Not Invited' : user.status} + +
{(user.status === 'NONE' || user.status === 'INVITED') && ( diff --git a/src/components/shared/avatar-upload.tsx b/src/components/shared/avatar-upload.tsx index 3025d0d..b6b00a8 100644 --- a/src/components/shared/avatar-upload.tsx +++ b/src/components/shared/avatar-upload.tsx @@ -139,16 +139,23 @@ export function AvatarUpload({ }) // Upload cropped blob directly to storage - const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', - body: croppedBlob, - headers: { - 'Content-Type': 'image/jpeg', - }, - }) + let uploadResponse: Response + try { + uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + body: croppedBlob, + headers: { + 'Content-Type': 'image/jpeg', + }, + }) + } catch (fetchError) { + console.error('Avatar upload network error (possible CORS issue):', fetchError) + throw new Error('Network error uploading file. The storage server may not be reachable.') + } if (!uploadResponse.ok) { - throw new Error('Failed to upload file') + console.error('Avatar upload failed:', uploadResponse.status, uploadResponse.statusText) + throw new Error(`Upload failed (${uploadResponse.status}). Please try again.`) } // Confirm upload diff --git a/src/components/shared/logo-upload.tsx b/src/components/shared/logo-upload.tsx index 450f8e7..a5f61ab 100644 --- a/src/components/shared/logo-upload.tsx +++ b/src/components/shared/logo-upload.tsx @@ -142,16 +142,23 @@ export function LogoUpload({ }) // Upload cropped blob directly to storage - const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', - body: croppedBlob, - headers: { - 'Content-Type': 'image/png', - }, - }) + let uploadResponse: Response + try { + uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + body: croppedBlob, + headers: { + 'Content-Type': 'image/png', + }, + }) + } catch (fetchError) { + console.error('Logo upload network error (possible CORS issue):', fetchError) + throw new Error('Network error uploading file. The storage server may not be reachable.') + } if (!uploadResponse.ok) { - throw new Error('Failed to upload file') + console.error('Logo upload failed:', uploadResponse.status, uploadResponse.statusText) + throw new Error(`Upload failed (${uploadResponse.status}). Please try again.`) } // Confirm upload with the provider type that was used diff --git a/src/components/shared/project-logo-upload.tsx b/src/components/shared/project-logo-upload.tsx index 218b560..fea8e35 100644 --- a/src/components/shared/project-logo-upload.tsx +++ b/src/components/shared/project-logo-upload.tsx @@ -128,14 +128,21 @@ export function ProjectLogoUpload({ contentType: 'image/png', }) - const uploadResponse = await fetch(uploadUrl, { - method: 'PUT', - body: croppedBlob, - headers: { 'Content-Type': 'image/png' }, - }) + let uploadResponse: Response + try { + uploadResponse = await fetch(uploadUrl, { + method: 'PUT', + body: croppedBlob, + headers: { 'Content-Type': 'image/png' }, + }) + } catch (fetchError) { + console.error('Logo upload network error (possible CORS issue):', fetchError) + throw new Error('Network error uploading file. The storage server may not be reachable.') + } if (!uploadResponse.ok) { - throw new Error('Failed to upload file') + console.error('Logo upload failed:', uploadResponse.status, uploadResponse.statusText) + throw new Error(`Upload failed (${uploadResponse.status}). Please try again.`) } await confirmUpload.mutateAsync({ projectId, key, providerType }) diff --git a/src/server/routers/user.ts b/src/server/routers/user.ts index e803563..c8357b5 100644 --- a/src/server/routers/user.ts +++ b/src/server/routers/user.ts @@ -5,7 +5,7 @@ import { UserRole } from '@prisma/client' import { router, protectedProcedure, adminProcedure, superAdminProcedure, publicProcedure } from '../trpc' import { sendInvitationEmail, sendMagicLinkEmail } from '@/lib/email' import { hashPassword, validatePassword } from '@/lib/password' -import { attachAvatarUrls } from '@/server/utils/avatar-url' +import { attachAvatarUrls, getUserAvatarUrl } from '@/server/utils/avatar-url' import { logAudit } from '@/server/utils/audit' import { generateInviteToken, getInviteExpiryHours, getInviteExpiryMs } from '@/server/utils/invite' @@ -353,7 +353,8 @@ export const userRouter = router({ }, }, }) - return user + const avatarUrl = await getUserAvatarUrl(user.profileImageKey, user.profileImageProvider) + return { ...user, avatarUrl } }), /** diff --git a/src/server/utils/avatar-url.ts b/src/server/utils/avatar-url.ts index 8c96ab7..2c174ab 100644 --- a/src/server/utils/avatar-url.ts +++ b/src/server/utils/avatar-url.ts @@ -14,7 +14,8 @@ export async function getUserAvatarUrl( const providerType = (profileImageProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) return await provider.getDownloadUrl(profileImageKey) - } catch { + } catch (error) { + console.error('[AvatarURL] Failed to generate URL for key:', profileImageKey, error) return null } } diff --git a/src/server/utils/project-logo-url.ts b/src/server/utils/project-logo-url.ts index ee9cdfd..3d24dd5 100644 --- a/src/server/utils/project-logo-url.ts +++ b/src/server/utils/project-logo-url.ts @@ -14,7 +14,8 @@ export async function getProjectLogoUrl( const providerType = (logoProvider as StorageProviderType) || 's3' const provider = createStorageProvider(providerType) return await provider.getDownloadUrl(logoKey) - } catch { + } catch (error) { + console.error('[LogoURL] Failed to generate URL for key:', logoKey, error) return null } }