22
33namespace App \Controllers ;
44
5+ use App \Models \AccessToken ;
6+ use App \Repositories \AccessTokenRepository ;
7+ use App \Repositories \AuthorizationCodeRepository ;
58use App \Repositories \EventRepository ;
69use App \Repositories \FileManager ;
710use App \Repositories \StreamRepository ;
811use App \Repositories \UserRepository ;
912use App \Repositories \WidgetRepository ;
13+ use App \Services \ApiWrapper ;
14+ use DateInterval ;
15+ use DateTime ;
16+ use Exception ;
1017use Psr \Http \Message \ResponseInterface as Response ;
1118use Psr \Http \Message \ServerRequestInterface as Request ;
1219use Slim \Flash \Messages ;
@@ -23,6 +30,9 @@ public function __construct(
2330 private UserRepository $ userRepository ,
2431 private WidgetRepository $ widgetRepository ,
2532 private Messages $ messages ,
33+ private ApiWrapper $ apiWrapper ,
34+ private AccessTokenRepository $ accessTokenRepository ,
35+ private AuthorizationCodeRepository $ authorizationCodeRepository ,
2636 ) {}
2737
2838 public function index (Request $ request , Response $ response ): Response
@@ -40,6 +50,7 @@ public function index(Request $request, Response $response): Response
4050 "streams " => $ streams ,
4151 "events " => $ events ,
4252 "messages " => $ this ->messages ->getMessages (),
53+ "currentUser " => $ user ,
4354 ];
4455
4556 $ template = $ user ->role == "ADMIN " ? 'stream/index-admin.html.twig ' : 'stream/index.html.twig ' ;
@@ -238,4 +249,139 @@ public function editStreamPost(Request $request, Response $response, array $args
238249
239250 return $ response ->withHeader ('Location ' , $ url )->withStatus (302 );
240251 }
252+
253+ /**
254+ * Génère l'URL d'autorisation OAuth HelloAsso pour connecter une association dans le cadre de la création d'un stream.
255+ * Retourne un JSON { "url": "..." } pour que le front puisse ouvrir la mire dans un nouvel onglet.
256+ *
257+ * @param Request $request
258+ * @param Response $response
259+ * @return Response
260+ */
261+ public function initStreamAuth (Request $ request , Response $ response ): Response
262+ {
263+ $ streamCallbackUrl = $ _SERVER ['WEBSITE_DOMAIN ' ] . '/admin/stream/auth-callback ' ;
264+ $ authorizationUrl = $ this ->apiWrapper ->generateAuthorizationUrl (null , $ streamCallbackUrl );
265+
266+ $ response ->getBody ()->write (json_encode (['url ' => $ authorizationUrl ]));
267+ return $ response ->withHeader ('Content-Type ' , 'application/json ' );
268+ }
269+
270+ /**
271+ * Callback OAuth pour la connexion d'une association lors de la création d'un stream.
272+ * Échange le code d'autorisation, stocke les tokens et récupère la liste des formulaires de don.
273+ * Retourne une page HTML qui transmet les données à la fenêtre parente via postMessage.
274+ *
275+ * @param Request $request
276+ * @param Response $response
277+ * @return Response
278+ */
279+ public function streamAuthCallback (Request $ request , Response $ response ): Response
280+ {
281+ $ error = $ request ->getQueryParams ()['error ' ] ?? null ;
282+ $ errorDescription = $ request ->getQueryParams ()['error_description ' ] ?? null ;
283+
284+ if ($ error ) {
285+ $ response ->getBody ()->write ($ this ->buildCallbackPage (null , [], $ errorDescription ));
286+ return $ response ;
287+ }
288+
289+ $ state = $ request ->getQueryParams ()['state ' ] ?? null ;
290+ $ code = $ request ->getQueryParams ()['code ' ] ?? null ;
291+
292+ if (!$ state || !$ code ) {
293+ $ response ->getBody ()->write ($ this ->buildCallbackPage (null , [], 'Paramètres manquants dans la réponse. ' ));
294+ return $ response ;
295+ }
296+
297+ try {
298+ $ authorizationCodeData = $ this ->authorizationCodeRepository ->selectById ($ state );
299+ if (!$ authorizationCodeData ) {
300+ throw new Exception ("State invalide ou expiré. " );
301+ }
302+
303+ $ tokenData = $ this ->apiWrapper ->exchangeAuthorizationCode (
304+ $ code ,
305+ $ authorizationCodeData ->redirect_uri ,
306+ $ authorizationCodeData ->code_verifier
307+ );
308+
309+ $ organizationSlug = $ tokenData ['organization_slug ' ];
310+
311+ // Stocker / mettre à jour les tokens
312+ $ existingToken = $ this ->accessTokenRepository ->selectBySlug ($ organizationSlug );
313+
314+ $ token = new AccessToken ();
315+ $ token ->access_token = $ tokenData ['access_token ' ];
316+ $ token ->refresh_token = $ tokenData ['refresh_token ' ];
317+ $ token ->organization_slug = $ organizationSlug ;
318+ $ token ->access_token_expires_at = (new DateTime ())->add (new DateInterval ('PT28M ' ));
319+ $ token ->refresh_token_expires_at = (new DateTime ())->add (new DateInterval ('P28D ' ));
320+
321+ if ($ existingToken === null ) {
322+ $ this ->accessTokenRepository ->insert ($ token );
323+ } else {
324+ $ this ->accessTokenRepository ->update ($ token );
325+ }
326+
327+ // Récupérer les formulaires de don
328+ $ forms = $ this ->apiWrapper ->getDonationForms ($ organizationSlug );
329+ } catch (Exception $ e ) {
330+ $ response ->getBody ()->write ($ this ->buildCallbackPage (null , [], $ e ->getMessage ()));
331+ return $ response ;
332+ }
333+
334+ $ response ->getBody ()->write ($ this ->buildCallbackPage ($ organizationSlug , $ forms ));
335+ return $ response ;
336+ }
337+
338+ /**
339+ * Construit la page HTML de callback OAuth qui communique les données à la fenêtre parente via postMessage.
340+ */
341+ private function buildCallbackPage (?string $ organizationSlug , array $ forms , ?string $ error = null ): string
342+ {
343+ $ organizationSlugJson = json_encode ($ organizationSlug );
344+ $ formsJson = json_encode ($ forms );
345+ $ errorJson = json_encode ($ error );
346+
347+ return <<<HTML
348+ <!DOCTYPE html>
349+ <html lang="fr">
350+ <head>
351+ <meta charset="UTF-8">
352+ <title>Connexion HelloAsso</title>
353+ <style>
354+ body { font-family: sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #1a1a2e; color: #eee; }
355+ .card { background: #16213e; padding: 2rem; border-radius: 12px; text-align: center; max-width: 400px; }
356+ .success { color: #4ade80; font-size: 2rem; }
357+ .error { color: #f87171; }
358+ </style>
359+ </head>
360+ <body>
361+ <div class="card">
362+ <div id="msg"><p>Connexion en cours, veuillez patienter...</p></div>
363+ </div>
364+ <script>
365+ var organizationSlug = {$ organizationSlugJson };
366+ var forms = {$ formsJson };
367+ var error = {$ errorJson };
368+
369+ if (error) {
370+ document.getElementById('msg').innerHTML = '<p class="error">❌ Erreur : ' + error + '</p><p>Vous pouvez fermer cet onglet.</p>';
371+ } else if (window.opener && !window.opener.closed) {
372+ window.opener.postMessage({
373+ type: 'ha_stream_auth_success',
374+ organizationSlug: organizationSlug,
375+ forms: forms
376+ }, window.location.origin);
377+ document.getElementById('msg').innerHTML = '<p class="success">✅</p><p>Association connectée ! Fermeture en cours...</p>';
378+ setTimeout(function() { window.close(); }, 1500);
379+ } else {
380+ document.getElementById('msg').innerHTML = '<p class="success">✅ Association connectée !</p><p>Vous pouvez fermer cet onglet et retourner à la page d\'administration.</p>';
381+ }
382+ </script>
383+ </body>
384+ </html>
385+ HTML ;
386+ }
241387}
0 commit comments