Fix auth for all file thumbnails, add upload error logging

All image thumbnails in ticket photos and note attachments now use
AuthImage component that fetches via Bearer token. Fixed 401 errors
on Photos & Docs tab. Added error logging for note photo uploads to
surface upload failures.
This commit is contained in:
Ryan Moon
2026-03-29 11:19:39 -05:00
parent d936871a49
commit e224b535ce
2 changed files with 33 additions and 4 deletions

View File

@@ -116,11 +116,16 @@ export function TicketNotes({ ticketId }: TicketNotesProps) {
formData.append('entityType', 'repair_note')
formData.append('entityId', note.id)
formData.append('category', 'attachment')
await fetch('/v1/files', {
const uploadRes = await fetch('/v1/files', {
method: 'POST',
headers: { Authorization: `Bearer ${token}` },
body: formData,
})
if (!uploadRes.ok) {
const err = await uploadRes.json().catch(() => ({}))
console.error('Photo upload failed:', err)
toast.error(`Photo upload failed: ${(err as any).error?.message ?? 'Unknown error'}`)
}
}
queryClient.invalidateQueries({ queryKey: repairNoteKeys.all(ticketId) })

View File

@@ -1,4 +1,4 @@
import { useRef } from 'react'
import { useRef, useState, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { queryOptions } from '@tanstack/react-query'
import { api } from '@/lib/api-client'
@@ -7,6 +7,30 @@ import { Button } from '@/components/ui/button'
import { FileText, ImageIcon, Plus, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
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} />
}
async function openSignedFile(fileId: string) {
try {
const res = await api.get<{ url: string }>(`/v1/files/signed-url/${fileId}`)
@@ -164,8 +188,8 @@ function PhotoSection({
</button>
) : (
<button type="button" onClick={() => openSignedFile(photo.id)}>
<img
src={`/v1/files/serve/${photo.path}`}
<AuthImage
path={photo.path}
alt={photo.filename}
className="h-20 w-20 object-cover rounded-md border cursor-pointer hover:opacity-80 transition-opacity"
/>