Преглед изворни кода

add the HelloAssoProfile resource

Olivier Massot пре 2 месеци
родитељ
комит
8436a86a7e

+ 1 - 0
config/opentalent/products.yaml

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

+ 58 - 0
src/ApiResources/HelloAsso/HelloAssoProfile.php

@@ -0,0 +1,58 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\ApiResources\HelloAsso;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+use ApiPlatform\Metadata\Get;
+use App\State\Provider\HelloAsso\AuthUrlProvider;
+use App\State\Provider\HelloAsso\HelloAssoProfileProvider;
+
+/**
+ * Ressource contenant l'URL d'authentification HelloAsso et le vérificateur de défi PKCE.
+ */
+#[ApiResource(
+    operations: [
+        new Get(
+            uriTemplate: '/helloasso/profile',
+        ),
+    ],
+    provider: HelloAssoProfileProvider::class,
+    security: 'is_granted("ROLE_ORGANIZATION")'
+)]
+class HelloAssoProfile
+{
+    /**
+     * Token HelloAsso pour l'autorisation OAuth2.
+     */
+    private string | null $token = null;
+
+    /**
+     * Slug de l'organization HelloAsso
+     */
+    private string | null $organizationSlug = null;
+
+    public function getToken(): ?string
+    {
+        return $this->token;
+    }
+
+    public function setToken(?string $token): self
+    {
+        $this->token = $token;
+        return $this;
+    }
+
+    public function getOrganizationSlug(): ?string
+    {
+        return $this->organizationSlug;
+    }
+
+    public function setOrganizationSlug(?string $organizationSlug): self
+    {
+        $this->organizationSlug = $organizationSlug;
+        return $this;
+    }
+}

+ 103 - 53
src/Service/HelloAsso/ConnectionService.php

@@ -1,19 +1,19 @@
 <?php
+
 declare(strict_types=1);
 
 namespace App\Service\HelloAsso;
 
 use App\ApiResources\HelloAsso\AuthUrl;
+use App\ApiResources\HelloAsso\HelloAssoProfile;
 use App\Entity\Organization\HelloAsso;
 use App\Entity\Organization\Organization;
 use App\Service\Rest\ApiRequestService;
 use App\Service\Security\OAuthPkceGenerator;
 use App\Service\Utils\UrlBuilder;
 use Doctrine\ORM\EntityManagerInterface;
-use Symfony\Contracts\HttpClient\HttpClientInterface;
-use Symfony\Contracts\HttpClient\ResponseInterface;
 use Symfony\Component\HttpKernel\Exception\HttpException;
-
+use Symfony\Contracts\HttpClient\HttpClientInterface;
 
 /**
  * Service de connexion à HelloAsso.
@@ -24,13 +24,13 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
 class ConnectionService extends ApiRequestService
 {
     public function __construct(
-        HttpClientInterface                     $client,
-        private readonly string                 $baseUrl,
-        private readonly string                 $publicAppBaseUrl,
-        private readonly string                 $helloAssoApiBaseUrl,
-        private readonly string                 $helloAssoAuthBaseUrl,
-        private readonly string                 $helloAssoClientId,
-        private readonly string                 $helloAssoClientSecret,
+        HttpClientInterface $client,
+        private readonly string $baseUrl,
+        private readonly string $publicAppBaseUrl,
+        private readonly string $helloAssoApiBaseUrl,
+        private readonly string $helloAssoAuthBaseUrl,
+        private readonly string $helloAssoClientId,
+        private readonly string $helloAssoClientSecret,
         private readonly EntityManagerInterface $entityManager,
     ) {
         parent::__construct($client);
@@ -45,38 +45,52 @@ class ConnectionService extends ApiRequestService
      *
      * @see doc/helloasso.md#2-enregistrement-de-votre-domaine-de-redirection
      * @see https://dev.helloasso.com/reference/put_partners-me-api-clients
-     *
-     * @return void
      */
