Browse Source

add AddSubdomainCommand and SubdomainService, remove OnSubdomainChange

Olivier Massot 2 năm trước cách đây
mục cha
commit
becc07b16e

+ 50 - 0
src/Commands/AddSubdomainCommand.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace App\Commands;
+
+use App\Repository\Organization\OrganizationRepository;
+use App\Service\Typo3\SubdomainService;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+#[AsCommand(
+    name: 'ot:subdomain:add',
+    description: 'Add a subdomain to the given organization and make it the active one'
+)]
+class AddSubdomainCommand extends Command
+{
+    public function __construct(
+        private readonly OrganizationRepository $organizationRepository,
+        private readonly SubdomainService $subdomainService
+    ) {
+        parent::__construct();
+    }
+
+    protected function configure()
+    {
+        $this->addArgument('organization-id', InputArgument::REQUIRED, 'Id of the organization');
+        $this->addArgument('subdomain', InputArgument::REQUIRED, 'The new active subdomain');
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $organizationId = $input->getArgument('organization-id');
+        if (!is_numeric($organizationId)) {
+            throw new InvalidArgumentException('Invalid organization id : ' . $organizationId);
+        }
+        $organization = $this->organizationRepository->find((int)$organizationId);
+
+        $subdomainValue = $input->getArgument('subdomain');
+
+        $output->writeln("Setting up a new subdomain for organization " . $organizationId . " : " . $subdomainValue);
+
+        $this->subdomainService->addNewSubdomain($organization, $subdomainValue, true);
+
+        $output->writeln("New subdomain added and activated");
+        return Command::SUCCESS;
+    }
+}

+ 0 - 102
src/Service/OnChange/Organization/OnSubdomainChange.php

@@ -1,102 +0,0 @@
-<?php
-
-namespace App\Service\OnChange\Organization;
-
-use App\Entity\Organization\Organization;
-use App\Entity\Organization\Subdomain;
-use App\Message\Command\MailerCommand;
-use App\Message\Command\Typo3\Typo3UpdateCommand;
-use App\Service\Access\Utils as AccessUtils;
-use App\Service\Mailer\Model\SubdomainChangeModel;
-use App\Service\MailHub;
-use App\Service\OnChange\OnChangeContext;
-use App\Service\OnChange\OnChangeDefault;
-use App\Service\Organization\Utils as OrganizationUtils;
-use App\Service\Typo3\BindFileService;
-use App\Tests\Service\OnChange\Organization\OnSubdomainChangeTest;
-use Doctrine\ORM\EntityManagerInterface;
-use Symfony\Component\Messenger\MessageBusInterface;
-use Symfony\Bundle\SecurityBundle\Security;
-
-class OnSubdomainChange extends OnChangeDefault
-{
-    public function __construct(
-        private OrganizationUtils $organizationUtils,
-        private BindFileService $bindFileService,
-        private MessageBusInterface $messageBus,
-        private Security $security,
-        private EntityManagerInterface $em
-    ) {}
-
-    /** @noinspection PhpParameterNameChangedDuringInheritanceInspection */
-    public function validate($subdomain, OnChangeContext $context): void {
-        // Ensure we do not exceed the limit of 3 subdomains per organization
-        if (
-            $context->isPostRequest() &&
-            count($subdomain->getOrganization()->getSubdomains()) >= 3
-        ) {
-            throw new \RuntimeException('This organization has already registered 3 subdomains');
-        }
-    }
-
-    /** @noinspection PhpParameterNameChangedDuringInheritanceInspection */
-    public function beforeChange($subdomain, OnChangeContext $context): void
-    {
-        // Ensure it is the only active subdomain of this organization by disabling other organization subdomains
-        if ($subdomain->isActive()) {
-            foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
-                if ($other !== $subdomain && $other->isActive()) {
-                    $other->setActive(false);
-                }
-            }
-        }
-    }
-
-    /** @noinspection PhpParameterNameChangedDuringInheritanceInspection */
-    public function onChange($subdomain, OnChangeContext $context): void {
-        // Register into the BindFile
-        if ($context->isPostRequest()) {
-            $this->bindFileService->registerSubdomain($subdomain->getSubdomain());
-        }
-
-        // A new subdomain is active
-        // /!\ This has to be executed after everything has been persisted
-        if ($subdomain->isActive() && !($context->previousData() && $context->previousData()->isActive())) {
-            // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour
-            $this->em->refresh($subdomain->getOrganization());
-
-            // Update the typo3 website (asynchronously with messenger)
-            $this->messageBus->dispatch(
-                new Typo3UpdateCommand($subdomain->getOrganization()->getId())
-            );
-
-            // Envoi d'un email
-            $this->sendEmailAfterSubdomainChange($subdomain);
-        }
-    }
-
-    /**
-     * @param Subdomain $subdomain
-     * @see OnSubdomainChangeTest::testSendEmailAfterSubdomainChange()
-     */
-    public function sendEmailAfterSubdomainChange(Subdomain $subdomain): void
-    {
-        $this->messageBus->dispatch(
-            new MailerCommand($this->getMailModel($subdomain))
-        );
-    }
-
-    /**
-     * @param Subdomain $subdomain
-     * @return SubdomainChangeModel
-     * @see OnSubdomainChangeTest::testGetMailModel()
-     */
-    public function getMailModel(Subdomain $subdomain): SubdomainChangeModel
-    {
-        return (new SubdomainChangeModel())
-            ->setSenderId($this->security->getUser()->getId())
-            ->setOrganizationId($subdomain->getOrganization()->getId())
-            ->setSubdomainId($subdomain->getId())
-            ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization()));
-    }
-}

