Skip to content

Commit 882ffca

Browse files
committed
Password reset endpoints
1 parent bf11b81 commit 882ffca

File tree

5 files changed

+248
-0
lines changed

5 files changed

+248
-0
lines changed

config/services/managers.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,6 @@ services:
5252
autowire: true
5353
autoconfigure: true
5454

55+
PhpList\Core\Domain\Identity\Service\PasswordManager:
56+
autowire: true
57+
autoconfigure: true
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Identity\Controller;
6+
7+
use OpenApi\Attributes as OA;
8+
use PhpList\Core\Domain\Identity\Service\PasswordManager;
9+
use PhpList\Core\Security\Authentication;
10+
use PhpList\RestBundle\Common\Controller\BaseController;
11+
use PhpList\RestBundle\Common\Validator\RequestValidator;
12+
use PhpList\RestBundle\Identity\Request\RequestPasswordResetRequest;
13+
use PhpList\RestBundle\Identity\Request\ResetPasswordRequest;
14+
use PhpList\RestBundle\Identity\Request\ValidateTokenRequest;
15+
use Symfony\Component\HttpFoundation\JsonResponse;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpFoundation\Response;
18+
use Symfony\Component\Routing\Attribute\Route;
19+
20+
/**
21+
* This controller provides methods to reset admin passwords.
22+
*/
23+
#[Route('/password-reset', name: 'password_reset_')]
24+
class PasswordResetController extends BaseController
25+
{
26+
private PasswordManager $passwordManager;
27+
28+
public function __construct(
29+
Authentication $authentication,
30+
RequestValidator $validator,
31+
PasswordManager $passwordManager,
32+
) {
33+
parent::__construct($authentication, $validator);
34+
35+
$this->passwordManager = $passwordManager;
36+
}
37+
38+
#[Route('/request', name: 'request', methods: ['POST'])]
39+
#[OA\Post(
40+
path: '/api/v2/password-reset/request',
41+
description: 'Request a password reset token for an administrator account.',
42+
summary: 'Request a password reset.',
43+
requestBody: new OA\RequestBody(
44+
description: 'Administrator email',
45+
required: true,
46+
content: new OA\JsonContent(
47+
required: ['email'],
48+
properties: [
49+
new OA\Property(property: 'email', type: 'string', format: 'email', example: '[email protected]'),
50+
]
51+
)
52+
),
53+
tags: ['password-reset'],
54+
responses: [
55+
new OA\Response(
56+
response: 204,
57+
description: 'Password reset token generated',
58+
),
59+
new OA\Response(
60+
response: 400,
61+
description: 'Failure',
62+
content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse')
63+
),
64+
new OA\Response(
65+
response: 404,
66+
description: 'Failure',
67+
content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse')
68+
)
69+
]
70+
)]
71+
public function requestPasswordReset(Request $request): JsonResponse
72+
{
73+
/** @var RequestPasswordResetRequest $resetRequest */
74+
$resetRequest = $this->validator->validate($request, RequestPasswordResetRequest::class);
75+
76+
$this->passwordManager->generatePasswordResetToken($resetRequest->email);
77+
78+
return $this->json(null, Response::HTTP_NO_CONTENT);
79+
}
80+
81+
#[Route('/validate', name: 'validate', methods: ['POST'])]
82+
#[OA\Post(
83+
path: '/api/v2/password-reset/validate',
84+
description: 'Validate a password reset token.',
85+
summary: 'Validate a password reset token.',
86+
requestBody: new OA\RequestBody(
87+
description: 'Password reset token',
88+
required: true,
89+
content: new OA\JsonContent(
90+
required: ['token'],
91+
properties: [
92+
new OA\Property(property: 'token', type: 'string', example: 'a1b2c3d4e5f6'),
93+
]
94+
)
95+
),
96+
tags: ['password-reset'],
97+
responses: [
98+
new OA\Response(
99+
response: 200,
100+
description: 'Success',
101+
content: new OA\JsonContent(
102+
properties: [
103+
new OA\Property(property: 'valid', type: 'boolean', example: true),
104+
]
105+
)
106+
),
107+
new OA\Response(
108+
response: 400,
109+
description: 'Failure',
110+
content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse')
111+
)
112+
]
113+
)]
114+
public function validateToken(Request $request): JsonResponse
115+
{
116+
/** @var ValidateTokenRequest $validateRequest */
117+
$validateRequest = $this->validator->validate($request, ValidateTokenRequest::class);
118+
119+
$administrator = $this->passwordManager->validatePasswordResetToken($validateRequest->token);
120+
121+
return $this->json([ 'valid' => $administrator !== null]);
122+
}
123+
124+
#[Route('/reset', name: 'reset', methods: ['POST'])]
125+
#[OA\Post(
126+
path: '/api/v2/password-reset/reset',
127+
description: 'Reset an administrator password using a token.',
128+
summary: 'Reset password with token.',
129+
requestBody: new OA\RequestBody(
130+
description: 'Password reset information',
131+
required: true,
132+
content: new OA\JsonContent(
133+
required: ['token', 'newPassword'],
134+
properties: [
135+
new OA\Property(property: 'token', type: 'string', example: 'a1b2c3d4e5f6'),
136+
new OA\Property(
137+
property: 'newPassword',
138+
type: 'string',
139+
format: 'password',
140+
example: 'newSecurePassword123',
141+
),
142+
]
143+
)
144+
),
145+
tags: ['password-reset'],
146+
responses: [
147+
new OA\Response(
148+
response: 200,
149+
description: 'Success',
150+
content: new OA\JsonContent(
151+
properties: [
152+
new OA\Property(property: 'message', type: 'string', example: 'Password updated successfully'),
153+
]
154+
)
155+
),
156+
new OA\Response(
157+
response: 400,
158+
description: 'Invalid or expired token',
159+
content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse')
160+
)
161+
]
162+
)]
163+
public function resetPassword(Request $request): JsonResponse
164+
{
165+
/** @var ResetPasswordRequest $resetRequest */
166+
$resetRequest = $this->validator->validate($request, ResetPasswordRequest::class);
167+
168+
$success = $this->passwordManager->updatePasswordWithToken(
169+
$resetRequest->token,
170+
$resetRequest->newPassword
171+
);
172+
173+
if ($success) {
174+
return $this->json([ 'message' => 'Password updated successfully']);
175+
}
176+
177+
return $this->json(['message' => 'Invalid or expired token'], Response::HTTP_BAD_REQUEST);
178+
}
179+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Identity\Request;
6+
7+
use PhpList\RestBundle\Common\Request\RequestInterface;
8+
use Symfony\Component\Validator\Constraints as Assert;
9+
10+
class RequestPasswordResetRequest implements RequestInterface
11+
{
12+
#[Assert\NotBlank]
13+
#[Assert\Email]
14+
#[Assert\Type(type: 'string')]
15+
public string $email;
16+
17+
public function getDto(): RequestPasswordResetRequest
18+
{
19+
return $this;
20+
}
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Identity\Request;
6+
7+
use PhpList\RestBundle\Common\Request\RequestInterface;
8+
use Symfony\Component\Validator\Constraints as Assert;
9+
10+
class ResetPasswordRequest implements RequestInterface
11+
{
12+
#[Assert\NotBlank]
13+
#[Assert\Type(type: 'string')]
14+
public string $token;
15+
16+
#[Assert\NotBlank]
17+
#[Assert\Type(type: 'string')]
18+
#[Assert\Length(min: 8)]
19+
public string $newPassword;
20+
21+
public function getDto(): ResetPasswordRequest
22+
{
23+
return $this;
24+
}
25+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Identity\Request;
6+
7+
use PhpList\RestBundle\Common\Request\RequestInterface;
8+
use Symfony\Component\Validator\Constraints as Assert;
9+
10+
class ValidateTokenRequest implements RequestInterface
11+
{
12+
#[Assert\NotBlank]
13+
#[Assert\Type(type: 'string')]
14+
public string $token;
15+
16+
public function getDto(): ValidateTokenRequest
17+
{
18+
return $this;
19+
}
20+
}

0 commit comments

Comments
 (0)