From 546bc1571a62f4eaf90e412cbba99e11563c8f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Le=CC=81o?= Date: Tue, 12 May 2026 15:11:00 +0200 Subject: [PATCH 1/9] Refacto and UI imrpovement --- migrations/11-move-goal-to-stream-event.sql | 15 + src/Assets/js/admin.js | 14 +- src/Controllers/AdminController.php | 97 +++-- src/Controllers/ApiController.php | 6 - src/Controllers/LoginController.php | 185 +++------ src/Controllers/WidgetController.php | 404 +++++++++----------- src/Models/Event.php | 1 + src/Models/Stream.php | 1 + src/Repositories/EventRepository.php | 22 ++ src/Repositories/StreamRepository.php | 22 ++ src/Services/ApiWrapper.php | 305 ++++++--------- src/views/event/edit.html.twig | 50 ++- src/views/stream/edit.html.twig | 58 ++- src/views/widget/card.html.twig | 4 +- src/views/widget/donation.html.twig | 8 +- 15 files changed, 571 insertions(+), 621 deletions(-) create mode 100644 migrations/11-move-goal-to-stream-event.sql diff --git a/migrations/11-move-goal-to-stream-event.sql b/migrations/11-move-goal-to-stream-event.sql new file mode 100644 index 0000000..1ccb11f --- /dev/null +++ b/migrations/11-move-goal-to-stream-event.sql @@ -0,0 +1,15 @@ +-- Ajouter la colonne goal aux tables stream et event +ALTER TABLE {prefix}charity_stream ADD goal INT NOT NULL DEFAULT 1000; +ALTER TABLE {prefix}charity_event ADD goal INT NOT NULL DEFAULT 1000; + +-- Copier les objectifs existants depuis les widgets barre de don +UPDATE {prefix}charity_stream cs + INNER JOIN {prefix}widget_donation_goal_bar w ON w.charity_stream_guid = cs.guid + SET cs.goal = w.goal + WHERE w.goal IS NOT NULL AND w.goal > 0; + +UPDATE {prefix}charity_event ce + INNER JOIN {prefix}widget_donation_goal_bar w ON w.charity_event_guid = ce.guid + SET ce.goal = w.goal + WHERE w.goal IS NOT NULL AND w.goal > 0; + diff --git a/src/Assets/js/admin.js b/src/Assets/js/admin.js index 3fe9fa6..b02b40c 100644 --- a/src/Assets/js/admin.js +++ b/src/Assets/js/admin.js @@ -17,7 +17,8 @@ function updateDonationGoalPreview() { const front = document.querySelector('.front'); const backTitle = document.getElementById('back-title'); const frontTitle = document.getElementById('front-title'); - const goal = document.getElementById('goal'); + // L'objectif est maintenant dans le formulaire d'infos (stream_goal ou event_goal) + const goalInput = document.getElementById('stream_goal') || document.getElementById('event_goal'); back.style.backgroundColor = document.getElementById('background_color').value; front.style.backgroundColor = document.getElementById('bar_color').value; @@ -29,7 +30,7 @@ function updateDonationGoalPreview() { backTitle.textContent = textContent; frontTitle.textContent = textContent; - const goalValue = parseFloat(goal.value) || 0; + const goalValue = goalInput ? (parseFloat(goalInput.value) || 0) : 0; const currentDonation = goalValue / 2; document.getElementById('back-goal-total').textContent = `${goalValue} €`; @@ -43,7 +44,7 @@ function updateDonationGoalPreview() { const donationBarForm = document.getElementById('donationBarForm'); if (donationBarForm) { bindPreviewInputs( - ['text_color_main', 'text_color_alt', 'text_content', 'bar_color', 'background_color', 'goal'], + ['text_color_main', 'text_color_alt', 'text_content', 'bar_color', 'background_color', 'stream_goal', 'event_goal'], updateDonationGoalPreview ); } @@ -74,7 +75,9 @@ if (cardWidgetForm) { const barBgColor = document.getElementById('card_bar_background_color').value; const tagColor = document.getElementById('card_tag_color').value; const tagBgColor = document.getElementById('card_tag_background_color').value; - const goalValue = parseFloat(document.getElementById('card_goal').value) || 1; + // L'objectif est maintenant dans le formulaire d'infos (stream_goal ou event_goal) + const goalInput = document.getElementById('stream_goal') || document.getElementById('event_goal'); + const goalValue = goalInput ? (parseFloat(goalInput.value) || 1) : 1; if (preview) { preview.style.backgroundColor = bgColor; @@ -101,9 +104,10 @@ if (cardWidgetForm) { bindPreviewInputs( [ - 'card_tag', 'card_title', 'card_description', 'card_goal', + 'card_tag', 'card_title', 'card_description', 'card_background_color', 'card_text_color', 'card_bar_color', 'card_bar_background_color', 'card_tag_color', 'card_tag_background_color', + 'stream_goal', 'event_goal', ], updateCardPreview ); diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index ff7bdc4..1590062 100644 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -32,6 +32,28 @@ public function __construct( private AuthorizationCodeRepository $authorizationCodeRepository, ) {} + /** + * Retourne les slugs d'organisation dont le token est expiré parmi les streams donnés. + */ + private function getInvalidTokenSlugs(array $streams): array + { + $validSlugs = $this->accessTokenRepository->getValidOrganizationSlugs(); + $invalidSlugs = []; + foreach ($streams as $stream) { + if ($stream->organization_slug && !in_array($stream->organization_slug, $validSlugs)) { + $invalidSlugs[] = $stream->organization_slug; + } + } + return array_unique($invalidSlugs); + } + + private function redirectToRoute(Request $request, Response $response, string $routeName, array $params = []): Response + { + $routeParser = RouteContext::fromRequest($request)->getRouteParser(); + $url = $routeParser->urlFor($routeName, $params); + return $response->withHeader('Location', $url)->withStatus(302); + } + public function index(Request $request, Response $response): Response { $user = $request->getAttribute('user'); @@ -43,13 +65,6 @@ public function index(Request $request, Response $response): Response $events = $this->eventRepository->selectListByUser($user); } - $validSlugs = $this->accessTokenRepository->getValidOrganizationSlugs(); - $invalidTokenSlugs = []; - foreach ($streams as $stream) { - if ($stream->organization_slug && !in_array($stream->organization_slug, $validSlugs)) { - $invalidTokenSlugs[] = $stream->organization_slug; - } - } $data = [ "streams" => $streams, @@ -58,7 +73,7 @@ public function index(Request $request, Response $response): Response "currentUser" => $user, "selectedEventId" => $request->getQueryParams()['eventId'] ?? null, "openCreateStream" => isset($request->getQueryParams()['createStream']), - "invalidTokenSlugs" => $invalidTokenSlugs, + "invalidTokenSlugs" => $this->getInvalidTokenSlugs($streams), ]; $template = $user->role === "ADMIN" ? 'stream/index-admin.html.twig' : 'stream/index.html.twig'; @@ -74,28 +89,23 @@ public function newEvent(Request $request, Response $response): Response $this->userRepository->insertRight($user, null, $event); $this->messages->addMessage('success', 'Évènement ajouté'); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_admin_index'); - - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } public function deleteEvent(Request $request, Response $response, array $args): Response { $user = $request->getAttribute('user'); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_admin_index'); $event = $this->eventRepository->selectByUserAndGuid($user, $args['id']); if (!$event) { $this->messages->addMessage('error', 'Tu n\'as pas accès cet évènement'); - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } $this->eventRepository->delete($event); $this->messages->addMessage('success', 'Évènement supprimé'); - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } public function editEvent(Request $request, Response $response, array $args): Response @@ -107,19 +117,11 @@ public function editEvent(Request $request, Response $response, array $args): Re $streams = $this->streamRepository->selectListByEvent($event); $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $validSlugs = $this->accessTokenRepository->getValidOrganizationSlugs(); - $invalidTokenSlugs = []; - foreach ($streams as $stream) { - if ($stream->organization_slug && !in_array($stream->organization_slug, $validSlugs)) { - $invalidTokenSlugs[] = $stream->organization_slug; - } - } - $data = [ "logged" => true, "event" => $event, "streams" => $streams, - "invalidTokenSlugs" => $invalidTokenSlugs, + "invalidTokenSlugs" => $this->getInvalidTokenSlugs($streams), "donationGoalWidget" => $donationGoalWidget, "cardWidget" => $cardWidget, "cardWidgetPictureUrl" => ($cardWidget && $cardWidget->image) ? $this->fileManager->getPictureUrl($cardWidget->image) : null, @@ -135,12 +137,22 @@ public function editEventPost(Request $request, Response $response, array $args) $user = $request->getAttribute('user'); $event = $this->eventRepository->selectByUserAndGuid($user, $args['id']); - $this->handleWidgetFormSave($request, null, $event->guid); + $body = $request->getParsedBody(); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_event_edit', ["id" => $event->guid]); + if (isset($body['save_event_info'])) { + $updateData = []; + if (isset($body['event_title'])) { + $updateData['title'] = $body['event_title']; + } + if (isset($body['event_goal'])) { + $updateData['goal'] = (int) $body['event_goal']; + } + $this->eventRepository->update($event, $updateData); + } - return $response->withHeader('Location', $url)->withStatus(302); + $this->handleWidgetFormSave($request, null, $event->guid); + + return $this->redirectToRoute($request, $response, 'app_event_edit', ["id" => $event->guid]); } public function newStream(Request $request, Response $response): Response @@ -177,28 +189,23 @@ public function newStream(Request $request, Response $response): Response } $this->messages->addMessage('success', 'Stream ajouté'); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_admin_index'); - - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } public function deleteStream(Request $request, Response $response, array $args): Response { $user = $request->getAttribute('user'); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_admin_index'); $stream = $this->streamRepository->selectByUserAndGuid($user, $args['id']); if (!$stream) { $this->messages->addMessage('error', 'Tu n\'as pas accès ce stream'); - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } $this->streamRepository->delete($stream); $this->messages->addMessage('success', 'Stream supprimé'); - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } public function editStream(Request $request, Response $response, array $args): Response @@ -246,6 +253,17 @@ public function editStreamPost(Request $request, Response $response, array $args $body = $request->getParsedBody(); + if (isset($body['save_stream_info'])) { + $updateData = []; + if (isset($body['stream_title'])) { + $updateData['title'] = $body['stream_title']; + } + if (isset($body['stream_goal'])) { + $updateData['goal'] = (int) $body['stream_goal']; + } + $this->streamRepository->update($charityStream, $updateData); + } + if (isset($body['save_alert_box'])) { $uploadedFiles = $request->getUploadedFiles(); $image = isset($uploadedFiles['image']) && $uploadedFiles['image']->getSize() > 0 @@ -260,10 +278,7 @@ public function editStreamPost(Request $request, Response $response, array $args $this->handleWidgetFormSave($request, $guid, null); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_stream_edit', ["id" => $guid]); - - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_stream_edit', ["id" => $guid]); } /** diff --git a/src/Controllers/ApiController.php b/src/Controllers/ApiController.php index 47e18ff..8d2bf07 100644 --- a/src/Controllers/ApiController.php +++ b/src/Controllers/ApiController.php @@ -2,23 +2,17 @@ namespace App\Controllers; -use App\Repositories\FileManager; use App\Repositories\StreamRepository; use App\Repositories\UserRepository; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Slim\Flash\Messages; use Slim\Routing\RouteContext; -use Slim\Views\Twig; class ApiController { public function __construct( - private Twig $view, - private FileManager $fileManager, private StreamRepository $streamRepository, private UserRepository $userRepository, - private Messages $messages, ) {} public function new(Request $request, Response $response): Response diff --git a/src/Controllers/LoginController.php b/src/Controllers/LoginController.php index ac63412..295b847 100644 --- a/src/Controllers/LoginController.php +++ b/src/Controllers/LoginController.php @@ -9,16 +9,15 @@ use App\Services\ApiWrapper; use Exception; use MailchimpTransactional\ApiClient; +use Monolog\Logger; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Flash\Messages; use Slim\Routing\RouteContext; use Slim\Views\Twig; -use Monolog\Logger; class LoginController { - public function __construct( private Twig $view, private ApiWrapper $apiWrapper, @@ -28,19 +27,18 @@ public function __construct( private UserRepository $userRepository, private ApiClient $mailchimp, private Messages $messages, - private Logger $logger - ) { - + private Logger $logger, + ) {} + + private function redirectToRoute(Request $request, Response $response, string $routeName, array $params = []): Response + { + $routeParser = RouteContext::fromRequest($request)->getRouteParser(); + $url = $routeParser->urlFor($routeName, $params); + return $response->withHeader('Location', $url)->withStatus(302); } - - /** - * Valide la page de connexion après soumission du formulaire. Si les identifiants sont corrects, stocke l'utilisateur en session et redirige vers la page d'administration. Sinon, affiche un message d'erreur et redirige vers la page d'accueil. - * - * @param Request $request - * @param Response $response - * @return Response + * Valide la page de connexion après soumission du formulaire. */ public function login(Request $request, Response $response): Response { @@ -49,194 +47,140 @@ public function login(Request $request, Response $response): Response $password = $data['password'] ?? ''; $user = $this->userRepository->select($email); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - if ($user && password_verify($password, $user->password)) { session_regenerate_id(true); $_SESSION['user'] = $user; - $url = $routeParser->urlFor('app_admin_index'); - } else { - $this->messages->addMessage('login_failed', true); - $url = $routeParser->urlFor('app_index'); + return $this->redirectToRoute($request, $response, 'app_admin_index'); } - return $response->withHeader('Location', $url)->withStatus(302); + + $this->messages->addMessage('login_failed', true); + return $this->redirectToRoute($request, $response, 'app_index'); } /** - * Valide la page de mot de passe oublié après soumission du formulaire. Si l'email existe, génère un token de réinitialisation, l'associe à l'utilisateur et envoie un email avec le lien de réinitialisation. Affiche un message indiquant que l'email a été envoyé, même si l'email n'existe pas pour éviter les attaques de type enumeration. - * - * @param Request $request - * @param Response $response - * @return Response + * Envoie un email de réinitialisation de mot de passe si l'adresse existe. */ public function forgotPassword(Request $request, Response $response): Response { $data = $request->getParsedBody(); - $email = $data['email'] ?? ''; $user = $this->userRepository->select($email); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - if ($user) { $user = $this->userRepository->insertResetToken($user); - $url = $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_reset_password', ["token" => $user->reset_token]); + $routeParser = RouteContext::fromRequest($request)->getRouteParser(); + $resetUrl = $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_reset_password', ["token" => $user->reset_token]); $this->mailchimp->messages->send([ "message" => [ "from_email" => "contact@helloasso.io", "from_name" => "HelloAsso", "subject" => "Mot de passe oublié", - "html" => "

Vous avez fait une demande de réinitialisation de mot de passe. Merci de le définir sur cette page
Ou en suivant ce lien " . $url . "

", - "to" => [ - [ - "email" => $user->email - ] - ], - ] + "html" => "

Vous avez fait une demande de réinitialisation de mot de passe. Merci de le définir sur cette page
Ou en suivant ce lien {$resetUrl}

", + "to" => [["email" => $user->email]], + ], ]); } $this->messages->addMessage('mail_sent', true); - $url = $routeParser->urlFor('app_index'); - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_index'); } /** - * Valide la page de réinitialisation de mot de passe après soumission du formulaire. Si les mots de passe correspondent, met à jour le mot de passe de l'utilisateur et affiche un message de succès. Sinon, affiche un message d'erreur. - * - * @param Request $request - * @param Response $response - * @return Response + * Réinitialise le mot de passe après soumission du formulaire. */ public function resetPassword(Request $request, Response $response): Response { $data = $request->getParsedBody(); - $password = $data['password'] ?? ''; $passwordRepeat = $data['passwordRepeat'] ?? ''; $token = $data['token'] ?? ''; $user = $this->userRepository->selectByToken($token); - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - - if ($user && $password && $passwordRepeat && $password == $passwordRepeat) { + if ($user && $password && $passwordRepeat && $password === $passwordRepeat) { $this->userRepository->updatePassword($user, $password); $this->messages->addMessage('password_reset', true); - $url = $routeParser->urlFor('app_index'); - } else { - $this->messages->addMessage('password_reset_error', true); - $url = $routeParser->urlFor('app_reset_password', ["token" => $token]); + return $this->redirectToRoute($request, $response, 'app_index'); } - return $response->withHeader('Location', $url)->withStatus(302); + $this->messages->addMessage('password_reset_error', true); + return $this->redirectToRoute($request, $response, 'app_reset_password', ["token" => $token]); } + /** * Détruit la session de l'utilisateur et redirige vers la page d'accueil. - * - * @param Request $request - * @param Response $response - * @return Response */ public function logout(Request $request, Response $response): Response { unset($_SESSION['user']); - - $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $url = $routeParser->urlFor('app_index'); - - return $response->withHeader('Location', $url)->withStatus(302); + return $this->redirectToRoute($request, $response, 'app_index'); } - - /** * Redirige vers l'URL d'autorisation pour l'organisation donnée. - * - * @param Response $response - * @param [type] $organizationSlug - * @return Response - */ - private function redirectionToAuthorizationUrl(Response $response, $organizationSlug): Response - { - - // Génération de l'URL d'autorisation (nouvelle authentification OAuth) - $authorizationUrl = $this->apiWrapper->generateAuthorizationUrl($organizationSlug); - - return $response->withHeader('Location', $authorizationUrl)->withStatus(302); - } - - /** - * Redirige vers l'URL d'autorisation pour l'organisation donnée. Si un token existe déjà, tente de le rafraîchir avant de rediriger. - * - * @param Request $request - * @param Response $response - * @return Response + * Si un token existe déjà et est encore valide, le rafraîchit et affiche un message. */ public function redirectAuthPage(Request $request, Response $response): Response { - $organizationSlug = $request->getQueryParams()['organizationSlug']; - if ($organizationSlug == null) { + $organizationSlug = $request->getQueryParams()['organizationSlug'] ?? null; + if (!$organizationSlug) { throw new Exception("Erreur : OrganizationSlug introuvable"); } - $organizationToken = $this->accessTokenRepository->selectBySlug($organizationSlug); + $existingToken = $this->accessTokenRepository->selectBySlug($organizationSlug); - if ($organizationToken != null) { + if ($existingToken) { try { $this->apiWrapper->getOrganizationAccessToken($organizationSlug); - $response->getBody()->write('Nous possédons déjà un token pour le compte ' . $organizationSlug . ' et nous l\'avons rafraichi, vous pouvez fermer cette page.'); - - } catch (Exception $e) { - - return $this->redirectionToAuthorizationUrl($response, $organizationSlug); - } - } - else { - return $this->redirectionToAuthorizationUrl($response, $organizationSlug); + $response->getBody()->write( + 'Nous possédons déjà un token pour le compte ' . $organizationSlug + . ' et nous l\'avons rafraichi, vous pouvez fermer cette page.' + ); + return $response; + } catch (Exception) { + // Token invalide → on redirige vers la mire d'auth + } } - return $response; + $authorizationUrl = $this->apiWrapper->generateAuthorizationUrl($organizationSlug); + return $response->withHeader('Location', $authorizationUrl)->withStatus(302); } /** - * Valide la page d'autorisation après redirection depuis HelloAsso. Si une erreur est présente dans les query params, l'affiche. Sinon, échange le code d'autorisation contre un token d'accès, le stocke et affiche un message de succès. - * - * @param Request $request - * @param Response $response - * @return Response + * Callback OAuth : échange le code d'autorisation, stocke les tokens et notifie par email si c'est un nouveau compte. */ public function validateAuthPage(Request $request, Response $response): Response { - $error = $request->getQueryParams()['error'] ?? null; - $errorDescription = $request->getQueryParams()['error_description'] ?? null; + $queryParams = $request->getQueryParams(); + $error = $queryParams['error'] ?? null; if ($error) { - $response->getBody()->write($errorDescription); + $response->getBody()->write($queryParams['error_description'] ?? 'Erreur inconnue'); return $response; } - $state = $request->getQueryParams()['state']; - $code = $request->getQueryParams()['code']; + $state = $queryParams['state']; + $code = $queryParams['code']; $authorizationCodeData = $this->authorizationCodeRepository->selectById($state); - $redirect_uri = $authorizationCodeData->redirect_uri; - $codeVerifier = $authorizationCodeData->code_verifier; + $tokenData = $this->apiWrapper->exchangeAuthorizationCode( + $code, + $authorizationCodeData->redirect_uri, + $authorizationCodeData->code_verifier, + ); - $tokenDataGrantAuthorization = $this->apiWrapper->exchangeAuthorizationCode($code, $redirect_uri, $codeVerifier); - - if ($authorizationCodeData->organization_slug !== $tokenDataGrantAuthorization['organization_slug']) { + if ($authorizationCodeData->organization_slug !== $tokenData['organization_slug']) { $this->logger->warning('Incohérence de slug lors de l\'échange du code d\'autorisation', [ 'slug_attendu' => $authorizationCodeData->organization_slug, - 'slug_reçu' => $tokenDataGrantAuthorization['organization_slug'], + 'slug_reçu' => $tokenData['organization_slug'], ]); $response->getBody()->write('Erreur : le slug de l\'association ne correspond pas à celui attendu. L\'authentification a été annulée.'); return $response->withStatus(400); } - $isNewToken = $this->accessTokenRepository->selectBySlug($tokenDataGrantAuthorization['organization_slug']) === null; - $this->apiWrapper->storeOrUpdateToken($tokenDataGrantAuthorization); + $isNewToken = $this->accessTokenRepository->selectBySlug($tokenData['organization_slug']) === null; + $this->apiWrapper->storeOrUpdateToken($tokenData); if ($isNewToken) { $this->mailchimp->messages->send([ @@ -244,18 +188,13 @@ public function validateAuthPage(Request $request, Response $response): Response "from_email" => "contact@helloasso.io", "from_name" => "HelloAsso", "subject" => "Une association vient de valider sa mire", - "html" => "

L'association " . $tokenDataGrantAuthorization['organization_slug'] . " vient de valider sa mire d'authorisation sur l'environnement " . $_SERVER['WEBSITE_DOMAIN'] . "

", - "to" => [ - [ - "email" => "helloasso.stream@helloasso.org" - ] - ], - ] + "html" => "

L'association {$tokenData['organization_slug']} vient de valider sa mire d'authorisation sur l'environnement {$_SERVER['WEBSITE_DOMAIN']}

", + "to" => [["email" => "helloasso.stream@helloasso.org"]], + ], ]); } - $response->getBody()->write('Votre compte ' . $tokenDataGrantAuthorization['organization_slug'] . ' à bien été lié à HelloAssoCharityStream, vous pouvez fermer cette page.'); - + $response->getBody()->write('Votre compte ' . $tokenData['organization_slug'] . ' à bien été lié à HelloAssoCharityStream, vous pouvez fermer cette page.'); return $response; } } diff --git a/src/Controllers/WidgetController.php b/src/Controllers/WidgetController.php index 8d62aa2..4bc6743 100644 --- a/src/Controllers/WidgetController.php +++ b/src/Controllers/WidgetController.php @@ -36,9 +36,23 @@ private function jsonError(Response $response, string $message, int $status): Re return $this->jsonResponse($response, ['error' => $message], $status); } + private function calculatePercentage(int $amountInCents, int $goal): int + { + $safeGoal = $goal ?: 1; + return min(100, (int) round(($amountInCents / 100) / $safeGoal * 100)); + } + + private function requireIdArg(array $args, string $label): string + { + $id = $args['id'] ?? ''; + if (!$id) { + throw new Exception("$label ID manquant ou incorrect."); + } + return $id; + } + /** * Agrège les montants de tous les streams d'un event via le cache par stream. - * Retourne le cache mis à jour avec le total. */ private function aggregateEventStreams(array $streams, array $cacheData, bool $trackDonors = false): array { @@ -92,20 +106,40 @@ private function aggregateEventStreams(array $streams, array $cacheData, bool $t return $cacheData; } - // ── Event Donation ─────────────────────────────────────────── + // ── Stream donation: shared logic ──────────────────────────── - public function widgetEventDonation(Request $request, Response $response, array $args): Response + private function fetchStreamDonationData(string $streamGuid): array { - $eventGuid = $args['id'] ?? ''; - if (!$eventGuid) { - throw new Exception("Event ID manquant ou incorrect."); + $charityStream = $this->streamRepository->selectByGuid($streamGuid); + if (!$charityStream) { + throw new Exception("Charity Stream non trouvé."); } - $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $eventGuid); - if (!$donationGoalWidget) { - throw new Exception("Aucun widget trouvé pour le Event ID fourni."); + $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream) + ?? ['amount' => 0, 'continuation_token' => '']; + + $result = $this->apiWrapper->getAllOrders( + $charityStream->organization_slug, + $charityStream->form_slug, + $cacheData['amount'], + $cacheData['continuation_token'], + ); + + if ($cacheData['continuation_token'] !== $result['continuation_token'] + || $cacheData['amount'] !== $result['amount']) { + $this->widgetRepository->updateStreamDonationWidgetCacheData($charityStream->guid, [ + 'amount' => $result['amount'], + 'continuation_token' => $result['continuation_token'], + ]); } + return ['stream' => $charityStream, 'result' => $result]; + } + + // ── Event donation: shared logic ───────────────────────────── + + private function fetchEventDonationData(string $eventGuid): array + { $event = $this->eventRepository->selectByGuid($eventGuid); if (!$event) { throw new Exception("Event non trouvé."); @@ -122,51 +156,112 @@ public function widgetEventDonation(Request $request, Response $response, array $this->widgetRepository->updateEventDonationWidgetCacheData($event->guid, $cacheData); } - $data = [ - "donationGoalWidget" => $donationGoalWidget, - "currentAmount" => $cacheData['amount'], - "event" => 1 - ]; + return ['event' => $event, 'cacheData' => $cacheData]; + } + + // ── Stream card: shared logic ──────────────────────────────── + + private function fetchStreamCardData(string $streamGuid): array + { + $charityStream = $this->streamRepository->selectByGuid($streamGuid); + if (!$charityStream) { + throw new Exception("Charity Stream non trouvé."); + } + + $cacheData = $this->widgetRepository->selectStreamCardWidgetCacheData($charityStream) + ?? ['amount' => 0, 'donors' => 0, 'continuation_token' => '']; + + $result = $this->apiWrapper->getAllOrders( + $charityStream->organization_slug, + $charityStream->form_slug, + $cacheData['amount'], + $cacheData['continuation_token'], + ); + + $newDonors = count($result['donations'] ?? []); + $donors = ($cacheData['donors'] ?? 0) + $newDonors; + + if ($cacheData['continuation_token'] !== $result['continuation_token'] + || $cacheData['amount'] !== $result['amount']) { + $this->widgetRepository->updateStreamCardWidgetCacheData($charityStream->guid, [ + 'amount' => $result['amount'], + 'donors' => $donors, + 'continuation_token' => $result['continuation_token'], + ]); + } + + return ['stream' => $charityStream, 'amount' => $result['amount'], 'donors' => $donors]; + } + + // ── Event card: shared logic ───────────────────────────────── + + private function fetchEventCardData(string $eventGuid): array + { + $event = $this->eventRepository->selectByGuid($eventGuid); + if (!$event) { + throw new Exception("Event non trouvé."); + } + + $cacheData = $this->widgetRepository->selectEventCardWidgetCacheData($event) + ?? ['amount' => 0, 'donors' => 0, 'streams' => []]; + + $streams = $this->streamRepository->selectListByEvent($event); + $oldAmount = $cacheData['amount']; + $cacheData = $this->aggregateEventStreams($streams, $cacheData, true); + + if ($oldAmount !== $cacheData['amount']) { + $this->widgetRepository->updateEventCardWidgetCacheData($event->guid, $cacheData); + } - return $this->view->render($response, 'widget/donation.html.twig', $data); + return ['event' => $event, 'amount' => $cacheData['amount'], 'donors' => $cacheData['donors']]; + } + + // ── Event Donation ─────────────────────────────────────────── + + public function widgetEventDonation(Request $request, Response $response, array $args): Response + { + $eventGuid = $this->requireIdArg($args, 'Event'); + + $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $eventGuid); + if (!$donationGoalWidget) { + throw new Exception("Aucun widget trouvé pour le Event ID fourni."); + } + + $data = $this->fetchEventDonationData($eventGuid); + + return $this->view->render($response, 'widget/donation.html.twig', [ + 'donationGoalWidget' => $donationGoalWidget, + 'currentAmount' => $data['cacheData']['amount'], + 'goal' => $data['event']->goal, + 'event' => 1, + ]); } public function widgetEventDonationFetch(Request $request, Response $response, array $args): Response { - $eventId = $args['id'] ?? ''; - if (!$eventId) { + $eventGuid = $args['id'] ?? ''; + if (!$eventGuid) { return $this->jsonError($response, 'Event ID manquant ou incorrect.', 400); } - $event = $this->eventRepository->selectByGuid($eventId); + $event = $this->eventRepository->selectByGuid($eventGuid); if (!$event) { return $this->jsonError($response, 'Event non trouvé.', 404); } - $cacheData = $this->widgetRepository->selectEventDonationWidgetCacheData($event) - ?? ['amount' => 0, 'streams' => []]; - try { - $streams = $this->streamRepository->selectListByEvent($event); - $oldAmount = $cacheData['amount']; - $cacheData = $this->aggregateEventStreams($streams, $cacheData); - - if ($oldAmount !== $cacheData['amount']) { - $this->widgetRepository->updateEventDonationWidgetCacheData($event->guid, $cacheData); - } - - return $this->jsonResponse($response, ['amount' => $cacheData['amount']]); + $data = $this->fetchEventDonationData($eventGuid); + return $this->jsonResponse($response, ['amount' => $data['cacheData']['amount']]); } catch (Exception $e) { return $this->jsonError($response, 'Impossible de récupérer le montant', 500); } } + // ── Stream Alert ───────────────────────────────────────────── + public function widgetAlert(Request $request, Response $response, array $args): Response { - $charityStreamId = $args['id'] ?? ''; - if (!$charityStreamId) { - throw new Exception("Charity Stream ID manquant ou incorrect."); - } + $charityStreamId = $this->requireIdArg($args, 'Charity Stream'); $alertBoxWidget = $this->widgetRepository->selectAlertWidgetByGuid($charityStreamId); if (!$alertBoxWidget) { @@ -174,38 +269,31 @@ public function widgetAlert(Request $request, Response $response, array $args): } $charityStream = $this->streamRepository->selectByGuid($charityStreamId); - if (!$charityStream) { throw new Exception("Charity Stream non trouvé."); } - $cacheData = $this->widgetRepository->selectAlertWidgetCacheData($charityStream); - if (!$cacheData) { - $cacheData = [ - 'continuation_token' => "", - ]; - } + $cacheData = $this->widgetRepository->selectAlertWidgetCacheData($charityStream) + ?? ['continuation_token' => '']; $result = $this->apiWrapper->getAllOrders( $charityStream->organization_slug, $charityStream->form_slug, 0, - $cacheData["continuation_token"], + $cacheData['continuation_token'], ); - if ($cacheData["continuation_token"] !== $result['continuation_token']) { + if ($cacheData['continuation_token'] !== $result['continuation_token']) { $this->widgetRepository->updateAlertWidgetCacheData($charityStream->guid, [ - "continuation_token" => $result['continuation_token'] + 'continuation_token' => $result['continuation_token'], ]); } - $data = [ - "alertBoxWidget" => $alertBoxWidget, - "alertBoxWidgetPictureUrl" => $this->fileManager->getPictureUrl($alertBoxWidget->image), - "alertBoxWidgetSoundUrl" => $this->fileManager->getSoundUrl($alertBoxWidget->sound), - ]; - - return $this->view->render($response, 'widget/alert.html.twig', $data); + return $this->view->render($response, 'widget/alert.html.twig', [ + 'alertBoxWidget' => $alertBoxWidget, + 'alertBoxWidgetPictureUrl' => $this->fileManager->getPictureUrl($alertBoxWidget->image), + 'alertBoxWidgetSoundUrl' => $this->fileManager->getSoundUrl($alertBoxWidget->sound), + ]); } public function widgetAlertFetch(Request $request, Response $response, array $args): Response @@ -233,7 +321,7 @@ public function widgetAlertFetch(Request $request, Response $response, array $ar if ($cacheData['continuation_token'] !== $result['continuation_token']) { $this->widgetRepository->updateAlertWidgetCacheData($charityStream->guid, [ - "continuation_token" => $result['continuation_token'] + 'continuation_token' => $result['continuation_token'], ]); } @@ -243,47 +331,25 @@ public function widgetAlertFetch(Request $request, Response $response, array $ar } } + // ── Stream Donation ────────────────────────────────────────── + public function widgetDonation(Request $request, Response $response, array $args): Response { - $streamGuid = $args['id'] ?? ''; - if (!$streamGuid) { - throw new Exception("Charity Stream ID manquant ou incorrect."); - } + $streamGuid = $this->requireIdArg($args, 'Charity Stream'); $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid($streamGuid, null); if (!$donationGoalWidget) { throw new Exception("Aucun widget trouvé pour le Charity Stream ID fourni."); } - $charityStream = $this->streamRepository->selectByGuid($streamGuid); - if (!$charityStream) { - throw new Exception("Charity Stream non trouvé."); - } - - $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream) - ?? ['amount' => 0, 'continuation_token' => '']; - - $result = $this->apiWrapper->getAllOrders( - $charityStream->organization_slug, - $charityStream->form_slug, - $cacheData['amount'], - $cacheData['continuation_token'], - ); - - if ($cacheData['continuation_token'] !== $result['continuation_token'] || $cacheData['amount'] !== $result['amount']) { - $this->widgetRepository->updateStreamDonationWidgetCacheData($charityStream->guid, [ - "amount" => $result['amount'], - "continuation_token" => $result['continuation_token'] - ]); - } - - $data = [ - "donationGoalWidget" => $donationGoalWidget, - "currentAmount" => $result['amount'], - "stream" => 1 - ]; + $data = $this->fetchStreamDonationData($streamGuid); - return $this->view->render($response, 'widget/donation.html.twig', $data); + return $this->view->render($response, 'widget/donation.html.twig', [ + 'donationGoalWidget' => $donationGoalWidget, + 'currentAmount' => $data['result']['amount'], + 'goal' => $data['stream']->goal, + 'stream' => 1, + ]); } public function widgetDonationFetch(Request $request, Response $response, array $args): Response @@ -298,26 +364,9 @@ public function widgetDonationFetch(Request $request, Response $response, array return $this->jsonError($response, 'Charity Stream non trouvé.', 404); } - $cacheData = $this->widgetRepository->selectStreamDonationWidgetCacheData($charityStream) - ?? ['amount' => 0, 'continuation_token' => '']; - try { - $result = $this->apiWrapper->getAllOrders( - $charityStream->organization_slug, - $charityStream->form_slug, - $cacheData['amount'], - $cacheData['continuation_token'] - ); - - if ($cacheData['continuation_token'] !== $result['continuation_token'] - || $cacheData['amount'] !== $result['amount']) { - $this->widgetRepository->updateStreamDonationWidgetCacheData($charityStream->guid, [ - "amount" => $result['amount'], - "continuation_token" => $result['continuation_token'] - ]); - } - - return $this->jsonResponse($response, $result); + $data = $this->fetchStreamDonationData($charityStreamId); + return $this->jsonResponse($response, $data['result']); } catch (Exception $e) { return $this->jsonError($response, 'Impossible de récupérer les commandes.', 500); } @@ -327,56 +376,24 @@ public function widgetDonationFetch(Request $request, Response $response, array public function widgetStreamCard(Request $request, Response $response, array $args): Response { - $streamGuid = $args['id'] ?? ''; - if (!$streamGuid) { - throw new Exception("Charity Stream ID manquant ou incorrect."); - } + $streamGuid = $this->requireIdArg($args, 'Charity Stream'); $cardWidget = $this->widgetRepository->selectCardWidgetByGuid($streamGuid, null); if (!$cardWidget) { throw new Exception("Aucun widget card trouvé pour le Charity Stream ID fourni."); } - $charityStream = $this->streamRepository->selectByGuid($streamGuid); - if (!$charityStream) { - throw new Exception("Charity Stream non trouvé."); - } + $data = $this->fetchStreamCardData($streamGuid); - $cacheData = $this->widgetRepository->selectStreamCardWidgetCacheData($charityStream) - ?? ['amount' => 0, 'donors' => 0, 'continuation_token' => '']; - - $result = $this->apiWrapper->getAllOrders( - $charityStream->organization_slug, - $charityStream->form_slug, - $cacheData['amount'], - $cacheData['continuation_token'], - ); - - $newDonors = count($result['donations'] ?? []); - $donors = ($cacheData['donors'] ?? 0) + $newDonors; - - if ($cacheData['continuation_token'] !== $result['continuation_token'] || $cacheData['amount'] !== $result['amount']) { - $this->widgetRepository->updateStreamCardWidgetCacheData($charityStream->guid, [ - "amount" => $result['amount'], - "donors" => $donors, - "continuation_token" => $result['continuation_token'] - ]); - } - - $currentAmount = $result['amount']; - $goal = $cardWidget->goal ?: 1; - $percentage = min(100, round(($currentAmount / 100) / $goal * 100)); - - $data = [ - "cardWidget" => $cardWidget, - "cardWidgetPictureUrl" => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, - "currentAmount" => $currentAmount, - "donorCount" => $donors, - "percentage" => $percentage, - "stream" => 1 - ]; - - return $this->view->render($response, 'widget/card.html.twig', $data); + return $this->view->render($response, 'widget/card.html.twig', [ + 'cardWidget' => $cardWidget, + 'cardWidgetPictureUrl' => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, + 'currentAmount' => $data['amount'], + 'donorCount' => $data['donors'], + 'percentage' => $this->calculatePercentage($data['amount'], $data['stream']->goal), + 'goal' => $data['stream']->goal ?: 1, + 'stream' => 1, + ]); } public function widgetStreamCardFetch(Request $request, Response $response, array $args): Response @@ -386,35 +403,9 @@ public function widgetStreamCardFetch(Request $request, Response $response, arra return $this->jsonError($response, 'Charity Stream ID manquant ou incorrect.', 400); } - $charityStream = $this->streamRepository->selectByGuid($charityStreamId); - if (!$charityStream) { - return $this->jsonError($response, 'Charity Stream non trouvé.', 404); - } - - $cacheData = $this->widgetRepository->selectStreamCardWidgetCacheData($charityStream) - ?? ['amount' => 0, 'donors' => 0, 'continuation_token' => '']; - try { - $result = $this->apiWrapper->getAllOrders( - $charityStream->organization_slug, - $charityStream->form_slug, - $cacheData['amount'], - $cacheData['continuation_token'] - ); - - $newDonors = count($result['donations'] ?? []); - $donors = ($cacheData['donors'] ?? 0) + $newDonors; - - if ($cacheData['continuation_token'] !== $result['continuation_token'] - || $cacheData['amount'] !== $result['amount']) { - $this->widgetRepository->updateStreamCardWidgetCacheData($charityStream->guid, [ - "amount" => $result['amount'], - "donors" => $donors, - "continuation_token" => $result['continuation_token'] - ]); - } - - return $this->jsonResponse($response, ['amount' => $result['amount'], 'donors' => $donors]); + $data = $this->fetchStreamCardData($charityStreamId); + return $this->jsonResponse($response, ['amount' => $data['amount'], 'donors' => $data['donors']]); } catch (Exception $e) { return $this->jsonError($response, 'Impossible de récupérer les données.', 500); } @@ -424,45 +415,24 @@ public function widgetStreamCardFetch(Request $request, Response $response, arra public function widgetEventCard(Request $request, Response $response, array $args): Response { - $eventGuid = $args['id'] ?? ''; - if (!$eventGuid) { - throw new Exception("Event ID manquant ou incorrect."); - } + $eventGuid = $this->requireIdArg($args, 'Event'); $cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $eventGuid); if (!$cardWidget) { throw new Exception("Aucun widget card trouvé pour le Event ID fourni."); } - $event = $this->eventRepository->selectByGuid($eventGuid); - if (!$event) { - throw new Exception("Event non trouvé."); - } - - $cacheData = $this->widgetRepository->selectEventCardWidgetCacheData($event) - ?? ['amount' => 0, 'donors' => 0, 'streams' => []]; - - $streams = $this->streamRepository->selectListByEvent($event); - $oldAmount = $cacheData['amount']; - $cacheData = $this->aggregateEventStreams($streams, $cacheData, true); - - if ($oldAmount !== $cacheData['amount']) { - $this->widgetRepository->updateEventCardWidgetCacheData($event->guid, $cacheData); - } + $data = $this->fetchEventCardData($eventGuid); - $goal = $cardWidget->goal ?: 1; - $percentage = min(100, round(($cacheData['amount'] / 100) / $goal * 100)); - - $data = [ - "cardWidget" => $cardWidget, - "cardWidgetPictureUrl" => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, - "currentAmount" => $cacheData['amount'], - "donorCount" => $cacheData['donors'], - "percentage" => $percentage, - "event" => 1 - ]; - - return $this->view->render($response, 'widget/card.html.twig', $data); + return $this->view->render($response, 'widget/card.html.twig', [ + 'cardWidget' => $cardWidget, + 'cardWidgetPictureUrl' => $cardWidget->image ? $this->fileManager->getPictureUrl($cardWidget->image) : null, + 'currentAmount' => $data['amount'], + 'donorCount' => $data['donors'], + 'percentage' => $this->calculatePercentage($data['amount'], $data['event']->goal), + 'goal' => $data['event']->goal ?: 1, + 'event' => 1, + ]); } public function widgetEventCardFetch(Request $request, Response $response, array $args): Response @@ -472,27 +442,9 @@ public function widgetEventCardFetch(Request $request, Response $response, array return $this->jsonError($response, 'Event ID manquant ou incorrect.', 400); } - $event = $this->eventRepository->selectByGuid($eventId); - if (!$event) { - return $this->jsonError($response, 'Event non trouvé.', 404); - } - - $cacheData = $this->widgetRepository->selectEventCardWidgetCacheData($event) - ?? ['amount' => 0, 'donors' => 0, 'streams' => []]; - try { - $streams = $this->streamRepository->selectListByEvent($event); - $oldAmount = $cacheData['amount']; - $cacheData = $this->aggregateEventStreams($streams, $cacheData, true); - - if ($oldAmount !== $cacheData['amount']) { - $this->widgetRepository->updateEventCardWidgetCacheData($event->guid, $cacheData); - } - - return $this->jsonResponse($response, [ - 'amount' => $cacheData['amount'], - 'donors' => $cacheData['donors'] - ]); + $data = $this->fetchEventCardData($eventId); + return $this->jsonResponse($response, ['amount' => $data['amount'], 'donors' => $data['donors']]); } catch (Exception $e) { return $this->jsonError($response, 'Impossible de récupérer les données.', 500); } diff --git a/src/Models/Event.php b/src/Models/Event.php index eb18f7b..cef136a 100644 --- a/src/Models/Event.php +++ b/src/Models/Event.php @@ -7,6 +7,7 @@ class Event public $id; public $guid; public $title; + public $goal; public $creation_date; public $last_update; public $admin; diff --git a/src/Models/Stream.php b/src/Models/Stream.php index 2b7f322..d13ed3d 100644 --- a/src/Models/Stream.php +++ b/src/Models/Stream.php @@ -8,6 +8,7 @@ class Stream public $charity_event_id; public $guid; public $title; + public $goal; public $form_slug; public $organization_slug; public $creation_date; diff --git a/src/Repositories/EventRepository.php b/src/Repositories/EventRepository.php index 52e6172..8585c10 100644 --- a/src/Repositories/EventRepository.php +++ b/src/Repositories/EventRepository.php @@ -139,6 +139,28 @@ public function insert(string $title): Event } } + public function update(Event $event, array $data): void + { + $fields = []; + $params = []; + + if (array_key_exists('title', $data)) { + $fields[] = 'title = ?'; + $params[] = $data['title']; + } + if (array_key_exists('goal', $data)) { + $fields[] = 'goal = ?'; + $params[] = $data['goal']; + } + + if (empty($fields)) return; + + $params[] = $event->id; + $sql = 'UPDATE ' . $this->prefix . 'charity_event SET ' . implode(', ', $fields) . ' WHERE id = ?'; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + } + public function delete(Event $event) { $this->pdo->beginTransaction(); diff --git a/src/Repositories/StreamRepository.php b/src/Repositories/StreamRepository.php index dfdbe81..201ac9b 100644 --- a/src/Repositories/StreamRepository.php +++ b/src/Repositories/StreamRepository.php @@ -164,6 +164,28 @@ public function insert(string $form_slug, string $organization_slug, string $tit } } + public function update(Stream $stream, array $data): void + { + $fields = []; + $params = []; + + if (array_key_exists('title', $data)) { + $fields[] = 'title = ?'; + $params[] = $data['title']; + } + if (array_key_exists('goal', $data)) { + $fields[] = 'goal = ?'; + $params[] = $data['goal']; + } + + if (empty($fields)) return; + + $params[] = $stream->id; + $sql = 'UPDATE ' . $this->prefix . 'charity_stream SET ' . implode(', ', $fields) . ' WHERE id = ?'; + $stmt = $this->pdo->prepare($sql); + $stmt->execute($params); + } + public function delete(Stream $stream): void { $this->pdo->beginTransaction(); diff --git a/src/Services/ApiWrapper.php b/src/Services/ApiWrapper.php index 45aae85..c17e52e 100644 --- a/src/Services/ApiWrapper.php +++ b/src/Services/ApiWrapper.php @@ -18,10 +18,9 @@ class ApiWrapper { - private $client; + private Client $client; public function __construct( - private AccessTokenRepository $accessTokenRepository, private AuthorizationCodeRepository $authorizationCodeRepository, private string $haAuthUrl, @@ -30,48 +29,65 @@ public function __construct( private string $clientId, private string $clientSecret, private string $webSiteDomain, - private Logger $apiLogger - + private Logger $apiLogger, ) { $this->client = new Client(); } - /** - * Génère un token d'accès global en utilisant le flux client_credentials, et le stocke en base de données. + * Exécute une requête HTTP via Guzzle avec gestion d'erreur centralisée. * - * @return AccessToken + * @return \Psr\Http\Message\ResponseInterface */ - private function generateGlobalAccessToken(): AccessToken + private function httpRequest(string $method, string $url, array $options, string $errorContext): \Psr\Http\Message\ResponseInterface { try { - $response = $this->client->request('POST', $this->apiAuthUrl, [ - 'form_params' => [ - 'grant_type' => 'client_credentials', - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret - ], - 'headers' => [ - 'content-type' => 'application/x-www-form-urlencoded', - 'accept' => 'application/json', - ], - ]); + return $this->client->request($method, $url, $options); } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors de la génération du token global: ' . $e->getMessage()); + $this->apiLogger->error("Erreur lors de {$errorContext}: " . $e->getMessage()); if ($e->hasResponse()) { $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); } - throw new Exception("Erreur lors de la requête d'authentification : " . $e->getMessage(), 0, $e); + throw new Exception("Erreur lors de {$errorContext} : " . $e->getMessage(), 0, $e); } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors de la génération du token global: ' . $e->getMessage()); + $this->apiLogger->error("Erreur Guzzle lors de {$errorContext}: " . $e->getMessage()); throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); } + } - $responseData = json_decode($response->getBody(), true); - + /** + * Décode la réponse JSON et vérifie sa validité. + */ + private function decodeJsonResponse(\Psr\Http\Message\ResponseInterface $response): array + { + $data = json_decode($response->getBody(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception("Erreur de décodage JSON : " . json_last_error_msg()); } + return $data; + } + + + /** + * Génère un token d'accès global en utilisant le flux client_credentials, et le stocke en base de données. + * + * @return AccessToken + */ + private function generateGlobalAccessToken(): AccessToken + { + $response = $this->httpRequest('POST', $this->apiAuthUrl, [ + 'form_params' => [ + 'grant_type' => 'client_credentials', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + ], + 'headers' => [ + 'content-type' => 'application/x-www-form-urlencoded', + 'accept' => 'application/json', + ], + ], 'la génération du token global'); + + $responseData = $this->decodeJsonResponse($response); if (!isset($responseData['access_token']) || !isset($responseData['refresh_token'])) { throw new Exception("Erreur : Les tokens ne sont pas présents dans la réponse."); @@ -107,37 +123,20 @@ private function generateGlobalAccessToken(): AccessToken * @param string $organization_slug * @return AccessToken */ - public function refreshToken($refreshToken, $organization_slug): ?AccessToken - { - try { - $response = $this->client->request('POST', $this->apiAuthUrl, [ - 'form_params' => [ - 'grant_type' => 'refresh_token', - 'refresh_token' => $refreshToken, - ], - 'headers' => [ - 'content-type' => 'application/x-www-form-urlencoded', - 'accept' => 'application/json', - ], - ]); - } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors du refresh token pour ' . $organization_slug . ': ' . $e->getMessage()); - if ($e->hasResponse()) { - $statusCode = $e->getResponse()->getStatusCode(); - $this->apiLogger->error('Response status: ' . $statusCode); - $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); - } - throw new Exception("Erreur lors du rafraîchissement du token : " . $e->getMessage(), 0, $e); - } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors du refresh token pour ' . $organization_slug . ': ' . $e->getMessage()); - throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); - } - - $responseData = json_decode($response->getBody(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception("Erreur de décodage JSON : " . json_last_error_msg()); - } + public function refreshToken(string $refreshToken, string $organizationSlug): ?AccessToken + { + $response = $this->httpRequest('POST', $this->apiAuthUrl, [ + 'form_params' => [ + 'grant_type' => 'refresh_token', + 'refresh_token' => $refreshToken, + ], + 'headers' => [ + 'content-type' => 'application/x-www-form-urlencoded', + 'accept' => 'application/json', + ], + ], "le refresh token pour {$organizationSlug}"); + + $responseData = $this->decodeJsonResponse($response); if (!isset($responseData['access_token']) || !isset($responseData['refresh_token'])) { throw new Exception("Erreur : Les tokens ne sont pas présents dans la réponse."); @@ -149,13 +148,11 @@ public function refreshToken($refreshToken, $organization_slug): ?AccessToken $obj = new AccessToken(); $obj->access_token = $responseData['access_token']; $obj->refresh_token = $responseData['refresh_token']; - $obj->organization_slug = $organization_slug; + $obj->organization_slug = $organizationSlug; $obj->access_token_expires_at = $accessTokenExpiresAt; $obj->refresh_token_expires_at = $refreshTokenExpiresAt; - $this->apiLogger->info('New organisation access token generated successfully. it will expires at '.$obj->access_token_expires_at->format('Y-m-d H:i:s')); - return $this->accessTokenRepository->update( - $obj - ); + $this->apiLogger->info('New organisation access token generated successfully. it will expires at ' . $obj->access_token_expires_at->format('Y-m-d H:i:s')); + return $this->accessTokenRepository->update($obj); } /** @@ -188,32 +185,29 @@ public function getGlobalAccessToken(): AccessToken * @param string $organization_slug * @return AccessToken|null */ - public function getOrganizationAccessToken(string $organization_slug): AccessToken + public function getOrganizationAccessToken(string $organizationSlug): AccessToken { - $tokenData = $this->accessTokenRepository->selectBySlug($organization_slug); + $tokenData = $this->accessTokenRepository->selectBySlug($organizationSlug); - $expiration_refresh_date = $tokenData->refresh_token_expires_at ?? false; - $expiration_access_date = $tokenData->access_token_expires_at ?? false; + if ($tokenData === null) { + $this->apiLogger->error('Aucun token trouvé pour organization_slug: ' . $organizationSlug); + throw new Exception('Aucun token trouvé pour l\'organisation: ' . $organizationSlug); + } - if ($this->isExpired($expiration_access_date)) { - $this->apiLogger->debug('Access token for organization_slug: ' . $organization_slug . ' is expired. Attempting to refresh token.'); - $tokenData = $this->refreshToken( $tokenData->refresh_token, $organization_slug); - $this->apiLogger->info('Access token refreshed for organization_slug: ' . $organization_slug . '. New expiry time: ' . - ($tokenData->access_token_expires_at instanceof DateTime ? $tokenData->access_token_expires_at->format('Y-m-d H:i:s') : $tokenData->access_token_expires_at)); + $this->apiLogger->info('Check expiration for access token of organization_slug: ' . $organizationSlug); - } - - $this->apiLogger->info('Check expiration for access token of organization_slug: ' . $organization_slug); - if ($this->isExpired($expiration_refresh_date)) { - $this->apiLogger->error('Refresh token is expired for organization_slug: ' . $organization_slug); + if ($this->isExpired($tokenData->refresh_token_expires_at ?? false)) { + $this->apiLogger->error('Refresh token is expired for organization_slug: ' . $organizationSlug); throw new Exception('Invalid token data: refresh_token is expired'); } - if ($tokenData === null) { - $this->apiLogger->error('Aucun token trouvé pour organization_slug: ' . $organization_slug); - throw new Exception('Aucun token trouvé pour l\'organisation: ' . $organization_slug); + if ($this->isExpired($tokenData->access_token_expires_at ?? false)) { + $this->apiLogger->debug('Access token for organization_slug: ' . $organizationSlug . ' is expired. Attempting to refresh token.'); + $tokenData = $this->refreshToken($tokenData->refresh_token, $organizationSlug); + $this->apiLogger->info('Access token refreshed for organization_slug: ' . $organizationSlug . '. New expiry time: ' . + ($tokenData->access_token_expires_at instanceof \DateTime ? $tokenData->access_token_expires_at->format('Y-m-d H:i:s') : $tokenData->access_token_expires_at)); } - + return $tokenData; } @@ -223,15 +217,15 @@ public function getOrganizationAccessToken(string $organization_slug): AccessTok * @param [type] $expirationDate * @return boolean */ - private function isExpired($expirationDate): bool + private function isExpired(string|\DateTime|false $expirationDate): bool { - if(!$expirationDate) { + if (!$expirationDate) { return true; } - $expiration = is_string($expirationDate) ? new DateTime($expirationDate) : $expirationDate; - $now = new DateTime(); + $expiration = is_string($expirationDate) ? new \DateTime($expirationDate) : $expirationDate; + $now = new \DateTime(); $this->apiLogger->debug('Current time: ' . $now->format('Y-m-d H:i:s')); - $this->apiLogger->debug('Refresh token expiry time: ' . $expiration->format('Y-m-d H:i:s')); + $this->apiLogger->debug('Token expiry time: ' . $expiration->format('Y-m-d H:i:s')); return $expiration < $now; } @@ -279,33 +273,18 @@ public function getDonationForms(string $organizationSlug): array { $tokenData = $this->getOrganizationAccessToken($organizationSlug); - try { - $response = $this->client->request('GET', "{$this->apiUrl}/organizations/{$organizationSlug}/forms", [ - 'query' => [ - 'formTypes' => 'Donation', - 'pageSize' => 50, - ], - 'headers' => [ - 'Authorization' => 'Bearer ' . $tokenData->access_token, - 'accept' => 'application/json', - ], - ]); - } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors de la récupération des formulaires de don: ' . $e->getMessage()); - if ($e->hasResponse()) { - $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); - } - throw new Exception("Erreur lors de la récupération des formulaires : " . $e->getMessage(), 0, $e); - } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors de la récupération des formulaires: ' . $e->getMessage()); - throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); - } - - $data = json_decode($response->getBody(), true); + $response = $this->httpRequest('GET', "{$this->apiUrl}/organizations/{$organizationSlug}/forms", [ + 'query' => [ + 'formTypes' => 'Donation', + 'pageSize' => 50, + ], + 'headers' => [ + 'Authorization' => 'Bearer ' . $tokenData->access_token, + 'accept' => 'application/json', + ], + ], "la récupération des formulaires de don pour {$organizationSlug}"); - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception("Erreur de décodage JSON : " . json_last_error_msg()); - } + $data = $this->decodeJsonResponse($response); return $data['data'] ?? []; } @@ -316,29 +295,16 @@ public function getDonationForms(string $organizationSlug): array * @param [type] $accessToken * @return void */ - public function setClientDomain($accessToken) + public function setClientDomain(string $accessToken): void { - try { - $this->client->request('PUT', "$this->apiUrl/partners/me/api-clients", [ - 'body' => json_encode([ - "Domain" => $this->webSiteDomain - ]), - 'headers' => [ - 'content-type' => 'application/*+json', - 'accept' => 'application/json', - 'Authorization' => "Bearer $accessToken", - ], - ]); - } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors de la configuration du domaine client: ' . $e->getMessage()); - if ($e->hasResponse()) { - $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); - } - throw new Exception("Erreur lors de la configuration du domaine : " . $e->getMessage(), 0, $e); - } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors de la configuration du domaine client: ' . $e->getMessage()); - throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); - } + $this->httpRequest('PUT', "{$this->apiUrl}/partners/me/api-clients", [ + 'body' => json_encode(["Domain" => $this->webSiteDomain]), + 'headers' => [ + 'content-type' => 'application/*+json', + 'accept' => 'application/json', + 'Authorization' => "Bearer {$accessToken}", + ], + ], 'la configuration du domaine client'); } /** @@ -373,39 +339,24 @@ public function storeOrUpdateToken(array $tokenData): AccessToken * @param [type] $codeVerifier * @return void */ - public function exchangeAuthorizationCode($code, $redirect_uri, $codeVerifier) + public function exchangeAuthorizationCode(string $code, string $redirectUri, string $codeVerifier): array { - try { - $response = $this->client->request('POST', $this->apiAuthUrl, [ - 'form_params' => [ - 'grant_type' => 'authorization_code', - 'client_id' => $this->clientId, - 'client_secret' => $this->clientSecret, - 'code' => $code, - 'redirect_uri' => $redirect_uri, - 'code_verifier' => $codeVerifier - ], - 'headers' => [ - 'content-type' => 'application/x-www-form-urlencoded', - 'accept' => 'application/json', - ], - ]); - } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors de l\'échange du code d\'autorisation: ' . $e->getMessage()); - if ($e->hasResponse()) { - $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); - } - throw new Exception("Erreur lors de l'échange du code d'autorisation : " . $e->getMessage(), 0, $e); - } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors de l\'échange du code d\'autorisation: ' . $e->getMessage()); - throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); - } - - $responseData = json_decode($response->getBody(), true); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new Exception("Erreur de décodage JSON : " . json_last_error_msg()); - } + $response = $this->httpRequest('POST', $this->apiAuthUrl, [ + 'form_params' => [ + 'grant_type' => 'authorization_code', + 'client_id' => $this->clientId, + 'client_secret' => $this->clientSecret, + 'code' => $code, + 'redirect_uri' => $redirectUri, + 'code_verifier' => $codeVerifier, + ], + 'headers' => [ + 'content-type' => 'application/x-www-form-urlencoded', + 'accept' => 'application/json', + ], + ], "l'échange du code d'autorisation"); + + $responseData = $this->decodeJsonResponse($response); if ( !isset($responseData['access_token']) || @@ -428,31 +379,25 @@ public function exchangeAuthorizationCode($code, $redirect_uri, $codeVerifier) * @param [type] $continuationToken * @return array */ - private function getDonationFormOrders($organizationSlug, $donationSlug, $accessToken, $continuationToken = null) + private function getDonationFormOrders(string $organizationSlug, string $donationSlug, string $accessToken, ?string $continuationToken = null): array { $query = ['withDetails' => 'true', 'sortOrder' => 'asc']; if ($continuationToken) { $query['continuationToken'] = $continuationToken; } - try { - $response = $this->client->request('GET', $this->apiUrl . '/organizations/' . $organizationSlug . '/forms/donation/' . $donationSlug . '/orders', [ + $response = $this->httpRequest( + 'GET', + "{$this->apiUrl}/organizations/{$organizationSlug}/forms/donation/{$donationSlug}/orders", + [ 'query' => $query, 'headers' => [ 'Authorization' => 'Bearer ' . $accessToken, 'accept' => 'application/json', ], - ]); - } catch (RequestException $e) { - $this->apiLogger->error('Erreur lors de la récupération des commandes: ' . $e->getMessage()); - if ($e->hasResponse()) { - $this->apiLogger->error('Response body: ' . $e->getResponse()->getBody()); - } - throw new Exception("Erreur lors de la récupération des commandes : " . $e->getMessage(), 0, $e); - } catch (GuzzleException $e) { - $this->apiLogger->error('Erreur Guzzle lors de la récupération des commandes: ' . $e->getMessage()); - throw new Exception("Erreur de connexion à l'API : " . $e->getMessage(), 0, $e); - } + ], + "la récupération des commandes pour {$organizationSlug}/{$donationSlug}", + ); return json_decode($response->getBody(), true); } @@ -513,13 +458,11 @@ public function getAllOrders(string $organizationSlug, string $formSlug, int $cu $amount = isset($order['amount']['total']) && is_numeric($order['amount']['total']) ? $order['amount']['total'] : 0; $currentAmount += $amount; - $donation = [ + $donations[] = [ "pseudo" => $pseudo, "message" => $message, - "amount" => $amount + "amount" => $amount, ]; - - array_push($donations, $donation); } $previousToken = $continuationToken; diff --git a/src/views/event/edit.html.twig b/src/views/event/edit.html.twig index 236c112..5b1c100 100644 --- a/src/views/event/edit.html.twig +++ b/src/views/event/edit.html.twig @@ -6,6 +6,36 @@ Retour + {# ── Informations de l'événement ──────────────────────────── #} +
+
+
📋 Informations de l'événement
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+
+ {# ── Streams liés à cet événement ────────────────────────── #}
@@ -79,22 +109,18 @@
-
- - -
-

{{ donationGoalWidget.goal / 2 }} €

+

{{ event.goal / 2 }} €

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} €

+

{{ event.goal }} €

-

{{ donationGoalWidget.goal / 2 }} €

+

{{ event.goal / 2 }} €

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} €

+

{{ event.goal }} €

@@ -143,10 +169,6 @@
-
- - -
Couleurs
@@ -188,12 +210,12 @@
{{ cardWidget.title }}
{{ cardWidget.description }}
-
{{ (cardWidget.goal / 2) }} €
+
{{ (event.goal / 2) }} €
-
Objectif : {{ cardWidget.goal }} €
+
Objectif : {{ event.goal }} €
50% collectés diff --git a/src/views/stream/edit.html.twig b/src/views/stream/edit.html.twig index 26aa910..32d5b7d 100644 --- a/src/views/stream/edit.html.twig +++ b/src/views/stream/edit.html.twig @@ -16,6 +16,36 @@
{% endif %} + {# ── Informations du stream ────────────────────────────────── #} +
+
+
📋 Informations du stream
+
+
+
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+
+
Formulaire de don Helloasso {{ donationUrl }} @@ -57,22 +87,18 @@
-
- - -
-

{{ donationGoalWidget.goal / 2 }} €

+

{{ charityStream.goal / 2 }} €

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} €

+

{{ charityStream.goal }} €

-

{{ donationGoalWidget.goal / 2 }} €

+

{{ charityStream.goal / 2 }} €

{{ donationGoalWidget.text_content }}

-

{{ donationGoalWidget.goal }} €

+

{{ charityStream.goal }} €

@@ -190,15 +216,11 @@
-
- - -
-
- - -
+
+ +
+
Couleurs
@@ -239,12 +261,12 @@
{{ cardWidget.title }}
{{ cardWidget.description }}
-
{{ (cardWidget.goal / 2) }} €
+
{{ (charityStream.goal / 2) }} €
-
Objectif : {{ cardWidget.goal }} €
+
Objectif : {{ charityStream.goal }} €
50% collectés diff --git a/src/views/widget/card.html.twig b/src/views/widget/card.html.twig index fbca762..89b4382 100644 --- a/src/views/widget/card.html.twig +++ b/src/views/widget/card.html.twig @@ -149,7 +149,7 @@
-
Objectif : {{ cardWidget.goal }} €
+
Objectif : {{ goal }} €
{{ percentage }}% collectés @@ -161,7 +161,7 @@ {% endblock %} {% block scripts %} + diff --git a/src/views/stream/index-admin.html.twig b/src/views/stream/index-admin.html.twig index 0897a88..7cae04c 100644 --- a/src/views/stream/index-admin.html.twig +++ b/src/views/stream/index-admin.html.twig @@ -1,4 +1,3 @@ - {% block title %}HelloAsso Widgets - Administration{% endblock %} {% extends "layout.html.twig" %} @@ -50,217 +49,8 @@
-

HelloAsso Widgets

@@ -468,127 +258,5 @@ {% endblock %} {% block scripts %} - + {% include 'stream/_scripts-create-stream.html.twig' %} {% endblock %} \ No newline at end of file diff --git a/src/views/stream/index.html.twig b/src/views/stream/index.html.twig index 95f18b4..336b530 100644 --- a/src/views/stream/index.html.twig +++ b/src/views/stream/index.html.twig @@ -8,7 +8,7 @@