+ 0 - 1
src/Service/Organization/Utils.php

@@ -178,7 +178,6 @@ class Utils
      * @see https://ressources.opentalent.fr/display/SPEC/Preferences#Preferences-Siteinternet
      *
      * @param Organization $organization
-     * @param Organization $organization
      * @return mixed
      */
     public function getOrganizationWebsite(Organization $organization): mixed

+ 180 - 0
src/Service/Typo3/SubdomainService.php

@@ -0,0 +1,180 @@
+<?php
+
+namespace App\Service\Typo3;
+
+use App\Entity\Organization\Organization;
+use App\Entity\Organization\Subdomain;
+use App\Message\Command\MailerCommand;
+use App\Message\Command\Typo3\Typo3UpdateCommand;
+use App\Repository\Access\AccessRepository;
+use App\Repository\Organization\SubdomainRepository;
+use App\Service\Mailer\Model\SubdomainChangeModel;
+use App\Service\Organization\Utils as OrganizationUtils;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Messenger\MessageBusInterface;
+
+/**
+ * Service de gestion des sous-domaines des utilisateurs
+ */
+class SubdomainService
+{
+    // Max number of subdomains that an organization can own
+    const MAX_SUBDOMAINS_NUMBER = 3;
+
+    const RX_VALIDATE_SUBDOMAIN = '/^[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?$/';
+
+    public function __construct(
+        private readonly SubdomainRepository $subdomainRepository,
+        private readonly EntityManagerInterface $em,
+        private readonly MessageBusInterface $messageBus,
+        private readonly Security $security,
+        private readonly OrganizationUtils $organizationUtils,
+        private readonly BindFileService $bindFileService,
+        private readonly AccessRepository $accessRepository
+    ) {}
+
+    /**
+     * Is the organization allowed to register a new subdomain
+     *
+     * @param Organization $organization
+     * @return bool
+     */
+    public function canRegisterNewSubdomain(Organization $organization): bool {
+        return count($organization->getSubdomains()) < self::MAX_SUBDOMAINS_NUMBER;
+    }
+
+    /**
+     * Is the input a valid value for a subdomain
+     *
+     * @param string $subdomainValue
+     * @return false|int
+     */
+    public function isValidSubdomain(string $subdomainValue): bool
+    {
+        return (bool)preg_match(self::RX_VALIDATE_SUBDOMAIN, $subdomainValue);
+    }
+
+    /**
+     * Register a new subdomain for the organization
+     * Is $activate is true, makes this new subdomain the active one too.
+     *
+     * @param Organization $organization
+     * @param string $subdomainValue
+     * @param bool $activate
+     * @return Subdomain
+     */
+    public function addNewSubdomain(
+        Organization $organization,
+        string $subdomainValue,
+        bool $activate = false
+    ): Subdomain {
+
+        if (!$this->isValidSubdomain($subdomainValue)) {
+            throw new \RuntimeException("Not a valid subdomain");
+        }
+
+        if (!$this->canRegisterNewSubdomain($organization)) {
+            throw new \RuntimeException("This organization can not register new subdomains");
+        }
+
+        // Vérifie que le sous-domaine n'est pas déjà utilisé
+        if ($this->subdomainRepository->findBy(['subdomain' => $subdomainValue])) {
+            throw new InvalidArgumentException('This subdomain is already registered');
+        }
+
+        $subdomain = new Subdomain();
+        $subdomain->setSubdomain($subdomainValue);
+        $subdomain->setOrganization($organization);
+        $subdomain->setActive(false);
+
+        $this->em->persist($subdomain);
+        $this->em->flush();
+
+        // Register into the BindFile (takes up to 5min to take effect)
+        $this->bindFileService->registerSubdomain($subdomain->getSubdomain());
+
+        if ($activate) {
+            $subdomain = $this->activateSubdomain($subdomain);
+        }
+
+        return $subdomain;
+    }
+
+    /**
+     * Makes the $subdomain the active one for the organization.
+     *
+     * @param Subdomain $subdomain
+     * @return void
+     */
+    public function activateSubdomain(Subdomain $subdomain): Subdomain {
+        if ($subdomain->isActive()) {
+            throw new \RuntimeException('The subdomain is already active');
+        }
+        if (!$subdomain->getId()) {
+            throw new \RuntimeException('Can not activate a non-persisted subdomain');
+        }
+
+        foreach ($subdomain->getOrganization()->getSubdomains() as $other) {
+            if ($other !== $subdomain && $other->isActive()) {
+                $other->setActive(false);
+            }
+        }
+        $subdomain->setActive(true);
+
+        // TODO: comprendre pourquoi ce refresh est indispensable pour que l'organisation soit à jour
+        $this->em->flush();
+        $this->em->refresh($subdomain->getOrganization());
+
+        $this->renameAdminUserToMatchSubdomain($subdomain);
+
+        // Update the typo3 website (asynchronously with messenger)
+        $this->messageBus->dispatch(
+            new Typo3UpdateCommand($subdomain->getOrganization()->getId())
+        );
+
+        // Send confirmation email
+        $this->sendConfirmationEmail($subdomain);
+
+        return $subdomain;
+    }
+
+    /**
+     * Rename the admin user of the organization to match the given subdomain
+     *
+     * @param Organization $organization
+     * @return void
+     */
+    protected function renameAdminUserToMatchSubdomain(Subdomain $subdomain): void {
+        $adminAccess = $this->accessRepository->findOneBy([
+            'adminAccess' => 1,
+            'organization' => $subdomain->getOrganization()
+        ]);
+
+        $adminAccess->getPerson()->setUsername('admin' . $subdomain->getSubdomain());
+        $this->em->flush();
+    }
+
+    /**
+     * Send the confirmation email to the organization after a new subdomain has been activated
+     *
+     * @param Subdomain $subdomain
+     * @return void
+     */
+    protected function sendConfirmationEmail(Subdomain $subdomain): void {
+        // TODO: revoir quel sender par défaut
+        $senderId = $this->security->getUser() ? $this->security->getUser()->getId() : 211;
+
+        $mailModel = (new SubdomainChangeModel())
+            ->setSenderId($senderId)
+            ->setOrganizationId($subdomain->getOrganization()->getId())
+            ->setSubdomainId($subdomain->getId())
+            ->setUrl($this->organizationUtils->getOrganizationWebsite($subdomain->getOrganization()));
+
+        // Envoi d'un email
+        $this->messageBus->dispatch(
+            new MailerCommand($mailModel)
+        );
+    }
+}

