Skip to content

Commit 86962ef

Browse files
committed
code challenge 9 solution
1 parent 606e4be commit 86962ef

File tree

48 files changed

+999
-17
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+999
-17
lines changed

CODING-CHALLENGE-10.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# RESTful Webservices in Symfony
2+
3+
## Coding Challenge 10 - JSON WEB TOKEN
4+
5+
### Tasks
6+
7+
Let's set up the Authentication/Authorization for our API based on JSON Web Token
8+
9+
1. Implement a Token provider
10+
11+
- configure the security system for basic auth
12+
- add a TokenController to retrieve a token
13+
14+
2. Implement a Guard
15+
16+
- implement a JwtGuardAuthenticator
17+
18+
3. Secure your Controllers to meet the requirements described in README.md
19+
20+
- use the `#[IsGranted]` attribute
21+
- or alternatively add `access_control` entries
22+
23+
### Solution
24+
25+
#### Preparation
26+
27+
- require Symfony's SecurityBundle: `composer req security`
28+
- require LexikJWTAuthenticationBundle: `composer req lexik/jwt-authentication-bundle`
29+
- run `php bin/console lexik:jwt:generate-keypair`
30+
31+
#### GuardAuthenticator
32+
33+
- adjust firewall configuration (firewall for token: `http_basic`, firewall for api: `custom_authenticators`)
34+
- implement a `TokenController`, secure it with http_basic and return a JWT token
35+
- implement a `JwtTokenAuthenticator` extending the `AbstractAuthenticator`
36+
- configure the `JwtTokenAuthenticator` on the api firewall (`custom_authenticators` option)
37+
- add `#[IsGranted]` attributes to your controller actions
38+
- add an Authorization header to your Postman endpoints
39+
40+
#### Make things shiny
41+
42+
- use the serializer to create a nice error response in the `JwtTokenAuthenticator::start()` method
43+
- implement the `JwtTokenAuthenticator::onAuthenticationFailure()` and use the serializer to return an
44+
HTTP 401 Unauthorized Response with a nice error message

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
"symfony/console": "5.4.*",
2020
"symfony/deprecation-contracts": "^2.1|^3",
2121
"symfony/dotenv": "5.4.*",
22+
"symfony/expression-language": "5.4.*",
2223
"symfony/flex": "^1.17",
2324
"symfony/framework-bundle": "5.4.*",
2425
"symfony/property-access": "5.4.*",

composer.lock

