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