Fix note photos not displaying by using authenticated image fetching

Note photo thumbnails were failing because img src pointed at an
authenticated endpoint without auth headers. Added AuthImage component
that fetches images via Bearer token and renders as blob URLs. Photos
now display inline in note entries. Clicking still opens via signed URL.
This commit is contained in:
Ryan Moon
2026-03-29 11:10:05 -05:00
parent cf98c305df
commit d936871a49

View File

@@ -9,6 +9,7 @@ import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Send, Trash2, Eye, Lock, ImageIcon, X } from 'lucide-react' import { Send, Trash2, Eye, Lock, ImageIcon, X } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import { useEffect } from 'react'
import type { RepairNote } from '@/types/repair' import type { RepairNote } from '@/types/repair'
const STATUS_LABELS: Record<string, string> = { const STATUS_LABELS: Record<string, string> = {
@@ -48,6 +49,31 @@ async function openSignedFile(fileId: string) {
} }
} }
/** Image component that fetches via authenticated request and displays as blob */
function AuthImage({ path, alt, className, onClick }: { path: string; alt: string; className?: string; onClick?: () => void }) {
const token = useAuthStore((s) => s.token)
const [src, setSrc] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
async function load() {
try {
const res = await fetch(`/v1/files/serve/${path}`, {
headers: token ? { Authorization: `Bearer ${token}` } : {},
})
if (!res.ok || cancelled) return
const blob = await res.blob()
if (!cancelled) setSrc(URL.createObjectURL(blob))
} catch { /* ignore */ }
}
load()
return () => { cancelled = true }
}, [path, token])
if (!src) return <div className={`${className} bg-muted animate-pulse`} />
return <img src={src} alt={alt} className={className} onClick={onClick} />
}
interface TicketNotesProps { interface TicketNotesProps {
ticketId: string ticketId: string
} }
@@ -263,8 +289,8 @@ function NoteEntry({ note, formatDate, canDelete, onDelete }: {
<div className="flex flex-wrap gap-2 mt-2"> <div className="flex flex-wrap gap-2 mt-2">
{photos.map((photo) => ( {photos.map((photo) => (
<button key={photo.id} type="button" onClick={() => openSignedFile(photo.id)}> <button key={photo.id} type="button" onClick={() => openSignedFile(photo.id)}>
<img <AuthImage
src={`/v1/files/serve/${photo.path}`} path={photo.path}
alt={photo.filename} alt={photo.filename}
className="h-24 w-24 object-cover rounded-md border cursor-pointer hover:opacity-80 transition-opacity" className="h-24 w-24 object-cover rounded-md border cursor-pointer hover:opacity-80 transition-opacity"
/> />