-    public function setupOpentalentDomain(): void {
+    public function setupOpentalentDomain(): void
+    {
         $accessToken = $this->fetchAccessToken(null);
-        $this->updateDomain($accessToken, "https://*.opentalent.fr");
+        $this->updateDomain($accessToken, 'https://*.opentalent.fr');
     }
 
     /**
-     * Créé l'URL du formulaire d'authentification HelloAsso
+     * Créé l'URL du formulaire d'authentification HelloAsso.
      *
      * @see doc/helloasso.md#se-connecter-avec-helloasso
+     *
+     * @param int    $organizationId    the ID of the organization to connect
      */
-    public function getAuthUrl(): AuthUrl
+    public function getAuthUrl(int $organizationId): AuthUrl
     {
-        $callbackUrl = UrlBuilder::concat($this->publicAppBaseUrl, ['helloasso']);
+        $organization = $this->entityManager->getRepository(Organization::class)->find($organizationId);
+
+        if (!$organization) {
+            throw new \RuntimeException('Organization not found');
+        }
 
         $challenge = OAuthPkceGenerator::generatePkce();
 
         $params = [
             'client_id' => $this->helloAssoClientId,
-            'redirect_uri' => $callbackUrl,
+            'redirect_uri' => $this->getCallbackUrl(),
             'code_challenge' => $challenge['challenge'],
-            'code_challenge_method' => 'S256'
+            'code_challenge_method' => 'S256',
         ];
 
         $authUrl = UrlBuilder::concat($this->helloAssoAuthBaseUrl, ['authorize'], $params);
         $challengeVerifier = $challenge['verifier'];
 
+        $helloAssoEntity = $organization->getHelloAsso();
+        if (!$helloAssoEntity) {
+            $helloAssoEntity = new HelloAsso();
+            $helloAssoEntity->setOrganization($organization);
+        }
+
+        $helloAssoEntity->setChallengeVerifier($challengeVerifier);
+        $this->entityManager->persist($helloAssoEntity);
+        $this->entityManager->flush();
+
         $authUrlResource = new AuthUrl();
         $authUrlResource->setAuthUrl($authUrl);
-        $authUrlResource->setChallengeVerifier($challengeVerifier);
 
         return $authUrlResource;
     }
@@ -86,36 +100,38 @@ class ConnectionService extends ApiRequestService
      *
      * @see doc/helloasso.md#r%C3%A9cup%C3%A9rer-et-stocker-les-jetons-dacc%C3%A8s
      *
-     * @param int $organizationId The ID of the organization to connect.
+     * @param int    $organizationId    the ID of the organization to connect
      * @param string $authorizationCode Le code d'autorisation Helloasso fourni après l'authentification de l'utilisateur.'
      *
-     * @return HelloAsso The HelloAsso entity for the organization.
+     * @return HelloAsso the HelloAsso entity for the organization
      *
-     * @throws \RuntimeException If the organization is not found or if any connection step fails.
+     * @throws \RuntimeException if the organization is not found or if any connection step fails
      */
     public function connect(int $organizationId, string $authorizationCode): HelloAsso
     {
         $organization = $this->entityManager->getRepository(Organization::class)->find($organizationId);
-
         if (!$organization) {
             throw new \RuntimeException('Organization not found');
         }
 
-        $tokens = $this->fetchAccessToken($authorizationCode);
+        $helloAssoEntity = $organization->getHelloAsso();
+        if (!$helloAssoEntity) {
+            throw new \RuntimeException('HelloAsso entity not found');
+        }
+
+        $tokens = $this->fetchAccessToken(
+            $authorizationCode,
+            $helloAssoEntity->getChallengeVerifier()
+        );
 
         if ($tokens['token_type'] !== 'bearer') {
             throw new \RuntimeException('Invalid token type received');
         }
 
-        $helloAssoEntity = $organization->getHelloAsso();
-        if (!$helloAssoEntity) {
-            $helloAssoEntity = new HelloAsso();
-            $helloAssoEntity->setOrganization($organization);
-        }
-
         $helloAssoEntity->setToken($tokens['access_token']);
         $helloAssoEntity->setRefreshToken($tokens['refresh_token']);
         $helloAssoEntity->setOrganizationSlug($tokens['organization_slug'] ?? null);
+        $helloAssoEntity->setChallengeVerifier(null);
 
         $this->entityManager->persist($helloAssoEntity);
         $this->entityManager->flush();
@@ -123,6 +139,30 @@ class ConnectionService extends ApiRequestService
         return $helloAssoEntity;
     }
 
+    public function makeHelloAssoProfile(int $organizationId): HelloAssoProfile
+    {
+        $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');
+        }
+
+        $profile = new HelloAssoProfile();
+        $profile->setToken($helloAssoEntity->getToken());
+        $profile->setOrganizationSlug($helloAssoEntity->getOrganizationSlug());
+
+        return $profile;
+    }
+
+    protected function getCallbackUrl(): string
+    {
+        return UrlBuilder::concat($this->publicAppBaseUrl, ['helloasso/callback']);
+    }
+
     /**
      * Récupère les jetons d'accès auprès de l'API HelloAsso.
      *
@@ -130,13 +170,13 @@ class ConnectionService extends ApiRequestService
      *                                       retournés seront pour le compte principal Opentalent et non pour une
      *                                       organisation (par exemple pour la mise à jour du domaine).
      *
-     * @return array<string, string> An array containing access token details: access_token, refresh_token, token_type, and expires_in.
+     * @return array<string, string> an array containing access token details: access_token, refresh_token, token_type, and expires_in
      *
-     * @throws \InvalidArgumentException If an authorization code is required but not provided for organization tokens.
-     * @throws \JsonException If the authentication response cannot be parsed.
-     * @throws HttpException If there is an error in parsing the authentication response or the request fails.
+     * @throws \InvalidArgumentException if an authorization code is required but not provided for organization tokens
+     * @throws \JsonException            if the authentication response cannot be parsed
+     * @throws HttpException             if there is an error in parsing the authentication response or the request fails
      */
