feat: Désignation de remplaçants temporaires avec corrections sécurité
Permet aux administrateurs de désigner un enseignant remplaçant pour un autre enseignant absent, sur des classes et matières précises, pour une période donnée. Le dashboard enseignant affiche les remplacements actifs avec les noms de classes/matières au lieu des identifiants bruts. Inclut les corrections de la code review : - Requête findActiveByTenant qui excluait les remplacements en cours mais incluait les futurs (manquait start_date <= :at) - Validation tenant et rôle enseignant dans le handler de désignation pour empêcher l'affectation cross-tenant ou de non-enseignants - Validation structurée du payload classes (Assert\Collection + UUID) pour éviter les erreurs serveur sur payloads malformés - API replaced-classes enrichie avec les noms classe/matière
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Domain\Exception\TenantMismatchException;
|
||||
use App\Administration\Domain\Exception\UserNotFoundException;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Command\DesignateReplacement\DesignateReplacementCommand;
|
||||
use App\Scolarite\Application\Command\DesignateReplacement\DesignateReplacementHandler;
|
||||
use App\Scolarite\Domain\Exception\DatesRemplacementInvalidesException;
|
||||
use App\Scolarite\Domain\Exception\RemplacementSameTeacherException;
|
||||
use App\Scolarite\Domain\Exception\UtilisateurNonEnseignantException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\TeacherReplacementResource;
|
||||
use App\Scolarite\Infrastructure\Security\TeacherReplacementVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<TeacherReplacementResource, TeacherReplacementResource>
|
||||
*/
|
||||
final readonly class CreateTeacherReplacementProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DesignateReplacementHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TeacherReplacementResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): TeacherReplacementResource
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(TeacherReplacementVoter::CREATE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à créer un remplacement.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
try {
|
||||
$command = new DesignateReplacementCommand(
|
||||
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
|
||||
replacedTeacherId: $data->replacedTeacherId ?? '',
|
||||
replacementTeacherId: $data->replacementTeacherId ?? '',
|
||||
startDate: $data->startDate ?? '',
|
||||
endDate: $data->endDate ?? '',
|
||||
classes: $data->classes ?? [],
|
||||
reason: $data->reason,
|
||||
createdBy: $user->userId(),
|
||||
);
|
||||
|
||||
$replacement = ($this->handler)($command);
|
||||
|
||||
foreach ($replacement->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return TeacherReplacementResource::fromDomain($replacement);
|
||||
} catch (RemplacementSameTeacherException|DatesRemplacementInvalidesException|TenantMismatchException|UtilisateurNonEnseignantException $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
} catch (UserNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
} catch (InvalidUuidStringException $e) {
|
||||
throw new BadRequestHttpException('UUID invalide : ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Scolarite\Application\Command\EndReplacement\EndReplacementCommand;
|
||||
use App\Scolarite\Application\Command\EndReplacement\EndReplacementHandler;
|
||||
use App\Scolarite\Domain\Exception\RemplacementDejaTermineException;
|
||||
use App\Scolarite\Domain\Exception\RemplacementNotFoundException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\TeacherReplacementResource;
|
||||
use App\Scolarite\Infrastructure\Security\TeacherReplacementVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<TeacherReplacementResource, null>
|
||||
*/
|
||||
final readonly class EndTeacherReplacementProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EndReplacementHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): null
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(TeacherReplacementVoter::DELETE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à terminer un remplacement.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'];
|
||||
|
||||
try {
|
||||
($this->handler)(new EndReplacementCommand(
|
||||
replacementId: $id,
|
||||
tenantId: (string) $this->tenantContext->getCurrentTenantId(),
|
||||
));
|
||||
} catch (RemplacementNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
} catch (RemplacementDejaTermineException $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user