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/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/config/packages/security.yaml b/config/packages/security.yaml index 9c8d719..a764dfa 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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 @@ -26,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: @@ -35,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 dead06a..bba229c 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -5,15 +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: 'dashboard')] public function index(): Response { return $this->render('dashboard/index.html.twig'); } - -} +} \ No newline at end of file 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/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..4e4de2f --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,37 @@ +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(); + + + return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]); + } + + #[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/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/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/Security/LoginFormAuthenticator.php b/src/Security/LoginFormAuthenticator.php new file mode 100644 index 0000000..1ea3330 --- /dev/null +++ b/src/Security/LoginFormAuthenticator.php @@ -0,0 +1,60 @@ +getPayload()->getString('_username'); + + $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('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/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..5b22657 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,41 @@ +{% extends 'base.html.twig' %} + +{% block title %}HegreLand{% endblock %} + +{% block body %} +
+ {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + + {% 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 + +
+ + +
+ #} + + +
+{% endblock %}