Quellcode durchsuchen

gestion des utilisateurs, vérification des mots de passe

garthh vor 1 Woche
Ursprung
Commit
97e7b4b548
37 geänderte Dateien mit 1136 neuen und 13 gelöschten Zeilen
  1. 5 0
      .env
  2. 5 0
      .env.dev
  3. 19 0
      assets/controllers.json
  4. 1 3
      assets/styles/app.css
  5. 3 0
      composer.json
  6. 193 1
      composer.lock
  7. 3 0
      config/bundles.php
  8. 2 0
      config/packages/reset_password.yaml
  9. 1 0
      config/packages/security.yaml
  10. 106 0
      importmap.php
  11. 33 0
      migrations/Version20250720220735.php
  12. 31 0
      migrations/Version20250720232123.php
  13. 31 0
      migrations/Version20250721070144.php
  14. 1 1
      src/Controller/MainController.php
  15. 2 1
      src/Controller/RegistrationController.php
  16. 176 0
      src/Controller/ResetPasswordController.php
  17. 43 0
      src/Controller/UserController.php
  18. 39 0
      src/Entity/ResetPasswordRequest.php
  19. 134 0
      src/Entity/User.php
  20. 10 0
      src/Enum/Exceptions.php
  21. 58 0
      src/Form/ChangePasswordFormType.php
  22. 34 1
      src/Form/RegistrationFormType.php
  23. 31 0
      src/Form/ResetPasswordRequestFormType.php
  24. 32 0
      src/Repository/ResetPasswordRequestRepository.php
  25. 15 0
      src/Repository/UserRepository.php
  26. 36 0
      src/Security/UserChecker.php
  27. 18 0
      symfony.lock
  28. 1 1
      templates/admin/index.html.twig
  29. 2 1
      templates/base.html.twig
  30. 1 1
      templates/main/index.html.twig
  31. 1 1
      templates/registration/register.html.twig
  32. 11 0
      templates/reset_password/check_email.html.twig
  33. 9 0
      templates/reset_password/email.html.twig
  34. 22 0
      templates/reset_password/request.html.twig
  35. 12 0
      templates/reset_password/reset.html.twig
  36. 6 2
      templates/security/login.html.twig
  37. 9 0
      templates/user/index.html.twig

+ 5 - 0
.env

@@ -39,3 +39,8 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
 ###> symfony/mailer ###
 MAILER_DSN=null://null
 ###< symfony/mailer ###
+
+### Spécifiques à l'application ###
+# Adresse de courriel pour l'envoi des messages
+CONTACT_EMAIL=no-reply@mail.com
+CONTACT_NAME=Orgasso

+ 5 - 0
.env.dev

@@ -2,3 +2,8 @@
 ###> symfony/framework-bundle ###
 APP_SECRET=9444953c16af07ebd200c01d4daf96a4
 ###< symfony/framework-bundle ###
+
+### Spécifiques à l'application ###
+# Adresse de courriel pour l'envoi des messages
+CONTACT_EMAIL=no-reply@mail.com
+CONTACT_NAME=Orgasso

+ 19 - 0
assets/controllers.json

@@ -1,5 +1,24 @@
 {
     "controllers": {
+        "@pentiminax/ux-datatables": {
+            "datatable": {
+                "enabled": true,
+                "fetch": "eager",
+                "autoimport": {
+                    "datatables.net-bs5/css/dataTables.bootstrap5.min.css": false,
+                    "datatables.net-dt/css/dataTables.dataTables.min.css": true
+                }
+            }
+        },
+        "@symfony/ux-toggle-password": {
+            "toggle-password": {
+                "enabled": true,
+                "fetch": "eager",
+                "autoimport": {
+                    "@symfony/ux-toggle-password/dist/style.min.css": true
+                }
+            }
+        },
         "@symfony/ux-turbo": {
             "turbo-core": {
                 "enabled": true,

+ 1 - 3
assets/styles/app.css

@@ -1,4 +1,2 @@
 /* Styles spécifiques à l'application */
-body {
-    background-color: skyblue;
-}
+

+ 3 - 0
composer.json

@@ -12,6 +12,7 @@
         "doctrine/doctrine-migrations-bundle": "^3.4",
         "doctrine/orm": "^3.5",
         "odolbeau/phone-number-bundle": "^4.2",
+        "pentiminax/ux-datatables": "^0.8.2",
         "phpdocumentor/reflection-docblock": "^5.6",
         "phpstan/phpdoc-parser": "^2.2",
         "symfony/asset": "7.3.*",
@@ -41,11 +42,13 @@
         "symfony/twig-bundle": "7.3.*",
         "symfony/uid": "7.3.*",
         "symfony/ux-icons": "^2.27",
+        "symfony/ux-toggle-password": "^2.27",
         "symfony/ux-turbo": "^2.27",
         "symfony/ux-twig-component": "^2.27",
         "symfony/validator": "7.3.*",
         "symfony/web-link": "7.3.*",
         "symfony/yaml": "7.3.*",
+        "symfonycasts/reset-password-bundle": "^1.23",
         "symfonycasts/verify-email-bundle": "^1.17",
         "twig/extra-bundle": "^2.12|^3.0",
         "twig/twig": "^2.12|^3.0"

+ 193 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "3c8000e9218b34c791423332c7230c6a",
+    "content-hash": "a21b39ff901abc79e1cb3db926838072",
     "packages": [
         {
             "name": "composer/semver",
@@ -1592,6 +1592,70 @@
             },
             "time": "2025-07-15T20:50:09+00:00"
         },
+        {
+            "name": "pentiminax/ux-datatables",
+            "version": "v0.8.2",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/pentiminax/ux-datatables.git",
+                "reference": "d25a59855ac17f69cf4e2dc9940c54099ad53afb"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/pentiminax/ux-datatables/zipball/d25a59855ac17f69cf4e2dc9940c54099ad53afb",
+                "reference": "d25a59855ac17f69cf4e2dc9940c54099ad53afb",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.2",
+                "symfony/config": "^7.0",
+                "symfony/dependency-injection": "^7.0",
+                "symfony/http-kernel": "^7.0",
+                "symfony/stimulus-bundle": "^2.22"
+            },
+            "conflict": {
+                "symfony/flex": "<1.13"
+            },
+            "require-dev": {
+                "phpunit/phpunit": "^11.5",
+                "symfony/framework-bundle": "^7.0",
+                "symfony/phpunit-bridge": "^7.0",
+                "symfony/twig-bundle": "^7.0",
+                "symfony/var-dumper": "^7.0"
+            },
+            "type": "symfony-bundle",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/ux",
+                    "name": "symfony/ux"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Pentiminax\\UX\\DataTables\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Tanguy Lemarié",
+                    "email": "tanguy.lemarie6@gmail.com"
+                }
+            ],
+            "description": "DataTables.net integration for Symfony",
+            "homepage": "https://github.com/pentiminax/ux-datatables",
+            "keywords": [
+                "symfony-ux"
+            ],
+            "support": {
+                "issues": "https://github.com/pentiminax/ux-datatables/issues",
+                "source": "https://github.com/pentiminax/ux-datatables/tree/v0.8.2"
+            },
+            "time": "2025-07-12T10:09:31+00:00"
+        },
         {
             "name": "phpdocumentor/reflection-common",
             "version": "2.2.0",
@@ -7297,6 +7361,86 @@
             ],
             "time": "2025-06-17T06:15:15+00:00"
         },
+        {
+            "name": "symfony/ux-toggle-password",
+            "version": "v2.27.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/symfony/ux-toggle-password.git",
+                "reference": "9753f554d00e0f86f44726165f9af6e6513086b7"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/symfony/ux-toggle-password/zipball/9753f554d00e0f86f44726165f9af6e6513086b7",
+                "reference": "9753f554d00e0f86f44726165f9af6e6513086b7",
+                "shasum": ""
+            },
+            "require": {
+                "php": ">=8.1",
+                "symfony/config": "^5.4|^6.0|^7.0",
+                "symfony/dependency-injection": "^5.4|^6.0|^7.0",
+                "symfony/form": "^5.4|^6.0|^7.0",
+                "symfony/http-kernel": "^5.4|^6.0|^7.0",
+                "symfony/options-resolver": "^5.4|^6.0|^7.0",
+                "symfony/translation": "^5.4|^6.0|^7.0"
+            },
+            "require-dev": {
+                "symfony/framework-bundle": "^5.4|^6.0|^7.0",
+                "symfony/phpunit-bridge": "^5.4|^6.0|^7.0",
+                "symfony/twig-bundle": "^5.4|^6.0|^7.0",
+                "symfony/var-dumper": "^5.4|^6.0|^7.0",
+                "twig/twig": "^2.14.7|^3.0.4"
+            },
+            "type": "symfony-bundle",
+            "extra": {
+                "thanks": {
+                    "url": "https://github.com/symfony/ux",
+                    "name": "symfony/ux"
+                }
+            },
+            "autoload": {
+                "psr-4": {
+                    "Symfony\\UX\\TogglePassword\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "Félix Eymonot",
+                    "email": "felix.eymonot@alximy.io"
+                },
+                {
+                    "name": "Symfony Community",
+                    "homepage": "https://symfony.com/contributors"
+                }
+            ],
+            "description": "Toggle visibility of password inputs for Symfony Forms",
+            "homepage": "https://symfony.com",
+            "keywords": [
+                "symfony-ux"
+            ],
+            "support": {
+                "source": "https://github.com/symfony/ux-toggle-password/tree/v2.27.0"
+            },
+            "funding": [
+                {
+                    "url": "https://symfony.com/sponsor",
+                    "type": "custom"
+                },
+                {
+                    "url": "https://github.com/fabpot",
+                    "type": "github"
+                },
+                {
+                    "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+                    "type": "tidelift"
+                }
+            ],
+            "time": "2025-06-06T20:27:21+00:00"
+        },
         {
             "name": "symfony/ux-turbo",
             "version": "v2.27.0",
@@ -7893,6 +8037,54 @@
             ],
             "time": "2025-06-03T06:57:57+00:00"
         },
