From 23b92f101cb9fd50489b9015f13bbd169bf9634e Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 26 Mar 2026 09:04:27 +0100 Subject: [PATCH] Add admin event actions (online/offline, edit, delete) and fix Meilisearch depends_on - Add toggle online/offline and delete routes in AdminController - Add action buttons (En ligne, Modifier, Supprimer) in admin events template - Bypass requireEventOwnership and requireStripeReady for ROLE_ROOT so admin can edit any event - Add Meilisearch healthcheck and depends_on in messenger service (prod + dev) - Add tests for all new admin routes and ROLE_ROOT bypass Co-Authored-By: Claude Opus 4.6 (1M context) --- ansible/docker-compose-prod.yml.j2 | 7 ++ docker-compose-dev.yml | 7 ++ src/Controller/AccountController.php | 7 ++ src/Controller/AdminController.php | 34 +++++- templates/admin/events.html.twig | 16 +++ tests/Controller/AccountControllerTest.php | 24 +++++ tests/Controller/AdminControllerTest.php | 115 +++++++++++++++++++++ 7 files changed, 209 insertions(+), 1 deletion(-) diff --git a/ansible/docker-compose-prod.yml.j2 b/ansible/docker-compose-prod.yml.j2 index 1a5ff37..9ca7925 100644 --- a/ansible/docker-compose-prod.yml.j2 +++ b/ansible/docker-compose-prod.yml.j2 @@ -108,6 +108,8 @@ services: condition: service_healthy redis: condition: service_healthy + meilisearch: + condition: service_healthy redis: image: redis:7-alpine @@ -134,6 +136,11 @@ services: - meilisearch-data:/meili_data networks: - e-ticket + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700/health"] + interval: 5s + timeout: 5s + retries: 5 networks: e-ticket: driver: bridge diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index ceee70b..85b32be 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -71,6 +71,8 @@ services: condition: service_healthy redis: condition: service_healthy + meilisearch: + condition: service_healthy bun: image: oven/bun:alpine @@ -135,6 +137,11 @@ services: - "7700:7700" volumes: - meilisearch-data:/meili_data + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7700/health"] + interval: 5s + timeout: 5s + retries: 5 libretranslate: image: libretranslate/libretranslate:latest diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php index e88bbfd..d607f0c 100644 --- a/src/Controller/AccountController.php +++ b/src/Controller/AccountController.php @@ -1189,6 +1189,9 @@ class AccountController extends AbstractController { /** @var User $user */ $user = $this->getUser(); + if ($this->isGranted('ROLE_ROOT')) { + return $event->getAccount(); + } if ($event->getAccount()->getId() !== $user->getId()) { throw $this->createAccessDeniedException(); } @@ -1209,6 +1212,10 @@ class AccountController extends AbstractController /** @codeCoverageIgnore Tested via testOrganizerWithoutStripeBlocksEventCreation */ private function requireStripeReady(): ?Response { + if ($this->isGranted('ROLE_ROOT')) { + return null; + } + /** @var User $user */ $user = $this->getUser(); diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 12a2de1..c088168 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -4,9 +4,11 @@ namespace App\Controller; use App\Entity\AuditLog; use App\Entity\BilletBuyer; +use App\Entity\Event; use App\Entity\OrganizerInvitation; use App\Entity\User; use App\Service\AuditService; +use App\Service\EventIndexService; use App\Service\ExportService; use App\Service\MailerService; use App\Service\MeilisearchService; @@ -550,7 +552,7 @@ class AdminController extends AbstractController } #[Route('/evenements', name: 'app_admin_events')] - public function events(Request $request, PaginatorInterface $paginator, \App\Service\EventIndexService $eventIndex): Response + public function events(Request $request, PaginatorInterface $paginator, EventIndexService $eventIndex): Response { $searchQuery = $request->query->getString('q', ''); $eventsQuery = $eventIndex->searchEvents('event_admin', $searchQuery); @@ -563,6 +565,36 @@ class AdminController extends AbstractController ]); } + #[Route('/evenement/{id}/en-ligne', name: 'app_admin_toggle_event_online', methods: ['POST'])] + public function toggleEventOnline(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex): Response + { + $event->setIsOnline(!$event->isOnline()); + $em->flush(); + + $eventIndex->indexEvent($event); + + $this->addFlash('success', $event->isOnline() ? 'Evenement mis en ligne.' : 'Evenement passe hors ligne.'); + + return $this->redirectToRoute('app_admin_events'); + } + + #[Route('/evenement/{id}/supprimer', name: 'app_admin_delete_event', methods: ['POST'])] + public function adminDeleteEvent(Event $event, EntityManagerInterface $em, EventIndexService $eventIndex, AuditService $audit): Response + { + $eventTitle = $event->getTitle(); + $eventDbId = $event->getId(); + $eventIndex->removeEvent($event); + + $em->remove($event); + $em->flush(); + + $audit->log('event_deleted', 'Event', $eventDbId, ['title' => $eventTitle]); + + $this->addFlash('success', 'Evenement supprime.'); + + return $this->redirectToRoute('app_admin_events'); + } + #[Route('/export/{year}/{month}', name: 'app_admin_export', requirements: ['year' => '\d{4}', 'month' => '\d{1,2}'], methods: ['GET'])] public function export(int $year, int $month, ExportService $exportService): Response { diff --git a/templates/admin/events.html.twig b/templates/admin/events.html.twig index 0f6107c..c808b64 100644 --- a/templates/admin/events.html.twig +++ b/templates/admin/events.html.twig @@ -32,6 +32,7 @@ Lieu Statut Secret + Actions @@ -66,6 +67,21 @@ {% endif %} + +
+
+ {% if event.online %} + + {% else %} + + {% endif %} +
+ Modifier +
+ +
+
+ {% endfor %} diff --git a/tests/Controller/AccountControllerTest.php b/tests/Controller/AccountControllerTest.php index a66dee9..aa2d339 100644 --- a/tests/Controller/AccountControllerTest.php +++ b/tests/Controller/AccountControllerTest.php @@ -2202,6 +2202,30 @@ class AccountControllerTest extends WebTestCase self::assertResponseIsSuccessful(); } + public function testRootCanEditEventOfAnotherUser(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $owner = $this->createUser(['ROLE_ORGANIZER'], true); + $root = $this->createUser(['ROLE_ROOT']); + + $event = new \App\Entity\Event(); + $event->setAccount($owner); + $event->setTitle('Owner Event Root'); + $event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00')); + $event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00')); + $event->setAddress('1 rue'); + $event->setZipcode('75001'); + $event->setCity('Paris'); + $em->persist($event); + $em->flush(); + + $client->loginUser($root); + $client->request('GET', '/mon-compte/evenement/'.$event->getId().'/modifier'); + + self::assertResponseIsSuccessful(); + } + /** * @param list $roles */ diff --git a/tests/Controller/AdminControllerTest.php b/tests/Controller/AdminControllerTest.php index 8db7f4c..a50ed01 100644 --- a/tests/Controller/AdminControllerTest.php +++ b/tests/Controller/AdminControllerTest.php @@ -2,7 +2,9 @@ namespace App\Tests\Controller; +use App\Entity\Event; use App\Entity\User; +use App\Service\EventIndexService; use App\Service\MailerService; use App\Service\MeilisearchService; use Doctrine\ORM\EntityManagerInterface; @@ -862,4 +864,117 @@ class AdminControllerTest extends WebTestCase self::assertResponseIsSuccessful(); self::assertStringContainsString('application/pdf', $client->getResponse()->headers->get('Content-Type')); } + + private function createEvent(EntityManagerInterface $em, User $organizer): Event + { + $event = new Event(); + $event->setAccount($organizer); + $event->setTitle('Test Event '.uniqid()); + $event->setStartAt(new \DateTimeImmutable('2026-08-01 10:00')); + $event->setEndAt(new \DateTimeImmutable('2026-08-01 18:00')); + $event->setAddress('1 rue de la Paix'); + $event->setZipcode('75001'); + $event->setCity('Paris'); + + $em->persist($event); + $em->flush(); + + return $event; + } + + public function testToggleEventOnline(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $admin = $this->createUser(['ROLE_ROOT']); + $orga = $this->createOrganizer($em); + $event = $this->createEvent($em, $orga); + + $eventIndex = $this->createMock(EventIndexService::class); + $eventIndex->expects(self::once())->method('indexEvent'); + static::getContainer()->set(EventIndexService::class, $eventIndex); + + self::assertFalse($event->isOnline()); + + $client->loginUser($admin); + $client->request('POST', '/admin/evenement/'.$event->getId().'/en-ligne'); + + self::assertResponseRedirects('/admin/evenements'); + + $em->refresh($event); + self::assertTrue($event->isOnline()); + } + + public function testToggleEventOffline(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $admin = $this->createUser(['ROLE_ROOT']); + $orga = $this->createOrganizer($em); + $event = $this->createEvent($em, $orga); + $event->setIsOnline(true); + $em->flush(); + + $eventIndex = $this->createMock(EventIndexService::class); + $eventIndex->expects(self::once())->method('indexEvent'); + static::getContainer()->set(EventIndexService::class, $eventIndex); + + $client->loginUser($admin); + $client->request('POST', '/admin/evenement/'.$event->getId().'/en-ligne'); + + self::assertResponseRedirects('/admin/evenements'); + + $em->refresh($event); + self::assertFalse($event->isOnline()); + } + + public function testDeleteEvent(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $admin = $this->createUser(['ROLE_ROOT']); + $orga = $this->createOrganizer($em); + $event = $this->createEvent($em, $orga); + $eventId = $event->getId(); + + $eventIndex = $this->createMock(EventIndexService::class); + $eventIndex->expects(self::once())->method('removeEvent'); + static::getContainer()->set(EventIndexService::class, $eventIndex); + + $client->loginUser($admin); + $client->request('POST', '/admin/evenement/'.$eventId.'/supprimer'); + + self::assertResponseRedirects('/admin/evenements'); + + $deleted = $em->getRepository(Event::class)->find($eventId); + self::assertNull($deleted); + } + + public function testToggleEventOnlineDeniedForNonRoot(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $user = $this->createUser(); + $orga = $this->createOrganizer($em); + $event = $this->createEvent($em, $orga); + + $client->loginUser($user); + $client->request('POST', '/admin/evenement/'.$event->getId().'/en-ligne'); + + self::assertResponseStatusCodeSame(403); + } + + public function testDeleteEventDeniedForNonRoot(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + $user = $this->createUser(); + $orga = $this->createOrganizer($em); + $event = $this->createEvent($em, $orga); + + $client->loginUser($user); + $client->request('POST', '/admin/evenement/'.$event->getId().'/supprimer'); + + self::assertResponseStatusCodeSame(403); + } }