Open michaelHottomali opened 4 years ago
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":"me@me.com"} 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 , i had the same problem like you. For me i added the denormalization group and it works:
/**
* @var string email
* @Assert\NotBlank
* @Groups({"resetpasswordrequestinput:collection:post"})
*/
public string $email;
Also with Api Platform 2.6, i needed to add:
/**
* @var integer|null
*/
public $id;
to make it work to send the email. I didn't go further for now because i need to follow the end of the part 3 of Api Platform tutorial to undestand DTO.
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. My whole api is locked down, and like you still working through the tutorials which are amazing. So what I did was... in security.yaml under access_control I have
- { path: ^/api/reset_password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
and I have a firewall for it as well
reset_password_request:
pattern: ^/api/reset_password
stateless: true
lazy: true
then in the ResetPasswordRequestInput.php mine looks like this
namespace App\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ApiResource(
* messenger=true,
* collectionOperations={
* "post"={"status"=202,
* "path"="/reset_password/request.{_format}",
* "is_granted('ROLE_PUBLIC')"
* }
* },
* itemOperations={},
* output=false)
* @IsGranted("IS_AUTHENTICATED_ANONYMOUSLY")
* @IsGranted("ROLE_PUBLIC")
*/
final class ResetPasswordRequestInput
{
/**
* @var string emailaddress
* @Assert\NotBlank
* @IsGranted("ROLE_PUBLIC")
*/
public $emailaddress;
}
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 MakerBundle
as we speak... I'm committing myself to have a PR up later today! I'm simultaneously working on a PR to update our readme here in this repo to explain how to integrate reset-password w/ Api Platform..
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?
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
...
/**
* @ApiResource(
* attributes={"security"="is_granted('ROLE_ADMIN')"},
* collectionOperations={
* "post_reset_password"={
* "method"="POST",
* "status"=202,
* "messenger"="input",
* "security"="is_granted('IS_AUTHENTICATED_ANONYMOUSLY')",
* "input"=ResetPasswordRequestInput::class,
* "output"=false,
* "path"="/reset_password/request.{_format}",
* },
* "get","post",
File: App\Dto\ResetPasswordRequestInput.php
<?php
namespace App\Dto;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
class ResetPasswordRequestInput
{
/**
* @var string
* @Assert\NotBlank()
*/
public $email;
}
File: App\DataTransformer\ResetPasswordRequestDataTransformer.php
<?php
namespace App\DataTransformer;
use App\Entity\User;
use App\Dto\ResetPasswordRequestInput;
use ApiPlatform\Core\DataTransformer\DataTransformerInterface;
class ResetPasswordRequestDataTransformer implements DataTransformerInterface
{
public function supportsTransformation($data, string $to, array $context = []): bool
{
$this->data = $data;
return User::class === $to && ResetPasswordRequestInput::class === ($context['input']['class'] ?? null);
}
/**
* @param ResetPasswordChangeInput $dto
* @param string $to
* @param array $context
*/
public function transform($dto, string $to, array $context = [])
{
$dto->email = $this->data['email'];
return $dto;
}
}
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.
and
Then for the actual reset I used a DTO and DataTransformer. On my
User
entity I added apost_change_password
custom method with custom input class ofResetPasswordChangeInput
and then have a DataTransformer that deals with the actual password change:
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.