+        {
+            "name": "symfonycasts/reset-password-bundle",
+            "version": "v1.23.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/SymfonyCasts/reset-password-bundle.git",
+                "reference": "bde42fe5956e0cd523931da886ee41ab660c45b2"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/SymfonyCasts/reset-password-bundle/zipball/bde42fe5956e0cd523931da886ee41ab660c45b2",
+                "reference": "bde42fe5956e0cd523931da886ee41ab660c45b2",
+                "shasum": ""
+            },
+            "require": {
+                "ext-json": "*",
+                "php": ">=8.1.10",
+                "symfony/config": "^5.4 | ^6.0 | ^7.0",
+                "symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0",
+                "symfony/deprecation-contracts": "^2.2 | ^3.0",
+                "symfony/http-kernel": "^5.4 | ^6.0 | ^7.0"
+            },
+            "require-dev": {
+                "doctrine/annotations": "^1.0",
+                "doctrine/doctrine-bundle": "^2.8",
+                "doctrine/orm": "^2.13",
+                "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0",
+                "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0",
+                "symfony/process": "^6.4 | ^7.0 | ^7.1",
+                "symfonycasts/internal-test-helpers": "dev-main"
+            },
+            "type": "symfony-bundle",
+            "autoload": {
+                "psr-4": {
+                    "SymfonyCasts\\Bundle\\ResetPassword\\": "src/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "Symfony bundle that adds password reset functionality.",
+            "support": {
+                "issues": "https://github.com/SymfonyCasts/reset-password-bundle/issues",
+                "source": "https://github.com/SymfonyCasts/reset-password-bundle/tree/v1.23.1"
+            },
+            "time": "2024-12-09T19:04:36+00:00"
+        },
         {
             "name": "symfonycasts/verify-email-bundle",
             "version": "v1.17.3",

+ 3 - 0
config/bundles.php

@@ -17,4 +17,7 @@ return [
     Symfony\UX\Icons\UXIconsBundle::class => ['all' => true],
     Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true],
     SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
+    Pentiminax\UX\DataTables\DataTablesBundle::class => ['all' => true],
+    SymfonyCasts\Bundle\ResetPassword\SymfonyCastsResetPasswordBundle::class => ['all' => true],
+    Symfony\UX\TogglePassword\TogglePasswordBundle::class => ['all' => true],
 ];

+ 2 - 0
config/packages/reset_password.yaml

@@ -0,0 +1,2 @@
+symfonycasts_reset_password:
+    request_password_repository: App\Repository\ResetPasswordRequestRepository

+ 1 - 0
config/packages/security.yaml

@@ -21,6 +21,7 @@ security:
                 login_path: app_login
                 check_path: app_login
                 enable_csrf: true
+            user_checker: App\Security\UserChecker
             remember_me:
                 # https://symfony.com/doc/current/security/remember_me.html
                 secret: '%kernel.secret%'

+ 106 - 0
importmap.php

@@ -25,4 +25,110 @@ return [
     '@hotwired/turbo' => [
         'version' => '7.3.0',
     ],
+    'datatables.net-bs5' => [
+        'version' => '2.3.2',
+    ],
+    'jquery' => [
+        'version' => '3.7.1',
+    ],
+    'datatables.net' => [
+        'version' => '2.3.2',
+    ],
+    'datatables.net-bs5/css/dataTables.bootstrap5.min.css' => [
+        'version' => '2.3.2',
+        'type' => 'css',
+    ],
+    'datatables.net-buttons' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-buttons-bs5' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-buttons-bs5/css/buttons.bootstrap5.min.css' => [
+        'version' => '3.2.4',
+        'type' => 'css',
+    ],
+    'datatables.net-buttons-dt' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-dt' => [
+        'version' => '2.3.2',
+    ],
+    'datatables.net-buttons-dt/css/buttons.dataTables.min.css' => [
+        'version' => '3.2.4',
+        'type' => 'css',
+    ],
+    'datatables.net-dt/css/dataTables.dataTables.min.css' => [
+        'version' => '2.3.2',
+        'type' => 'css',
+    ],
+    'datatables.net-columncontrol-bs5' => [
+        'version' => '1.0.7',
+    ],
+    'datatables.net-columncontrol' => [
+        'version' => '1.0.7',
+    ],
+    'datatables.net-columncontrol-bs5/css/columnControl.bootstrap5.min.css' => [
+        'version' => '1.0.7',
+        'type' => 'css',
+    ],
+    'datatables.net-columncontrol-dt' => [
+        'version' => '1.0.7',
+    ],
+    'datatables.net-columncontrol-dt/css/columnControl.dataTables.min.css' => [
+        'version' => '1.0.7',
+        'type' => 'css',
+    ],
+    'datatables.net-buttons/js/buttons.colVis' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-buttons/js/buttons.html5' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-buttons/js/buttons.print' => [
+        'version' => '3.2.4',
+    ],
+    'datatables.net-select-bs5' => [
+        'version' => '3.0.1',
+    ],
+    'datatables.net-select' => [
+        'version' => '3.0.1',
+    ],
+    'datatables.net-select-bs5/css/select.bootstrap5.min.css' => [
+        'version' => '3.0.1',
+        'type' => 'css',
+    ],
+    'datatables.net-select-dt' => [
+        'version' => '3.0.1',
+    ],
+    'datatables.net-select-dt/css/select.dataTables.min.css' => [
+        'version' => '3.0.1',
+        'type' => 'css',
+    ],
+    'datatables.net-responsive-bs5' => [
+        'version' => '3.0.5',
+    ],
+    'datatables.net-responsive' => [
+        'version' => '3.0.5',
+    ],
+    'datatables.net-responsive-bs5/css/responsive.bootstrap5.min.css' => [
+        'version' => '3.0.5',
+        'type' => 'css',
+    ],
+    'datatables.net-responsive-dt' => [
+        'version' => '3.0.5',
+    ],
+    'datatables.net-responsive-dt/css/responsive.dataTables.min.css' => [
+        'version' => '3.0.5',
+        'type' => 'css',
+    ],
+    'jszip' => [
+        'version' => '3.10.1',
+    ],
+    'pdfmake' => [
+        'version' => '0.2.20',
+    ],
+    'pdfmake/build/vfs_fonts' => [
+        'version' => '0.2.20',
+    ],
 ];

