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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Lieu</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Statut</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-center">Secret</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -66,6 +67,21 @@
|
||||
<span class="text-xs text-gray-300">—</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<div class="flex gap-2 justify-end">
|
||||
<form method="post" action="{{ path('app_admin_toggle_event_online', {id: event.id}) }}">
|
||||
{% if event.online %}
|
||||
<button type="submit" class="admin-btn-sm-yellow text-xs font-black uppercase tracking-widest">Hors ligne</button>
|
||||
{% else %}
|
||||
<button type="submit" class="admin-btn-sm-white text-xs font-black uppercase tracking-widest">En ligne</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
<a href="{{ path('app_account_edit_event', {id: event.id}) }}" class="admin-btn-sm-white text-xs font-black uppercase tracking-widest">Modifier</a>
|
||||
<form method="post" action="{{ path('app_admin_delete_event', {id: event.id}) }}" data-confirm="Supprimer l'evenement « {{ event.title }} » ? Cette action est irreversible.">
|
||||
<button type="submit" class="admin-btn-sm-danger text-xs font-black uppercase tracking-widest">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
||||
@@ -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<string> $roles
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user