From df7680d938dfcdf3135e026d6d09b6cec80256eb Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Thu, 19 Mar 2026 14:07:07 +0100 Subject: [PATCH] Add admin panel, Meilisearch buyer search, email redesign, and multiple features Admin panel (/admin, ROLE_ROOT): - Dashboard with CA HT Global/Commission cards and Meilisearch sync button - Buyers page with search (Meilisearch), create form, pagination (KnpPaginator) - Buyer actions: resend verification, force verify, reset password, delete - Organizers page with tabs (pending/approved), approve/reject with emails - Neo-brutalist design matching main site theme - Vite admin entry point with dedicated SCSS - CSP-compatible confirm dialogs via data-confirm attributes Meilisearch integration: - Auto-index buyers on email verification - Remove from index on buyer deletion - Manual sync button on dashboard - Search bar on buyers page - Add Meilisearch service to CI/SonarQube workflows - Add MEILISEARCH env vars to .env.test - Fix MeilisearchMessageHandler infinite loop: use request() directly instead of service methods that re-dispatch messages Email templates: - Redesign base email template to neo-brutalist style (borders, shadows, yellow footer) - Add E-Cosplay logo, "E-Ticket solution proposee par e-cosplay.fr" - Add admin_reset_password, organizer_approved, organizer_rejected templates Other: - Install knplabs/knp-paginator-bundle - Add ^/admin access_control for ROLE_ROOT in security.yaml - Update site footer with E-Ticket branding - 18 admin tests, updated MeilisearchMessageHandler tests Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.test | 2 + .gitea/workflows/ci.yml | 14 + .gitea/workflows/sonarqube.yml | 7 + assets/admin.js | 11 + assets/admin.scss | 1 + composer.json | 1 + composer.lock | 166 ++++++++- config/bundles.php | 1 + config/packages/security.yaml | 1 + src/Controller/AdminController.php | 294 +++++++++++++++ src/Controller/RegistrationController.php | 14 +- .../MeilisearchMessageHandler.php | 17 +- symfony.lock | 3 + templates/admin/base.html.twig | 47 +++ templates/admin/buyers.html.twig | 111 ++++++ templates/admin/create_buyer.html.twig | 57 +++ templates/admin/dashboard.html.twig | 29 ++ templates/admin/organizers.html.twig | 88 +++++ templates/admin/users.html.twig | 47 +++ templates/base.html.twig | 3 +- .../email/admin_reset_password.html.twig | 13 + templates/email/base.html.twig | 79 ++-- templates/email/organizer_approved.html.twig | 12 + templates/email/organizer_rejected.html.twig | 12 + tests/Controller/AdminControllerTest.php | 350 ++++++++++++++++++ .../MeilisearchMessageHandlerTest.php | 28 +- vite.config.js | 1 + 27 files changed, 1346 insertions(+), 63 deletions(-) create mode 100644 assets/admin.js create mode 100644 assets/admin.scss create mode 100644 src/Controller/AdminController.php create mode 100644 templates/admin/base.html.twig create mode 100644 templates/admin/buyers.html.twig create mode 100644 templates/admin/create_buyer.html.twig create mode 100644 templates/admin/dashboard.html.twig create mode 100644 templates/admin/organizers.html.twig create mode 100644 templates/admin/users.html.twig create mode 100644 templates/email/admin_reset_password.html.twig create mode 100644 templates/email/organizer_approved.html.twig create mode 100644 templates/email/organizer_rejected.html.twig create mode 100644 tests/Controller/AdminControllerTest.php diff --git a/.env.test b/.env.test index 64bd111..978776d 100644 --- a/.env.test +++ b/.env.test @@ -1,3 +1,5 @@ # define your env variables for the test env here KERNEL_CLASS='App\Kernel' APP_SECRET='$ecretf0rt3st' +MEILISEARCH_URL=http://meilisearch:7700 +MEILISEARCH_API_KEY=test diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e9fe71f..3da1a30 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -29,10 +29,17 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 5 + meilisearch: + image: getmeili/meilisearch:latest + env: + MEILI_MASTER_KEY: test + MEILI_ENV: development env: DATABASE_URL: "postgresql://app:secret@database:5432/e_ticket?serverVersion=16&charset=utf8" MESSENGER_TRANSPORT_DSN: "redis://redis:6379/messages" MAILER_DSN: "null://null" + MEILISEARCH_URL: "http://meilisearch:7700" + MEILISEARCH_API_KEY: "test" APP_ENV: test steps: - name: Checkout @@ -126,10 +133,17 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 5 + meilisearch: + image: getmeili/meilisearch:latest + env: + MEILI_MASTER_KEY: test + MEILI_ENV: development env: DATABASE_URL: "postgresql://app:secret@database:5432/e_ticket?serverVersion=16&charset=utf8" MESSENGER_TRANSPORT_DSN: "redis://redis:6379/messages" MAILER_DSN: "null://null" + MEILISEARCH_URL: "http://meilisearch:7700" + MEILISEARCH_API_KEY: "test" APP_ENV: test steps: - name: Checkout diff --git a/.gitea/workflows/sonarqube.yml b/.gitea/workflows/sonarqube.yml index 4b7b4c6..9058c4e 100644 --- a/.gitea/workflows/sonarqube.yml +++ b/.gitea/workflows/sonarqube.yml @@ -25,10 +25,17 @@ jobs: --health-interval 5s --health-timeout 5s --health-retries 5 + meilisearch: + image: getmeili/meilisearch:latest + env: + MEILI_MASTER_KEY: test + MEILI_ENV: development env: DATABASE_URL: "postgresql://app:secret@database:5432/e_ticket?serverVersion=16&charset=utf8" MESSENGER_TRANSPORT_DSN: "redis://redis:6379/messages" MAILER_DSN: "null://null" + MEILISEARCH_URL: "http://meilisearch:7700" + MEILISEARCH_API_KEY: "test" APP_ENV: test steps: - name: Checkout diff --git a/assets/admin.js b/assets/admin.js new file mode 100644 index 0000000..0662b1c --- /dev/null +++ b/assets/admin.js @@ -0,0 +1,11 @@ +import "./admin.scss" + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-confirm]').forEach(form => { + form.addEventListener('submit', (e) => { + if (!confirm(form.dataset.confirm)) { + e.preventDefault() + } + }) + }) +}) diff --git a/assets/admin.scss b/assets/admin.scss new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/assets/admin.scss @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/composer.json b/composer.json index 444b74f..c196185 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/orm": "^3.6", "dompdf/dompdf": "*", "endroid/qr-code-bundle": "*", + "knplabs/knp-paginator-bundle": "^6.10", "knpuniversity/oauth2-client-bundle": "^2.20", "league/flysystem-aws-s3-v3": "^3.32", "league/flysystem-bundle": "^3.6", diff --git a/composer.lock b/composer.lock index b614b8b..35b0944 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ecb55cf346fc28e16c4caec521a016e2", + "content-hash": "53da9b302a2b28356f861372e8679637", "packages": [ { "name": "aws/aws-crt-php", @@ -2387,6 +2387,170 @@ }, "time": "2025-11-30T20:12:26+00:00" }, + { + "name": "knplabs/knp-components", + "version": "v5.2.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/knp-components.git", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/knp-components/zipball/eabf39263fff305c0024820c3736e5b03e7edf50", + "reference": "eabf39263fff305c0024820c3736e5b03e7edf50", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/event-dispatcher-contracts": "^3.0" + }, + "conflict": { + "doctrine/dbal": "<3.8" + }, + "require-dev": { + "doctrine/dbal": "^3.8 || ^4.0", + "doctrine/mongodb-odm": "^2.5.5", + "doctrine/orm": "^2.13 || ^3.0", + "doctrine/phpcr-odm": "^1.8 || ^2.0", + "ext-pdo_sqlite": "*", + "jackalope/jackalope-doctrine-dbal": "^1.12 || ^2.0", + "phpunit/phpunit": "^10.5 || ^11.3", + "propel/propel1": "^1.7", + "ruflin/elastica": "^7.0", + "solarium/solarium": "^6.0", + "symfony/http-foundation": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/http-kernel": "^5.4.38 || ^6.4.4 || ^7.0", + "symfony/property-access": "^5.4.38 || ^6.4.4 || ^7.0" + }, + "suggest": { + "doctrine/common": "to allow usage pagination with Doctrine ArrayCollection", + "doctrine/mongodb-odm": "to allow usage pagination with Doctrine ODM MongoDB", + "doctrine/orm": "to allow usage pagination with Doctrine ORM", + "doctrine/phpcr-odm": "to allow usage pagination with Doctrine ODM PHPCR", + "propel/propel1": "to allow usage pagination with Propel ORM", + "ruflin/elastica": "to allow usage pagination with ElasticSearch Client", + "solarium/solarium": "to allow usage pagination with Solarium Client", + "symfony/http-foundation": "to retrieve arguments from Request", + "symfony/property-access": "to allow sorting arrays" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Component\\": "src/Knp/Component" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/knp-components/contributors" + } + ], + "description": "Knplabs component library", + "homepage": "https://github.com/KnpLabs/knp-components", + "keywords": [ + "components", + "knp", + "knplabs", + "pager", + "paginator" + ], + "support": { + "issues": "https://github.com/KnpLabs/knp-components/issues", + "source": "https://github.com/KnpLabs/knp-components/tree/v5.2.0" + }, + "time": "2025-03-20T07:35:37+00:00" + }, + { + "name": "knplabs/knp-paginator-bundle", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/KnpLabs/KnpPaginatorBundle.git", + "reference": "8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/KnpLabs/KnpPaginatorBundle/zipball/8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41", + "reference": "8d41f8ed47d880f8fa569389ffa4fecfbc5b8d41", + "shasum": "" + }, + "require": { + "knplabs/knp-components": "^4.4 || ^5.0", + "php": "^8.1", + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0", + "symfony/http-foundation": "^6.4 || ^7.0 || ^8.0", + "symfony/http-kernel": "^6.4 || ^7.0 || ^8.0", + "symfony/routing": "^6.4 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.12", + "phpunit/phpunit": "^10.5 || ^11.5 || ^12.2", + "symfony/templating": "^6.4 || ^7.0 || ^8.0", + "symfony/translation": "^6.4 || ^7.0 || ^8.0", + "twig/twig": "^3.0" + }, + "suggest": { + "symfony/translation": "To use the templates", + "twig/twig": "To use the templates" + }, + "type": "symfony-bundle", + "extra": { + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Knp\\Bundle\\PaginatorBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KnpLabs Team", + "homepage": "https://knplabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle/contributors" + } + ], + "description": "Paginator bundle for Symfony to automate pagination and simplify sorting and other features", + "homepage": "https://github.com/KnpLabs/KnpPaginatorBundle", + "keywords": [ + "bundle", + "knp", + "knplabs", + "pager", + "pagination", + "paginator", + "symfony" + ], + "support": { + "issues": "https://github.com/KnpLabs/KnpPaginatorBundle/issues", + "source": "https://github.com/KnpLabs/KnpPaginatorBundle/tree/v6.10.0" + }, + "time": "2025-11-29T09:14:09+00:00" + }, { "name": "knpuniversity/oauth2-client-bundle", "version": "v2.20.2", diff --git a/config/bundles.php b/config/bundles.php index e87a17a..e36e03b 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -17,4 +17,5 @@ return [ League\FlysystemBundle\FlysystemBundle::class => ['all' => true], Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true], KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], + Knp\Bundle\PaginatorBundle\KnpPaginatorBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index b823e91..7ccb71d 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -27,6 +27,7 @@ security: target: app_home access_control: + - { path: ^/admin, roles: ROLE_ROOT } - { path: ^/mon-compte, roles: ROLE_USER } when@test: diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php new file mode 100644 index 0000000..757fd75 --- /dev/null +++ b/src/Controller/AdminController.php @@ -0,0 +1,294 @@ +render('admin/dashboard.html.twig'); + } + + #[Route('/sync-meilisearch', name: 'app_admin_sync_meilisearch', methods: ['POST'])] + public function syncMeilisearch(EntityManagerInterface $em, MeilisearchService $meilisearch): Response + { + $allUsers = $em->getRepository(User::class)->findAll(); + $buyers = array_filter($allUsers, fn (User $u) => $u->isVerified() && !\in_array('ROLE_ORGANIZER', $u->getRoles(), true) && !\in_array('ROLE_ROOT', $u->getRoles(), true)); + + $meilisearch->createIndexIfNotExists('buyers'); + + $documents = array_map(fn (User $u) => [ + 'id' => $u->getId(), + 'firstName' => $u->getFirstName(), + 'lastName' => $u->getLastName(), + 'email' => $u->getEmail(), + 'createdAt' => $u->getCreatedAt()->format('d/m/Y'), + ], array_values($buyers)); + + if ([] !== $documents) { + $meilisearch->addDocuments('buyers', $documents); + } + + $this->addFlash('success', sprintf('%d acheteur(s) synchronise(s) dans Meilisearch.', \count($documents))); + + return $this->redirectToRoute('app_admin_dashboard'); + } + + #[Route('/utilisateurs', name: 'app_admin_users')] + public function users(EntityManagerInterface $em): Response + { + $users = $em->getRepository(User::class)->findBy([], ['createdAt' => 'DESC']); + + return $this->render('admin/users.html.twig', [ + 'users' => $users, + ]); + } + + #[Route('/acheteurs', name: 'app_admin_buyers')] + public function buyers(Request $request, EntityManagerInterface $em, PaginatorInterface $paginator, MeilisearchService $meilisearch): Response + { + $query = $request->query->getString('q', ''); + $searchResults = null; + + if ('' !== $query) { + try { + $searchResults = $meilisearch->search('buyers', $query, ['limit' => 50]); + } catch (\Throwable) { + $this->addFlash('error', 'Erreur de recherche Meilisearch.'); + } + } + + if (null !== $searchResults && isset($searchResults['hits'])) { + $hitIds = array_map(fn (array $hit) => $hit['id'], $searchResults['hits']); + $allUsers = $em->getRepository(User::class)->findBy(['id' => $hitIds]); + $buyers = $allUsers; + } else { + $allUsers = $em->getRepository(User::class)->findBy([], ['createdAt' => 'DESC']); + $buyers = array_values(array_filter($allUsers, fn (User $u) => !\in_array('ROLE_ORGANIZER', $u->getRoles(), true) && !\in_array('ROLE_ROOT', $u->getRoles(), true))); + } + + $pagination = $paginator->paginate( + $buyers, + $request->query->getInt('page', 1), + 10, + ); + + return $this->render('admin/buyers.html.twig', [ + 'buyers' => $pagination, + 'query' => $query, + ]); + } + + #[Route('/organisateurs', name: 'app_admin_organizers')] + public function organizers(Request $request, EntityManagerInterface $em, PaginatorInterface $paginator): Response + { + $allUsers = $em->getRepository(User::class)->findBy([], ['createdAt' => 'DESC']); + $organizers = array_values(array_filter($allUsers, fn (User $u) => \in_array('ROLE_ORGANIZER', $u->getRoles(), true))); + + $tab = $request->query->getString('tab', 'pending'); + if ('approved' === $tab) { + $filtered = array_values(array_filter($organizers, fn (User $u) => $u->isApproved())); + } else { + $filtered = array_values(array_filter($organizers, fn (User $u) => !$u->isApproved())); + } + + $pagination = $paginator->paginate( + $filtered, + $request->query->getInt('page', 1), + 10, + ); + + return $this->render('admin/organizers.html.twig', [ + 'organizers' => $pagination, + 'tab' => $tab, + ]); + } + + #[Route('/acheteurs/creer', name: 'app_admin_create_buyer', methods: ['POST'])] + public function createBuyer( + Request $request, + EntityManagerInterface $em, + UserPasswordHasherInterface $passwordHasher, + ValidatorInterface $validator, + MailerService $mailerService, + ): Response { + $user = new User(); + $user->setFirstName(trim($request->request->getString('first_name'))); + $user->setLastName(trim($request->request->getString('last_name'))); + $user->setEmail(trim($request->request->getString('email'))); + + $user->setPassword($passwordHasher->hashPassword($user, bin2hex(random_bytes(16)))); + + $token = bin2hex(random_bytes(32)); + $user->setEmailVerificationToken($token); + + $errors = $validator->validate($user); + if (0 === count($errors)) { + $em->persist($user); + $em->flush(); + + $verificationUrl = $this->generateUrl('app_verify_email', [ + 'token' => $token, + ], UrlGeneratorInterface::ABSOLUTE_URL); + + $mailerService->sendEmail( + to: $user->getEmail(), + subject: 'Verifiez votre adresse email - E-Ticket', + content: $this->renderView('email/verification.html.twig', [ + 'firstName' => $user->getFirstName(), + 'verificationUrl' => $verificationUrl, + ]), + withUnsubscribe: false, + ); + + $this->addFlash('success', sprintf('Acheteur %s %s cree.', $user->getFirstName(), $user->getLastName())); + + return $this->redirectToRoute('app_admin_buyers'); + } + + foreach ($errors as $error) { + $this->addFlash('error', $error->getMessage()); + } + + return $this->redirectToRoute('app_admin_buyers'); + } + + #[Route('/acheteur/{id}/renvoyer-verification', name: 'app_admin_resend_verification', methods: ['POST'])] + public function resendVerification(User $user, EntityManagerInterface $em, MailerService $mailerService): Response + { + $token = bin2hex(random_bytes(32)); + $user->setEmailVerificationToken($token); + $em->flush(); + + $verificationUrl = $this->generateUrl('app_verify_email', [ + 'token' => $token, + ], UrlGeneratorInterface::ABSOLUTE_URL); + + $mailerService->sendEmail( + to: $user->getEmail(), + subject: 'Verifiez votre adresse email - E-Ticket', + content: $this->renderView('email/verification.html.twig', [ + 'firstName' => $user->getFirstName(), + 'verificationUrl' => $verificationUrl, + ]), + withUnsubscribe: false, + ); + + $this->addFlash('success', sprintf('Email de verification renvoye a %s.', $user->getEmail())); + + return $this->redirectToRoute('app_admin_buyers'); + } + + #[Route('/acheteur/{id}/forcer-verification', name: 'app_admin_force_verification', methods: ['POST'])] + public function forceVerification(User $user, EntityManagerInterface $em): Response + { + $user->setIsVerified(true); + $user->setEmailVerifiedAt(new \DateTimeImmutable()); + $user->setEmailVerificationToken(null); + $em->flush(); + + $this->addFlash('success', sprintf('Email de %s %s force comme verifie.', $user->getFirstName(), $user->getLastName())); + + return $this->redirectToRoute('app_admin_buyers'); + } + + #[Route('/acheteur/{id}/reset-password', name: 'app_admin_reset_password', methods: ['POST'])] + public function resetPassword(User $user, MailerService $mailerService): Response + { + $resetUrl = $this->generateUrl('app_forgot_password', [], UrlGeneratorInterface::ABSOLUTE_URL); + + $mailerService->sendEmail( + to: $user->getEmail(), + subject: 'Reinitialisation de votre mot de passe - E-Ticket', + content: $this->renderView('email/admin_reset_password.html.twig', [ + 'firstName' => $user->getFirstName(), + 'email' => $user->getEmail(), + 'resetUrl' => $resetUrl, + ]), + withUnsubscribe: false, + ); + + $this->addFlash('success', sprintf('Lien de reinitialisation envoye a %s.', $user->getEmail())); + + return $this->redirectToRoute('app_admin_buyers'); + } + + #[Route('/acheteur/{id}/supprimer', name: 'app_admin_delete_buyer', methods: ['POST'])] + public function deleteBuyer(User $user, EntityManagerInterface $em, MeilisearchService $meilisearch): Response + { + $name = sprintf('%s %s', $user->getFirstName(), $user->getLastName()); + $userId = $user->getId(); + + $em->remove($user); + $em->flush(); + + try { + $meilisearch->deleteDocument('buyers', $userId); + } catch (\Throwable) { + } + + $this->addFlash('success', sprintf('Compte de %s supprime.', $name)); + + return $this->redirectToRoute('app_admin_buyers'); + } + + #[Route('/organisateur/{id}/approuver', name: 'app_admin_approve_organizer', methods: ['POST'])] + public function approveOrganizer(User $user, EntityManagerInterface $em, MailerService $mailerService): Response + { + $user->setIsApproved(true); + $em->flush(); + + $loginUrl = $this->generateUrl('app_login', [], UrlGeneratorInterface::ABSOLUTE_URL); + + $mailerService->sendEmail( + to: $user->getEmail(), + subject: 'Votre compte organisateur a ete approuve - E-Ticket', + content: $this->renderView('email/organizer_approved.html.twig', [ + 'firstName' => $user->getFirstName(), + 'loginUrl' => $loginUrl, + ]), + withUnsubscribe: false, + ); + + $this->addFlash('success', sprintf('Organisateur %s %s approuve.', $user->getFirstName(), $user->getLastName())); + + return $this->redirectToRoute('app_admin_organizers'); + } + + #[Route('/organisateur/{id}/refuser', name: 'app_admin_reject_organizer', methods: ['POST'])] + public function rejectOrganizer(User $user, EntityManagerInterface $em, MailerService $mailerService): Response + { + $em->remove($user); + $em->flush(); + + $mailerService->sendEmail( + to: $user->getEmail(), + subject: 'Votre demande de compte organisateur a ete refusee - E-Ticket', + content: $this->renderView('email/organizer_rejected.html.twig', [ + 'firstName' => $user->getFirstName(), + ]), + withUnsubscribe: false, + ); + + $this->addFlash('success', sprintf('Demande de %s %s refusee.', $user->getFirstName(), $user->getLastName())); + + return $this->redirectToRoute('app_admin_organizers'); + } +} diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 1e09ca7..0fa8caa 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -4,6 +4,7 @@ namespace App\Controller; use App\Entity\User; use App\Service\MailerService; +use App\Service\MeilisearchService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -90,7 +91,7 @@ class RegistrationController extends AbstractController } #[Route('/verification-email/{token}', name: 'app_verify_email')] - public function verifyEmail(string $token, EntityManagerInterface $em, MailerService $mailerService): Response + public function verifyEmail(string $token, EntityManagerInterface $em, MailerService $mailerService, MeilisearchService $meilisearch): Response { $user = $em->getRepository(User::class)->findOneBy(['emailVerificationToken' => $token]); @@ -105,6 +106,17 @@ class RegistrationController extends AbstractController $user->setEmailVerificationToken(null); $em->flush(); + if (!\in_array('ROLE_ORGANIZER', $user->getRoles(), true)) { + $meilisearch->createIndexIfNotExists('buyers'); + $meilisearch->addDocuments('buyers', [[ + 'id' => $user->getId(), + 'firstName' => $user->getFirstName(), + 'lastName' => $user->getLastName(), + 'email' => $user->getEmail(), + 'createdAt' => $user->getCreatedAt()->format('d/m/Y'), + ]]); + } + if (\in_array('ROLE_ORGANIZER', $user->getRoles(), true)) { $mailerService->sendEmail( to: $user->getEmail(), diff --git a/src/MessageHandler/MeilisearchMessageHandler.php b/src/MessageHandler/MeilisearchMessageHandler.php index 584f929..c7aee37 100644 --- a/src/MessageHandler/MeilisearchMessageHandler.php +++ b/src/MessageHandler/MeilisearchMessageHandler.php @@ -17,13 +17,16 @@ class MeilisearchMessageHandler public function __invoke(MeilisearchMessage $message): void { match ($message->action) { - 'createIndex' => $this->meilisearch->createIndex($message->index, $message->payload['primaryKey'] ?? 'id'), - 'deleteIndex' => $this->meilisearch->deleteIndex($message->index), - 'addDocuments' => $this->meilisearch->addDocuments($message->index, $message->payload['documents']), - 'updateDocuments' => $this->meilisearch->updateDocuments($message->index, $message->payload['documents']), - 'deleteDocument' => $this->meilisearch->deleteDocument($message->index, $message->payload['documentId']), - 'deleteDocuments' => $this->meilisearch->deleteDocuments($message->index, $message->payload['ids']), - 'updateSettings' => $this->meilisearch->updateSettings($message->index, $message->payload['settings']), + 'createIndex' => $this->meilisearch->request('POST', '/indexes', [ + 'uid' => $message->index, + 'primaryKey' => $message->payload['primaryKey'] ?? 'id', + ]), + 'deleteIndex' => $this->meilisearch->request('DELETE', "/indexes/{$message->index}"), + 'addDocuments' => $this->meilisearch->request('POST', "/indexes/{$message->index}/documents", $message->payload['documents']), + 'updateDocuments' => $this->meilisearch->request('PUT', "/indexes/{$message->index}/documents", $message->payload['documents']), + 'deleteDocument' => $this->meilisearch->request('DELETE', "/indexes/{$message->index}/documents/{$message->payload['documentId']}"), + 'deleteDocuments' => $this->meilisearch->request('POST', "/indexes/{$message->index}/documents/delete-batch", $message->payload['ids']), + 'updateSettings' => $this->meilisearch->request('PATCH', "/indexes/{$message->index}/settings", $message->payload['settings']), default => throw new \InvalidArgumentException("Unknown action: {$message->action}"), }; } diff --git a/symfony.lock b/symfony.lock index 70d8dc3..f6fd310 100644 --- a/symfony.lock +++ b/symfony.lock @@ -50,6 +50,9 @@ ".php-cs-fixer.dist.php" ] }, + "knplabs/knp-paginator-bundle": { + "version": "v6.10.0" + }, "knpuniversity/oauth2-client-bundle": { "version": "2.20", "recipe": { diff --git a/templates/admin/base.html.twig b/templates/admin/base.html.twig new file mode 100644 index 0000000..9ff7adc --- /dev/null +++ b/templates/admin/base.html.twig @@ -0,0 +1,47 @@ + + + + + + {% block title %}Admin{% endblock %} - E-Ticket + {% block javascripts %} + {{ vite_asset('admin.js') }} + {% endblock %} + + +
+
+
+ +
Admin
+ E-Ticket +
+ +
+ +
+
+ +
+ {% for message in app.flashes('success') %} +
+

