diff --git a/src/Service/Project/ProjectService.php b/src/Service/Project/ProjectService.php new file mode 100644 index 0000000..375c4d7 --- /dev/null +++ b/src/Service/Project/ProjectService.php @@ -0,0 +1,50 @@ +value => [ProjectStatus::ToCampaignReview], + ProjectStatus::InCampaignReviewToChange->value => [ProjectStatus::InCampaignReviewToReview], + ProjectStatus::ToCampaign->value => [ProjectStatus::InCampaign], + ProjectStatus::InFundingReviewToChange->value => [ProjectStatus::InFundingReviewToReview], + ProjectStatus::ToFunding->value => [ProjectStatus::InFunding], + ]; + + /** + * Check if an User can move a Project to some status. + * + * @param User $actor The User that is to apply the status change + * @param Project $project The Project in their current (pre-change) state + * @param ProjectStatus $to The ProjectStatus that the actor wishes to move the Project to + */ + public function canTransition(User $actor, Project $project, ProjectStatus $to): bool + { + if ($actor->hasRoles(['ROLE_ADMIN'])) { + return true; + } + + if (!$this->isOwnerOf($project, $actor)) { + return false; + } + + $from = $project->getStatus(); + + if ( + array_key_exists($from->value, self::OWNER_VALID_TRANSITIONS) + && in_array($to, self::OWNER_VALID_TRANSITIONS[$from->value]) + ) { + return true; + } + + return false; + } +} diff --git a/src/State/Project/ProjectStateProcessor.php b/src/State/Project/ProjectStateProcessor.php index 53cd38b..3273b4b 100644 --- a/src/State/Project/ProjectStateProcessor.php +++ b/src/State/Project/ProjectStateProcessor.php @@ -2,6 +2,7 @@ namespace App\State\Project; +use ApiPlatform\Metadata\Exception\AccessDeniedException; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; use App\ApiResource\Project\ProjectApiResource; @@ -10,6 +11,7 @@ use App\Dto\ProjectUpdationDto; use App\Entity\Project\Project; use App\Entity\Project\ProjectCalendar; use App\Mapping\AutoMapper; +use App\Service\Project\ProjectService; use App\State\EntityStateProcessor; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -20,22 +22,20 @@ class ProjectStateProcessor implements ProcessorInterface private EntityStateProcessor $entityStateProcessor, private AutoMapper $autoMapper, private Security $security, + private ProjectService $projectService, ) {} /** * @param ProjectCreationDto|ProjectUpdationDto $data - * @param array{id: int} $uriVariables - * - * @return ProjectApiResource|null */ public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { if ($data instanceof ProjectCreationDto) { $project = $this->getProjectFromCreation($data); - } - - if ($data instanceof ProjectUpdationDto) { - $project = $this->getProjectFromUpdate($data, $uriVariables); + } elseif ($data instanceof ProjectUpdationDto) { + $project = $this->getProjectFromUpdate($data, $context); + } else { + throw new \InvalidArgumentException('Unsupported input for Project resource'); } $project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context); @@ -72,13 +72,27 @@ class ProjectStateProcessor implements ProcessorInterface return $project; } - private function getProjectFromUpdate(ProjectUpdationDto $data, array $uriVariables): Project + private function getProjectFromUpdate(ProjectUpdationDto $data, array $context): Project { - $data = $this->autoMapper->map($uriVariables, $data); - /** @var Project */ - $project = $this->autoMapper->map($data, Project::class); + $project = $this->autoMapper->map($context['previous_data'], Project::class); - return $project; + $actor = $this->security->getUser(); + + if (!$actor) { + throw new AuthenticationException(); + } + + if ($data->status !== $project->getStatus()) { + if (!$this->projectService->canTransition($actor, $project, $data->status)) { + throw new AccessDeniedException(\sprintf( + "Cannot move the Project from status '%s' to '%s'", + $project->getStatus()->value, + $data->status->value + )); + } + } + + return $this->autoMapper->map($data, $project); } }