Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>
* @author Ryan Weaver <[email protected]>
*
* @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