From dbe8f4fe995f5c431f6cef8338bed0eefcb2c543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 3 Oct 2024 01:44:31 +0200 Subject: [PATCH 1/2] Init admin login page --- app/DataFixtures/AppFixtures.php | 2 + app/Entity/User.php | 109 ++++++++++++++++++ app/Factory/UserFactory.php | 68 +++++++++++ app/Repository/UserRepository.php | 46 ++++++++ app/Story/DefaultUsersStory.php | 29 +++++ composer.json | 2 + config/bundles.php | 1 + config/packages/security.yaml | 55 +++++++++ config/routes/security.yaml | 3 + config/routes/sylius_admin_ui.yaml | 3 + src/AdminUi/composer.json | 2 + src/AdminUi/config/routes.php | 26 +++++ src/AdminUi/config/services.php | 2 + src/AdminUi/config/services/controller.php | 29 +++++ .../Symfony/Controller/LoginController.php | 47 ++++++++ .../src/Symfony/Form/Type/LoginType.php | 44 +++++++ .../templates/security/login.html.twig | 13 +++ .../.application/config/bundles.php | 1 + .../config/packages/security.yaml | 4 + .../assets/images/sylius-logo-dark-text.png | Bin 0 -> 17408 bytes .../config/app/twig_hooks/security/login.php | 59 ++++++++++ .../public/images/sylius-logo-dark-text.png | Bin 0 -> 17408 bytes src/BootstrapAdminUi/public/manifest.json | 1 + .../security/common/content.html.twig | 5 + .../security/common/content/header.html.twig | 1 + .../templates/security/common/logo.html.twig | 3 + .../security/login/content/form.html.twig | 8 ++ .../login/content/form/error.html.twig | 7 ++ .../login/content/form/password.html.twig | 1 + .../login/content/form/remember_me.html.twig | 1 + .../login/content/form/submit.html.twig | 3 + .../login/content/form/username.html.twig | 3 + .../translations/messages.en.yaml | 8 ++ symfony.lock | 13 +++ tests/Functional/BookTest.php | 11 ++ tests/Functional/ConferenceTest.php | 10 ++ tests/Functional/LegacyBookTest.php | 10 ++ tests/Functional/LoginTest.php | 62 ++++++++++ tests/Functional/SpeakerTest.php | 10 ++ tests/Functional/TalkTest.php | 10 ++ 40 files changed, 712 insertions(+) create mode 100644 app/Entity/User.php create mode 100644 app/Factory/UserFactory.php create mode 100644 app/Repository/UserRepository.php create mode 100644 app/Story/DefaultUsersStory.php create mode 100644 config/packages/security.yaml create mode 100644 config/routes/security.yaml create mode 100644 config/routes/sylius_admin_ui.yaml create mode 100644 src/AdminUi/config/routes.php create mode 100644 src/AdminUi/config/services/controller.php create mode 100644 src/AdminUi/src/Symfony/Controller/LoginController.php create mode 100644 src/AdminUi/src/Symfony/Form/Type/LoginType.php create mode 100644 src/AdminUi/templates/security/login.html.twig create mode 100644 src/AdminUi/tests/Functional/.application/config/packages/security.yaml create mode 100644 src/BootstrapAdminUi/assets/images/sylius-logo-dark-text.png create mode 100644 src/BootstrapAdminUi/config/app/twig_hooks/security/login.php create mode 100644 src/BootstrapAdminUi/public/images/sylius-logo-dark-text.png create mode 100644 src/BootstrapAdminUi/templates/security/common/content.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/common/content/header.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/common/logo.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form/error.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form/password.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form/remember_me.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form/submit.html.twig create mode 100644 src/BootstrapAdminUi/templates/security/login/content/form/username.html.twig create mode 100644 tests/Functional/LoginTest.php diff --git a/app/DataFixtures/AppFixtures.php b/app/DataFixtures/AppFixtures.php index cad5f0e4..8ef072f8 100644 --- a/app/DataFixtures/AppFixtures.php +++ b/app/DataFixtures/AppFixtures.php @@ -17,6 +17,7 @@ use App\Story\DefaultConferencesStory; use App\Story\DefaultSpeakersStory; use App\Story\DefaultSyliusCon2024TalksStory; +use App\Story\DefaultUsersStory; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; @@ -28,5 +29,6 @@ public function load(ObjectManager $manager): void DefaultConferencesStory::load(); DefaultSpeakersStory::load(); DefaultSyliusCon2024TalksStory::load(); + DefaultUsersStory::load(); } } diff --git a/app/Entity/User.php b/app/Entity/User.php new file mode 100644 index 00000000..7376aaa1 --- /dev/null +++ b/app/Entity/User.php @@ -0,0 +1,109 @@ + The user roles */ + #[ORM\Column] + private array $roles = []; + + #[ORM\Column] + private ?string $password = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): void + { + $this->email = $email; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + * + * @return list + */ + public function getRoles(): array + { + $roles = $this->roles; + // guarantee every user at least has ROLE_USER + $roles[] = 'ROLE_USER'; + + return array_unique($roles); + } + + /** + * @param list $roles + */ + public function setRoles(array $roles): void + { + $this->roles = $roles; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(?string $password): void + { + $this->password = $password; + } + + /** + * @see UserInterface + */ + public function eraseCredentials(): void + { + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; + } +} diff --git a/app/Factory/UserFactory.php b/app/Factory/UserFactory.php new file mode 100644 index 00000000..7f182fa2 --- /dev/null +++ b/app/Factory/UserFactory.php @@ -0,0 +1,68 @@ + + */ +final class UserFactory extends PersistentProxyObjectFactory +{ + public function __construct( + private UserPasswordHasherInterface $userPasswordHasher, + ) { + parent::__construct(); + } + + public static function class(): string + { + return User::class; + } + + public function withEmail(string $email): self + { + return $this->with(['email' => $email]); + } + + public function admin(): self + { + return $this->with(['roles' => ['ROLE_ADMIN']]); + } + + public function withPassword(string $password): self + { + return $this->with(['password' => $password]); + } + + protected function defaults(): array|callable + { + return [ + 'email' => self::faker()->email(), + 'password' => self::faker()->password(), + 'roles' => [], + ]; + } + + protected function initialize(): static + { + return $this + ->afterInstantiate(function (User $user): void { + $user->setPassword($this->userPasswordHasher->hashPassword($user, $user->getPassword() ?? '')); + }) + ; + } +} diff --git a/app/Repository/UserRepository.php b/app/Repository/UserRepository.php new file mode 100644 index 00000000..dff6f35c --- /dev/null +++ b/app/Repository/UserRepository.php @@ -0,0 +1,46 @@ + + */ +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(); + } +} diff --git a/app/Story/DefaultUsersStory.php b/app/Story/DefaultUsersStory.php new file mode 100644 index 00000000..99c16c34 --- /dev/null +++ b/app/Story/DefaultUsersStory.php @@ -0,0 +1,29 @@ +admin() + ->withEmail('admin@example.com') + ->withPassword('admin') + ->create(); + } +} diff --git a/composer.json b/composer.json index 2d4e8c75..9adeb53d 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,8 @@ "symfony/dependency-injection": "^6.4 || ^7.0", "symfony/expression-language": "^6.4 || ^7.0", "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-http": "^6.4 || ^7.0", "symfony/stopwatch": "^6.4 || ^7.0", "symfony/twig-bundle": "^6.4 || ^7.0", "symfony/ux-autocomplete": "^2.17", diff --git a/config/bundles.php b/config/bundles.php index 739ced7e..ba51620d 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -24,4 +24,5 @@ Vich\UploaderBundle\VichUploaderBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], Symfony\UX\Autocomplete\AutocompleteBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], ]; diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 00000000..455e23d8 --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,55 @@ +security: + # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords + password_hashers: + 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) + app_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ + security: false + main: + lazy: true + provider: app_user_provider + + # activate different ways to authenticate + # https://symfony.com/doc/current/security.html#the-firewall + + # https://symfony.com/doc/current/security/impersonating_user.html + # switch_user: true + + form_login: + # "app_login" is the name of the route created previously + login_path: sylius_admin_ui_login + check_path: sylius_admin_ui_login_check + default_target_path: app_admin_conference_index + logout: + path: sylius_admin_ui_logout + target: sylius_admin_ui_login + + # Easy way to control access for large sections of your site + # Note: Only the *first* access control that matches will be used + access_control: + - { path: ^/admin/login, roles: PUBLIC_ACCESS } + - { path: ^/admin/logout, roles: PUBLIC_ACCESS } + - { path: ^/admin, roles: ROLE_ADMIN } + - { path: ^/, roles: PUBLIC_ACCESS } + # - { path: ^/profile, roles: ROLE_USER } + +when@test: + security: + password_hashers: + # By default, password hashers are resource intensive and take time. This is + # 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: + algorithm: auto + cost: 4 # Lowest possible value for bcrypt + time_cost: 3 # Lowest possible value for argon + memory_cost: 10 # Lowest possible value for argon diff --git a/config/routes/security.yaml b/config/routes/security.yaml new file mode 100644 index 00000000..f853be15 --- /dev/null +++ b/config/routes/security.yaml @@ -0,0 +1,3 @@ +_security_logout: + resource: security.route_loader.logout + type: service diff --git a/config/routes/sylius_admin_ui.yaml b/config/routes/sylius_admin_ui.yaml new file mode 100644 index 00000000..23595fff --- /dev/null +++ b/config/routes/sylius_admin_ui.yaml @@ -0,0 +1,3 @@ +sylius_admin_ui: + resource: '@SyliusAdminUiBundle/config/routes.php' + prefix: /admin diff --git a/src/AdminUi/composer.json b/src/AdminUi/composer.json index 69a77345..b04766e5 100644 --- a/src/AdminUi/composer.json +++ b/src/AdminUi/composer.json @@ -6,6 +6,8 @@ "knplabs/knp-menu-bundle": "^3.0", "sylius/twig-hooks": "^0.3 || dev-main", "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-http": "^6.4 || ^7.0", "symfony/twig-bundle": "^6.4 || ^7.0", "sylius/resource-bundle": "^1.11 || ^1.12@alpha", "twig/twig": "^2.15 || ^3.0" diff --git a/src/AdminUi/config/routes.php b/src/AdminUi/config/routes.php new file mode 100644 index 00000000..a02e4abe --- /dev/null +++ b/src/AdminUi/config/routes.php @@ -0,0 +1,26 @@ +add('sylius_admin_ui_login', '/login') + ->controller('sylius_admin_ui.controller.login') + ; + + $routes->add('sylius_admin_ui_login_check', '/login_check'); + $routes->add('sylius_admin_ui_logout', '/logout') + ->methods(['GET']); +}; diff --git a/src/AdminUi/config/services.php b/src/AdminUi/config/services.php index 5424dfb9..7999fbc5 100644 --- a/src/AdminUi/config/services.php +++ b/src/AdminUi/config/services.php @@ -18,6 +18,8 @@ use Sylius\AdminUi\TwigHooks\Hookable\Metadata\RoutingHookableMetadataFactory; return function (ContainerConfigurator $configurator): void { + $configurator->import('./services/**/**.php'); + $services = $configurator->services(); $services->set('sylius_admin_ui.knp.menu_builder', MenuBuilder::class) diff --git a/src/AdminUi/config/services/controller.php b/src/AdminUi/config/services/controller.php new file mode 100644 index 00000000..87933ef2 --- /dev/null +++ b/src/AdminUi/config/services/controller.php @@ -0,0 +1,29 @@ +services(); + + $services->set('sylius_admin_ui.controller.login', LoginController::class) + ->public() + ->args([ + service('security.authentication_utils'), + service('form.factory'), + service('twig'), + ]) + ; +}; diff --git a/src/AdminUi/src/Symfony/Controller/LoginController.php b/src/AdminUi/src/Symfony/Controller/LoginController.php new file mode 100644 index 00000000..39e6d7a0 --- /dev/null +++ b/src/AdminUi/src/Symfony/Controller/LoginController.php @@ -0,0 +1,47 @@ +authenticationUtils->getLastAuthenticationError(); + $lastUsername = $this->authenticationUtils->getLastUsername(); + + // TODO use a Twig component instead + $formType = LoginType::class; + $form = $this->formFactory->createNamed('', $formType); + + return new Response($this->twig->render('@SyliusAdminUi/security/login.html.twig', [ + 'form' => $form->createView(), + 'last_username' => $lastUsername, + 'last_error' => $lastError, + ])); + } +} diff --git a/src/AdminUi/src/Symfony/Form/Type/LoginType.php b/src/AdminUi/src/Symfony/Form/Type/LoginType.php new file mode 100644 index 00000000..0348178f --- /dev/null +++ b/src/AdminUi/src/Symfony/Form/Type/LoginType.php @@ -0,0 +1,44 @@ +add('_username', TextType::class, [ + 'label' => 'sylius.form.login.username', + ]) + ->add('_password', PasswordType::class, [ + 'label' => 'sylius.form.login.password', + ]) + ->add('_remember_me', CheckboxType::class, [ + 'label' => 'sylius.form.login.remember_me', + 'required' => false, + ]) + ; + } + + public function getBlockPrefix(): string + { + return 'sylius_security_login'; + } +} diff --git a/src/AdminUi/templates/security/login.html.twig b/src/AdminUi/templates/security/login.html.twig new file mode 100644 index 00000000..03081e73 --- /dev/null +++ b/src/AdminUi/templates/security/login.html.twig @@ -0,0 +1,13 @@ +{% extends '@SyliusAdminUi/base.html.twig' %} + +{% block title %}{{ parent() }} | {{ 'sylius.ui.administration_panel_login'|trans }}{% endblock %} + +{% block body_class %}d-flex flex-column{% endblock %} + +{% block body %} +
+
+ {% hook 'sylius_admin.security.login' with { form, last_error, last_username } %} +
+
+{% endblock %} diff --git a/src/AdminUi/tests/Functional/.application/config/bundles.php b/src/AdminUi/tests/Functional/.application/config/bundles.php index 74cef7f5..46d7ddbb 100644 --- a/src/AdminUi/tests/Functional/.application/config/bundles.php +++ b/src/AdminUi/tests/Functional/.application/config/bundles.php @@ -14,6 +14,7 @@ return [ Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true], Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], Symfony\UX\TwigComponent\TwigComponentBundle::class => ['all' => true], Sylius\TwigHooks\SyliusTwigHooksBundle::class => ['all' => true], Sylius\Bundle\ResourceBundle\SyliusResourceBundle::class => ['all' => true], diff --git a/src/AdminUi/tests/Functional/.application/config/packages/security.yaml b/src/AdminUi/tests/Functional/.application/config/packages/security.yaml new file mode 100644 index 00000000..fbd7d5fa --- /dev/null +++ b/src/AdminUi/tests/Functional/.application/config/packages/security.yaml @@ -0,0 +1,4 @@ +security: + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|css|images|js)/ diff --git a/src/BootstrapAdminUi/assets/images/sylius-logo-dark-text.png b/src/BootstrapAdminUi/assets/images/sylius-logo-dark-text.png new file mode 100644 index 0000000000000000000000000000000000000000..e561950db290f8cfe767d453d2d8dba8b450e589 GIT binary patch literal 17408 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'sylius_twig_hooks', [ + 'hooks' => [ + 'sylius_admin.security.login' => [ + 'logo' => [ + 'template' => '@SyliusBootstrapAdminUi/security/common/logo.html.twig', + ], + 'content' => [ + 'template' => '@SyliusBootstrapAdminUi/security/common/content.html.twig', + ], + ], + + 'sylius_admin.security.login.content' => [ + 'header' => [ + 'template' => '@SyliusBootstrapAdminUi/security/common/content/header.html.twig', + ], + 'flashes' => [ + 'template' => '@SyliusBootstrapAdminUi/shared/crud/common/content/flashes.html.twig', + ], + 'form' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form.html.twig', + ], + ], + + 'sylius_admin.security.login.content.form' => [ + 'error' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form/error.html.twig', + ], + 'username' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form/username.html.twig', + ], + 'password' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form/password.html.twig', + ], + 'remember_me' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form/remember_me.html.twig', + ], + 'submit' => [ + 'template' => '@SyliusBootstrapAdminUi/security/login/content/form/submit.html.twig', + ], + ], + ], + ]); +}; diff --git a/src/BootstrapAdminUi/public/images/sylius-logo-dark-text.png b/src/BootstrapAdminUi/public/images/sylius-logo-dark-text.png new file mode 100644 index 0000000000000000000000000000000000000000..e561950db290f8cfe767d453d2d8dba8b450e589 GIT binary patch literal 17408 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 +
+ {% hook 'content' %} +
+ diff --git a/src/BootstrapAdminUi/templates/security/common/content/header.html.twig b/src/BootstrapAdminUi/templates/security/common/content/header.html.twig new file mode 100644 index 00000000..3ebc7201 --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/common/content/header.html.twig @@ -0,0 +1 @@ +

{{ 'sylius.ui.login_to_your_account'|trans }}

diff --git a/src/BootstrapAdminUi/templates/security/common/logo.html.twig b/src/BootstrapAdminUi/templates/security/common/logo.html.twig new file mode 100644 index 00000000..4e2a53e3 --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/common/logo.html.twig @@ -0,0 +1,3 @@ +
+ Sylius +
diff --git a/src/BootstrapAdminUi/templates/security/login/content/form.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form.html.twig new file mode 100644 index 00000000..53c1b454 --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form.html.twig @@ -0,0 +1,8 @@ +{% set form = hookable_metadata.context.form %} + +{% form_theme form '@SyliusBootstrapAdminUi/shared/form_theme.html.twig' %} + +{{ form_start(form, {'action': path('sylius_admin_ui_login_check'), 'attr': {'class': 'ui large loadable form', 'novalidate': 'novalidate'}}) }} + {% hook 'form' with { form } %} + +{{ form_end(form, {'render_rest': false}) }} diff --git a/src/BootstrapAdminUi/templates/security/login/content/form/error.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form/error.html.twig new file mode 100644 index 00000000..7e2ee1dd --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form/error.html.twig @@ -0,0 +1,7 @@ +{% set last_error = hookable_metadata.context.last_error|default(null) %} + +{% if last_error is not null %} +
+ {{ last_error.messageKey|trans }} +
+{% endif %} diff --git a/src/BootstrapAdminUi/templates/security/login/content/form/password.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form/password.html.twig new file mode 100644 index 00000000..eac61d9e --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form/password.html.twig @@ -0,0 +1 @@ +{{ form_row(hookable_metadata.context.form._password) }} diff --git a/src/BootstrapAdminUi/templates/security/login/content/form/remember_me.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form/remember_me.html.twig new file mode 100644 index 00000000..669c346c --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form/remember_me.html.twig @@ -0,0 +1 @@ +{{ form_row(hookable_metadata.context.form._remember_me) }} diff --git a/src/BootstrapAdminUi/templates/security/login/content/form/submit.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form/submit.html.twig new file mode 100644 index 00000000..71bf7232 --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form/submit.html.twig @@ -0,0 +1,3 @@ +{% import '@SyliusBootstrapAdminUi/shared/helper/button.html.twig' as button %} + +{{ button.primary({ text: 'sylius.ui.login'|trans, type: 'submit', class: 'btn btn-primary w-100' }) }} diff --git a/src/BootstrapAdminUi/templates/security/login/content/form/username.html.twig b/src/BootstrapAdminUi/templates/security/login/content/form/username.html.twig new file mode 100644 index 00000000..9dc070cf --- /dev/null +++ b/src/BootstrapAdminUi/templates/security/login/content/form/username.html.twig @@ -0,0 +1,3 @@ +{% set last_username = hookable_metadata.context.last_username|default('') %} + +{{ form_row(hookable_metadata.context.form._username, { 'value': last_username }) }} diff --git a/src/UiTranslations/translations/messages.en.yaml b/src/UiTranslations/translations/messages.en.yaml index 1291dc16..dfdeb9d8 100644 --- a/src/UiTranslations/translations/messages.en.yaml +++ b/src/UiTranslations/translations/messages.en.yaml @@ -1,7 +1,13 @@ sylius: + form: + login: + username: 'Username' + password: 'Password' + remember_me: 'Remember me' ui: actions: Actions add_new_entry: Add a new entry + administration_panel_login: 'Connexion à l''espace d''administration' all: All are_your_sure_you_want_to_perform_this_action: 'Are you sure you want to perform this action?' cancel: Cancel @@ -17,6 +23,8 @@ sylius: filters: Filters from: From info: Info + login: Login + login_to_your_account: 'Login to your account' new: New no_label: No no_results: No results found diff --git a/symfony.lock b/symfony.lock index 76188370..8e543bcf 100644 --- a/symfony.lock +++ b/symfony.lock @@ -168,6 +168,19 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "7.1", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "6.4", + "ref": "2ae08430db28c8eb4476605894296c82a642028f" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, "symfony/stimulus-bundle": { "version": "2.19", "recipe": { diff --git a/tests/Functional/BookTest.php b/tests/Functional/BookTest.php index 1c9d5d7c..2b329a58 100644 --- a/tests/Functional/BookTest.php +++ b/tests/Functional/BookTest.php @@ -6,14 +6,18 @@ use App\Entity\Book; use App\Factory\BookFactory; +use App\Factory\UserFactory; +use App\Story\DefaultUsersStory; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; use Zenstruck\Foundry\Persistence\Proxy; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class BookTest extends WebTestCase { + Use Factories; use ResetDatabase; private KernelBrowser $client; @@ -21,6 +25,13 @@ final class BookTest extends WebTestCase protected function setUp(): void { $this->client = self::createClient(); + + $user = UserFactory::new() + ->admin() + ->create() + ; + + $this->client->loginUser($user->_real()); } public function testShowingBook(): void diff --git a/tests/Functional/ConferenceTest.php b/tests/Functional/ConferenceTest.php index 785d5f59..56b4de2d 100644 --- a/tests/Functional/ConferenceTest.php +++ b/tests/Functional/ConferenceTest.php @@ -5,13 +5,16 @@ namespace MainTests\Sylius\Functional; use App\Factory\ConferenceFactory; +use App\Factory\UserFactory; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class ConferenceTest extends WebTestCase { + use Factories; use ResetDatabase; private KernelBrowser $client; @@ -19,6 +22,13 @@ final class ConferenceTest extends WebTestCase protected function setUp(): void { $this->client = self::createClient(); + + $user = UserFactory::new() + ->admin() + ->create() + ; + + $this->client->loginUser($user->_real()); } public function testBrowsingConferences(): void diff --git a/tests/Functional/LegacyBookTest.php b/tests/Functional/LegacyBookTest.php index 0cb3a1b2..e751d36a 100644 --- a/tests/Functional/LegacyBookTest.php +++ b/tests/Functional/LegacyBookTest.php @@ -5,12 +5,15 @@ namespace MainTests\Sylius\Functional; use App\Factory\BookFactory; +use App\Factory\UserFactory; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class LegacyBookTest extends WebTestCase { + use Factories; use ResetDatabase; private KernelBrowser $client; @@ -18,6 +21,13 @@ final class LegacyBookTest extends WebTestCase protected function setUp(): void { $this->client = self::createClient(); + + $user = UserFactory::new() + ->admin() + ->create() + ; + + $this->client->loginUser($user->_real()); } public function testBrowsingBooks(): void diff --git a/tests/Functional/LoginTest.php b/tests/Functional/LoginTest.php new file mode 100644 index 00000000..e472e93f --- /dev/null +++ b/tests/Functional/LoginTest.php @@ -0,0 +1,62 @@ +client = self::createClient(); + } + + public function testLoginContent(): void + { + $this->client->request('GET', '/admin/login'); + + self::assertResponseIsSuccessful(); + + // Validate Header + self::assertSelectorTextContains('h2', 'Login to your account'); + + // Validate page body + self::assertSelectorExists('#_username'); + self::assertSelectorExists('#_password'); + } + + public function testLoginSuccess(): void + { + UserFactory::new() + ->withEmail('admin@example.com') + ->withPassword('password') + ->admin() + ->create() + ; + + $this->client->request('GET', '/admin/login'); + + $this->client->submitForm('Login', [ + '_username' => 'admin@example.com', + '_password' => 'password', + ]); + + self::assertResponseRedirects(expectedLocation: '/admin/conferences', expectedCode: Response::HTTP_FOUND); + } +} diff --git a/tests/Functional/SpeakerTest.php b/tests/Functional/SpeakerTest.php index 6fdca124..6213ecb4 100644 --- a/tests/Functional/SpeakerTest.php +++ b/tests/Functional/SpeakerTest.php @@ -5,13 +5,16 @@ namespace Functional; use App\Factory\SpeakerFactory; +use App\Factory\UserFactory; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class SpeakerTest extends WebTestCase { + use Factories; use ResetDatabase; private KernelBrowser $client; @@ -19,6 +22,13 @@ final class SpeakerTest extends WebTestCase protected function setUp(): void { $this->client = self::createClient(); + + $user = UserFactory::new() + ->admin() + ->create() + ; + + $this->client->loginUser($user->_real()); } public function testBrowsingSpeakers(): void diff --git a/tests/Functional/TalkTest.php b/tests/Functional/TalkTest.php index 053d172b..e4f70d0e 100644 --- a/tests/Functional/TalkTest.php +++ b/tests/Functional/TalkTest.php @@ -8,13 +8,16 @@ use App\Factory\ConferenceFactory; use App\Factory\SpeakerFactory; use App\Factory\TalkFactory; +use App\Factory\UserFactory; use Symfony\Bundle\FrameworkBundle\KernelBrowser; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Response; +use Zenstruck\Foundry\Test\Factories; use Zenstruck\Foundry\Test\ResetDatabase; final class TalkTest extends WebTestCase { + use Factories; use ResetDatabase; private KernelBrowser $client; @@ -22,6 +25,13 @@ final class TalkTest extends WebTestCase protected function setUp(): void { $this->client = self::createClient(); + + $user = UserFactory::new() + ->admin() + ->create() + ; + + $this->client->loginUser($user->_real()); } public function testBrowsingTalks(): void From 2a43c1238ef50efb6b880d69b28b4240c206e2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Fr=C3=A9mont?= Date: Thu, 3 Oct 2024 16:29:13 +0200 Subject: [PATCH 2/2] Fix translation --- src/UiTranslations/translations/messages.en.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/UiTranslations/translations/messages.en.yaml b/src/UiTranslations/translations/messages.en.yaml index dfdeb9d8..6f536695 100644 --- a/src/UiTranslations/translations/messages.en.yaml +++ b/src/UiTranslations/translations/messages.en.yaml @@ -7,7 +7,7 @@ sylius: ui: actions: Actions add_new_entry: Add a new entry - administration_panel_login: 'Connexion à l''espace d''administration' + administration_panel_login: 'Administration panel login' all: All are_your_sure_you_want_to_perform_this_action: 'Are you sure you want to perform this action?' cancel: Cancel