From e57722cabbb3041e166f8b3209f826da950443e6 Mon Sep 17 00:00:00 2001 From: samuel Date: Thu, 5 Jun 2025 09:16:01 +0200 Subject: [PATCH] Auth with Symfony Form --- config/packages/security.yaml | 29 +++++- public/css/login.css | 118 ++++++++++++++++++++++ public/css/register.css | 88 ++++++++++++++++ src/Controller/EmojiController.php | 8 +- src/Controller/HomeController.php | 2 +- src/Controller/LoginController.php | 29 ++++++ src/Controller/RegistrationController.php | 48 +++++++++ src/Controller/UserController.php | 59 +++++++++++ src/Entity/User.php | 116 +++++++++++++++++++++ src/Form/RegistrationFormType.php | 44 ++++++++ src/Repository/UserRepository.php | 67 ++++++++++++ src/Security/LoginFormAuthenticator.php | 59 +++++++++++ templates/auth/login.html.twig | 43 ++++++++ templates/auth/register.html.twig | 30 ++++++ templates/home/index.html.twig | 8 ++ 15 files changed, 739 insertions(+), 9 deletions(-) create mode 100644 public/css/login.css create mode 100644 public/css/register.css create mode 100644 src/Controller/LoginController.php create mode 100644 src/Controller/RegistrationController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Entity/User.php create mode 100644 src/Form/RegistrationFormType.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/Security/LoginFormAuthenticator.php create mode 100644 templates/auth/login.html.twig create mode 100644 templates/auth/register.html.twig diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..9ebc483 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -4,14 +4,30 @@ 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\LoginFormAuthenticator + logout: + path: app_logout + target: app_login + entry_point: App\Security\LoginFormAuthenticator + + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + always_remember_me: true # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -22,8 +38,13 @@ 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: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/register, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_USER } + + + + when@test: security: diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..50e0556 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,118 @@ +body { + background-color: #314e57; + font-family: 'Georgia', serif; + text-align: center; +} + +.login-container { + background-color: #3b6068; + border: 4px solid #000; + border-radius: 15px; + padding: 40px 30px; + margin: 60px auto; + width: 90%; + max-width: 400px; + box-shadow: 0 0 30px rgba(0,0,0,0.6); + color: #f8f5e0; + font-family: 'Georgia', serif; +} + +.login-title { + font-size: 2.5rem; + color: #f8b435; + margin-bottom: 20px; + text-shadow: 2px 2px 4px #000; +} + +.login-error { + background-color: #8b0000; + color: #fff8e1; + padding: 10px; + border: 2px solid #000; + border-radius: 8px; + margin-bottom: 20px; + font-weight: bold; +} + +.already-logged { + background-color: #f2e6c9; + color: #000; + padding: 12px; + border: 2px dashed #000; + border-radius: 8px; + margin-bottom: 20px; + font-size: 0.95rem; +} + +.logout-link { + display: inline-block; + margin-top: 10px; + color: #8b0000; + font-weight: bold; + text-decoration: underline; +} + +.login-form { + display: flex; + flex-direction: column; + gap: 15px; +} + +.login-form label { + text-align: left; + color: #f8f5e0; + font-weight: bold; +} + +.login-form input[type="text"], +.login-form input[type="password"] { + padding: 10px; + border: 2px solid #000; + border-radius: 6px; + background-color: #f2e6c9; + font-family: 'Georgia', serif; + font-size: 1rem; + box-shadow: inset 2px 2px 5px rgba(0,0,0,0.2); +} + +.btn-login { + background-color: #f8b435; + color: #000; + font-weight: bold; + border: 3px solid #000; + padding: 10px; + border-radius: 8px; + font-size: 1.2rem; + cursor: pointer; + box-shadow: 3px 3px 0 #000; + transition: transform 0.2s ease; +} + +.btn-login:hover { + transform: scale(1.05); + background-color: #e09f30; +} + +.no-account { + margin-top: 30px; + color: #f8f5e0; +} + +.btn-register { + display: inline-block; + margin-top: 10px; + background-color: #f2e6c9; + color: #000; + border: 2px solid #000; + padding: 8px 20px; + border-radius: 6px; + text-decoration: none; + font-weight: bold; + box-shadow: 2px 2px 0 #000; + transition: transform 0.2s ease; +} + +.btn-register:hover { + transform: scale(1.05); + background-color: #e5d6b8; +} diff --git a/public/css/register.css b/public/css/register.css new file mode 100644 index 0000000..bb85486 --- /dev/null +++ b/public/css/register.css @@ -0,0 +1,88 @@ +body { + background-color: #314e57 !important; + font-family: 'Georgia', serif; + text-align: center; +} + +.register-container { + background-color: #3b6068; + border: 4px solid #000; + border-radius: 15px; + padding: 40px 30px; + margin: 60px auto; + width: 90%; + max-width: 400px; + box-shadow: 0 0 30px rgba(0,0,0,0.6); + color: #f8f5e0; + font-family: 'Georgia', serif; +} + +.register-title { + font-size: 2.5rem; + color: #f8b435; + margin-bottom: 20px; + text-shadow: 2px 2px 4px #000; +} + +.register-form label { + text-align: left; + color: #f8f5e0; + font-weight: bold; +} + +.register-form input[type="text"], +.register-form input[type="password"] { + padding: 10px; + border: 2px solid #000; + border-radius: 6px; + background-color: #f2e6c9; + font-family: 'Georgia', serif; + font-size: 1rem; + box-shadow: inset 2px 2px 5px rgba(0,0,0,0.2); + width: 100%; + margin-bottom: 15px; +} + +.btn-register-submit { + background-color: #f8b435; + color: #000; + font-weight: bold; + border: 3px solid #000; + padding: 10px; + border-radius: 8px; + font-size: 1.2rem; + cursor: pointer; + box-shadow: 3px 3px 0 #000; + transition: transform 0.2s ease; + width: 100%; + margin-bottom: 20px; +} + +.btn-register-submit:hover { + transform: scale(1.05); + background-color: #e09f30; +} + +.already-account { + color: #f8f5e0; + font-size: 0.95rem; +} + +.btn-login-link { + display: inline-block; + margin-top: 10px; + background-color: #f2e6c9; + color: #000; + border: 2px solid #000; + padding: 8px 20px; + border-radius: 6px; + text-decoration: none; + font-weight: bold; + box-shadow: 2px 2px 0 #000; + transition: transform 0.2s ease; +} + +.btn-login-link:hover { + transform: scale(1.05); + background-color: #e5d6b8; +} diff --git a/src/Controller/EmojiController.php b/src/Controller/EmojiController.php index 416e20e..8c5ffad 100644 --- a/src/Controller/EmojiController.php +++ b/src/Controller/EmojiController.php @@ -13,7 +13,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use App\Repository\EmojiRepository; -#[Route('/emoji', name: 'emoji')] +#[Route('/emojis', name: 'app_emoji_')] class EmojiController extends AbstractController { private RarityRepository $rarityRepository; @@ -25,7 +25,7 @@ class EmojiController extends AbstractController $this->httpClient = $httpClient; } - #[Route('/emoji', name: 'app_emoji')] + #[Route('/', name: 'emojis')] public function index(): Response { $this->testToMove(); @@ -41,7 +41,7 @@ class EmojiController extends AbstractController return new Response(['count' => $count]); } - #[Route('/addRarity', name: 'addR')] + #[Route('/addRarity', name: 'add_rarity')] public function addRarityDebug(EntityManagerInterface $entityManager) { $rarity = new Rarity(); $rarity->setName('Bip'); @@ -52,7 +52,7 @@ class EmojiController extends AbstractController return new Response(); } - #[Route('/add/{code}', name: 'add')] + #[Route('/add/{code}', name: 'add_code')] public function addEmojiDebug(string $code, EntityManagerInterface $entityManager) { $emoji = new Emoji(); $emoji->setCode($code); diff --git a/src/Controller/HomeController.php b/src/Controller/HomeController.php index 95761d7..87f291d 100644 --- a/src/Controller/HomeController.php +++ b/src/Controller/HomeController.php @@ -8,7 +8,7 @@ use Symfony\Component\Routing\Annotation\Route; class HomeController extends AbstractController { - #[Route('/', name: 'home')] + #[Route('/', name: 'app_home')] public function index(): Response { $emojisDeBase = [ diff --git a/src/Controller/LoginController.php b/src/Controller/LoginController.php new file mode 100644 index 0000000..1b26f59 --- /dev/null +++ b/src/Controller/LoginController.php @@ -0,0 +1,29 @@ +getUser()) { + return $this->redirectToRoute('app_home'); + } + $error = $authenticationUtils->getLastAuthenticationError(); + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('auth/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/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..50f4cf5 --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,48 @@ +createForm(RegistrationFormType::class, $user); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $user->setPassword( + $userPasswordHasher->hashPassword( + $user, + $form->get('plainPassword')->getData() + ) + ); + $user->setRoles(['ROLE_USER']); + + $entityManager->persist($user); + $entityManager->flush(); + + return $userAuthenticator->authenticateUser( + $user, + $authenticator, + $request + ); + } + + return $this->render('auth/register.html.twig', [ + 'registrationForm' => $form->createView(), + ]); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php new file mode 100644 index 0000000..b4006be --- /dev/null +++ b/src/Controller/UserController.php @@ -0,0 +1,59 @@ +entityManager = $entityManager; + } + + #[Route('/{userId}', name: 'get_by_id', methods: ['GET'])] + public function getUserById(int $userId): JsonResponse + { + $user = $this->entityManager->getRepository(User::class)->find($userId); + + if (!$user) { + return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } + + $data = [ + 'id' => $user->getId(), + 'username' => $user->getUsername(), + 'roles' => $user->getRoles(), + ]; + + return $this->json($data); + } + + #[Route('/{userId}/emojis', name: 'get_emojis_by_user', methods: ['GET'])] + public function getEmojisByUserId(int $userId): JsonResponse + { + $user = $this->entityManager->getRepository(User::class)->find($userId); + + if (!$user) { + return $this->json(['error' => 'User not found'], Response::HTTP_NOT_FOUND); + } + + $emojis = $user->getEmojis()->map(function ($emoji) { + return [ + 'id' => $emoji->getId(), + 'name' => $emoji->getName(), + 'code' => $emoji->getCode(), + ]; + })->toArray(); + + return $this->json($emojis); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..bef596e --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,116 @@ +emojis = new ArrayCollection(); + } + + 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); + } + + 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; + } + + public function getEmojis(): Collection +{ + return $this->emojis; +} + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..b5f3be1 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,44 @@ +add('username') + ->add('plainPassword', PasswordType::class, [ + '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' => 4096, + ]), + ], + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..c788f46 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,67 @@ + + * + * @implements PasswordUpgraderInterface + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +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/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php new file mode 100644 index 0000000..84bebf1 --- /dev/null +++ b/src/Security/LoginFormAuthenticator.php @@ -0,0 +1,59 @@ +request->get('username', ''); + + $request->getSession()->set(Security::LAST_USERNAME, $username); + + return new Passport( + new UserBadge($username), + new PasswordCredentials($request->request->get('password', '')), + [ + new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')), + new RememberMeBadge(), + ] + ); + } + + 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/auth/login.html.twig b/templates/auth/login.html.twig new file mode 100644 index 0000000..e6b945f --- /dev/null +++ b/templates/auth/login.html.twig @@ -0,0 +1,43 @@ +{% extends 'base.html.twig' %} + +{% block title %}Connexion{% endblock %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body %} + + +{% endblock %} diff --git a/templates/auth/register.html.twig b/templates/auth/register.html.twig new file mode 100644 index 0000000..3e7cb4d --- /dev/null +++ b/templates/auth/register.html.twig @@ -0,0 +1,30 @@ +{% extends 'base.html.twig' %} + +{% block title %}Inscription{% endblock %} + +{% block stylesheets %} + {{ parent() }} + +{% endblock %} + +{% block body %} +
+

📝 Inscription

+ + {{ form_start(registrationForm, {'attr': {'class': 'register-form'}}) }} + {{ form_errors(registrationForm) }} + + {{ form_row(registrationForm.username) }} + {{ form_row(registrationForm.plainPassword, { + label: 'Mot de passe' + }) }} + + + + + {{ form_end(registrationForm) }} +
+{% endblock %} diff --git a/templates/home/index.html.twig b/templates/home/index.html.twig index 2f191c6..986565c 100644 --- a/templates/home/index.html.twig +++ b/templates/home/index.html.twig @@ -7,6 +7,14 @@ {% endblock %} {% block body %} + {% if app.user %} +
+ Connecté en tant que {{ app.user.username }} +
+ +
+
+ {% endif %}

🧬 Ma collection de créatures 🐾