Explorar el Código

Adds HelloAsso organization forms API resource

This commit introduces a new API resource to fetch HelloAsso organization forms.

It also renames `ConnectionService` to `HelloAssoService` for better clarity and reflects that it is not only about connection management.
The changes include adding a new `OrganizationForm` API resource, a provider, and updating the service to fetch and refresh access tokens.
Additionally, the resource is added to the `products.yaml` configuration.
Olivier Massot hace 2 meses
padre
commit
3925b2f4a7

+ 1 - 0
config/opentalent/products.yaml

@@ -38,6 +38,7 @@ parameters:
           - ConnectionRequest
           - UnlinkRequest
           - HelloAssoProfile
+          - OrganizationForm
         roles:
           - ROLE_IMPORT
           - ROLE_TAGG

+ 98 - 0
src/ApiResources/HelloAsso/OrganizationForm.php

@@ -0,0 +1,98 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\HelloAsso;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use ApiPlatform\Metadata\GetCollection;
+use App\State\Provider\HelloAsso\AuthUrlProvider;
+use App\State\Provider\HelloAsso\HelloAssoProfileProvider;
+use App\State\Provider\HelloAsso\OrganizationFormProvider;
+
+/**
+ * Retourne les formulaires d'une organisation.
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/helloasso/form/{id}',
+        ),
+        new GetCollection(
+            uriTemplate: '/helloasso/forms',
+        ),
+    ],
+    provider: OrganizationFormProvider::class,
+    security: 'is_granted("ROLE_ORGANIZATION")'
+)]
+class OrganizationForm
+{
+    /**
+     * Id 'bidon' ajouté par défaut pour permettre la construction
+     * de l'IRI par api platform.
+     */
+    #[ApiProperty(identifier: true)]
+    private int $id = 1;
+
+    /**
+     * Slug du formulaire
+     */
+    private string $slug;
+
+    /**
+     * Titre du formulaire
+     * @var bool
+     */
+    private string $title;
+
+    /**
+     * Url du formulaire
+     */
+    private string $url;
+
+    public function getId(): int
+    {
+        return $this->id;
+    }
+
+    public function setId(int $id): self
+    {
+        $this->id = $id;
+        return $this;
+    }
+
+    public function getSlug(): string
+    {
+        return $this->slug;
+    }
+
+    public function setSlug(string $slug): self
+    {
+        $this->slug = $slug;
+        return $this;
+    }
+
+    public function getTitle(): string
+    {
+        return $this->title;
+    }
+
+    public function setTitle(string $title): self
+    {
+        $this->title = $title;
+        return $this;
+    }
+
+    public function getUrl(): string
+    {
+        return $this->url;
+    }
+
+    public function setUrl(string $url): self
+    {
+        $this->url = $url;
+        return $this;
+    }
+}

+ 98 - 1
src/Service/HelloAsso/ConnectionService.php → src/Service/HelloAsso/HelloAssoService.php

@@ -6,12 +6,14 @@ namespace App\Service\HelloAsso;
 
 use App\ApiResources\HelloAsso\AuthUrl;
 use App\ApiResources\HelloAsso\HelloAssoProfile;
+use App\ApiResources\HelloAsso\OrganizationForm;
 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\Common\Collections\Collection;
 use Doctrine\ORM\EntityManagerInterface;
 use Psr\Log\LoggerInterface;
 use Symfony\Component\HttpKernel\Exception\HttpException;
@@ -23,7 +25,7 @@ use Symfony\Contracts\HttpClient\HttpClientInterface;
  * @see doc/helloasso.md
  * @see https://dev.helloasso.com/docs/mire-authorisation
  */