+ 33 - 0
migrations/Version20250720220735.php

@@ -0,0 +1,33 @@
+<?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 Version20250720220735 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 reset_password_request (id INT AUTO_INCREMENT NOT NULL, user_id BINARY(16) NOT NULL COMMENT \'(DC2Type:uuid)\', selector VARCHAR(20) NOT NULL, hashed_token VARCHAR(100) NOT NULL, requested_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', expires_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_7CE748AA76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
+        $this->addSql('ALTER TABLE reset_password_request ADD CONSTRAINT FK_7CE748AA76ED395 FOREIGN KEY (user_id) REFERENCES user (id)');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE reset_password_request DROP FOREIGN KEY FK_7CE748AA76ED395');
+        $this->addSql('DROP TABLE reset_password_request');
+    }
+}

+ 31 - 0
migrations/Version20250720232123.php

@@ -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 Version20250720232123 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('ALTER TABLE user ADD first_name VARCHAR(180) NOT NULL, ADD last_name VARCHAR(180) NOT NULL, ADD phone VARCHAR(255) DEFAULT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE user DROP first_name, DROP last_name, DROP phone');
+    }
+}

+ 31 - 0
migrations/Version20250721070144.php

@@ -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 Version20250721070144 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('ALTER TABLE user ADD last_update DATE NOT NULL, ADD last_login DATE NOT NULL, ADD last_action DATE NOT NULL');
+    }
+
+    public function down(Schema $schema): void
+    {
+        // this down() migration is auto-generated, please modify it to your needs
+        $this->addSql('ALTER TABLE user DROP last_update, DROP last_login, DROP last_action');
+    }
+}

+ 1 - 1
src/Controller/MainController.php

