From 2782b154eda74f5b4825d47488a36479cf75d6ef Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 17 Oct 2024 16:37:01 +0200 Subject: [PATCH 1/9] Added some style.css --- assets/styles/style.css | 76 ++++++++++++++++++++++++++++++++++ src/Entity/MissionCategory.php | 2 - 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 assets/styles/style.css diff --git a/assets/styles/style.css b/assets/styles/style.css new file mode 100644 index 0000000..4f52014 --- /dev/null +++ b/assets/styles/style.css @@ -0,0 +1,76 @@ +body { + background: linear-gradient(to right, #5CE1E6, #C1FF72, #cb6ce6); +} + +h1 { + text-align: center; + font-family: "Fredoka", sans-serif; + font-optical-sizing: auto; + font-style: normal; + font-variation-settings: "wdth" 100; +} + +img { + width: 60%; + height: auto; + float: right; + margin: 0; +} + +.container { + display: flex; + justify-content: center; + align-items: center; + height: auto; + margin: auto 0 auto; +} + +form { + margin-right: 20px; + background: rgba(255, 255, 255, 0.15); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + justify-content: center; + align-items: center; + padding: 50px; +} + +form div { + margin-bottom: 10px; +} + +.logo{ + margin-bottom: 65%; + margin-right: 30%; +} + +label { + display: block; + margin-bottom: 5px; + font-family: "Fredoka", sans-serif; + font-style: normal; +} + +input[type="text"], +input[type="password"] { + width: 90%; + padding: 5px; +} + +button { + padding: 10px 20px; + background-color: #5CE1E6; + color: black; + border: none; + cursor: pointer; +} + +button:hover { + background-color: #CB6CE6; +} + +.submit { + margin-left: 15%; +} \ No newline at end of file diff --git a/src/Entity/MissionCategory.php b/src/Entity/MissionCategory.php index 862d5a8..dc6f1fd 100644 --- a/src/Entity/MissionCategory.php +++ b/src/Entity/MissionCategory.php @@ -10,12 +10,10 @@ class MissionCategory { #[ORM\Id] #[ORM\OneToMany(targetEntity: Mission::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] private ?Mission $mission = null; #[ORM\Id] #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] private ?Category $category = null; public function getMission(): ?Mission From 17fd3ef23aaa0c730fa304b40888ac01090fb863 Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 17 Oct 2024 17:17:21 +0200 Subject: [PATCH 2/9] MissionCategory.php --- src/Entity/MissionCategory.php | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Entity/MissionCategory.php b/src/Entity/MissionCategory.php index 862d5a8..ea5680d 100644 --- a/src/Entity/MissionCategory.php +++ b/src/Entity/MissionCategory.php @@ -5,17 +5,18 @@ namespace App\Entity; use App\Repository\MissionCategoryRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: MissionCategoryRepository::class)] +#[ORM\Entity] +#[ORM\UniqueConstraint(columns: ['mission', 'category'])] class MissionCategory { #[ORM\Id] - #[ORM\OneToMany(targetEntity: Mission::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Mission::class, inversedBy: 'missionCategories')] + #[ORM\Column(type: 'integer')] private ?Mission $mission = null; #[ORM\Id] - #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'missionCategories')] + #[ORM\Column(type: 'integer')] private ?Category $category = null; public function getMission(): ?Mission From 136dc8367e54bdfa50d40b6c414f64d684275241 Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 28 Nov 2024 14:28:32 +0100 Subject: [PATCH 3/9] Form login, WIP --- .env | 2 +- composer.json | 1 + composer.lock | 48 ++++++++++- config/bundles.php | 1 + config/packages/security.yaml | 18 +++- migrations/Version20241017154551.php | 49 +++++++++++ migrations/Version20241017154952.php | 49 +++++++++++ migrations/Version20241114142616.php | 49 +++++++++++ src/Controller/DashboardController.php | 7 +- src/Controller/RegistrationController.php | 86 +++++++++++++++++++ src/Controller/SecurityController.php | 33 +++++++ src/Form/LoginType.php | 28 ++++++ src/Form/RegistrationFormType.php | 64 ++++++++++++++ src/Repository/UserRepository.php | 60 +++++++++++++ src/Security/EmailVerifier.php | 52 +++++++++++ src/Security/LoginFormAuthenticator.php | 60 +++++++++++++ symfony.lock | 3 + .../registration/confirmation_email.html.twig | 11 +++ templates/registration/register.html.twig | 32 +++++++ templates/security/login.html.twig | 26 ++++++ 20 files changed, 672 insertions(+), 7 deletions(-) create mode 100644 migrations/Version20241017154551.php create mode 100644 migrations/Version20241017154952.php create mode 100644 migrations/Version20241114142616.php create mode 100644 src/Controller/RegistrationController.php create mode 100644 src/Controller/SecurityController.php create mode 100644 src/Form/LoginType.php create mode 100644 src/Form/RegistrationFormType.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/Security/EmailVerifier.php create mode 100644 src/Security/LoginFormAuthenticator.php create mode 100644 templates/registration/confirmation_email.html.twig create mode 100644 templates/registration/register.html.twig create mode 100644 templates/security/login.html.twig diff --git a/.env b/.env index 3a8e236..9ca30d9 100644 --- a/.env +++ b/.env @@ -37,5 +37,5 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/messenger ### ###> symfony/mailer ### -# MAILER_DSN=null://null +MAILER_DSN=null://null ###< symfony/mailer ### diff --git a/composer.json b/composer.json index b7dd080..c5a6576 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "symfony/validator": "7.1.*", "symfony/web-link": "7.1.*", "symfony/yaml": "7.1.*", + "symfonycasts/verify-email-bundle": "^1.17", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index 465f63c..fa59f9f 100644 --- a/composer.lock +++ b/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": "6a957fcd0a6de48ba22ea99ed82f113f", + "content-hash": "fa6b415ac11ece0fbfc562be9a5d1df3", "packages": [ { "name": "composer/semver", @@ -7342,6 +7342,52 @@ ], "time": "2024-08-12T09:59:40+00:00" }, + { + "name": "symfonycasts/verify-email-bundle", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/f72af149070b39ef82a7095074378d0a98b4d2ef", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "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", + "symfony/routing": "^5.4 | ^6.0 | ^7.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "doctrine/persistence": "^2.0", + "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0", + "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SymfonyCasts\\Bundle\\VerifyEmail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple, stylish Email Verification for Symfony", + "support": { + "issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues", + "source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.17.0" + }, + "time": "2024-03-17T02:29:53+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.13.0", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..cf72b69 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index dc7ca17..07dc7df 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,7 @@ security: # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + Symfony\Component\Security\Core\Employee: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: # used to reload user from session & other features (e.g. switch_user) @@ -9,6 +9,7 @@ security: entity: class: App\Entity\Employee property: email + # used to reload user from session & other features (e.g. switch_user) firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -16,6 +17,19 @@ security: main: lazy: true provider: app_user_provider + custom_authenticator: App\Security\LoginFormAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + # by default, the feature is enabled by checking a checkbox in the + # login form, uncomment the following line to always enable it. + #always_remember_me: true # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -36,7 +50,7 @@ when@test: # important to generate secure password hashes. In tests however, secure hashes # are not important, waste resources and increase test times. The following # reduces the work factor to the lowest possible values. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + Symfony\Component\Security\Core\Employee\PasswordAuthenticatedUserInterface: algorithm: auto cost: 4 # Lowest possible value for bcrypt time_cost: 3 # Lowest possible value for argon diff --git a/migrations/Version20241017154551.php b/migrations/Version20241017154551.php new file mode 100644 index 0000000..56466b4 --- /dev/null +++ b/migrations/Version20241017154551.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/migrations/Version20241017154952.php b/migrations/Version20241017154952.php new file mode 100644 index 0000000..25732e6 --- /dev/null +++ b/migrations/Version20241017154952.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, is_verified BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/migrations/Version20241114142616.php b/migrations/Version20241114142616.php new file mode 100644 index 0000000..a8d7019 --- /dev/null +++ b/migrations/Version20241114142616.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, is_verified BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index e93d322..594ec91 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -5,13 +5,14 @@ namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; -#[Route('/dashboard', name: 'DashboardController')] class DashboardController extends AbstractController { - #[Route('', name: '_index')] + #[Route(path: '/dashboard', name: 'app_dashboard')] public function index(): Response { return $this->render('dashboard/index.html.twig'); } -} + +} \ No newline at end of file diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..6548727 --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,86 @@ +createForm(RegistrationFormType::class, $employee); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + + + // encode the plain password + $employee->setPassword( + password_hash($form->get('plainPassword')->getData(), PASSWORD_BCRYPT) + ); + + $entityManager->persist($employee); + $entityManager->flush(); + + // generate a signed url and email it to the employee + $this->emailVerifier->sendEmailConfirmation('app_verify_email', $employee, + (new TemplatedEmail()) + ->from(new Address('no-reply@HegreLand.com', 'Hegre Land')) + ->to((string) $employee->getEmail()) + ->subject('Please Confirm your Email') + ->htmlTemplate('registration/confirmation_email.html.twig') + ); + + // do anything else you need here, like send an email + + return $security->login($employee, LoginFormAuthenticator::class, 'main'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form, + ]); + } + + #[Route('/verify/email', name: 'app_verify_email')] + public function verifyUserEmail(Request $request, TranslatorInterface $translator): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // validate email confirmation link, sets User::isVerified=true and persists + try { + /** @var Employee $employee */ + $employee = $this->getUser(); + $this->emailVerifier->handleEmailConfirmation($request, $employee); + } catch (VerifyEmailExceptionInterface $exception) { + $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle')); + + return $this->redirectToRoute('DashboardController'); + } + + // @TODO Change the redirect on success and handle or remove the flash message in your templates + $this->addFlash('success', 'Your email address has been verified.'); + + return $this->redirectToRoute('app_register'); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..9453eae --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,33 @@ +getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'app_logout')] + 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/src/Form/LoginType.php b/src/Form/LoginType.php new file mode 100644 index 0000000..cbb4ef1 --- /dev/null +++ b/src/Form/LoginType.php @@ -0,0 +1,28 @@ +add('email', EmailType::class) + ->add('password', PasswordType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Employee::class, + ]); + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..352c917 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,64 @@ +add('email', EmailType::class, ['label' => 'Email Address']) + ->add('firstName', TextType::class, ['label' => 'First Name']) + ->add('lastName', TextType::class, ['label' => 'Last Name']) + ->add('roles', ChoiceType::class, [ + 'label' => 'Roles (comma-separated)', + 'required' => false, + 'choices' => [ + 'User' => 'ROLE_USER', + 'Admin' => 'ROLE_ADMIN', + ], + 'multiple' => true, // Allow multiple selections + 'expanded' => true, // Render as checkboxes + ]) + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + '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, + ]), + ], + ]) + ->add('save', SubmitType::class, ['label' => 'Add Employee']) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Employee::class, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php new file mode 100644 index 0000000..3c0cac9 --- /dev/null +++ b/src/Security/EmailVerifier.php @@ -0,0 +1,52 @@ +verifyEmailHelper->generateSignature( + $verifyEmailRouteName, + (string) $employee->getId(), + (string) $employee->getEmail() + ); + + $context = $email->getContext(); + $context['signedUrl'] = $signatureComponents->getSignedUrl(); + $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); + $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); + + $email->context($context); + + } + + /** + * @throws VerifyEmailExceptionInterface + */ + public function handleEmailConfirmation(Request $request, Employee $employee): void + { + $this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $employee->getId(), (string) $employee->getEmail()); + + $employee->setVerified(true); + + $this->entityManager->persist($employee); + $this->entityManager->flush(); + } +} diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php new file mode 100644 index 0000000..1d26146 --- /dev/null +++ b/src/Security/LoginFormAuthenticator.php @@ -0,0 +1,60 @@ +getPayload()->getString('email'); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email); + + return new Passport( + new UserBadge($email), + new PasswordCredentials($request->getPayload()->getString('password')), + [ + new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), + new RememberMeBadge(), + ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + // For example: + return new RedirectResponse($this->urlGenerator->generate('app_dashboard')); + //return new RedirectResponse($this->urlGenerator->generate('DashboardController')); + } + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/symfony.lock b/symfony.lock index d7fbb3c..23a0c64 100644 --- a/symfony.lock +++ b/symfony.lock @@ -288,6 +288,9 @@ "config/packages/messenger.yaml" ] }, + "symfonycasts/verify-email-bundle": { + "version": "v1.17.0" + }, "twig/extra-bundle": { "version": "v3.13.0" } diff --git a/templates/registration/confirmation_email.html.twig b/templates/registration/confirmation_email.html.twig new file mode 100644 index 0000000..7c79d8a --- /dev/null +++ b/templates/registration/confirmation_email.html.twig @@ -0,0 +1,11 @@ +

Hi! Please confirm your email!

+ +

+ Please confirm your email address by clicking the following link:

+ Confirm my Email. + This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. +

+ +

+ Cheers! +

diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..2bf8f16 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,32 @@ +{% extends 'base.html.twig' %} + +{% block title %}HegreLand{% endblock %} + +{% block body %} + {% for flash_error in app.flashes('verify_email_error') %} + + {% endfor %} + +

M'inscrire

+ + {{ form_errors(registrationForm) }} + + {{ form_start(registrationForm) }} +
+ {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.firstName) }} + {{ form_row(registrationForm.lastName) }} + {{ form_row(registrationForm.roles) }} +
+
+ {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + +
+ + +

Déjà inscrit(e) ? Me connecter

+ + {{ form_end(registrationForm) }} +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..70ddce5 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,26 @@ +{% extends 'base.html.twig' %} + +{% block title %}HegreLand{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ Vous êtes déjà connecté(e) en tant que {{ app.user.userIdentifier }}, Me déconnecter +
+ {% endif %} + + +

Me connecter

+ {{ form_row(loginForm.email) }} + +

Mot de passe oublié

+

Pas encore inscrit(e) ? M'inscrire

+
+{% endblock %} From 3a8d60fddbb0e24d7106ac4436751e28605cff78 Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 17 Oct 2024 16:37:01 +0200 Subject: [PATCH 4/9] Rebase develop --- assets/styles/style.css | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 assets/styles/style.css diff --git a/assets/styles/style.css b/assets/styles/style.css new file mode 100644 index 0000000..4f52014 --- /dev/null +++ b/assets/styles/style.css @@ -0,0 +1,76 @@ +body { + background: linear-gradient(to right, #5CE1E6, #C1FF72, #cb6ce6); +} + +h1 { + text-align: center; + font-family: "Fredoka", sans-serif; + font-optical-sizing: auto; + font-style: normal; + font-variation-settings: "wdth" 100; +} + +img { + width: 60%; + height: auto; + float: right; + margin: 0; +} + +.container { + display: flex; + justify-content: center; + align-items: center; + height: auto; + margin: auto 0 auto; +} + +form { + margin-right: 20px; + background: rgba(255, 255, 255, 0.15); + border-radius: 16px; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + justify-content: center; + align-items: center; + padding: 50px; +} + +form div { + margin-bottom: 10px; +} + +.logo{ + margin-bottom: 65%; + margin-right: 30%; +} + +label { + display: block; + margin-bottom: 5px; + font-family: "Fredoka", sans-serif; + font-style: normal; +} + +input[type="text"], +input[type="password"] { + width: 90%; + padding: 5px; +} + +button { + padding: 10px 20px; + background-color: #5CE1E6; + color: black; + border: none; + cursor: pointer; +} + +button:hover { + background-color: #CB6CE6; +} + +.submit { + margin-left: 15%; +} \ No newline at end of file From f71e0027eb557f785d7c0eea0cc48cd01a32e4d2 Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 17 Oct 2024 17:17:21 +0200 Subject: [PATCH 5/9] MissionCategory.php --- src/Entity/MissionCategory.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Entity/MissionCategory.php b/src/Entity/MissionCategory.php index 3081dc2..862d5a8 100644 --- a/src/Entity/MissionCategory.php +++ b/src/Entity/MissionCategory.php @@ -6,17 +6,16 @@ use App\Repository\MissionCategoryRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MissionCategoryRepository::class)] -#[ORM\UniqueConstraint(columns: ['mission', 'category'])] class MissionCategory { #[ORM\Id] - #[ORM\ManyToOne(targetEntity: Mission::class, inversedBy: 'missionCategories')] - #[ORM\Column(type: 'integer')] + #[ORM\OneToMany(targetEntity: Mission::class, mappedBy: 'MissionCategory')] + #[ORM\JoinColumn(nullable: false)] private ?Mission $mission = null; #[ORM\Id] - #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'missionCategories')] - #[ORM\Column(type: 'integer')] + #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'MissionCategory')] + #[ORM\JoinColumn(nullable: false)] private ?Category $category = null; public function getMission(): ?Mission From c60aa4c43a3862815331bb9761d1dffb3b22d7f7 Mon Sep 17 00:00:00 2001 From: besbota Date: Thu, 28 Nov 2024 14:28:32 +0100 Subject: [PATCH 6/9] Form login, WIP --- .env | 2 +- composer.json | 1 + composer.lock | 48 ++++++++++- config/bundles.php | 1 + config/packages/security.yaml | 18 +++- migrations/Version20241017154551.php | 49 +++++++++++ migrations/Version20241017154952.php | 49 +++++++++++ migrations/Version20241114142616.php | 49 +++++++++++ src/Controller/DashboardController.php | 2 - src/Controller/RegistrationController.php | 86 +++++++++++++++++++ src/Controller/SecurityController.php | 33 +++++++ src/Form/LoginType.php | 28 ++++++ src/Form/RegistrationFormType.php | 64 ++++++++++++++ src/Repository/UserRepository.php | 60 +++++++++++++ src/Security/EmailVerifier.php | 52 +++++++++++ src/Security/LoginFormAuthenticator.php | 60 +++++++++++++ symfony.lock | 3 + .../registration/confirmation_email.html.twig | 11 +++ templates/registration/register.html.twig | 32 +++++++ templates/security/login.html.twig | 26 ++++++ 20 files changed, 668 insertions(+), 6 deletions(-) create mode 100644 migrations/Version20241017154551.php create mode 100644 migrations/Version20241017154952.php create mode 100644 migrations/Version20241114142616.php create mode 100644 src/Controller/RegistrationController.php create mode 100644 src/Controller/SecurityController.php create mode 100644 src/Form/LoginType.php create mode 100644 src/Form/RegistrationFormType.php create mode 100644 src/Repository/UserRepository.php create mode 100644 src/Security/EmailVerifier.php create mode 100644 src/Security/LoginFormAuthenticator.php create mode 100644 templates/registration/confirmation_email.html.twig create mode 100644 templates/registration/register.html.twig create mode 100644 templates/security/login.html.twig diff --git a/.env b/.env index 3a8e236..9ca30d9 100644 --- a/.env +++ b/.env @@ -37,5 +37,5 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###< symfony/messenger ### ###> symfony/mailer ### -# MAILER_DSN=null://null +MAILER_DSN=null://null ###< symfony/mailer ### diff --git a/composer.json b/composer.json index b7dd080..c5a6576 100644 --- a/composer.json +++ b/composer.json @@ -42,6 +42,7 @@ "symfony/validator": "7.1.*", "symfony/web-link": "7.1.*", "symfony/yaml": "7.1.*", + "symfonycasts/verify-email-bundle": "^1.17", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index 465f63c..fa59f9f 100644 --- a/composer.lock +++ b/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": "6a957fcd0a6de48ba22ea99ed82f113f", + "content-hash": "fa6b415ac11ece0fbfc562be9a5d1df3", "packages": [ { "name": "composer/semver", @@ -7342,6 +7342,52 @@ ], "time": "2024-08-12T09:59:40+00:00" }, + { + "name": "symfonycasts/verify-email-bundle", + "version": "v1.17.0", + "source": { + "type": "git", + "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/f72af149070b39ef82a7095074378d0a98b4d2ef", + "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=8.1", + "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", + "symfony/routing": "^5.4 | ^6.0 | ^7.0" + }, + "require-dev": { + "doctrine/orm": "^2.7", + "doctrine/persistence": "^2.0", + "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0", + "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "SymfonyCasts\\Bundle\\VerifyEmail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Simple, stylish Email Verification for Symfony", + "support": { + "issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues", + "source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.17.0" + }, + "time": "2024-03-17T02:29:53+00:00" + }, { "name": "twig/extra-bundle", "version": "v3.13.0", diff --git a/config/bundles.php b/config/bundles.php index 4e3a560..cf72b69 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,4 +13,5 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 9c8d719..0f5af18 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,7 @@ security: # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + Symfony\Component\Security\Core\Employee: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: # used to reload user from session & other features (e.g. switch_user) @@ -9,6 +9,7 @@ security: entity: class: App\Entity\Employee property: email + # used to reload user from session & other features (e.g. switch_user) firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ @@ -16,6 +17,19 @@ security: main: lazy: true provider: app_user_provider + custom_authenticator: App\Security\LoginFormAuthenticator + logout: + path: app_logout + # where to redirect after logout + # target: app_any_route + + remember_me: + secret: '%kernel.secret%' + lifetime: 604800 + path: / + # by default, the feature is enabled by checking a checkbox in the + # login form, uncomment the following line to always enable it. + #always_remember_me: true # activate different ways to authenticate # https://symfony.com/doc/current/security.html#the-firewall @@ -35,7 +49,7 @@ when@test: # important to generate secure password hashes. In tests however, secure hashes # are not important, waste resources and increase test times. The following # reduces the work factor to the lowest possible values. - Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + Symfony\Component\Security\Core\Employee\PasswordAuthenticatedUserInterface: algorithm: auto cost: 4 # Lowest possible value for bcrypt time_cost: 3 # Lowest possible value for argon diff --git a/migrations/Version20241017154551.php b/migrations/Version20241017154551.php new file mode 100644 index 0000000..56466b4 --- /dev/null +++ b/migrations/Version20241017154551.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/migrations/Version20241017154952.php b/migrations/Version20241017154952.php new file mode 100644 index 0000000..25732e6 --- /dev/null +++ b/migrations/Version20241017154952.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, is_verified BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/migrations/Version20241114142616.php b/migrations/Version20241114142616.php new file mode 100644 index 0000000..a8d7019 --- /dev/null +++ b/migrations/Version20241114142616.php @@ -0,0 +1,49 @@ +addSql('CREATE SEQUENCE category_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE employee_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE incident_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE mission_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE ride_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE skill_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE "user_id_seq" INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE "user" (id INT NOT NULL, email VARCHAR(180) NOT NULL, roles JSON NOT NULL, password VARCHAR(255) NOT NULL, is_verified BOOLEAN NOT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_EMAIL ON "user" (email)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('DROP SEQUENCE category_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE employee_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE incident_type_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE mission_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE ride_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE skill_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE "user_id_seq" CASCADE'); + $this->addSql('DROP TABLE "user"'); + } +} diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index dead06a..e93d322 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -14,6 +14,4 @@ class DashboardController extends AbstractController { return $this->render('dashboard/index.html.twig'); } - - } diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php new file mode 100644 index 0000000..6548727 --- /dev/null +++ b/src/Controller/RegistrationController.php @@ -0,0 +1,86 @@ +createForm(RegistrationFormType::class, $employee); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + + + + // encode the plain password + $employee->setPassword( + password_hash($form->get('plainPassword')->getData(), PASSWORD_BCRYPT) + ); + + $entityManager->persist($employee); + $entityManager->flush(); + + // generate a signed url and email it to the employee + $this->emailVerifier->sendEmailConfirmation('app_verify_email', $employee, + (new TemplatedEmail()) + ->from(new Address('no-reply@HegreLand.com', 'Hegre Land')) + ->to((string) $employee->getEmail()) + ->subject('Please Confirm your Email') + ->htmlTemplate('registration/confirmation_email.html.twig') + ); + + // do anything else you need here, like send an email + + return $security->login($employee, LoginFormAuthenticator::class, 'main'); + } + + return $this->render('registration/register.html.twig', [ + 'registrationForm' => $form, + ]); + } + + #[Route('/verify/email', name: 'app_verify_email')] + public function verifyUserEmail(Request $request, TranslatorInterface $translator): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); + + // validate email confirmation link, sets User::isVerified=true and persists + try { + /** @var Employee $employee */ + $employee = $this->getUser(); + $this->emailVerifier->handleEmailConfirmation($request, $employee); + } catch (VerifyEmailExceptionInterface $exception) { + $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle')); + + return $this->redirectToRoute('DashboardController'); + } + + // @TODO Change the redirect on success and handle or remove the flash message in your templates + $this->addFlash('success', 'Your email address has been verified.'); + + return $this->redirectToRoute('app_register'); + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..9453eae --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,33 @@ +getUser()) { + // return $this->redirectToRoute('target_path'); + // } + + // get the login error if there is one + $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[Route(path: '/logout', name: 'app_logout')] + 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/src/Form/LoginType.php b/src/Form/LoginType.php new file mode 100644 index 0000000..cbb4ef1 --- /dev/null +++ b/src/Form/LoginType.php @@ -0,0 +1,28 @@ +add('email', EmailType::class) + ->add('password', PasswordType::class) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Employee::class, + ]); + } +} diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php new file mode 100644 index 0000000..352c917 --- /dev/null +++ b/src/Form/RegistrationFormType.php @@ -0,0 +1,64 @@ +add('email', EmailType::class, ['label' => 'Email Address']) + ->add('firstName', TextType::class, ['label' => 'First Name']) + ->add('lastName', TextType::class, ['label' => 'Last Name']) + ->add('roles', ChoiceType::class, [ + 'label' => 'Roles (comma-separated)', + 'required' => false, + 'choices' => [ + 'User' => 'ROLE_USER', + 'Admin' => 'ROLE_ADMIN', + ], + 'multiple' => true, // Allow multiple selections + 'expanded' => true, // Render as checkboxes + ]) + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + '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, + ]), + ], + ]) + ->add('save', SubmitType::class, ['label' => 'Add Employee']) + ; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_class' => Employee::class, + ]); + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..4f2804e --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,60 @@ + + */ +class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + /** + * Used to upgrade (rehash) the user's password automatically over time. + */ + public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); + } + + $user->setPassword($newHashedPassword); + $this->getEntityManager()->persist($user); + $this->getEntityManager()->flush(); + } + + // /** + // * @return User[] Returns an array of User objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?User + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php new file mode 100644 index 0000000..3c0cac9 --- /dev/null +++ b/src/Security/EmailVerifier.php @@ -0,0 +1,52 @@ +verifyEmailHelper->generateSignature( + $verifyEmailRouteName, + (string) $employee->getId(), + (string) $employee->getEmail() + ); + + $context = $email->getContext(); + $context['signedUrl'] = $signatureComponents->getSignedUrl(); + $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); + $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); + + $email->context($context); + + } + + /** + * @throws VerifyEmailExceptionInterface + */ + public function handleEmailConfirmation(Request $request, Employee $employee): void + { + $this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $employee->getId(), (string) $employee->getEmail()); + + $employee->setVerified(true); + + $this->entityManager->persist($employee); + $this->entityManager->flush(); + } +} diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php new file mode 100644 index 0000000..1d26146 --- /dev/null +++ b/src/Security/LoginFormAuthenticator.php @@ -0,0 +1,60 @@ +getPayload()->getString('email'); + + $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email); + + return new Passport( + new UserBadge($email), + new PasswordCredentials($request->getPayload()->getString('password')), + [ + new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), + new RememberMeBadge(), + ] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + // For example: + return new RedirectResponse($this->urlGenerator->generate('app_dashboard')); + //return new RedirectResponse($this->urlGenerator->generate('DashboardController')); + } + + protected function getLoginUrl(Request $request): string + { + return $this->urlGenerator->generate(self::LOGIN_ROUTE); + } +} diff --git a/symfony.lock b/symfony.lock index d7fbb3c..23a0c64 100644 --- a/symfony.lock +++ b/symfony.lock @@ -288,6 +288,9 @@ "config/packages/messenger.yaml" ] }, + "symfonycasts/verify-email-bundle": { + "version": "v1.17.0" + }, "twig/extra-bundle": { "version": "v3.13.0" } diff --git a/templates/registration/confirmation_email.html.twig b/templates/registration/confirmation_email.html.twig new file mode 100644 index 0000000..7c79d8a --- /dev/null +++ b/templates/registration/confirmation_email.html.twig @@ -0,0 +1,11 @@ +

Hi! Please confirm your email!

+ +

+ Please confirm your email address by clicking the following link:

+ Confirm my Email. + This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. +

+ +

+ Cheers! +

diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig new file mode 100644 index 0000000..2bf8f16 --- /dev/null +++ b/templates/registration/register.html.twig @@ -0,0 +1,32 @@ +{% extends 'base.html.twig' %} + +{% block title %}HegreLand{% endblock %} + +{% block body %} + {% for flash_error in app.flashes('verify_email_error') %} + + {% endfor %} + +

M'inscrire

+ + {{ form_errors(registrationForm) }} + + {{ form_start(registrationForm) }} +
+ {{ form_row(registrationForm.email) }} + {{ form_row(registrationForm.firstName) }} + {{ form_row(registrationForm.lastName) }} + {{ form_row(registrationForm.roles) }} +
+
+ {{ form_row(registrationForm.plainPassword, { + label: 'Password' + }) }} + +
+ + +

Déjà inscrit(e) ? Me connecter

+ + {{ form_end(registrationForm) }} +{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..70ddce5 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,26 @@ +{% extends 'base.html.twig' %} + +{% block title %}HegreLand{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% if app.user %} +
+ Vous êtes déjà connecté(e) en tant que {{ app.user.userIdentifier }}, Me déconnecter +
+ {% endif %} + + +

Me connecter

+ {{ form_row(loginForm.email) }} + +

Mot de passe oublié

+

Pas encore inscrit(e) ? M'inscrire

+
+{% endblock %} From 3ab2ef5a9e0ddd8d6504e5ea4e7d298826ae5437 Mon Sep 17 00:00:00 2001 From: ASTIER Yann Date: Thu, 28 Nov 2024 15:00:06 +0100 Subject: [PATCH 7/9] login WIP astier --- composer.json | 1 - composer.lock | 48 +---------- config/bundles.php | 1 - src/Controller/EmployeeController.php | 5 ++ src/Controller/RegistrationController.php | 86 ------------------- src/Controller/SecurityController.php | 3 +- src/Form/EmployeeType.php | 21 ++++- src/Form/RegistrationFormType.php | 64 -------------- src/Repository/UserRepository.php | 60 ------------- src/Security/EmailVerifier.php | 52 ----------- .../registration/confirmation_email.html.twig | 11 --- templates/registration/register.html.twig | 32 ------- templates/security/login.html.twig | 1 - 13 files changed, 27 insertions(+), 358 deletions(-) delete mode 100644 src/Controller/RegistrationController.php delete mode 100644 src/Form/RegistrationFormType.php delete mode 100644 src/Repository/UserRepository.php delete mode 100644 src/Security/EmailVerifier.php delete mode 100644 templates/registration/confirmation_email.html.twig delete mode 100644 templates/registration/register.html.twig diff --git a/composer.json b/composer.json index c5a6576..b7dd080 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,6 @@ "symfony/validator": "7.1.*", "symfony/web-link": "7.1.*", "symfony/yaml": "7.1.*", - "symfonycasts/verify-email-bundle": "^1.17", "twig/extra-bundle": "^2.12|^3.0", "twig/twig": "^2.12|^3.0" }, diff --git a/composer.lock b/composer.lock index fa59f9f..465f63c 100644 --- a/composer.lock +++ b/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": "fa6b415ac11ece0fbfc562be9a5d1df3", + "content-hash": "6a957fcd0a6de48ba22ea99ed82f113f", "packages": [ { "name": "composer/semver", @@ -7342,52 +7342,6 @@ ], "time": "2024-08-12T09:59:40+00:00" }, - { - "name": "symfonycasts/verify-email-bundle", - "version": "v1.17.0", - "source": { - "type": "git", - "url": "https://github.com/SymfonyCasts/verify-email-bundle.git", - "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/SymfonyCasts/verify-email-bundle/zipball/f72af149070b39ef82a7095074378d0a98b4d2ef", - "reference": "f72af149070b39ef82a7095074378d0a98b4d2ef", - "shasum": "" - }, - "require": { - "ext-json": "*", - "php": ">=8.1", - "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", - "symfony/routing": "^5.4 | ^6.0 | ^7.0" - }, - "require-dev": { - "doctrine/orm": "^2.7", - "doctrine/persistence": "^2.0", - "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0", - "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0" - }, - "type": "symfony-bundle", - "autoload": { - "psr-4": { - "SymfonyCasts\\Bundle\\VerifyEmail\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Simple, stylish Email Verification for Symfony", - "support": { - "issues": "https://github.com/SymfonyCasts/verify-email-bundle/issues", - "source": "https://github.com/SymfonyCasts/verify-email-bundle/tree/v1.17.0" - }, - "time": "2024-03-17T02:29:53+00:00" - }, { "name": "twig/extra-bundle", "version": "v3.13.0", diff --git a/config/bundles.php b/config/bundles.php index cf72b69..4e3a560 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -13,5 +13,4 @@ return [ Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], - SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true], ]; diff --git a/src/Controller/EmployeeController.php b/src/Controller/EmployeeController.php index 3eee805..1b17860 100644 --- a/src/Controller/EmployeeController.php +++ b/src/Controller/EmployeeController.php @@ -30,6 +30,11 @@ final class EmployeeController extends AbstractController $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { + + $employee->setPassword( + password_hash($form->get('plainPassword')->getData(), PASSWORD_BCRYPT) + ); + $entityManager->persist($employee); $entityManager->flush(); diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php deleted file mode 100644 index 6548727..0000000 --- a/src/Controller/RegistrationController.php +++ /dev/null @@ -1,86 +0,0 @@ -createForm(RegistrationFormType::class, $employee); - $form->handleRequest($request); - - if ($form->isSubmitted() && $form->isValid()) { - - - - // encode the plain password - $employee->setPassword( - password_hash($form->get('plainPassword')->getData(), PASSWORD_BCRYPT) - ); - - $entityManager->persist($employee); - $entityManager->flush(); - - // generate a signed url and email it to the employee - $this->emailVerifier->sendEmailConfirmation('app_verify_email', $employee, - (new TemplatedEmail()) - ->from(new Address('no-reply@HegreLand.com', 'Hegre Land')) - ->to((string) $employee->getEmail()) - ->subject('Please Confirm your Email') - ->htmlTemplate('registration/confirmation_email.html.twig') - ); - - // do anything else you need here, like send an email - - return $security->login($employee, LoginFormAuthenticator::class, 'main'); - } - - return $this->render('registration/register.html.twig', [ - 'registrationForm' => $form, - ]); - } - - #[Route('/verify/email', name: 'app_verify_email')] - public function verifyUserEmail(Request $request, TranslatorInterface $translator): Response - { - $this->denyAccessUnlessGranted('IS_AUTHENTICATED_FULLY'); - - // validate email confirmation link, sets User::isVerified=true and persists - try { - /** @var Employee $employee */ - $employee = $this->getUser(); - $this->emailVerifier->handleEmailConfirmation($request, $employee); - } catch (VerifyEmailExceptionInterface $exception) { - $this->addFlash('verify_email_error', $translator->trans($exception->getReason(), [], 'VerifyEmailBundle')); - - return $this->redirectToRoute('DashboardController'); - } - - // @TODO Change the redirect on success and handle or remove the flash message in your templates - $this->addFlash('success', 'Your email address has been verified.'); - - return $this->redirectToRoute('app_register'); - } -} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 9453eae..664f476 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -7,9 +7,10 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; +#[Route(path: '/login', name: 'security')] class SecurityController extends AbstractController { - #[Route(path: '/login', name: 'app_login')] + #[Route(path: '', name: '_login')] public function login(AuthenticationUtils $authenticationUtils): Response { // if ($this->getUser()) { diff --git a/src/Form/EmployeeType.php b/src/Form/EmployeeType.php index 9a0048f..a2c0c35 100644 --- a/src/Form/EmployeeType.php +++ b/src/Form/EmployeeType.php @@ -10,6 +10,8 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Constraints\NotBlank; class EmployeeType extends AbstractType { @@ -19,7 +21,6 @@ class EmployeeType extends AbstractType ->add('email', EmailType::class, ['label' => 'Email Address']) ->add('firstName', TextType::class, ['label' => 'First Name']) ->add('lastName', TextType::class, ['label' => 'Last Name']) - ->add('password', PasswordType::class, ['label' => 'Password']) ->add('roles', ChoiceType::class, [ 'label' => 'Roles (comma-separated)', 'required' => false, @@ -30,7 +31,23 @@ class EmployeeType extends AbstractType 'multiple' => true, // Allow multiple selections 'expanded' => true, // Render as checkboxes ]) - ; + ->add('plainPassword', PasswordType::class, [ + // instead of being set onto the object directly, + // this is read and encoded in the controller + 'mapped' => false, + '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, + ]), + ], + ]); } public function configureOptions(OptionsResolver $resolver): void diff --git a/src/Form/RegistrationFormType.php b/src/Form/RegistrationFormType.php deleted file mode 100644 index 352c917..0000000 --- a/src/Form/RegistrationFormType.php +++ /dev/null @@ -1,64 +0,0 @@ -add('email', EmailType::class, ['label' => 'Email Address']) - ->add('firstName', TextType::class, ['label' => 'First Name']) - ->add('lastName', TextType::class, ['label' => 'Last Name']) - ->add('roles', ChoiceType::class, [ - 'label' => 'Roles (comma-separated)', - 'required' => false, - 'choices' => [ - 'User' => 'ROLE_USER', - 'Admin' => 'ROLE_ADMIN', - ], - 'multiple' => true, // Allow multiple selections - 'expanded' => true, // Render as checkboxes - ]) - ->add('plainPassword', PasswordType::class, [ - // instead of being set onto the object directly, - // this is read and encoded in the controller - 'mapped' => false, - '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, - ]), - ], - ]) - ->add('save', SubmitType::class, ['label' => 'Add Employee']) - ; - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_class' => Employee::class, - ]); - } -} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php deleted file mode 100644 index 4f2804e..0000000 --- a/src/Repository/UserRepository.php +++ /dev/null @@ -1,60 +0,0 @@ - - */ -class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, User::class); - } - - /** - * Used to upgrade (rehash) the user's password automatically over time. - */ - public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void - { - if (!$user instanceof User) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', $user::class)); - } - - $user->setPassword($newHashedPassword); - $this->getEntityManager()->persist($user); - $this->getEntityManager()->flush(); - } - - // /** - // * @return User[] Returns an array of User objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('u') - // ->andWhere('u.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('u.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?User - // { - // return $this->createQueryBuilder('u') - // ->andWhere('u.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } -} diff --git a/src/Security/EmailVerifier.php b/src/Security/EmailVerifier.php deleted file mode 100644 index 3c0cac9..0000000 --- a/src/Security/EmailVerifier.php +++ /dev/null @@ -1,52 +0,0 @@ -verifyEmailHelper->generateSignature( - $verifyEmailRouteName, - (string) $employee->getId(), - (string) $employee->getEmail() - ); - - $context = $email->getContext(); - $context['signedUrl'] = $signatureComponents->getSignedUrl(); - $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey(); - $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData(); - - $email->context($context); - - } - - /** - * @throws VerifyEmailExceptionInterface - */ - public function handleEmailConfirmation(Request $request, Employee $employee): void - { - $this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $employee->getId(), (string) $employee->getEmail()); - - $employee->setVerified(true); - - $this->entityManager->persist($employee); - $this->entityManager->flush(); - } -} diff --git a/templates/registration/confirmation_email.html.twig b/templates/registration/confirmation_email.html.twig deleted file mode 100644 index 7c79d8a..0000000 --- a/templates/registration/confirmation_email.html.twig +++ /dev/null @@ -1,11 +0,0 @@ -

Hi! Please confirm your email!

- -

- Please confirm your email address by clicking the following link:

- Confirm my Email. - This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}. -

- -

- Cheers! -

diff --git a/templates/registration/register.html.twig b/templates/registration/register.html.twig deleted file mode 100644 index 2bf8f16..0000000 --- a/templates/registration/register.html.twig +++ /dev/null @@ -1,32 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block title %}HegreLand{% endblock %} - -{% block body %} - {% for flash_error in app.flashes('verify_email_error') %} - - {% endfor %} - -

M'inscrire

- - {{ form_errors(registrationForm) }} - - {{ form_start(registrationForm) }} -
- {{ form_row(registrationForm.email) }} - {{ form_row(registrationForm.firstName) }} - {{ form_row(registrationForm.lastName) }} - {{ form_row(registrationForm.roles) }} -
-
- {{ form_row(registrationForm.plainPassword, { - label: 'Password' - }) }} - -
- - -

Déjà inscrit(e) ? Me connecter

- - {{ form_end(registrationForm) }} -{% endblock %} diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 70ddce5..89379d3 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -21,6 +21,5 @@ Se connecter

Mot de passe oublié

-

Pas encore inscrit(e) ? M'inscrire

{% endblock %} From f38c5e43506241118bbc5def9d9e1de72895bdae Mon Sep 17 00:00:00 2001 From: ASTIER Yann Date: Thu, 28 Nov 2024 16:46:35 +0100 Subject: [PATCH 8/9] login finished. --- config/packages/security.yaml | 4 +-- src/Controller/DashboardController.php | 2 +- src/Controller/SecurityController.php | 11 +++--- src/Entity/MissionCategory.php | 9 ++--- src/Security/LoginFormAuthenticator.php | 12 +++---- templates/security/login.html.twig | 48 ++++++++++++++++--------- 6 files changed, 53 insertions(+), 33 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 0f5af18..a8f9810 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,7 @@ security: # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: - Symfony\Component\Security\Core\Employee: 'auto' + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: # used to reload user from session & other features (e.g. switch_user) @@ -40,7 +40,7 @@ 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: ^/*, roles: ROLE_USER } + - { path: ^/*, roles: ROLE_USER } when@test: security: diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index 594ec91..bba229c 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -9,7 +9,7 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class DashboardController extends AbstractController { - #[Route(path: '/dashboard', name: 'app_dashboard')] + #[Route(path: '/dashboard', name: 'dashboard')] public function index(): Response { return $this->render('dashboard/index.html.twig'); diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index 664f476..4e4de2f 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -2,7 +2,9 @@ namespace App\Controller; +use App\Form\LoginType; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; @@ -13,12 +15,13 @@ class SecurityController extends AbstractController #[Route(path: '', name: '_login')] public function login(AuthenticationUtils $authenticationUtils): Response { - // if ($this->getUser()) { - // return $this->redirectToRoute('target_path'); - // } +// if ($this->getUser()) { +// return $this->redirectToRoute('dashboard'); +// } // get the login error if there is one $error = $authenticationUtils->getLastAuthenticationError(); + // last username entered by the user $lastUsername = $authenticationUtils->getLastUsername(); @@ -26,7 +29,7 @@ class SecurityController extends AbstractController return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); } - #[Route(path: '/logout', name: 'app_logout')] + #[Route(path: '/logout', name: '_logout')] 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/src/Entity/MissionCategory.php b/src/Entity/MissionCategory.php index 862d5a8..3081dc2 100644 --- a/src/Entity/MissionCategory.php +++ b/src/Entity/MissionCategory.php @@ -6,16 +6,17 @@ use App\Repository\MissionCategoryRepository; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: MissionCategoryRepository::class)] +#[ORM\UniqueConstraint(columns: ['mission', 'category'])] class MissionCategory { #[ORM\Id] - #[ORM\OneToMany(targetEntity: Mission::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Mission::class, inversedBy: 'missionCategories')] + #[ORM\Column(type: 'integer')] private ?Mission $mission = null; #[ORM\Id] - #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'MissionCategory')] - #[ORM\JoinColumn(nullable: false)] + #[ORM\ManyToOne(targetEntity: Category::class, inversedBy: 'missionCategories')] + #[ORM\Column(type: 'integer')] private ?Category $category = null; public function getMission(): ?Mission diff --git a/src/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php index 1d26146..1ea3330 100644 --- a/src/Security/LoginFormAuthenticator.php +++ b/src/Security/LoginFormAuthenticator.php @@ -20,7 +20,7 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator { use TargetPathTrait; - public const LOGIN_ROUTE = 'app_login'; + public const LOGIN_ROUTE = 'security_login'; public function __construct(private UrlGeneratorInterface $urlGenerator) { @@ -28,13 +28,13 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator public function authenticate(Request $request): Passport { - $email = $request->getPayload()->getString('email'); + $email = $request->getPayload()->getString('_username'); $request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $email); return new Passport( new UserBadge($email), - new PasswordCredentials($request->getPayload()->getString('password')), + new PasswordCredentials($request->getPayload()->getString('_password')), [ new CsrfTokenBadge('authenticate', $request->getPayload()->getString('_csrf_token')), new RememberMeBadge(), @@ -44,12 +44,12 @@ class LoginFormAuthenticator extends AbstractLoginFormAuthenticator public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { - if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + /*if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { return new RedirectResponse($targetPath); - } + }*/ // For example: - return new RedirectResponse($this->urlGenerator->generate('app_dashboard')); + return new RedirectResponse($this->urlGenerator->generate('dashboard')); //return new RedirectResponse($this->urlGenerator->generate('DashboardController')); } diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig index 89379d3..5b22657 100644 --- a/templates/security/login.html.twig +++ b/templates/security/login.html.twig @@ -3,23 +3,39 @@ {% block title %}HegreLand{% endblock %} {% block body %} -
- {% if error %} -
{{ error.messageKey|trans(error.messageData, 'security') }}
- {% endif %} + + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} - {% if app.user %} -
- Vous êtes déjà connecté(e) en tant que {{ app.user.userIdentifier }}, Me déconnecter + {% if app.user %} +
+ You are logged in as {{ app.user.userIdentifier }}, Logout +
+ {% endif %} + +

Please sign in

+ + + + + + + + {# + Uncomment this section and add a remember_me option below your firewall to activate remember me functionality. + See https://symfony.com/doc/current/security/remember_me.html + +
+ +
- {% endif %} + #} - -

Me connecter

- {{ form_row(loginForm.email) }} - -

Mot de passe oublié

- + + {% endblock %} From 3f2fcfd2695933e2fd5f53833fec86a56610e242 Mon Sep 17 00:00:00 2001 From: ASTIER Yann Date: Thu, 28 Nov 2024 16:48:02 +0100 Subject: [PATCH 9/9] login finished. --- config/packages/security.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index a8f9810..a764dfa 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -40,7 +40,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: ^/*, roles: ROLE_USER } + - { path: ^/login, roles: PUBLIC_ACCESS } + - { path: ^/*, roles: IS_AUTHENTICATED_FULLY} when@test: security: