Quellcode durchsuchen

Adds HelloAsso account unlinking feature

Adds functionality allowing users to unlink their HelloAsso accounts from Opentalent.

This change introduces a new API resource for unlinking,
and a corresponding processor to handle the logic.
It also updates the HelloAsso entity to allow for null values.
Olivier Massot vor 2 Monaten
Ursprung
Commit
974813a0a9

+ 1 - 0
config/opentalent/products.yaml

@@ -36,6 +36,7 @@ parameters:
           - DolibarrDocDownload
           - AuthUrl
           - ConnectionRequest
+          - UnlinkRequest
           - HelloAssoProfile
         roles:
           - ROLE_IMPORT

+ 51 - 0
src/ApiResources/HelloAsso/UnlinkRequest.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\HelloAsso;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Post;
+use App\ApiResources\ApiResourcesInterface;
+use App\State\Processor\HelloAsso\UnlinkRequestProcessor;
+
+/**
+ * Demande d'une organisation de dissocier son compte Opentalent de son compte HelloAsso.
+ */
+#[ApiResource(
+    operations: [
+        new Post(
+            uriTemplate: '/helloasso/unlink',
+        ),
+    ],
+    processor: UnlinkRequestProcessor::class,
+)]
+class UnlinkRequest implements ApiResourcesInterface
+{
+    /**
+     * Id 'bidon' ajouté par défaut pour permettre la construction
+     * de l'IRI par api platform.
+     */
+    #[ApiProperty(identifier: true)]
+    private int $id = 1;
+
+    private int $organizationId;
+
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    public function getOrganizationId(): int
+    {
+        return $this->organizationId;
+    }
+
+    public function setOrganizationId(int $organizationId): self
+    {
+        $this->organizationId = $organizationId;
+
+        return $this;
+    }
+}

+ 38 - 3
src/Entity/Organization/HelloAsso.php → src/Entity/HelloAsso/HelloAsso.php

@@ -1,7 +1,8 @@
 <?php
 
-namespace App\Entity\Organization;
+namespace App\Entity\HelloAsso;
 
+use App\Entity\Organization\Organization;
 use App\Entity\Traits\CreatedOnAndByTrait;
 use Doctrine\ORM\Mapping as ORM;
 
