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:
Ryan Moon
2026-03-29 12:16:17 -05:00
parent 21ef7e7059
commit 72d0ff0a33
7 changed files with 89 additions and 24 deletions

View File

@@ -143,6 +143,10 @@ export const fileRoutes: FastifyPluginAsync = async (app) => {
if (payload.purpose !== 'file-access' || payload.path !== filePath) {
return reply.status(403).send({ error: { message: 'Invalid token', statusCode: 403 } })
}
// Validate company isolation — file path must start with the token's companyId
if (payload.companyId && !filePath.startsWith(payload.companyId)) {
return reply.status(403).send({ error: { message: 'Access denied', statusCode: 403 } })
}
} catch {
return reply.status(403).send({ error: { message: 'Token expired or invalid', statusCode: 403 } })
}

View File

@@ -109,14 +109,14 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
if (!parsed.success) {
return reply.status(400).send({ error: { message: 'Validation failed', details: parsed.error.flatten(), statusCode: 400 } })
}
const item = await RepairLineItemService.update(app.db, id, parsed.data)
const item = await RepairLineItemService.update(app.db, request.companyId, id, parsed.data)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
app.delete('/repair-line-items/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const item = await RepairLineItemService.delete(app.db, id)
const item = await RepairLineItemService.delete(app.db, request.companyId, id)
if (!item) return reply.status(404).send({ error: { message: 'Line item not found', statusCode: 404 } })
return reply.send(item)
})
@@ -210,13 +210,14 @@ export const repairRoutes: FastifyPluginAsync = async (app) => {
app.get('/repair-tickets/:ticketId/notes', { preHandler: [app.authenticate, app.requirePermission('repairs.view')] }, async (request, reply) => {
const { ticketId } = request.params as { ticketId: string }
const notes = await RepairNoteService.listByTicket(app.db, ticketId)
return reply.send({ data: notes })
const params = PaginationSchema.parse(request.query)
const result = await RepairNoteService.listByTicket(app.db, ticketId, params)
return reply.send(result)
})
app.delete('/repair-notes/:id', { preHandler: [app.authenticate, app.requirePermission('repairs.admin')] }, async (request, reply) => {
const { id } = request.params as { id: string }
const note = await RepairNoteService.delete(app.db, id)
const note = await RepairNoteService.delete(app.db, request.companyId, id)
if (!note) return reply.status(404).send({ error: { message: 'Note not found', statusCode: 404 } })
return reply.send(note)
})