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/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/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 %} +
+ +

🔐 Connexion

+ + {% if error %} + + {% endif %} + + {% if app.user %} +
+ Connecté en tant que {{ app.user.userIdentifier }}, + Se déconnecter +
+ {% endif %} + + + +
+
+

Pas encore de compte ?

+ Créer un compte +
+{% endblock %} diff --git a/templates/auth/register.html.twig b/templates/auth/register.html.twig new file mode 100644 index 0000000..481e573 --- /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) }} + {{ 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 🐾