@@ -8,7 +8,7 @@ use Symfony\Component\Routing\Attribute\Route;
 
 final class MainController extends AbstractController
 {
-    #[Route('/main', name: 'app_main')]
+    #[Route('/', name: 'app_main')]
     public function index(): Response
     {
         return $this->render('main/index.html.twig', [

+ 2 - 1
src/Controller/RegistrationController.php

@@ -30,6 +30,7 @@ class RegistrationController extends AbstractController
         $form->handleRequest($request);
 
         if ($form->isSubmitted() && $form->isValid()) {
+            
             /** @var string $plainPassword */
             $plainPassword = $form->get('plainPassword')->getData();
 
@@ -42,7 +43,7 @@ class RegistrationController extends AbstractController
             // generate a signed url and email it to the user
             $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
                 (new TemplatedEmail())
-                    ->from(new Address('contact@portes-imaginaire.org', 'Association'))
+                    ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
                     ->to((string) $user->getEmail())
                     ->subject('Please Confirm your Email')
                     ->htmlTemplate('registration/confirmation_email.html.twig')

+ 176 - 0
src/Controller/ResetPasswordController.php

@@ -0,0 +1,176 @@
+<?php
+
+namespace App\Controller;
+
+use App\Entity\User;
+use App\Form\ChangePasswordFormType;
+use App\Form\ResetPasswordRequestFormType;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bridge\Twig\Mime\TemplatedEmail;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Mailer\MailerInterface;
+use Symfony\Component\Mime\Address;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Contracts\Translation\TranslatorInterface;
+use SymfonyCasts\Bundle\ResetPassword\Controller\ResetPasswordControllerTrait;
+use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
+use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
+
+#[Route('/reset-password')]
+class ResetPasswordController extends AbstractController
+{
+    use ResetPasswordControllerTrait;
+
+    public function __construct(
+        private ResetPasswordHelperInterface $resetPasswordHelper,
+        private EntityManagerInterface $entityManager
+    ) {
+    }
+
+    /**
+     * Display & process form to request a password reset.
+     */
+    #[Route('', name: 'app_forgot_password_request')]
+    public function request(Request $request, MailerInterface $mailer, TranslatorInterface $translator): Response
+    {
+        $form = $this->createForm(ResetPasswordRequestFormType::class);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            /** @var string $email */
+            $email = $form->get('email')->getData();
+
+            return $this->processSendingPasswordResetEmail($email, $mailer, $translator
+            );
+        }
+
+        return $this->render('reset_password/request.html.twig', [
+            'requestForm' => $form,
+        ]);
+    }
+
+    /**
+     * Confirmation page after a user has requested a password reset.
+     */
+    #[Route('/check-email', name: 'app_check_email')]
+    public function checkEmail(): Response
+    {
+        // Generate a fake token if the user does not exist or someone hit this page directly.
+        // This prevents exposing whether or not a user was found with the given email address or not
+        if (null === ($resetToken = $this->getTokenObjectFromSession())) {
+            $resetToken = $this->resetPasswordHelper->generateFakeResetToken();
+        }
+
+        return $this->render('reset_password/check_email.html.twig', [
+            'resetToken' => $resetToken,
+        ]);
+    }
+
+    /**
+     * Validates and process the reset URL that the user clicked in their email.
+     */
+    #[Route('/reset/{token}', name: 'app_reset_password')]
+    public function reset(Request $request, UserPasswordHasherInterface $passwordHasher, TranslatorInterface $translator, ?string $token = null): Response
+    {
+        if ($token) {
+            // We store the token in session and remove it from the URL, to avoid the URL being
+            // loaded in a browser and potentially leaking the token to 3rd party JavaScript.
+            $this->storeTokenInSession($token);
+
+            return $this->redirectToRoute('app_reset_password');
+        }
+
+        $token = $this->getTokenFromSession();
+
+        if (null === $token) {
+            throw $this->createNotFoundException('No reset password token found in the URL or in the session.');
+        }
+
+        try {
+            /** @var User $user */
+            $user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
+        } catch (ResetPasswordExceptionInterface $e) {
+            $this->addFlash('reset_password_error', sprintf(
+                '%s - %s',
+                $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_VALIDATE, [], 'ResetPasswordBundle'),
+                $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
+            ));
+
+            return $this->redirectToRoute('app_forgot_password_request');
+        }
+
+        // The token is valid; allow the user to change their password.
+        $form = $this->createForm(ChangePasswordFormType::class);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            // A password reset token should be used only once, remove it.
+            $this->resetPasswordHelper->removeResetRequest($token);
+
+            /** @var string $plainPassword */
+            $plainPassword = $form->get('plainPassword')->getData();
+
+            // Encode(hash) the plain password, and set it.
+            $user->setPassword($passwordHasher->hashPassword($user, $plainPassword));
+            $this->entityManager->flush();
+
+            // The session is cleaned up after the password has been changed.
+            $this->cleanSessionAfterReset();
+
+            return $this->redirectToRoute('app_main');
+        }
+
+        return $this->render('reset_password/reset.html.twig', [
+            'resetForm' => $form,
+        ]);
+    }
+
+    private function processSendingPasswordResetEmail(string $emailFormData, MailerInterface $mailer, TranslatorInterface $translator): RedirectResponse
+    {
+        $user = $this->entityManager->getRepository(User::class)->findOneBy([
+            'email' => $emailFormData,
+        ]);
+
+        // Do not reveal whether a user account was found or not.
+        if (!$user) {
+            return $this->redirectToRoute('app_check_email');
+        }
+
+        try {
+            $resetToken = $this->resetPasswordHelper->generateResetToken($user);
+        } catch (ResetPasswordExceptionInterface $e) {
+            // If you want to tell the user why a reset email was not sent, uncomment
+            // the lines below and change the redirect to 'app_forgot_password_request'.
+            // Caution: This may reveal if a user is registered or not.
+            //
+            // $this->addFlash('reset_password_error', sprintf(
+            //     '%s - %s',
+            //     $translator->trans(ResetPasswordExceptionInterface::MESSAGE_PROBLEM_HANDLE, [], 'ResetPasswordBundle'),
+            //     $translator->trans($e->getReason(), [], 'ResetPasswordBundle')
+            // ));
+
+            return $this->redirectToRoute('app_check_email');
+        }
+
+        $email = (new TemplatedEmail())
+            ->from(new Address($_ENV['CONTACT_EMAIL'], $_ENV['CONTACT_NAME']))
+            ->to((string) $user->getEmail())
+            ->subject('Your password reset request')
+            ->htmlTemplate('reset_password/email.html.twig')
+            ->context([
+                'resetToken' => $resetToken,
+            ])
+        ;
+
+        $mailer->send($email);
+
+        // Store the token object in session for retrieval in check-email route.
+        $this->setTokenObjectInSession($resetToken);
+
+        return $this->redirectToRoute('app_check_email');
+    }
+}

+ 43 - 0
src/Controller/UserController.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace App\Controller;
+
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+use App\Repository\UserRepository;
+use Pentiminax\UX\DataTables\Builder\DataTableBuilderInterface;
+use Pentiminax\UX\DataTables\Model\Column;
+
+final class UserController extends AbstractController
+{
+    // Lister tous les utilisateurs 
+
+    #[Route('/admin/user', name: 'app_user')]
+    public function index(UserRepository $repository, DataTableBuilderInterface $builder): Response
+    {
+        // Récupérer tous les utilisateurs du dépot
+        $usersRequest = $repository->findAll();
+        $users = [['1', 'admin', 'ADMIN', "supprimer"]];
+
+        // Création du datatable 
+        $table = $builder->createDataTable('usersTable')
+            ->columns([
+                Column::new('uid', "Identifiant"),
+                Column::new('email', 'Email'),
+                Column::new('roles', 'Rôles'),
+                Column::new('actions', 'Actions')
+            ])
+            ->data($users);
+
+        // Envoyer à la vue
+        return $this->render('user/index.html.twig', [
+            'controller_name' => 'UserController',
+            'users' => $users,
+            'table' => $table,
+        ]);
+    }
+
+    // Supprimer un utilisateur
+    
+}

+ 39 - 0
src/Entity/ResetPasswordRequest.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace App\Entity;
+
+use App\Repository\ResetPasswordRequestRepository;
+use Doctrine\ORM\Mapping as ORM;
+use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
+use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestTrait;
+
+#[ORM\Entity(repositoryClass: ResetPasswordRequestRepository::class)]
+class ResetPasswordRequest implements ResetPasswordRequestInterface
+{
+    use ResetPasswordRequestTrait;
+
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column]
+    private ?int $id = null;
+
+    #[ORM\ManyToOne]
+    #[ORM\JoinColumn(nullable: false)]
+    private ?User $user = null;
+
+    public function __construct(User $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken)
+    {
+        $this->user = $user;
+        $this->initialize($expiresAt, $selector, $hashedToken);
+    }
+
+    public function getId(): ?int
+    {
+        return $this->id;
+    }
+
+    public function getUser(): User
+    {
+        return $this->user;
+    }
+}

+ 134 - 0
src/Entity/User.php

@@ -4,11 +4,14 @@ namespace App\Entity;
 
 use App\Repository\UserRepository;
 use Doctrine\ORM\Mapping as ORM;
+use Doctrine\DBAL\Types\Types;
 use Symfony\Bridge\Doctrine\Types\UuidType;
 use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
 use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
 use Symfony\Component\Security\Core\User\UserInterface;
 use Symfony\Component\Uid\Uuid;
+use Misd\PhoneNumberBundle\Validator\Constraints as MisdAssert;
+use Symfony\Component\Validator\Constraints as Assert;
 
 #[ORM\Entity(repositoryClass: UserRepository::class)]
 #[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_EMAIL', fields: ['email'])]
@@ -21,6 +24,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
     private ?Uuid $id = null;
 
+    #[Assert\Email]
     #[ORM\Column(length: 180)]
     private ?string $email = null;
 
@@ -39,6 +43,30 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     #[ORM\Column]
     private bool $isVerified = false;
 
+    #[ORM\Column(length: 180)]
+    private ?string $firstName = null;
+
+    #[ORM\Column(length: 180)]
+    private ?string $lastName = null;
+
+    #[MisdAssert\PhoneNumber()]
+    #[ORM\Column(length: 255, nullable: true)]
+    private ?string $phone = null;
+
+    #[ORM\Column(type: Types::DATE_MUTABLE,nullable: true)]
+    private ?\DateTimeInterface $lastUpdate = null;
+
+    #[ORM\Column(type: Types::DATE_MUTABLE,nullable: true)]
+    private ?\DateTimeInterface $lastLogin = null;
+
+    #[ORM\Column(type: Types::DATE_MUTABLE,nullable: true)]
+    private ?\DateTimeInterface $lastAction = null;
+
+    public function __construct()
+    {
+        $this->lastUpdate = new \DateTime('now');
+    }
+
     public function getId(): ?Uuid
     {
         return $this->id;
@@ -57,6 +85,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     public function setEmail(string $email): static
     {
         $this->email = $email;
+        $this->lastUpdate = new \DateTime('now');
 
         return $this;
     }
@@ -89,6 +118,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     public function setRoles(array $roles): static
     {
         $this->roles = $roles;
+        $this->lastUpdate = new \DateTime('now');
 
         return $this;
     }
@@ -104,6 +134,110 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
     public function setPassword(string $password): static
     {
         $this->password = $password;
+        $this->lastUpdate = new \DateTime('now');
+
+        return $this;
+    }
+
+    /**
+     * Getter / Setter pour prénom
+     */
+    public function getFirstName(): ?string
+    {
+        return $this->firstName;
+    }
+
+    public function setFirstName(string $firstName): static
+    {
+        $this->firstName = $firstName;
+        $this->lastUpdate = new \DateTime('now');
+
+        return $this;
+    }
+
+    /**
+     * Getter / Setter pour nom
+     */
+    public function getLastName(): ?string
+    {
+        return $this->lastName;
+    }
+
+    public function setLastName(string $lastName): static
+    {
+        $this->lastName = $lastName;
+        $this->lastUpdate = new \DateTime('now');
+
+        return $this;
+    }
+
+    /**
+     * Getter / Setter pour numéro de téléphone
+     */
+    public function getPhone(): ?string
+    {
+        return $this->phone;
+    }
+
+    public function setPhone(string $phone): static
+    {
+        $this->phone = $phone;
+        $this->lastUpdate = new \DateTime('now');
+
+        return $this;
+    }
+
+    /**
+     * Getter / Setter lastUpdate
+     */
+    public function getLastUpdate(): ?\DateTimeInterface
+    {
+        return $this->lastUpdate;
+    }
+
+    public function setLastUpdate(\DateTimeInterface $lastUpdate): static
+    {
+        // Si la date est vide, on mets à jour à la date actuelle
+        if (!$lastUpdate) {
+            $lastUpdate = new \DateTime('now');
+        }
+        $this->lastUpdate = $lastUpdate;
+
+        return $this;
+    }
+    /**
+     * Getter / Setter pour le dernier login
+     */
+    public function getLastLogin(): ?\DateTimeInterface
+    {
+        return $this->lastLogin;
+    }
+
+    public function setLastLogin(\DateTimeInterface $lastLogin): static
+    {
+        // Si la date est vide, on mets à jour à la date actuelle
+        if (!$lastLogin) {
+            $lastLogin = new \DateTime('now');
+        }
+        $this->lastLogin = $lastLogin;
+
+        return $this;
+    }
+    /**
+     * Getter / Setter pour la dernière action
+     */
+    public function getLastAction(): ?\DateTimeInterface
+    {
+        return $this->lastAction;
+    }
+
+    public function setLastAction(\DateTimeInterface $lastAction): static
+    {
+        // Si la date est vide, on mets à jour à la date actuelle
+        if (!$lastAction) {
+            $lastAction = new \DateTime('now');
+        }
+        $this->lastAction = $lastAction;
 
         return $this;
     }

+ 10 - 0
src/Enum/Exceptions.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace App\Enum;
+
+enum Exceptions: string
+{
+    case NOT_VERIFIED_USER = 'Merci de confirmer avant votre courriel en cliquant sur le lien reçu.';
+}
+
+?>

+ 58 - 0
src/Form/ChangePasswordFormType.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace App\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\PasswordType;
+use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Validator\Constraints\Length;
+use Symfony\Component\Validator\Constraints\NotBlank;
+use Symfony\Component\Validator\Constraints\NotCompromisedPassword;
+use Symfony\Component\Validator\Constraints\PasswordStrength;
+
+class ChangePasswordFormType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('plainPassword', RepeatedType::class, [
+                'type' => PasswordType::class,
+                'options' => [
+                    'attr' => [
+                        'autocomplete' => 'new-password',
+                    ],
+                ],
+                'first_options' => [
+                    '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,
+                        ]),
+                        new PasswordStrength(),
+                        new NotCompromisedPassword(),
+                    ],
+                    'label' => 'New password',
+                ],
+                'second_options' => [
+                    'label' => 'Repeat Password',
+                ],
+                'invalid_message' => 'The password fields must match.',
+                // Instead of being set onto the object directly,
+                // this is read and encoded in the controller
+                'mapped' => false,
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([]);
+    }
+}

