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 %}
+
+
+ |
{% 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);
+ }
}