From 851953df1e746a4a412dbea76ff427c41a1ca811 Mon Sep 17 00:00:00 2001 From: matthieu Date: Sun, 15 Mar 2026 19:28:04 +0100 Subject: [PATCH] feat : generalize TaskDocumentProcessor for client tickets Co-Authored-By: Claude Sonnet 4.6 --- src/Entity/TaskDocument.php | 2 +- src/State/TaskDocumentProcessor.php | 42 +++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/src/Entity/TaskDocument.php b/src/Entity/TaskDocument.php index df03cbd..b11364a 100644 --- a/src/Entity/TaskDocument.php +++ b/src/Entity/TaskDocument.php @@ -22,7 +22,7 @@ use Symfony\Component\Serializer\Attribute\Groups; new GetCollection(paginationEnabled: false, security: "is_granted('ROLE_USER')"), new Get(security: "is_granted('ROLE_USER')"), new Post( - security: "is_granted('ROLE_ADMIN')", + security: "is_granted('ROLE_ADMIN') or is_granted('ROLE_CLIENT')", processor: TaskDocumentProcessor::class, deserialize: false, ), diff --git a/src/State/TaskDocumentProcessor.php b/src/State/TaskDocumentProcessor.php index 3ed8ed6..aa98f63 100644 --- a/src/State/TaskDocumentProcessor.php +++ b/src/State/TaskDocumentProcessor.php @@ -6,12 +6,14 @@ namespace App\State; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use App\Entity\ClientTicket; use App\Entity\Task; use App\Entity\TaskDocument; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Uid\Uuid; @@ -50,18 +52,41 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface throw new BadRequestHttpException('File size exceeds 50 MB limit.'); } - $taskIri = $request->request->get('task'); + $taskIri = $request->request->get('task'); + $clientTicketIri = $request->request->get('clientTicket'); - if (null === $taskIri || '' === $taskIri) { - throw new BadRequestHttpException('Task IRI is required.'); + if ((null === $taskIri || '' === $taskIri) && (null === $clientTicketIri || '' === $clientTicketIri)) { + throw new BadRequestHttpException('Either task or clientTicket IRI is required.'); } - // Extract task ID from IRI (e.g., "/api/tasks/42" -> 42) - $taskId = (int) basename((string) $taskIri); - $task = $this->entityManager->getRepository(Task::class)->find($taskId); + $task = null; + $clientTicket = null; - if (null === $task) { - throw new BadRequestHttpException('Task not found.'); + if (null !== $taskIri && '' !== $taskIri) { + // Extract task ID from IRI (e.g., "/api/tasks/42" -> 42) + $taskId = (int) basename((string) $taskIri); + $task = $this->entityManager->getRepository(Task::class)->find($taskId); + + if (null === $task) { + throw new BadRequestHttpException('Task not found.'); + } + } + + if (null !== $clientTicketIri && '' !== $clientTicketIri) { + $clientTicketId = (int) basename((string) $clientTicketIri); + $clientTicket = $this->entityManager->getRepository(ClientTicket::class)->find($clientTicketId); + + if (null === $clientTicket) { + throw new BadRequestHttpException('Client ticket not found.'); + } + + // Ownership validation for ROLE_CLIENT + if (!$this->security->isGranted('ROLE_ADMIN')) { + $currentUser = $this->security->getUser(); + if ($clientTicket->getSubmittedBy() !== $currentUser) { + throw new AccessDeniedHttpException('You can only upload documents to your own tickets.'); + } + } } // Capture file metadata BEFORE move() — move invalidates the temp file @@ -80,6 +105,7 @@ final readonly class TaskDocumentProcessor implements ProcessorInterface $document = new TaskDocument(); $document->setTask($task); + $document->setClientTicket($clientTicket); $document->setOriginalName($originalName); $document->setFileName($fileName); $document->setMimeType($mimeType);