+ 34 - 1
src/Form/RegistrationFormType.php

@@ -3,6 +3,7 @@
 namespace App\Form;
 
 use App\Entity\User;
+use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
 use Symfony\Component\Form\Extension\Core\Type\PasswordType;
@@ -17,6 +18,12 @@ class RegistrationFormType extends AbstractType
     public function buildForm(FormBuilderInterface $builder, array $options): void
     {
         $builder
+            ->add('firstName')
+            ->add('lastName')
+            ->add('phone', null, [
+                'required' => false,
+                'label' => 'Phone Number',
+            ])
             ->add('email')
             ->add('agreeTerms', CheckboxType::class, [
                 'mapped' => false,
@@ -26,10 +33,11 @@ class RegistrationFormType extends AbstractType
                     ]),
                 ],
             ])
-            ->add('plainPassword', PasswordType::class, [
+/*             ->add('plainPassword', PasswordType::class, [
                 // instead of being set onto the object directly,
                 // this is read and encoded in the controller
                 'mapped' => false,
+                'toggle' => true,
                 'attr' => ['autocomplete' => 'new-password'],
                 'constraints' => [
                     new NotBlank([
@@ -42,6 +50,31 @@ class RegistrationFormType extends AbstractType
                         'max' => 4096,
                     ]),
                 ],
+            ]) */
+            ->add('plainPassword', RepeatedType::class, [
+                'mapped' => false,
+                'invalid_message' => 'The password fields must match.',
+                'type' => PasswordType::class,
+                'first_options' => [
+                    'label' => 'Password',
+                    'toggle' => true,
+                    'attr' => ['autocomplete' => 'new-password'],
+                    '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,
+                        ]),
+                    ]],
+                'second_options' => [
+                    'label' => 'Repeat Password',
+                    'toggle' => true,
+                    'attr' => ['autocomplete' => 'new-password'],
+                    ],
             ])
         ;
     }

