Fix security and quality issues from code review
Critical: Add company scoping to line item update/delete and note delete via ownership verification through ticket join. Add companyId validation to signed URL file serving. High: Paginate notes list endpoint with search and sort support. Fix blob URL memory leaks in AuthImage components with proper cleanup on unmount. Improve photo upload error handling — count failures and show specific error count instead of silently clearing form.
This commit is contained in:
@@ -56,6 +56,7 @@ function AuthImage({ path, alt, className, onClick }: { path: string; alt: strin
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let blobUrl: string | null = null
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(`/v1/files/serve/${path}`, {
|
||||
@@ -63,11 +64,17 @@ function AuthImage({ path, alt, className, onClick }: { path: string; alt: strin
|
||||
})
|
||||
if (!res.ok || cancelled) return
|
||||
const blob = await res.blob()
|
||||
if (!cancelled) setSrc(URL.createObjectURL(blob))
|
||||
if (!cancelled) {
|
||||
blobUrl = URL.createObjectURL(blob)
|
||||
setSrc(blobUrl)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}, [path, token])
|
||||
|
||||
if (!src) return <div className={`${className} bg-muted animate-pulse`} />
|
||||
@@ -110,6 +117,7 @@ export function TicketNotes({ ticketId }: TicketNotesProps) {
|
||||
const note = await repairNoteMutations.create(ticketId, { content: content.trim(), visibility })
|
||||
|
||||
// Upload attached photos to the note
|
||||
let uploadFailures = 0
|
||||
for (const photo of photos) {
|
||||
const formData = new FormData()
|
||||
formData.append('entityType', 'repair_note')
|
||||
@@ -122,16 +130,20 @@ export function TicketNotes({ ticketId }: TicketNotesProps) {
|
||||
body: formData,
|
||||
})
|
||||
if (!uploadRes.ok) {
|
||||
uploadFailures++
|
||||
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) })
|
||||
setContent('')
|
||||
setPhotos([])
|
||||
toast.success('Note added')
|
||||
if (uploadFailures > 0) {
|
||||
toast.error(`Note added but ${uploadFailures} photo(s) failed to upload`)
|
||||
} else {
|
||||
toast.success(photos.length > 0 ? `Note added with ${photos.length} photo(s)` : 'Note added')
|
||||
}
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to post note')
|
||||
} finally {
|
||||
|
||||
@@ -13,6 +13,7 @@ function AuthImage({ path, alt, className, onClick }: { path: string; alt: strin
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
let blobUrl: string | null = null
|
||||
async function load() {
|
||||
try {
|
||||
const res = await fetch(`/v1/files/serve/${path}`, {
|
||||
@@ -20,11 +21,17 @@ function AuthImage({ path, alt, className, onClick }: { path: string; alt: strin
|
||||
})
|
||||
if (!res.ok || cancelled) return
|
||||
const blob = await res.blob()
|
||||
if (!cancelled) setSrc(URL.createObjectURL(blob))
|
||||
if (!cancelled) {
|
||||
blobUrl = URL.createObjectURL(blob)
|
||||
setSrc(blobUrl)
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
load()
|
||||
return () => { cancelled = true }
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (blobUrl) URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
}, [path, token])
|
||||
|
||||
if (!src) return <div className={`${className} bg-muted animate-pulse`} />
|
||||
|
||||
Reference in New Issue
Block a user