Sign-In : use symfony form component (with Bootstrap)

master
Julien Rosset 5 years ago
parent fc23008e57
commit fc96e0edff

@ -0,0 +1,11 @@
form {
text-align: left;
label {
text-align: right;
}
.form-buttons {
text-align: center;
}
}

@ -8,6 +8,7 @@ header {
display: flex;
justify-content: space-between;
z-index: 9999;
nav a {
margin: 0 5px;

@ -5,8 +5,16 @@
body {
@extend %font;
margin: 80px 5px 0 5px; // Vertical translation for the fixed header
margin: 80px 5px 0 5px; // Vertical translation for the fixed header
}
.ui-widget {
@extend %font;
}
pre.xdebug-var-dump {
font-family: sans-serif;
font-size: 1em;
text-align: left;
background-color: rgba(255, 255, 255, 1);
}

@ -5,4 +5,5 @@
@import "../components/messages";
@import "../layouts/page";
@import "../layouts/header";
@import "../layouts/header";
@import "../layouts/form";

@ -3,9 +3,10 @@ body {
}
#sign-in {
display: inline-block;
width: 50%;
border: 1px solid rgba(0, 0, 0, .25);
padding: 10px;
margin: 0 auto;
}
#presentation {

@ -2,39 +2,42 @@
"type": "project",
"license": "proprietary",
"require": {
"php": "^7.4.2",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-intl": "^7.4",
"ext-sodium": "^7.4",
"doctrine/doctrine-bundle": "*",
"php": "^7.4.2",
"ext-ctype": "*",
"ext-iconv": "*",
"ext-intl": "^7.4",
"ext-sodium": "^7.4",
"doctrine/annotations": "^1.10",
"doctrine/doctrine-bundle": "*",
"doctrine/doctrine-migrations-bundle": "2.*.*",
"doctrine/orm": "*",
"sensio/framework-extra-bundle": "^5.5",
"symfony/apache-pack": "^1.0",
"symfony/asset": "5.*.*",
"symfony/console": "5.*.*",
"symfony/dotenv": "5.*.*",
"symfony/expression-language": "5.*.*",
"symfony/flex": "^1.3.1",
"symfony/form": "5.*.*",
"symfony/framework-bundle": "5.*.*",
"symfony/http-client": "5.*.*",
"symfony/intl": "5.*.*",
"symfony/mailer": "5.*.*",
"symfony/monolog-bundle": "^3.1",
"symfony/notifier": "5.*.*",
"symfony/process": "5.*.*",
"symfony/profiler-pack": "^1.0",
"symfony/security-bundle": "5.*.*",
"symfony/serializer-pack": "*",
"symfony/string": "5.*.*",
"symfony/translation": "5.*.*",
"symfony/twig-pack": "^1.0",
"symfony/validator": "5.*.*",
"symfony/web-link": "5.*.*",
"symfony/webpack-encore-bundle": "^1.7",
"symfony/yaml": "5.*.*"
"doctrine/orm": "*",
"sensio/framework-extra-bundle": "^5.5",
"symfony/apache-pack": "^1.0",
"symfony/asset": "5.*.*",
"symfony/console": "5.*.*",
"symfony/dotenv": "5.*.*",
"symfony/expression-language": "5.*.*",
"symfony/flex": "^1.3.1",
"symfony/form": "5.*.*",
"symfony/framework-bundle": "5.*.*",
"symfony/http-client": "5.*.*",
"symfony/intl": "5.*.*",
"symfony/mailer": "5.*.*",
"symfony/monolog-bundle": "^3.1",
"symfony/notifier": "5.*.*",
"symfony/process": "5.*.*",
"symfony/profiler-pack": "^1.0",
"symfony/property-info": "5.*.*",
"symfony/security-bundle": "5.*.*",
"symfony/security-csrf": "5.*.*",
"symfony/serializer-pack": "*",
"symfony/string": "5.*.*",
"symfony/translation": "5.*.*",
"symfony/twig-pack": "^1.0",
"symfony/validator": "5.*.*",
"symfony/web-link": "5.*.*",
"symfony/webpack-encore-bundle": "^1.7",
"symfony/yaml": "5.*.*"
},
"require-dev": {
"symfony/debug-pack": "*",

92
composer.lock generated

@ -1,20 +1,20 @@
{
"_readme": [
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "1988f77310fab5e902b0c0cf2be42503",
"packages": [
"content-hash": "b95652040ba10ba1825a39e3277a3516",
"packages": [
{
"name": "doctrine/annotations",
"name": "doctrine/annotations",
"version": "1.10.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d"
},
"dist": {
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/5db60a4969eba0e0c197a19c077780aadbc43c5d",
"reference": "5db60a4969eba0e0c197a19c077780aadbc43c5d",
@ -6372,24 +6372,24 @@
"time": "2020-05-20T17:43:50+00:00"
},
{
"name": "twig/extra-bundle",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "6eaf1637abe6b68518e7e0949ebb84e55770d5c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/6eaf1637abe6b68518e7e0949ebb84e55770d5c6",
"reference": "6eaf1637abe6b68518e7e0949ebb84e55770d5c6",
"shasum": ""
},
"require": {
"php": "^7.1.3",
"name": "twig/extra-bundle",
"version": "v3.0.4",
"source": {
"type": "git",
"url": "https://github.com/twigphp/twig-extra-bundle.git",
"reference": "a7c5799cf742ab0827f5d32df37528ee8bf5a233"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/twig-extra-bundle/zipball/a7c5799cf742ab0827f5d32df37528ee8bf5a233",
"reference": "a7c5799cf742ab0827f5d32df37528ee8bf5a233",
"shasum": ""
},
"require": {
"php": "^7.1.3|^8.0",
"symfony/framework-bundle": "^4.3|^5.0",
"symfony/twig-bundle": "^4.3|^5.0",
"twig/twig": "^2.4|^3.0"
"symfony/twig-bundle": "^4.3|^5.0",
"twig/twig": "^2.4|^3.0"
},
"require-dev": {
"twig/cssinliner-extra": "^2.12|^3.0",
@ -6428,32 +6428,32 @@
"extra",
"twig"
],
"time": "2020-01-01T17:11:09+00:00"
"time": "2020-05-21T09:56:39+00:00"
},
{
"name": "twig/twig",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/3b88ccd180a6b61ebb517aea3b1a8906762a1dc2",
"reference": "3b88ccd180a6b61ebb517aea3b1a8906762a1dc2",
"shasum": ""
},
"require": {
"php": "^7.2.5",
"symfony/polyfill-ctype": "^1.8",
"name": "twig/twig",
"version": "v3.0.4",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "582bdbdc173027ebfba3c93dc750a40b8f9ebc02"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/582bdbdc173027ebfba3c93dc750a40b8f9ebc02",
"reference": "582bdbdc173027ebfba3c93dc750a40b8f9ebc02",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4|^5.0"
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
},
"type": "library",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
@ -6490,7 +6490,7 @@
"keywords": [
"templating"
],
"time": "2020-02-11T15:33:47+00:00"
"time": "2020-07-05T13:18:14+00:00"
},
{
"name": "webimpress/safe-writer",

@ -15,3 +15,9 @@ framework:
#fragments: true
php_errors:
log: true
csrf_protection: true
validation:
enabled: true
enable_annotations: true

@ -0,0 +1,8 @@
# As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists
#monolog:
# channels: [deprecation]
# handlers:
# deprecation:
# type: stream
# channels: [deprecation]
# path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"

@ -1,7 +1,12 @@
security:
# https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
encoders:
App\Entity\User:
algorithm: auto
providers:
users_in_memory: { memory: null }
app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
@ -9,16 +14,18 @@ security:
main:
anonymous: true
lazy: true
provider: users_in_memory
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#firewalls-authentication
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used
remember_me:
secret: '%kernel.secret%'
lifetime: 604800 # 1 week
path: /
secure: true
guard:
authenticators:
- App\Security\LoginFormAuthenticator
logout:
path: app_security_logout
target: app_site_index
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
# - { path: ^/profile, roles: ROLE_USER }
- { path: ^/sign-in$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
role_hierarchy:
ROLE_ADMIN: ROLE_USER

@ -1,2 +0,0 @@
#webpack_encore:
# strict_mode: false

@ -1,2 +1,3 @@
twig:
default_path: '%kernel.project_dir%/templates'
form_themes: ['bootstrap_4_horizontal_layout.html.twig']

@ -1,3 +1,8 @@
#index:
# path: /
# controller: App\Controller\DefaultController::index
# Redirige l'URL racine vers celle de la langue par défaut
index:
path: /
controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
defaults:
route: 'app_site_index'
_locale: '%kernel.default_locale%'
permanent: true

@ -1,6 +1,11 @@
controllers:
resource: ../../src/Controller/
type: annotation
prefix: /{_locale}
requirements:
_locale: '%app.supported_locales%'
defaults:
_locale: '%kernel.default_locale%'
kernel:
resource: ../../src/Kernel.php

@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
app.supported_locales: 'fr|en'
services:
# default configuration for services in *this* file

@ -2,6 +2,7 @@
namespace App\Controller;
use App\Form\SecuritySignInForm;
use Exception;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
@ -18,7 +19,7 @@ class SecurityController extends AbstractController {
/**
* Show login form
*
* @param TranslatorInterface $translator Interface for translation
* @param TranslatorInterface $translator Interface for translation
* @param AuthenticationUtils $authenticationUtils Helper for authentication information
*
* @return Response Page response
@ -26,15 +27,23 @@ class SecurityController extends AbstractController {
* @Route("/sign-in")
*/
public function sign_in (TranslatorInterface $translator, AuthenticationUtils $authenticationUtils): Response {
$error = $authenticationUtils->getLastAuthenticationError();
if ($error !== null) {
$this->addFlash('error', $translator->trans($error->getMessageKey(), $error->getMessageData(), 'security'));
}
$form = $this->createForm(
SecuritySignInForm::class,
[
'email' => $authenticationUtils->getLastUsername(),
],
[
// 'attr' => [
// 'novalidate' => 'novalidate',
// ],
]
);
return $this->render(
'security/sign-in.html.twig',
[
'username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError(),
'form' => $form->createView(),
]
);
}

@ -8,6 +8,8 @@ use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* User entity
*
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
*/
class User implements UserInterface {

@ -0,0 +1,21 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
abstract class AbstractBootstrapForm extends AbstractType {
public function configureOptions (OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(
[
'attr' => [
'class' => 'form-validation',
'novalidate' => 'novalidate',
],
]
);
}
}

@ -0,0 +1,50 @@
<?php
namespace App\Form;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
/**
* Security : sign-in form
*
* @package App\Form
*/
class SecuritySignInForm extends AbstractBootstrapForm {
public function buildForm (FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
$builder
->add(
'email',
EmailType::class,
[
'label_format' => 'user.email',
]
)
->add(
'password',
PasswordType::class,
[
'label_format' => 'user.password',
]
)
->add(
'_remember_me',
CheckboxType::class,
[
'label_format' => 'sign.remember_me',
'required' => false,
]
);
}
public function configureOptions (OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefault('translation_domain', 'security');
}
}

@ -3,19 +3,18 @@
namespace App\Security;
use App\Entity\User;
use App\Form\SecuritySignInForm;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\FormFactoryInterface;
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;
@ -25,19 +24,19 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements P
private EntityManagerInterface $entityManager;
private UrlGeneratorInterface $urlGenerator;
private CsrfTokenManagerInterface $csrfTokenManager;
private UserPasswordEncoderInterface $passwordEncoder;
private FormFactoryInterface $formFactory;
public function __construct (
EntityManagerInterface $entityManager,
UrlGeneratorInterface $urlGenerator,
CsrfTokenManagerInterface $csrfTokenManager,
UserPasswordEncoderInterface $passwordEncoder
UserPasswordEncoderInterface $passwordEncoder,
FormFactoryInterface $formFactory
) {
$this->entityManager = $entityManager;
$this->urlGenerator = $urlGenerator;
$this->csrfTokenManager = $csrfTokenManager;
$this->passwordEncoder = $passwordEncoder;
$this->formFactory = $formFactory;
}
public function supports (Request $request) {
@ -45,11 +44,17 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements P
}
public function getCredentials (Request $request) {
$credentials = [
'email' => $request->request->get('email'),
'password' => $request->request->get('password'),
'csrf_token' => $request->request->get('_csrf_token'),
];
if (!$request->isMethod('POST') || $request->attributes->get('_route') != 'app_security_sign_in') {
return null;
}
$form = $this->formFactory->create(SecuritySignInForm::class);
$form->handleRequest($request);
if (!$form->isSubmitted() || !$form->isValid()) {
return null;
}
$credentials = $form->getData();
$request->getSession()->set(
Security::LAST_USERNAME,
$credentials['email']
@ -59,15 +64,8 @@ class LoginFormAuthenticator extends AbstractFormLoginAuthenticator implements P
}
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.');
}

@ -11,31 +11,23 @@
{% block body %}
<section id="sign-in">
<h1>{{ 'sign.in'|trans }}</h1>
<form method="POST" class="form-validation" novalidate>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}">
<div class="form-group">
<label for="email" class="sr-only">{{ 'user.email'|trans }}</label>
<input type="email" value="{{ username }}" name="email" id="email" class="form-control" placeholder="{{ 'user.email'|trans }}" required autofocus>
<div class="invalid-feedback">{{ 'user.invalid.email'|trans }}</div>
{% if error %}
<div class="alert alert-danger" role="alert">
<span class="sr-only">{{ 'error'|trans({}, 'messages') }}</span>
{{ error.messageKey|trans(error.messageData) }}
</div>
<div class="form-group">
<label for="password" class="sr-only">{{ 'user.password'|trans }}</label>
<input type="password" name="password" id="password" class="form-control" placeholder="{{ 'user.password'|trans }}" required>
<div class="invalid-feedback">{{ 'user.invalid.password'|trans }}</div>
</div>
<div class="form-group">
<input type="checkbox" name="_remember_me" id="remember_me">
<label for="remember_me">{{ 'sign.remember_me'|trans }}</label>
</div>
<div class="form-group">
<button class="btn btn-lg btn-primary" type="submit">{{ 'sign.in'|trans }}</button>
<a class="btn btn-lg btn-secondary" href="#">{{ 'sign.up'|trans }}</a>
</div>
</form>
{% endif %}
{{ form_start(form) }}
{{ form_row(form.email) }}
{{ form_row(form.password) }}
{{ form_row(form._remember_me) }}
<div class="form-group form-buttons">
<button class="btn btn-lg btn-primary" type="submit">{{ 'sign.in'|trans }}</button>
<a class="btn btn-lg btn-secondary" href="#">{{ 'sign.up'|trans }}</a>
</div>
{{ form_end(form) }}
</section>
<section id="presentation">

@ -0,0 +1,12 @@
<?php
use Symfony\Component\Dotenv\Dotenv;
require dirname(__DIR__) . '/vendor/autoload.php';
if (file_exists(dirname(__DIR__) . '/config/bootstrap.php')) {
require dirname(__DIR__) . '/config/bootstrap.php';
}
elseif (method_exists(Dotenv::class, 'bootEnv')) {
(new Dotenv())->bootEnv(dirname(__DIR__) . '/.env');
}
Loading…
Cancel
Save