-class ConnectionService extends ApiRequestService
+class HelloAssoService extends ApiRequestService
 {
     public function __construct(
         HttpClientInterface $client,
@@ -193,6 +195,58 @@ class ConnectionService extends ApiRequestService
         $this->entityManager->flush();
     }
 
+    public function getHelloAssoForms(int $organizationId): array
+    {
+        $organization = $this->entityManager->getRepository(Organization::class)->find($organizationId);
+        if (!$organization) {
+            throw new \RuntimeException('Organization not found');
+        }
+
+        $helloAssoEntity = $organization->getHelloAsso();
+        if (!$helloAssoEntity) {
+            throw new \RuntimeException('HelloAsso entity not found');
+        }
+        if (!$helloAssoEntity->getOrganizationSlug() || !$helloAssoEntity->getToken()) {
+            throw new \RuntimeException('HelloAsso entity incomplete');
+        }
+
+        $helloAssoEntity = $this->refreshTokenIfNeeded($helloAssoEntity);
+
+        $response = $this->get(
+            UrlBuilder::concat(
+                $this->helloAssoApiBaseUrl,
+                ['/v5/organizations', $helloAssoEntity->getOrganizationSlug(), '/forms']
+            ),
+            [],
+            ['headers' =>
+                [
+                    'accept' => 'application/json',
+                    'authorization' => 'Bearer '.$helloAssoEntity->getToken(),
+                ]
+            ]
+        );
+
+        if ($response->getStatusCode() !== 200) {
+            throw new HttpException(500, 'Failed to fetch access token: '.$response->getContent(false));
+        }
+
+        $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR)['data'];
+
+        $forms = [];
+
+        foreach ($data as $formData) {
+            $form = new OrganizationForm();
+            $form->setSlug($formData['formSlug']);
+            $form->setTitle($formData['title']);
+            $form->setUrl($formData['url']);
+
+            $forms[] = $form;
+        }
+
+        return $forms;
+    }
+
+
     /**
      * Génère l'URL de rappel pour les callbacks suite à l'authentification HelloAsso
      *
@@ -285,4 +339,47 @@ class ConnectionService extends ApiRequestService
             throw new HttpException(500, 'Failed to update domain: '.$response->getContent());
         }
     }
+
+    protected function refreshTokenIfNeeded(HelloAsso $helloAssoEntity): HelloAsso {
+        if (!$helloAssoEntity->getRefreshToken() || !$helloAssoEntity->getRefreshTokenCreatedAt()) {
+            throw new \RuntimeException('HelloAsso entity incomplete');
+        }
+
+        // Les tokens ont une durée de validité de 30min, on les rafraichit passé 25min.
+        $needsRefreshing = $helloAssoEntity->getRefreshTokenCreatedAt()->add(new \DateInterval('PT25M')) < DatesUtils::new();
+        if (!$needsRefreshing) {
+            return $helloAssoEntity;
+        }
+
+        $body = [
+            'grant_type' => 'refresh_token',
+            'refresh_token' => $helloAssoEntity->getRefreshToken(),
+        ];
+
+        $response = $this->client->request('POST',
+            UrlBuilder::concat($this->helloAssoApiBaseUrl, ['/oauth2/token']),
+            [
+                'headers' => [
+                    'Content-Type' => 'application/x-www-form-urlencoded',
+                ],
+                'body' => $body,
+            ]
+        );
+
+        if ($response->getStatusCode() !== 200) {
+            throw new HttpException(500, 'Failed to refresh access token: '.$response->getContent(false));
+        }
+
+        $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
+
+        $helloAssoEntity->setToken($data['access_token']);
+        $helloAssoEntity->setTokenCreatedAt(DatesUtils::new());
+        $helloAssoEntity->setRefreshToken($data['refresh_token']);
+        $helloAssoEntity->setRefreshTokenCreatedAt(DatesUtils::new());
+
+        $this->entityManager->persist($helloAssoEntity);
+        $this->entityManager->flush();
+
+        return $helloAssoEntity;
+    }
 }

+ 6 - 6
src/State/Processor/HelloAsso/ConnectionRequestProcessor.php

@@ -9,7 +9,7 @@ use ApiPlatform\Metadata\Post;
 use ApiPlatform\State\ProcessorInterface;
 use App\ApiResources\HelloAsso\ConnectionRequest;
 use App\Entity\Access\Access;
-use App\Service\HelloAsso\ConnectionService;
+use App\Service\HelloAsso\HelloAssoService;
 use App\Service\MercureHub;
 use http\Client\Response;
 use Symfony\Bundle\SecurityBundle\Security;
@@ -20,9 +20,9 @@ use Symfony\Bundle\SecurityBundle\Security;
 class ConnectionRequestProcessor implements ProcessorInterface
 {
     public function __construct(
-        private readonly ConnectionService $connectionService,
-        private Security $security,
-        private MercureHub $mercureHub,
+        private readonly HelloAssoService $helloAssoService,
+        private Security                  $security,
+        private MercureHub                $mercureHub,
     ) {
     }
 
@@ -51,12 +51,12 @@ class ConnectionRequestProcessor implements ProcessorInterface
             throw new \RuntimeException('Forbidden');
         }
 
-        $helloAssoEntity = $this->connectionService->connect(
+        $helloAssoEntity = $this->helloAssoService->connect(
             $connectionRequest->getOrganizationId(),
             $connectionRequest->getAuthorizationCode()
         );
 
-        $helloAssoProfile = $this->connectionService->makeHelloAssoProfile($connectionRequest->getOrganizationId());
+        $helloAssoProfile = $this->helloAssoService->makeHelloAssoProfile($connectionRequest->getOrganizationId());
 
         $this->mercureHub->publishUpdate($access->getId(), $helloAssoProfile);
 

+ 5 - 5
src/State/Processor/HelloAsso/UnlinkRequestProcessor.php

@@ -9,7 +9,7 @@ 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\HelloAsso\HelloAssoService;
 use App\Service\MercureHub;
 use http\Client\Response;
 use Symfony\Bundle\SecurityBundle\Security;
@@ -20,9 +20,9 @@ use Symfony\Bundle\SecurityBundle\Security;
 class UnlinkRequestProcessor implements ProcessorInterface
 {
     public function __construct(
-        private readonly ConnectionService $connectionService,
-        private Security $security,
-        private MercureHub $mercureHub,
+        private readonly HelloAssoService $helloAssoService,
+        private Security                  $security,
+        private MercureHub                $mercureHub,
     ) {
     }
 
@@ -51,7 +51,7 @@ class UnlinkRequestProcessor implements ProcessorInterface
             throw new \RuntimeException('Forbidden: ' . $unlinkRequest->getOrganizationId() . ' !== ' . $access->getOrganization()->getId());
         }
 
-        $this->connectionService->unlinkHelloAssoAccount($unlinkRequest->getOrganizationId());
+        $this->helloAssoService->unlinkHelloAssoAccount($unlinkRequest->getOrganizationId());
 
         return $unlinkRequest;
     }

+ 4 - 4
src/State/Provider/HelloAsso/AuthUrlProvider.php

@@ -9,7 +9,7 @@ use ApiPlatform\Metadata\Operation;
 use ApiPlatform\State\ProviderInterface;
 use App\ApiResources\HelloAsso\AuthUrl;
 use App\Entity\Access\Access;
-use App\Service\HelloAsso\ConnectionService;
+use App\Service\HelloAsso\HelloAssoService;
 use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -19,8 +19,8 @@ use Symfony\Component\HttpFoundation\Response;
 final class AuthUrlProvider implements ProviderInterface
 {
     public function __construct(
-        private ConnectionService $connectionService,
-        private Security $security,
+        private HelloAssoService $helloAssoService,
+        private Security         $security,
     ) {
     }
 
@@ -41,6 +41,6 @@ final class AuthUrlProvider implements ProviderInterface
 
         $organizationId = $access->getOrganization()->getId();
 
-        return $this->connectionService->getAuthUrl($organizationId);
+        return $this->helloAssoService->getAuthUrl($organizationId);
     }
 }

+ 4 - 4
src/State/Provider/HelloAsso/HelloAssoProfileProvider.php

@@ -10,7 +10,7 @@ use ApiPlatform\State\ProviderInterface;
 use App\ApiResources\HelloAsso\AuthUrl;
 use App\ApiResources\HelloAsso\HelloAssoProfile;
 use App\Entity\Access\Access;
-use App\Service\HelloAsso\ConnectionService;
+use App\Service\HelloAsso\HelloAssoService;
 use Symfony\Bundle\SecurityBundle\Security;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -20,8 +20,8 @@ use Symfony\Component\HttpFoundation\Response;
 final class HelloAssoProfileProvider implements ProviderInterface
 {
     public function __construct(
-        private ConnectionService $connectionService,
-        private Security $security,
+        private HelloAssoService $helloAssoService,
+        private Security         $security,
     ) {
     }
 
@@ -42,6 +42,6 @@ final class HelloAssoProfileProvider implements ProviderInterface
 
         $organizationId = $access->getOrganization()->getId();
 
-        return $this->connectionService->makeHelloAssoProfile($organizationId);
+        return $this->helloAssoService->makeHelloAssoProfile($organizationId);
     }
 }

+ 51 - 0
src/State/Provider/HelloAsso/OrganizationFormProvider.php

@@ -0,0 +1,51 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\State\Provider\HelloAsso;
+
+use ApiPlatform\Metadata\GetCollection;
+use ApiPlatform\Metadata\Operation;
+use ApiPlatform\State\ProviderInterface;
+use App\ApiResources\HelloAsso\AuthUrl;
+use App\ApiResources\HelloAsso\HelloAssoProfile;
+use App\ApiResources\HelloAsso\OrganizationForm;
+use App\Entity\Access\Access;
+use App\Service\HelloAsso\HelloAssoService;
+use Doctrine\Common\Collections\ArrayCollection;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Provider pour la ressource HelloAssoProfile.
+ */
+final class OrganizationFormProvider implements ProviderInterface
+{
+    public function __construct(
+        private HelloAssoService $helloAssoService,
+        private Security         $security,
+    ) {
+    }
+
+    /**
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     *
+     * @throws \Exception
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ArrayCollection | OrganizationForm
+    {
+        /** @var Access $access */
+        $access = $this->security->getUser();
+
+        $organizationId = $access->getOrganization()->getId();
+
+        if ($operation instanceof GetCollection) {
+            $forms = $this->helloAssoService->getHelloAssoForms($organizationId);
+
+            return new ArrayCollection($forms);
+        } else {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+    }
+}