{{ message }}

+
+ {% endfor %} + {% for message in app.flashes('error') %} +
+

{{ message }}

+
+ {% endfor %} + {% block body %}{% endblock %} +
+ + diff --git a/templates/admin/buyers.html.twig b/templates/admin/buyers.html.twig new file mode 100644 index 0000000..4272867 --- /dev/null +++ b/templates/admin/buyers.html.twig @@ -0,0 +1,111 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Acheteurs{% endblock %} + +{% block body %} +
+

Acheteurs

+

{{ buyers.getTotalItemCount }} acheteur{{ buyers.getTotalItemCount > 1 ? 's' : '' }} enregistre{{ buyers.getTotalItemCount > 1 ? 's' : '' }}.

+
+ +
+

Rechercher

+
+
+ +
+ + {% if query %} + Effacer + {% endif %} +
+
+ +{% if not query %} +
+

Creer un acheteur

+
+
+ + +
+
+ + +
+
+ + +
+ +
+
+{% endif %} + +
+ + + + + + + + + + + + {% for buyer in buyers %} + + + + + + + + {% else %} + + + + {% endfor %} + +
NomEmailInscriptionEmail verifieActions
{{ buyer.firstName }} {{ buyer.lastName }}{{ buyer.email }}{{ buyer.createdAt|date('d/m/Y') }} + {% if buyer.verified %} + Oui + {% else %} + Non + {% endif %} + +
+ {% if not buyer.verified %} +
+ +
+
+ +
+ {% else %} +
+ +
+ {% endif %} +
+ +
+
+
Aucun acheteur enregistre.
+
+ +{% if buyers.getTotalItemCount > 10 %} +
+ {% for page in 1..buyers.getPageCount %} + {% if page == buyers.getCurrentPageNumber %} + {{ page }} + {% else %} + {{ page }} + {% endif %} + {% endfor %} +
+{% endif %} +{% endblock %} diff --git a/templates/admin/create_buyer.html.twig b/templates/admin/create_buyer.html.twig new file mode 100644 index 0000000..a1e56ba --- /dev/null +++ b/templates/admin/create_buyer.html.twig @@ -0,0 +1,57 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Creer un acheteur{% endblock %} + +{% block body %} +
+

