├── .markdownlint.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── Resources
└── config
│ └── routing.xml
├── composer.json
├── config
├── api_platform.xml
├── doctrine
│ └── AbstractPasswordToken.orm.xml
└── services.xml
├── phpunit-legacy.xml.dist
├── rector.php
└── src
├── Bridge
└── ApiPlatform
│ ├── OpenApi
│ ├── AbstractOpenApiFactory.php
│ └── OpenApiFactory.php
│ └── Serializer
│ └── DocumentationNormalizer.php
├── Controller
├── ForgotPasswordController.php
├── GetToken.php
├── ResetPassword.php
└── UpdatePassword.php
├── CoopTilleulsForgotPasswordBundle.php
├── DependencyInjection
├── BCExtensionTrait.php
├── CompilerPass
│ └── ApiPlatformCompilerPass.php
├── Configuration.php
├── CoopTilleulsForgotPasswordExtension.php
└── CoopTilleulsForgotPasswordLegacyExtension.php
├── Entity
└── AbstractPasswordToken.php
├── Event
├── CreateTokenEvent.php
├── ForgotPasswordEvent.php
├── PolyfillEvent.php
├── UpdatePasswordEvent.php
└── UserNotFoundEvent.php
├── EventListener
├── ExceptionEventListener.php
├── MainRequestTrait.php
└── RequestEventListener.php
├── Exception
├── InvalidJsonHttpException.php
├── JsonHttpExceptionInterface.php
├── MissingFieldHttpException.php
├── NoParameterException.php
├── UnauthorizedFieldException.php
└── UndefinedProviderException.php
├── Manager
├── Bridge
│ ├── DoctrineManager.php
│ └── ManagerInterface.php
├── ForgotPasswordManager.php
└── PasswordTokenManager.php
├── Normalizer
├── JMSNormalizer.php
├── NormalizerInterface.php
└── SymfonyNormalizer.php
├── Provider
├── Provider.php
├── ProviderChain.php
├── ProviderChainInterface.php
└── ProviderInterface.php
├── Routing
└── RouteLoader.php
└── TokenGenerator
├── Bridge
└── Bin2HexTokenGenerator.php
└── TokenGeneratorInterface.php
/.markdownlint.json:
--------------------------------------------------------------------------------
1 | {
2 | "MD013": {
3 | "line_length": 120,
4 | "code_block_line_length": 200
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making
6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size,
7 | disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race,
8 | religion, or sexual identity and orientation.
9 |
10 | ## Our Standards
11 |
12 | Examples of behavior that contributes to creating a positive environment include:
13 |
14 | * Using welcoming and inclusive language
15 | * Being respectful of differing viewpoints and experiences
16 | * Gracefully accepting constructive criticism
17 | * Focusing on what is best for the community
18 | * Showing empathy towards other community members
19 |
20 | Examples of unacceptable behavior by participants include:
21 |
22 | * The use of sexualized language or imagery and unwelcome sexual attention or advances
23 | * Trolling, insulting/derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or electronic address, without explicit permission
26 | * Other conduct which could reasonably be considered inappropriate in a professional setting
27 |
28 | ## Our Responsibilities
29 |
30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take
31 | appropriate and fair corrective action in response to any instances of unacceptable behavior.
32 |
33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits,
34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any
35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
36 |
37 | ## Scope
38 |
39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the
40 | project or its community. Examples of representing a project or community include using an official project e-mail
41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline
42 | event. Representation of a project may be further defined and clarified by project maintainers.
43 |
44 | ## Enforcement
45 |
46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at
47 | gregoire+abuse@les-tilleuls.coop We will review and investigate all complaints, and will respond in a way that it deems
48 | appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter
49 | of an incident. Further details of specific enforcement policies may be posted separately.
50 |
51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent
52 | repercussions as determined by other members of the project's leadership.
53 |
54 | ## Attribution
55 |
56 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available
57 | at [http://contributor-covenant.org/version/1/4][version]
58 |
59 | [homepage]: http://contributor-covenant.org
60 |
61 | [version]: http://contributor-covenant.org/version/1/4/
62 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to CoopTilleulsForgotPasswordBundle
2 |
3 | First of all, thank you for contributing, you're awesome!
4 |
5 | To have your code integrated in the CoopTilleulsForgotPasswordBundle project, there is some rules to follow, but don't
6 | panic, it's easy!
7 |
8 | ## Reporting bugs
9 |
10 | If you happen to find a bug, we kindly request you to report it using GitHub by following these 3 points:
11 |
12 | * Check if the bug is not already reported
13 | * A clear title to resume the issue
14 | * A description of the workflow needed to reproduce the bug
15 |
16 | > _NOTE:_ Don't hesitate giving as much information as you can (OS, PHP version, extensions...)
17 |
18 | ## Pull Requests
19 |
20 | ### Matching coding standards
21 |
22 | The CoopTilleulsForgotPasswordBundle project follows [Symfony coding standards](https://symfony.com/doc/current/contributing/code/standards.html).
23 | But don't worry, you can fix CS issues automatically using the [PHP CS Fixer](http://cs.sensiolabs.org/) tool. Run phpcs fixer:
24 |
25 | ```bash
26 | vendor/bin/php-cs-fixer fix
27 | ```
28 |
29 | And then, add fixed file to your commit before push. Be sure to add only **your modified files**. If another files are
30 | fixed by cs tools, just revert it before commit.
31 |
32 | ### Sending a Pull Request
33 |
34 | When you send a PR, just make sure that:
35 |
36 | * You add valid test cases (Behat and PHPUnit)
37 | * Tests are green
38 | * You add some documentation (PHPDoc & user doc: README, custom documentation file)
39 | * You make the PR on the same branch you based your changes on. If you see commits that you did not make in your PR,
40 | you're doing it wrong
41 | * Also don't forget to add a comment when you update a PR with a ping to the maintainer (`@vincentchalamon`),
42 | so we will get a notification
43 | * [Squash your commits](#squash-your-commits) into one commit
44 |
45 | All Pull Requests must include [this header](.github/PULL_REQUEST_TEMPLATE.md).
46 |
47 | ## Squash your commits
48 |
49 | If you have 3 commits. So start with:
50 |
51 | ```bash
52 | git rebase -i HEAD~3
53 | ```
54 |
55 | An editor will be opened with your 3 commits, all prefixed by `pick`.
56 |
57 | Replace all `pick` prefixes by `fixup` (or `f`) **except the first commit** of the list.
58 |
59 | Save and quit the editor.
60 |
61 | After that, all your commits where squashed into the first one and the commit message of the first commit.
62 |
63 | If you would like to rename your commit message type:
64 |
65 | ```bash
66 | git commit --amend
67 | ```
68 |
69 | Now force push to update your PR:
70 |
71 | ```bash
72 | git push --force
73 | ```
74 |
75 | # License and copyright attribution
76 |
77 | When you open a Pull Request to the CoopTilleulsForgotPasswordBundle project, you agree to license your code under the
78 | [MIT license](LICENSE) and to transfer the copyright on the submitted code to Vincent CHALAMON.
79 |
80 | Be sure to you have the right to do that (if you are a professional, ask your company)!
81 |
82 | If you include code from another project, please mention it in the Pull Request description and credit the original
83 | author.
84 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT license
2 |
3 | Copyright (c) 2016 La Coopérative des Tilleuls
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is furnished
10 | to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CoopTilleulsForgotPasswordBundle
2 |
3 | This Symfony bundle provides a _forgot password_ feature for a REST API.
4 | It is bridged for [API Platform](https://api-platform.com/).
5 |
6 | [](https://github.com/coopTilleuls/CoopTilleulsForgotPasswordBundle/actions)
7 | [](https://packagist.org/packages/tilleuls/forgot-password-bundle)
8 | [](https://github.com/coopTilleuls/CoopTilleulsForgotPasswordBundle/blob/main/LICENSE)
9 |
10 | ## Installation
11 |
12 | [Read the installation guide](docs/index.md).
13 |
14 | ## Contributing
15 |
16 | [Read the contributing guide](CONTRIBUTING.md).
17 |
18 | ## Backward Compatibility Promise
19 |
20 | This project follows the [Semantic Versioning](https://semver.org/), and the Backward Compatibility promise from
21 | the [Symfony framework](https://symfony.com/doc/current/contributing/code/bc.html).
22 |
23 | ## Credits
24 |
25 | Created by [Vincent CHALAMON](https://github.com/vincentchalamon) for [Les-Tilleuls.coop](https://les-tilleuls.coop/).
26 |
--------------------------------------------------------------------------------
/Resources/config/routing.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "tilleuls/forgot-password-bundle",
3 | "type": "symfony-bundle",
4 | "description": "Provides a 'forgot password' feature for a REST API",
5 | "keywords": [
6 | "Forgot password",
7 | "REST",
8 | "API"
9 | ],
10 | "license": "MIT",
11 | "homepage": "https://github.com/coopTilleuls/CoopTilleulsForgotPasswordBundle",
12 | "authors": [
13 | {
14 | "name": "Vincent CHALAMON",
15 | "email": "vincent@les-tilleuls.coop"
16 | }
17 | ],
18 | "require": {
19 | "php": ">=8.1",
20 | "symfony/config": "^5.1 || ^6.0 || ^7.0",
21 | "symfony/dependency-injection": "^5.1 || ^6.0 || ^7.0",
22 | "symfony/event-dispatcher": "^5.1 || ^6.0 || ^7.0",
23 | "symfony/http-foundation": "^5.1 || ^6.0 || ^7.0",
24 | "symfony/http-kernel": "^5.1.5 || ^6.0 || ^7.0",
25 | "symfony/serializer": "^5.1 || ^6.0 || ^7.0"
26 | },
27 | "require-dev": {
28 | "ext-json": "*",
29 | "behat/behat": "^3.1",
30 | "dg/bypass-finals": "^1.1",
31 | "doctrine/data-fixtures": "^1.2 || ^2.0",
32 | "doctrine/doctrine-bundle": "^2.11",
33 | "doctrine/orm": "^2.6.3 || ^3.0",
34 | "friends-of-behat/symfony-extension": "^2.0.11 || ^2.1.0",
35 | "jms/serializer-bundle": "^1.4 || ^2.3 || ^3.0 || ^4.0 || ^5.0",
36 | "laminas/laminas-code": "^3.4 || ^4.0",
37 | "ocramius/proxy-manager": "^2.0.4",
38 | "phpunit/phpunit": "^10.0 || ^11.0 || ^12.0",
39 | "symfony/asset": "^5.1 || ^6.0 || ^7.0",
40 | "symfony/browser-kit": "^5.1 || ^6.0 || ^7.0",
41 | "symfony/framework-bundle": "^5.1 || ^6.0 || ^7.0",
42 | "symfony/mailer": "^5.1 || ^6.0 || ^7.0",
43 | "symfony/property-access": "^5.1 || ^6.0 || ^7.0",
44 | "symfony/security-bundle": "^5.1 || ^6.0 || ^7.0",
45 | "symfony/stopwatch": "^5.1 || ^6.0 || ^7.0",
46 | "symfony/templating": "^5.1 || ^6.0 || ^7.0",
47 | "symfony/twig-bundle": "^5.1 || ^6.0 || ^7.0",
48 | "symfony/var-dumper": "^5.1 || ^6.0 || ^7.0"
49 | },
50 | "suggest": {
51 | "doctrine/doctrine-bundle": "To connect with Doctrine in Symfony project",
52 | "doctrine/orm": "To connect with Doctrine",
53 | "api-platform/openapi": "To connect with API Platform"
54 | },
55 | "autoload": {
56 | "psr-4": {
57 | "CoopTilleuls\\ForgotPasswordBundle\\": "src/"
58 | }
59 | },
60 | "autoload-dev": {
61 | "psr-4": {
62 | "CoopTilleuls\\ForgotPasswordBundle\\Tests\\": "tests",
63 | "App\\": "features/app/src"
64 | }
65 | },
66 | "config": {
67 | "sort-packages": true,
68 | "allow-plugins": {
69 | "composer/package-versions-deprecated": true
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/config/api_platform.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/config/doctrine/AbstractPasswordToken.orm.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/config/services.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/phpunit-legacy.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | tests
23 |
24 |
25 |
26 |
27 |
28 | src
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | use Rector\Config\RectorConfig;
15 | use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
16 |
17 | return RectorConfig::configure()
18 | ->withPaths([
19 | __DIR__.'/features',
20 | __DIR__.'/src',
21 | __DIR__.'/tests',
22 | ])
23 | // uncomment to reach your current PHP version
24 | // ->withPhpSets()
25 | ->withRules([
26 | AddVoidReturnTypeWhereNoReturnRector::class,
27 | ])
28 | ->withPhpSets(php81: true);
29 |
--------------------------------------------------------------------------------
/src/Bridge/ApiPlatform/OpenApi/AbstractOpenApiFactory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Bridge\ApiPlatform\OpenApi;
15 |
16 | use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface as LegacyOpenApiFactoryInterface;
17 | use ApiPlatform\Core\OpenApi\Model\Operation as LegacyOperation;
18 | use ApiPlatform\Core\OpenApi\Model\PathItem as LegacyPathItem;
19 | use ApiPlatform\Core\OpenApi\Model\RequestBody as LegacyRequestBody;
20 | use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
21 | use ApiPlatform\OpenApi\Model\Operation;
22 | use ApiPlatform\OpenApi\Model\PathItem;
23 | use ApiPlatform\OpenApi\Model\RequestBody;
24 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface;
25 | use Symfony\Component\Routing\RouterInterface;
26 |
27 | /**
28 | * @author Vincent CHALAMON
29 | */
30 | abstract class AbstractOpenApiFactory
31 | {
32 | public function __construct(protected readonly LegacyOpenApiFactoryInterface|OpenApiFactoryInterface $decorated, protected readonly RouterInterface $router, protected readonly ProviderChainInterface $providerChain)
33 | {
34 | }
35 |
36 | public function __invoke(array $context = [])
37 | {
38 | $routes = $this->router->getRouteCollection();
39 | $openApi = ($this->decorated)($context);
40 | $schemas = $openApi->getComponents()->getSchemas();
41 | $paths = $openApi->getPaths();
42 |
43 | $resetProperties = [];
44 | $requestProperties = [];
45 | foreach ($this->providerChain->all() as $provider) {
46 | $userPasswordField = $provider->getUserPasswordField();
47 | if (!\array_key_exists($userPasswordField, $resetProperties)) {
48 | $resetProperties[$userPasswordField] = [
49 | 'type' => 'object',
50 | 'required' => [$userPasswordField],
51 | 'properties' => [
52 | $userPasswordField => ['type' => 'string'],
53 | ],
54 | ];
55 | }
56 |
57 | $userAuthorizedFields = $provider->getUserAuthorizedFields();
58 | foreach ($userAuthorizedFields as $userAuthorizedField) {
59 | if (!\array_key_exists($userAuthorizedField, $requestProperties)) {
60 | $requestProperties[$userAuthorizedField] = [
61 | 'type' => 'object',
62 | 'required' => [$userAuthorizedField],
63 | 'properties' => [
64 | $userAuthorizedField => [
65 | 'type' => ['string', 'integer'],
66 | ],
67 | ],
68 | ];
69 | }
70 | }
71 | }
72 | $resetSchema = 1 < \count($resetProperties) ? ['oneOf' => array_values($resetProperties)] : array_values($resetProperties)[0];
73 | $requestSchema = 1 < \count($requestProperties) ? ['oneOf' => array_values($requestProperties)] : array_values($requestProperties)[0];
74 |
75 | $schemas['ForgotPassword:reset'] = new \ArrayObject($resetSchema);
76 |
77 | $schemas['ForgotPassword:request'] = new \ArrayObject($requestSchema);
78 |
79 | $schemas['ForgotPassword:validate'] = new \ArrayObject([
80 | 'type' => ['object', 'null'],
81 | ]);
82 |
83 | $resetForgotPasswordPath = $routes->get('coop_tilleuls_forgot_password.reset')->getPath();
84 | $paths->addPath($resetForgotPasswordPath, ($paths->getPath($resetForgotPasswordPath) ?: (class_exists(PathItem::class) ? new PathItem() : new LegacyPathItem()))
85 | ->withRef('ForgotPassword')
86 | ->withPost((class_exists(Operation::class) ? new Operation() : new LegacyOperation())
87 | ->withOperationId('postForgotPassword')
88 | ->withTags(['Forgot password'])
89 | ->withResponses([
90 | 204 => [
91 | 'description' => 'Valid email address, no matter if user exists or not',
92 | ],
93 | 422 => [
94 | 'description' => 'Missing email parameter or invalid format',
95 | ],
96 | ])
97 | ->withSummary('Generates a token and send email')
98 | ->withParameters([
99 | [
100 | 'name' => 'FP-provider',
101 | 'in' => 'header',
102 | 'required' => false,
103 | 'schema' => [
104 | 'type' => 'string',
105 | ],
106 | ],
107 | ])
108 | ->withRequestBody((class_exists(RequestBody::class) ? new RequestBody() : new LegacyRequestBody())
109 | ->withDescription('Request a new password')
110 | ->withRequired(true)
111 | ->withContent(new \ArrayObject([
112 | 'application/json' => [
113 | 'schema' => [
114 | '$ref' => '#/components/schemas/ForgotPassword:request',
115 | ],
116 | ],
117 | ])
118 | )
119 | )
120 | )
121 | );
122 |
123 | $getForgotPasswordPath = $routes->get('coop_tilleuls_forgot_password.get_token')->getPath();
124 | $paths->addPath($getForgotPasswordPath, ($paths->getPath($getForgotPasswordPath) ?: (class_exists(PathItem::class) ? new PathItem() : new LegacyPathItem()))
125 | ->withRef('ForgotPassword')
126 | ->withGet((class_exists(Operation::class) ? new Operation() : new LegacyOperation())
127 | ->withOperationId('getForgotPassword')
128 | ->withTags(['Forgot password'])
129 | ->withResponses([
130 | 200 => [
131 | 'description' => 'Authenticated user',
132 | 'content' => [
133 | 'application/json' => [
134 | 'schema' => [
135 | '$ref' => '#/components/schemas/ForgotPassword:validate',
136 | ],
137 | ],
138 | ],
139 | ],
140 | 404 => [
141 | 'description' => 'Token not found or expired',
142 | ],
143 | ])
144 | ->withSummary('Validates token')
145 | ->withParameters([
146 | [
147 | 'name' => 'tokenValue',
148 | 'in' => 'path',
149 | 'required' => true,
150 | 'schema' => [
151 | 'type' => 'string',
152 | ],
153 | ],
154 | [
155 | 'name' => 'FP-provider',
156 | 'in' => 'header',
157 | 'required' => false,
158 | 'schema' => [
159 | 'type' => 'string',
160 | ],
161 | ],
162 | ],
163 | )
164 | )
165 | );
166 |
167 | $updateForgotPasswordPath = $routes->get('coop_tilleuls_forgot_password.update')->getPath();
168 | $paths->addPath($updateForgotPasswordPath, ($paths->getPath($updateForgotPasswordPath) ?: (class_exists(PathItem::class) ? new PathItem() : new LegacyPathItem()))
169 | ->withRef('ForgotPassword')
170 | ->withPost((class_exists(Operation::class) ? new Operation() : new LegacyOperation())
171 | ->withOperationId('postForgotPasswordToken')
172 | ->withTags(['Forgot password'])
173 | ->withResponses([
174 | 204 => [
175 | 'description' => 'Email address format valid, no matter if user exists or not',
176 | ],
177 | 422 => [
178 | 'description' => 'Missing password parameter',
179 | ],
180 | 404 => [
181 | 'description' => 'Token not found',
182 | ],
183 | ])
184 | ->withSummary('Validates token')
185 | ->withParameters([
186 | [
187 | 'name' => 'tokenValue',
188 | 'in' => 'path',
189 | 'required' => true,
190 | 'schema' => [
191 | 'type' => 'string',
192 | ],
193 | ],
194 | [
195 | 'name' => 'FP-provider',
196 | 'in' => 'header',
197 | 'required' => false,
198 | 'schema' => [
199 | 'type' => 'string',
200 | ],
201 | ],
202 | ])
203 | ->withRequestBody((class_exists(RequestBody::class) ? new RequestBody() : new LegacyRequestBody())
204 | ->withDescription('Reset password')
205 | ->withRequired(true)
206 | ->withContent(new \ArrayObject([
207 | 'application/json' => [
208 | 'schema' => [
209 | '$ref' => '#/components/schemas/ForgotPassword:reset',
210 | ],
211 | ],
212 | ])
213 | )
214 | )
215 | )
216 | );
217 |
218 | return $openApi;
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/Bridge/ApiPlatform/OpenApi/OpenApiFactory.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Bridge\ApiPlatform\OpenApi;
15 |
16 | use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface as LegacyOpenApiFactoryInterface;
17 | use ApiPlatform\Core\OpenApi\OpenApi as LegacyOpenApi;
18 | use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface;
19 | use ApiPlatform\OpenApi\OpenApi;
20 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface;
21 | use Symfony\Component\Routing\RouterInterface;
22 |
23 | if (interface_exists(OpenApiFactoryInterface::class)) {
24 | final class OpenApiFactory extends AbstractOpenApiFactory implements OpenApiFactoryInterface
25 | {
26 | public function __construct(OpenApiFactoryInterface $decorated, RouterInterface $router, ProviderChainInterface $providerChain)
27 | {
28 | parent::__construct($decorated, $router, $providerChain);
29 | }
30 |
31 | public function __invoke(array $context = []): OpenApi
32 | {
33 | return parent::__invoke($context);
34 | }
35 | }
36 | } else {
37 | final class OpenApiFactory extends AbstractOpenApiFactory implements LegacyOpenApiFactoryInterface
38 | {
39 | public function __construct(LegacyOpenApiFactoryInterface $decorated, RouterInterface $router, ProviderChainInterface $providerChain)
40 | {
41 | parent::__construct($decorated, $router, $providerChain);
42 | }
43 |
44 | public function __invoke(array $context = []): LegacyOpenApi
45 | {
46 | return parent::__invoke($context);
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Bridge/ApiPlatform/Serializer/DocumentationNormalizer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Bridge\ApiPlatform\Serializer;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface;
17 | use Symfony\Component\Routing\RouterInterface;
18 | use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
19 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
20 |
21 | /**
22 | * @author Vincent CHALAMON
23 | */
24 | final class DocumentationNormalizer implements NormalizerInterface
25 | {
26 | public function __construct(private readonly NormalizerInterface $decorated, private readonly RouterInterface $router, private readonly ProviderChainInterface $providerChain)
27 | {
28 | }
29 |
30 | public function normalize($object, $format = null, array $context = []): array
31 | {
32 | $routes = $this->router->getRouteCollection();
33 | $docs = $this->decorated->normalize($object, $format, $context);
34 |
35 | $resetProperties = [];
36 | $requestProperties = [];
37 | foreach ($this->providerChain->all() as $provider) {
38 | $userPasswordField = $provider->getUserPasswordField();
39 | if (!\array_key_exists($userPasswordField, $resetProperties)) {
40 | $resetProperties[$userPasswordField] = [
41 | 'type' => 'object',
42 | 'required' => [$userPasswordField],
43 | 'properties' => [
44 | $userPasswordField => ['type' => 'string'],
45 | ],
46 | ];
47 | }
48 |
49 | $userAuthorizedFields = $provider->getUserAuthorizedFields();
50 | foreach ($userAuthorizedFields as $userAuthorizedField) {
51 | if (!\array_key_exists($userAuthorizedField, $requestProperties)) {
52 | $requestProperties[$userAuthorizedField] = [
53 | 'type' => 'object',
54 | 'required' => [$userAuthorizedField],
55 | 'properties' => [
56 | $userAuthorizedField => [
57 | 'type' => ['string', 'integer'],
58 | ],
59 | ],
60 | ];
61 | }
62 | }
63 | }
64 | $resetSchema = 1 < \count($resetProperties) ? ['oneOf' => array_values($resetProperties)] : array_values($resetProperties)[0];
65 | $requestSchema = 1 < \count($requestProperties) ? ['oneOf' => array_values($requestProperties)] : array_values($requestProperties)[0];
66 |
67 | // Add POST /forgot-password/ path
68 | $docs['tags'][] = ['name' => 'Forgot password'];
69 | $docs['paths'][$routes->get('coop_tilleuls_forgot_password.reset')->getPath()]['post'] = [
70 | 'tags' => ['Forgot password'],
71 | 'operationId' => 'postForgotPassword',
72 | 'summary' => 'Generates a token and send email',
73 | 'responses' => [
74 | 204 => [
75 | 'description' => 'Valid email address, no matter if user exists or not',
76 | ],
77 | 422 => [
78 | 'description' => 'Missing email parameter or invalid format',
79 | ],
80 | ],
81 | 'requestBody' => [
82 | 'description' => 'Request a new password',
83 | 'content' => [
84 | 'application/json' => [
85 | 'schema' => [
86 | '$ref' => '#/components/schemas/ForgotPassword:request',
87 | ],
88 | ],
89 | ],
90 | ],
91 | ];
92 | $docs['components']['schemas']['ForgotPassword:request'] = array_merge([
93 | 'description' => 'New password request object',
94 | ], $requestSchema);
95 |
96 | // Add GET /forgot-password/{tokenValue} path
97 | $docs['paths'][$routes->get('coop_tilleuls_forgot_password.get_token')->getPath()]['get'] = [
98 | 'tags' => ['Forgot password'],
99 | 'operationId' => 'getForgotPassword',
100 | 'summary' => 'Validates token',
101 | 'responses' => [
102 | 200 => [
103 | 'description' => 'Authenticated user',
104 | 'content' => [
105 | 'application/json' => [
106 | 'schema' => [
107 | '$ref' => '#/components/schemas/ForgotPassword:validate',
108 | ],
109 | ],
110 | ],
111 | ],
112 | 404 => [
113 | 'description' => 'Token not found or expired',
114 | ],
115 | ],
116 | 'parameters' => [
117 | [
118 | 'name' => 'tokenValue',
119 | 'in' => 'path',
120 | 'required' => true,
121 | 'schema' => [
122 | 'type' => 'string',
123 | ],
124 | ],
125 | [
126 | 'name' => 'FP-provider',
127 | 'in' => 'headers',
128 | 'required' => false,
129 | 'schema' => [
130 | 'type' => 'string',
131 | ],
132 | ],
133 | ],
134 | ];
135 | $docs['components']['schemas']['ForgotPassword:validate'] = [
136 | 'type' => ['object', 'null'],
137 | 'description' => 'Authenticated user',
138 | ];
139 |
140 | // Add POST /forgot-password/{tokenValue} path
141 | $docs['paths'][$routes->get('coop_tilleuls_forgot_password.update')->getPath()]['post'] = [
142 | 'tags' => ['Forgot password'],
143 | 'operationId' => 'postForgotPasswordToken',
144 | 'summary' => 'Resets user password from token',
145 | 'responses' => [
146 | 204 => [
147 | 'description' => 'Email address format valid, no matter if user exists or not',
148 | ],
149 | 422 => [
150 | 'description' => 'Missing password parameter',
151 | ],
152 | 404 => [
153 | 'description' => 'Token not found',
154 | ],
155 | ],
156 | 'parameters' => [
157 | [
158 | 'name' => 'tokenValue',
159 | 'in' => 'path',
160 | 'required' => true,
161 | 'schema' => [
162 | 'type' => 'string',
163 | ],
164 | ],
165 | ],
166 | 'requestBody' => [
167 | 'description' => 'Reset password',
168 | 'content' => [
169 | 'application/json' => [
170 | 'schema' => [
171 | '$ref' => '#/components/schemas/ForgotPassword:reset',
172 | ],
173 | ],
174 | ],
175 | ],
176 | ];
177 | $docs['components']['schemas']['ForgotPassword:reset'] = array_merge([
178 | 'description' => 'Reset password object',
179 | ], $resetSchema);
180 |
181 | return $docs;
182 | }
183 |
184 | public function supportsNormalization($data, $format = null, array $context = []): bool
185 | {
186 | return $this->decorated->supportsNormalization($data, $format);
187 | }
188 |
189 | public function getSupportedTypes(?string $format): array
190 | {
191 | // @deprecated remove condition when support for symfony versions under 6.4 is dropped
192 | if (!method_exists($this->decorated, 'getSupportedTypes')) {
193 | return [
194 | '*' => $this->decorated instanceof CacheableSupportsMethodInterface && $this->decorated->hasCacheableSupportsMethod(),
195 | ];
196 | }
197 |
198 | return $this->decorated->getSupportedTypes($format);
199 | }
200 | }
201 |
--------------------------------------------------------------------------------
/src/Controller/ForgotPasswordController.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Controller;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
18 | use Symfony\Component\HttpFoundation\JsonResponse;
19 | use Symfony\Component\HttpFoundation\Response;
20 |
21 | /**
22 | * @author Vincent CHALAMON
23 | *
24 | * @deprecated Use invokable controllers instead
25 | */
26 | final class ForgotPasswordController
27 | {
28 | public function __construct(private readonly GetToken $getToken, private readonly UpdatePassword $updatePassword, private readonly ResetPassword $resetPassword)
29 | {
30 | }
31 |
32 | /**
33 | * @param string $propertyName
34 | * @param string $value
35 | *
36 | * @return Response
37 | */
38 | public function resetPasswordAction($propertyName, $value, ProviderInterface $provider)
39 | {
40 | $resetPassword = $this->resetPassword;
41 |
42 | return $resetPassword($propertyName, $value, $provider);
43 | }
44 |
45 | /**
46 | * @return JsonResponse
47 | */
48 | public function getTokenAction(AbstractPasswordToken $token, ProviderInterface $provider)
49 | {
50 | $getToken = $this->getToken;
51 |
52 | return $getToken($token, $provider);
53 | }
54 |
55 | /**
56 | * @param string $password
57 | *
58 | * @return Response
59 | */
60 | public function updatePasswordAction(AbstractPasswordToken $token, $password, ProviderInterface $provider)
61 | {
62 | $updatePassword = $this->updatePassword;
63 |
64 | return $updatePassword($token, $password, $provider);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Controller/GetToken.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Controller;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use CoopTilleuls\ForgotPasswordBundle\Normalizer\NormalizerInterface;
18 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
19 | use Symfony\Component\HttpFoundation\JsonResponse;
20 |
21 | /**
22 | * @author Vincent CHALAMON
23 | */
24 | final class GetToken
25 | {
26 | public function __construct(private readonly NormalizerInterface $normalizer)
27 | {
28 | }
29 |
30 | /**
31 | * @return JsonResponse
32 | */
33 | public function __invoke(AbstractPasswordToken $token, ProviderInterface $provider)
34 | {
35 | $groups = $provider->getPasswordTokenSerializationGroups();
36 |
37 | return new JsonResponse(
38 | $this->normalizer->normalize($token, 'json', $groups ? ['groups' => $groups] : [])
39 | );
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Controller/ResetPassword.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Controller;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Manager\ForgotPasswordManager;
17 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
18 | use Symfony\Component\HttpFoundation\Response;
19 |
20 | /**
21 | * @author Vincent CHALAMON
22 | */
23 | final class ResetPassword
24 | {
25 | public function __construct(private readonly ForgotPasswordManager $forgotPasswordManager)
26 | {
27 | }
28 |
29 | /**
30 | * @param string $propertyName
31 | * @param string $value
32 | *
33 | * @return Response
34 | */
35 | public function __invoke($propertyName, $value, ProviderInterface $provider)
36 | {
37 | $this->forgotPasswordManager->resetPassword($propertyName, $value, $provider);
38 |
39 | return new Response('', 204);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Controller/UpdatePassword.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Controller;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use CoopTilleuls\ForgotPasswordBundle\Manager\ForgotPasswordManager;
18 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
19 | use Symfony\Component\HttpFoundation\Response;
20 |
21 | /**
22 | * @author Vincent CHALAMON
23 | */
24 | final class UpdatePassword
25 | {
26 | public function __construct(private readonly ForgotPasswordManager $forgotPasswordManager)
27 | {
28 | }
29 |
30 | /**
31 | * @param string $password
32 | *
33 | * @return Response
34 | */
35 | public function __invoke(AbstractPasswordToken $token, $password, ProviderInterface $provider)
36 | {
37 | $this->forgotPasswordManager->updatePassword($token, $password, $provider);
38 |
39 | return new Response('', 204);
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/CoopTilleulsForgotPasswordBundle.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\DependencyInjection\CompilerPass\ApiPlatformCompilerPass;
17 | use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass;
18 | use Symfony\Component\DependencyInjection\ContainerBuilder;
19 | use Symfony\Component\DependencyInjection\Extension\Extension;
20 | use Symfony\Component\HttpKernel\Bundle\Bundle;
21 |
22 | /**
23 | * @author Vincent CHALAMON
24 | */
25 | final class CoopTilleulsForgotPasswordBundle extends Bundle
26 | {
27 | public function getPath(): string
28 | {
29 | return \dirname(__DIR__);
30 | }
31 |
32 | public function build(ContainerBuilder $container): void
33 | {
34 | $container->addCompilerPass(new ApiPlatformCompilerPass());
35 |
36 | if (class_exists(DoctrineOrmMappingsPass::class)) {
37 | $container->addCompilerPass(DoctrineOrmMappingsPass::createXmlMappingDriver([
38 | realpath(__DIR__.'/../config/doctrine') => 'CoopTilleuls\ForgotPasswordBundle\Entity',
39 | ]));
40 | }
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | protected function getContainerExtensionClass(): string
47 | {
48 | return \sprintf(
49 | '%s\\DependencyInjection\\%s%sExtension',
50 | $this->getNamespace(),
51 | preg_replace('/Bundle$/', '', $this->getName()),
52 | class_exists(Extension::class) ? '' : 'Legacy'
53 | );
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/DependencyInjection/BCExtensionTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\DependencyInjection;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Normalizer\JMSNormalizer;
17 | use CoopTilleuls\ForgotPasswordBundle\Normalizer\SymfonyNormalizer;
18 | use CoopTilleuls\ForgotPasswordBundle\Provider\Provider;
19 | use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
20 | use Symfony\Component\Config\FileLocator;
21 | use Symfony\Component\DependencyInjection\ContainerBuilder;
22 | use Symfony\Component\DependencyInjection\Definition;
23 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
24 | use Symfony\Component\DependencyInjection\Reference;
25 |
26 | /**
27 | * @author Vincent CHALAMON
28 | */
29 | trait BCExtensionTrait
30 | {
31 | public function load(array $configs, ContainerBuilder $container): void
32 | {
33 | $configuration = new Configuration();
34 | $config = $this->processConfiguration($configuration, $configs);
35 |
36 | if (!$defaultProvider = $this->getDefaultProvider($config)) {
37 | throw new InvalidConfigurationException('Multiple "ForgotPassword" providers have been defined but none of them is set as default. Did you forget to set "default" option?');
38 | }
39 |
40 | $this->buildProvider($config, $container);
41 |
42 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../../config'));
43 | $loader->load('services.xml');
44 |
45 | // Load API-Platform bridge
46 | if (isset($container->getParameter('kernel.bundles')['ApiPlatformBundle'])) {
47 | $loader->load('api_platform.xml');
48 | }
49 |
50 | $alias = $container->setAlias('coop_tilleuls_forgot_password.manager', $defaultProvider['manager']);
51 | if (method_exists(Definition::class, 'getDeprecation')) {
52 | $alias->setDeprecated('tilleuls/forgot-password-bundle', '1.5', 'Alias "%alias_id%" is deprecated and will be removed without replacement in 2.0.');
53 | } else {
54 | $alias->setDeprecated(true, 'Alias "%alias_id%" is deprecated and will be removed without replacement in 2.0.');
55 | }
56 |
57 | // Build normalizer
58 | $class = true === $config['use_jms_serializer'] ? JMSNormalizer::class : SymfonyNormalizer::class;
59 | $serializerId = true === $config['use_jms_serializer'] ? 'jms_serializer.serializer' : 'serializer';
60 | $container->setDefinition('coop_tilleuls_forgot_password.normalizer', new Definition($class, [new Reference($serializerId)]))->setPublic(false);
61 |
62 | $container
63 | ->getDefinition('coop_tilleuls_forgot_password.manager.password_token')
64 | ->replaceArgument(0, new Reference($config['token_generator']));
65 | }
66 |
67 | private function buildProvider(array $config, ContainerBuilder $container): void
68 | {
69 | foreach ($config['providers'] as $key => $value) {
70 | $container->setDefinition($key, new Definition(Provider::class,
71 | [
72 | new Reference($value['manager']),
73 | $key,
74 | $value['password_token']['class'],
75 | $value['password_token']['expires_in'],
76 | $value['password_token']['user_field'],
77 | $value['user']['class'],
78 | $value['password_token']['serialization_groups'],
79 | $value['user']['email_field'],
80 | $value['user']['password_field'],
81 | array_unique(array_merge($value['user']['authorized_fields'], [$value['user']['email_field']])),
82 | $value['default'],
83 | ]))->setPublic(false)
84 | ->addTag('coop_tilleuls_forgot_password.provider');
85 | }
86 | }
87 |
88 | private function getDefaultProvider(array $config): ?array
89 | {
90 | foreach ($config['providers'] as $value) {
91 | if (true === $value['default']) {
92 | return $value;
93 | }
94 | }
95 |
96 | return null;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CompilerPass/ApiPlatformCompilerPass.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\DependencyInjection\CompilerPass;
15 |
16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
17 | use Symfony\Component\DependencyInjection\ContainerBuilder;
18 |
19 | /**
20 | * @author Vincent CHALAMON
21 | */
22 | final class ApiPlatformCompilerPass implements CompilerPassInterface
23 | {
24 | public function process(ContainerBuilder $container): void
25 | {
26 | if (!$container->hasDefinition('api_platform.swagger.normalizer.documentation')) {
27 | $container->removeDefinition('coop_tilleuls_forgot_password.normalizer.documentation');
28 | }
29 |
30 | if (!$container->hasDefinition('api_platform.openapi.factory') && !$container->hasAlias('api_platform.openapi.factory')) {
31 | $container->removeDefinition('coop_tilleuls_forgot_password.openapi.factory');
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/DependencyInjection/Configuration.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\DependencyInjection;
15 |
16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder;
17 | use Symfony\Component\Config\Definition\ConfigurationInterface;
18 |
19 | /**
20 | * @author Vincent CHALAMON
21 | */
22 | final class Configuration implements ConfigurationInterface
23 | {
24 | public function getConfigTreeBuilder(): TreeBuilder
25 | {
26 | if (method_exists(TreeBuilder::class, 'root')) {
27 | $treeBuilder = new TreeBuilder();
28 | $rootNode = $treeBuilder->root('coop_tilleuls_forgot_password');
29 | } else {
30 | $treeBuilder = new TreeBuilder('coop_tilleuls_forgot_password');
31 | $rootNode = $treeBuilder->getRootNode();
32 | }
33 |
34 | $rootNode
35 | ->beforeNormalization()
36 | ->ifTrue(fn ($config) => \array_key_exists('password_token_class', $config) || \array_key_exists('user_class', $config))
37 | ->then(function ($config) {
38 | if (\array_key_exists('password_token_class', $config)) {
39 | if (!isset($config['password_token'])) {
40 | $config['password_token'] = [];
41 | }
42 | $config['password_token']['class'] = $config['password_token_class'];
43 | }
44 |
45 | if (\array_key_exists('user_class', $config)) {
46 | if (!isset($config['user'])) {
47 | $config['user'] = [];
48 | }
49 | $config['user']['class'] = $config['user_class'];
50 | }
51 | unset($config['password_token_class'], $config['user_class']);
52 |
53 | return $config;
54 | })
55 | ->ifTrue(fn ($config) => !\array_key_exists('providers', $config))
56 | ->then(function ($config) {
57 | $config['providers']['default']['default'] = true;
58 | $config['providers']['default']['password_token'] = $config['password_token'];
59 | $config['providers']['default']['user'] = $config['user'];
60 | if (\array_key_exists('manager', $config)) {
61 | $config['providers']['default']['manager'] = $config['manager'];
62 | }
63 | unset($config['user'], $config['password_token'], $config['manager']);
64 |
65 | return $config;
66 | })
67 | ->end()
68 | ->children()
69 | ->arrayNode('providers')
70 | ->useAttributeAsKey('name')
71 | ->prototype('array')
72 | ->children()
73 | ->scalarNode('manager')
74 | ->defaultValue('coop_tilleuls_forgot_password.manager.doctrine')
75 | ->cannotBeEmpty()
76 | ->info('Persistence manager service to handle the token storage.')
77 | ->end()
78 | ->booleanNode('default')
79 | ->defaultFalse()
80 | ->end()
81 | ->arrayNode('password_token')
82 | ->children()
83 | ->scalarNode('class')
84 | ->cannotBeEmpty()
85 | ->isRequired()
86 | ->info('PasswordToken class.')
87 | ->end()
88 | ->scalarNode('expires_in')
89 | ->defaultValue('1 day')
90 | ->cannotBeEmpty()
91 | ->info('Expiration time using Datetime format. see : https://www.php.net/manual/en/datetime.format.php.')
92 | ->end()
93 | ->scalarNode('user_field')
94 | ->defaultValue('user')
95 | ->cannotBeEmpty()
96 | ->info('User field name on PasswordToken entity.')
97 | ->end()
98 | ->arrayNode('serialization_groups')
99 | ->info('PasswordToken serialization groups.')
100 | ->defaultValue([])
101 | ->useAttributeAsKey('name')
102 | ->prototype('scalar')->end()
103 | ->end()
104 | ->end()
105 | ->end()
106 | ->arrayNode('user')
107 | ->children()
108 | ->scalarNode('class')
109 | ->cannotBeEmpty()
110 | ->isRequired()
111 | ->info('User class.')
112 | ->end()
113 | ->scalarNode('email_field')
114 | ->defaultValue('email')
115 | ->cannotBeEmpty()
116 | ->info('User email field name to retrieve it (email, username...).')
117 | ->end()
118 | ->scalarNode('password_field')
119 | ->defaultValue('password')
120 | ->cannotBeEmpty()
121 | ->info('User password field name.')
122 | ->end()
123 | ->arrayNode('authorized_fields')
124 | ->defaultValue(['email'])
125 | ->requiresAtLeastOneElement()
126 | ->info('User fields names to retrieve it (email, username...).')
127 | ->prototype('scalar')->end()
128 | ->end()
129 | ->end()
130 | ->end()
131 | ->end()
132 | ->end()
133 | ->end()
134 | ->booleanNode('use_jms_serializer')
135 | ->defaultFalse()
136 | ->end()
137 | ->scalarNode('token_generator')
138 | ->defaultValue('coop_tilleuls_forgot_password.token_generator.bin2hex')
139 | ->cannotBeEmpty()
140 | ->info('Persistence manager service to handle the token storage.')
141 | ->end()
142 | ->end();
143 |
144 | return $treeBuilder;
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CoopTilleulsForgotPasswordExtension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\DependencyInjection;
15 |
16 | use Symfony\Component\DependencyInjection\Extension\Extension;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | */
21 | final class CoopTilleulsForgotPasswordExtension extends Extension
22 | {
23 | use BCExtensionTrait;
24 | }
25 |
--------------------------------------------------------------------------------
/src/DependencyInjection/CoopTilleulsForgotPasswordLegacyExtension.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\DependencyInjection;
15 |
16 | use Symfony\Component\HttpKernel\DependencyInjection\Extension;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | */
21 | final class CoopTilleulsForgotPasswordLegacyExtension extends Extension
22 | {
23 | use BCExtensionTrait;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Entity/AbstractPasswordToken.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Entity;
15 |
16 | /**
17 | * @author Vincent CHALAMON
18 | */
19 | abstract class AbstractPasswordToken
20 | {
21 | /**
22 | * @var string
23 | */
24 | protected $token;
25 |
26 | /**
27 | * @var \DateTime
28 | */
29 | protected $expiresAt;
30 |
31 | abstract public function getId();
32 |
33 | abstract public function getUser();
34 |
35 | abstract public function setUser($user);
36 |
37 | /**
38 | * @return string
39 | */
40 | public function getToken()
41 | {
42 | return $this->token;
43 | }
44 |
45 | /**
46 | * @param string $token
47 | */
48 | public function setToken($token): void
49 | {
50 | $this->token = $token;
51 | }
52 |
53 | /**
54 | * @return \DateTime
55 | */
56 | public function getExpiresAt()
57 | {
58 | return $this->expiresAt;
59 | }
60 |
61 | public function setExpiresAt(\DateTime $expiresAt): void
62 | {
63 | $this->expiresAt = $expiresAt;
64 | }
65 |
66 | /**
67 | * @return bool
68 | */
69 | public function isExpired()
70 | {
71 | return (new \DateTime()) > $this->expiresAt;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Event/CreateTokenEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Event;
15 |
16 | /**
17 | * @author Vincent CHALAMON
18 | */
19 | final class CreateTokenEvent extends ForgotPasswordEvent
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/src/Event/ForgotPasswordEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Event;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | *
21 | * @deprecated Use CreateTokenEvent and UpdatePasswordEvent instead
22 | */
23 | class ForgotPasswordEvent extends PolyfillEvent
24 | {
25 | public const CREATE_TOKEN = 'coop_tilleuls_forgot_password.create_token';
26 | public const UPDATE_PASSWORD = 'coop_tilleuls_forgot_password.update_password';
27 |
28 | public function __construct(protected readonly AbstractPasswordToken $passwordToken, protected readonly ?string $password = null)
29 | {
30 | }
31 |
32 | /**
33 | * @return AbstractPasswordToken
34 | */
35 | public function getPasswordToken()
36 | {
37 | return $this->passwordToken;
38 | }
39 |
40 | /**
41 | * @return string|null
42 | */
43 | public function getPassword()
44 | {
45 | return $this->password;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Event/PolyfillEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Event;
15 |
16 | use Symfony\Component\EventDispatcher\Event;
17 | use Symfony\Component\EventDispatcher\EventDispatcher;
18 | use Symfony\Contracts\EventDispatcher\Event as ContractsEvent;
19 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
20 |
21 | if (is_subclass_of(EventDispatcher::class, EventDispatcherInterface::class)) {
22 | // Symfony 4.4 and upper
23 | abstract class PolyfillEvent extends ContractsEvent
24 | {
25 | }
26 | } else {
27 | // Symfony 4.3 and inferior
28 | abstract class PolyfillEvent extends Event
29 | {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/Event/UpdatePasswordEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Event;
15 |
16 | /**
17 | * @author Vincent CHALAMON
18 | */
19 | final class UpdatePasswordEvent extends ForgotPasswordEvent
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/src/Event/UserNotFoundEvent.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Event;
15 |
16 | final class UserNotFoundEvent extends PolyfillEvent
17 | {
18 | public const USER_NOT_FOUND = 'coop_tilleuls_forgot_password.user_not_found';
19 |
20 | public function __construct(private readonly array $context = [])
21 | {
22 | }
23 |
24 | /**
25 | * @return array
26 | */
27 | public function getContext()
28 | {
29 | return $this->context;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/EventListener/ExceptionEventListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\EventListener;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Exception\JsonHttpExceptionInterface;
17 | use Symfony\Component\HttpFoundation\JsonResponse;
18 | use Symfony\Component\HttpKernel\Event\KernelEvent;
19 |
20 | /**
21 | * @author Vincent CHALAMON
22 | */
23 | final class ExceptionEventListener
24 | {
25 | use MainRequestTrait;
26 |
27 | public function onKernelException(KernelEvent $event): void
28 | {
29 | $exception = method_exists($event, 'getThrowable') ? $event->getThrowable() : $event->getException();
30 | if (!$this->isMainRequest($event) || !$exception instanceof JsonHttpExceptionInterface) {
31 | return;
32 | }
33 |
34 | $event->setResponse(new JsonResponse(['message' => $exception->getMessage()], $exception->getStatusCode()));
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/EventListener/MainRequestTrait.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\EventListener;
15 |
16 | use Symfony\Component\HttpKernel\Event\KernelEvent;
17 |
18 | /**
19 | * @author Jon Gotlin
20 | */
21 | trait MainRequestTrait
22 | {
23 | private function isMainRequest(KernelEvent $event): bool
24 | {
25 | return method_exists($event, 'isMainRequest') ? $event->isMainRequest() : $event->isMasterRequest();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/EventListener/RequestEventListener.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\EventListener;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Exception\InvalidJsonHttpException;
17 | use CoopTilleuls\ForgotPasswordBundle\Exception\MissingFieldHttpException;
18 | use CoopTilleuls\ForgotPasswordBundle\Exception\NoParameterException;
19 | use CoopTilleuls\ForgotPasswordBundle\Exception\UnauthorizedFieldException;
20 | use CoopTilleuls\ForgotPasswordBundle\Manager\PasswordTokenManager;
21 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderChainInterface;
22 | use Symfony\Component\HttpKernel\Event\KernelEvent;
23 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
24 |
25 | /**
26 | * @author Vincent CHALAMON
27 | */
28 | final class RequestEventListener
29 | {
30 | use MainRequestTrait;
31 |
32 | public function __construct(private readonly PasswordTokenManager $passwordTokenManager, private readonly ProviderChainInterface $providerChain)
33 | {
34 | }
35 |
36 | public function decodeRequest(KernelEvent $event): void
37 | {
38 | $request = $event->getRequest();
39 | $routeName = $request->attributes->get('_route');
40 | if (!$this->isMainRequest($event) || !\in_array(
41 | $routeName,
42 | ['coop_tilleuls_forgot_password.reset', 'coop_tilleuls_forgot_password.update'], true
43 | )
44 | ) {
45 | return;
46 | }
47 |
48 | $content = $request->getContent();
49 | $data = json_decode($content, true);
50 |
51 | if (!empty($content) && \JSON_ERROR_NONE !== json_last_error()) {
52 | throw new InvalidJsonHttpException();
53 | }
54 | if (!\is_array($data) || empty($data)) {
55 | throw new NoParameterException();
56 | }
57 |
58 | $fieldName = key($data);
59 | if (empty($data[$fieldName])) {
60 | throw new MissingFieldHttpException($fieldName);
61 | }
62 |
63 | $provider = $this->providerChain->get($request->headers->get('FP-provider'));
64 | $request->attributes->set('provider', $provider);
65 |
66 | if ('coop_tilleuls_forgot_password.reset' === $routeName) {
67 | foreach ($data as $fieldName => $value) {
68 | if (\in_array($fieldName, $provider->getUserAuthorizedFields(), true)) {
69 | $request->attributes->set('propertyName', $fieldName);
70 | $request->attributes->set('value', $value);
71 |
72 | return;
73 | }
74 | }
75 |
76 | throw new UnauthorizedFieldException($fieldName);
77 | }
78 |
79 | // if $routeName is 'coop_tilleuls_forgot_password.update'
80 | if (!\array_key_exists($userPasswordField = $provider->getUserPasswordField(), $data)) {
81 | throw new MissingFieldHttpException($userPasswordField);
82 | }
83 |
84 | $request->attributes->set('password', $data[$userPasswordField]);
85 | }
86 |
87 | public function getTokenFromRequest(KernelEvent $event): void
88 | {
89 | $request = $event->getRequest();
90 | $routeName = $request->attributes->get('_route');
91 | if (!$this->isMainRequest($event) || !\in_array(
92 | $routeName,
93 | ['coop_tilleuls_forgot_password.get_token', 'coop_tilleuls_forgot_password.update'], true
94 | )
95 | ) {
96 | return;
97 | }
98 |
99 | $provider = $this->providerChain->get($request->headers->get('FP-provider'));
100 | $token = $this->passwordTokenManager->findOneByToken($request->attributes->get('tokenValue'), $provider);
101 |
102 | if (null === $token || $token->isExpired()) {
103 | throw new NotFoundHttpException('Invalid token.');
104 | }
105 |
106 | $request->attributes->set('token', $token);
107 | $request->attributes->set('provider', $provider);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/Exception/InvalidJsonHttpException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpException;
17 |
18 | final class InvalidJsonHttpException extends HttpException implements JsonHttpExceptionInterface
19 | {
20 | public function __construct()
21 | {
22 | parent::__construct(422, 'Invalid JSON data.');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Exception/JsonHttpExceptionInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | */
21 | interface JsonHttpExceptionInterface extends HttpExceptionInterface
22 | {
23 | }
24 |
--------------------------------------------------------------------------------
/src/Exception/MissingFieldHttpException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpException;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | */
21 | final class MissingFieldHttpException extends HttpException implements JsonHttpExceptionInterface
22 | {
23 | public function __construct($fieldName)
24 | {
25 | parent::__construct(422, \sprintf('Parameter "%s" is missing.', $fieldName));
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Exception/NoParameterException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpException;
17 |
18 | final class NoParameterException extends HttpException implements JsonHttpExceptionInterface
19 | {
20 | public function __construct()
21 | {
22 | parent::__construct(422, 'No parameter sent.');
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Exception/UnauthorizedFieldException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpException;
17 |
18 | final class UnauthorizedFieldException extends HttpException implements JsonHttpExceptionInterface
19 | {
20 | public function __construct($propertyName)
21 | {
22 | parent::__construct(422, \sprintf('The parameter "%s" is not authorized in your configuration.', $propertyName));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Exception/UndefinedProviderException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Exception;
15 |
16 | use Symfony\Component\HttpKernel\Exception\HttpException;
17 |
18 | final class UndefinedProviderException extends HttpException implements JsonHttpExceptionInterface
19 | {
20 | public function __construct(string $message = 'This provider is not defined.')
21 | {
22 | parent::__construct(422, $message);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/Manager/Bridge/DoctrineManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Manager\Bridge;
15 |
16 | use Doctrine\Common\Persistence\ManagerRegistry as LegacyManagerRegistry;
17 | use Doctrine\Persistence\ManagerRegistry;
18 |
19 | /**
20 | * @author Vincent CHALAMON
21 | */
22 | final class DoctrineManager implements ManagerInterface
23 | {
24 | public function __construct(private readonly LegacyManagerRegistry|ManagerRegistry $registry)
25 | {
26 | }
27 |
28 | public function findOneBy($class, array $criteria)
29 | {
30 | return $this->registry->getManagerForClass($class)
31 | ->getRepository($class)
32 | ->findOneBy($criteria);
33 | }
34 |
35 | public function persist($object): void
36 | {
37 | $manager = $this->registry->getManagerForClass($object::class);
38 | $manager->persist($object);
39 | $manager->flush();
40 | }
41 |
42 | public function remove($object): void
43 | {
44 | $manager = $this->registry->getManagerForClass($object::class);
45 | $manager->remove($object);
46 | $manager->flush();
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Manager/Bridge/ManagerInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Manager\Bridge;
15 |
16 | /**
17 | * @author Vincent CHALAMON
18 | */
19 | interface ManagerInterface
20 | {
21 | /**
22 | * @param string $class
23 | *
24 | * @return mixed|null
25 | */
26 | public function findOneBy($class, array $criteria);
27 |
28 | public function persist($object);
29 |
30 | public function remove($object);
31 | }
32 |
--------------------------------------------------------------------------------
/src/Manager/ForgotPasswordManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Manager;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use CoopTilleuls\ForgotPasswordBundle\Event\CreateTokenEvent;
18 | use CoopTilleuls\ForgotPasswordBundle\Event\ForgotPasswordEvent;
19 | use CoopTilleuls\ForgotPasswordBundle\Event\UpdatePasswordEvent;
20 | use CoopTilleuls\ForgotPasswordBundle\Event\UserNotFoundEvent;
21 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
22 | use Symfony\Component\EventDispatcher\EventDispatcherInterface;
23 | use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as ContractsEventDispatcherInterface;
24 |
25 | /**
26 | * @author Vincent CHALAMON
27 | */
28 | class ForgotPasswordManager
29 | {
30 | public function __construct(private readonly PasswordTokenManager $passwordTokenManager, private readonly EventDispatcherInterface $dispatcher)
31 | {
32 | }
33 |
34 | public function resetPassword($propertyName, $value, ProviderInterface $provider): void
35 | {
36 | $context = [$propertyName => $value];
37 |
38 | $user = $provider->getManager()->findOneBy($provider->getUserClass(), $context);
39 |
40 | if (null === $user) {
41 | if ($this->dispatcher instanceof ContractsEventDispatcherInterface) {
42 | $this->dispatcher->dispatch(new UserNotFoundEvent($context));
43 | } else {
44 | $this->dispatcher->dispatch(UserNotFoundEvent::USER_NOT_FOUND, new UserNotFoundEvent($context));
45 | }
46 |
47 | return;
48 | }
49 |
50 | $token = $this->passwordTokenManager->findOneByUser($user, $provider);
51 |
52 | // A token already exists and has not expired
53 | if (null === $token || $token->isExpired()) {
54 | $expiredAt = new \DateTime($provider->getPasswordTokenExpiredIn());
55 | $expiredAt->setTime((int) $expiredAt->format('H'), (int) $expiredAt->format('i'), (int) $expiredAt->format('s'), 0);
56 |
57 | $token = $this->passwordTokenManager->createPasswordToken($user, $provider, $expiredAt);
58 | }
59 |
60 | // Generate password token
61 | if ($this->dispatcher instanceof ContractsEventDispatcherInterface) {
62 | $this->dispatcher->dispatch(new CreateTokenEvent($token));
63 | } else {
64 | $this->dispatcher->dispatch(ForgotPasswordEvent::CREATE_TOKEN, new CreateTokenEvent($token));
65 | }
66 | }
67 |
68 | /**
69 | * @param string $password
70 | *
71 | * @return bool
72 | */
73 | public function updatePassword(AbstractPasswordToken $passwordToken, $password, ProviderInterface $provider)
74 | {
75 | // Update user password
76 | if ($this->dispatcher instanceof ContractsEventDispatcherInterface) {
77 | $this->dispatcher->dispatch(new UpdatePasswordEvent($passwordToken, $password));
78 | } else {
79 | $this->dispatcher->dispatch(ForgotPasswordEvent::UPDATE_PASSWORD, new UpdatePasswordEvent($passwordToken, $password));
80 | }
81 |
82 | // Remove PasswordToken
83 | $provider->getManager()->remove($passwordToken);
84 |
85 | return true;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Manager/PasswordTokenManager.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Manager;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use CoopTilleuls\ForgotPasswordBundle\Provider\ProviderInterface;
18 | use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface;
19 |
20 | /**
21 | * @author Vincent CHALAMON
22 | */
23 | class PasswordTokenManager
24 | {
25 | public function __construct(private readonly TokenGeneratorInterface $tokenGenerator)
26 | {
27 | }
28 |
29 | /**
30 | * @return AbstractPasswordToken
31 | */
32 | public function createPasswordToken($user, ProviderInterface $provider, ?\DateTime $expiresAt = null)
33 | {
34 | if (!$expiresAt) {
35 | $expiresAt = new \DateTime($provider->getPasswordTokenExpiredIn());
36 | $expiresAt->setTime((int) $expiresAt->format('H'), (int) $expiresAt->format('i'), (int) $expiresAt->format('s'), 0);
37 | }
38 |
39 | $tokenClass = $provider->getPasswordTokenClass();
40 |
41 | /** @var AbstractPasswordToken $passwordToken */
42 | $passwordToken = new $tokenClass();
43 | $passwordToken->setToken($this->tokenGenerator->generate());
44 | $passwordToken->setUser($user);
45 | $passwordToken->setExpiresAt($expiresAt);
46 | $provider->getManager()->persist($passwordToken);
47 |
48 | return $passwordToken;
49 | }
50 |
51 | /**
52 | * @param string $token
53 | *
54 | * @return AbstractPasswordToken
55 | */
56 | public function findOneByToken($token, ProviderInterface $provider)
57 | {
58 | return $provider->getManager()->findOneBy($provider->getPasswordTokenClass(), ['token' => $token]);
59 | }
60 |
61 | /**
62 | * @return AbstractPasswordToken
63 | */
64 | public function findOneByUser($user, ProviderInterface $provider)
65 | {
66 | return $provider->getManager()->findOneBy($provider->getPasswordTokenClass(), [$provider->getPasswordTokenUserField() => $user]);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/Normalizer/JMSNormalizer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Normalizer;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use JMS\Serializer\ArrayTransformerInterface;
18 |
19 | /**
20 | * @author Vincent CHALAMON
21 | */
22 | final class JMSNormalizer implements NormalizerInterface
23 | {
24 | public function __construct(private readonly ArrayTransformerInterface $normalizer)
25 | {
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function normalize(AbstractPasswordToken $object, $format, array $context = [])
32 | {
33 | return $this->normalizer->toArray($object);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Normalizer/NormalizerInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Normalizer;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 |
18 | /**
19 | * @author Vincent CHALAMON
20 | */
21 | interface NormalizerInterface
22 | {
23 | /**
24 | * @param string $format
25 | */
26 | public function normalize(AbstractPasswordToken $object, $format, array $context = []);
27 | }
28 |
--------------------------------------------------------------------------------
/src/Normalizer/SymfonyNormalizer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Normalizer;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Entity\AbstractPasswordToken;
17 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface as SymfonyNormalizerInterface;
18 |
19 | /**
20 | * @author Vincent CHALAMON
21 | */
22 | final class SymfonyNormalizer implements NormalizerInterface
23 | {
24 | public function __construct(private readonly SymfonyNormalizerInterface $normalizer)
25 | {
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function normalize(AbstractPasswordToken $object, $format, array $context = [])
32 | {
33 | return $this->normalizer->normalize($object, $format, $context);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Provider/Provider.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Provider;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Manager\Bridge\ManagerInterface;
17 |
18 | final class Provider implements ProviderInterface
19 | {
20 | public function __construct(private readonly ManagerInterface $manager, private readonly string $name, private readonly string $passwordTokenClass, private readonly string $passwordTokenExpiredIn, private readonly string $passwordTokenUserField, private readonly string $userClass, private readonly array $passwordTokenSerializationGroups = [], private readonly string $userEmailField = 'email', private readonly string $userPasswordField = 'password', private readonly array $userAuthorizedFields = [], private readonly bool $isDefault = false)
21 | {
22 | }
23 |
24 | public function getManager(): ManagerInterface
25 | {
26 | return $this->manager;
27 | }
28 |
29 | public function getUserClass(): string
30 | {
31 | return $this->userClass;
32 | }
33 |
34 | public function getPasswordTokenClass(): string
35 | {
36 | return $this->passwordTokenClass;
37 | }
38 |
39 | public function getPasswordTokenExpiredIn(): string
40 | {
41 | return $this->passwordTokenExpiredIn;
42 | }
43 |
44 | public function getPasswordTokenUserField(): string
45 | {
46 | return $this->passwordTokenUserField;
47 | }
48 |
49 | public function getPasswordTokenSerializationGroups(): array
50 | {
51 | return $this->passwordTokenSerializationGroups;
52 | }
53 |
54 | public function getUserEmailField(): string
55 | {
56 | return $this->userEmailField;
57 | }
58 |
59 | public function getUserPasswordField(): string
60 | {
61 | return $this->userPasswordField;
62 | }
63 |
64 | public function getUserAuthorizedFields(): array
65 | {
66 | return $this->userAuthorizedFields;
67 | }
68 |
69 | public function isDefault(): bool
70 | {
71 | return $this->isDefault;
72 | }
73 |
74 | public function getName(): string
75 | {
76 | return $this->name;
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/Provider/ProviderChain.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Provider;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Exception\UndefinedProviderException;
17 |
18 | final class ProviderChain implements ProviderChainInterface
19 | {
20 | /**
21 | * @var array
22 | */
23 | private array $providers;
24 |
25 | public function __construct(iterable $providers)
26 | {
27 | $this->providers = iterator_to_array($providers);
28 | }
29 |
30 | /**
31 | * Returns a provider by its name, without name the default provider is returned.
32 | */
33 | public function get(?string $name = null): ProviderInterface
34 | {
35 | if (null === $name) {
36 | return $this->getDefault();
37 | }
38 |
39 | if (!isset($this->providers[$name])) {
40 | throw new UndefinedProviderException(\sprintf('The provider "%s" is not defined.', $name));
41 | }
42 |
43 | return $this->providers[$name];
44 | }
45 |
46 | /**
47 | * Returns all providers indexed by name.
48 | *
49 | * @return array
50 | */
51 | public function all(): iterable
52 | {
53 | return $this->providers;
54 | }
55 |
56 | private function getDefault(): ProviderInterface
57 | {
58 | foreach ($this->providers as $provider) {
59 | if (true === $provider->isDefault()) {
60 | return $provider;
61 | }
62 | }
63 |
64 | throw new UndefinedProviderException();
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/Provider/ProviderChainInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Provider;
15 |
16 | interface ProviderChainInterface
17 | {
18 | public function get(?string $name = null): ProviderInterface;
19 |
20 | /**
21 | * @return array
22 | */
23 | public function all(): iterable;
24 | }
25 |
--------------------------------------------------------------------------------
/src/Provider/ProviderInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Provider;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\Manager\Bridge\ManagerInterface;
17 |
18 | /**
19 | * Configuration of ForgotPassword for each provider.
20 | */
21 | interface ProviderInterface
22 | {
23 | /**
24 | * User Class.
25 | */
26 | public function getUserClass(): string;
27 |
28 | /**
29 | * PasswordToken Class.
30 | */
31 | public function getPasswordTokenClass(): string;
32 |
33 | /**
34 | * PasswordToken expiration property.
35 | */
36 | public function getPasswordTokenExpiredIn(): string;
37 |
38 | /**
39 | * PasswordToken user field property.
40 | */
41 | public function getPasswordTokenUserField(): string;
42 |
43 | /**
44 | * PasswordToken serialization groups.
45 | */
46 | public function getPasswordTokenSerializationGroups(): array;
47 |
48 | /**
49 | * User email property.
50 | */
51 | public function getUserEmailField(): string;
52 |
53 | /**
54 | * User password property.
55 | */
56 | public function getUserPasswordField(): string;
57 |
58 | /**
59 | * Manager to handle related objects.
60 | */
61 | public function getManager(): ManagerInterface;
62 |
63 | /**
64 | * User password/email property authorized.
65 | */
66 | public function getUserAuthorizedFields(): array;
67 |
68 | /**
69 | * If provider is Default no need to mention it in queries.
70 | */
71 | public function isDefault(): bool;
72 |
73 | public function getName(): string;
74 | }
75 |
--------------------------------------------------------------------------------
/src/Routing/RouteLoader.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\Routing;
15 |
16 | use Symfony\Component\Config\Loader\Loader;
17 | use Symfony\Component\Routing\Route;
18 | use Symfony\Component\Routing\RouteCollection;
19 |
20 | /**
21 | * @author Vincent CHALAMON
22 | */
23 | final class RouteLoader extends Loader
24 | {
25 | /**
26 | * {@inheritdoc}
27 | */
28 | public function load($resource, $type = null): RouteCollection
29 | {
30 | $collection = new RouteCollection();
31 | $collection->add(
32 | 'coop_tilleuls_forgot_password.reset',
33 | (new Route('/', [
34 | '_controller' => 'coop_tilleuls_forgot_password.controller.reset_password',
35 | ]))->setMethods('POST')
36 | );
37 | $collection->add(
38 | 'coop_tilleuls_forgot_password.update',
39 | (new Route('/{tokenValue}', [
40 | '_controller' => 'coop_tilleuls_forgot_password.controller.update_password',
41 | ]))->setMethods('POST')
42 | );
43 | $collection->add(
44 | 'coop_tilleuls_forgot_password.get_token',
45 | (new Route('/{tokenValue}', [
46 | '_controller' => 'coop_tilleuls_forgot_password.controller.get_token',
47 | ]))->setMethods('GET')
48 | );
49 |
50 | return $collection;
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function supports($resource, $type = null): bool
57 | {
58 | return 'coop_tilleuls_forgot_password' === $type;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/TokenGenerator/Bridge/Bin2HexTokenGenerator.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\TokenGenerator\Bridge;
15 |
16 | use CoopTilleuls\ForgotPasswordBundle\TokenGenerator\TokenGeneratorInterface;
17 |
18 | final class Bin2HexTokenGenerator implements TokenGeneratorInterface
19 | {
20 | public function generate(): string
21 | {
22 | return bin2hex(random_bytes(25));
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/TokenGenerator/TokenGeneratorInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * For the full copyright and license information, please view the LICENSE
9 | * file that was distributed with this source code.
10 | */
11 |
12 | declare(strict_types=1);
13 |
14 | namespace CoopTilleuls\ForgotPasswordBundle\TokenGenerator;
15 |
16 | /**
17 | * @author Vincent CHALAMON
18 | */
19 | interface TokenGeneratorInterface
20 | {
21 | public function generate(): string;
22 | }
23 |
--------------------------------------------------------------------------------