Lines changed: 64 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ArgumentValueResolver;
6+
7+
use App\Domain\Model\CreateWorkshopModel;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
10+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
use Symfony\Component\Validator\Exception\ValidationFailedException;
13+
use Symfony\Component\Validator\Validator\ValidatorInterface;
14+
15+
final class CreateWorkshopModelResolver implements ArgumentValueResolverInterface
16+
{
17+
public function __construct(
18+
private SerializerInterface $serializer,
19+
private ValidatorInterface $validator,
20+
) {
21+
}
22+
23+
public function supports(Request $request, ArgumentMetadata $argument): bool
24+
{
25+
return CreateWorkshopModel::class === $argument->getType() && 'POST' === $request->getMethod();
26+
}
27+
28+
/**
29+
* @return iterable<CreateWorkshopModel>
30+
*/
31+
public function resolve(Request $request, ArgumentMetadata $argument)
32+
{
33+
$model = $this->serializer->deserialize(
34+
$request->getContent(),
35+
CreateWorkshopModel::class,
36+
$request->getRequestFormat(),
37+
);
38+
39+
$validationErrors = $this->validator->validate($model);
40+
41+
if (\count($validationErrors) > 0) {
42+
throw new ValidationFailedException($model, $validationErrors);
43+
}
44+
45+
yield $model;
46+
}
47+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\ArgumentValueResolver;
6+
7+
use App\Domain\Model\UpdateWorkshopModel;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
10+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
11+
use Symfony\Component\Serializer\SerializerInterface;
12+
use Symfony\Component\Validator\Exception\ValidationFailedException;
13+
use Symfony\Component\Validator\Validator\ValidatorInterface;
14+
15+
final class UpdateWorkshopModelResolver implements ArgumentValueResolverInterface
16+
{
17+
public function __construct(
18+
private SerializerInterface $serializer,
19+
private ValidatorInterface $validator,
20+
) {
21+
}
22+
23+
public function supports(Request $request, ArgumentMetadata $argument): bool
24+
{
25+
return UpdateWorkshopModel::class === $argument->getType() && 'PUT' === $request->getMethod();
26+
}
27+
28+
/**
29+
* @return iterable<UpdateWorkshopModel>
30+
*/
31+
public function resolve(Request $request, ArgumentMetadata $argument)
32+
{
33+
$model = $this->serializer->deserialize(
34+
$request->getContent(),
35+
UpdateWorkshopModel::class,
36+
$request->getRequestFormat(),
37+
);
38+
39+
$validationErrors = $this->validator->validate($model);
40+
41+
if (\count($validationErrors) > 0) {
42+
throw new ValidationFailedException($model, $validationErrors);
43+
}
44+
45+
yield $model;
46+
}
47+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Attendee;
6+
7+
use App\Domain\AttendeeRemover;
8+
use App\Entity\Attendee;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\Routing\Annotation\Route;
12+
13+
#[Route('/attendees/{identifier}', name: 'delete_attendee', methods: ['DELETE'])]
14+
class DeleteController
15+
{
16+
public function __construct(
17+
private AttendeeRemover $attendeeRemover,
18+
) {
19+
}
20+
21+
public function __invoke(Request $request, Attendee $attendee)
22+
{
23+
$this->attendeeRemover->remove($attendee);
24+
25+
return new Response(null, Response::HTTP_NO_CONTENT);
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Workshop;
6+
7+
use App\Entity\Attendee;
8+
use App\Entity\Workshop;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpFoundation\Response;
13+
use Symfony\Component\Routing\Annotation\Route;
14+
15+
#[Route('/workshops/{identifier}/attendees/add/{attendee_identifier}', name: 'add_attendee_to_workshop', methods: ['POST'])]
16+
#[Entity('attendee', expr: 'repository.findOneByIdentifier(attendee_identifier)')]
17+
class AddAttendeeToWorkshopController
18+
{
19+
public function __construct(
20+
private EntityManagerInterface $entityManager
21+
) {
22+
}
23+
24+
public function __invoke(Request $request, Workshop $workshop, Attendee $attendee)
25+
{
26+
$workshop->addAttendee($attendee);
27+
28+
$this->entityManager->flush();
29+
30+
return new Response(null, Response::HTTP_NO_CONTENT);
31+
}
32+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Workshop;
6+
7+
use App\Domain\Model\CreateWorkshopModel;
8+
use App\Domain\WorkshopCreator;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\Routing\Annotation\Route;
12+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
13+
use Symfony\Component\Serializer\SerializerInterface;
14+
15+
#[Route('/workshops', name: 'create_workshop', methods: ['POST'])]
16+
final class CreateController
17+
{
18+
public function __construct(
19+
private WorkshopCreator $workshopCreator,
20+
private SerializerInterface $serializer,
21+
private UrlGeneratorInterface $urlGenerator,
22+
) {
23+
}
24+
25+
public function __invoke(Request $request, CreateWorkshopModel $createWorkshopModel)
26+
{
27+
$createdWorkshop = $this->workshopCreator->create($createWorkshopModel);
28+
29+
$serializedCreatedWorkshop = $this->serializer->serialize($createdWorkshop, $request->getRequestFormat());
30+
31+
return new Response($serializedCreatedWorkshop, Response::HTTP_CREATED, [
32+
'Location' => $this->urlGenerator->generate('read_workshop', [
33+
'identifier' => $createdWorkshop->getIdentifier(),
34+
], UrlGeneratorInterface::ABSOLUTE_URL),
35+
]);
36+
}
37+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Workshop;
6+
7+
use App\Domain\WorkshopRemover;
8+
use App\Entity\Workshop;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpFoundation\Response;
11+
use Symfony\Component\Routing\Annotation\Route;
12+
13+
#[Route('/workshops/{identifier}', name: 'delete_workshop', methods: ['DELETE'])]
14+
class DeleteController
15+
{
16+
public function __construct(
17+
private WorkshopRemover $workshopRemover,
18+
) {
19+
}
20+
21+
public function __invoke(Request $request, Workshop $workshop)
22+
{
23+
$this->workshopRemover->remove($workshop);
24+
25+
return new Response(null, Response::HTTP_NO_CONTENT);
26+
}
27+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Controller\Workshop;
6+
7+
use App\Entity\Attendee;
8+
use App\Entity\Workshop;
9+
use Doctrine\ORM\EntityManagerInterface;
10+
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Entity;
11+
use Symfony\Component\HttpFoundation\Request;
12+
use Symfony\Component\HttpFoundation\Response;
13+
use Symfony\Component\Routing\Annotation\Route;
14+
15+
#[Route('/workshops/{identifier}/attendees/remove/{attendee_identifier}', name: 'remove_attendee_from_workshop', methods: ['POST'])]
16+
#[Entity('attendee', expr: 'repository.findOneByIdentifier(attendee_identifier)')]
17+
class RemoveAttendeeFromWorkshopController
18+
{
19+
public function __construct(
20+
private EntityManagerInterface $entityManager
21+
) {
22+
}
23+
24+
public function __invoke(Request $request, Workshop $workshop, Attendee $attendee)
25+
{
26+
$workshop->removeAttendee($attendee);
27+
28+
$this->entityManager->flush();
29+
30+
return new Response(null, Response::HTTP_NO_CONTENT);
31+
}
32+
}

0 commit comments

Comments
 (0)