diff --git a/composer.json b/composer.json index 2a284c5..84899ac 100644 --- a/composer.json +++ b/composer.json @@ -100,7 +100,7 @@ "symfony/browser-kit": "6.4.*", "symfony/css-selector": "6.4.*", "symfony/debug-bundle": "6.4.*", - "symfony/maker-bundle": "^1.0", + "symfony/maker-bundle": "^1.65", "symfony/phpunit-bridge": "^7.0", "symfony/stopwatch": "6.4.*", "symfony/web-profiler-bundle": "6.4.*" diff --git a/composer.lock b/composer.lock index 115a876..cb87fd3 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": "449a04e5b2ca2b8ce88b30d4b38fa5c0", + "content-hash": "ffcef31327f8f7d322be34e2515dedf0", "packages": [ { "name": "composer/semver", @@ -5672,16 +5672,16 @@ }, { "name": "symfony/security-bundle", - "version": "v6.4.7", + "version": "v6.4.10", "source": { "type": "git", "url": "https://github.com/symfony/security-bundle.git", - "reference": "c9112933215b9b3c48851eb6644263d5c9d93245" + "reference": "50007f4f76632741b62fa9604c5f65807f268b72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/security-bundle/zipball/c9112933215b9b3c48851eb6644263d5c9d93245", - "reference": "c9112933215b9b3c48851eb6644263d5c9d93245", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/50007f4f76632741b62fa9604c5f65807f268b72", + "reference": "50007f4f76632741b62fa9604c5f65807f268b72", "shasum": "" }, "require": { @@ -5764,7 +5764,7 @@ "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/security-bundle/tree/v6.4.7" + "source": "https://github.com/symfony/security-bundle/tree/v6.4.10" }, "funding": [ { @@ -5780,7 +5780,7 @@ "type": "tidelift" } ], - "time": "2024-04-18T09:22:46+00:00" + "time": "2024-07-17T10:49:44+00:00" }, { "name": "symfony/security-core", @@ -9473,31 +9473,31 @@ }, { "name": "symfony/maker-bundle", - "version": "v1.59.1", + "version": "v1.65.1", "source": { "type": "git", "url": "https://github.com/symfony/maker-bundle.git", - "reference": "b87b1b25c607a8a50832395bc751c784946a0350" + "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/b87b1b25c607a8a50832395bc751c784946a0350", - "reference": "b87b1b25c607a8a50832395bc751c784946a0350", + "url": "https://api.github.com/repos/symfony/maker-bundle/zipball/eba30452d212769c9a5bcf0716959fd8ba1e54e3", + "reference": "eba30452d212769c9a5bcf0716959fd8ba1e54e3", "shasum": "" }, "require": { "doctrine/inflector": "^2.0", - "nikic/php-parser": "^4.18|^5.0", + "nikic/php-parser": "^5.0", "php": ">=8.1", - "symfony/config": "^6.4|^7.0", - "symfony/console": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", "symfony/deprecation-contracts": "^2.2|^3", - "symfony/filesystem": "^6.4|^7.0", - "symfony/finder": "^6.4|^7.0", - "symfony/framework-bundle": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/process": "^6.4|^7.0" + "symfony/filesystem": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0" }, "conflict": { "doctrine/doctrine-bundle": "<2.10", @@ -9505,12 +9505,14 @@ }, "require-dev": { "composer/semver": "^3.0", - "doctrine/doctrine-bundle": "^2.5.0", + "doctrine/doctrine-bundle": "^2.5.0|^3.0.0", "doctrine/orm": "^2.15|^3", - "symfony/http-client": "^6.4|^7.0", - "symfony/phpunit-bridge": "^6.4.1|^7.0", - "symfony/security-core": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0", + "doctrine/persistence": "^3.1|^4.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/phpunit-bridge": "^6.4.1|^7.0|^8.0", + "symfony/security-core": "^6.4|^7.0|^8.0", + "symfony/security-http": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", "twig/twig": "^3.0|^4.x-dev" }, "type": "symfony-bundle", @@ -9545,7 +9547,7 @@ ], "support": { "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/v1.59.1" + "source": "https://github.com/symfony/maker-bundle/tree/v1.65.1" }, "funding": [ { @@ -9556,12 +9558,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-05-06T03:59:59+00:00" + "time": "2025-12-02T07:14:37+00:00" }, { "name": "symfony/phpunit-bridge", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..7e2aaa3 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,23 @@ security: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: username firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider + custom_authenticator: App\Security\AppCustomAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,7 +31,7 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/admin, roles: ROLE_ADMIN } # - { path: ^/profile, roles: ROLE_USER } when@test: @@ -33,7 +42,7 @@ when@test: # are not important, waste resources and increase test times. The following # reduces the work factor to the lowest possible values. Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: - algorithm: auto + algorithm: bcrypt cost: 4 # Lowest possible value for bcrypt time_cost: 3 # Lowest possible value for argon memory_cost: 10 # Lowest possible value for argon diff --git a/migrations/Version20260123101220.php b/migrations/Version20260123101220.php new file mode 100644 index 0000000..2a927e2 --- /dev/null +++ b/migrations/Version20260123101220.php @@ -0,0 +1,31 @@ +addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, username VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_IDENTIFIER_USERNAME (username), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user'); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..2133610 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,32 @@ +getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'app_logout')] + public function logout(): void + { + throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } +} diff --git a/src/Controller/admin/AdminCategoriesController.php b/src/Controller/admin/AdminCategoriesController.php new file mode 100644 index 0000000..3488f35 --- /dev/null +++ b/src/Controller/admin/AdminCategoriesController.php @@ -0,0 +1,68 @@ +categorieRepository = $categorieRepository; + } + + #[Route('/admin/categories', name:'admin.categories')] + public function index(){ + $categories = $this->categorieRepository->findAll(); + $count = []; + foreach ($categories as $categorie) { + $count[$categorie->getId()] = $this->categorieRepository->countFormationsByCategorie($categorie); + } + + return $this->render('pages/admin/admin.categories.html.twig', [ + 'categories'=> $categories, + 'usageCounts' => $count, + ]); + } + + #[Route('/admin/categories/remove/{id}', name:'admin.categories.remove')] + public function remove(int $id){ + $categorie = $this->categorieRepository->find($id); + $count = $this->categorieRepository->countFormationsByCategorie($categorie); + + if($count == 0){ + $this->categorieRepository->remove($categorie); + return $this->redirectToRoute('admin.categories'); + } + return $this->redirectToRoute('admin.categories'); + } + + #[Route('admin/categories/add', name:'admin.categories.add')] + public function add(Request $request){ + $name = $request->request->get('name'); + $token = $request->request->get('_token'); + + if (!$this->isCsrfTokenValid('filtre_title', $token)) { + throw $this->createAccessDeniedException('Token CSRF invalide.'); + } + + if ($name) { + $category = new Categorie(); + $category->setName($name); + + $this->categorieRepository->add($category); + + $this->addFlash('success', 'Catégorie ajoutée !'); + } + + return $this->redirectToRoute('admin.categories'); + } +} \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..37c7259 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,103 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[ORM\Column] + private ?string $password = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getUsername(): ?string + { + return $this->username; + } + + public function setUsername(string $username): static + { + $this->username = $username; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->username; + } + + /** + * @see UserInterface + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + #[\Deprecated] + public function eraseCredentials(): void + { + // @deprecated, to be removed when upgrading to Symfony 8 + } +} diff --git a/src/Repository/CategorieRepository.php b/src/Repository/CategorieRepository.php index 625624d..0a25183 100644 --- a/src/Repository/CategorieRepository.php +++ b/src/Repository/CategorieRepository.php @@ -44,4 +44,20 @@ class CategorieRepository extends ServiceEntityRepository ->getResult(); } + /** + * Compte le nombre de formations pour une catégorie donnée + * @param Categorie $categorie + * @return int + */ + public function countFormationsByCategorie(Categorie $categorie): int + { + return (int) $this->createQueryBuilder('c') + ->select('COUNT(f.id)') + ->join('c.formations', 'f') + ->where('c.id = :id') + ->setParameter('id', $categorie->getId()) + ->getQuery() + ->getSingleScalarResult(); + } + } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/AppCustomAuthenticator.php b/src/Security/AppCustomAuthenticator.php new file mode 100644 index 0000000..eb0bb08 --- /dev/null +++ b/src/Security/AppCustomAuthenticator.php @@ -0,0 +1,54 @@ +getPayload()->getString('username'); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $username); + + return new Passport( + new UserBadge($username), + new PasswordCredentials($request->getPayload()->getString('password')), + [ + new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response +{ + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + return new RedirectResponse($this->urlGenerator->generate('app_home')); +} + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/templates/baseadmin.html.twig b/templates/baseadmin.html.twig index e7e87e1..6405dd7 100644 --- a/templates/baseadmin.html.twig +++ b/templates/baseadmin.html.twig @@ -13,13 +13,13 @@ @@ -30,6 +30,8 @@ {% block footer %}