+ 4 - 2
src/State/Processor/EntityProcessor.php

@@ -30,13 +30,15 @@ class EntityProcessor implements ProcessorInterface
     public function setProcessorInterface(ProcessorInterface $persistProcessor): void {$this->persistProcessor = $persistProcessor;}
 
     /**
-     * Persist l'entité et déclenche les différents hooks de la classe OnChangeInterface définie par le data persister
+     * Persiste l'entité et déclenche les différents hooks de la classe OnChangeInterface définie par le data persister
      *
      * @param mixed $data
+     * @param Operation $operation
+     * @param array $uriVariables
      * @param array $context
      * @return object
      */
-    public function process($data, Operation $operation, array $uriVariables = [], array $context = [])
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object
     {
         if($operation instanceof Delete){
             throw new \RuntimeException('not supported', 500);

+ 44 - 8
src/State/Processor/Organization/SubdomainProcessor.php

@@ -3,19 +3,55 @@ declare(strict_types=1);
 
 namespace App\State\Processor\Organization;
 
-use App\State\Processor\EntityProcessor;
-use App\Service\OnChange\Organization\OnSubdomainChange;
-use JetBrains\PhpStorm\Pure;
+use ApiPlatform\Metadata\Delete;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\Metadata\Put;
+use ApiPlatform\State\ProcessorInterface;
+use App\Entity\Organization\Subdomain;
+use App\Repository\Organization\SubdomainRepository;
+use App\Service\Typo3\SubdomainService;
+use Doctrine\ORM\EntityManagerInterface;
 
 /**
  * Custom Processor gérant la resource Subdomain
  */
-class SubdomainProcessor extends EntityProcessor
+class SubdomainProcessor implements ProcessorInterface
 {
-    #[Pure]
     public function __construct(
-        OnSubdomainChange $onChange
-    ) {
-        parent::__construct($onChange);
+        private readonly SubdomainService $subdomainService,
+        private EntityManagerInterface $entityManager,
+    ) {}
+
+    /**
+     * Persiste l'entité et déclenche les différents hooks de la classe OnChangeInterface définie par le data persister
+     *
+     * @param Subdomain $data
+     * @param Operation $operation
+     * @param array $uriVariables
+     * @param array $context
+     * @return object
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) {
+        if($operation instanceof Delete){
+            throw new \RuntimeException('not supported', 500);
+        }
+
+        if ($operation instanceof Post) {
+            // Create a new subdomain
+            $subdomain = $this->subdomainService->addNewSubdomain(
+                $data->getOrganization(),
+                $data->getSubdomain(),
+                $data->isActive()
+            );
+        } else if ($operation instanceof Put && $data->isActive()) {
+            // Activate a subdomain
+            $data->setActive(false); // On triche : c'est le service qui va activer ce sous-domaine, pas le processor
+            $subdomain = $this->subdomainService->activateSubdomain($data);
+        } else {
+            throw new \RuntimeException('not supported', 500);
+        }
+
+        return $subdomain;
     }
 }