@@ -14,8 +15,6 @@ use Doctrine\ORM\Mapping as ORM;
 #[ORM\Table]
 class HelloAsso
 {
-    use CreatedOnAndByTrait;
-
     #[ORM\Id]
     #[ORM\Column]
     #[ORM\GeneratedValue]
@@ -39,6 +38,13 @@ class HelloAsso
     #[ORM\Column(type: 'text', nullable: true)]
     private ?string $token = null;
 
+    /**
+     * Date à laquelle le token a été généré
+     * @var \DateTimeInterface|null
+     */
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private $tokenCreatedAt;
+
     /**
      * Un autre jeton, valable 30jours, permettant de regénérer le token d'authentification HelloAsso.'
      * @var string|null
@@ -46,6 +52,13 @@ class HelloAsso
     #[ORM\Column(type: 'text', nullable: true)]
     private ?string $refreshToken = null;
 
+    /**
+     * Date à laquelle le refreshToken a été généré
+     * @var \DateTimeInterface|null
+     */
+    #[ORM\Column(type: 'datetime', nullable: true)]
+    private $refreshTokenCreatedAt;
+
     /**
      * Le slug de l'organisation sur HelloAsso.
      * @var string|null
@@ -100,6 +113,17 @@ class HelloAsso
         return $this;
     }
 
+    public function getTokenCreatedAt(): ?\DateTimeInterface
+    {
+        return $this->tokenCreatedAt;
+    }
+
+    public function setTokenCreatedAt(?\DateTimeInterface $tokenCreatedAt): self
+    {
+        $this->tokenCreatedAt = $tokenCreatedAt;
+        return $this;
+    }
+
     public function getRefreshToken(): ?string
     {
         return $this->refreshToken;
@@ -112,6 +136,17 @@ class HelloAsso
         return $this;
     }
 
+    public function getRefreshTokenCreatedAt(): ?\DateTimeInterface
+    {
+        return $this->refreshTokenCreatedAt;
+    }
+
+    public function setRefreshTokenCreatedAt(?\DateTimeInterface $refreshTokenCreatedAt): self
+    {
+        $this->refreshTokenCreatedAt = $refreshTokenCreatedAt;
+        return $this;
+    }
+
     public function getOrganizationSlug(): ?string
     {
         return $this->organizationSlug;

+ 3 - 1
src/Entity/Organization/Organization.php

@@ -31,6 +31,7 @@ use App\Entity\Education\EducationCategory;
 use App\Entity\Education\EducationNotationConfig;
 use App\Entity\Education\EducationTiming;
 use App\Entity\Education\PeriodNotation;
+use App\Entity\HelloAsso\HelloAsso;
 use App\Entity\Message\AbstractMessage;
 use App\Entity\Message\AbstractReport;
 use App\Entity\Message\Email;
@@ -53,13 +54,14 @@ use App\Enum\Organization\SchoolCategoryEnum;
 use App\Enum\Organization\TypeEstablishmentDetailEnum;
 use App\Enum\Organization\TypeEstablishmentEnum;
 use App\Repository\Organization\OrganizationRepository;
-// use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
 use App\State\Processor\Organization\OrganizationProcessor;
 use Doctrine\Common\Collections\ArrayCollection;
 use Doctrine\Common\Collections\Collection;
 use Doctrine\ORM\Mapping as ORM;
 use JetBrains\PhpStorm\Pure;
 
+// use DH\Auditor\Provider\Doctrine\Auditing\Annotation\Auditable;
+
 /**
  * Structure, organisation.
  *

+ 26 - 1
src/Service/HelloAsso/ConnectionService.php

@@ -6,10 +6,11 @@ namespace App\Service\HelloAsso;
 
 use App\ApiResources\HelloAsso\AuthUrl;
 use App\ApiResources\HelloAsso\HelloAssoProfile;
-use App\Entity\Organization\HelloAsso;
+use App\Entity\HelloAsso\HelloAsso;
 use App\Entity\Organization\Organization;
 use App\Service\Rest\ApiRequestService;
 use App\Service\Security\OAuthPkceGenerator;
+use App\Service\Utils\DatesUtils;
 use App\Service\Utils\UrlBuilder;
 use Doctrine\ORM\EntityManagerInterface;
 use Psr\Log\LoggerInterface;
@@ -131,7 +132,9 @@ class ConnectionService extends ApiRequestService
         }
 
         $helloAssoEntity->setToken($tokens['access_token']);
+        $helloAssoEntity->setTokenCreatedAt(DatesUtils::new());
         $helloAssoEntity->setRefreshToken($tokens['refresh_token']);
+        $helloAssoEntity->setRefreshTokenCreatedAt(DatesUtils::new());
         $helloAssoEntity->setOrganizationSlug($tokens['organization_slug'] ?? null);
         $helloAssoEntity->setChallengeVerifier(null);
 
@@ -168,6 +171,28 @@ class ConnectionService extends ApiRequestService
         return $profile;
     }
 
+    public function unlinkHelloAssoAccount(int $organizationId): void
+    {
+        $organization = $this->entityManager->getRepository(Organization::class)->find($organizationId);
+        if (!$organization) {
+            throw new \RuntimeException('Organization not found');
+        }
+
+        $helloAssoEntity = $organization->getHelloAsso();
+        if (!$helloAssoEntity) {
+            return;
+        }
+
+        $helloAssoEntity->setToken(null);
+        $helloAssoEntity->setTokenCreatedAt(null);
+        $helloAssoEntity->setRefreshToken(null);
+        $helloAssoEntity->setRefreshTokenCreatedAt(null);
+        $helloAssoEntity->setOrganizationSlug(null);
+
+        $this->entityManager->persist($helloAssoEntity);
+        $this->entityManager->flush();
+    }
+
     /**
      * Génère l'URL de rappel pour les callbacks suite à l'authentification HelloAsso
      *

+ 0 - 3
src/State/Processor/HelloAsso/ConnectionRequestProcessor.php

@@ -8,14 +8,11 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\Metadata\Post;
 use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\HelloAsso\ConnectionRequest;
-use App\ApiResources\HelloAsso\HelloAssoProfile;
 use App\Entity\Access\Access;
-use App\Entity\Organization\HelloAsso;
 use App\Service\HelloAsso\ConnectionService;
 use App\Service\MercureHub;
 use http\Client\Response;
 use Symfony\Bundle\SecurityBundle\Security;
-use Symfony\Component\HttpFoundation\RedirectResponse;
 
 /**
  * Processor pour la ressource ConnectionRequest.

+ 58 - 0
src/State/Processor/HelloAsso/UnlinkRequestProcessor.php

@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Processor\HelloAsso;
+
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\Metadata\Post;
+use ApiPlatform\State\ProcessorInterface;
+use App\ApiResources\HelloAsso\UnlinkRequest;
+use App\Entity\Access\Access;
+use App\Service\HelloAsso\ConnectionService;
+use App\Service\MercureHub;
+use http\Client\Response;
+use Symfony\Bundle\SecurityBundle\Security;
+
+/**
+ * Processor pour la ressource ConnectionRequest.
+ */
+class UnlinkRequestProcessor implements ProcessorInterface
+{
+    public function __construct(
+        private readonly ConnectionService $connectionService,
+        private Security $security,
+        private MercureHub $mercureHub,
+    ) {
+    }
+
+    /**
+     * @param UnlinkRequest $data
+     * @param mixed[]           $uriVariables
+     * @param mixed[]           $context
+     *
+     * @throws \Exception
+     */
+    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): UnlinkRequest
+    {
+        /**
+         * @var UnlinkRequest $unlinkRequest
+         */
+        $unlinkRequest = $data;
+
+        if (!$operation instanceof Post) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+
+        /** @var Access $access */
+        $access = $this->security->getUser();
+
+        if ($unlinkRequest->getOrganizationId() !== $access->getOrganization()->getId()) {
+            throw new \RuntimeException('Forbidden: ' . $unlinkRequest->getOrganizationId() . ' !== ' . $access->getOrganization()->getId());
+        }
+
+        $this->connectionService->unlinkHelloAssoAccount($unlinkRequest->getOrganizationId());
+
+        return $unlinkRequest;
+    }
+}