Skip to content

Commit

Permalink
feature #284 [persistence] remove ResetPasswordRequest objects progra…
Browse files Browse the repository at this point in the history
…mmatically
  • Loading branch information
jrushlow committed Mar 5, 2024
1 parent df64d82 commit 598fc74
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 0 deletions.
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions src/Persistence/Fake/FakeResetPasswordInternalRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,9 @@ public function removeExpiredResetPasswordRequests(): int
{
throw new FakeRepositoryException();
}

public function removeRequests(object $user): void
{
throw new FakeRepositoryException();
}
}
20 changes: 20 additions & 0 deletions src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
2 changes: 2 additions & 0 deletions src/Persistence/ResetPasswordRequestRepositoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
/**
* @author Jesse Rushlow <jr@rushlow.dev>
* @author Ryan Weaver <ryan@symfonycasts.com>
*
* @method void removeRequests(object $user) Remove a users ResetPasswordRequest objects from persistence.
*/
interface ResetPasswordRequestRepositoryInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public function methodDataProvider(): \Generator
yield ['findResetPasswordRequest', ['']];
yield ['getMostRecentNonExpiredRequestDate', [new \stdClass()]];
yield ['removeResetPasswordRequest', [$this->createMock(ResetPasswordRequestInterface::class)]];
yield ['removeRequests', [new \stdClass()]];
}

/**
Expand Down

0 comments on commit 598fc74

Please sign in to comment.