├── phpstan.dist.neon ├── UPGRADING.md ├── src ├── Exception │ ├── ExpiredResetPasswordTokenException.php │ ├── InvalidResetPasswordTokenException.php │ ├── ResetPasswordExceptionInterface.php │ ├── FakeRepositoryException.php │ └── TooManyPasswordRequestsException.php ├── SymfonyCastsResetPasswordBundle.php ├── Resources │ ├── translations │ │ ├── ResetPasswordBundle.hu.xlf │ │ ├── ResetPasswordBundle.ja.xlf │ │ ├── ResetPasswordBundle.tr.xlf │ │ ├── ResetPasswordBundle.fa.xlf │ │ ├── ResetPasswordBundle.da.xlf │ │ ├── ResetPasswordBundle.id.xlf │ │ ├── ResetPasswordBundle.sr.xlf │ │ ├── ResetPasswordBundle.ar.xlf │ │ ├── ResetPasswordBundle.fi.xlf │ │ ├── ResetPasswordBundle.sk.xlf │ │ ├── ResetPasswordBundle.en.xlf │ │ ├── ResetPasswordBundle.mn.xlf │ │ ├── ResetPasswordBundle.pt.xlf │ │ ├── ResetPasswordBundle.ro.xlf │ │ ├── ResetPasswordBundle.pl.xlf │ │ ├── ResetPasswordBundle.cs.xlf │ │ ├── ResetPasswordBundle.el.xlf │ │ ├── ResetPasswordBundle.ru.xlf │ │ ├── ResetPasswordBundle.it.xlf │ │ ├── ResetPasswordBundle.mk.xlf │ │ ├── ResetPasswordBundle.es.xlf │ │ ├── ResetPasswordBundle.uk.xlf │ │ ├── ResetPasswordBundle.nl.xlf │ │ ├── ResetPasswordBundle.de.xlf │ │ ├── ResetPasswordBundle.fr.xlf │ │ └── ResetPasswordBundle.ca.xlf │ └── config │ │ └── reset_password_services.php ├── Generator │ ├── ResetPasswordRandomGenerator.php │ └── ResetPasswordTokenGenerator.php ├── Model │ ├── ResetPasswordRequestInterface.php │ ├── ResetPasswordTokenComponents.php │ ├── ResetPasswordRequestTrait.php │ └── ResetPasswordToken.php ├── Util │ └── ResetPasswordCleaner.php ├── Command │ └── ResetPasswordRemoveExpiredCommand.php ├── DependencyInjection │ ├── Configuration.php │ └── SymfonyCastsResetPasswordExtension.php ├── Persistence │ ├── Fake │ │ └── FakeResetPasswordInternalRepository.php │ ├── ResetPasswordRequestRepositoryInterface.php │ └── Repository │ │ └── ResetPasswordRequestRepositoryTrait.php ├── ResetPasswordHelperInterface.php ├── Controller │ └── ResetPasswordControllerTrait.php └── ResetPasswordHelper.php ├── .php-cs-fixer.dist.php ├── LICENSE.md ├── composer.json ├── phpstan-baseline.neon ├── README.md └── CHANGELOG.md /phpstan.dist.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | parameters: 4 | level: 6 5 | bootstrapFiles: 6 | - vendor/autoload.php 7 | paths: 8 | - src 9 | #- tests 10 | # excludePaths: 11 | # ignoreErrors: 12 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrade from 1.x to 2.0 2 | 3 | ## ResetPasswordHelper 4 | 5 | - Class became `@final` in `v1.22.0`. Extending this class will not be allowed 6 | in version `v2.0.0`. 7 | 8 | ## ResetPasswordRemoveExpiredCommand 9 | 10 | - Class became `@final` in `v1.22.0`. Extending this class will not be allowed 11 | in version `v2.0.0`. 12 | -------------------------------------------------------------------------------- /src/Exception/ExpiredResetPasswordTokenException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Exception; 11 | 12 | /** 13 | * @author Ryan Weaver 14 | */ 15 | final class ExpiredResetPasswordTokenException extends \Exception implements ResetPasswordExceptionInterface 16 | { 17 | public function getReason(): string 18 | { 19 | return 'The link in your email is expired. Please try to reset your password again.'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/InvalidResetPasswordTokenException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Exception; 11 | 12 | /** 13 | * @author Ryan Weaver 14 | */ 15 | final class InvalidResetPasswordTokenException extends \Exception implements ResetPasswordExceptionInterface 16 | { 17 | public function getReason(): string 18 | { 19 | return 'The reset password link is invalid. Please try to reset your password again.'; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/ResetPasswordExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Exception; 11 | 12 | /** 13 | * @author Ryan Weaver 14 | */ 15 | interface ResetPasswordExceptionInterface extends \Throwable 16 | { 17 | public const MESSAGE_PROBLEM_VALIDATE = 'There was a problem validating your password reset request'; 18 | public const MESSAGE_PROBLEM_HANDLE = 'There was a problem handling your password reset request'; 19 | 20 | public function getReason(): string; 21 | } 22 | -------------------------------------------------------------------------------- /src/Exception/FakeRepositoryException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Exception; 11 | 12 | /** 13 | * @author Jesse Rushlow 14 | * @author Ryan Weaver 15 | */ 16 | final class FakeRepositoryException extends \Exception implements ResetPasswordExceptionInterface 17 | { 18 | public function getReason(): string 19 | { 20 | return 'Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service.'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([__DIR__.'/src', __DIR__.'/tests']) 9 | ->exclude([ 10 | 'tmp' 11 | ]) 12 | ; 13 | 14 | return (new PhpCsFixer\Config()) 15 | ->setRules(array( 16 | '@Symfony' => true, 17 | '@Symfony:risky' => true, 18 | 'header_comment' => [ 19 | 'header' => << 22 | For the full copyright and license information, please view the LICENSE 23 | file that was distributed with this source code. 24 | EOF 25 | ], 26 | // Because of the commented out argument in ResetPasswordHelperInterface 27 | 'no_superfluous_phpdoc_tags' => false, 28 | )) 29 | ->setRiskyAllowed(true) 30 | ->setFinder($finder) 31 | ; 32 | -------------------------------------------------------------------------------- /src/SymfonyCastsResetPasswordBundle.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword; 11 | 12 | use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 13 | use Symfony\Component\HttpKernel\Bundle\Bundle; 14 | use SymfonyCasts\Bundle\ResetPassword\DependencyInjection\SymfonyCastsResetPasswordExtension; 15 | 16 | /** 17 | * @author Jesse Rushlow 18 | * @author Ryan Weaver 19 | */ 20 | class SymfonyCastsResetPasswordBundle extends Bundle 21 | { 22 | public function getContainerExtension(): ?ExtensionInterface 23 | { 24 | if (null === $this->extension) { 25 | $this->extension = new SymfonyCastsResetPasswordExtension(); 26 | } 27 | 28 | return $this->extension ?: null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) SymfonyCasts 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.hu.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% év 8 | 9 | 10 | %count% month|%count% months 11 | %count% hónap 12 | 13 | 14 | %count% day|%count% days 15 | %count% nap 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% óra 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% perc 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Generator/ResetPasswordRandomGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Generator; 11 | 12 | /** 13 | * @author Jesse Rushlow 14 | * @author Ryan Weaver 15 | * 16 | * @internal 17 | * 18 | * @final 19 | */ 20 | class ResetPasswordRandomGenerator 21 | { 22 | /** 23 | * Original credit to Laravel's Str::random() method. 24 | * 25 | * String length is 20 characters 26 | */ 27 | public function getRandomAlphaNumStr(): string 28 | { 29 | $string = ''; 30 | 31 | while (($len = \strlen($string)) < 20) { 32 | /** @var int<1, max> $size */ 33 | $size = 20 - $len; 34 | 35 | $bytes = random_bytes($size); 36 | 37 | $string .= substr( 38 | str_replace(['/', '+', '='], '', base64_encode($bytes)), 0, $size); 39 | } 40 | 41 | return $string; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.ja.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% 年|%count% 年 8 | 9 | 10 | %count% month|%count% months 11 | %count% 月|%count% 月 12 | 13 | 14 | %count% day|%count% days 15 | %count% 日|%count% 日 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% 時間|%count% 時間 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% 分|%count% 分 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.tr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% yıl|%count% yıl 8 | 9 | 10 | %count% month|%count% months 11 | %count% ay|%count% ay 12 | 13 | 14 | %count% day|%count% days 15 | %count% gün|%count% gün 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% saat|%count% saat 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% dakika|%count% dakika 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.fa.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% سال|%count% سال‌ 8 | 9 | 10 | %count% month|%count% months 11 | %count% ماه|%count% ماه‌ 12 | 13 | 14 | %count% day|%count% days 15 | %count% روز|%count% روز 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% ساعت|%count% ساعت 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% دقیقه|%count% دقیقه 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.da.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% år|%count% år 8 | 9 | 10 | %count% month|%count% months 11 | %count% måned|%count% måneder 12 | 13 | 14 | %count% day|%count% days 15 | %count% dag|%count% dage 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% time|%count% timer 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minut|%count% minutter 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.id.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% tahun|%count% tahun 8 | 9 | 10 | %count% month|%count% months 11 | %count% bulan|%count% bulan 12 | 13 | 14 | %count% day|%count% days 15 | %count% hari|%count% hari 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% jam|%count% jam 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% menit|%count% menit 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.sr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% godina|%count% godine 8 | 9 | 10 | %count% month|%count% months 11 | %count% mesec|%count% meseca 12 | 13 | 14 | %count% day|%count% days 15 | %count% dan|%count% dana 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% sat|%count% sata 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minut|%count% minuta 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.ar.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | سنة %count% | سنوات %count% 8 | 9 | 10 | %count% month|%count% months 11 | شهر %count% | أشهر %count% 12 | 13 | 14 | %count% day|%count% days 15 | يوم %count% | أيام %count% 16 | 17 | 18 | %count% hour|%count% hours 19 | ساعة %count% | ساعات %count% 20 | 21 | 22 | %count% minute|%count% minutes 23 | دقيقة %count% | دقائق %count% 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Model/ResetPasswordRequestInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Model; 11 | 12 | /** 13 | * @author Jesse Rushlow 14 | * @author Ryan Weaver 15 | */ 16 | interface ResetPasswordRequestInterface 17 | { 18 | /** 19 | * Get the time the reset password request was created. 20 | */ 21 | public function getRequestedAt(): \DateTimeInterface; 22 | 23 | /** 24 | * Check if the reset password request is expired. 25 | */ 26 | public function isExpired(): bool; 27 | 28 | /** 29 | * Get the time the reset password request expires. 30 | */ 31 | public function getExpiresAt(): \DateTimeInterface; 32 | 33 | /** 34 | * Get the non-public hashed token used to verify a request password request. 35 | */ 36 | public function getHashedToken(): string; 37 | 38 | /** 39 | * Get the user whom requested a password reset. 40 | */ 41 | public function getUser(): object; 42 | } 43 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.fi.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% vuoden|%count% vuoden 8 | 9 | 10 | %count% month|%count% months 11 | %count% kuukauden|%count% kuukauden 12 | 13 | 14 | %count% day|%count% days 15 | %count% päivän|%count% päivän 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% tunnin|%count% tunnin 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuutin|%count% minuutin 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.sk.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% rok|%count% roky|%count% rokov 8 | 9 | 10 | %count% month|%count% months 11 | %count% mesiac|%count% mesiace|%count% mesiacov 12 | 13 | 14 | %count% day|%count% days 15 | %count% den|%count% dni|%count% dní 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hodina|%count% hodiny|%count% hodín 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minúta|%count% minúty|%count% minút 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/Exception/TooManyPasswordRequestsException.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Exception; 11 | 12 | /** 13 | * @author Ryan Weaver 14 | */ 15 | final class TooManyPasswordRequestsException extends \Exception implements ResetPasswordExceptionInterface 16 | { 17 | private $availableAt; 18 | 19 | public function __construct(\DateTimeInterface $availableAt, string $message = '', int $code = 0, ?\Throwable $previous = null) 20 | { 21 | parent::__construct($message, $code, $previous); 22 | 23 | $this->availableAt = $availableAt; 24 | } 25 | 26 | public function getAvailableAt(): \DateTimeInterface 27 | { 28 | return $this->availableAt; 29 | } 30 | 31 | public function getRetryAfter(): int 32 | { 33 | return $this->getAvailableAt()->getTimestamp() - (new \DateTime('now'))->getTimestamp(); 34 | } 35 | 36 | public function getReason(): string 37 | { 38 | return 'You have already requested a reset password email. Please check your email or try again soon.'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Util/ResetPasswordCleaner.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Util; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; 13 | 14 | /** 15 | * @author Jesse Rushlow 16 | * @author Ryan Weaver 17 | * 18 | * @internal 19 | * 20 | * @final 21 | */ 22 | class ResetPasswordCleaner 23 | { 24 | /** 25 | * @var bool Enable/disable garbage collection 26 | */ 27 | private $enabled; 28 | 29 | private $repository; 30 | 31 | public function __construct(ResetPasswordRequestRepositoryInterface $repository, bool $enabled = true) 32 | { 33 | $this->repository = $repository; 34 | $this->enabled = $enabled; 35 | } 36 | 37 | /** 38 | * Clears expired reset password requests from persistence. 39 | * 40 | * Enable/disable in configuration. Calling with $force = true 41 | * will attempt to remove expired requests regardless of 42 | * configuration setting. 43 | */ 44 | public function handleGarbageCollection(bool $force = false): int 45 | { 46 | if ($this->enabled || $force) { 47 | return $this->repository->removeExpiredResetPasswordRequests(); 48 | } 49 | 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Command/ResetPasswordRemoveExpiredCommand.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Command; 11 | 12 | use Symfony\Component\Console\Command\Command; 13 | use Symfony\Component\Console\Input\InputInterface; 14 | use Symfony\Component\Console\Output\OutputInterface; 15 | use SymfonyCasts\Bundle\ResetPassword\Util\ResetPasswordCleaner; 16 | 17 | /** 18 | * @author Jesse Rushlow 19 | * @author Ryan Weaver 20 | * 21 | * @final 22 | */ 23 | class ResetPasswordRemoveExpiredCommand extends Command 24 | { 25 | private $cleaner; 26 | 27 | public function __construct(ResetPasswordCleaner $cleaner) 28 | { 29 | $this->cleaner = $cleaner; 30 | 31 | parent::__construct('reset-password:remove-expired'); 32 | } 33 | 34 | protected function configure(): void 35 | { 36 | $this->setDescription('Remove expired reset password requests from persistence.'); 37 | } 38 | 39 | protected function execute(InputInterface $input, OutputInterface $output): int 40 | { 41 | $output->writeln('Removing expired reset password requests...'); 42 | 43 | $intRemoved = $this->cleaner->handleGarbageCollection(true); 44 | 45 | $output->writeln(\sprintf('Garbage collection successful. Removed %s reset password request object(s).', $intRemoved)); 46 | 47 | return 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Model/ResetPasswordTokenComponents.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Model; 11 | 12 | /** 13 | * @author Jesse Rushlow 14 | * @author Ryan Weaver 15 | * 16 | * @internal 17 | * 18 | * @final 19 | */ 20 | class ResetPasswordTokenComponents 21 | { 22 | /** 23 | * @var string 24 | */ 25 | private $selector; 26 | 27 | /** 28 | * @var string 29 | */ 30 | private $verifier; 31 | 32 | /** 33 | * @var string 34 | */ 35 | private $hashedToken; 36 | 37 | public function __construct(string $selector, string $verifier, string $hashedToken) 38 | { 39 | $this->selector = $selector; 40 | $this->verifier = $verifier; 41 | $this->hashedToken = $hashedToken; 42 | } 43 | 44 | /** 45 | * @return string Non-hashed random string used to fetch request objects from persistence 46 | */ 47 | public function getSelector(): string 48 | { 49 | return $this->selector; 50 | } 51 | 52 | /** 53 | * @return string The hashed non-public token used to validate reset password requests 54 | */ 55 | public function getHashedToken(): string 56 | { 57 | return $this->hashedToken; 58 | } 59 | 60 | /** 61 | * The public token consists of a concatenated random non-hashed selector string and random non-hashed verifier string. 62 | */ 63 | public function getPublicToken(): string 64 | { 65 | return $this->selector.$this->verifier; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\DependencyInjection; 11 | 12 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 13 | use Symfony\Component\Config\Definition\ConfigurationInterface; 14 | 15 | /** 16 | * @author Jesse Rushlow 17 | * @author Ryan Weaver 18 | */ 19 | final class Configuration implements ConfigurationInterface 20 | { 21 | public function getConfigTreeBuilder(): TreeBuilder 22 | { 23 | $treeBuilder = new TreeBuilder('symfonycasts_reset_password'); 24 | $rootNode = $treeBuilder->getRootNode(); 25 | 26 | $rootNode 27 | ->children() 28 | ->scalarNode('request_password_repository') 29 | ->isRequired() 30 | ->info('A class that implements ResetPasswordRequestRepositoryInterface - usually your ResetPasswordRequestRepository.') 31 | ->end() 32 | ->integerNode('lifetime') 33 | ->defaultValue(3600) 34 | ->info('The length of time in seconds that a password reset request is valid for after it is created.') 35 | ->end() 36 | ->integerNode('throttle_limit') 37 | ->defaultValue(3600) 38 | ->info('Another password reset cannot be made faster than this throttle time in seconds.') 39 | ->end() 40 | ->booleanNode('enable_garbage_collection') 41 | ->defaultValue(true) 42 | ->info('Enable/Disable automatic garbage collection.') 43 | ->end(); 44 | 45 | return $treeBuilder; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfonycasts/reset-password-bundle", 3 | "description": "Symfony bundle that adds password reset functionality.", 4 | "type": "symfony-bundle", 5 | "license": "MIT", 6 | "minimum-stability": "dev", 7 | "require": { 8 | "php": ">=8.1.10", 9 | "symfony/config": "^5.4 | ^6.0 | ^7.0 | ^8.0", 10 | "symfony/dependency-injection": "^5.4 | ^6.0 | ^7.0 | ^8.0", 11 | "symfony/deprecation-contracts": "^2.2 | ^3.0", 12 | "symfony/http-kernel": "^5.4 | ^6.0 | ^7.0 | ^8.0" 13 | }, 14 | "require-dev": { 15 | "doctrine/orm": "^2.13", 16 | "symfony/framework-bundle": "^5.4 | ^6.0 | ^7.0 | ^8.0", 17 | "symfony/phpunit-bridge": "^5.4 | ^6.0 | ^7.0 | ^8.0", 18 | "doctrine/doctrine-bundle": "^2.8", 19 | "doctrine/annotations": "^1.0", 20 | "symfony/process": "^6.4 | ^7.0 | ^8.0", 21 | "symfonycasts/internal-test-helpers": "dev-main" 22 | }, 23 | "autoload": { 24 | "psr-4": { 25 | "SymfonyCasts\\Bundle\\ResetPassword\\": "src/" 26 | } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { 30 | "SymfonyCasts\\Bundle\\ResetPassword\\Tests\\": "tests/" 31 | } 32 | }, 33 | "repositories": [ 34 | { 35 | "type": "vcs", 36 | "name": "symfonycasts/internal-test-helpers", 37 | "url": "https://github.com/symfonycasts/internal-test-helpers" 38 | } 39 | ], 40 | "scripts": { 41 | "tools:upgrade": [ 42 | "@tools:upgrade:php-cs-fixer", 43 | "@tools:upgrade:phpstan" 44 | ], 45 | "tools:upgrade:php-cs-fixer": "composer upgrade -W -d tools/php-cs-fixer", 46 | "tools:upgrade:phpstan": "composer upgrade -W -d tools/phpstan", 47 | "tools:run": [ 48 | "@tools:run:php-cs-fixer", 49 | "@tools:run:phpstan" 50 | ], 51 | "tools:run:php-cs-fixer": "tools/php-cs-fixer/vendor/bin/php-cs-fixer fix", 52 | "tools:run:phpstan": "tools/phpstan/vendor/bin/phpstan --memory-limit=1G" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /phpstan-baseline.neon: -------------------------------------------------------------------------------- 1 | parameters: 2 | ignoreErrors: 3 | - 4 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\Command\\\\ResetPasswordRemoveExpiredCommand\\:\\:\\$cleaner has no type specified\\.$#" 5 | count: 1 6 | path: src/Command/ResetPasswordRemoveExpiredCommand.php 7 | 8 | - 9 | message: "#^Method SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\DependencyInjection\\\\Configuration\\:\\:getConfigTreeBuilder\\(\\) return type with generic class Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder does not specify its types\\: T$#" 10 | count: 1 11 | path: src/DependencyInjection/Configuration.php 12 | 13 | - 14 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\Exception\\\\TooManyPasswordRequestsException\\:\\:\\$availableAt has no type specified\\.$#" 15 | count: 1 16 | path: src/Exception/TooManyPasswordRequestsException.php 17 | 18 | - 19 | message: "#^Method SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\Model\\\\ResetPasswordToken\\:\\:getExpirationMessageData\\(\\) return type has no value type specified in iterable type array\\.$#" 20 | count: 1 21 | path: src/Model/ResetPasswordToken.php 22 | 23 | - 24 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\ResetPasswordHelper\\:\\:\\$repository has no type specified\\.$#" 25 | count: 1 26 | path: src/ResetPasswordHelper.php 27 | 28 | - 29 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\ResetPasswordHelper\\:\\:\\$resetPasswordCleaner has no type specified\\.$#" 30 | count: 1 31 | path: src/ResetPasswordHelper.php 32 | 33 | - 34 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\ResetPasswordHelper\\:\\:\\$tokenGenerator has no type specified\\.$#" 35 | count: 1 36 | path: src/ResetPasswordHelper.php 37 | 38 | - 39 | message: "#^PHPDoc tag @param references unknown parameter\\: \\$resetRequestLifetime$#" 40 | count: 1 41 | path: src/ResetPasswordHelperInterface.php 42 | 43 | - 44 | message: "#^Property SymfonyCasts\\\\Bundle\\\\ResetPassword\\\\Util\\\\ResetPasswordCleaner\\:\\:\\$repository has no type specified\\.$#" 45 | count: 1 46 | path: src/Util/ResetPasswordCleaner.php 47 | -------------------------------------------------------------------------------- /src/Generator/ResetPasswordTokenGenerator.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Generator; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordTokenComponents; 13 | 14 | /** 15 | * @author Jesse Rushlow 16 | * @author Ryan Weaver 17 | * 18 | * @internal 19 | * 20 | * @final 21 | */ 22 | class ResetPasswordTokenGenerator 23 | { 24 | /** 25 | * @var string Unique, random, cryptographically secure string 26 | */ 27 | private $signingKey; 28 | 29 | /** 30 | * @var ResetPasswordRandomGenerator 31 | */ 32 | private $randomGenerator; 33 | 34 | public function __construct(string $signingKey, ResetPasswordRandomGenerator $generator) 35 | { 36 | $this->signingKey = $signingKey; 37 | $this->randomGenerator = $generator; 38 | } 39 | 40 | /** 41 | * Get a cryptographically secure token with it's non-hashed components. 42 | * 43 | * @param int|string $userId Unique user identifier 44 | * @param ?string $verifier Only required for token comparison 45 | */ 46 | public function createToken(\DateTimeInterface $expiresAt, $userId, ?string $verifier = null): ResetPasswordTokenComponents 47 | { 48 | if (null === $verifier) { 49 | $verifier = $this->randomGenerator->getRandomAlphaNumStr(); 50 | } 51 | 52 | $selector = $this->randomGenerator->getRandomAlphaNumStr(); 53 | 54 | $encodedData = json_encode([$verifier, $userId, $expiresAt->getTimestamp()]); 55 | 56 | return new ResetPasswordTokenComponents( 57 | $selector, 58 | $verifier, 59 | $this->getHashedToken($encodedData) 60 | ); 61 | } 62 | 63 | private function getHashedToken(string $data): string 64 | { 65 | return base64_encode(hash_hmac('sha256', $data, $this->signingKey, true)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/DependencyInjection/SymfonyCastsResetPasswordExtension.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\DependencyInjection; 11 | 12 | use Symfony\Component\Config\FileLocator; 13 | use Symfony\Component\DependencyInjection\ContainerBuilder; 14 | use Symfony\Component\DependencyInjection\Extension\Extension; 15 | use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | /** 19 | * @author Jesse Rushlow 20 | * @author Ryan Weaver 21 | */ 22 | final class SymfonyCastsResetPasswordExtension extends Extension 23 | { 24 | public function load(array $configs, ContainerBuilder $container): void 25 | { 26 | $loader = new PhpFileLoader($container, new FileLocator(\dirname(__DIR__).'/Resources/config')); 27 | $loader->load('reset_password_services.php'); 28 | 29 | $configuration = $this->getConfiguration($configs, $container); 30 | if (!$configuration) { 31 | throw new \Exception('Configuration is not expected to be null'); 32 | } 33 | 34 | $config = $this->processConfiguration($configuration, $configs); 35 | 36 | $helperDefinition = $container->getDefinition('symfonycasts.reset_password.helper'); 37 | $helperDefinition->replaceArgument(2, new Reference($config['request_password_repository'])); 38 | $helperDefinition->replaceArgument(3, $config['lifetime']); 39 | $helperDefinition->replaceArgument(4, $config['throttle_limit']); 40 | 41 | $cleanerDefinition = $container->getDefinition('symfonycasts.reset_password.cleaner'); 42 | $cleanerDefinition->replaceArgument(0, new Reference($config['request_password_repository'])); 43 | $cleanerDefinition->replaceArgument(1, $config['enable_garbage_collection']); 44 | } 45 | 46 | public function getAlias(): string 47 | { 48 | return 'symfonycasts_reset_password'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/ResetPasswordRequestTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Model; 11 | 12 | use Doctrine\DBAL\Types\Types; 13 | use Doctrine\ORM\Mapping as ORM; 14 | 15 | /** 16 | * @author Jesse Rushlow 17 | * @author Ryan Weaver 18 | */ 19 | trait ResetPasswordRequestTrait 20 | { 21 | /** 22 | * @var string 23 | * 24 | * @ORM\Column(type="string", length=20) 25 | */ 26 | #[ORM\Column(type: Types::STRING, length: 20)] 27 | protected $selector; 28 | 29 | /** 30 | * @var string 31 | * 32 | * @ORM\Column(type="string", length=100) 33 | */ 34 | #[ORM\Column(type: Types::STRING, length: 100)] 35 | protected $hashedToken; 36 | 37 | /** 38 | * @var \DateTimeImmutable 39 | * 40 | * @ORM\Column(type="datetime_immutable") 41 | */ 42 | #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] 43 | protected $requestedAt; 44 | 45 | /** 46 | * @var \DateTimeInterface 47 | * 48 | * @ORM\Column(type="datetime_immutable") 49 | */ 50 | #[ORM\Column(type: Types::DATETIME_IMMUTABLE)] 51 | protected $expiresAt; 52 | 53 | /** @return void */ 54 | protected function initialize(\DateTimeInterface $expiresAt, string $selector, string $hashedToken) 55 | { 56 | $this->requestedAt = new \DateTimeImmutable('now'); 57 | $this->expiresAt = $expiresAt; 58 | $this->selector = $selector; 59 | $this->hashedToken = $hashedToken; 60 | } 61 | 62 | public function getRequestedAt(): \DateTimeInterface 63 | { 64 | return $this->requestedAt; 65 | } 66 | 67 | public function isExpired(): bool 68 | { 69 | return $this->expiresAt->getTimestamp() <= time(); 70 | } 71 | 72 | public function getExpiresAt(): \DateTimeInterface 73 | { 74 | return $this->expiresAt; 75 | } 76 | 77 | public function getHashedToken(): string 78 | { 79 | return $this->hashedToken; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Persistence/Fake/FakeResetPasswordInternalRepository.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Persistence\Fake; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Exception\FakeRepositoryException; 13 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; 14 | use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; 15 | 16 | /** 17 | * Class is only used as a placeholder for the bundle configuration on new installs. 18 | * 19 | * The value of reset_request_repository should be changed to your 20 | * request password repository service in reset_password.yaml. 21 | * 22 | * @author Jesse Rushlow 23 | * @author Ryan Weaver 24 | * 25 | * @internal 26 | */ 27 | final class FakeResetPasswordInternalRepository implements ResetPasswordRequestRepositoryInterface 28 | { 29 | public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface 30 | { 31 | throw new FakeRepositoryException(); 32 | } 33 | 34 | public function getUserIdentifier(object $user): string 35 | { 36 | throw new FakeRepositoryException(); 37 | } 38 | 39 | public function persistResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void 40 | { 41 | throw new FakeRepositoryException(); 42 | } 43 | 44 | public function findResetPasswordRequest(string $selector): ?ResetPasswordRequestInterface 45 | { 46 | throw new FakeRepositoryException(); 47 | } 48 | 49 | public function getMostRecentNonExpiredRequestDate(object $user): ?\DateTimeInterface 50 | { 51 | throw new FakeRepositoryException(); 52 | } 53 | 54 | public function removeResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void 55 | { 56 | throw new FakeRepositoryException(); 57 | } 58 | 59 | public function removeExpiredResetPasswordRequests(): int 60 | { 61 | throw new FakeRepositoryException(); 62 | } 63 | 64 | public function removeRequests(object $user): void 65 | { 66 | throw new FakeRepositoryException(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ResetPasswordHelperInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface; 13 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken; 14 | 15 | /** 16 | * @author Jesse Rushlow 17 | * @author Ryan Weaver 18 | * 19 | * @method ResetPasswordToken generateFakeResetToken(?int $resetRequestLifetime = null) Generates a fake ResetPasswordToken. 20 | */ 21 | interface ResetPasswordHelperInterface 22 | { 23 | /** 24 | * Generate a new ResetPasswordToken that can be provided to the user. 25 | * 26 | * This method must also persist the token information to storage so that 27 | * the validateTokenAndFetchUser() method can verify the token validity 28 | * and removeResetRequest() can eventually invalidate it by removing it 29 | * from storage. 30 | * 31 | * @param ?int $resetRequestLifetime Override the default (to be added to interface in 2.0) 32 | * 33 | * @throws ResetPasswordExceptionInterface 34 | */ 35 | public function generateResetToken(object $user/* , ?int $resetRequestLifetime = null */): ResetPasswordToken; 36 | 37 | /** 38 | * Validate a reset request and fetch the user from persistence. 39 | * 40 | * The token provided to the user from generateResetToken() is validated 41 | * against a token stored in persistence. If the token cannot be validated, 42 | * a ResetPasswordExceptionInterface instance should be thrown. 43 | * 44 | * @param string $fullToken selector string + verifier string provided by the user 45 | * 46 | * @throws ResetPasswordExceptionInterface 47 | */ 48 | public function validateTokenAndFetchUser(string $fullToken): object; 49 | 50 | /** 51 | * Remove a password reset request token from persistence. 52 | * 53 | * Intended to be used after validation - this will typically remove 54 | * the token from storage so that it can't be used again. 55 | * 56 | * @param string $fullToken selector string + verifier string provided by the user 57 | */ 58 | public function removeResetRequest(string $fullToken): void; 59 | 60 | /** 61 | * Get the length of time in seconds a token is valid. 62 | */ 63 | public function getTokenLifetime(): int; 64 | } 65 | -------------------------------------------------------------------------------- /src/Resources/config/reset_password_services.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace Symfony\Component\DependencyInjection\Loader\Configurator; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Command\ResetPasswordRemoveExpiredCommand; 13 | use SymfonyCasts\Bundle\ResetPassword\Generator\ResetPasswordRandomGenerator; 14 | use SymfonyCasts\Bundle\ResetPassword\Generator\ResetPasswordTokenGenerator; 15 | use SymfonyCasts\Bundle\ResetPassword\Persistence\Fake\FakeResetPasswordInternalRepository; 16 | use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelper; 17 | use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface; 18 | use SymfonyCasts\Bundle\ResetPassword\Util\ResetPasswordCleaner; 19 | 20 | return static function (ContainerConfigurator $container) { 21 | $services = $container->services(); 22 | $parameters = $container->parameters(); 23 | 24 | $services->set('symfonycasts.reset_password.fake_request_repository', FakeResetPasswordInternalRepository::class) 25 | ->private(); 26 | 27 | $services->set('symfonycasts.reset_password.cleaner', ResetPasswordCleaner::class) 28 | ->private() 29 | ->args([ 30 | '', // reset password request persister 31 | '', // reset password request enable_garbage_collection 32 | ]); 33 | 34 | $services->set(ResetPasswordRemoveExpiredCommand::class) 35 | ->args([service('symfonycasts.reset_password.cleaner')]) 36 | ->tag('console.command', ['command' => 'reset-password:remove-expired']); 37 | 38 | $services->set('symfonycasts.reset_password.random_generator', ResetPasswordRandomGenerator::class) 39 | ->private(); 40 | 41 | $services->set('symfonycasts.reset_password.token_generator', ResetPasswordTokenGenerator::class) 42 | ->private() 43 | ->args([ 44 | '%kernel.secret%', 45 | service('symfonycasts.reset_password.random_generator'), 46 | ]); 47 | 48 | $services->alias(ResetPasswordHelperInterface::class, 'symfonycasts.reset_password.helper'); 49 | 50 | $services->set('symfonycasts.reset_password.helper', ResetPasswordHelper::class) 51 | ->args([ 52 | service('symfonycasts.reset_password.token_generator'), 53 | service('symfonycasts.reset_password.cleaner'), 54 | '', // reset password request persister 55 | '', // reset password request lifetime 56 | '', // reset password throttle limit 57 | ]); 58 | }; 59 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.en.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% year|%count% years 8 | 9 | 10 | %count% month|%count% months 11 | %count% month|%count% months 12 | 13 | 14 | %count% day|%count% days 15 | %count% day|%count% days 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hour|%count% hours 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minute|%count% minutes 24 | 25 | 26 | There was a problem validating your password reset request 27 | There was a problem validating your password reset request 28 | 29 | 30 | There was a problem handling your password reset request 31 | There was a problem handling your password reset request 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | The link in your email is expired. Please try to reset your password again. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | The reset password link is invalid. Please try to reset your password again. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | You have already requested a reset password email. Please check your email or try again soon. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.mn.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% жил|%count% жил 8 | 9 | 10 | %count% month|%count% months 11 | %count% сар|%count% сар 12 | 13 | 14 | %count% day|%count% days 15 | %count% өдөр|%count% өдөр 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% цаг|%count% цаг 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% минут|%count% минут 24 | 25 | 26 | There was a problem validating your password reset request 27 | Таны нууц үг шинэчлэх хүсэлтийг баталгаажуулахад асуудал гарлаа 28 | 29 | 30 | There was a problem handling your password reset request 31 | Таны нууц үг шинэчлэх хүсэлтийг шийдвэрлэхэд асуудал гарлаа 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Таны имэйл дэх холбоосын хугацаа дууссан байна. Нууц үгээ дахин сэргээхийг оролдоно уу. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | config/packages/reset_password.yaml доторх request_password_repository тохиргоог шинэчилж, "request password repository" service рүүгээ заана уу. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Нууц үг шинэчлэх холбоос буруу байна. Нууц үгээ дахин сэргээхийг оролдоно уу. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Та аль хэдийн нууц үгээ шинэчлэх имэйл хүссэн байна. Имэйлээ шалгаарай эсвэл удахгүй дахин оролдоно уу. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.pt.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% ano|%count% anos 8 | 9 | 10 | %count% month|%count% months 11 | %count% mês|%count% meses 12 | 13 | 14 | %count% day|%count% days 15 | %count% dia|%count% dias 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hora|%count% horas 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuto|%count% minutos 24 | 25 | 26 | There was a problem validating your password reset request 27 | Houve um problema ao validar sua solicitação de redefinição de senha 28 | 29 | 30 | There was a problem handling your password reset request 31 | Houve um problema ao processar sua solicitação de redefinição de senha 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | O link no seu e-mail expirou. Por favor, tente redefinir sua senha novamente. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Por favor, atualize a configuração de request_password_repository em config/packages/reset_password.yaml para apontar para o seu serviço de "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | O link para redefinição de senha é inválido. Por favor, tente redefinir sua senha novamente. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Você já solicitou um e-mail para redefinição de senha. Verifique seu e-mail ou tente novamente em breve. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.ro.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% an|%count% ani 8 | 9 | 10 | %count% month|%count% months 11 | %count% lună|%count% luni 12 | 13 | 14 | %count% day|%count% days 15 | %count% zi|%count% zile 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% oră|%count% ore 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minut|%count% minute 24 | 25 | 26 | There was a problem validating your password reset request 27 | A fost o problemă la validarea cererii tale de resetare a parolei 28 | 29 | 30 | There was a problem handling your password reset request 31 | A fost o problemă la gestionarea cererii tale de resetare a parolei 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Link-ul din emailul tău a expirat. Te rugăm să încerci să-ți resetezi parola din nou. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Te rugăm să actualizezi configurarea request_password_repository în fișierul config/packages/reset_password.yaml pentru a indica serviciul tău "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Link-ul de resetare a parolei este invalid. Te rugăm să încerci să-ți resetezi parola din nou. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Ai solicitat deja un email pentru resetarea parolei. Te rugăm să verifici emailul sau să încerci din nou mai târziu. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.pl.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% rok|%count% lata|%count% lat 8 | 9 | 10 | %count% month|%count% months 11 | %count% miesiąc|%count% miesiące|%count% miesięcy 12 | 13 | 14 | %count% day|%count% days 15 | %count% dzień|%count% dni|%count% dni 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% godzinę|%count% godziny|%count% godzin 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minutę|%count% minuty|%count% minut 24 | 25 | 26 | There was a problem validating your password reset request 27 | Wystąpił problem podczas potwierdzania prośby o zresetowanie hasła 28 | 29 | 30 | There was a problem handling your password reset request 31 | Wystąpił problem z obsługą Twojej prośby o zresetowanie hasła 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Link w Twoim e-mailu wygasł. Spróbuj ponownie zresetować hasło. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Zaktualizuj konfigurację request_password_repository w config/packages/reset_password.yaml, aby wskazywała na usługę "repozytorium żądań haseł". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Link do resetowania hasła jest nieprawidłowy. Spróbuj ponownie zresetować hasło. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Już zażądałeś wiadomości e-mail dotyczącej resetowania hasła. Sprawdź pocztę lub spróbuj ponownie wkrótce. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Persistence/ResetPasswordRequestRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Persistence; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; 13 | 14 | /** 15 | * @author Jesse Rushlow 16 | * @author Ryan Weaver 17 | * 18 | * @method void removeRequests(object $user) Remove a users ResetPasswordRequest objects from persistence. 19 | */ 20 | interface ResetPasswordRequestRepositoryInterface 21 | { 22 | /** 23 | * Create a new ResetPasswordRequest object. 24 | * 25 | * @param object $user User entity - typically implements Symfony\Component\Security\Core\User\UserInterface 26 | * @param string $selector A non-hashed random string used to fetch a request from persistence 27 | * @param string $hashedToken The hashed token used to verify a reset request 28 | */ 29 | public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface; 30 | 31 | /** 32 | * Get the unique user entity identifier from persistence. 33 | * 34 | * @param object $user User entity - typically implements Symfony\Component\Security\Core\User\UserInterface 35 | */ 36 | public function getUserIdentifier(object $user): string; 37 | 38 | /** 39 | * Save a reset password request entity to persistence. 40 | */ 41 | public function persistResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void; 42 | 43 | /** 44 | * Get a reset password request entity from persistence, if one exists, using the request's selector. 45 | * 46 | * @param string $selector A non-hashed random string used to fetch a request from persistence 47 | */ 48 | public function findResetPasswordRequest(string $selector): ?ResetPasswordRequestInterface; 49 | 50 | /** 51 | * Get the most recent non-expired reset password request date for the user, if one exists. 52 | * 53 | * @param object $user User entity - typically implements Symfony\Component\Security\Core\User\UserInterface 54 | */ 55 | public function getMostRecentNonExpiredRequestDate(object $user): ?\DateTimeInterface; 56 | 57 | /** 58 | * Remove this reset password request from persistence and any other for this user. 59 | * 60 | * This method should remove this ResetPasswordRequestInterface and also all 61 | * other ResetPasswordRequestInterface objects in storage for the same user. 62 | */ 63 | public function removeResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void; 64 | 65 | /** 66 | * Remove all expired reset password request objects from persistence. 67 | * 68 | * @return int Number of request objects removed from persistence 69 | */ 70 | public function removeExpiredResetPasswordRequests(): int; 71 | } 72 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.cs.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% rok|%count% roky|%count% let 8 | 9 | 10 | %count% month|%count% months 11 | %count% měsíc|%count% měsíce|%count% měsíců 12 | 13 | 14 | %count% day|%count% days 15 | %count% den|%count% dny|%count% dní 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hodina|%count% hodiny|%count% hodin 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuta|%count% minuty|%count% minut 24 | 25 | 26 | There was a problem validating your password reset request 27 | Při ověřování vaší žádosti o resetování hesla se vyskytl problém 28 | 29 | 30 | There was a problem handling your password reset request 31 | Při zpracování vaší žádosti o resetování hesla se vyskytl problém 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Odkaz v e-mailu je neplatný. Zkuste prosím znovu resetovat vaše heslo. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Aktualizujte prosím konfiguraci request_password_repository v souboru config/packages/reset_password.yaml, aby odkazovala na vaši službu "repozitář pro žádosti o změnu hesla". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Odkaz pro resetování hesla je neplatný. Pokuste se, prosím, o resetování hesla znovu. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Již jste požádali o e-mail pro resetování hesla. Zkontrolujte prosím svůj e-mail nebo to zkuste zase později. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.el.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% έτος|%count% έτη 8 | 9 | 10 | %count% month|%count% months 11 | %count% μήνας|%count% μήνες 12 | 13 | 14 | %count% day|%count% days 15 | %count% ημέρα|%count% ημέρες 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% ώρα|%count% ώρες 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% λεπτό|%count% λεπτά 24 | 25 | 26 | There was a problem validating your password reset request 27 | Παρουσιάστηκε πρόβλημα κατά την επαλήθευση του αιτήματος επαναφοράς του κωδικού πρόσβασής σας 28 | 29 | 30 | There was a problem handling your password reset request 31 | Παρουσιάστηκε πρόβλημα με τον χειρισμό του αιτήματος επαναφοράς του κωδικού πρόσβασής σας 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Ο σύνδεσμος στο email σας έχει λήξει. Δοκιμάστε να επαναφέρετε τον κωδικό πρόσβασής σας ξανά. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Ενημερώστε τη διαμόρφωση request_password_repository στο config/packages/reset_password.yaml για να οδηγεί στην υπηρεσία "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Ο σύνδεσμος επαναφοράς κωδικού πρόσβασης δεν είναι έγκυρος. Δοκιμάστε να επαναφέρετε τον κωδικό πρόσβασής σας ξανά. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Έχετε ήδη ζητήσει email επαναφοράς κωδικού πρόσβασης. Ελέγξτε το email σας ή δοκιμάστε ξανά σύντομα. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.ru.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% год|%count% года|%count% лет 8 | 9 | 10 | %count% month|%count% months 11 | %count% месяц|%count% месяца|%count% месяцев 12 | 13 | 14 | %count% day|%count% days 15 | %count% день|%count% дня|%count% дней 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% час|%count% часа|%count% часов 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% минута|%count% минуты|%count% минут 24 | 25 | 26 | There was a problem validating your password reset request 27 | Возникла проблема с проверкой вашего запроса на сброс пароля 28 | 29 | 30 | There was a problem handling your password reset request 31 | Не удалось обработать ваш запрос на сброс пароля 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Ссылка в вашем письме устарела. Пожалуйста, попробуйте сбросить пароль еще раз. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Обновите конфигурацию request_password_repository в config/packages/reset_password.yaml, чтобы она указывала на ваш "request password repository" сервис. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Ссылка для сброса пароля недействительна. Пожалуйста, попробуйте сбросить пароль еще раз. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Вы уже запрашивали электронное письмо для сброса пароля. Пожалуйста, проверьте свою электронную почту или повторите попытку еще раз в ближайшее время. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.it.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% anno|%count% anni 8 | 9 | 10 | %count% month|%count% months 11 | %count% mese|%count% mesi 12 | 13 | 14 | %count% day|%count% days 15 | %count% giorno|%count% giorni 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% ora|%count% ore 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuto|%count% minuti 24 | 25 | 26 | There was a problem validating your password reset request 27 | Si è verificato un problema durante la convalida della richiesta di reimpostazione della password 28 | 29 | 30 | There was a problem handling your password reset request 31 | Si è verificato un problema durante l'elaborazione della richiesta di reimpostazione della password 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Il link nella tua e-mail è scaduto. Provi a reimpostare nuovamente la password. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Aggiornare la configurazione request_password_repository in config/packages/reset_password.yaml per puntare al servizio "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Il link per la reimpostazione della password non è valido. Riprovare a reimpostare la password 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Hai già richiesto un'e-mail di reimpostazione della password. Si prega di controllare la posta elettronica o di riprovare al più presto. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.mk.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% година|%count% години 8 | 9 | 10 | %count% month|%count% months 11 | %count% месец|%count% месеци 12 | 13 | 14 | %count% day|%count% days 15 | %count% ден|%count% денови 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% час|%count% часови 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% минута|%count% минути 24 | 25 | 26 | There was a problem validating your password reset request 27 | Настана проблем при верификацијата на барањето за ресетирање на лозинката 28 | 29 | 30 | There was a problem handling your password reset request 31 | Настана проблем при справувањето со барањето за ресетирање лозинка 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Врската во вашата е-пошта е истечена. Обидете се повторно да ја ресетирате лозинката. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Ажурирајте ја конфигурацијата на request_password_repository опциијата во config/packages/reset_password.yaml за да покажува кон вашиот сервис "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Врската за ресетирање на лозинката е невалидна. Обидете се повторно да ја ресетирате лозинката. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Веќе побаравте да ви биде испратена порака во електронската пошта за ресетирање на лозинката. Проверете ја вашата е-пошта или обидете се повторно наскоро. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.es.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% año|%count% años 8 | 9 | 10 | %count% month|%count% months 11 | %count% mes|%count% meses 12 | 13 | 14 | %count% day|%count% days 15 | %count% día|%count% días 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hora|%count% horas 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuto|%count% minutos 24 | 25 | 26 | There was a problem validating your password reset request 27 | Error al validar la solicitud de restablecimiento de contraseña 28 | 29 | 30 | There was a problem handling your password reset request 31 | Se ha producido un problema al gestionar la solicitud de restablecimiento de contraseña 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | El enlace de tu correo electrónico ha caducado. Intente restablecer la contraseña de nuevo. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Por favor, actualice la configuración de request_password_repository en config/packages/reset_password.yaml para que apunte a su servicio "request password repository". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | El enlace para restablecer la contraseña no es válido. Por favor, intente restablecer su contraseña de nuevo. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Ya has solicitado un correo electrónico para restablecer la contraseña. Por favor, compruebe su correo electrónico o inténtelo de nuevo pronto. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.uk.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% рік|%count% роки|%count% років 8 | 9 | 10 | %count% month|%count% months 11 | %count% місяць|%count% місяці|%count% місяців 12 | 13 | 14 | %count% day|%count% days 15 | %count% день|%count% дні|%count% днів 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% година|%count% години|%count% годин 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% хвилина|%count% хвилини|%count% хвилин 24 | 25 | 26 | There was a problem validating your password reset request 27 | Виникла проблема під час перевірки вашого запиту на скидання пароля 28 | 29 | 30 | There was a problem handling your password reset request 31 | Виникла проблема з обробкою вашого запиту на скидання пароля 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Термін дії посилання у вашій електронній пошті закінчився. Спробуйте скинути пароль ще раз. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Будь ласка, оновіть конфігурацію request_password_repository у config/packages/reset_password.yaml, щоб вказати на ваш "request password repository" сервіс. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Посилання для скидання пароля недійсне. Спробуйте скинути пароль ще раз. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Ви вже подали запит на скидання пароля електронною поштою. Будь ласка, перевірте свою електронну пошту або повторіть спробу ще раз незабаром. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.nl.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% jaar|%count% jaar 8 | 9 | 10 | %count% month|%count% months 11 | %count% maand|%count% maanden 12 | 13 | 14 | %count% day|%count% days 15 | %count% dag|%count% dagen 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% uur|%count% uren 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minuut|%count% minuten 24 | 25 | 26 | There was a problem validating your password reset request 27 | Er is een probleem opgetreden bij het valideren van het verzoek om je wachtwoord te herstellen 28 | 29 | 30 | There was a problem handling your password reset request 31 | Er is een probleem opgetreden bij het afhandelen van het verzoek om je wachtwoord te herstellen 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | De link in je e-mail is verlopen. Probeer opnieuw je wachtwoord te herstellen. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Update de request_password_repository waarde in config/packages/reset_password.yaml met een verwijzing naar je "request password repository" service. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | De link om je wachtwoord te herstellen is niet geldig. Probeer opnieuw je wachtwoord te herstellen. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Je hebt al een verzoek ingediend voor een e-mail om je wachtwoord te herstellen. Controleer of je een e-mail hebt ontvangen of probeer het later nog eens. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.de.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% Jahr|%count% Jahren 8 | 9 | 10 | %count% month|%count% months 11 | %count% Monat|%count% Monaten 12 | 13 | 14 | %count% day|%count% days 15 | %count% Tag|%count% Tagen 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% Stunde|%count% Stunden 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% Minute|%count% Minuten 24 | 25 | 26 | There was a problem validating your password reset request 27 | Es gab ein Problem bei der Validierung Ihrer Anfrage zum Zurücksetzen des Passworts 28 | 29 | 30 | There was a problem handling your password reset request 31 | Es gab ein Problem bei der Bearbeitung Ihrer Anfrage zum Zurücksetzen des Passworts 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Der Link in Ihrer E-Mail ist abgelaufen. Bitte versuchen Sie erneut, Ihr Passwort zurückzusetzen. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Bitte aktualisieren Sie die request_password_repository-Konfiguration in config/packages/reset_password.yaml, um auf Ihren "request password repository"-Dienst zu verweisen. 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Der Link zum Zurücksetzen des Passworts ist ungültig. Bitte versuchen Sie erneut, Ihr Passwort zurückzusetzen. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Sie haben bereits eine E-Mail mit einem neuen Passwort angefordert. Bitte überprüfen Sie Ihre E-Mail oder versuchen Sie es später erneut. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.fr.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% an|%count% ans 8 | 9 | 10 | %count% month|%count% months 11 | %count% mois|%count% mois 12 | 13 | 14 | %count% day|%count% days 15 | %count% jour|%count% jours 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% heure|%count% heures 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minute|%count% minutes 24 | 25 | 26 | There was a problem validating your password reset request 27 | Un problème est survenu lors de la validation de votre demande de réinitialisation de mot de passe 28 | 29 | 30 | There was a problem handling your password reset request 31 | Un problème est survenu lors du traitement de votre demande de réinitialisation de mot de passe 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | Le lien dans votre e-mail est expiré. Veuillez réessayer de réinitialiser votre mot de passe. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Veuillez mettre à jour la configuration de request_password_repository dans config/packages/reset_password.yaml pour pointer vers votre service "request password repository" 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | Le lien de réinitialisation du mot de passe n'est pas valide. Veuillez réessayer de réinitialiser votre mot de passe 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Vous avez déjà demandé un e-mail de réinitialisation du mot de passe. Veuillez vérifier votre e-mail ou réessayer bientôt. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Resources/translations/ResetPasswordBundle.ca.xlf: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %count% year|%count% years 7 | %count% any|%count% anys 8 | 9 | 10 | %count% month|%count% months 11 | %count% mes|%count% mesos 12 | 13 | 14 | %count% day|%count% days 15 | %count% dia|%count% dies 16 | 17 | 18 | %count% hour|%count% hours 19 | %count% hora|%count% hores 20 | 21 | 22 | %count% minute|%count% minutes 23 | %count% minut|%count% minuts 24 | 25 | 26 | There was a problem validating your password reset request 27 | S'ha produït un problema en validar la vostra sol·licitud de restabliment de la contrasenya 28 | 29 | 30 | There was a problem handling your password reset request 31 | S'ha produït un problema en gestionar la vostra sol·licitud de restabliment de la contrasenya 32 | 33 | 34 | The link in your email is expired. Please try to reset your password again. 35 | L'enllaç del vostre correu electrònic ha caducat. Si us plau, proveu de restablir la contrasenya de nou. 36 | 37 | 38 | Please update the request_password_repository configuration in config/packages/reset_password.yaml to point to your "request password repository" service. 39 | Actualitzeu la configuració request_password_repository a config/packages/reset_password.yaml per apuntar al vostre servei "repositori de sol·licitud de restabliment de la contrasenya". 40 | 41 | 42 | The reset password link is invalid. Please try to reset your password again. 43 | L'enllaç de restabliment de la contrasenya no és vàlid. Si us plau, proveu de restablir la contrasenya de nou. 44 | 45 | 46 | You have already requested a reset password email. Please check your email or try again soon. 47 | Ja heu sol·licitat un correu electrònic de restabliment de la contrasenya. Si us plau, comproveu el vostre correu electrònic o torneu-ho a provar aviat. 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/Controller/ResetPasswordControllerTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Controller; 11 | 12 | use Symfony\Component\HttpFoundation\Request; 13 | use Symfony\Component\HttpFoundation\Session\SessionInterface; 14 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken; 15 | 16 | /** 17 | * Provides useful methods to a "reset password controller". 18 | * 19 | * Use of this trait requires a controller to extend 20 | * Symfony\Bundle\FrameworkBundle\Controller\AbstractController 21 | * 22 | * @author Jesse Rushlow 23 | * @author Ryan Weaver 24 | */ 25 | trait ResetPasswordControllerTrait 26 | { 27 | /** 28 | * @deprecated since 1.3.0, use ResetPasswordControllerTrait::setTokenObjectInSession() instead. 29 | */ 30 | private function setCanCheckEmailInSession(): void 31 | { 32 | trigger_deprecation( 33 | 'symfonycasts/reset-password-bundle', 34 | '1.3.0', 35 | 'Storing the ResetPasswordToken object in the session is more desirable, use ResetPasswordControllerTrait::setTokenObjectInSession() instead.' 36 | ); 37 | 38 | $this->getSessionService()->set('ResetPasswordCheckEmail', true); 39 | } 40 | 41 | /** 42 | * @deprecated since 1.3.0, use ResetPasswordControllerTrait::getTokenObjectFromSession() instead. 43 | */ 44 | private function canCheckEmail(): bool 45 | { 46 | trigger_deprecation( 47 | 'symfonycasts/reset-password-bundle', 48 | '1.3.0', 49 | 'Storing the ResetPasswordToken object in the session is more desirable, use ResetPasswordControllerTrait::getTokenObjectFromSession() instead.' 50 | ); 51 | 52 | return $this->getSessionService()->has('ResetPasswordCheckEmail'); 53 | } 54 | 55 | private function storeTokenInSession(string $token): void 56 | { 57 | $this->getSessionService()->set('ResetPasswordPublicToken', $token); 58 | } 59 | 60 | private function getTokenFromSession(): ?string 61 | { 62 | return $this->getSessionService()->get('ResetPasswordPublicToken'); 63 | } 64 | 65 | private function setTokenObjectInSession(ResetPasswordToken $token): void 66 | { 67 | $token->clearToken(); 68 | 69 | $this->getSessionService()->set('ResetPasswordToken', $token); 70 | } 71 | 72 | private function getTokenObjectFromSession(): ?ResetPasswordToken 73 | { 74 | return $this->getSessionService()->get('ResetPasswordToken'); 75 | } 76 | 77 | private function cleanSessionAfterReset(): void 78 | { 79 | $session = $this->getSessionService(); 80 | 81 | $session->remove('ResetPasswordPublicToken'); 82 | $session->remove('ResetPasswordCheckEmail'); 83 | $session->remove('ResetPasswordToken'); 84 | } 85 | 86 | private function getSessionService(): SessionInterface 87 | { 88 | /** @var Request $request */ 89 | $request = $this->container->get('request_stack')->getCurrentRequest(); 90 | 91 | return $request->getSession(); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Persistence\Repository; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; 13 | 14 | /** 15 | * Trait can be added to a Doctrine ORM repository to help implement 16 | * ResetPasswordRequestRepositoryInterface. 17 | * 18 | * @author Jesse Rushlow 19 | * @author Ryan Weaver 20 | */ 21 | trait ResetPasswordRequestRepositoryTrait 22 | { 23 | public function getUserIdentifier(object $user): string 24 | { 25 | return (string) $this->getEntityManager() 26 | ->getUnitOfWork() 27 | ->getSingleIdentifierValue($user) 28 | ; 29 | } 30 | 31 | public function persistResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void 32 | { 33 | $this->getEntityManager()->persist($resetPasswordRequest); 34 | $this->getEntityManager()->flush(); 35 | } 36 | 37 | public function findResetPasswordRequest(string $selector): ?ResetPasswordRequestInterface 38 | { 39 | return $this->findOneBy(['selector' => $selector]); 40 | } 41 | 42 | public function getMostRecentNonExpiredRequestDate(object $user): ?\DateTimeInterface 43 | { 44 | // Normally there is only 1 max request per use, but written to be flexible 45 | /** @var ResetPasswordRequestInterface $resetPasswordRequest */ 46 | $resetPasswordRequest = $this->createQueryBuilder('t') 47 | ->where('t.user = :user') 48 | ->setParameter('user', $user) 49 | ->orderBy('t.requestedAt', 'DESC') 50 | ->setMaxResults(1) 51 | ->getQuery() 52 | ->getOneOrNullResult() 53 | ; 54 | 55 | if (null !== $resetPasswordRequest && !$resetPasswordRequest->isExpired()) { 56 | return $resetPasswordRequest->getRequestedAt(); 57 | } 58 | 59 | return null; 60 | } 61 | 62 | public function removeResetPasswordRequest(ResetPasswordRequestInterface $resetPasswordRequest): void 63 | { 64 | $this->createQueryBuilder('t') 65 | ->delete() 66 | ->where('t.user = :user') 67 | ->setParameter('user', $resetPasswordRequest->getUser()) 68 | ->getQuery() 69 | ->execute() 70 | ; 71 | } 72 | 73 | public function removeExpiredResetPasswordRequests(): int 74 | { 75 | $time = new \DateTimeImmutable('-1 week'); 76 | $query = $this->createQueryBuilder('t') 77 | ->delete() 78 | ->where('t.expiresAt <= :time') 79 | ->setParameter('time', $time) 80 | ->getQuery() 81 | ; 82 | 83 | return $query->execute(); 84 | } 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 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Model/ResetPasswordToken.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword\Model; 11 | 12 | /** 13 | * @author Jesse Rushlow 14 | * @author Ryan Weaver 15 | */ 16 | final class ResetPasswordToken 17 | { 18 | /** 19 | * @var string|null selector + non-hashed verifier token 20 | */ 21 | private $token; 22 | 23 | /** 24 | * @var \DateTimeInterface 25 | */ 26 | private $expiresAt; 27 | 28 | /** 29 | * @var int|null timestamp when the token was created 30 | */ 31 | private $generatedAt; 32 | 33 | /** 34 | * @var int expiresAt translator interval 35 | */ 36 | private $transInterval = 0; 37 | 38 | public function __construct(string $token, \DateTimeInterface $expiresAt, ?int $generatedAt = null) 39 | { 40 | $this->token = $token; 41 | $this->expiresAt = $expiresAt; 42 | $this->generatedAt = $generatedAt; 43 | 44 | if (null === $generatedAt) { 45 | $this->triggerDeprecation(); 46 | } 47 | } 48 | 49 | /** 50 | * Returns the full token the user should use. 51 | * 52 | * Internally, this consists of two parts - the selector and 53 | * the hashed token - but that's an implementation detail 54 | * of how the token will later be parsed. 55 | */ 56 | public function getToken(): string 57 | { 58 | if (null === $this->token) { 59 | throw new \RuntimeException('The token property is not set. Calling getToken() after calling clearToken() is not allowed.'); 60 | } 61 | 62 | return $this->token; 63 | } 64 | 65 | /** 66 | * Allow the token object to be safely persisted in a session. 67 | */ 68 | public function clearToken(): void 69 | { 70 | $this->token = null; 71 | } 72 | 73 | public function getExpiresAt(): \DateTimeInterface 74 | { 75 | return $this->expiresAt; 76 | } 77 | 78 | /** 79 | * Get the translation message for when a token expires. 80 | * 81 | * This is used in conjunction with getExpirationMessageData() method. 82 | * Example usage in a Twig template: 83 | * 84 | *