+ 31 - 0
src/Form/ResetPasswordRequestFormType.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace App\Form;
+
+use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
+use Symfony\Component\Form\FormBuilderInterface;
+use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Validator\Constraints\NotBlank;
+
+class ResetPasswordRequestFormType extends AbstractType
+{
+    public function buildForm(FormBuilderInterface $builder, array $options): void
+    {
+        $builder
+            ->add('email', EmailType::class, [
+                'attr' => ['autocomplete' => 'email'],
+                'constraints' => [
+                    new NotBlank([
+                        'message' => 'Please enter your email',
+                    ]),
+                ],
+            ])
+        ;
+    }
+
+    public function configureOptions(OptionsResolver $resolver): void
+    {
+        $resolver->setDefaults([]);
+    }
+}

+ 32 - 0
src/Repository/ResetPasswordRequestRepository.php

@@ -0,0 +1,32 @@
+<?php
+
+namespace App\Repository;
+
+use App\Entity\ResetPasswordRequest;
+use App\Entity\User;
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
+use Doctrine\Persistence\ManagerRegistry;
+use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface;
+use SymfonyCasts\Bundle\ResetPassword\Persistence\Repository\ResetPasswordRequestRepositoryTrait;
+use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface;
+
+/**
+ * @extends ServiceEntityRepository<ResetPasswordRequest>
+ */
+class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
+{
+    use ResetPasswordRequestRepositoryTrait;
+
+    public function __construct(ManagerRegistry $registry)
+    {
+        parent::__construct($registry, ResetPasswordRequest::class);
+    }
+
+    /**
+     * @param User $user
+     */
+    public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
+    {
+        return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
+    }
+}

+ 15 - 0
src/Repository/UserRepository.php

@@ -33,6 +33,21 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
         $this->getEntityManager()->flush();
     }
 
+    /**
+     * Who's online
+     * = dernière action réalisée il y a moins de 5 minutes
+     */
+
+    public function findWhoIsOnline(): array
+    {
+        $qb = $this->createQueryBuilder('u')
+            ->where('u.lastAction > :lastAction')
+            ->setParameter('lastAction', new \DateTime('-5 minutes'))
+            ->getQuery();
+
+        return $qb->getResult();
+    }
+
     //    /**
     //     * @return User[] Returns an array of User objects
     //     */

+ 36 - 0
src/Security/UserChecker.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace App\Security;
+
+use App\Enum\Exceptions as ExceptionsMsg;
+use App\Entity\User as AppUser;
+use Symfony\Component\Security\Core\User\UserInterface;
+use Symfony\Component\Security\Core\User\UserCheckerInterface;
+use Symfony\Component\Security\Core\Exception\AuthenticationException;
+use App\Enum\Exceptions;
+
+
+class UserChecker implements UserCheckerInterface
+{
+    public function checkPreAuth(UserInterface $user): void
+    {
+        if (!$user instanceof AppUser) {
+            return;
+        }
+    }
+
+    public function checkPostAuth(UserInterface $user): void
+    {
+        if (!$user instanceof AppUser) {
+            return;
+        }
+
+        if ($user instanceof AppUser) {
+            if (!$user->isVerified()) {
+                throw new AuthenticationException(ExceptionsMsg::NOT_VERIFIED_USER->value);
+            }
+        }
+    }
+} 
+
+?>

