From 47c8cdcda483639321895e82a167fecb26d82268 Mon Sep 17 00:00:00 2001 From: Aymeric SERRA Date: Thu, 9 Feb 2023 15:30:23 +0100 Subject: [PATCH 1/5] Ajout CRUD pour gerer utilisateurs, uniquement accessible pour un admin --- .idea/codeStyles/codeStyleConfig.xml | 5 ++ README.md | 4 +- config/packages/security.yaml | 1 + src/Controller/SecurityController.php | 2 +- src/Controller/UserController.php | 80 +++++++++++++++++++ src/Entity/User.php | 6 +- src/Form/UserType.php | 52 ++++++++++++ .../AppAuthentificatorAuthenticator.php | 2 +- templates/components/navbar.html.twig | 10 ++- templates/user/_delete_form.html.twig | 4 + templates/user/_form.html.twig | 4 + templates/user/edit.html.twig | 13 +++ templates/user/index.html.twig | 41 ++++++++++ templates/user/new.html.twig | 11 +++ templates/user/show.html.twig | 38 +++++++++ 15 files changed, 265 insertions(+), 8 deletions(-) create mode 100644 .idea/codeStyles/codeStyleConfig.xml create mode 100644 src/Controller/UserController.php create mode 100644 src/Form/UserType.php create mode 100644 templates/user/_delete_form.html.twig create mode 100644 templates/user/_form.html.twig create mode 100644 templates/user/edit.html.twig create mode 100644 templates/user/index.html.twig create mode 100644 templates/user/new.html.twig create mode 100644 templates/user/show.html.twig diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml new file mode 100644 index 0000000..a55e7a1 --- /dev/null +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/README.md b/README.md index 5cffd8c..3fee068 100644 --- a/README.md +++ b/README.md @@ -93,4 +93,6 @@ Pas de commandes. ### Question 14 ```bash symfony console doctrine:fixtures:load -``` \ No newline at end of file +``` + +### Question 15 \ No newline at end of file diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 8196f0a..d6ee356 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -10,6 +10,7 @@ security: class: App\Entity\User property: email role_hierarchy: + ROLE_ADMINISTRATEUR: [ROLE_INSTRUCTOR, ROLE_APPRENTI, ROLE_USER] ROLE_INSTRUCTOR: ROLE_USER ROLE_APPRENTI: ROLE_USER firewalls: diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 73da5ac..3269b24 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -13,7 +13,7 @@ class SecurityController extends AbstractController public function login(AuthenticationUtils $authenticationUtils): Response { if ($this->getUser()) { - return $this->redirectToRoute('app_atelier_index'); + return $this->redirectToRoute('app_index'); } // get the login error if there is one diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..94b2cc8 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,80 @@ +render('user/index.html.twig', [ + 'users' => $userRepository->findAll(), + ]); + } + + #[Route('/new', name: 'app_user_new', methods: ['GET', 'POST'])] + public function new(Request $request, UserRepository $userRepository): Response + { + $user = new User(); + $form = $this->createForm(UserType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $userRepository->save($user, true); + + return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->renderForm('user/new.html.twig', [ + 'user' => $user, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_user_show', methods: ['GET'])] + public function show(User $user): Response + { + return $this->render('user/show.html.twig', [ + 'user' => $user, + ]); + } + + #[Route('/{id}/edit', name: 'app_user_edit', methods: ['GET', 'POST'])] + public function edit(Request $request, User $user, UserRepository $userRepository): Response + { + $form = $this->createForm(UserType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $userRepository->save($user, true); + + return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER); + } + + return $this->renderForm('user/edit.html.twig', [ + 'user' => $user, + 'form' => $form, + ]); + } + + #[Route('/{id}', name: 'app_user_delete', methods: ['POST'])] + public function delete(Request $request, User $user, UserRepository $userRepository): Response + { + if ($this->isCsrfTokenValid('delete' . $user->getId(), $request->request->get('_token'))) { + $userRepository->remove($user, true); + } + + return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php index e56617c..9db0d01 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -83,9 +83,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface { $roles = $this->roles; // guarantee every user at least has ROLE_USER - $roles[] = 'ROLE_USER'; - $roles[] = 'ROLE_INSTRUCTOR'; - $roles[] = 'ROLE_APPRENTI'; + if (sizeof($roles) == 0) { + $roles[] = "ROLE_APPRENTI"; + } return array_unique($roles); } diff --git a/src/Form/UserType.php b/src/Form/UserType.php new file mode 100644 index 0000000..49ed12b --- /dev/null +++ b/src/Form/UserType.php @@ -0,0 +1,52 @@ +add('email') + ->add('roles', ChoiceType::class, [ + 'choices' => [ + 'Apprenti' => 'ROLE_APPRENTI', + 'Instructeur' => 'ROLE_INSTRUCTOR', + 'Admin' => 'ROLE_ADMINISTRATEUR' + ], + 'required' => true, + 'multiple' => false, + 'expanded' => false, + ]) + ->add('nom') + ->add('prenom'); + + $builder->get('roles') + ->addModelTransformer(new CallbackTransformer( + function ($rolesArray) { + // transform the array to a string + return count($rolesArray)? $rolesArray[0]: null; + }, + function ($rolesString) { + // transform the string back to an array + return [$rolesString]; + } + )); + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Security/AppAuthentificatorAuthenticator.php b/src/Security/AppAuthentificatorAuthenticator.php index 946b252..7ca73ed 100644 --- a/src/Security/AppAuthentificatorAuthenticator.php +++ b/src/Security/AppAuthentificatorAuthenticator.php @@ -46,7 +46,7 @@ class AppAuthentificatorAuthenticator extends AbstractLoginFormAuthenticator return new RedirectResponse($targetPath); } - return new RedirectResponse($this->urlGenerator->generate('app_atelier_index')); + return new RedirectResponse($this->urlGenerator->generate('app_index')); } protected function getLoginUrl(Request $request): string diff --git a/templates/components/navbar.html.twig b/templates/components/navbar.html.twig index a590160..b0d18df 100644 --- a/templates/components/navbar.html.twig +++ b/templates/components/navbar.html.twig @@ -13,16 +13,22 @@ - {% if app.user %} + + {% if app.user and is_granted('ROLE_INSTRUCTOR') %} {% endif %} - {% if app.user %} + {% if app.user and is_granted('ROLE_APPRENTI') %} {% endif %} + {% if app.user and is_granted('ROLE_ADMINISTRATEUR') %} + + {% endif %} {% if app.user %} diff --git a/templates/user/_delete_form.html.twig b/templates/user/_delete_form.html.twig new file mode 100644 index 0000000..6d59fa6 --- /dev/null +++ b/templates/user/_delete_form.html.twig @@ -0,0 +1,4 @@ +
+ + +
diff --git a/templates/user/_form.html.twig b/templates/user/_form.html.twig new file mode 100644 index 0000000..bf20b98 --- /dev/null +++ b/templates/user/_form.html.twig @@ -0,0 +1,4 @@ +{{ form_start(form) }} + {{ form_widget(form) }} + +{{ form_end(form) }} diff --git a/templates/user/edit.html.twig b/templates/user/edit.html.twig new file mode 100644 index 0000000..141d94a --- /dev/null +++ b/templates/user/edit.html.twig @@ -0,0 +1,13 @@ +{% extends 'base.html.twig' %} + +{% block title %}Edit User{% endblock %} + +{% block body %} +