{{ components.expirationMessageKey|trans(components.expirationMessageData) }}

85 | * 86 | * symfony/translation is required to translate into a non-English locale. 87 | * 88 | * @throws \LogicException 89 | */ 90 | public function getExpirationMessageKey(): string 91 | { 92 | $interval = $this->getExpiresAtIntervalInstance(); 93 | 94 | switch ($interval) { 95 | case $interval->y > 0: 96 | $this->transInterval = $interval->y; 97 | 98 | return '%count% year|%count% years'; 99 | case $interval->m > 0: 100 | $this->transInterval = $interval->m; 101 | 102 | return '%count% month|%count% months'; 103 | case $interval->d > 0: 104 | $this->transInterval = $interval->d; 105 | 106 | return '%count% day|%count% days'; 107 | case $interval->h > 0: 108 | $this->transInterval = $interval->h; 109 | 110 | return '%count% hour|%count% hours'; 111 | default: 112 | $this->transInterval = $interval->i; 113 | 114 | return '%count% minute|%count% minutes'; 115 | } 116 | } 117 | 118 | /** 119 | * @throws \LogicException 120 | */ 121 | public function getExpirationMessageData(): array 122 | { 123 | $this->getExpirationMessageKey(); 124 | 125 | return ['%count%' => $this->transInterval]; 126 | } 127 | 128 | /** 129 | * Get the interval that the token is valid for. 130 | * 131 | * @throws \LogicException 132 | * 133 | * @psalm-suppress PossiblyFalseArgument 134 | */ 135 | public function getExpiresAtIntervalInstance(): \DateInterval 136 | { 137 | if (null === $this->generatedAt) { 138 | throw new \LogicException(\sprintf('%s initialized without setting the $generatedAt timestamp.', self::class)); 139 | } 140 | 141 | $createdAtTime = \DateTimeImmutable::createFromFormat('U', (string) $this->generatedAt); 142 | 143 | return $this->expiresAt->diff($createdAtTime); 144 | } 145 | 146 | /** 147 | * @psalm-suppress UndefinedFunction 148 | */ 149 | private function triggerDeprecation(): void 150 | { 151 | trigger_deprecation( 152 | 'symfonycasts/reset-password-bundle', 153 | '1.2', 154 | 'Initializing the %s without setting the "$generatedAt" constructor argument is deprecated. The default "null" will be removed in the future.', 155 | self::class 156 | ); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/ResetPasswordHelper.php: -------------------------------------------------------------------------------- 1 | 6 | * For the full copyright and license information, please view the LICENSE 7 | * file that was distributed with this source code. 8 | */ 9 | 10 | namespace SymfonyCasts\Bundle\ResetPassword; 11 | 12 | use SymfonyCasts\Bundle\ResetPassword\Exception\ExpiredResetPasswordTokenException; 13 | use SymfonyCasts\Bundle\ResetPassword\Exception\InvalidResetPasswordTokenException; 14 | use SymfonyCasts\Bundle\ResetPassword\Exception\TooManyPasswordRequestsException; 15 | use SymfonyCasts\Bundle\ResetPassword\Generator\ResetPasswordTokenGenerator; 16 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordRequestInterface; 17 | use SymfonyCasts\Bundle\ResetPassword\Model\ResetPasswordToken; 18 | use SymfonyCasts\Bundle\ResetPassword\Persistence\ResetPasswordRequestRepositoryInterface; 19 | use SymfonyCasts\Bundle\ResetPassword\Util\ResetPasswordCleaner; 20 | 21 | /** 22 | * @author Jesse Rushlow 23 | * @author Ryan Weaver 24 | * 25 | * @final 26 | */ 27 | class ResetPasswordHelper implements ResetPasswordHelperInterface 28 | { 29 | /** 30 | * The first 20 characters of the token are a "selector". 31 | */ 32 | private const SELECTOR_LENGTH = 20; 33 | 34 | private $tokenGenerator; 35 | private $resetPasswordCleaner; 36 | private $repository; 37 | 38 | /** 39 | * @var int How long a token is valid in seconds 40 | */ 41 | private $resetRequestLifetime; 42 | 43 | /** 44 | * @var int Another password reset cannot be made faster than this throttle time in seconds 45 | */ 46 | private $requestThrottleTime; 47 | 48 | public function __construct(ResetPasswordTokenGenerator $generator, ResetPasswordCleaner $cleaner, ResetPasswordRequestRepositoryInterface $repository, int $resetRequestLifetime, int $requestThrottleTime) 49 | { 50 | $this->tokenGenerator = $generator; 51 | $this->resetPasswordCleaner = $cleaner; 52 | $this->repository = $repository; 53 | $this->resetRequestLifetime = $resetRequestLifetime; 54 | $this->requestThrottleTime = $requestThrottleTime; 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | * 60 | * Some of the cryptographic strategies were taken from 61 | * https://paragonie.com/blog/2017/02/split-tokens-token-based-authentication-protocols-without-side-channels 62 | * 63 | * @throws TooManyPasswordRequestsException 64 | */ 65 | public function generateResetToken(object $user, ?int $resetRequestLifetime = null): ResetPasswordToken 66 | { 67 | $this->resetPasswordCleaner->handleGarbageCollection(); 68 | 69 | if ($availableAt = $this->hasUserHitThrottling($user)) { 70 | throw new TooManyPasswordRequestsException($availableAt); 71 | } 72 | 73 | $resetRequestLifetime = $resetRequestLifetime ?? $this->resetRequestLifetime; 74 | 75 | $expiresAt = new \DateTimeImmutable(\sprintf('+%d seconds', $resetRequestLifetime)); 76 | 77 | $generatedAt = ($expiresAt->getTimestamp() - $resetRequestLifetime); 78 | 79 | $tokenComponents = $this->tokenGenerator->createToken($expiresAt, $this->repository->getUserIdentifier($user)); 80 | 81 | $passwordResetRequest = $this->repository->createResetPasswordRequest( 82 | $user, 83 | $expiresAt, 84 | $tokenComponents->getSelector(), 85 | $tokenComponents->getHashedToken() 86 | ); 87 | 88 | $this->repository->persistResetPasswordRequest($passwordResetRequest); 89 | 90 | // final "public" token is the selector + non-hashed verifier token 91 | return new ResetPasswordToken( 92 | $tokenComponents->getPublicToken(), 93 | $expiresAt, 94 | $generatedAt 95 | ); 96 | } 97 | 98 | /** 99 | * @throws ExpiredResetPasswordTokenException 100 | * @throws InvalidResetPasswordTokenException 101 | */ 102 | public function validateTokenAndFetchUser(string $fullToken): object 103 | { 104 | $this->resetPasswordCleaner->handleGarbageCollection(); 105 | 106 | if (40 !== \strlen($fullToken)) { 107 | throw new InvalidResetPasswordTokenException(); 108 | } 109 | 110 | $resetRequest = $this->findResetPasswordRequest($fullToken); 111 | 112 | if (null === $resetRequest) { 113 | throw new InvalidResetPasswordTokenException(); 114 | } 115 | 116 | if ($resetRequest->isExpired()) { 117 | throw new ExpiredResetPasswordTokenException(); 118 | } 119 | 120 | $user = $resetRequest->getUser(); 121 | 122 | $hashedVerifierToken = $this->tokenGenerator->createToken( 123 | $resetRequest->getExpiresAt(), 124 | $this->repository->getUserIdentifier($user), 125 | substr($fullToken, self::SELECTOR_LENGTH) 126 | ); 127 | 128 | if (false === hash_equals($resetRequest->getHashedToken(), $hashedVerifierToken->getHashedToken())) { 129 | throw new InvalidResetPasswordTokenException(); 130 | } 131 | 132 | return $user; 133 | } 134 | 135 | /** 136 | * @throws InvalidResetPasswordTokenException 137 | */ 138 | public function removeResetRequest(string $fullToken): void 139 | { 140 | $request = $this->findResetPasswordRequest($fullToken); 141 | 142 | if (null === $request) { 143 | throw new InvalidResetPasswordTokenException(); 144 | } 145 | 146 | $this->repository->removeResetPasswordRequest($request); 147 | } 148 | 149 | public function getTokenLifetime(): int 150 | { 151 | return $this->resetRequestLifetime; 152 | } 153 | 154 | /** 155 | * Generate a fake reset token. 156 | * 157 | * Use this to generate a fake token so that you can, for example, show a 158 | * "reset confirmation email sent" page that includes a valid "expiration date", 159 | * even if the email was not actually found (and so, a true ResetPasswordToken 160 | * was not actually created). 161 | * 162 | * This method should not be used when timing attacks are a concern. 163 | */ 164 | public function generateFakeResetToken(?int $resetRequestLifetime = null): ResetPasswordToken 165 | { 166 | $resetRequestLifetime = $resetRequestLifetime ?? $this->resetRequestLifetime; 167 | $expiresAt = new \DateTimeImmutable(\sprintf('+%d seconds', $resetRequestLifetime)); 168 | 169 | $generatedAt = ($expiresAt->getTimestamp() - $resetRequestLifetime); 170 | 171 | $fakeToken = bin2hex(random_bytes(16)); 172 | 173 | return new ResetPasswordToken($fakeToken, $expiresAt, $generatedAt); 174 | } 175 | 176 | private function findResetPasswordRequest(string $token): ?ResetPasswordRequestInterface 177 | { 178 | $selector = substr($token, 0, self::SELECTOR_LENGTH); 179 | 180 | return $this->repository->findResetPasswordRequest($selector); 181 | } 182 | 183 | private function hasUserHitThrottling(object $user): ?\DateTimeInterface 184 | { 185 | /** @var \DateTime|\DateTimeImmutable|null $lastRequestDate */ 186 | $lastRequestDate = $this->repository->getMostRecentNonExpiredRequestDate($user); 187 | 188 | if (null === $lastRequestDate) { 189 | return null; 190 | } 191 | 192 | $availableAt = (clone $lastRequestDate)->add(new \DateInterval("PT{$this->requestThrottleTime}S")); 193 | 194 | if ($availableAt > new \DateTime('now')) { 195 | return $availableAt; 196 | } 197 | 198 | return null; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ResetPasswordBundle: Mind-Blowing (and Secure) Password Resetting for Symfony 2 | 3 | [![CI](https://github.com/SymfonyCasts/reset-password-bundle/actions/workflows/ci.yaml/badge.svg)](https://github.com/SymfonyCasts/reset-password-bundle/actions/workflows/ci.yaml) 4 | 5 | Worrying about how to deal with users that can't remember their password? We've 6 | got you covered! This bundle provides a secure out of the box solution to allow 7 | users to reset their forgotten passwords. 8 | 9 | ## Installation 10 | 11 | The bundle can be installed using Composer or the [Symfony binary](https://symfony.com/download): 12 | 13 | ``` 14 | composer require symfonycasts/reset-password-bundle 15 | ``` 16 | 17 | ## Usage 18 | 19 | There are two ways to get started, the easiest and preferred way is to use 20 | Symfony's [MakerBundle](https://github.com/symfony/maker-bundle). The Maker will 21 | take care of everything from creating configuration, to generating your 22 | templates, controllers, and entities. 23 | 24 | ### Using Symfony's Maker Bundle (Recommended) 25 | 26 | - Run `bin/console make:reset-password`, answer a couple questions, and enjoy our bundle! 27 | 28 | ### Setting things up manually 29 | 30 | If you prefer to take care of the leg work yourself, checkout the 31 | [manual setup](https://github.com/SymfonyCasts/reset-password-bundle/blob/master/docs/manual-setup.md) 32 | guide. We still recommend using the Maker command to get a feel for how we 33 | intended the bundle to be used. 34 | 35 | --- 36 | 37 | If you used our Symfony Maker command `bin/console make:reset-password` after 38 | installation, your app is ready to go. Go to `https://your-apps-domain/reset-password`, 39 | fill out the form, click on the link sent to your email, and change your password. 40 | That's it! The ResetPasswordBundle takes care of the rest. 41 | 42 | The above assumes you have already setup 43 | [authentication](https://symfony.com/doc/current/security.html) with a 44 | registered user account & configured Symfony's 45 | [mailer](https://symfony.com/doc/current/mailer.html) in your app. 46 | 47 | ## Configuration 48 | 49 | You can change the default configuration parameters for the bundle in the 50 | `config/packages/reset_password.yaml` config file created by Maker. 51 | 52 | ```yaml 53 | symfonycasts_reset_password: 54 | request_password_repository: App\Repository\ResetPasswordRequestRepository 55 | lifetime: 3600 56 | throttle_limit: 3600 57 | enable_garbage_collection: true 58 | ``` 59 | 60 | If using PHP configuration files: 61 | 62 |
63 | config/packages/reset_password.php 64 | 65 | ```php 66 | use App\Repository\ResetPasswordRequestRepository; 67 | use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; 68 | 69 | return static function (ContainerConfigurator $containerConfigurator): void { 70 | $containerConfigurator->extension('symfonycasts_reset_password', [ 71 | 'request_password_repository' => ResetPasswordRequestRepository::class, 72 | 'lifetime' => 3600, 73 | 'throttle_limit' => 3600, 74 | 'enable_garbage_collection' => true, 75 | ]); 76 | }; 77 | ``` 78 |
79 | 80 | 81 | The production environment may require the `default_uri` to be defined in the `config/packages/routing.yaml` to prevent the URI in emails to point to localhost. 82 | 83 | ```yaml 84 | # config/packages/routing.yaml 85 | when@prod: 86 | framework: 87 | router: 88 | # ... 89 | default_uri: '' 90 | ``` 91 | 92 | If using PHP configuration files: 93 | 94 |
95 | config/packages/routing.php 96 | 97 | ```php 98 | if ($containerConfigurator->env() === 'prod') { 99 | $containerConfigurator->extension('framework', [ 100 | 'router' => [ 101 | # ... 102 | 'default_uri' => '' 103 | ], 104 | ]); 105 | } 106 | ``` 107 |
108 | 109 | 110 | ### Parameters: 111 | 112 | #### `request_password_repository` 113 | 114 | _Required_ 115 | 116 | The complete namespace of the repository for the `ResetPasswordRequest` entity. If 117 | you used `make:reset-password`, this will be `App\Repository\ResetPasswordRequestRepository`. 118 | 119 | #### `lifetime` 120 | 121 | _Optional_ - Defaults to `3600` seconds 122 | 123 | This is the length of time a reset password request is valid for in seconds 124 | after it has been created. 125 | 126 | #### `throttle_limit` 127 | 128 | _Optional_ - Defaults to `3600` seconds 129 | 130 | This is the length of time in seconds that must pass before a user can request a 131 | subsequent reset request. 132 | 133 | Setting this value _equal to or higher_ than `lifetime` will prevent a user from 134 | requesting a password reset before a previous reset attempt has either 1) Been 135 | successfully completed. 2) The previous request has expired. 136 | 137 | Setting this value _lower_ than `lifetime` will allow a user to make several 138 | reset password requests, even if any previous requests have _not_ been successfully 139 | completed or have not expired. This would allow for cases such as a user never 140 | received the reset password request email. 141 | 142 | #### `enable_garbage_collection` 143 | 144 | _Optional_ - Defaults to `true` 145 | 146 | Enable or disable the Reset Password Cleaner which handles expired reset password 147 | requests that may have been left in persistence. 148 | 149 | ## Advanced Usage 150 | 151 | ### Purging `ResetPasswordRequest` objects from persistence 152 | 153 | The `ResetPasswordRequestRepositoryInterface::removeRequests()` method, which is 154 | implemented in the 155 | [ResetPasswordRequestRepositoryTrait](https://github.com/SymfonyCasts/reset-password-bundle/blob/main/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php), 156 | can be used to remove all request objects from persistence for a single user. This 157 | differs from the 158 | [garbage collection mechanism](https://github.com/SymfonyCasts/reset-password-bundle/blob/df64d82cca2ee371da5e8c03c227457069ae663e/src/Persistence/Repository/ResetPasswordRequestRepositoryTrait.php#L73) 159 | which only removes _expired_ request objects for _all_ users automatically. 160 | 161 | Typically, you'd call this method when you need to remove request object(s) for 162 | a user who changed their email address due to suspicious activity and potentially 163 | has valid request objects in persistence with their "old" compromised email address. 164 | 165 | ```php 166 | // ProfileController 167 | 168 | #[Route(path: '/profile/{id}', name: 'app_update_profile', methods: ['GET', 'POST'])] 169 | public function profile(Request $request, User $user, ResetPasswordRequestRepositoryInterface $repository): Response 170 | { 171 | $originalEmail = $user->getEmail(); 172 | 173 | $form = $this->createFormBuilder($user) 174 | ->add('email', EmailType::class) 175 | ->add('save', SubmitType::class, ['label' => 'Save Profile']) 176 | ->getForm() 177 | ; 178 | 179 | $form->handleRequest($request); 180 | 181 | if ($form->isSubmitted() && $form->isValid()) { 182 | if ($originalEmail !== $user->getEmail()) { 183 | // The user changed their email address. 184 | // Remove any old reset requests for the user. 185 | $repository->removeRequests($user); 186 | } 187 | 188 | // Persist the user object and redirect... 189 | } 190 | 191 | return $this->render('profile.html.twig', ['form' => $form]); 192 | } 193 | ``` 194 | 195 | ## Support 196 | 197 | Feel free to open an issue for questions, problems, or suggestions with our bundle. 198 | Issues pertaining to Symfony's Maker Bundle, specifically `make:reset-password`, 199 | should be addressed in the [Symfony Maker repository](https://github.com/symfony/maker-bundle). 200 | 201 | ## Security Issues 202 | For **security related vulnerabilities**, we ask that you send an email to 203 | `ryan [at] symfonycasts.com` instead of creating an issue. 204 | 205 | This will give us the opportunity to address the issue without exposing the 206 | vulnerability before a fix can be published. 207 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | *We intend to follow [Semantic Versioning 2.0.0](https://semver.org/), if you 4 | find a change that break's semver, please create an issue.* 5 | 6 | ## [v1.22.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.22.0) 7 | 8 | *June 6th, 2024* 9 | 10 | ### Feature 11 | 12 | - [#296](https://github.com/SymfonyCasts/reset-password-bundle/pull/296) - [command] make class final - *@jrushlow* 13 | - [#297](https://github.com/SymfonyCasts/reset-password-bundle/pull/297) - [ResetPasswordHelper] class to become final - *@jrushlow* 14 | - [#305](https://github.com/SymfonyCasts/reset-password-bundle/pull/305) - [trait] add type annotations to ResetPasswordRequestTrait - *@jrushlow* 15 | - [#310](https://github.com/SymfonyCasts/reset-password-bundle/pull/310) - [generator] [1.x] userId argument should be either a string or int - *@jrushlow* 16 | - [#320](https://github.com/SymfonyCasts/reset-password-bundle/pull/320) - Add missing Czech translations - *@dfridrich* 17 | 18 | ## [v1.21.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.21.0) 19 | 20 | *March 5th, 2024* 21 | 22 | ### Feature 23 | 24 | - [#284](https://github.com/SymfonyCasts/reset-password-bundle/pull/284) - [persistence] remove ResetPasswordRequest objects programmatically - *@jrushlow* 25 | 26 | ## [v1.20.3](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.20.3) 27 | 28 | *February 20th, 2024* 29 | 30 | ### Minor 31 | 32 | - [#283](https://github.com/SymfonyCasts/reset-password-bundle/pull/283) - [ci] php-cs-fixer it up - *@jrushlow* 33 | 34 | ## [v1.20.2](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.20.2) 35 | 36 | *January 22nd, 2024* 37 | 38 | ### Bug 39 | 40 | - [#280](https://github.com/SymfonyCasts/reset-password-bundle/pull/280) - Add ro transalation - *@dragosholban* 41 | - [#281](https://github.com/SymfonyCasts/reset-password-bundle/pull/281) - Fix risky falsy comparison thanks to Psalm - *@bocharsky-bw* 42 | 43 | ## [v1.20.1](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.20.1) 44 | 45 | *January 2nd, 2024* 46 | 47 | ### Bug 48 | 49 | - [#279](https://github.com/SymfonyCasts/reset-password-bundle/pull/279) - Fix incorrect case of getOneOrNullResult - *@glaubinix* 50 | 51 | ## [v1.20.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.20.0) 52 | 53 | *December 18th, 2023* 54 | 55 | ### Feature 56 | 57 | - [#277](https://github.com/SymfonyCasts/reset-password-bundle/pull/277) - Drop legacy PHP 7.2-8.0 support - *@bocharsky-bw* 58 | - [#278](https://github.com/SymfonyCasts/reset-password-bundle/pull/278) - Added Mongolian translations - *erkhem42* 59 | 60 | ## [v1.19.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.19.0) 61 | 62 | *December 1st, 2023* 63 | 64 | ### Feature 65 | 66 | - [#274](https://github.com/SymfonyCasts/reset-password-bundle/pull/274) - Allow Symfony 7 - *@bocharsky-bw* 67 | 68 | ## [v1.18.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.18.0) 69 | 70 | *September 19th, 2023* 71 | 72 | ### Feature 73 | 74 | - [#260](https://github.com/symfonycasts/reset-password-bundle/pull/260) - [ci] handle possible return types - *@jrushlow* 75 | - [#271](https://github.com/symfonycasts/reset-password-bundle/pull/271) - Add el, mk translations - *@zmakrevski* 76 | 77 | ### Bug 78 | 79 | - [#263](https://github.com/symfonycasts/reset-password-bundle/pull/263) - chore: fix type cast in `ResetPasswordRequestRepositoryTrait` when using `declare(strict_types=1);` - *@Crovitche-1623* 80 | 81 | ## [v1.17.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.17.0) 82 | 83 | *February 2nd, 2023* 84 | 85 | ### Feature 86 | 87 | - [#257](https://github.com/symfonycasts/reset-password-bundle/pull/257) - Allow overriding the $resetRequestLifetime when generating a token - *@kbond* 88 | 89 | ## [v1.16.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.16.0) 90 | 91 | *October 4th, 2022* 92 | 93 | ### Feature 94 | 95 | - [#245](https://github.com/symfonycasts/reset-password-bundle/pull/245) - [translations] add additional Polish translations and fix pluralizations - *@Flower7C3* 96 | - [#242](https://github.com/symfonycasts/reset-password-bundle/pull/242) - [translations] Additional German translations - *@dennis-g* 97 | 98 | ## [v1.15.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.15.0) 99 | 100 | *September 13th, 2022* 101 | 102 | ### Feature 103 | 104 | - [#237](https://github.com/symfonycasts/reset-password-bundle/pull/237) - drop symfony 4.4 support - *@jrushlow* 105 | 106 | ### Bug 107 | 108 | - [#235](https://github.com/symfonycasts/reset-password-bundle/pull/235) - fix expiration diff bug in php 8.1 - *@jrushlow* 109 | 110 | ## [v1.14.1](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.14.1) 111 | 112 | *August 6th, 2022* 113 | 114 | ### Bug 115 | 116 | - [#234](https://github.com/symfonycasts/reset-password-bundle/pull/234) - [command] Fix Override deprecation - *@chindit* 117 | 118 | ## [v1.14.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.14.0) 119 | 120 | *July 12th, 2022* 121 | 122 | ### Feature 123 | 124 | - [#229](https://github.com/symfonycasts/reset-password-bundle/pull/229) - Add catalan translations - *@victormhg* 125 | ### Bug 126 | 127 | - [#230](https://github.com/symfonycasts/reset-password-bundle/pull/230) - Add missing target-language attr to all translations - *@bocharsky-bw* 128 | 129 | ## [v1.13.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.13.0) 130 | 131 | *February 23rd, 2022* 132 | 133 | ### Feature 134 | 135 | - [#216](https://github.com/symfonycasts/reset-password-bundle/pull/216) - Changed private to protected for the ResetPasswordRequestTrait - *@DavidBilodeau1* 136 | - [#215](https://github.com/symfonycasts/reset-password-bundle/pull/215) - Added spanish translation for exceptions - *@idmarinas* 137 | - [#214](https://github.com/symfonycasts/reset-password-bundle/pull/214) - Add French translations for the exceptions - *@DennisdeBest* 138 | 139 | ## [v1.12.0](https://github.com/symfonycasts/reset-password-bundle/releases/tag/v1.12.0) 140 | 141 | *February 14th, 2022* 142 | 143 | ### Feature 144 | 145 | - [#211](https://github.com/symfonycasts/reset-password-bundle/pull/211) - Add Dutch translations for exception reasons - *@codedmonkey* 146 | - [#210](https://github.com/symfonycasts/reset-password-bundle/pull/210) - Add Russian translations for exception reasons - *@bocharsky-bw* 147 | - [#209](https://github.com/symfonycasts/reset-password-bundle/pull/209) - Add Ukrainian translations for exception reasons - *@bocharsky-bw* 148 | - [#206](https://github.com/symfonycasts/reset-password-bundle/pull/206) - RFC: Add translations for exception reasons - *@bocharsky-bw* 149 | - [#200](https://github.com/symfonycasts/reset-password-bundle/pull/200) - Added Slovak translation. - *@zalesak* 150 | - [#196](https://github.com/symfonycasts/reset-password-bundle/pull/196) - [translations] Add Hungarian translations - *@1ed* 151 | 152 | 153 | ## [v1.11.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.11.0) 154 | 155 | *Nov 30th, 2021* 156 | 157 | ### Feature 158 | 159 | - [#193](https://github.com/SymfonyCasts/reset-password-bundle/pull/193) - allow deprecation contracts 3.0 - *@jrushlow* 160 | 161 | ## [v1.10.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.10.0) 162 | 163 | *Nov 18th, 2021* 164 | 165 | ### Feature 166 | 167 | - [#183](https://github.com/SymfonyCasts/reset-password-bundle/pull/183) - add method return types for symfony 6 support - *@jrushlow* 168 | - [#184](https://github.com/SymfonyCasts/reset-password-bundle/pull/184) - [translations] add Dutch translations - *@jrushlow* 169 | - [#185](https://github.com/SymfonyCasts/reset-password-bundle/pull/185) - [translations] add Czech translations - *@jrushlow* 170 | - [#187](https://github.com/SymfonyCasts/reset-password-bundle/pull/187) - [translations] Add Japanese translation - *@jrushlow* 171 | - [#189](https://github.com/SymfonyCasts/reset-password-bundle/pull/187) - add support for symfony 6 - *@jrushlow* 172 | 173 | ## [v1.9.1](https://github.com/symfony/maker-bundle/releases/tag/v1.9.1) 174 | 175 | *July 12th, 2021* 176 | 177 | ### Bug Fix 178 | 179 | - [#176](https://github.com/SymfonyCasts/reset-password-bundle/pull/176) - Allow using PHP 8 attributes for doctrine entities - *@Tobion* 180 | 181 | ## [v1.9.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.9.0) 182 | 183 | *June 30th, 2021* 184 | 185 | ### Feature 186 | 187 | - [#169](https://github.com/SymfonyCasts/reset-password-bundle/pull/169) - Add Finnish language translations - *@nicodemuz* 188 | 189 | ## [v1.8.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.8.0) 190 | 191 | *May 5th, 2021* 192 | 193 | ### Feature 194 | 195 | - [#165](https://github.com/SymfonyCasts/reset-password-bundle/pull/165) - Add Arabic Translations - *@zairigimad* 196 | 197 | ## [v1.7.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.7.0) 198 | 199 | *April 12th, 2021* 200 | 201 | ### Feature 202 | 203 | - [#164](https://github.com/SymfonyCasts/reset-password-bundle/pull/164) - Turkish translation file - *@mssoylu* 204 | 205 | ## [v1.6.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.6.0) 206 | 207 | *March 31st, 2021* 208 | 209 | ### Feature 210 | 211 | - [#156](https://github.com/SymfonyCasts/reset-password-bundle/pull/156) - add ability to generate a fake reset token - *@jrushlow* 212 | 213 | ## [v1.5.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.5.0) 214 | 215 | *March 4th, 2021* 216 | 217 | ### Feature 218 | 219 | - [#160](https://github.com/SymfonyCasts/reset-password-bundle/pull/160) - Add Italian translation - *@cristoforocervino* 220 | - [#158](https://github.com/SymfonyCasts/reset-password-bundle/pull/158) - Add Portuguese translation- *@larzuk91* 221 | - [#157](https://github.com/SymfonyCasts/reset-password-bundle/pull/157) - Add spanish translations - *@larzuk91* 222 | 223 | ### Bug Fix 224 | 225 | - [#155](https://github.com/SymfonyCasts/reset-password-bundle/pull/155) - Typo fr translation - *@maxhelias* 226 | - [#153](https://github.com/SymfonyCasts/reset-password-bundle/pull/153) - Update ResetPasswordBundle.pl.xlf - *@thomas2411* 227 | 228 | ## [v1.4.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.4.0) 229 | 230 | *February 17th, 2021* 231 | 232 | ### Feature 233 | 234 | - [#152](https://github.com/SymfonyCasts/reset-password-bundle/pull/152) - Add Russian translation - *@bocharsky-bw* 235 | - [#151](https://github.com/SymfonyCasts/reset-password-bundle/pull/151) - Add Ukrainian translation - *@bocharsky-bw* 236 | - [#150](https://github.com/SymfonyCasts/reset-password-bundle/pull/150) - add Serbian translations - *@jrushlow* 237 | - [#149](https://github.com/SymfonyCasts/reset-password-bundle/pull/149) - Enhancement: Add Polish translations - *@thomas2411* 238 | - [#148](https://github.com/SymfonyCasts/reset-password-bundle/pull/148) - Enhancement: Add French translations - *@routmoute* 239 | - [#145](https://github.com/SymfonyCasts/reset-password-bundle/pull/145) - Enhancement: Add German translations - *@OskarStark* 240 | 241 | ## [v1.3.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.3.0) 242 | 243 | *January 15th, 2021* 244 | 245 | ### Feature 246 | 247 | - [#143](https://github.com/SymfonyCasts/reset-password-bundle/pull/143) - helper methods for storing ResetPasswordToken in session - *@jrushlow* 248 | 249 | ## [v1.2.2](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.2.2) 250 | 251 | *December 19th, 2020* 252 | 253 | ### Bug Fix 254 | 255 | - [#139](https://github.com/SymfonyCasts/reset-password-bundle/pull/139) - revert unintended default UTC tz on expiresAt - *@jrushlow* 256 | 257 | ## [v1.2.1](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.2.1) 258 | 259 | *December 18th, 2020* 260 | 261 | ### Bug Fix 262 | 263 | - [#135](https://github.com/SymfonyCasts/reset-password-bundle/pull/135) - improve token expiration - *@jrushlow* 264 | 265 | ## [v1.2.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.2.0) 266 | 267 | *December 10th, 2020* 268 | 269 | ### Feature 270 | 271 | - [#134](https://github.com/SymfonyCasts/reset-password-bundle/pull/134) - Allow installation with PHP8, add Symfony 5.2 to tests - *@ker0x* 272 | 273 | ## [v1.1.1](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.1.1) 274 | 275 | *April 18th, 2020* 276 | 277 | ### Bug Fix 278 | 279 | - [#105](https://github.com/SymfonyCasts/reset-password-bundle/pull/105) - [bug] ensure all requests are removed for user - *@kbond* 280 | 281 | ## [v1.1.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.1.0) 282 | 283 | *April 17th, 2020* 284 | 285 | ### Feature 286 | 287 | - [#104](https://github.com/SymfonyCasts/reset-password-bundle/pull/104) - [feature] add additional detail to TooManyPasswordRequestsException - *@kbond* 288 | 289 | ### Bug Fix 290 | 291 | - [#103](https://github.com/SymfonyCasts/reset-password-bundle/pull/103) - [bug] increase time before expired requests are garbage collected to 1 week - *@kbond* 292 | - [#99](https://github.com/SymfonyCasts/reset-password-bundle/pull/99) - Fix typo hasUserHisThrottling to hasUserHitThrottling (his/hit) - *@cjhaas* 293 | 294 | ## [v1.0.0](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.0.0) 295 | 296 | *April 5th, 2020* 297 | 298 | ### Bug Fix 299 | 300 | - [#93](https://github.com/SymfonyCasts/reset-password-bundle/pull/93) - fixed remove-expired CLI command error - *@jrushlow* 301 | 302 | ## [v1.0.0-BETA2](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.0.0-BETA2) 303 | 304 | *April 3rd, 2020* 305 | 306 | ### Bug Fix 307 | 308 | - [#79](https://github.com/SymfonyCasts/reset-password-bundle/pull/79) - fixed incorrect fake repo namespace - *@jrushlow* 309 | 310 | ## [v1.0.0-BETA1](https://github.com/SymfonyCasts/reset-password-bundle/releases/tag/v1.0.0-BETA1) 311 | 312 | *March 27th, 2020* 313 | 314 | - Initial pre-release for testing 315 | --------------------------------------------------------------------------------