+ 18 - 0
symfony.lock

@@ -47,6 +47,9 @@
             "config/packages/misd_phone_number.yaml"
         ]
     },
+    "pentiminax/ux-datatables": {
+        "version": "v0.8.2"
+    },
     "phpunit/phpunit": {
         "version": "12.2",
         "recipe": {
@@ -303,6 +306,9 @@
             "assets/icons/symfony.svg"
         ]
     },
+    "symfony/ux-toggle-password": {
+        "version": "v2.27.0"
+    },
     "symfony/ux-turbo": {
         "version": "2.27",
         "recipe": {
@@ -361,6 +367,18 @@
             "config/packages/messenger.yaml"
         ]
     },
+    "symfonycasts/reset-password-bundle": {
+        "version": "1.23",
+        "recipe": {
+            "repo": "github.com/symfony/recipes",
+            "branch": "main",
+            "version": "1.0",
+            "ref": "97c1627c0384534997ae1047b93be517ca16de43"
+        },
+        "files": [
+            "config/packages/reset_password.yaml"
+        ]
+    },
     "symfonycasts/verify-email-bundle": {
         "version": "v1.17.3"
     },

+ 1 - 1
templates/admin/index.html.twig

@@ -2,7 +2,7 @@
 
 {% block title %}Hello AdminController!{% endblock %}
 
-{% block body %}
+{% block content %}
 <style>
     .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
     .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }

+ 2 - 1
templates/base.html.twig

@@ -19,6 +19,7 @@
         <!-- /javascripts -->
     </head>
     <body>
-        {% block body %}{% endblock %}
+        {% block content %}
+        {% endblock %}
     </body>
 </html>

+ 1 - 1
templates/main/index.html.twig

@@ -2,7 +2,7 @@
 
 {% block title %}Hello MainController!{% endblock %}
 
-{% block body %}
+{% block content %}
 <style>
     .example-wrapper { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
     .example-wrapper code { background: #F5F5F5; padding: 2px 6px; }

+ 1 - 1
templates/registration/register.html.twig

@@ -2,7 +2,7 @@
 
 {% block title %}Register{% endblock %}
 
-{% block body %}
+{% block content %}
     {% for flash_error in app.flashes('verify_email_error') %}
         <div class="alert alert-danger" role="alert">{{ flash_error }}</div>
     {% endfor %}

+ 11 - 0
templates/reset_password/check_email.html.twig

@@ -0,0 +1,11 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Password Reset Email Sent{% endblock %}
+
+{% block content %}
+    <p>
+        If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.
+        This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
+    </p>
+    <p>If you don't receive an email please check your spam folder or <a href="{{ path('app_forgot_password_request') }}">try again</a>.</p>
+{% endblock %}

+ 9 - 0
templates/reset_password/email.html.twig

@@ -0,0 +1,9 @@
+<h1>Hi!</h1>
+
+<p>To reset your password, please visit the following link</p>
+
+<a href="{{ url('app_reset_password', {token: resetToken.token}) }}">{{ url('app_reset_password', {token: resetToken.token}) }}</a>
+
+<p>This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.</p>
+
+<p>Cheers!</p>

+ 22 - 0
templates/reset_password/request.html.twig

@@ -0,0 +1,22 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Reset your password{% endblock %}
+
+{% block content %}
+    {% for flash_error in app.flashes('reset_password_error') %}
+        <div class="alert alert-danger" role="alert">{{ flash_error }}</div>
+    {% endfor %}
+    <h1>Reset your password</h1>
+
+    {{ form_start(requestForm) }}
+        {{ form_row(requestForm.email) }}
+        <div>
+            <small>
+                Enter your email address, and we will send you a
+                link to reset your password.
+            </small>
+        </div>
+
+        <button class="btn btn-primary">Send password reset email</button>
+    {{ form_end(requestForm) }}
+{% endblock %}

+ 12 - 0
templates/reset_password/reset.html.twig

@@ -0,0 +1,12 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Reset your password{% endblock %}
+
+{% block content %}
+    <h1>Reset your password</h1>
+
+    {{ form_start(resetForm) }}
+        {{ form_row(resetForm.plainPassword) }}
+        <button class="btn btn-primary">Reset password</button>
+    {{ form_end(resetForm) }}
+{% endblock %}

+ 6 - 2
templates/security/login.html.twig

@@ -2,10 +2,10 @@
 
 {% block title %}Log in!{% endblock %}
 
-{% block body %}
+{% block content %}
     <form method="post">
         {% if error %}
-            <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
+            <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}<br/>{{ error.message }}</div>
         {% endif %}
 
         {% if app.user %}
@@ -30,6 +30,10 @@
                 <input type="checkbox" name="_remember_me" id="_remember_me">
                 <label for="_remember_me">Remember me</label>
             </div>
+
+            <div>
+                <a href="{{ path('app_forgot_password_request') }}">Forgot password?</a>
+            </div>
         {# #}
 
         <button class="btn btn-lg btn-primary" type="submit">

+ 9 - 0
templates/user/index.html.twig

@@ -0,0 +1,9 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}User admin{% endblock %}
+
+{% block content %}
+
+    {{ render_datatable(table)}}
+
+{% endblock %}