Files
pc_builder/app/services/picture_service.ts
T
Kevin L'Hereec 3cc189c534 fix(security): close IDOR on computers/components update & destroy
- Scope update/destroy queries by userId (Computers & Components), matching
  the pattern already used in index/show/edit
- Add missing auth.authenticate() calls on destroy actions
- handleComponents now requires userId and scopes component lookups to the
  current user, preventing cross-user component takeover
- handleComponents rejects attaching more than one component of the same
  'unique' type to a computer (new InvalidComponentSelectionException, 422)
- handlePictures scopes picture lookups by computerId
- destroy(Computer) now detaches its components (computerId = null) instead
  of losing them to cascade delete, and removes the computer's uploads dir
- Switch store/update actions to request.validateUsing() for consistency
- Fix relative redirects to absolute paths (/computers, /computers/:id)
- Untrack .env.production and add it to .gitignore (secrets must not be
  committed); local file kept on disk
2026-07-23 10:53:13 +02:00

64 lines
2.0 KiB
TypeScript

import type { HttpContext } from '@adonisjs/core/http'
import ComputerPicture from '#models/computer_picture'
import Computer from '#models/computer'
import { v4 as uuid } from 'uuid'
import type { Image } from '#types/UItypes'
import { existsSync, unlinkSync } from 'node:fs'
async function handlePictures(
pictures: Image[],
request: HttpContext['request'],
computer: Computer
) {
const currentPictures = await computer.related('pictures').query()
const currentPictureIds = currentPictures.map((picture) => picture.id)
const imagesToRemove = currentPictureIds.filter(
(id: number) => !pictures.some((picture) => 'id' in picture && picture.id === id)
)
await Promise.all(
pictures.map(async (picture: Image, index: number) => {
if ('id' in picture) {
const existingPicture = await ComputerPicture.query()
.where('computerId', computer.id)
.andWhere('id', picture.id)
.firstOrFail()
existingPicture.order = picture.order
await existingPicture.save()
} else {
const image = request.file(`pictures[${index}][file]`, {
size: '2mb',
extnames: ['jpg', 'png', 'jpeg', 'svg'],
})
if (image) {
const imageName = `img_${uuid()}.${image.extname}`
await image.move(`uploads/computer_${computer.id}`, {
name: imageName,
})
await ComputerPicture.create({
computerId: computer.id,
link: `/uploads/computer_${computer.id}/${imageName}`,
order: picture.order,
})
}
}
})
)
await Promise.all(
imagesToRemove.map(async (id: number) => {
const picture = await ComputerPicture.query()
.where('computerId', computer.id)
.andWhere('id', id)
.firstOrFail()
const filePath = `uploads/computer_${computer.id}/${picture.link.split('/').pop()}`
if (existsSync(filePath)) {
unlinkSync(filePath)
}
await picture.delete()
})
)
}
export { handlePictures }