|
|
@@ -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());
|
|
|
}
|
|
|
}
|
|
|
}
|