Auth with Symfony Form

samuel
samuel 4 days ago
parent 245f301602
commit 453178ba8f

@ -4,14 +4,30 @@ security:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers: 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: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
main: main:
lazy: true 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 # activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall # 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 # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/login, roles: PUBLIC_ACCESS }
# - { path: ^/profile, roles: ROLE_USER } - { path: ^/register, roles: PUBLIC_ACCESS }
- { path: ^/, roles: ROLE_USER }
when@test: when@test:
security: security:

@ -13,7 +13,7 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use App\Repository\EmojiRepository; use App\Repository\EmojiRepository;
#[Route('/emoji', name: 'emoji')] #[Route('/emojis', name: 'app_emoji_')]
class EmojiController extends AbstractController class EmojiController extends AbstractController
{ {
private RarityRepository $rarityRepository; private RarityRepository $rarityRepository;
@ -25,7 +25,7 @@ class EmojiController extends AbstractController
$this->httpClient = $httpClient; $this->httpClient = $httpClient;
} }
#[Route('/emoji', name: 'app_emoji')] #[Route('/', name: 'emojis')]
public function index(): Response public function index(): Response
{ {
$this->testToMove(); $this->testToMove();
@ -41,7 +41,7 @@ class EmojiController extends AbstractController
return new Response(['count' => $count]); return new Response(['count' => $count]);
} }
#[Route('/addRarity', name: 'addR')] #[Route('/addRarity', name: 'add_rarity')]
public function addRarityDebug(EntityManagerInterface $entityManager) { public function addRarityDebug(EntityManagerInterface $entityManager) {
$rarity = new Rarity(); $rarity = new Rarity();
$rarity->setName('Bip'); $rarity->setName('Bip');
@ -52,7 +52,7 @@ class EmojiController extends AbstractController
return new Response(); return new Response();
} }
#[Route('/add/{code}', name: 'add')] #[Route('/add/{code}', name: 'add_code')]
public function addEmojiDebug(string $code, EntityManagerInterface $entityManager) { public function addEmojiDebug(string $code, EntityManagerInterface $entityManager) {
$emoji = new Emoji(); $emoji = new Emoji();
$emoji->setCode($code); $emoji->setCode($code);

@ -8,7 +8,7 @@ use Symfony\Component\Routing\Annotation\Route;
class HomeController extends AbstractController class HomeController extends AbstractController
{ {
#[Route('/', name: 'home')] #[Route('/', name: 'app_home')]
public function index(): Response public function index(): Response
{ {
$emojisDeBase = [ $emojisDeBase = [

@ -0,0 +1,29 @@
<?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;
class LoginController extends AbstractController
{
#[Route(path: '/login', name: 'app_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
if ($this->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.');
}
}

@ -0,0 +1,48 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\LoginFormAuthenticator;
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;
use Symfony\Component\Security\Http\Authentication\UserAuthenticatorInterface;
class RegistrationController extends AbstractController
{
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher, UserAuthenticatorInterface $userAuthenticator, LoginFormAuthenticator $authenticator, EntityManagerInterface $entityManager): Response
{
$user = new User();
$form = $this->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(),
]);
}
}

@ -0,0 +1,116 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity(repositoryClass: UserRepository::class)]
#[UniqueEntity(fields: ['username'], message: 'There is already an account with this username')]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 180, unique: true)]
private ?string $username = null;
#[ORM\Column]
private array $roles = [];
/**
* @var string The hashed password
*/
#[ORM\Column]
private ?string $password = null;
#[ORM\ManyToMany(targetEntity: Emoji::class, inversedBy: 'users')]
#[ORM\JoinTable(name: 'posseder')]
private Collection $emojis;
public function __construct() {
$this->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;
}
}

@ -0,0 +1,44 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
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('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,
]);
}
}

@ -0,0 +1,67 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordUpgraderInterface;
/**
* @extends ServiceEntityRepository<User>
*
* @implements PasswordUpgraderInterface<User>
*
* @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()
// ;
// }
}

@ -0,0 +1,59 @@
<?php
namespace App\Security;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
public const LOGIN_ROUTE = 'app_login';
public function __construct(private UrlGeneratorInterface $urlGenerator)
{
}
public function authenticate(Request $request): Passport
{
$username = $request->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);
}
}

@ -0,0 +1,43 @@
{% extends 'base.html.twig' %}
{% block title %}Connexion{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
{% endblock %}
{% block body %}
<div class="login-container">
<h1 class="login-title">🔐 Connexion</h1>
{% if error %}
<div class="login-error">Mauvais nom d'utilisateur ou mot de passe.</div>
{% endif %}
{% if app.user %}
<div class="already-logged">
Connecté en tant que {{ app.user.userIdentifier }},
<a href="{{ path('app_logout') }}" class="logout-link">Se déconnecter</a>
</div>
{% endif %}
<form method="post" class="login-form">
<label for="inputUsername">Nom d'utilisateur</label>
<input type="text" id="inputUsername" name="username" value="{{ last_username }}" required autofocus>
<label for="inputPassword">Mot de passe</label>
<input type="password" id="inputPassword" name="password" required>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<button class="btn-login" type="submit">🚪 Se connecter</button>
</form>
</div>
<div class="no-account">
<p>Pas encore de compte ?</p>
<a href="{{ path('app_register') }}" class="btn-register">Créer un compte</a>
</div>
{% endblock %}

@ -0,0 +1,30 @@
{% extends 'base.html.twig' %}
{% block title %}Inscription{% endblock %}
{% block stylesheets %}
{{ parent() }}
<link rel="stylesheet" href="{{ asset('css/login.css') }}">
{% endblock %}
{% block body %}
<div class="register-container">
<h1>📝 Inscription</h1>
{{ form_start(registrationForm) }}
{{ form_errors(registrationForm) }}
{{ form_row(registrationForm.username) }}
{{ form_row(registrationForm.plainPassword, {
label: 'Mot de passe'
}) }}
<button type="submit" class="btn">Créer mon compte</button>
{{ form_end(registrationForm) }}
<div class="no-account">
<p>Déjà inscrit ?</p>
<a href="{{ path('app_login') }}" class="btn-register">Se connecter</a>
</div>
</div>
{% endblock %}

@ -7,6 +7,14 @@
{% endblock %} {% endblock %}
{% block body %} {% block body %}
{% if app.user %}
<div class="logout-container">
Connecté en tant que <strong>{{ app.user.username }}</strong>
<form action="{{ path('app_logout') }}" method="post" style="display: inline;">
<button type="submit" class="btn btn-logout">🚪 Se déconnecter</button>
</form>
</div>
{% endif %}
<h1>🧬 Ma collection de créatures 🐾</h1> <h1>🧬 Ma collection de créatures 🐾</h1>

Loading…
Cancel
Save