Add organizer suspension: toggle, badge, block access, audit log

- User.isSuspended (nullable bool, null = not suspended)
- Admin: toggle suspend/reactivate button on organizers list
- SuspendedUserSubscriber: redirects suspended users to homepage with error
- Audit log: organizer_suspended / organizer_reactivated
- Badge 'Suspendu' (red) replaces offer badge when suspended

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-03-22 20:53:46 +01:00
parent 64d0a6fd29
commit 372bf46136
6 changed files with 123 additions and 4 deletions

View File

@@ -20,13 +20,13 @@
### Paiements & Finances
- [ ] Ajouter le dashboard financier pour l'orga (total encaissé, en attente, remboursé)
- [ ] Ajouter les virements Stripe (payouts) dans l'onglet de l'orga
- [ ] Ajouter les virements Stripe (payouts) dans l'onglet de l'orga (il exite déja c'est "Virement")
- [ ] Générer un récapitulatif mensuel des ventes (export CSV/PDF)
### Admin
- [ ] Dashboard admin : stats globales (CA global, commission E-Ticket totale, commission Stripe totale, nb commandes, nb billets, nb orgas)
- [ ] Admin : liste de toutes les commandes avec filtres
- [ ] Admin : pouvoir suspendre un organisateur
- [x] Admin : pouvoir suspendre/réactiver un organisateur (badge, bouton toggle, redirect si suspendu, audit log)
- [x] Admin : pouvoir modifier l'offre/commission d'un orga existant
- [ ] Vérifier que les permissions des sous-comptes sont respectées (scanner, events, tickets)
- [x] Admin : logs des actions importantes (audit trail: commande, paiement, annulation, remboursement)

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20260322130000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add is_suspended to user';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" ADD COLUMN IF NOT EXISTS is_suspended BOOLEAN DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE "user" DROP COLUMN IF EXISTS is_suspended');
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Controller;
use App\Entity\AuditLog;
use App\Entity\OrganizerInvitation;
use App\Entity\User;
use App\Service\AuditService;
use App\Service\MailerService;
use App\Service\MeilisearchService;
use App\Service\SiretService;
@@ -430,6 +431,25 @@ class AdminController extends AbstractController
]);
}
#[Route('/organisateur/{id}/suspendre', name: 'app_admin_suspend_organizer', methods: ['POST'])]
public function suspendOrganizer(User $user, EntityManagerInterface $em, AuditService $audit): Response
{
$suspended = !$user->isSuspended();
$user->setIsSuspended($suspended ?: null);
$em->flush();
$audit->log($suspended ? 'organizer_suspended' : 'organizer_reactivated', 'User', $user->getId(), [
'email' => $user->getEmail(),
'companyName' => $user->getCompanyName(),
]);
$this->addFlash('success', $suspended
? 'Organisateur '.$user->getCompanyName().' suspendu.'
: 'Organisateur '.$user->getCompanyName().' reactive.');
return $this->redirectToRoute('app_admin_organizers', ['tab' => 'approved']);
}
#[Route('/evenements', name: 'app_admin_events')]
public function events(Request $request, PaginatorInterface $paginator, \App\Service\EventIndexService $eventIndex): Response
{

View File

@@ -94,6 +94,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\Column]
private bool $isApproved = false;
#[ORM\Column(nullable: true)]
private ?bool $isSuspended = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $offer = null;
@@ -510,6 +513,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
public function isSuspended(): ?bool
{
return $this->isSuspended;
}
public function setIsSuspended(?bool $isSuspended): static
{
$this->isSuspended = $isSuspended;
return $this;
}
public function getOffer(): ?string
{
return $this->offer;

View File

@@ -0,0 +1,51 @@
<?php
namespace App\EventSubscriber;
use App\Entity\User;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class SuspendedUserSubscriber implements EventSubscriberInterface
{
public function __construct(
private Security $security,
private UrlGeneratorInterface $urlGenerator,
) {
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::REQUEST => ['onKernelRequest', 8],
];
}
public function onKernelRequest(RequestEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$user = $this->security->getUser();
if (!$user instanceof User) {
return;
}
if (!$user->isSuspended()) {
return;
}
$route = $event->getRequest()->attributes->getString('_route');
if (\in_array($route, ['app_logout', 'app_home', 'app_login'], true)) {
return;
}
$event->getRequest()->getSession()->getFlashBag()->add('error', 'Votre compte a ete suspendu. Contactez contact@e-cosplay.fr.');
$event->setResponse(new RedirectResponse($this->urlGenerator->generate('app_home')));
}
}

View File

@@ -69,9 +69,16 @@
{% if not orga.approved %}
<a href="{{ path('app_admin_siret_check', {id: orga.id}) }}" class="admin-btn-sm-yellow inline-block text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Voir la demande</a>
{% else %}
<div class="flex gap-2 justify-end items-center">
<div class="flex gap-2 justify-end items-center flex-wrap">
{% if orga.suspended %}
<span class="admin-badge-red text-xs font-black uppercase">Suspendu</span>
{% else %}
<span class="admin-badge-green text-xs font-black uppercase">{{ orga.offer ?? '—' }}{{ orga.commissionRate ?? '3' }}%</span>
{% endif %}
<a href="{{ path('app_admin_edit_organizer', {id: orga.id}) }}" class="admin-btn-sm-white inline-block text-xs font-black uppercase tracking-widest hover:bg-indigo-600 hover:text-black transition-all">Modifier</a>
<form method="post" action="{{ path('app_admin_suspend_organizer', {id: orga.id}) }}" data-confirm="{{ orga.suspended ? 'Reactiver cet organisateur ?' : 'Suspendre cet organisateur ?' }}" class="inline">
<button type="submit" class="admin-btn-sm-white inline-block text-xs font-black uppercase tracking-widest {{ orga.suspended ? 'hover:bg-green-600' : 'hover:bg-red-600' }} hover:text-white transition-all">{{ orga.suspended ? 'Reactiver' : 'Suspendre' }}</button>
</form>
</div>
{% endif %}
</td>