Creer un acheteur

+

Le mot de passe sera genere aleatoirement.

+
+ +
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+ +
+ + + Annuler + +
+
+
+
+{% endblock %} diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig new file mode 100644 index 0000000..0d967e1 --- /dev/null +++ b/templates/admin/dashboard.html.twig @@ -0,0 +1,29 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Dashboard{% endblock %} + +{% block body %} +
+

Dashboard

+

Bonjour {{ app.user.firstName }}, bienvenue sur l'administration.

+
+ +
+
+

CA HT Global

+

0,00 €

+
+
+

CA HT Commission

+

0,00 €

+
+
+ +
+

Synchronisation Meilisearch

+

Synchronise manuellement les acheteurs verifies dans l'index Meilisearch.

+
+ +
+
+{% endblock %} diff --git a/templates/admin/organizers.html.twig b/templates/admin/organizers.html.twig new file mode 100644 index 0000000..e1b131e --- /dev/null +++ b/templates/admin/organizers.html.twig @@ -0,0 +1,88 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Organisateurs{% endblock %} + +{% block body %} +
+

Organisateurs

+

{{ organizers.getTotalItemCount }} organisateur{{ organizers.getTotalItemCount > 1 ? 's' : '' }}.

+
+ +
+ En attente + Valides +
+ +
+ + + + + + + + + + + + + {% for orga in organizers %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
OrganisateurRaison socialeSIRETVilleOffreActions
+

{{ orga.firstName }} {{ orga.lastName }}

+

{{ orga.email }}

+
{{ orga.companyName }}{{ orga.siret }}{{ orga.postalCode }} {{ orga.city }} + {% if orga.offer %} + {{ orga.offer }} + {% else %} + + {% endif %} + +
+ {% if not orga.approved %} +
+ +
+
+ +
+ {% else %} +
+ +
+ {% endif %} +
+
+ {% if tab == 'pending' %} + Aucune demande en attente. + {% else %} + Aucun organisateur valide. + {% endif %} +
+
+ +{% if organizers.getTotalItemCount > 10 %} +
+ {% for page in 1..organizers.getPageCount %} + {% if page == organizers.getCurrentPageNumber %} + {{ page }} + {% else %} + {{ page }} + {% endif %} + {% endfor %} +
+{% endif %} +{% endblock %} diff --git a/templates/admin/users.html.twig b/templates/admin/users.html.twig new file mode 100644 index 0000000..f6099c3 --- /dev/null +++ b/templates/admin/users.html.twig @@ -0,0 +1,47 @@ +{% extends 'admin/base.html.twig' %} + +{% block title %}Utilisateurs{% endblock %} + +{% block body %} +
+

Utilisateurs

+

{{ users|length }} utilisateur{{ users|length > 1 ? 's' : '' }} enregistre{{ users|length > 1 ? 's' : '' }}.

+
+ +
+
+ + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% endfor %} + +
NomEmailRolesVerifieApprouveInscription
{{ user.firstName }} {{ user.lastName }}{{ user.email }} + {% for role in user.roles %} + {{ role }} + {% endfor %} + + {% if user.verified %}{% else %}{% endif %} + + {% if user.approved %}{% else %}{% endif %} + {{ user.createdAt|date('d/m/Y') }}
+
+
+{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index 14dfa79..5407363 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -150,7 +150,8 @@
-

© {{ "now"|date("Y") }} E-COSPLAY.

+

© {{ "now"|date("Y") }} E-TICKET.

+

Solution proposee par l'association e-cosplay.fr

RNA N°W022006988

diff --git a/templates/email/admin_reset_password.html.twig b/templates/email/admin_reset_password.html.twig new file mode 100644 index 0000000..0c3818b --- /dev/null +++ b/templates/email/admin_reset_password.html.twig @@ -0,0 +1,13 @@ +{% extends 'email/base.html.twig' %} + +{% block title %}Reinitialisation de votre mot de passe{% endblock %} + +{% block content %} +

Bonjour {{ firstName }} !

+

Un administrateur E-Ticket a initie une reinitialisation de votre mot de passe pour le compte associe a l'adresse {{ email }}.

+

Pour definir un nouveau mot de passe, cliquez sur le bouton ci-dessous :

+

+ Reinitialiser mon mot de passe +

+

Si vous n'avez pas fait cette demande, vous pouvez ignorer cet email. Votre mot de passe actuel restera inchange.

+{% endblock %} diff --git a/templates/email/base.html.twig b/templates/email/base.html.twig index 1960d51..bd9ccff 100644 --- a/templates/email/base.html.twig +++ b/templates/email/base.html.twig @@ -10,75 +10,74 @@ margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - background-color: #f4f4f5; - color: #18181b; + background-color: #fbfbfb; + color: #111827; } .wrapper { width: 100%; padding: 40px 0; - background-color: #f4f4f5; + background-color: #fbfbfb; } .container { max-width: 600px; margin: 0 auto; background-color: #ffffff; - border-radius: 12px; - overflow: hidden; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); + border: 4px solid #111827; + box-shadow: 8px 8px 0 rgba(0,0,0,1); } .header { - background-color: #7c3aed; - color: #ffffff; - padding: 32px; + background-color: #111827; + padding: 24px 32px; text-align: center; } - .header h1 { - margin: 0; - font-size: 24px; - font-weight: 700; + .header img { + height: 40px; + width: auto; } .content { padding: 32px; } .content h2 { margin: 0 0 16px; - font-size: 20px; - color: #18181b; + font-size: 22px; + font-weight: 900; + text-transform: uppercase; + letter-spacing: -0.025em; + color: #111827; } .content p { margin: 0 0 16px; - font-size: 16px; + font-size: 15px; line-height: 1.6; - color: #3f3f46; + color: #374151; } .btn { display: inline-block; padding: 14px 28px; - background-color: #7c3aed; - color: #ffffff; + background-color: #fabf04; + color: #111827; text-decoration: none; - border-radius: 8px; - font-weight: 600; - font-size: 16px; - } - .qr-code { - text-align: center; - padding: 24px 0; - } - .qr-code img { - width: 200px; - height: 200px; + font-weight: 900; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.05em; + border: 3px solid #111827; + box-shadow: 4px 4px 0 rgba(0,0,0,1); } .footer { padding: 24px 32px; - text-align: center; - font-size: 13px; - color: #a1a1aa; - border-top: 1px solid #e4e4e7; + background-color: #fabf04; + border-top: 4px solid #111827; + } + .footer p { + margin: 0 0 4px; + font-size: 12px; + font-weight: 700; + color: #111827; } .footer a { - color: #7c3aed; - text-decoration: none; + color: #111827; + text-decoration: underline; } @@ -86,15 +85,17 @@
-

🎫 E-Ticket

+ E-Ticket
{% block content %}{% endblock %}
diff --git a/templates/email/organizer_approved.html.twig b/templates/email/organizer_approved.html.twig new file mode 100644 index 0000000..21da7ef --- /dev/null +++ b/templates/email/organizer_approved.html.twig @@ -0,0 +1,12 @@ +{% extends 'email/base.html.twig' %} + +{% block title %}Votre compte organisateur a ete approuve{% endblock %} + +{% block content %} +

Felicitations {{ firstName }} !

+

Votre demande de compte organisateur a ete approuvee par l'equipe E-Ticket.

+

Vous pouvez desormais vous connecter et commencer a creer vos evenements.

+

+ Se connecter +

+{% endblock %} diff --git a/templates/email/organizer_rejected.html.twig b/templates/email/organizer_rejected.html.twig new file mode 100644 index 0000000..aef3188 --- /dev/null +++ b/templates/email/organizer_rejected.html.twig @@ -0,0 +1,12 @@ +{% extends 'email/base.html.twig' %} + +{% block title %}Votre demande de compte organisateur a ete refusee{% endblock %} + +{% block content %} +

Bonjour {{ firstName }},

+

Nous avons examine votre demande de compte organisateur et malheureusement, celle-ci a ete refusee.

+

Si vous pensez qu'il s'agit d'une erreur ou si vous souhaitez obtenir plus d'informations, n'hesitez pas a nous contacter.

+

+ Nous contacter +

+{% endblock %} diff --git a/tests/Controller/AdminControllerTest.php b/tests/Controller/AdminControllerTest.php new file mode 100644 index 0000000..f7ee251 --- /dev/null +++ b/tests/Controller/AdminControllerTest.php @@ -0,0 +1,350 @@ +request('GET', '/admin'); + + self::assertResponseRedirects(); + } + + public function testDashboardDeniedForNonRoot(): void + { + $client = static::createClient(); + $user = $this->createUser(); + + $client->loginUser($user); + $client->request('GET', '/admin'); + + self::assertResponseStatusCodeSame(403); + } + + public function testDashboardReturnsSuccessForRoot(): void + { + $client = static::createClient(); + $user = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($user); + $client->request('GET', '/admin'); + + self::assertResponseIsSuccessful(); + } + + public function testUsersPageReturnsSuccessForRoot(): void + { + $client = static::createClient(); + $user = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($user); + $client->request('GET', '/admin/utilisateurs'); + + self::assertResponseIsSuccessful(); + } + + public function testUsersPageDeniedForNonRoot(): void + { + $client = static::createClient(); + $user = $this->createUser(); + + $client->loginUser($user); + $client->request('GET', '/admin/utilisateurs'); + + self::assertResponseStatusCodeSame(403); + } + + public function testBuyersPageReturnsSuccessForRoot(): void + { + $client = static::createClient(); + $user = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($user); + $client->request('GET', '/admin/acheteurs'); + + self::assertResponseIsSuccessful(); + } + + public function testBuyersSearchWithQuery(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $meilisearch = $this->createMock(MeilisearchService::class); + $meilisearch->expects(self::once())->method('search')->willReturn([ + 'hits' => [], + 'estimatedTotalHits' => 0, + ]); + static::getContainer()->set(MeilisearchService::class, $meilisearch); + + $client->loginUser($admin); + $client->request('GET', '/admin/acheteurs?q=test'); + + self::assertResponseIsSuccessful(); + } + + public function testSyncMeilisearch(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $meilisearch = $this->createMock(MeilisearchService::class); + $meilisearch->expects(self::once())->method('createIndexIfNotExists'); + static::getContainer()->set(MeilisearchService::class, $meilisearch); + + $client->loginUser($admin); + $client->request('POST', '/admin/sync-meilisearch'); + + self::assertResponseRedirects('/admin'); + } + + public function testCreateBuyerWithValidData(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects(self::once())->method('sendEmail'); + static::getContainer()->set(MailerService::class, $mailer); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteurs/creer', [ + 'first_name' => 'Nouveau', + 'last_name' => 'Acheteur', + 'email' => 'new-buyer-'.uniqid().'@example.com', + ]); + + self::assertResponseRedirects('/admin/acheteurs'); + } + + public function testCreateBuyerWithDuplicateEmail(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteurs/creer', [ + 'first_name' => 'Dup', + 'last_name' => 'Test', + 'email' => $admin->getEmail(), + ]); + + self::assertResponseRedirects('/admin/acheteurs'); + } + + public function testResendVerificationEmail(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + + $buyer = new User(); + $buyer->setEmail('test-buyer-'.uniqid().'@example.com'); + $buyer->setFirstName('Buyer'); + $buyer->setLastName('Test'); + $buyer->setPassword('$2y$13$hashed'); + $em->persist($buyer); + $em->flush(); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects(self::once())->method('sendEmail'); + static::getContainer()->set(MailerService::class, $mailer); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteur/'.$buyer->getId().'/renvoyer-verification'); + + self::assertResponseRedirects('/admin/acheteurs'); + + $em->refresh($buyer); + self::assertNotNull($buyer->getEmailVerificationToken()); + } + + public function testResetPasswordSendsEmail(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + + $buyer = new User(); + $buyer->setEmail('test-reset-admin-'.uniqid().'@example.com'); + $buyer->setFirstName('Reset'); + $buyer->setLastName('Test'); + $buyer->setPassword('$2y$13$hashed'); + $buyer->setIsVerified(true); + $em->persist($buyer); + $em->flush(); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects(self::once())->method('sendEmail'); + static::getContainer()->set(MailerService::class, $mailer); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteur/'.$buyer->getId().'/reset-password'); + + self::assertResponseRedirects('/admin/acheteurs'); + } + + public function testDeleteBuyer(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + + $buyer = new User(); + $buyer->setEmail('test-delete-'.uniqid().'@example.com'); + $buyer->setFirstName('Delete'); + $buyer->setLastName('Test'); + $buyer->setPassword('$2y$13$hashed'); + $em->persist($buyer); + $em->flush(); + $buyerId = $buyer->getId(); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteur/'.$buyerId.'/supprimer'); + + self::assertResponseRedirects('/admin/acheteurs'); + + $deleted = $em->getRepository(User::class)->find($buyerId); + self::assertNull($deleted); + } + + public function testForceVerification(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + + $buyer = new User(); + $buyer->setEmail('test-force-'.uniqid().'@example.com'); + $buyer->setFirstName('Force'); + $buyer->setLastName('Test'); + $buyer->setPassword('$2y$13$hashed'); + $em->persist($buyer); + $em->flush(); + + $client->loginUser($admin); + $client->request('POST', '/admin/acheteur/'.$buyer->getId().'/forcer-verification'); + + self::assertResponseRedirects('/admin/acheteurs'); + + $em->refresh($buyer); + self::assertTrue($buyer->isVerified()); + self::assertNotNull($buyer->getEmailVerifiedAt()); + self::assertNull($buyer->getEmailVerificationToken()); + } + + public function testOrganizersPagePendingTab(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($admin); + $client->request('GET', '/admin/organisateurs'); + + self::assertResponseIsSuccessful(); + } + + public function testOrganizersPageApprovedTab(): void + { + $client = static::createClient(); + $admin = $this->createUser(['ROLE_ROOT']); + + $client->loginUser($admin); + $client->request('GET', '/admin/organisateurs?tab=approved'); + + self::assertResponseIsSuccessful(); + } + + public function testApproveOrganizer(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + $orga = $this->createOrganizer($em); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects(self::once())->method('sendEmail'); + static::getContainer()->set(MailerService::class, $mailer); + + $client->loginUser($admin); + $client->request('POST', '/admin/organisateur/'.$orga->getId().'/approuver'); + + self::assertResponseRedirects('/admin/organisateurs'); + + $em->refresh($orga); + self::assertTrue($orga->isApproved()); + } + + public function testRejectOrganizer(): void + { + $client = static::createClient(); + $em = static::getContainer()->get(EntityManagerInterface::class); + + $admin = $this->createUser(['ROLE_ROOT']); + $orga = $this->createOrganizer($em); + $orgaId = $orga->getId(); + + $mailer = $this->createMock(MailerService::class); + $mailer->expects(self::once())->method('sendEmail'); + static::getContainer()->set(MailerService::class, $mailer); + + $client->loginUser($admin); + $client->request('POST', '/admin/organisateur/'.$orgaId.'/refuser'); + + self::assertResponseRedirects('/admin/organisateurs'); + + $deleted = $em->getRepository(User::class)->find($orgaId); + self::assertNull($deleted); + } + + /** + * @param list $roles + */ + private function createUser(array $roles = []): User + { + $em = static::getContainer()->get(EntityManagerInterface::class); + + $user = new User(); + $user->setEmail('test-admin-'.uniqid().'@example.com'); + $user->setFirstName('Admin'); + $user->setLastName('User'); + $user->setPassword('$2y$13$hashed'); + $user->setRoles($roles); + + $em->persist($user); + $em->flush(); + + return $user; + } + + private function createOrganizer(EntityManagerInterface $em): User + { + $orga = new User(); + $orga->setEmail('test-orga-'.uniqid().'@example.com'); + $orga->setFirstName('Orga'); + $orga->setLastName('Test'); + $orga->setPassword('$2y$13$hashed'); + $orga->setRoles(['ROLE_ORGANIZER']); + $orga->setIsVerified(true); + $orga->setCompanyName('Mon Asso'); + $orga->setSiret('12345678901234'); + + $em->persist($orga); + $em->flush(); + + return $orga; + } +} diff --git a/tests/MessageHandler/MeilisearchMessageHandlerTest.php b/tests/MessageHandler/MeilisearchMessageHandlerTest.php index 59eb00e..1085f74 100644 --- a/tests/MessageHandler/MeilisearchMessageHandlerTest.php +++ b/tests/MessageHandler/MeilisearchMessageHandlerTest.php @@ -21,8 +21,8 @@ class MeilisearchMessageHandlerTest extends TestCase public function testHandleCreateIndex(): void { $this->meilisearch->expects(self::once()) - ->method('createIndex') - ->with('events', 'uid'); + ->method('request') + ->with('POST', '/indexes', ['uid' => 'events', 'primaryKey' => 'uid']); ($this->handler)(new MeilisearchMessage('createIndex', 'events', ['primaryKey' => 'uid'])); } @@ -30,8 +30,8 @@ class MeilisearchMessageHandlerTest extends TestCase public function testHandleDeleteIndex(): void { $this->meilisearch->expects(self::once()) - ->method('deleteIndex') - ->with('events'); + ->method('request') + ->with('DELETE', '/indexes/events'); ($this->handler)(new MeilisearchMessage('deleteIndex', 'events')); } @@ -40,8 +40,8 @@ class MeilisearchMessageHandlerTest extends TestCase { $docs = [['id' => 1]]; $this->meilisearch->expects(self::once()) - ->method('addDocuments') - ->with('events', $docs); + ->method('request') + ->with('POST', '/indexes/events/documents', $docs); ($this->handler)(new MeilisearchMessage('addDocuments', 'events', ['documents' => $docs])); } @@ -50,8 +50,8 @@ class MeilisearchMessageHandlerTest extends TestCase { $docs = [['id' => 1, 'title' => 'Updated']]; $this->meilisearch->expects(self::once()) - ->method('updateDocuments') - ->with('events', $docs); + ->method('request') + ->with('PUT', '/indexes/events/documents', $docs); ($this->handler)(new MeilisearchMessage('updateDocuments', 'events', ['documents' => $docs])); } @@ -59,8 +59,8 @@ class MeilisearchMessageHandlerTest extends TestCase public function testHandleDeleteDocument(): void { $this->meilisearch->expects(self::once()) - ->method('deleteDocument') - ->with('events', 42); + ->method('request') + ->with('DELETE', '/indexes/events/documents/42'); ($this->handler)(new MeilisearchMessage('deleteDocument', 'events', ['documentId' => 42])); } @@ -68,8 +68,8 @@ class MeilisearchMessageHandlerTest extends TestCase public function testHandleDeleteDocuments(): void { $this->meilisearch->expects(self::once()) - ->method('deleteDocuments') - ->with('events', [1, 2, 3]); + ->method('request') + ->with('POST', '/indexes/events/documents/delete-batch', [1, 2, 3]); ($this->handler)(new MeilisearchMessage('deleteDocuments', 'events', ['ids' => [1, 2, 3]])); } @@ -78,8 +78,8 @@ class MeilisearchMessageHandlerTest extends TestCase { $settings = ['searchableAttributes' => ['title']]; $this->meilisearch->expects(self::once()) - ->method('updateSettings') - ->with('events', $settings); + ->method('request') + ->with('PATCH', '/indexes/events/settings', $settings); ($this->handler)(new MeilisearchMessage('updateSettings', 'events', ['settings' => $settings])); } diff --git a/vite.config.js b/vite.config.js index 284c25c..15fa0f5 100644 --- a/vite.config.js +++ b/vite.config.js @@ -42,6 +42,7 @@ export default defineConfig({ rollupOptions: { input: { app: resolve(__dirname, 'assets/app.js'), + admin: resolve(__dirname, 'assets/admin.js'), } }, },