-    protected function fetchAccessToken(string | null $authorizationCode): array
+    protected function fetchAccessToken(?string $authorizationCode, ?string $challengeVerifier): array
     {
         $grantType = $authorizationCode !== null ? 'authorization_code' : 'client_credentials';
 
@@ -148,23 +188,33 @@ class ConnectionService extends ApiRequestService
 
         if ($authorizationCode !== null) {
             $body['code'] = $authorizationCode;
-            $body['redirect_uri'] = UrlBuilder::concat($this->publicAppBaseUrl, ['helloasso/callback']);
+            $body['redirect_uri'] = $this->getCallbackUrl();
         }
 
-        $options = [
-            'headers' => [
-                'Content-Type' => 'application/x-www-form-urlencoded',
-            ],
-            'body' => http_build_query($body),
-        ];
-
-        $response = $this->post(
+        $response = $this->client->request('POST',
             UrlBuilder::concat($this->helloAssoApiBaseUrl, ['/oauth2/token']),
-            null,
-            [],
-            $options
+            [
+                'headers' => [
+                    'Content-Type' => 'application/x-www-form-urlencoded',
+                ],
+                'body' => $body,
+            ]
         );
 
+//        $options = [
+//            'headers' => [
+//                'Content-Type' => 'application/x-www-form-urlencoded',
+//            ],
+//            'body' => $body,
+//        ];
+//
+//        $response = $this->post(
+//            UrlBuilder::concat($this->helloAssoApiBaseUrl, ['/oauth2/token']),
+//            [],
+//            [],
+//            $options
+//        );
+
         try {
             $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
 
@@ -175,12 +225,12 @@ class ConnectionService extends ApiRequestService
                 'expires_in' => $data['expires_in'] ?? null,
             ];
         } catch (\JsonException $e) {
-            throw new HttpException(500, 'Failed to parse authentication response: ' . $e->getMessage(), $e);
+            throw new HttpException(500, 'Failed to parse authentication response: '.$e->getMessage(), $e);
         }
     }
 
     /**
-     * Updates the domain configuration
+     * Updates the domain configuration.
      *
      * @throws HttpException
      */
@@ -191,13 +241,13 @@ class ConnectionService extends ApiRequestService
             ['domain' => $domain],
             [],
             ['headers' => [
-                'Authorization' => 'Bearer ' . $accessToken,
-                'Content-Type' => 'application/json']
+                'Authorization' => 'Bearer '.$accessToken,
+                'Content-Type' => 'application/json'],
             ],
         );
 
         if ($response->getStatusCode() !== 200) {
-            throw new HttpException(500, 'Failed to update domain: ' . $response->getContent());
+            throw new HttpException(500, 'Failed to update domain: '.$response->getContent());
         }
     }
 }

+ 47 - 0
src/State/Provider/HelloAsso/HelloAssoProfileProvider.php

@@ -0,0 +1,47 @@
+<?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\Entity\Access\Access;
+use App\Service\HelloAsso\ConnectionService;
+use Symfony\Bundle\SecurityBundle\Security;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Provider pour la ressource AuthUrl HelloAsso.
+ */
+final class HelloAssoProfileProvider implements ProviderInterface
+{
+    public function __construct(
+        private ConnectionService $connectionService,
+        private Security $security,
+    ) {
+    }
+
+    /**
+     * @param mixed[] $uriVariables
+     * @param mixed[] $context
+     *
+     * @throws \Exception
+     */
+    public function provide(Operation $operation, array $uriVariables = [], array $context = []): ?HelloAssoProfile
+    {
+        if ($operation instanceof GetCollection) {
+            throw new \RuntimeException('not supported', Response::HTTP_METHOD_NOT_ALLOWED);
+        }
+
+        /** @var Access $access */
+        $access = $this->security->getUser();
+
+        $organizationId = $access->getOrganization()->getId();
+
+        return $this->connectionService->makeHelloAssoProfile($organizationId);
+    }
+}