diff --git a/.env b/.env index 1c73616..fe088ac 100644 --- a/.env +++ b/.env @@ -39,5 +39,5 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### # MAILER_DSN=null://null MAILER_EMAIL=jul.rosset@gmail.com -MAILER_NAME=WebEDM Mail Bot +MAILER_NAME="WebEDM Mail Bot" ###< symfony/mailer ### diff --git a/composer.json b/composer.json index acaf860..3df75e6 100644 --- a/composer.json +++ b/composer.json @@ -4,48 +4,49 @@ "minimum-stability": "stable", "prefer-stable": true, "require": { - "php": ">=8.1", - "ext-ctype": "*", - "ext-iconv": "*", - "doctrine/annotations": "^2.0", - "doctrine/doctrine-bundle": "^2.9", + "php": ">=8.1", + "ext-ctype": "*", + "ext-iconv": "*", + "doctrine/annotations": "^2.0", + "doctrine/doctrine-bundle": "^2.9", "doctrine/doctrine-migrations-bundle": "^3.2", - "doctrine/orm": "^2.15", + "doctrine/orm": "^2.15", "phpdocumentor/reflection-docblock": "^5.3", - "phpstan/phpdoc-parser": "^1.20", - "stof/doctrine-extensions-bundle": "^1.7", - "symfony/asset": "6.4.*", - "symfony/asset-mapper": "6.4.*", - "symfony/console": "6.4.*", - "symfony/doctrine-messenger": "6.4.*", - "symfony/dotenv": "6.4.*", + "phpstan/phpdoc-parser": "^1.20", + "stof/doctrine-extensions-bundle": "^1.7", + "symfony/asset": "6.4.*", + "symfony/asset-mapper": "6.4.*", + "symfony/console": "6.4.*", + "symfony/doctrine-messenger": "6.4.*", + "symfony/dotenv": "6.4.*", "symfony/expression-language": "6.4.*", - "symfony/flex": "^2", - "symfony/form": "6.4.*", - "symfony/framework-bundle": "6.4.*", - "symfony/http-client": "6.4.*", - "symfony/intl": "6.4.*", - "symfony/mailer": "6.4.*", - "symfony/mime": "6.4.*", - "symfony/monolog-bundle": "^3.0", - "symfony/notifier": "6.4.*", - "symfony/process": "6.4.*", - "symfony/property-access": "6.4.*", - "symfony/property-info": "6.4.*", - "symfony/runtime": "6.4.*", - "symfony/security-bundle": "6.4.*", - "symfony/serializer": "6.4.*", - "symfony/string": "6.4.*", - "symfony/translation": "6.4.*", - "symfony/twig-bundle": "6.4.*", - "symfony/validator": "6.4.*", - "symfony/web-link": "6.4.*", - "symfony/yaml": "6.4.*", - "symfonycasts/sass-bundle": "^0.6.0", + "symfony/flex": "^2", + "symfony/form": "6.4.*", + "symfony/framework-bundle": "6.4.*", + "symfony/http-client": "6.4.*", + "symfony/intl": "6.4.*", + "symfony/mailer": "6.4.*", + "symfony/mime": "6.4.*", + "symfony/monolog-bundle": "^3.0", + "symfony/notifier": "6.4.*", + "symfony/process": "6.4.*", + "symfony/property-access": "6.4.*", + "symfony/property-info": "6.4.*", + "symfony/rate-limiter": "6.4.*", + "symfony/runtime": "6.4.*", + "symfony/security-bundle": "6.4.*", + "symfony/serializer": "6.4.*", + "symfony/string": "6.4.*", + "symfony/translation": "6.4.*", + "symfony/twig-bundle": "6.4.*", + "symfony/validator": "6.4.*", + "symfony/web-link": "6.4.*", + "symfony/yaml": "6.4.*", + "symfonycasts/sass-bundle": "^0.6.0", "symfonycasts/verify-email-bundle": "^1.17", - "twbs/bootstrap": "^5.3", - "twig/extra-bundle": "^3.0", - "twig/twig": "^3.0" + "twbs/bootstrap": "^5.3", + "twig/extra-bundle": "^3.0", + "twig/twig": "^3.0" }, "config": { "allow-plugins": { diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0b9d75c..6a2e7cd 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -17,6 +17,22 @@ security: lazy: true provider: app_user_provider user_checker: App\Security\UserChecker + login_throttling: + max_attempts: 3 # per minute + form_login: + login_path: user_signIn + check_path: user_signIn + enable_csrf: true + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 # 1 week, in seconds + secure: true + samesite: strict + signature_properties: [ 'password', 'validationAdministrator', 'validationDate' ] + logout: + path: user_signOut + # where to redirect after logout + # target: app_any_route # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -27,8 +43,8 @@ security: # Easy way to control access for large sections of your site # Note: Only the *first* access control that matches will be used access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + # - { path: ^/admin, roles: ROLE_ADMIN } + # - { path: ^/profile, roles: ROLE_USER } when@test: security: diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 688eef4..6154471 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -7,6 +7,7 @@ use App\Form\SignUpFormType; use App\Repository\UserRepository; use App\Security\EmailVerifier; use Doctrine\ORM\EntityManagerInterface; +use LogicException; use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; @@ -15,6 +16,7 @@ use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mime\Address; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use Symfony\Contracts\Translation\TranslatorInterface; use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface; @@ -86,7 +88,6 @@ class UserController extends AbstractController { 'registrationForm' => $form, ]); } - /** * User email verification * @@ -120,4 +121,46 @@ class UserController extends AbstractController { $this->addFlash('success', 'Your email address has been verified, now please wait for an administrator confirmation'); return $this->redirectToRoute('core_main'); } + + /** + * Sign in a user + * + * @param AuthenticationUtils $authenticationUtils Security errors from query + * + * @return Response The response + */ + #[Route(path: '/signIn', name: 'user_signIn')] + public function login (AuthenticationUtils $authenticationUtils): Response { + /** @var User|null $user */ + $user = $this->getUser(); + if ($user !== null) { + $this->addFlash( + 'warning', + 'You are already logged in, please sign out first.' + ); + } + + $error = $authenticationUtils->getLastAuthenticationError(); + + $lastUsername = $authenticationUtils->getLastUsername(); + return $this->render( + 'user/signIn.html.twig', + [ + 'last_username' => $lastUsername, + 'error' => $error, + ] + ); + } + + /** + * Sign out + * + * NOTE : dummy controller, intercepted by firewall + * + * @return void + */ + #[Route(path: '/signOut', name: 'user_signOut')] + public function logout (): void { + throw new LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.'); + } } diff --git a/templates/user/signIn.html.twig b/templates/user/signIn.html.twig new file mode 100644 index 0000000..0e8ca02 --- /dev/null +++ b/templates/user/signIn.html.twig @@ -0,0 +1,31 @@ +{% extends 'base.html.twig' %} + +{% block title %}Log in!{% endblock %} + +{% block mainContent %} +

Sign in

+
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + + + + + + + + +
+ +
+ + +
+{% endblock %}