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:
@@ -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)
|
||||
|
||||
26
migrations/Version20260322130000.php
Normal file
26
migrations/Version20260322130000.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
51
src/EventSubscriber/SuspendedUserSubscriber.php
Normal file
51
src/EventSubscriber/SuspendedUserSubscriber.php
Normal 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')));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user