diff --git a/.github/workflows/php-sandbox.yml b/.github/workflows/php-sandbox.yml index 0cc98c1..be1a82b 100644 --- a/.github/workflows/php-sandbox.yml +++ b/.github/workflows/php-sandbox.yml @@ -2,7 +2,7 @@ name: Build & deploy sandbox on: push: - branches: [ "develop" ] + branches: [ "develop"] workflow_dispatch: permissions: diff --git a/migrations/19-stream-owner.sql b/migrations/19-stream-owner.sql new file mode 100644 index 0000000..7fbf189 --- /dev/null +++ b/migrations/19-stream-owner.sql @@ -0,0 +1,2 @@ +-- Fix: les créateurs de streams existants doivent avoir is_owner = 1 +UPDATE {prefix}user_right SET is_owner = 1 WHERE id_charity_stream IS NOT NULL; diff --git a/src/Controllers/AdminController.php b/src/Controllers/AdminController.php index 82eba7a..857f671 100644 --- a/src/Controllers/AdminController.php +++ b/src/Controllers/AdminController.php @@ -2,6 +2,7 @@ namespace App\Controllers; +use App\Models\User; use App\Repositories\AccessTokenRepository; use App\Repositories\AuthorizationCodeRepository; use App\Repositories\EventRepository; @@ -170,6 +171,42 @@ public function deleteUser(Request $request, Response $response, array $args): R return $this->redirectToRoute($request, $response, 'app_admin_index', [], ['tab' => 'users']); } + private function provisionAdminUser(string $email, Request $request): User + { + $existing = $this->userRepository->select($email); + if ($existing) { + return $existing; + } + $user = $this->userRepository->insert($email); + $user = $this->userRepository->insertResetToken($user); + $this->sendAdminInvitationEmail($user, $request); + return $user; + } + + private function sendAdminInvitationEmail(User $user, Request $request): void + { + $routeParser = RouteContext::fromRequest($request)->getRouteParser(); + $resetUrl = $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_reset_password', ["token" => $user->reset_token]); + + try { + $result = $this->mailchimp->messages->send([ + "message" => [ + "from_email" => "contact@helloasso.io", + "from_name" => "HelloAsso", + "subject" => "Bienvenue sur HelloAsso Stream !", + "html" => $this->buildWelcomeEmail($resetUrl), + "to" => [["email" => $user->email, "type" => "to"]], + ], + ]); + + if ($result instanceof \Exception) { + throw $result; + } + } catch (\Exception $e) { + $this->messages->addMessage('error', 'Admin ajouté mais l\'email d\'invitation n\'a pas pu être envoyé.'); + } + } + /** * Génère le contenu HTML de l'email de bienvenue envoyé aux nouveaux utilisateurs. */ @@ -242,7 +279,7 @@ public function editEvent(Request $request, Response $response, array $args): Re $cardWidget = $this->widgetRepository->selectCardWidgetByGuid(null, $event->guid); $streams = $this->streamRepository->selectListByEvent($event); $routeParser = RouteContext::fromRequest($request)->getRouteParser(); - $isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isEventOwner($user, $event); + $isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $event); $data = [ "logged" => true, @@ -255,7 +292,7 @@ public function editEvent(Request $request, Response $response, array $args): Re "widgetDonationGoalUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_donation', ["id" => $event->guid]), "widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_event_widget_card', ["id" => $event->guid]), "eventGoals" => $this->goalRepository->selectAmountsByEventGuid($event->guid), - "eventAdmins" => $this->userRepository->selectEventAdmins($event), + "eventAdmins" => $this->userRepository->selectAdmins($event), "isEventOwner" => $isEventOwner, "currentUserId" => $user->id, ]; @@ -309,19 +346,18 @@ public function editEventPost(Request $request, Response $response, array $args) } // Gestion des admins (owner ou ADMIN global uniquement) - $isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isEventOwner($user, $event); + $isEventOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $event); if ($isEventOwner && isset($body['add_admin'])) { $email = trim($body['admin_email'] ?? ''); if ($email) { - $newAdmin = $this->userRepository->findOrCreate($email); - $existing = $this->userRepository->selectEventAdmins($event); - $alreadyIn = array_filter($existing, fn($a) => (int) $a['id'] === $newAdmin->id); - if (empty($alreadyIn)) { + $existing = $this->userRepository->selectAdmins($event); + if (in_array($email, array_column($existing, 'email'))) { + $this->messages->addMessage('info', "{$email} est déjà admin de cet évènement"); + } else { + $newAdmin = $this->provisionAdminUser($email, $request); $this->userRepository->insertRight($newAdmin, null, $event, false); $this->messages->addMessage('success', "Admin {$email} ajouté"); - } else { - $this->messages->addMessage('info', "{$email} est déjà admin de cet évènement"); } } } @@ -329,7 +365,7 @@ public function editEventPost(Request $request, Response $response, array $args) if ($isEventOwner && isset($body['remove_admin'])) { $removeId = (int) ($body['remove_admin'] ?? 0); if ($removeId && $removeId !== $user->id) { - $this->userRepository->deleteEventRight($removeId, $event); + $this->userRepository->deleteRight($removeId, $event); $this->messages->addMessage('success', 'Admin retiré'); } } @@ -360,7 +396,7 @@ public function newStream(Request $request, Response $response): Response } $stream = $this->streamRepository->insert($data['form_slug'], $data['organization_slug'], $data['title'], $event->id ?? null, $data['form_type'] ?? 'Donation'); - $this->userRepository->insertRight($owner, $stream, null); + $this->userRepository->insertRight($owner, $stream, null, true); if ($event !== null && $parentStyle) { $donationGoalWidget = $this->widgetRepository->selectDonationWidgetByGuid(null, $event->guid); @@ -422,6 +458,8 @@ public function editStream(Request $request, Response $response, array $args): R $donationUrl = $_SERVER['HA_URL'] . '/associations/' . $charityStream->organization_slug . '/' . $formTypeUrlSegment . '/' . $charityStream->form_slug; $routeParser = RouteContext::fromRequest($request)->getRouteParser(); + $isStreamOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $charityStream); + $data = [ "logged" => true, "charityStream" => $charityStream, @@ -439,6 +477,9 @@ public function editStream(Request $request, Response $response, array $args): R "widgetCardUrl" => $_SERVER['WEBSITE_DOMAIN'] . $routeParser->urlFor('app_stream_widget_card', ["id" => $guid]), "messages" => $this->messages->getMessages(), "streamGoals" => $this->goalRepository->selectAmountsByStreamGuid($guid), + "streamAdmins" => $this->userRepository->selectAdmins($charityStream), + "isStreamOwner" => $isStreamOwner, + "currentUserId" => $user->id, ]; return $this->view->render($response, 'stream/edit.html.twig', $data); @@ -519,6 +560,31 @@ public function editStreamPost(Request $request, Response $response, array $args } } + // Gestion des admins (owner ou ADMIN global uniquement) + $isStreamOwner = $user->role === 'ADMIN' || $this->userRepository->isOwner($user, $charityStream); + + if ($isStreamOwner && isset($body['add_admin'])) { + $email = trim($body['admin_email'] ?? ''); + if ($email) { + $existing = $this->userRepository->selectAdmins($charityStream); + if (in_array($email, array_column($existing, 'email'))) { + $this->messages->addMessage('info', "{$email} est déjà admin de ce stream"); + } else { + $newAdmin = $this->provisionAdminUser($email, $request); + $this->userRepository->insertRight($newAdmin, $charityStream, null, false); + $this->messages->addMessage('success', "Admin {$email} ajouté"); + } + } + } + + if ($isStreamOwner && isset($body['remove_admin'])) { + $removeId = (int) ($body['remove_admin'] ?? 0); + if ($removeId && $removeId !== $user->id) { + $this->userRepository->deleteRight($removeId, $charityStream); + $this->messages->addMessage('success', 'Admin retiré'); + } + } + $this->handleWidgetFormSave($request, $guid, null); return $this->redirectToRoute($request, $response, 'app_stream_edit', ["id" => $guid]); diff --git a/src/Models/Event.php b/src/Models/Event.php index 9683723..a3ccdb4 100644 --- a/src/Models/Event.php +++ b/src/Models/Event.php @@ -2,7 +2,7 @@ namespace App\Models; -class Event +class Event implements HasRight { public $id; public $guid; @@ -12,4 +12,7 @@ class Event public $admin; public $is_test_mode = 0; public $test_amount = 0; + + public function getRightColumn(): string { return 'id_charity_event'; } + public function getRightId(): int { return (int) $this->id; } } diff --git a/src/Models/HasRight.php b/src/Models/HasRight.php new file mode 100644 index 0000000..d0bec92 --- /dev/null +++ b/src/Models/HasRight.php @@ -0,0 +1,9 @@ +id; } } diff --git a/src/Repositories/UserRepository.php b/src/Repositories/UserRepository.php index ffc2405..9289862 100644 --- a/src/Repositories/UserRepository.php +++ b/src/Repositories/UserRepository.php @@ -3,6 +3,7 @@ namespace App\Repositories; use App\Models\Event; +use App\Models\HasRight; use App\Models\Stream; use App\Models\User; use DateTime; @@ -74,37 +75,40 @@ public function insertRight(User $user, ?Stream $stream, ?Event $event, bool $is ]); } - public function selectEventAdmins(Event $event): array + public function selectAdmins(HasRight $entity): array { - $stmt = $this->pdo->prepare(' + $col = $entity->getRightColumn(); + $stmt = $this->pdo->prepare(" SELECT u.id, u.email, ur.is_owner - FROM ' . $this->prefix . 'user_right ur - INNER JOIN ' . $this->prefix . 'users u ON u.id = ur.id_user - WHERE ur.id_charity_event = ? + FROM {$this->prefix}user_right ur + INNER JOIN {$this->prefix}users u ON u.id = ur.id_user + WHERE ur.{$col} = ? ORDER BY ur.is_owner DESC, u.email ASC - '); - $stmt->execute([$event->id]); + "); + $stmt->execute([$entity->getRightId()]); return $stmt->fetchAll(PDO::FETCH_ASSOC); } - public function isEventOwner(User $user, Event $event): bool + public function isOwner(User $user, HasRight $entity): bool { - $stmt = $this->pdo->prepare(' - SELECT 1 FROM ' . $this->prefix . 'user_right - WHERE id_user = ? AND id_charity_event = ? AND is_owner = 1 + $col = $entity->getRightColumn(); + $stmt = $this->pdo->prepare(" + SELECT 1 FROM {$this->prefix}user_right + WHERE id_user = ? AND {$col} = ? AND is_owner = 1 LIMIT 1 - '); - $stmt->execute([$user->id, $event->id]); + "); + $stmt->execute([$user->id, $entity->getRightId()]); return (bool) $stmt->fetch(); } - public function deleteEventRight(int $userId, Event $event): void + public function deleteRight(int $userId, HasRight $entity): void { - $stmt = $this->pdo->prepare(' - DELETE FROM ' . $this->prefix . 'user_right - WHERE id_user = ? AND id_charity_event = ? - '); - $stmt->execute([$userId, $event->id]); + $col = $entity->getRightColumn(); + $stmt = $this->pdo->prepare(" + DELETE FROM {$this->prefix}user_right + WHERE id_user = ? AND {$col} = ? + "); + $stmt->execute([$userId, $entity->getRightId()]); } public function select(string $email): ?User diff --git a/src/views/stream/edit.html.twig b/src/views/stream/edit.html.twig index f5c00d8..83da4c7 100644 --- a/src/views/stream/edit.html.twig +++ b/src/views/stream/edit.html.twig @@ -126,6 +126,35 @@ + {# ── Administrateurs ─────────────────────────────────────── #} + {% if isStreamOwner %} +
+
+
👥 Administrateurs
+
+
+
+ {% for admin in streamAdmins %} + + {{ admin.email | e }} + {% if admin.is_owner %} + + {% elseif admin.id != currentUserId %} +
+ +
+ {% endif %} +
+ {% endfor %} +
+
+ + +
+
+
+ {% endif %} + {# ── Objectifs de collecte ───────────────────────────────── #}