diff --git a/.env b/.env index 89db31614a100fed6a0e412a66745805e3c94820..f52592bcdde2a1b1934985edbe9d9254fc2ac9f6 100644 --- a/.env +++ b/.env @@ -29,4 +29,6 @@ MESSENGER_TRANSPORT_DSN=sync:// # DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4" DATABASE_URL="mysql://root:root@172.18.0.3:3306/wishlist?serverVersion=8.0" ###< doctrine/doctrine-bundle ### +APP_SECRET=3d9f431b0fd768e0cb2e8b139c66e9fd67ecbd49762f6a345d459e2f115f61b4 + diff --git a/config/packages/security.yaml b/config/packages/security.yaml index ab531b70d83163ee4e35fbf3b49221c4c77712b1..c1cc3ab486014f1f933d5b65f9e8ec7b4f14a6bf 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -24,9 +24,9 @@ security: remember_me: secret: '%kernel.secret%' - access_control: - - { path: ^/admin, allow_if: "user and user.isAdmin == true" } - - { path: ^/locked, allow_if: "user and user.isLocked == true" } + # access_control: + # - { path: ^/admin, allow_if: "user and user.isAdmin == true" } + # - { path: ^/locked, allow_if: "user and user.isLocked == true" } when@test: security: diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php index 6c80d9afc0494720dc0090d0ed5f8cbc5dd6c0dc..f214274cddb282a90f3ffd853d8bc28b84243884 100644 --- a/src/Controller/AdminController.php +++ b/src/Controller/AdminController.php @@ -5,6 +5,10 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Doctrine\ORM\EntityManagerInterface; + +use App\Entity\Item; +use App\Entity\Wishlist; final class AdminController extends AbstractController { @@ -19,9 +23,17 @@ final class AdminController extends AbstractController #[Route('/admin/dashboard', name: 'admin_dashboard')] public function dashboard(EntityManagerInterface $entityManager): Response { - $topItems = $entityManager->getRepository(WishlistItem::class)->findTopExpensiveItems(); + $topItems = $entityManager->getRepository(Item::class)->findTopExpensiveItems(); $topWishlists = $entityManager->getRepository(Wishlist::class)->findTopWishlistsByValue(); + + if (!$topItems) { + $topItems = []; + } + + if (!$topWishlists) { + $topWishlists = []; + } return $this->render('admin/dashboard.html.twig', [ 'topItems' => $topItems, 'topWishlists' => $topWishlists, diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000000000000000000000000000000000000..fbf5028dc649bb727b6452e82a533df8d11bd6ad --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,40 @@ +<?php + +namespace App\Controller; + +use App\Entity\User; +use App\Form\RegistrationFormType; +use Doctrine\ORM\EntityManagerInterface; +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; + +class RegistrationController extends AbstractController +{ + #[Route('/register', name: 'register')] + public function register(Request $request, UserPasswordHasherInterface $passwordHasher, EntityManagerInterface $entityManager): Response + { + $user = new User(); + $form = $this->createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Hacher le mot de passe + $hashedPassword = $passwordHasher->hashPassword($user, $user->getPassword()); + $user->setPassword($hashedPassword); + + // Sauvegarder l'utilisateur + $entityManager->persist($user); + $entityManager->flush(); + + // Rediriger vers la page de connexion + return $this->redirectToRoute('login'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } +} \ No newline at end of file diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000000000000000000000000000000000000..af98a7890bd20a3874338b304a04450b892529d1 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,33 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +use Doctrine\ORM\EntityManagerInterface; + +class SecurityController extends AbstractController +{ + #[Route('/login', name: 'login')] + public function login(AuthenticationUtils $authenticationUtils): Response + { + // Récupère les erreurs de connexion, s'il y en a + $error = $authenticationUtils->getLastAuthenticationError(); + + // Récupère le dernier email saisi par l'utilisateur + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route('/logout', name: 'logout')] + public function logout(): void + { + // Ce contrôleur peut rester vide, Symfony gère la déconnexion automatiquement + } +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 97f6ded8c1b78b04ac0b7546b70f770162397754..b058aa940aab4de75d2b82cd10a39f23e66d420c 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -5,6 +5,7 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Doctrine\ORM\EntityManagerInterface; final class UserController extends AbstractController { diff --git a/src/Entity/User.php b/src/Entity/User.php index 9ba7e12960731e0b435e0d741ba710b4f132a9e7..197c87673a763194b5e52811967924622573e979 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -5,6 +5,8 @@ namespace App\Entity; use App\Repository\UserRepository; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\User\UserInterface; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; #[ORM\Entity(repositoryClass: UserRepository::class)] class User implements UserInterface @@ -53,6 +55,20 @@ class User implements UserInterface #[ORM\Column(length: 255, nullable: true)] private ?string $image = null; + #[ORM\OneToMany(mappedBy: 'owner', targetEntity: Wishlist::class)] + private Collection $wishlists; + + #[ORM\OneToMany(mappedBy: 'invitedUser', targetEntity: Item::class)] + private Collection $invitations; + + public function __construct() + { + $this->wishlists = new ArrayCollection(); + $this->invitations = new ArrayCollection(); + $this->roles = ['ROLE_USER']; + $this->isLocked = false; + } + public function getId(): ?int { return $this->id; @@ -142,11 +158,23 @@ class User implements UserInterface return $this; } - public function __construct() + public function getWishlists(): Collection { - $this->wishlists = new ArrayCollection(); - $this->purchasedItems = new ArrayCollection(); - $this->roles = ['ROLE_USER']; - $this->isLocked = false; + return $this->wishlists; } + + // public function getInvitations(): Collection + // { + // return $this->invitations; + // } + + // public function addInvitation(Item $invitation): static + // { + // if (!$this->invitations->contains($invitation)) { + // $this->invitations[] = $invitation; + // $invitation->setInvitedUser($this); + // } + + // } + } diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000000000000000000000000000000000000..89c463ad2a136d76a099b5bd4a20cb14d5d139e0 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,70 @@ +<?php + +namespace App\Form; + +use App\Entity\User; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\FileType; +use Symfony\Component\Form\Extension\Core\Type\PasswordType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\File; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; + +class RegistrationFormType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('firstName', TextType::class, [ + 'label' => 'Prénom', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('lastName', TextType::class, [ + 'label' => 'Nom', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'constraints' => [ + new NotBlank(), + ], + ]) + ->add('password', PasswordType::class, [ + 'label' => 'Mot de passe', + 'constraints' => [ + new NotBlank(), + new Length(['min' => 6]), + ], + ]) + ->add('image', FileType::class, [ + 'label' => 'Image de profil (optionnel)', + 'required' => false, + 'mapped' => false, + 'constraints' => [ + new File([ + 'maxSize' => '2M', + 'mimeTypes' => [ + 'image/jpeg', + 'image/png', + ], + 'mimeTypesMessage' => 'Veuillez télécharger une image valide (JPEG ou PNG).', + ]), + ], + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} \ No newline at end of file diff --git a/src/Repository/ItemRepository.php b/src/Repository/ItemRepository.php index 229f40652e92f36659747e345c2569cb626b2aa2..1d57e0458ffc005511fbbefc7e45b7d958689ceb 100644 --- a/src/Repository/ItemRepository.php +++ b/src/Repository/ItemRepository.php @@ -16,6 +16,15 @@ class ItemRepository extends ServiceEntityRepository parent::__construct($registry, Item::class); } + + public function findTopExpensiveItems(): array + { + return $this->createQueryBuilder('i') // Alias "i" pour Item + ->orderBy('i.price', 'DESC') // Trier par prix décroissant + ->setMaxResults(3) // Limiter à 3 résultats + ->getQuery() + ->getResult(); // Exécuter la requête + } // /** // * @return Item[] Returns an array of Item objects // */ diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index 96ef606b44e63ae4ca3673cfc5dd2016833b6519..58793646c59e2a296843548663a195d99b0d8f75 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -32,6 +32,16 @@ public function findLockedUsers(): array ->getQuery() ->getResult(); } + +public function findByEmail(string $email): ?User +{ + return $this->createQueryBuilder('u') + ->where('u.email = :email') + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(); +} + // /** // * @return User[] Returns an array of User objects // */ diff --git a/src/Repository/WishlistRepository.php b/src/Repository/WishlistRepository.php index ed4383671dbe11e3d04de5b95f6824b78087077e..e714b9fd2c22f8e7747f0b05ed93313b1cd3b89d 100644 --- a/src/Repository/WishlistRepository.php +++ b/src/Repository/WishlistRepository.php @@ -60,7 +60,7 @@ class WishlistRepository extends ServiceEntityRepository public function findTopWishlistsByValue(): array { return $this->createQueryBuilder('u') - ->join('u.wishlists', 'w') + ->join('u.wishlist', 'w') ->join('w.items', 'i') ->where('i.isPurchased = true') ->groupBy('w.id') diff --git a/templates/base.html.twig b/templates/base.html.twig index 4b5725c98dfdee9ed4a86c34f9aa67f62daa524a..213ebafff224acd34ceb8ae6605b8a2da7215aae 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -6,10 +6,16 @@ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text><text y=%221.3em%22 x=%220.2em%22 font-size=%2276%22 fill=%22%23fff%22>sf</text></svg>"> <link rel="stylesheet" href="{{ asset('css/style.css') }}"> {% block stylesheets %} + <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> + <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"> + <link href="{{ asset('css/style.css') }}" rel="stylesheet"> {% endblock %} {% block javascripts %} - {% block importmap %}{{ importmap('app') }}{% endblock %} + {% block importmap %}{{ importmap('app') }} + {% endblock %} + <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> + <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script> {% endblock %} </head> <body> @@ -22,17 +28,7 @@ {% endfor %} {% endfor %} - {% block stylesheets %} - <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> - <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"> - <link href="{{ asset('css/style.css') }}" rel="stylesheet"> - {% endblock %} - - {% block javascripts %} - <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> - <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/js/all.min.js"></script> - {% endblock %} - + {% block body %}{% endblock %} </body> </html> diff --git a/templates/registration/index.html.twig b/templates/registration/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..e7ded79bcf6cbee1615e2aec99461f2ee3d298a3 --- /dev/null +++ b/templates/registration/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello RegistrationController!{% endblock %} + +{% block body %} +<style> + .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } + .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } +</style> + +<div class="example-wrapper"> + <h1>Hello {{ controller_name }}! ✅</h1> + + This friendly message is coming from: + <ul> + <li>Your controller at <code>/home/user/www/Exercices/Wishlist-application/src/Controller/RegistrationController.php</code></li> + <li>Your template at <code>/home/user/www/Exercices/Wishlist-application/templates/registration/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..1f9833dab4f5fb7ea0d896e4431a7ff418c77a1e --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,18 @@ +{% extends 'base.html.twig' %} + +{% block title %}Inscription{% endblock %} + +{% block body %} +<div class="container mt-5"> + <h1>Inscription</h1> + + {{ form_start(registrationForm) }} + {{ form_row(registrationForm.firstName) }} + {{ form_row(registrationForm.lastName) }} + {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.password) }} + {{ form_row(registrationForm.image) }} + <button type="submit" class="btn btn-primary">S'inscrire</button> + {{ form_end(registrationForm) }} +</div> +{% endblock %} \ No newline at end of file diff --git a/templates/security/index.html.twig b/templates/security/index.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..71358c2f5b7080448345245c47ee3bb24f7ec76b --- /dev/null +++ b/templates/security/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello SecurityController!{% endblock %} + +{% block body %} +<style> + .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; } + .example-wrapper code { background: #F5F5F5; padding: 2px 6px; } +</style> + +<div class="example-wrapper"> + <h1>Hello {{ controller_name }}! ✅</h1> + + This friendly message is coming from: + <ul> + <li>Your controller at <code>/home/user/www/Exercices/Wishlist-application/src/Controller/SecurityController.php</code></li> + <li>Your template at <code>/home/user/www/Exercices/Wishlist-application/templates/security/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..244b6d55b638885ea8710f7eda847f831bc10cbf --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,29 @@ +{% extends 'base.html.twig' %} + +{% block title %}Connexion{% endblock %} + +{% block body %} +<div class="container mt-5"> + <h1>Connexion</h1> + + {% if error %} + <div class="alert alert-danger"> + {{ error.messageKey|trans(error.messageData, 'security') }} + </div> + {% endif %} + + <form method="post" action="{{ path('login') }}"> + <div class="mb-3"> + <label for="username" class="form-label">Email</label> + <input type="text" name="_username" id="username" class="form-control" value="{{ last_username }}" required autofocus> + </div> + + <div class="mb-3"> + <label for="password" class="form-label">Mot de passe</label> + <input type="password" name="_password" id="password" class="form-control" required> + </div> + + <button type="submit" class="btn btn-primary">Se connecter</button> + </form> +</div> +{% endblock %} \ No newline at end of file