diff --git a/README.md b/README.md index 9284fc45..540c2922 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,52 @@ _Optional_ - Defaults to `true` Enable or disable the Reset Password Cleaner which handles expired reset password requests that may have been left in persistence. +## Advanced Usage + +### Purging `ResetPasswordRequest` objects from persistence + +The `ResetPasswordRequestRepositoryInterface::removeRequests()` method, which is +implemented in the +[ResetPasswordRequestRepositoryTrait](https://github.com/SymfonyCasts/reset-password-bundle/blob/main/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php), +can be used to remove all request objects from persistence for a single user. This +differs from the +[garbage collection mechanism](https://github.com/SymfonyCasts/reset-password-bundle/blob/df64d82cca2ee371da5e8c03c227457069ae663e/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php#L73) +which only removes _expired_ request objects for _all_ users automatically. + +Typically, you'd call this method when you need to remove request object(s) for +a user who changed their email address due to suspicious activity and potentially +has valid request objects in persistence with their "old" compromised email address. + +```php +// ProfileController + +#[Route(path: '/profile/{id}', name: 'app_update_profile', methods: ['GET', 'POST'])] +public function profile(Request $request, User $user, ResetPasswordRequestRepositoryInterface $repository): Response +{ + $originalEmail = $user->getEmail(); + + $form = $this->createFormBuilder($user) + ->add('email', EmailType::class) + ->add('save', SubmitType::class, ['label' => 'Save Profile']) + ->getForm() + ; + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + if ($originalEmail !== $user->getEmail()) { + // The user changed their email address. + // Remove any old reset requests for the user. + $repository->removeRequests($user); + } + + // Persist the user object and redirect... + } + + return $this->render('profile.html.twig', ['form' => $form]); +} +``` + ## Support Feel free to open an issue for questions, problems, or suggestions with our bundle. diff --git a/src/Persistence/Fake/FakeResetPasswordInternalRepository.php b/src/Persistence/Fake/FakeResetPasswordInternalRepository.php index d146b8f7..ec9d465e 100644 --- a/src/Persistence/Fake/FakeResetPasswordInternalRepository.php +++ b/src/Persistence/Fake/FakeResetPasswordInternalRepository.php @@ -60,4 +60,9 @@ public function removeExpiredResetPasswordRequests(): int { throw new FakeRepositoryException(); } + + public function removeRequests(object $user): void + { + throw new FakeRepositoryException(); + } } diff --git a/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php b/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php index 52dfcaae..f446f5cf 100644 --- a/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php +++ b/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php @@ -82,4 +82,24 @@ public function removeExpiredResetPasswordRequests(): int return $query->execute(); } + + /** + * Remove a users ResetPasswordRequest objects from persistence. + * + * Warning - This is a destructive operation. Calling this method + * may have undesired consequences for users who have valid + * ResetPasswordRequests but have not "checked their email" yet. + * + * @see https://github.com/SymfonyCasts/reset-password-bundle?tab=readme-ov-file#advanced-usage + */ + public function removeRequests(object $user): void + { + $query = $this->createQueryBuilder('t') + ->delete() + ->where('t.user = :user') + ->setParameter('user', $user) + ; + + $query->getQuery()->execute(); + } } diff --git a/src/Persistence/ResetPasswordRequestRepositoryInterface.php b/src/Persistence/ResetPasswordRequestRepositoryInterface.php index 25818c1f..ede76311 100644 --- a/src/Persistence/ResetPasswordRequestRepositoryInterface.php +++ b/src/Persistence/ResetPasswordRequestRepositoryInterface.php @@ -14,6 +14,8 @@ /** * @author Jesse Rushlow * @author Ryan Weaver + * + * @method void removeRequests(object $user) Remove a users ResetPasswordRequest objects from persistence. */ interface ResetPasswordRequestRepositoryInterface { diff --git a/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryTest.php b/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryTest.php index 8a4034ef..1cbe9314 100644 --- a/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryTest.php +++ b/tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryTest.php @@ -208,6 +208,33 @@ public function testRemovedExpiredResetPasswordRequestsOnlyRemovedExpiredRequest self::assertSame($futureFixture, $result[0]); } + public function testRemoveRequestsRemovesAllRequestsForASingleUser(): void + { + $this->manager->persist($userFixture = new ResetPasswordTestFixtureUser()); + $requestFixtures = [new ResetPasswordTestFixtureRequest(), new ResetPasswordTestFixtureRequest()]; + + foreach ($requestFixtures as $fixture) { + $fixture->user = $userFixture; + + $this->manager->persist($fixture); + } + + $this->manager->persist($differentUserFixture = new ResetPasswordTestFixtureUser()); + + $existingRequestFixture = new ResetPasswordTestFixtureRequest(); + $existingRequestFixture->user = $differentUserFixture; + + $this->manager->persist($existingRequestFixture); + $this->manager->flush(); + + self::assertCount(3, $this->repository->findAll()); + + $this->repository->removeRequests($userFixture); + + self::assertCount(1, $result = $this->repository->findAll()); + self::assertSame($existingRequestFixture, $result[0]); + } + private function configureDatabase(): void { $metaData = $this->manager->getMetadataFactory(); diff --git a/tests/UnitTests/Persistence/FakeResetPasswordInternalRepositoryTest.php b/tests/UnitTests/Persistence/FakeResetPasswordInternalRepositoryTest.php index cf505f79..0246202f 100644 --- a/tests/UnitTests/Persistence/FakeResetPasswordInternalRepositoryTest.php +++ b/tests/UnitTests/Persistence/FakeResetPasswordInternalRepositoryTest.php @@ -28,6 +28,7 @@ public function methodDataProvider(): \Generator yield ['findResetPasswordRequest', ['']]; yield ['getMostRecentNonExpiredRequestDate', [new \stdClass()]]; yield ['removeResetPasswordRequest', [$this->createMock(ResetPasswordRequestInterface::class)]]; + yield ['removeRequests', [new \stdClass()]]; } /**