Skip to content

Commit 598fc74

Browse files
authored
feature #284 [persistence] remove ResetPasswordRequest objects programmatically
1 parent df64d82 commit 598fc74

File tree

6 files changed

+101
-0
lines changed

6 files changed

+101
-0
lines changed

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,52 @@ _Optional_ - Defaults to `true`
105105
Enable or disable the Reset Password Cleaner which handles expired reset password
106106
requests that may have been left in persistence.
107107

108+
## Advanced Usage
109+
110+
### Purging `ResetPasswordRequest` objects from persistence
111+
112+
The `ResetPasswordRequestRepositoryInterface::removeRequests()` method, which is
113+
implemented in the
114+
[ResetPasswordRequestRepositoryTrait](https://github.com/SymfonyCasts/reset-password-bundle/blob/main/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php),
115+
can be used to remove all request objects from persistence for a single user. This
116+
differs from the
117+
[garbage collection mechanism](https://github.com/SymfonyCasts/reset-password-bundle/blob/df64d82cca2ee371da5e8c03c227457069ae663e/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php#L73)
118+
which only removes _expired_ request objects for _all_ users automatically.
119+
120+
Typically, you'd call this method when you need to remove request object(s) for
121+
a user who changed their email address due to suspicious activity and potentially
122+
has valid request objects in persistence with their "old" compromised email address.
123+
124+
```php
125+
// ProfileController
126+
127+
#[Route(path: '/profile/{id}', name: 'app_update_profile', methods: ['GET', 'POST'])]
128+
public function profile(Request $request, User $user, ResetPasswordRequestRepositoryInterface $repository): Response
129+
{
130+
$originalEmail = $user->getEmail();
131+
132+
$form = $this->createFormBuilder($user)
133+
->add('email', EmailType::class)
134+
->add('save', SubmitType::class, ['label' => 'Save Profile'])
135+
->getForm()
136+
;
137+
138+
$form->handleRequest($request);
139+
140+
if ($form->isSubmitted() && $form->isValid()) {
141+
if ($originalEmail !== $user->getEmail()) {
142+
// The user changed their email address.
143+
// Remove any old reset requests for the user.
144+
$repository->removeRequests($user);
145+
}
146+
147+
// Persist the user object and redirect...
148+
}
149+
150+
return $this->render('profile.html.twig', ['form' => $form]);
151+
}
152+
```
153+
108154
## Support
109155

110156
Feel free to open an issue for questions, problems, or suggestions with our bundle.

src/Persistence/Fake/FakeResetPasswordInternalRepository.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,9 @@ public function removeExpiredResetPasswordRequests(): int
6060
{
6161
throw new FakeRepositoryException();
6262
}
63+
64+
public function removeRequests(object $user): void
65+
{
66+
throw new FakeRepositoryException();
67+
}
6368
}

src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,24 @@ public function removeExpiredResetPasswordRequests(): int
8282

8383
return $query->execute();
8484
}
85+
86+
/**
87+
* Remove a users ResetPasswordRequest objects from persistence.
88+
*
89+
* Warning - This is a destructive operation. Calling this method
90+
* may have undesired consequences for users who have valid
91+
* ResetPasswordRequests but have not "checked their email" yet.
92+
*
93+
* @see https://github.com/SymfonyCasts/reset-password-bundle?tab=readme-ov-file#advanced-usage
94+
*/
95+
public function removeRequests(object $user): void
96+
{
97+
$query = $this->createQueryBuilder('t')
98+
->delete()
99+
->where('t.user = :user')
100+
->setParameter('user', $user)
101+
;
102+
103+
$query->getQuery()->execute();
104+
}
85105
}

src/Persistence/ResetPasswordRequestRepositoryInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
/**
1515
* @author Jesse Rushlow <jr@rushlow.dev>
1616
* @author Ryan Weaver <ryan@symfonycasts.com>
17+
*
18+
* @method void removeRequests(object $user) Remove a users ResetPasswordRequest objects from persistence.
1719
*/
1820
interface ResetPasswordRequestRepositoryInterface
1921
{

tests/FunctionalTests/Persistence/ResetPasswordRequestRepositoryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,33 @@ public function testRemovedExpiredResetPasswordRequestsOnlyRemovedExpiredRequest
208208
self::assertSame($futureFixture, $result[0]);
209209
}
210210

211+
public function testRemoveRequestsRemovesAllRequestsForASingleUser(): void
212+
{
213+
$this->manager->persist($userFixture = new ResetPasswordTestFixtureUser());
214+
$requestFixtures = [new ResetPasswordTestFixtureRequest(), new ResetPasswordTestFixtureRequest()];
215+
216+
foreach ($requestFixtures as $fixture) {
217+
$fixture->user = $userFixture;
218+
219+
$this->manager->persist($fixture);
220+
}
221+
222+
$this->manager->persist($differentUserFixture = new ResetPasswordTestFixtureUser());
223+
224+
$existingRequestFixture = new ResetPasswordTestFixtureRequest();
225+
$existingRequestFixture->user = $differentUserFixture;
226+
227+
$this->manager->persist($existingRequestFixture);
228+
$this->manager->flush();
229+
230+
self::assertCount(3, $this->repository->findAll());
231+
232+
$this->repository->removeRequests($userFixture);
233+
234+
self::assertCount(1, $result = $this->repository->findAll());
235+
self::assertSame($existingRequestFixture, $result[0]);
236+
}
237+
211238
private function configureDatabase(): void
212239
{
213240
$metaData = $this->manager->getMetadataFactory();

tests/UnitTests/Persistence/FakeResetPasswordInternalRepositoryTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public function methodDataProvider(): \Generator
2828
yield ['findResetPasswordRequest', ['']];
2929
yield ['getMostRecentNonExpiredRequestDate', [new \stdClass()]];
3030
yield ['removeResetPasswordRequest', [$this->createMock(ResetPasswordRequestInterface::class)]];
31+
yield ['removeRequests', [new \stdClass()]];
3132
}
3233

3334
/**

0 commit comments

Comments
 (0)