Edit User

+ + {{ include('user/_form.html.twig', {'button_label': 'Update'}) }} + + back to list + + {{ include('user/_delete_form.html.twig') }} +{% endblock %} diff --git a/templates/user/index.html.twig b/templates/user/index.html.twig new file mode 100644 index 0000000..e2e3f7f --- /dev/null +++ b/templates/user/index.html.twig @@ -0,0 +1,41 @@ +{% extends 'base.html.twig' %} + +{% block title %}User index{% endblock %} + +{% block body %} +

User index

+ + + + + + + + + + + + + + {% for user in users %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
IdEmailRolesNomPrenomactions
{{ user.id }}{{ user.email }}{{ user.roles ? user.roles[0] : '' }}{{ user.nom }}{{ user.prenom }} + show + edit +
no records found
+ + Create new +{% endblock %} diff --git a/templates/user/new.html.twig b/templates/user/new.html.twig new file mode 100644 index 0000000..35e728d --- /dev/null +++ b/templates/user/new.html.twig @@ -0,0 +1,11 @@ +{% extends 'base.html.twig' %} + +{% block title %}New User{% endblock %} + +{% block body %} +

Create new User

+ + {{ include('user/_form.html.twig') }} + + back to list +{% endblock %} diff --git a/templates/user/show.html.twig b/templates/user/show.html.twig new file mode 100644 index 0000000..ffc5b61 --- /dev/null +++ b/templates/user/show.html.twig @@ -0,0 +1,38 @@ +{% extends 'base.html.twig' %} + +{% block title %}User{% endblock %} + +{% block body %} +

User

+ + + + + + + + + + + + + + + + + + + + + + + + +
Id{{ user.id }}
Email{{ user.email }}
Roles{{ user.roles ? user.roles[0] : '' }}
Nom{{ user.nom }}
Prenom{{ user.prenom }}
+ + back to list + + edit + + {{ include('user/_delete_form.html.twig') }} +{% endblock %} From 0389eb814e2c4003d1a3fd32fe642372263ddd05 Mon Sep 17 00:00:00 2001 From: Aymeric SERRA Date: Thu, 9 Feb 2023 15:34:19 +0100 Subject: [PATCH 2/5] Remplacement ROLE_INSTRUCTOR en ROLE_INSTRUCTEUR --- config/packages/security.yaml | 8 ++++---- src/Controller/AtelierController.php | 8 ++++---- src/Form/UserType.php | 2 +- templates/components/navbar.html.twig | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index d6ee356..5beaf40 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -10,8 +10,8 @@ security: class: App\Entity\User property: email role_hierarchy: - ROLE_ADMINISTRATEUR: [ROLE_INSTRUCTOR, ROLE_APPRENTI, ROLE_USER] - ROLE_INSTRUCTOR: ROLE_USER + ROLE_ADMINISTRATEUR: [ ROLE_INSTRUCTEUR, ROLE_APPRENTI, ROLE_USER ] + ROLE_INSTRUCTEUR: ROLE_USER ROLE_APPRENTI: ROLE_USER firewalls: dev: @@ -35,8 +35,8 @@ 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: ^/profile, roles: ROLE_USER } + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } when@test: security: diff --git a/src/Controller/AtelierController.php b/src/Controller/AtelierController.php index 0c99ca8..bb860ce 100644 --- a/src/Controller/AtelierController.php +++ b/src/Controller/AtelierController.php @@ -24,7 +24,7 @@ class AtelierController extends AbstractController ]); } - #[IsGranted('ROLE_INSTRUCTOR')] + #[IsGranted('ROLE_INSTRUCTEUR')] #[Route('/byme', name: 'app_atelier_by_me', methods: ['GET'])] public function index_created_by_user(AtelierRepository $atelierRepository, MarkdownAtelier $markdown): Response { @@ -44,7 +44,7 @@ class AtelierController extends AbstractController ]); } - #[IsGranted('ROLE_INSTRUCTOR')] + #[IsGranted('ROLE_INSTRUCTEUR')] #[Route('/new', name: 'app_atelier_new', methods: ['GET', 'POST'])] public function new(Request $request, AtelierRepository $atelierRepository): Response { @@ -74,7 +74,7 @@ class AtelierController extends AbstractController ]); } - #[IsGranted('ROLE_INSTRUCTOR')] + #[IsGranted('ROLE_INSTRUCTEUR')] #[Route('/{id}/edit', name: 'app_atelier_edit', methods: ['GET', 'POST'])] public function edit(Request $request, Atelier $atelier, AtelierRepository $atelierRepository): Response { @@ -97,7 +97,7 @@ class AtelierController extends AbstractController ]); } - #[IsGranted('ROLE_INSTRUCTOR')] + #[IsGranted('ROLE_INSTRUCTEUR')] #[Route('/{id}', name: 'app_atelier_delete', methods: ['POST'])] public function delete(Request $request, Atelier $atelier, AtelierRepository $atelierRepository): Response { diff --git a/src/Form/UserType.php b/src/Form/UserType.php index 49ed12b..02f7926 100644 --- a/src/Form/UserType.php +++ b/src/Form/UserType.php @@ -19,7 +19,7 @@ class UserType extends AbstractType ->add('roles', ChoiceType::class, [ 'choices' => [ 'Apprenti' => 'ROLE_APPRENTI', - 'Instructeur' => 'ROLE_INSTRUCTOR', + 'Instructeur' => 'ROLE_INSTRUCTEUR', 'Admin' => 'ROLE_ADMINISTRATEUR' ], 'required' => true, diff --git a/templates/components/navbar.html.twig b/templates/components/navbar.html.twig index b0d18df..31c43b8 100644 --- a/templates/components/navbar.html.twig +++ b/templates/components/navbar.html.twig @@ -14,7 +14,7 @@ Atelier - {% if app.user and is_granted('ROLE_INSTRUCTOR') %} + {% if app.user and is_granted('ROLE_INSTRUCTEUR') %} From edada158794173beaf93ddc8cd36be7b22696199 Mon Sep 17 00:00:00 2001 From: Aymeric SERRA Date: Thu, 9 Feb 2023 15:41:12 +0100 Subject: [PATCH 3/5] =?UTF-8?q?Mot=20de=20passe=20quand=20on=20cr=C3=A9er?= =?UTF-8?q?=20un=20utilisateur=20depuis=20interface=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Controller/UserController.php | 12 +++++- src/Form/NewUserType.php | 71 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 src/Form/NewUserType.php diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 94b2cc8..6e6bad2 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -3,12 +3,14 @@ namespace App\Controller; use App\Entity\User; +use App\Form\NewUserType; use App\Form\UserType; use App\Repository\UserRepository; use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Annotation\Route; #[IsGranted('ROLE_ADMINISTRATEUR')] @@ -24,13 +26,19 @@ class UserController extends AbstractController } #[Route('/new', name: 'app_user_new', methods: ['GET', 'POST'])] - public function new(Request $request, UserRepository $userRepository): Response + public function new(Request $request, UserRepository $userRepository, UserPasswordHasherInterface $userPasswordHasher): Response { $user = new User(); - $form = $this->createForm(UserType::class, $user); + $form = $this->createForm(NewUserType::class, $user); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); $userRepository->save($user, true); return $this->redirectToRoute('app_user_index', [], Response::HTTP_SEE_OTHER); diff --git a/src/Form/NewUserType.php b/src/Form/NewUserType.php new file mode 100644 index 0000000..2940762 --- /dev/null +++ b/src/Form/NewUserType.php @@ -0,0 +1,71 @@ +add('email') + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + 'attr' => ['autocomplete' => 'new-password'], + 'constraints' => [ + new NotBlank([ + 'message' => 'Please enter a password', + ]), + new Length([ + 'min' => 6, + 'minMessage' => 'Your password should be at least {{ limit }} characters', + // max length allowed by Symfony for security reasons + 'max' => 4096, + ]), + ], + ]) + ->add('roles', ChoiceType::class, [ + 'choices' => [ + 'Apprenti' => 'ROLE_APPRENTI', + 'Instructeur' => 'ROLE_INSTRUCTEUR', + 'Admin' => 'ROLE_ADMINISTRATEUR' + ], + 'required' => true, + 'multiple' => false, + 'expanded' => false, + ]) + ->add('nom') + ->add('prenom'); + + $builder->get('roles') + ->addModelTransformer(new CallbackTransformer( + function ($rolesArray) { + // transform the array to a string + return count($rolesArray) ? $rolesArray[0] : null; + }, + function ($rolesString) { + // transform the string back to an array + return [$rolesString]; + } + )); + + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} From 1557ff0347df4e07ec210e0cce963a6344ee9fae Mon Sep 17 00:00:00 2001 From: Aymeric SERRA Date: Thu, 9 Feb 2023 15:52:24 +0100 Subject: [PATCH 4/5] Ajout d'un admin dans la fixture --- src/DataFixtures/AtelierFixture.php | 30 ++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/DataFixtures/AtelierFixture.php b/src/DataFixtures/AtelierFixture.php index be7dc3c..8606d61 100644 --- a/src/DataFixtures/AtelierFixture.php +++ b/src/DataFixtures/AtelierFixture.php @@ -7,29 +7,45 @@ use App\Entity\User; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; use Faker\Generator; +use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; class AtelierFixture extends Fixture { + private UserPasswordHasherInterface $userPasswordHasher; + + public function __construct(UserPasswordHasherInterface $userPasswordHasher) + { + $this->userPasswordHasher = $userPasswordHasher; + } + public function load(ObjectManager $manager): void { $faker = \Faker\Factory::create("fr_FR"); $user = new User(); - $user->setEmail('test@hotmail.com') - ->setNom("test") - ->setPrenom("test") - ->setPassword(""); + $user + ->setEmail('admin@admin.fr') + ->setNom("admin") + ->setPrenom("admin") + ->setRoles(['ROLE_ADMINISTRATEUR']); + + $user->setPassword( + $this->userPasswordHasher->hashPassword( + $user, + 'admin' + ) + ); + $manager->persist($user); $users = $this->createUsers($manager, $faker); - $manager->persist($user); for ($i = 0; $i <= 20; $i++) { $atelier = new Atelier(); $atelier->setNom($faker->word) ->setDescription("# " . $faker->sentence(3) . "\n" . $faker->paragraph()) ->setInstructeur($user); - foreach ($faker->randomElements($users, $faker->randomNumber() % sizeof($users)) as $user) { - $atelier->addEleve($user); + foreach ($faker->randomElements($users, $faker->randomNumber() % sizeof($users)) as $eleve) { + $atelier->addEleve($eleve); } $manager->persist($atelier); From fc5b58fffb47d28f9d83b315371d3cb067c1a437 Mon Sep 17 00:00:00 2001 From: Aymeric SERRA Date: Thu, 9 Feb 2023 15:56:43 +0100 Subject: [PATCH 5/5] =?UTF-8?q?Mise=20=C3=A0=20jour=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3fee068..b935f4c 100644 --- a/README.md +++ b/README.md @@ -95,4 +95,7 @@ Pas de commandes. symfony console doctrine:fixtures:load ``` -### Question 15 \ No newline at end of file +### Question 15 +```bash +symfony console doctrine:fixtures:load +``` \ No newline at end of file