From 30773ddfd74b4737567bb2342b1a6789f4f417d4 Mon Sep 17 00:00:00 2001 From: user <user@imta.fr> Date: Tue, 25 Mar 2025 18:57:18 +0100 Subject: [PATCH] User V1 --- config/packages/security.yaml | 41 ++++--- config/routes/security.yaml | 2 +- public/css/style.css | 39 +++++++ src/Controller/AdminController.php | 68 ++++++++++++ src/Controller/UserController.php | 64 +++++++++++ src/Entity/User.php | 152 ++++++++++++++++++++++++++ src/Repository/UserRepository.php | 59 ++++++++++ src/Repository/WishlistRepository.php | 12 ++ templates/admin/dashboard.html.twig | 118 ++++++++++++++++++++ templates/admin/index.html.twig | 20 ++++ templates/base.html.twig | 20 ++++ templates/user/index.html.twig | 20 ++++ 12 files changed, 593 insertions(+), 22 deletions(-) create mode 100644 src/Controller/AdminController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Entity/User.php create mode 100644 src/Repository/UserRepository.php create mode 100644 templates/admin/dashboard.html.twig create mode 100644 templates/admin/index.html.twig create mode 100644 templates/user/index.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25a..ab531b70 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,39 +1,38 @@ security: - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: 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 } + app_user_provider: + entity: + class: App\Entity\User + property: email # Ou 'username' si tu utilises un autre identifiant + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false + main: lazy: true - provider: users_in_memory - - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall - - # https://symfony.com/doc/current/security/impersonating_user.html - # switch_user: true + provider: app_user_provider + form_login: + login_path: login + check_path: login + logout: + path: logout + remember_me: + secret: '%kernel.secret%' - # 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, allow_if: "user and user.isAdmin == true" } + - { path: ^/locked, allow_if: "user and user.isLocked == true" } when@test: security: password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # 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 - cost: 4 # Lowest possible value for bcrypt - time_cost: 3 # Lowest possible value for argon - memory_cost: 10 # Lowest possible value for argon + cost: 4 + time_cost: 3 + memory_cost: 10 diff --git a/config/routes/security.yaml b/config/routes/security.yaml index f853be15..60a7afbd 100644 --- a/config/routes/security.yaml +++ b/config/routes/security.yaml @@ -1,3 +1,3 @@ _security_logout: resource: security.route_loader.logout - type: service + type: service \ No newline at end of file diff --git a/public/css/style.css b/public/css/style.css index d99f15ed..31d7b132 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -287,4 +287,43 @@ h1 { border-radius: 3px; padding: 5px; cursor: pointer; +} + +/* Couleurs IMT Atlantique */ +:root { + --imt-primary: #00326E; + --imt-secondary: #009FE3; + --imt-accent: #96C11F; +} + +/* Styles généraux */ +body { + background-color: #f8f9fa; +} + +/* En-tête */ +.navbar { + background-color: var(--imt-primary); +} + +/* Boutons */ +.btn-primary { + background-color: var(--imt-primary); + border-color: var(--imt-primary); +} + +.btn-secondary { + background-color: var(--imt-secondary); + border-color: var(--imt-secondary); +} + +/* Cards */ +.card { + box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075); +} + +/* Tables */ +.table thead th { + background-color: var(--imt-primary); + color: white; } \ No newline at end of file diff --git a/src/Controller/AdminController.php b/src/Controller/AdminController.php new file mode 100644 index 00000000..6c80d9af --- /dev/null +++ b/src/Controller/AdminController.php @@ -0,0 +1,68 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class AdminController extends AbstractController +{ + #[Route('/admin', name: 'app_admin')] + public function index(): Response + { + return $this->render('admin/index.html.twig', [ + 'controller_name' => 'AdminController', + ]); + } + + #[Route('/admin/dashboard', name: 'admin_dashboard')] + public function dashboard(EntityManagerInterface $entityManager): Response + { + $topItems = $entityManager->getRepository(WishlistItem::class)->findTopExpensiveItems(); + $topWishlists = $entityManager->getRepository(Wishlist::class)->findTopWishlistsByValue(); + + return $this->render('admin/dashboard.html.twig', [ + 'topItems' => $topItems, + 'topWishlists' => $topWishlists, + ]); + } + + #[Route('/admin/users', name: 'admin_users')] + public function manageUsers(EntityManagerInterface $entityManager): Response + { + $users = $entityManager->getRepository(User::class)->findAll(); + + return $this->render('admin/users.html.twig', [ + 'users' => $users, + ]); + } + + #[Route('/admin/user/{id}/lock', name: 'admin_user_lock', methods: ['POST'])] + public function lockUser(User $user, EntityManagerInterface $entityManager): Response + { + $user->setIsLocked(true); + $entityManager->flush(); + + return $this->redirectToRoute('admin_users'); + } + + #[Route('/admin/user/{id}/unlock', name: 'admin_user_unlock', methods: ['POST'])] + public function unlockUser(User $user, EntityManagerInterface $entityManager): Response + { + $user->setIsLocked(false); + $entityManager->flush(); + + return $this->redirectToRoute('admin_users'); + } + + #[Route('/admin/user/{id}/delete', name: 'admin_user_delete', methods: ['POST'])] + public function deleteUser(User $user, EntityManagerInterface $entityManager): Response + { + $entityManager->remove($user); + $entityManager->flush(); + + return $this->redirectToRoute('admin_users'); + } + +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 00000000..97f6ded8 --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,64 @@ +<?php + +namespace App\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; + +final class UserController extends AbstractController +{ + #[Route('/user', name: 'app_user')] + public function index(): Response + { + return $this->render('user/index.html.twig', [ + 'controller_name' => 'UserController', + ]); + } + + + #[Route('/user/dashboard', name: 'user_dashboard')] + public function dashboard(EntityManagerInterface $entityManager): Response + { + $user = $this->getUser(); + $wishlists = $entityManager->getRepository(Wishlist::class)->findBy(['owner' => $user]); + + return $this->render('user/dashboard.html.twig', [ + 'user' => $user, + 'wishlists' => $wishlists, + ]); + } + + #[Route('/user/wishlist/new', name: 'user_wishlist_new', methods: ['GET', 'POST'])] + public function createWishlist(Request $request, EntityManagerInterface $entityManager): Response + { + $wishlist = new Wishlist(); + $form = $this->createForm(WishlistType::class, $wishlist); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $wishlist->setOwner($this->getUser()); + $entityManager->persist($wishlist); + $entityManager->flush(); + + $this->addFlash('success', 'Wishlist créée avec succès.'); + return $this->redirectToRoute('user_dashboard'); + } + + return $this->render('user/wishlist_form.html.twig', [ + 'form' => $form->createView(), + ]); + } + + #[Route('/user/wishlist/{id}/delete', name: 'user_wishlist_delete', methods: ['POST'])] + public function deleteWishlist(Wishlist $wishlist, EntityManagerInterface $entityManager): Response + { + $this->denyAccessUnlessGranted('delete', $wishlist); + + $entityManager->remove($wishlist); + $entityManager->flush(); + + $this->addFlash('success', 'Wishlist supprimée avec succès.'); + return $this->redirectToRoute('user_dashboard'); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 00000000..9ba7e129 --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,152 @@ +<?php + +namespace App\Entity; + +use App\Repository\UserRepository; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\User\UserInterface; + +#[ORM\Entity(repositoryClass: UserRepository::class)] +class User implements UserInterface +{ + private array $roles = []; + + public function getRoles(): array + { + return $this->roles; + } + + public function getUserIdentifier(): string + { + return $this->email; + } + + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } + + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column] + private ?int $id = null; + + #[ORM\Column(length: 255)] + private ?string $email = null; + + #[ORM\Column(length: 63)] + private ?string $firstName = null; + + #[ORM\Column(length: 63)] + private ?string $lastName = null; + + #[ORM\Column(length: 255)] + private ?string $password = null; + + #[ORM\Column] + private ?bool $isLocked = null; + + #[ORM\Column] + private ?bool $isAdmin = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $image = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + public function isLocked(): ?bool + { + return $this->isLocked; + } + + public function setIsLocked(bool $isLocked): static + { + $this->isLocked = $isLocked; + + return $this; + } + + public function isAdmin(): ?bool + { + return $this->isAdmin; + } + + public function setIsAdmin(bool $isAdmin): static + { + $this->isAdmin = $isAdmin; + + return $this; + } + + public function getImage(): ?string + { + return $this->image; + } + + public function setImage(?string $image): static + { + $this->image = $image; + + return $this; + } + + public function __construct() + { + $this->wishlists = new ArrayCollection(); + $this->purchasedItems = new ArrayCollection(); + $this->roles = ['ROLE_USER']; + $this->isLocked = false; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 00000000..96ef606b --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,59 @@ +<?php + +namespace App\Repository; + +use App\Entity\User; +use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +use Doctrine\Persistence\ManagerRegistry; + +/** + * @extends ServiceEntityRepository<User> + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + public function findAllUsers(): array +{ + return $this->createQueryBuilder('u') + ->orderBy('u.username', 'ASC') + ->getQuery() + ->getResult(); +} + +public function findLockedUsers(): array +{ + return $this->createQueryBuilder('u') + ->where('u.isLocked = :locked') + ->setParameter('locked', true) + ->getQuery() + ->getResult(); +} + // /** + // * @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/Repository/WishlistRepository.php b/src/Repository/WishlistRepository.php index 543e6355..24833563 100644 --- a/src/Repository/WishlistRepository.php +++ b/src/Repository/WishlistRepository.php @@ -16,6 +16,18 @@ class WishlistRepository extends ServiceEntityRepository parent::__construct($registry, Wishlist::class); } + public function findTopWishlistsByValue(): array + { + return $this->createQueryBuilder('u') + ->join('u.wishlists', 'w') + ->join('w.items', 'i') + ->where('i.isPurchased = true') + ->groupBy('w.id') + ->orderBy('SUM(i.price)', 'DESC') + ->setMaxResults(3) + ->getQuery() + ->getResult(); + } // /** // * @return Wishlist[] Returns an array of Wishlist objects // */ diff --git a/templates/admin/dashboard.html.twig b/templates/admin/dashboard.html.twig new file mode 100644 index 00000000..8c663bf6 --- /dev/null +++ b/templates/admin/dashboard.html.twig @@ -0,0 +1,118 @@ +{% extends 'base.html.twig' %} + +{% block title %}Administration{% endblock %} + +{% block body %} +<div class="container mt-4"> + <h1>Dashboard Administrateur</h1> + + <div class="row mt-4"> + <!-- Statistiques --> + <div class="col-md-6"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Top 3 des items les plus chers</h5> + {% if topItems is empty %} + <p class="text-muted">Aucun item trouvé</p> + {% else %} + <ul class="list-group"> + {% for item in topItems %} + <li class="list-group-item d-flex justify-content-between align-items-center"> + {{ item.title }} + <span class="badge bg-primary rounded-pill">{{ item.price }} €</span> + </li> + {% endfor %} + </ul> + {% endif %} + </div> + </div> + </div> + + <div class="col-md-6"> + <div class="card"> + <div class="card-body"> + <h5 class="card-title">Top 3 des wishlists par valeur</h5> + {% if topWishlists is empty %} + <p class="text-muted">Aucune wishlist trouvée</p> + {% else %} + <ul class="list-group"> + {% for wishlist in topWishlists %} + <li class="list-group-item d-flex justify-content-between align-items-center"> + {{ wishlist.name }} + <span class="badge bg-success rounded-pill">{{ wishlist.totalValue }} €</span> + </li> + {% endfor %} + </ul> + {% endif %} + </div> + </div> + </div> + </div> + + <!-- Gestion des utilisateurs --> + <div class="card mt-4"> + <div class="card-body"> + <h5 class="card-title">Gestion des utilisateurs</h5> + + {% if users is empty %} + <p class="text-muted">Aucun utilisateur enregistré</p> + {% else %} + <div class="table-responsive"> + <table class="table"> + <thead> + <tr> + <th>ID</th> + <th>Nom d'utilisateur</th> + <th>Email</th> + <th>Statut</th> + <th>Actions</th> + </tr> + </thead> + <tbody> + {% for user in users %} + <tr> + <td>{{ user.id }}</td> + <td>{{ user.username }}</td> + <td>{{ user.email }}</td> + <td> + {% if user.isLocked %} + <span class="badge bg-danger">Verrouillé</span> + {% else %} + <span class="badge bg-success">Actif</span> + {% endif %} + </td> + <td> + <div class="btn-group"> + {% if user.isLocked %} + <form method="post" action="{{ path('admin_user_unlock', {'id': user.id}) }}" style="display: inline;"> + <button type="submit" class="btn btn-sm btn-success"> + <i class="fas fa-unlock"></i> + </button> + </form> + {% else %} + <form method="post" action="{{ path('admin_user_lock', {'id': user.id}) }}" style="display: inline;"> + <button type="submit" class="btn btn-sm btn-warning"> + <i class="fas fa-lock"></i> + </button> + </form> + {% endif %} + <form method="post" + action="{{ path('admin_user_delete', {'id': user.id}) }}" + onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?');" + style="display: inline;"> + <button type="submit" class="btn btn-sm btn-danger"> + <i class="fas fa-trash"></i> + </button> + </form> + </div> + </td> + </tr> + {% endfor %} + </tbody> + </table> + </div> + {% endif %} + </div> + </div> +</div> +{% endblock %} \ No newline at end of file diff --git a/templates/admin/index.html.twig b/templates/admin/index.html.twig new file mode 100644 index 00000000..5e68ff4e --- /dev/null +++ b/templates/admin/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello AdminController!{% 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/AdminController.php</code></li> + <li>Your template at <code>/home/user/www/Exercices/Wishlist-application/templates/admin/index.html.twig</code></li> + </ul> +</div> +{% endblock %} diff --git a/templates/base.html.twig b/templates/base.html.twig index 2eac7d55..4b5725c9 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -13,6 +13,26 @@ {% endblock %} </head> <body> + {% for label, messages in app.flashes %} + {% for message in messages %} + <div class="alert alert-{{ label }} alert-dismissible fade show" role="alert"> + {{ message }} + <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> + </div> + {% 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/user/index.html.twig b/templates/user/index.html.twig new file mode 100644 index 00000000..bea6b090 --- /dev/null +++ b/templates/user/index.html.twig @@ -0,0 +1,20 @@ +{% extends 'base.html.twig' %} + +{% block title %}Hello UserController!{% 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/UserController.php</code></li> + <li>Your template at <code>/home/user/www/Exercices/Wishlist-application/templates/user/index.html.twig</code></li> + </ul> +</div> +{% endblock %} -- GitLab