-
Notifications
You must be signed in to change notification settings - Fork 67
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Does this bundle support API platform? #128
Comments
I've made progress with API platform by using the maker install and then making some modifications. For the reset request I've used the messenger integration and a handler. <?php
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* messenger=true,
* collectionOperations={
* "post"={"status"=202, "path"="/reset_password/request.{_format}"}
* },
* itemOperations={},
* output=false)
*
*/
final class ResetPasswordRequestInput
{
/**
* @var string email
* @Assert\NotBlank
*/
public $email;
} and <?php
namespace App\Messenger\Handler;
use App\Entity\ResetPasswordRequestInput;
use App\Repository\ResetPasswordRequestRepository;
use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
use Symfony\Component\Mime\Address;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
class ResetPasswordRequestHandler implements MessageHandlerInterface
{
/**
* @var UserRepository
*/
private UserRepository $userRepository;
/**
* @var ResetPasswordRequestRepository
*/
private ResetPasswordRequestRepository $resetPasswordRequestRepository;
/**
* @var ResetPasswordHelperInterface
*/
private ResetPasswordHelperInterface $resetPasswordHelper;
/**
* @var MailerInterface
*/
private MailerInterface $mailer;
public function __construct(UserRepository $userRepository, ResetPasswordRequestRepository $resetPasswordRequestRepository, ResetPasswordHelperInterface $resetPasswordHelper, MailerInterface $mailer)
{
$this->userRepository = $userRepository;
$this->resetPasswordRequestRepository = $resetPasswordRequestRepository;
$this->resetPasswordHelper = $resetPasswordHelper;
$this->mailer = $mailer;
}
public function __invoke(ResetPasswordRequestInput $resetPasswordRequestInput)
{
$user = $this->userRepository->findOneByEmail($resetPasswordRequestInput->email);
if (!$user) {
return;
}
try {
$resetToken = $this->resetPasswordHelper->generateResetToken($user);
} catch (ResetPasswordExceptionInterface $e) {
return;
}
$email = (new TemplatedEmail())
->from(new Address('[email protected]', 'Password reset'))
->to($user->getEmail())
->subject('Your password reset request')
->htmlTemplate('reset_password/email.html.twig')
->context([
'resetToken' => $resetToken,
'tokenLifetime' => $this->resetPasswordHelper->getTokenLifetime(),
])
;
$this->mailer->send($email);
}
} Then for the actual reset I used a DTO and DataTransformer. On my /**
* @ApiResource(
* collectionOperations={
* "post"={"security"="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')"},
* "post_change_password"={
* "method"="POST",
* "path"="/reset_password/change",
* "input"=ResetPasswordChangeInput::class,
* "output"=false,
* "status"=201
* },
//.... etc <?php
namespace App\Dto;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class ResetPasswordChangeInput
{
/**
* @var string
* @Assert\NotBlank()
* @Groups({"user:write"})
*/
public $token;
/**
* @var string
* @Groups({"user:write"})
* @Assert\Length(min=8, max=255, allowEmptyString = false )
*/
public $plainPassword;
} and then have a DataTransformer that deals with the actual password change: <?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use App\Dto\ResetPasswordChangeInput;
use App\Entity\User;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
class ResetPasswordChangeDataTransformer implements DataTransformerInterface
{
/**
* @var ResetPasswordHelperInterface
*/
private ResetPasswordHelperInterface $resetPasswordHelper;
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper)
{
$this->resetPasswordHelper = $resetPasswordHelper;
}
/**
* @inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return User::class === $to && ResetPasswordChangeInput::class === ($context['input']['class'] ?? null);
}
/**
* @param ResetPasswordChangeInput $dto
* @param string $to
* @param array $context
*/
public function transform($dto, string $to, array $context = [])
{
$token = $dto->token;
try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
throw new BadRequestHttpException(
sprintf(
'There was a problem validating your reset request - %s',
$e->getReason()
)
);
}
// do our own validation here so that the token doesn't get invalidated on password validation failure
$violations = $this->validator->validate($dto);
if (count($violations) > 0) {
throw new ValidationException($violations);
}
if ($user instanceof User) {
$this->resetPasswordHelper->removeResetRequest($token);
$user->setPlainPassword($dto->plainPassword);
}
return $user;
}
} I removed the Controller, FormTypes and some of the templates that were made by the maker as I don't need them, but if you wanted to retain the ability to do resets via API or forms I imagine that you could keep them in place. |
Thanks for sharing :). I haven't looked at your code in detail, but the Messenger integration is probably what I would have chosen too. I think this is actually something we should put into MakerBundle or at least the docs here. Basically, you should be able to choose that you want to use this bundle in "API mode" and get code generated. I would welcome a PR for that. |
I'm also going to cover this at some point soonish in a SymfonyCasts tutorial. If anyone wants to turn this into a MakerBundle PR, you can ping me on the Symfony Slack so we can chat what that should look like so that you don't lose time. I would love if someone wanted to take that challenge on! |
Thank you for the code @sh41 ! It's missing de little part on the DataTransformer with the service validator: <?php
namespace App\DataTransformer;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
use ApiPlatform\Core\Validator\Exception\ValidationException;
use ApiPlatform\Core\Validator\ValidatorInterface;
use App\Dto\ResetPasswordChangeInput;
use App\Entity\User;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use SymfonyCasts\Bundle\ResetPassword\Exception\ResetPasswordExceptionInterface;
use SymfonyCasts\Bundle\ResetPassword\ResetPasswordHelperInterface;
class ResetPasswordChangeDataTransformer implements DataTransformerInterface
{
private ValidatorInterface $validator; // fix here
private ResetPasswordHelperInterface $resetPasswordHelper;
public function __construct(ResetPasswordHelperInterface $resetPasswordHelper, ValidatorInterface $validator)
{
$this->resetPasswordHelper = $resetPasswordHelper;
$this->validator = $validator;
}
/**
* @inheritDoc
*/
public function supportsTransformation($data, string $to, array $context = []): bool
{
return User::class === $to && ResetPasswordChangeInput::class === ($context['input']['class'] ?? null);
}
/**
* @param ResetPasswordChangeInput $dto
* @param string $to
* @param array $context
*/
public function transform($dto, string $to, array $context = [])
{
$token = $dto->token;
try {
$user = $this->resetPasswordHelper->validateTokenAndFetchUser($token);
} catch (ResetPasswordExceptionInterface $e) {
throw new BadRequestHttpException(
sprintf(
'There was a problem validating your reset request - %s',
$e->getReason()
)
);
}
// do our own validation here so that the token doesn't get invalidated on password validation failure
$violations = $this->validator->validate($dto);
if (null !== $violations) { // fix here
throw new ValidationException($violations);
}
if ($user instanceof User) {
$this->resetPasswordHelper->removeResetRequest($token);
$user->setPlainPassword($dto->plainPassword);
}
return $user;
}
} @weaverryan Indeed a maker for this would be nice ^^ I am trying to adapt your bundles reset-password and email-verify with Api platform but not so easy even if I just finished the part 3 with your tuto ^^ |
Hi all, I've tried to implement this, but I get a 400 error in ResetPasswordRequestInput as $email is null, in the profiler, I can see that the raw content is {"email":"[email protected]"} but the Post Parameters are empty, I do have the JWT bundle installed as well, so I'm not sure if I have missed something or that I need to set something else in security.yaml, tbh I'm a little lost and obviously, I need someone smarter than me to point me in the right direction. ;-) |
Hello @Tempest99 ,
Also with Api Platform 2.6, i needed to add:
to make it work to send the email. |
Hi @GrandOurs35, wow thats neater compared to what I finally did, I meant to post up what I did, but I got way too busy with my daytime job.
and I have a firewall for it as well
then in the ResetPasswordRequestInput.php mine looks like this
I doubt I've missed anything else I did, maybe some of it's overkill, but until I fully understand things better, I won't be refactoring anything, but it works :-) |
Just a heads up, I'm working on the implementation for this in |
The password endpoints are functional, but the react-admin frontend that reads the Hydra generated document is complaining that the ResetPasswordRequestInput endpoint does not provide a GET item operation declared. I do believe that is the expected behavior, but is there a way to help Hydra documentation to fullfill this requirement? |
I've added the reset request operation to User entity instead of using a new ApiResource for it. This made the password request and change operations to be related to the User resource on the API. Then migrate the code from ResetPasswordRequestInput from an entity to become a new DTO. And then add a DataTransformer to dispatch the message of ResetPasswordRequestInput to be handled by the message handler. (remember to change the handler to reference the ResetPasswordRequestInput from Dto namespace instead of older Entity File: User.php
File: App\Dto\ResetPasswordRequestInput.php
File: App\DataTransformer\ResetPasswordRequestDataTransformer.php
|
No description provided.
The text was updated successfully, but these errors were encountered: