fix: avatar/logo display diagnostics and upload error handling
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m31s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m31s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,7 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/ui/alert-dialog'
|
} from '@/components/ui/alert-dialog'
|
||||||
|
import { UserAvatar } from '@/components/shared/user-avatar'
|
||||||
import {
|
import {
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
Save,
|
Save,
|
||||||
@@ -196,15 +197,18 @@ export default function MemberDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div className="flex items-center gap-4">
|
||||||
<h1 className="text-2xl font-semibold tracking-tight">
|
<UserAvatar user={user} avatarUrl={user.avatarUrl} size="lg" />
|
||||||
{user.name || 'Unnamed Member'}
|
<div>
|
||||||
</h1>
|
<h1 className="text-2xl font-semibold tracking-tight">
|
||||||
<div className="flex items-center gap-2 mt-1">
|
{user.name || 'Unnamed Member'}
|
||||||
<p className="text-muted-foreground">{user.email}</p>
|
</h1>
|
||||||
<Badge variant={user.status === 'ACTIVE' ? 'success' : user.status === 'SUSPENDED' ? 'destructive' : 'secondary'}>
|
<div className="flex items-center gap-2 mt-1">
|
||||||
{user.status === 'NONE' ? 'Not Invited' : user.status}
|
<p className="text-muted-foreground">{user.email}</p>
|
||||||
</Badge>
|
<Badge variant={user.status === 'ACTIVE' ? 'success' : user.status === 'SUSPENDED' ? 'destructive' : 'secondary'}>
|
||||||
|
{user.status === 'NONE' ? 'Not Invited' : user.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{(user.status === 'NONE' || user.status === 'INVITED') && (
|
{(user.status === 'NONE' || user.status === 'INVITED') && (
|
||||||
|
|||||||
@@ -139,16 +139,23 @@ export function AvatarUpload({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Upload cropped blob directly to storage
|
// Upload cropped blob directly to storage
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
let uploadResponse: Response
|
||||||
method: 'PUT',
|
try {
|
||||||
body: croppedBlob,
|
uploadResponse = await fetch(uploadUrl, {
|
||||||
headers: {
|
method: 'PUT',
|
||||||
'Content-Type': 'image/jpeg',
|
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) {
|
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
|
// Confirm upload
|
||||||
|
|||||||
@@ -142,16 +142,23 @@ export function LogoUpload({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Upload cropped blob directly to storage
|
// Upload cropped blob directly to storage
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
let uploadResponse: Response
|
||||||
method: 'PUT',
|
try {
|
||||||
body: croppedBlob,
|
uploadResponse = await fetch(uploadUrl, {
|
||||||
headers: {
|
method: 'PUT',
|
||||||
'Content-Type': 'image/png',
|
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) {
|
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
|
// Confirm upload with the provider type that was used
|
||||||
|
|||||||
@@ -128,14 +128,21 @@ export function ProjectLogoUpload({
|
|||||||
contentType: 'image/png',
|
contentType: 'image/png',
|
||||||
})
|
})
|
||||||
|
|
||||||
const uploadResponse = await fetch(uploadUrl, {
|
let uploadResponse: Response
|
||||||
method: 'PUT',
|
try {
|
||||||
body: croppedBlob,
|
uploadResponse = await fetch(uploadUrl, {
|
||||||
headers: { 'Content-Type': 'image/png' },
|
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) {
|
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 })
|
await confirmUpload.mutateAsync({ projectId, key, providerType })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { UserRole } from '@prisma/client'
|
|||||||
import { router, protectedProcedure, adminProcedure, superAdminProcedure, publicProcedure } from '../trpc'
|
import { router, protectedProcedure, adminProcedure, superAdminProcedure, publicProcedure } from '../trpc'
|
||||||
import { sendInvitationEmail, sendMagicLinkEmail } from '@/lib/email'
|
import { sendInvitationEmail, sendMagicLinkEmail } from '@/lib/email'
|
||||||
import { hashPassword, validatePassword } from '@/lib/password'
|
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 { logAudit } from '@/server/utils/audit'
|
||||||
import { generateInviteToken, getInviteExpiryHours, getInviteExpiryMs } from '@/server/utils/invite'
|
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 }
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export async function getUserAvatarUrl(
|
|||||||
const providerType = (profileImageProvider as StorageProviderType) || 's3'
|
const providerType = (profileImageProvider as StorageProviderType) || 's3'
|
||||||
const provider = createStorageProvider(providerType)
|
const provider = createStorageProvider(providerType)
|
||||||
return await provider.getDownloadUrl(profileImageKey)
|
return await provider.getDownloadUrl(profileImageKey)
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('[AvatarURL] Failed to generate URL for key:', profileImageKey, error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ export async function getProjectLogoUrl(
|
|||||||
const providerType = (logoProvider as StorageProviderType) || 's3'
|
const providerType = (logoProvider as StorageProviderType) || 's3'
|
||||||
const provider = createStorageProvider(providerType)
|
const provider = createStorageProvider(providerType)
|
||||||
return await provider.getDownloadUrl(logoKey)
|
return await provider.getDownloadUrl(logoKey)
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
console.error('[LogoURL] Failed to generate URL for key:', logoKey, error)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user