diff --git a/assets/css/layouts/form.scss b/assets/css/layouts/form.scss new file mode 100644 index 0000000..4aa1a06 --- /dev/null +++ b/assets/css/layouts/form.scss @@ -0,0 +1,11 @@ +form { + text-align: left; + + label { + text-align: right; + } + + .form-buttons { + text-align: center; + } +} \ No newline at end of file diff --git a/assets/css/layouts/header.scss b/assets/css/layouts/header.scss index a1191ac..e14c4a5 100644 --- a/assets/css/layouts/header.scss +++ b/assets/css/layouts/header.scss @@ -8,6 +8,7 @@ header { display: flex; justify-content: space-between; + z-index: 9999; nav a { margin: 0 5px; diff --git a/assets/css/layouts/page.scss b/assets/css/layouts/page.scss index 8459bbf..b7681f9 100644 --- a/assets/css/layouts/page.scss +++ b/assets/css/layouts/page.scss @@ -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); } \ No newline at end of file diff --git a/assets/css/pages/main.scss b/assets/css/pages/main.scss index 61d8154..88f0d78 100644 --- a/assets/css/pages/main.scss +++ b/assets/css/pages/main.scss @@ -5,4 +5,5 @@ @import "../components/messages"; @import "../layouts/page"; -@import "../layouts/header"; \ No newline at end of file +@import "../layouts/header"; +@import "../layouts/form"; \ No newline at end of file diff --git a/assets/css/pages/security/sign-in.scss b/assets/css/pages/security/sign-in.scss index d21eb39..c985356 100644 --- a/assets/css/pages/security/sign-in.scss +++ b/assets/css/pages/security/sign-in.scss @@ -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 { diff --git a/composer.json b/composer.json index 77fb94a..eade95d 100644 --- a/composer.json +++ b/composer.json @@ -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": "*", diff --git a/composer.lock b/composer.lock index 2936a0c..702b694 100644 --- a/composer.lock +++ b/composer.lock @@ -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", diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index cad7f78..eafef74 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -15,3 +15,9 @@ framework: #fragments: true php_errors: log: true + + csrf_protection: true + + validation: + enabled: true + enable_annotations: true \ No newline at end of file diff --git a/config/packages/prod/deprecations.yaml b/config/packages/prod/deprecations.yaml new file mode 100644 index 0000000..920a061 --- /dev/null +++ b/config/packages/prod/deprecations.yaml @@ -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" diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0e4cf3d..cf791d8 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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 diff --git a/config/packages/test/webpack_encore.yaml b/config/packages/test/webpack_encore.yaml deleted file mode 100644 index 02a7651..0000000 --- a/config/packages/test/webpack_encore.yaml +++ /dev/null @@ -1,2 +0,0 @@ -#webpack_encore: -# strict_mode: false diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index b3cdf30..7030ebe 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,2 +1,3 @@ twig: default_path: '%kernel.project_dir%/templates' + form_themes: ['bootstrap_4_horizontal_layout.html.twig'] \ No newline at end of file diff --git a/config/routes.yaml b/config/routes.yaml index c3283aa..559eb0a 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -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 \ No newline at end of file diff --git a/config/routes/annotations.yaml b/config/routes/annotations.yaml index e92efc5..eb3fbaf 100644 --- a/config/routes/annotations.yaml +++ b/config/routes/annotations.yaml @@ -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 diff --git a/config/services.yaml b/config/services.yaml index 5c4b417..611d2ff 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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 diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 449a3ae..d1366bd 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -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(), ] ); } diff --git a/src/Entity/User.php b/src/Entity/User.php index 37fd3c0..1a3e5e3 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -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 { diff --git a/src/Form/AbstractBootstrapForm.php b/src/Form/AbstractBootstrapForm.php new file mode 100644 index 0000000..a9f1db9 --- /dev/null +++ b/src/Form/AbstractBootstrapForm.php @@ -0,0 +1,21 @@ +setDefaults( + [ + 'attr' => [ + 'class' => 'form-validation', + 'novalidate' => 'novalidate', + ], + ] + ); + } +} \ No newline at end of file diff --git a/src/Form/SecuritySignInForm.php b/src/Form/SecuritySignInForm.php new file mode 100644 index 0000000..5c74c62 --- /dev/null +++ b/src/Form/SecuritySignInForm.php @@ -0,0 +1,50 @@ +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'); + } +} \ No newline at end of file diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php index e878f3c..4d15c80 100644 --- a/src/Security/LoginFormAuthenticator.php +++ b/src/Security/LoginFormAuthenticator.php @@ -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.'); } diff --git a/templates/security/sign-in.html.twig b/templates/security/sign-in.html.twig index 611c38b..50abb83 100644 --- a/templates/security/sign-in.html.twig +++ b/templates/security/sign-in.html.twig @@ -11,31 +11,23 @@ {% block body %}

{{ 'sign.in'|trans }}

-
- -
- - -
{{ 'user.invalid.email'|trans }}
+ {% if error %} + - -
- - -
{{ 'user.invalid.password'|trans }}
-
- -
- - -
- -
- - {{ 'sign.up'|trans }} -
- + {% endif %} + + {{ form_start(form) }} + {{ form_row(form.email) }} + {{ form_row(form.password) }} + {{ form_row(form._remember_me) }} +
+ + {{ 'sign.up'|trans }} +
+ {{ form_end(form) }}
diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3a529f9 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,12 @@ +bootEnv(dirname(__DIR__) . '/.env'); +}