11 changed files with 560 additions and 4 deletions
@ -0,0 +1,31 @@ |
|||||
|
<?php |
||||
|
|
||||
|
declare(strict_types=1); |
||||
|
|
||||
|
namespace DoctrineMigrations; |
||||
|
|
||||
|
use Doctrine\DBAL\Schema\Schema; |
||||
|
use Doctrine\Migrations\AbstractMigration; |
||||
|
|
||||
|
/** |
||||
|
* Auto-generated Migration: Please modify to your needs! |
||||
|
*/ |
||||
|
final class Version20210416121426 extends AbstractMigration |
||||
|
{ |
||||
|
public function getDescription() : string |
||||
|
{ |
||||
|
return ''; |
||||
|
} |
||||
|
|
||||
|
public function up(Schema $schema) : void |
||||
|
{ |
||||
|
// this up() migration is auto-generated, please modify it to your needs |
||||
|
$this->addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, roles LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\', password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); |
||||
|
} |
||||
|
|
||||
|
public function down(Schema $schema) : void |
||||
|
{ |
||||
|
// this down() migration is auto-generated, please modify it to your needs |
||||
|
$this->addSql('DROP TABLE user'); |
||||
|
} |
||||
|
} |
@ -0,0 +1,52 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Controller; |
||||
|
|
||||
|
use App\Entity\User; |
||||
|
use App\Form\RegistrationFormType; |
||||
|
use App\Security\PokedexAuthenticator; |
||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; |
||||
|
use Symfony\Component\HttpFoundation\Request; |
||||
|
use Symfony\Component\HttpFoundation\Response; |
||||
|
use Symfony\Component\Routing\Annotation\Route; |
||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; |
||||
|
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; |
||||
|
|
||||
|
class RegistrationController extends AbstractController |
||||
|
{ |
||||
|
/** |
||||
|
* @Route("/register", name="app_register") |
||||
|
*/ |
||||
|
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder, GuardAuthenticatorHandler $guardHandler, PokedexAuthenticator $authenticator): Response |
||||
|
{ |
||||
|
$user = new User(); |
||||
|
$form = $this->createForm(RegistrationFormType::class, $user); |
||||
|
$form->handleRequest($request); |
||||
|
|
||||
|
if ($form->isSubmitted() && $form->isValid()) { |
||||
|
// encode the plain password |
||||
|
$user->setPassword( |
||||
|
$passwordEncoder->encodePassword( |
||||
|
$user, |
||||
|
$form->get('plainPassword')->getData() |
||||
|
) |
||||
|
); |
||||
|
|
||||
|
$entityManager = $this->getDoctrine()->getManager(); |
||||
|
$entityManager->persist($user); |
||||
|
$entityManager->flush(); |
||||
|
// do anything else you need here, like send an email |
||||
|
|
||||
|
return $guardHandler->authenticateUserAndHandleSuccess( |
||||
|
$user, |
||||
|
$request, |
||||
|
$authenticator, |
||||
|
'main' // firewall name in security.yaml |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return $this->render('registration/register.html.twig', [ |
||||
|
'registrationForm' => $form->createView(), |
||||
|
]); |
||||
|
} |
||||
|
} |
@ -0,0 +1,36 @@ |
|||||
|
<?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 SecurityController extends AbstractController |
||||
|
{ |
||||
|
/** |
||||
|
* @Route("/login", name="app_login") |
||||
|
*/ |
||||
|
public function login(AuthenticationUtils $authenticationUtils): Response |
||||
|
{ |
||||
|
if ($this->getUser()) { |
||||
|
return $this->redirectToRoute('pokemon_index'); |
||||
|
} |
||||
|
|
||||
|
// get the login error if there is one |
||||
|
$error = $authenticationUtils->getLastAuthenticationError(); |
||||
|
// last username entered by the user |
||||
|
$lastUsername = $authenticationUtils->getLastUsername(); |
||||
|
|
||||
|
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @Route("/logout", name="app_logout") |
||||
|
*/ |
||||
|
public function logout() |
||||
|
{ |
||||
|
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); |
||||
|
} |
||||
|
} |
@ -0,0 +1,119 @@ |
|||||
|
<?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\UserInterface; |
||||
|
|
||||
|
/** |
||||
|
* @ORM\Entity(repositoryClass=UserRepository::class) |
||||
|
* @UniqueEntity(fields={"email"}, message="There is already an account with this email") |
||||
|
*/ |
||||
|
class User implements UserInterface |
||||
|
{ |
||||
|
/** |
||||
|
* @ORM\Id |
||||
|
* @ORM\GeneratedValue |
||||
|
* @ORM\Column(type="integer") |
||||
|
*/ |
||||
|
private $id; |
||||
|
|
||||
|
/** |
||||
|
* @ORM\Column(type="string", length=180, unique=true) |
||||
|
*/ |
||||
|
private $email; |
||||
|
|
||||
|
/** |
||||
|
* @ORM\Column(type="json") |
||||
|
*/ |
||||
|
private $roles = []; |
||||
|
|
||||
|
/** |
||||
|
* @var string The hashed password |
||||
|
* @ORM\Column(type="string") |
||||
|
*/ |
||||
|
private $password; |
||||
|
|
||||
|
public function getId(): ?int |
||||
|
{ |
||||
|
return $this->id; |
||||
|
} |
||||
|
|
||||
|
public function getEmail(): ?string |
||||
|
{ |
||||
|
return $this->email; |
||||
|
} |
||||
|
|
||||
|
public function setEmail(string $email): self |
||||
|
{ |
||||
|
$this->email = $email; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* A visual identifier that represents this user. |
||||
|
* |
||||
|
* @see UserInterface |
||||
|
*/ |
||||
|
public function getUsername(): string |
||||
|
{ |
||||
|
return (string) $this->email; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @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): self |
||||
|
{ |
||||
|
$this->roles = $roles; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see UserInterface |
||||
|
*/ |
||||
|
public function getPassword(): string |
||||
|
{ |
||||
|
return (string) $this->password; |
||||
|
} |
||||
|
|
||||
|
public function setPassword(string $password): self |
||||
|
{ |
||||
|
$this->password = $password; |
||||
|
|
||||
|
return $this; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returning a salt is only needed, if you are not using a modern |
||||
|
* hashing algorithm (e.g. bcrypt or sodium) in your security.yaml. |
||||
|
* |
||||
|
* @see UserInterface |
||||
|
*/ |
||||
|
public function getSalt(): ?string |
||||
|
{ |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* @see UserInterface |
||||
|
*/ |
||||
|
public function eraseCredentials() |
||||
|
{ |
||||
|
// If you store any temporary, sensitive data on the user, clear it here |
||||
|
// $this->plainPassword = null; |
||||
|
} |
||||
|
} |
@ -0,0 +1,54 @@ |
|||||
|
<?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) |
||||
|
{ |
||||
|
$builder |
||||
|
->add('email') |
||||
|
->add('agreeTerms', CheckboxType::class, [ |
||||
|
'mapped' => false, |
||||
|
'constraints' => [ |
||||
|
new IsTrue([ |
||||
|
'message' => 'You should agree to our terms.', |
||||
|
]), |
||||
|
], |
||||
|
]) |
||||
|
->add('plainPassword', PasswordType::class, [ |
||||
|
// instead of being set onto the object directly, |
||||
|
// this is read and encoded in the controller |
||||
|
'mapped' => false, |
||||
|
'constraints' => [ |
||||
|
new NotBlank([ |
||||
|
'message' => 'Please enter a password', |
||||
|
]), |
||||
|
new Length([ |
||||
|
'min' => 6, |
||||
|
'minMessage' => 'Your password should be at least {{ limit }} characters', |
||||
|
// max length allowed by Symfony for security reasons |
||||
|
'max' => 4096, |
||||
|
]), |
||||
|
], |
||||
|
]) |
||||
|
; |
||||
|
} |
||||
|
|
||||
|
public function configureOptions(OptionsResolver $resolver) |
||||
|
{ |
||||
|
$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\PasswordUpgraderInterface; |
||||
|
use Symfony\Component\Security\Core\User\UserInterface; |
||||
|
|
||||
|
/** |
||||
|
* @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(UserInterface $user, string $newEncodedPassword): void |
||||
|
{ |
||||
|
if (!$user instanceof User) { |
||||
|
throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); |
||||
|
} |
||||
|
|
||||
|
$user->setPassword($newEncodedPassword); |
||||
|
$this->_em->persist($user); |
||||
|
$this->_em->flush(); |
||||
|
} |
||||
|
|
||||
|
// /** |
||||
|
// * @return User[] Returns an array of User objects |
||||
|
// */ |
||||
|
/* |
||||
|
public function findByExampleField($value) |
||||
|
{ |
||||
|
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,110 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Security; |
||||
|
|
||||
|
use App\Entity\User; |
||||
|
use Doctrine\ORM\EntityManagerInterface; |
||||
|
use Symfony\Component\HttpFoundation\RedirectResponse; |
||||
|
use Symfony\Component\HttpFoundation\Request; |
||||
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; |
||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; |
||||
|
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface; |
||||
|
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; |
||||
|
use Symfony\Component\Security\Core\Exception\InvalidCsrfTokenException; |
||||
|
use Symfony\Component\Security\Core\Security; |
||||
|
use Symfony\Component\Security\Core\User\UserInterface; |
||||
|
use Symfony\Component\Security\Core\User\UserProviderInterface; |
||||
|
use Symfony\Component\Security\Csrf\CsrfToken; |
||||
|
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; |
||||
|
use Symfony\Component\Security\Guard\Authenticator\AbstractFormLoginAuthenticator; |
||||
|
use Symfony\Component\Security\Guard\PasswordAuthenticatedInterface; |
||||
|
use Symfony\Component\Security\Http\Util\TargetPathTrait; |
||||
|
|
||||
|
class PokedexAuthenticator extends AbstractFormLoginAuthenticator implements PasswordAuthenticatedInterface |
||||
|
{ |
||||
|
use TargetPathTrait; |
||||
|
|
||||
|
public const LOGIN_ROUTE = 'app_login'; |
||||
|
|
||||
|
private $entityManager; |
||||
|
private $urlGenerator; |
||||
|
private $csrfTokenManager; |
||||
|
private $passwordEncoder; |
||||
|
|
||||
|
public function __construct(EntityManagerInterface $entityManager, UrlGeneratorInterface $urlGenerator, CsrfTokenManagerInterface $csrfTokenManager, UserPasswordEncoderInterface $passwordEncoder) |
||||
|
{ |
||||
|
$this->entityManager = $entityManager; |
||||
|
$this->urlGenerator = $urlGenerator; |
||||
|
$this->csrfTokenManager = $csrfTokenManager; |
||||
|
$this->passwordEncoder = $passwordEncoder; |
||||
|
} |
||||
|
|
||||
|
public function supports(Request $request) |
||||
|
{ |
||||
|
return self::LOGIN_ROUTE === $request->attributes->get('_route') |
||||
|
&& $request->isMethod('POST'); |
||||
|
} |
||||
|
|
||||
|
public function getCredentials(Request $request) |
||||
|
{ |
||||
|
$credentials = [ |
||||
|
'email' => $request->request->get('email'), |
||||
|
'password' => $request->request->get('password'), |
||||
|
'csrf_token' => $request->request->get('_csrf_token'), |
||||
|
]; |
||||
|
$request->getSession()->set( |
||||
|
Security::LAST_USERNAME, |
||||
|
$credentials['email'] |
||||
|
); |
||||
|
|
||||
|
return $credentials; |
||||
|
} |
||||
|
|
||||
|
public function getUser($credentials, UserProviderInterface $userProvider) |
||||
|
{ |
||||
|
$token = new CsrfToken('authenticate', $credentials['csrf_token']); |
||||
|
if (!$this->csrfTokenManager->isTokenValid($token)) { |
||||
|
throw new InvalidCsrfTokenException(); |
||||
|
} |
||||
|
|
||||
|
$user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); |
||||
|
|
||||
|
if (!$user) { |
||||
|
// fail authentication with a custom error |
||||
|
throw new CustomUserMessageAuthenticationException('Email could not be found.'); |
||||
|
} |
||||
|
|
||||
|
return $user; |
||||
|
} |
||||
|
|
||||
|
public function checkCredentials($credentials, UserInterface $user) |
||||
|
{ |
||||
|
return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Used to upgrade (rehash) the user's password automatically over time. |
||||
|
*/ |
||||
|
public function getPassword($credentials): ?string |
||||
|
{ |
||||
|
return $credentials['password']; |
||||
|
} |
||||
|
|
||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey) |
||||
|
{ |
||||
|
if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { |
||||
|
return new RedirectResponse($targetPath); |
||||
|
} |
||||
|
|
||||
|
// For example : return new RedirectResponse($this->urlGenerator->generate('some_route')); |
||||
|
// throw new \Exception('provide a valid redirect inside '.__FILE__); |
||||
|
|
||||
|
return new RedirectResponse($this->urlGenerator->generate('pokemon_index')); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
protected function getLoginUrl() |
||||
|
{ |
||||
|
return $this->urlGenerator->generate(self::LOGIN_ROUTE); |
||||
|
} |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
{% extends 'base.html.twig' %} |
||||
|
|
||||
|
{% block title %}Register{% endblock %} |
||||
|
|
||||
|
{% block body %} |
||||
|
{% for flashError in app.flashes('verify_email_error') %} |
||||
|
<div class="alert alert-danger" role="alert">{{ flashError }}</div> |
||||
|
{% endfor %} |
||||
|
|
||||
|
<h1>Register</h1> |
||||
|
|
||||
|
{{ form_start(registrationForm) }} |
||||
|
{{ form_row(registrationForm.email) }} |
||||
|
{{ form_row(registrationForm.plainPassword, { |
||||
|
label: 'Password' |
||||
|
}) }} |
||||
|
{{ form_row(registrationForm.agreeTerms) }} |
||||
|
|
||||
|
<button type="submit" class="btn btn-primary">Register</button> |
||||
|
{{ form_end(registrationForm) }} |
||||
|
{% endblock %} |
@ -0,0 +1,46 @@ |
|||||
|
{% extends 'base.html.twig' %} |
||||
|
|
||||
|
{% block title %}Log in!{% endblock %} |
||||
|
|
||||
|
{% block body %} |
||||
|
<form method="post"> |
||||
|
{% if error %} |
||||
|
<div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div> |
||||
|
{% endif %} |
||||
|
|
||||
|
{% if app.user %} |
||||
|
<div class="mb-3"> |
||||
|
You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a> |
||||
|
</div> |
||||
|
{% endif %} |
||||
|
|
||||
|
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1> |
||||
|
<label for="inputEmail">Email</label> |
||||
|
<input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus> |
||||
|
<label for="inputPassword">Password</label> |
||||
|
<input type="password" name="password" id="inputPassword" class="form-control" required> |
||||
|
|
||||
|
<input type="hidden" name="_csrf_token" |
||||
|
value="{{ csrf_token('authenticate') }}" |
||||
|
> |
||||
|
|
||||
|
{# |
||||
|
Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. |
||||
|
See https://symfony.com/doc/current/security/remember_me.html |
||||
|
|
||||
|
<div class="checkbox mb-3"> |
||||
|
<label> |
||||
|
<input type="checkbox" name="_remember_me"> Remember me |
||||
|
</label> |
||||
|
</div> |
||||
|
#} |
||||
|
|
||||
|
<button class="btn btn-lg btn-primary" type="submit"> |
||||
|
Sign in |
||||
|
</button> |
||||
|
</form> |
||||
|
|
||||
|
<button class="btn btn-lg btn-secondary" type="submit"> |
||||
|
<a href={{ path('app_register') }}>Register</a> |
||||
|
</button> |
||||
|
{% endblock %} |
Loading…
Reference in new issue