From cc213bfb15d4d2b68e136ef972cce144c6775396 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Tue, 2 Jun 2026 22:26:03 +0000 Subject: [PATCH] Add basic auth --- config/packages/security.yaml | 8 +- migrations/Version20260602214533.php | 32 +++++ src/Controller/AccountController.php | 68 +++++++++++ src/Entity/User.php | 109 ++++++++++++++++++ src/Repository/UserRepository.php | 60 ++++++++++ templates/account/register.html.twig | 10 ++ tests/Integration/Auth/LoginTest.php | 34 ++++++ .../Auth/RegistrationControllerTest.php | 34 ++++++ .../AddSubjectTest.php} | 4 +- 9 files changed, 355 insertions(+), 4 deletions(-) create mode 100644 migrations/Version20260602214533.php create mode 100644 src/Controller/AccountController.php create mode 100644 src/Entity/User.php create mode 100644 src/Repository/UserRepository.php create mode 100644 templates/account/register.html.twig create mode 100644 tests/Integration/Auth/LoginTest.php create mode 100644 tests/Integration/Auth/RegistrationControllerTest.php rename tests/Integration/{Controller/SubjectControllerTest.php => Subject/AddSubjectTest.php} (91%) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 8964044..68f1e5c 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -5,7 +5,11 @@ security: # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider providers: - users_in_memory: { memory: null } + # used to reload user from session & other features (e.g. switch_user) + app_user_provider: + entity: + class: App\Entity\User + property: email firewalls: dev: @@ -14,7 +18,7 @@ security: security: false main: lazy: true - provider: users_in_memory + provider: app_user_provider # Activate different ways to authenticate: # https://symfony.com/doc/current/security.html#the-firewall diff --git a/migrations/Version20260602214533.php b/migrations/Version20260602214533.php new file mode 100644 index 0000000..e495530 --- /dev/null +++ b/migrations/Version20260602214533.php @@ -0,0 +1,32 @@ +addSql('CREATE TABLE "user" (id INT GENERATED BY DEFAULT AS IDENTITY 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('DROP TABLE "user"'); + } +} diff --git a/src/Controller/AccountController.php b/src/Controller/AccountController.php new file mode 100644 index 0000000..2b6029f --- /dev/null +++ b/src/Controller/AccountController.php @@ -0,0 +1,68 @@ +getLastAuthenticationError(); + $lastUsername = $authUtils->getLastUsername(); + + return $this->render('account/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + #[Route('/account/register')] + public function register( + Request $request, + UserPasswordHasherInterface $passwordHasher, + EntityManagerInterface $em, + ): Response { + $user = new User(); + + $form = $this->createFormBuilder($user) + ->add('email', EmailType::class) + ->add('password', PasswordType::class) + ->add('password_again', PasswordType::class, [ + 'mapped' => false, + ]) + ->add('register', SubmitType::class) + ->getForm(); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $plaintextPassword = $form->get('password')->getData(); + $hashedPassword = $passwordHasher->hashPassword( + $user, + $plaintextPassword + ); + $user->setPassword($hashedPassword); + + $em->persist($user); + $em->flush(); + + return $this->redirectToRoute('app_account_login'); + } + + return $this->render('account/register.html.twig', [ + 'form' => $form, + ]); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..4d9463a --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,109 @@ + The user roles + */ + #[ORM\Column] + private array $roles = []; + + /** + * @var string The hashed password + */ + #[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): static + { + $this->email = $email; + + return $this; + } + + /** + * A visual identifier that represents this user. + * + * @see UserInterface + */ + public function getUserIdentifier(): string + { + return (string) $this->email; + } + + /** + * @see UserInterface + */ + 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): static + { + $this->roles = $roles; + + return $this; + } + + /** + * @see PasswordAuthenticatedUserInterface + */ + public function getPassword(): ?string + { + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; + } + + /** + * Ensure the session doesn't contain actual password hashes by CRC32C-hashing them, as supported since Symfony 7.3. + */ + public function __serialize(): array + { + $data = (array) $this; + $data["\0" . self::class . "\0password"] = hash('crc32c', $this->password); + + return $data; + } +} 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/templates/account/register.html.twig b/templates/account/register.html.twig new file mode 100644 index 0000000..a6ef55a --- /dev/null +++ b/templates/account/register.html.twig @@ -0,0 +1,10 @@ + + + Rater Login + + + body + {{ form(form) }} + + + diff --git a/tests/Integration/Auth/LoginTest.php b/tests/Integration/Auth/LoginTest.php new file mode 100644 index 0000000..e929153 --- /dev/null +++ b/tests/Integration/Auth/LoginTest.php @@ -0,0 +1,34 @@ +email(); + $plainPwd = $faker->word(8); + + $client->request('GET', '/account/register'); + + $client->submitForm('form_register', [ + 'form[email]' => $email, + 'form[password]' => $plainPwd, + 'form[password_again]' => $plainPwd, + ]); + + $this->assertResponseRedirects(); + + $subject = static::getContainer() + ->get(UserRepository::class) + ->findOneBy(['email' => $email]); + + $this->assertNotNull($subject); + } +} diff --git a/tests/Integration/Auth/RegistrationControllerTest.php b/tests/Integration/Auth/RegistrationControllerTest.php new file mode 100644 index 0000000..babd218 --- /dev/null +++ b/tests/Integration/Auth/RegistrationControllerTest.php @@ -0,0 +1,34 @@ +email(); + $plainPwd = $faker->word(8); + + $client->request('GET', '/account/register'); + + $client->submitForm('form_register', [ + 'form[email]' => $email, + 'form[password]' => $plainPwd, + 'form[password_again]' => $plainPwd, + ]); + + $this->assertResponseRedirects(); + + $subject = static::getContainer() + ->get(UserRepository::class) + ->findOneBy(['email' => $email]); + + $this->assertNotNull($subject); + } +} diff --git a/tests/Integration/Controller/SubjectControllerTest.php b/tests/Integration/Subject/AddSubjectTest.php similarity index 91% rename from tests/Integration/Controller/SubjectControllerTest.php rename to tests/Integration/Subject/AddSubjectTest.php index 67c53a0..7885f62 100644 --- a/tests/Integration/Controller/SubjectControllerTest.php +++ b/tests/Integration/Subject/AddSubjectTest.php @@ -1,13 +1,13 @@