Skip to content
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

PATCH route with custom uriTemplate ignores state provider #2647

Open
MateuszCzz opened this issue Feb 28, 2024 · 0 comments
Open

PATCH route with custom uriTemplate ignores state provider #2647

MateuszCzz opened this issue Feb 28, 2024 · 0 comments

Comments

@MateuszCzz
Copy link

I'm trying to set up a PATCH route for the User entity that retrieves the user information from the access token instead of using uriVariables. In my mind, such approach would allow users to only edit their own profile. I followed the Smartcast tutorial for a similar approach on PUT routes in dragonTresures.

Steps to Reproduce:

  1. Changed uriTemplate of the PATCH route.
  2. Implemented UserFromTokenProvider to fetch the user from the token.
  3. Sent a PATCH request with newEmail, oldPassword, and newPassword fields.
  4. Catch results in custom state persister.

Expected Behaviour:,

The provider should fill the data with returned object.

Actual Behaviour:

The provider is completely omitted and by extension route always returns an empty object filled only by data provided by user in request.

Code Snippets:

1. Relevant API Platform Configuration:

#[ApiResource(
  description: "Represents a single user in the system.",
  operations: [
    new GetCollection(
      normalizationContext: ['groups' => ['user:read_list']],
    ),
    new Get(
      normalizationContext: ['groups'=> ['user:read']],
    ),
    new Patch(
      name: 'credentials',
      uriTemplate: '/users/credentials',
      processor: UserCredentialsPersistStateProcessor::class,
      validationContext: ['groups' => ['user:write_credentials']],
      denormalizationContext: ['groups' => ['user:write_credentials']],
      security: "is_granted('ROLE_REDDIT_ADMIN') or is_granted('ROLE_USER')",
      securityMessage: "Only user himself can modify his settings.",
      provider: UserFromTokenProvider::class,
    ),
    // rest of routes
  ],
  paginationItemsPerPage: 25
)]

2. UserFromTokenProvider:

class UserFromTokenProvider implements ProviderInterface
{
  public function __construct(
    private UserRepository $repository,
    private Security $security
  ) {}

  public function provide(Operation $operation, array $uriVariables = [], array $context = []): User|null
  {
    $currentUser = $this->security->getUser();
    if ($currentUser !== null) {
      $user = $this->repository->findOneBy(['login' => $currentUser->getUserIdentifier()]);
      return $user;
    } else {
      throw new ItemNotFoundException('Token is not assosiated with user.', 404);
    }
  }
}

3. UserCredentialsPersistStateProcessor:

class UserCredentialsPersistStateProcessor implements ProcessorInterface
{
  public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User|null
  {
    var_dump($data); // line used for debugging purposes

    //validate data
    $this->validator->validate($data);

    //compare passwords
    if (!$this->passwordHasher->isPasswordValid($data, $data->getPlainOldPassword())) {
        $data->eraseCredentials();
        throw new InvalidArgumentException('Invalid credentials.', 404);
    }

    //check if password or email are set
    if (!$data->getPlainPassword() && !$data->getNewEmail()) {
        $data->eraseCredentials();
        throw new InvalidArgumentException('Either email or new password should be provided.', 400);
    }
    // rest of the class...
  }
}

4. Sample Request and Response:

Request:

curl -X 'PATCH' \
  'https://localhost/api/users/credentials' \
  -H 'accept: application/ld+json' \
  -H 'Authorization: Bearer rpa_44166cbb10dfa38586b899ae891d749f266be2577638ee7f5a9f0a933026b34c' \
  -H 'Content-Type: application/merge-patch+json' \
  -d '{
    "newEmail": "user@example.com",
    "oldPassword": "string",
    "newPassword": "string"
  }'

Response:

object(App\Entity\User)#1242 (17) {
  ["id":"App\Entity\User":private]=>
  NULL
  ["login":"App\Entity\User":private]=>
  NULL
  ["nickname":"App\Entity\User":private]=>
  NULL
  ["email":"App\Entity\User":private]=>
  NULL
  ["newEmail":"App\Entity\User":private]=>
  string(16) "user@example.com"
  // ... other properties
}

Additional Information:

  • I have tried various approaches, including throwing exceptions in state provider, tweeking route configuration, hooking into the Built-In State Provider like shown in documentation and returning objects with the given ID, but the issue persists.

I would appreciate any help in identifying the root cause of this issue and achieving the desired behaviour for the PATCH route.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant