| @@ -16,12 +16,14 @@ security: | |||
| auth: | |||
| pattern: ^/api/auth | |||
| stateless: true | |||
| json_login: | |||
| check_path: /api/auth | |||
| username_path: email | |||
| password_path: password | |||
| success_handler: lexik_jwt_authentication.handler.authentication_success | |||
| failure_handler: lexik_jwt_authentication.handler.authentication_failure | |||
| custom_authenticators: | |||
| - App\Security\JwtAuthenticator | |||
| # json_login: | |||
| # check_path: /api/auth | |||
| # username_path: email | |||
| # password_path: password | |||
| # success_handler: lexik_jwt_authentication.handler.authentication_success | |||
| # failure_handler: lexik_jwt_authentication.handler.authentication_failure | |||
| api: | |||
| pattern: ^/api/ | |||
| stateless: true | |||
| @@ -4,7 +4,7 @@ controllers: | |||
| namespace: App\Controller | |||
| type: attribute | |||
| # Bestehende Auth-Route | |||
| ## Bestehende Auth-Route | |||
| auth: | |||
| path: /api/auth | |||
| methods: ['POST'] | |||
| @@ -23,6 +23,14 @@ services: | |||
| # add more service definitions when explicit configuration is needed | |||
| # please note that last definitions always *replace* previous ones | |||
| App\OpenApi\AuthDecorator: | |||
| decorates: 'api_platform.openapi.factory' | |||
| arguments: ['@.inner'] | |||
| App\OpenApi\OpenApiFactory: | |||
| decorates: 'api_platform.openapi.factory' | |||
| arguments: [ '@.inner' ] | |||
| acme_api.event.authentication_success_listener: | |||
| class: App\EventListener\AuthenticationSuccessListener | |||
| tags: | |||
| @@ -0,0 +1,16 @@ | |||
| <?php | |||
| // src/Dto/LoginRequest.php | |||
| namespace App\Dto; | |||
| use ApiPlatform\Metadata\ApiResource; | |||
| use ApiPlatform\Metadata\ApiProperty; | |||
| #[ApiResource] | |||
| class LoginRequest | |||
| { | |||
| #[ApiProperty(example: "user@example.com")] | |||
| public string $email = ''; | |||
| #[ApiProperty(example: "password123")] | |||
| public string $password = ''; | |||
| } | |||
| @@ -27,7 +27,20 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; | |||
| shortName: 'MediaObject', | |||
| types: ['https://schema.org/MediaObject'], | |||
| operations: [ | |||
| new Get(), | |||
| new Get( | |||
| openapi: new Model\Operation( | |||
| responses: [ | |||
| '200' => [ | |||
| 'content' => [ | |||
| 'application/ld+json' => [ | |||
| 'schema' => ['$ref' => '#/components/schemas/MediaObject'] | |||
| ] | |||
| ] | |||
| ] | |||
| ] | |||
| ), | |||
| normalizationContext: ['groups' => ['media_object:read']] | |||
| ), | |||
| new GetCollection(), | |||
| new Post( | |||
| controller: CreateMediaObjectAction::class, | |||
| @@ -0,0 +1,111 @@ | |||
| <?php | |||
| // src/OpenApi/AuthDecorator.php | |||
| namespace App\OpenApi; | |||
| use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |||
| use ApiPlatform\OpenApi\Model\PathItem; | |||
| use ApiPlatform\OpenApi\Model\Operation; | |||
| use ApiPlatform\OpenApi\Model\RequestBody; | |||
| use ApiPlatform\OpenApi\OpenApi; | |||
| use ArrayObject; | |||
| final class AuthDecorator implements OpenApiFactoryInterface | |||
| { | |||
| public function __construct( | |||
| private OpenApiFactoryInterface $decorated | |||
| ) {} | |||
| public function __invoke(array $context = []): OpenApi | |||
| { | |||
| $openApi = ($this->decorated)($context); | |||
| $schemas = $openApi->getComponents()->getSchemas(); | |||
| $schemas['Credentials'] = new \ArrayObject([ | |||
| 'type' => 'object', | |||
| 'properties' => [ | |||
| 'email' => [ | |||
| 'type' => 'string', | |||
| 'example' => 'user@example.com', | |||
| ], | |||
| 'password' => [ | |||
| 'type' => 'string', | |||
| 'example' => 'password123', | |||
| ], | |||
| ], | |||
| ]); | |||
| $schemas['AuthResponse'] = new \ArrayObject([ | |||
| 'type' => 'object', | |||
| 'properties' => [ | |||
| 'token' => [ | |||
| 'type' => 'string', | |||
| 'readOnly' => true, | |||
| ], | |||
| 'id' => [ | |||
| 'type' => 'string', | |||
| 'readOnly' => true, | |||
| ], | |||
| 'email' => [ | |||
| 'type' => 'string', | |||
| 'readOnly' => true, | |||
| ], | |||
| 'firstName' => [ | |||
| 'type' => 'string', | |||
| 'readOnly' => true, | |||
| ], | |||
| 'lastName' => [ | |||
| 'type' => 'string', | |||
| 'readOnly' => true, | |||
| ], | |||
| 'roles' => [ | |||
| 'type' => 'array', | |||
| 'items' => [ | |||
| 'type' => 'string' | |||
| ], | |||
| 'readOnly' => true, | |||
| ], | |||
| 'user' => [ | |||
| '$ref' => '#/components/schemas/User', // API Platform's automatisch generiertes Schema | |||
| 'readOnly' => true, | |||
| ], | |||
| ], | |||
| ]); | |||
| $pathItem = new PathItem( | |||
| ref: 'JWT Token', | |||
| post: new Operation( | |||
| operationId: 'postCredentialsItem', | |||
| tags: ['Auth'], | |||
| responses: [ | |||
| '200' => [ | |||
| 'description' => 'Get JWT token', | |||
| 'content' => [ | |||
| 'application/json' => [ | |||
| 'schema' => [ | |||
| '$ref' => '#/components/schemas/AuthResponse', | |||
| ], | |||
| ], | |||
| ], | |||
| ], | |||
| ], | |||
| summary: 'Get JWT token to login.', | |||
| requestBody: new RequestBody( | |||
| description: 'Generate new JWT Token', | |||
| content: new \ArrayObject([ | |||
| 'application/json' => [ | |||
| 'schema' => [ | |||
| '$ref' => '#/components/schemas/Credentials', | |||
| ], | |||
| ], | |||
| ]), | |||
| ), | |||
| security: [], | |||
| ), | |||
| ); | |||
| $openApi->getPaths()->addPath('/api/auth', $pathItem); | |||
| return $openApi; | |||
| } | |||
| } | |||
| @@ -0,0 +1,34 @@ | |||
| <?php | |||
| namespace App\OpenApi; | |||
| use ApiPlatform\OpenApi\Factory\OpenApiFactoryInterface; | |||
| use ApiPlatform\OpenApi\OpenApi; | |||
| use ApiPlatform\OpenApi\Model; | |||
| class OpenApiFactory implements OpenApiFactoryInterface | |||
| { | |||
| public function __construct( | |||
| private OpenApiFactoryInterface $decorated | |||
| ) {} | |||
| public function __invoke(array $context = []): OpenApi | |||
| { | |||
| $openApi = ($this->decorated)($context); | |||
| // Cleanup schema names | |||
| $schemas = $openApi->getComponents()->getSchemas(); | |||
| $cleanedSchemas = new \ArrayObject(); | |||
| foreach ($schemas as $key => $schema) { | |||
| // Remove the .jsonld-media_object.read suffix | |||
| $newKey = preg_replace('/\.jsonld-.*/', '', $key); | |||
| $cleanedSchemas[$newKey] = $schema; | |||
| } | |||
| $openApi = $openApi->withComponents( | |||
| $openApi->getComponents()->withSchemas($cleanedSchemas) | |||
| ); | |||
| return $openApi; | |||
| } | |||
| } | |||
| @@ -0,0 +1,75 @@ | |||
| <?php | |||
| // src/Security/JwtAuthenticator.php | |||
| namespace App\Security; | |||
| use App\ApiResource\UserApi; | |||
| use App\Repository\UserRepository; | |||
| use Lexik\Bundle\JWTAuthenticationBundle\Services\JWTTokenManagerInterface; | |||
| use Symfony\Component\HttpFoundation\JsonResponse; | |||
| use Symfony\Component\HttpFoundation\Request; | |||
| use Symfony\Component\HttpFoundation\Response; | |||
| use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | |||
| use Symfony\Component\Security\Core\Exception\AuthenticationException; | |||
| use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; | |||
| use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials; | |||
| use Symfony\Component\Security\Http\Authenticator\Passport\Passport; | |||
| use Symfonycasts\MicroMapper\MicroMapperInterface; | |||
| class JwtAuthenticator extends AbstractAuthenticator | |||
| { | |||
| public function __construct( | |||
| private JWTTokenManagerInterface $jwtManager, | |||
| private UserRepository $userRepository, | |||
| private MicroMapperInterface $microMapper | |||
| ) {} | |||
| public function supports(Request $request): ?bool | |||
| { | |||
| return $request->getPathInfo() === '/api/auth' && $request->isMethod('POST'); | |||
| } | |||
| public function authenticate(Request $request): Passport | |||
| { | |||
| $data = json_decode($request->getContent(), true); | |||
| if (!$data) { | |||
| throw new CustomUserMessageAuthenticationException('Invalid JSON.'); | |||
| } | |||
| $email = $data['email'] ?? ''; | |||
| $password = $data['password'] ?? ''; | |||
| return new Passport( | |||
| new UserBadge($email), | |||
| new PasswordCredentials($password) | |||
| ); | |||
| } | |||
| public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response | |||
| { | |||
| $user = $token->getUser(); | |||
| $jwtToken = $this->jwtManager->create($user); | |||
| return new JsonResponse([ | |||
| 'token' => $jwtToken, | |||
| 'id' => $user->getId(), | |||
| 'email' => $user->getEmail(), | |||
| 'firstName' => $user->getFirstName(), | |||
| 'lastName' => $user->getLastName(), | |||
| 'roles' => $user->getRoles(), | |||
| 'user' => $this->microMapper->map( | |||
| $user, UserApi::class | |||
| ) | |||
| ]); | |||
| } | |||
| public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response | |||
| { | |||
| return new JsonResponse([ | |||
| 'message' => $exception->getMessage() | |||
| ], Response::HTTP_UNAUTHORIZED); | |||
| } | |||
| } | |||