feat : generalize TaskDocumentProcessor for client tickets

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-15 19:28:04 +01:00
parent b6cfe9d7d4
commit 851953df1e
2 changed files with 35 additions and 9 deletions

View File

@@ -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,
),

View File

@@ -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);