feat: api endpoints

This commit is contained in:
Björn Fromme
2024-12-04 14:18:47 +01:00
parent 7379f5ac93
commit 231aaaac9b
26 changed files with 606 additions and 43 deletions
+2
View File
@@ -44,3 +44,5 @@ APP_BPN_USER=
APP_BPN_PASSWORD= APP_BPN_PASSWORD=
APP_BPN_ENDPOINT= APP_BPN_ENDPOINT=
APP_BPN_DEBUG=false APP_BPN_DEBUG=false
API_KEYS=
+7 -2
View File
@@ -10,6 +10,12 @@ security:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
api:
lazy: true
stateless: true
pattern: ^/api
custom_authenticator:
- App\Security\ApiKeyAuthenticator
main: main:
lazy: true lazy: true
provider: bpn_user_provider provider: bpn_user_provider
@@ -27,8 +33,7 @@ security:
# Easy way to control access for large sections of your site # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } - { path: ^/api, roles: ROLE_API }
# - { path: ^/profile, roles: ROLE_USER }
when@test: when@test:
security: security:
+4
View File
@@ -22,6 +22,10 @@ services:
- '../src/Entity/' - '../src/Entity/'
- '../src/Kernel.php' - '../src/Kernel.php'
App\Security\ApiKeyAuthenticator:
arguments:
$apiKeys: '%env(csv:API_KEYS)%'
App\BusProNet\ApiClient: App\BusProNet\ApiClient:
arguments: arguments:
$logger: '@monolog.logger.bpn' $logger: '@monolog.logger.bpn'
+15
View File
@@ -2,13 +2,28 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Groups;
class Hotel class Hotel
{ {
#[Groups(['api:list', 'api:single'])]
public ?int $id = null; public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
public ?string $code = null; public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
public ?string $name = null; public ?string $name = null;
#[Groups(['api:list', 'api:single'])]
public ?string $city = null; public ?string $city = null;
#[Groups(['api:list', 'api:single'])]
public ?string $street = null; public ?string $street = null;
#[Groups(['api:list', 'api:single'])]
public ?string $phone = null; public ?string $phone = null;
#[Groups(['api:list', 'api:single'])]
public ?string $country = null; public ?string $country = null;
} }
+20
View File
@@ -2,14 +2,34 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Pickup class Pickup
{ {
#[Groups(['api:list'])]
public ?int $id = null; public ?int $id = null;
#[Groups(['api:list'])]
public ?string $code = null; public ?string $code = null;
#[Groups(['api:list'])]
public ?string $city = null; public ?string $city = null;
#[Groups(['api:list'])]
public ?string $postalCode = null; public ?string $postalCode = null;
#[Groups(['api:list'])]
public ?string $street = null; public ?string $street = null;
#[Groups(['api:single'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d H:i'])]
public ?\DateTimeImmutable $time = null; public ?\DateTimeImmutable $time = null;
#[Groups(['api:single'])]
public ?float $price = null; public ?float $price = null;
#[Groups(['api:booking'])]
public array $mapping = []; public array $mapping = [];
} }
+41
View File
@@ -2,24 +2,65 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Room class Room
{ {
#[Groups(['api:list', 'api:single'])]
public ?int $id = null; public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
public ?string $category = null; public ?string $category = null;
#[Groups(['api:list', 'api:single'])]
public ?int $boardId = null; public ?int $boardId = null;
#[Groups(['api:list', 'api:single'])]
public ?string $code = null; public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
public ?string $label = null; public ?string $label = null;
#[Groups(['booking'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateFrom = null; public ?\DateTimeImmutable $dateFrom = null;
#[Groups(['booking'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateTo = null; public ?\DateTimeImmutable $dateTo = null;
#[Groups(['booking'])]
public ?int $totalCount = null; public ?int $totalCount = null;
#[Groups(['api:list', 'api:single'])]
public ?int $minPax = null; public ?int $minPax = null;
#[Groups(['api:list', 'api:single'])]
public ?int $maxPax = null; public ?int $maxPax = null;
#[Groups(['api:list', 'api:single'])]
public ?int $nights = null; public ?int $nights = null;
#[Groups(['api:list', 'api:single'])]
public ?string $status = null; public ?string $status = null;
#[Groups(['api:list', 'api:single'])]
public ?int $available = null; public ?int $available = null;
#[Groups(['api:list', 'api:single'])]
public ?string $board = null; public ?string $board = null;
#[Groups(['booking'])]
public array $mapping = []; public array $mapping = [];
#[Groups(['api:list', 'api:single'])]
public ?float $price = null; public ?float $price = null;
#[Groups(['booking'])]
public ?float $totalPrice = null; public ?float $totalPrice = null;
#[Groups(['booking'])]
public array $individualPrice = []; public array $individualPrice = [];
} }
+36 -2
View File
@@ -2,6 +2,10 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Service class Service
{ {
public const SOURCE_TRAVEL = 'travel'; public const SOURCE_TRAVEL = 'travel';
@@ -16,20 +20,50 @@ class Service
public const TOKEN_BOARD = 'VPF'; public const TOKEN_BOARD = 'VPF';
public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8']; public const TOKEN_RENTALS = ['VER', 'VE2', 'VE3', 'VE4', 'VE5', 'VE6', 'VE7', 'VE8'];
#[Groups(['api:single', 'api:list'])]
public ?int $id = null; public ?int $id = null;
public ?string $source = null;
#[Groups(['api:single', 'api:list'])]
public ?string $category = null; public ?string $category = null;
#[Groups(['api:single', 'api:list'])]
public ?string $status = null; public ?string $status = null;
#[Groups(['api:single', 'api:list'])]
public bool $mandatory = false; public bool $mandatory = false;
#[Groups(['api:single', 'api:list'])]
public ?string $label = null; public ?string $label = null;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateFrom = null; public ?\DateTimeImmutable $dateFrom = null;
#[Groups(['api:single', 'api:list'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateTo = null; public ?\DateTimeImmutable $dateTo = null;
#[Groups(['api:single', 'api:list'])]
public ?string $subType = null; public ?string $subType = null;
#[Groups(['booking'])]
public ?int $totalCount = null; public ?int $totalCount = null;
#[Groups(['api:single'])]
public array $mapping = []; public array $mapping = [];
#[Groups(['api:single', 'api:list'])]
public ?float $price = null; public ?float $price = null;
#[Groups(['api:single'])]
public ?float $totalPrice = null; public ?float $totalPrice = null;
#[Groups(['api:single'])]
public array $individualPrice = []; public array $individualPrice = [];
public ?string $direction = null;
#[Groups(['api:single', 'api:list'])]
public ?int $available = null; public ?int $available = null;
#[Groups(['api:single', 'api:list'])]
public ?string $source = null;
} }
+47
View File
@@ -2,25 +2,72 @@
namespace App\BusProNet\Model; namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Attribute\Context;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Travel class Travel
{ {
#[Groups(['api:list', 'api:single'])]
public ?int $id = null; public ?int $id = null;
#[Groups(['api:list', 'api:single'])]
public ?string $code = null; public ?string $code = null;
#[Groups(['api:list', 'api:single'])]
public ?string $label = null; public ?string $label = null;
#[Groups(['api:list', 'api:single'])]
public ?int $hotelId = null;
#[Groups(['api:single'])]
public ?Hotel $hotel = null;
#[Groups(['api:list', 'api:single'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateFrom = null; public ?\DateTimeImmutable $dateFrom = null;
#[Groups(['api:list', 'api:single'])]
#[Context([DateTimeNormalizer::FORMAT_KEY => 'Y-m-d'])]
public ?\DateTimeImmutable $dateTo = null; public ?\DateTimeImmutable $dateTo = null;
#[Groups(['api:list', 'api:single'])]
public ?string $type = null; public ?string $type = null;
#[Groups(['api:list', 'api:single'])]
public ?float $priceFrom = null; public ?float $priceFrom = null;
#[Groups(['api:single'])]
public array $selectionGroups = []; public array $selectionGroups = [];
#[Groups(['api:single'])]
public array $additionalServices = []; public array $additionalServices = [];
#[Groups(['api:single'])]
public bool $additionalServicesMutable = true; public bool $additionalServicesMutable = true;
#[Groups(['api:single'])]
public array $transportationServices = []; public array $transportationServices = [];
#[Groups(['api:single'])]
public bool $transportationServicesMutable = true; public bool $transportationServicesMutable = true;
#[Groups(['api:single'])]
public array $pickups = []; public array $pickups = [];
#[Groups(['api:single'])]
public bool $pickupsMutable = true; public bool $pickupsMutable = true;
#[Groups(['api:single'])]
public array $rooms = []; public array $rooms = [];
#[Groups(['api:single'])]
public bool $roomsMutable = true; public bool $roomsMutable = true;
#[Groups(['api:single'])]
public bool $participantDataMutable = true; public bool $participantDataMutable = true;
#[Groups(['api:single'])]
public bool $participantCountMutable = true; public bool $participantCountMutable = true;
public function getAdditionalServicesByGroup(mixed $group, bool $availableOnly = true): array public function getAdditionalServicesByGroup(mixed $group, bool $availableOnly = true): array
@@ -1,11 +1,11 @@
<?php <?php
namespace App\BusProNet\DataLoader; namespace App\BusProNet\XmlLoader;
use App\BusProNet\TypeConversionTrait; use App\BusProNet\TypeConversionTrait;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
abstract class AbstractDataLoader abstract class AbstractXmlLoader
{ {
use TypeConversionTrait; use TypeConversionTrait;
@@ -1,12 +1,13 @@
<?php <?php
namespace App\BusProNet\DataLoader; namespace App\BusProNet\XmlLoader;
use App\BusProNet\Model\Hotel; use App\BusProNet\Model\Hotel;
use App\BusProNet\Model\Travel;
use Psr\Cache\InvalidArgumentException; use Psr\Cache\InvalidArgumentException;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
class HotelDataLoader extends AbstractDataLoader class HotelXmlLoader extends AbstractXmlLoader
{ {
public function loadAll(?string $filename = 'hotel.xml'): array public function loadAll(?string $filename = 'hotel.xml'): array
{ {
@@ -72,4 +73,11 @@ class HotelDataLoader extends AbstractDataLoader
return $hotel; return $hotel;
} }
public function patchHotelDetails(Travel $travel): void
{
$hotelId = $travel->hotelId;
$hotel = $this->loadById($hotelId);
$travel->hotel = $hotel;
}
} }
@@ -1,13 +1,13 @@
<?php <?php
namespace App\BusProNet\DataLoader; namespace App\BusProNet\XmlLoader;
use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Pickup;
use App\BusProNet\Model\Travel; use App\BusProNet\Model\Travel;
use Psr\Cache\InvalidArgumentException; use Psr\Cache\InvalidArgumentException;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
class PickupDataLoader extends AbstractDataLoader class PickupXmlLoader extends AbstractXmlLoader
{ {
public function loadAll(?string $filename = 'zustiege.xml'): array public function loadAll(?string $filename = 'zustiege.xml'): array
{ {
@@ -1,6 +1,6 @@
<?php <?php
namespace App\BusProNet\DataLoader; namespace App\BusProNet\XmlLoader;
use App\BusProNet\Model\BaseData; use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\CrmSelection; use App\BusProNet\Model\CrmSelection;
@@ -14,12 +14,12 @@ use Symfony\Component\Finder\Finder;
use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
class TravelDataLoader extends AbstractDataLoader class TravelXmlLoader extends AbstractXmlLoader
{ {
public function __construct( public function __construct(
private readonly HotelDataLoader $hotelDataLoader, private readonly HotelXmlLoader $hotelDataLoader,
CacheInterface $cache, CacheInterface $cache,
?string $xmlPath = null ?string $xmlPath = null
) { ) {
parent::__construct($cache, $xmlPath); parent::__construct($cache, $xmlPath);
} }
@@ -67,24 +67,24 @@ class TravelDataLoader extends AbstractDataLoader
}); });
} }
public function loadById(int $id, ?string $filename = null): ?Travel public function loadById(int $travelId, ?int $hotelId = null, ?string $filename = null): ?Travel
{ {
if (null !== $filename) { if (null !== $filename) {
return $this->loadXml($id, $filename); return $this->loadXml($travelId, $hotelId, $filename);
} }
$mapping = $this->generateMapping(); $mapping = $this->generateMapping();
if (false === isset($mapping[$id])) { if (false === isset($mapping[$travelId])) {
return null; return null;
} }
$file = $mapping[$id]['file']; $file = $mapping[$travelId]['file'];
return $this->loadXml($id, $file); return $this->loadXml($travelId, $hotelId, $file);
} }
private function loadXml(int $id, string $filename): ?Travel private function loadXml(int $id, ?int $hotelId, string $filename): ?Travel
{ {
$xml = simplexml_load_file($filename); $xml = simplexml_load_file($filename);
@@ -94,16 +94,19 @@ class TravelDataLoader extends AbstractDataLoader
return null; return null;
} }
return $this->parseXml($travelXml[0]); return $this->parseXml($travelXml[0], $hotelId);
} }
public function parseXml(\SimpleXMLElement $xml): Travel public function parseXml(\SimpleXMLElement $xml, ?int $hotelId): Travel
{ {
$hotelXml = $this->getHotelXml($xml, $hotelId);
$attributes = $xml->attributes(); $attributes = $xml->attributes();
$travel = new Travel(); $travel = new Travel();
$travel->id = (int) $attributes['idbuspro']; $travel->id = (int) $attributes['idbuspro'];
$travel->label = (string) $xml->text; $travel->label = (string) $xml->text;
$travel->hotelId = $hotelId;
$travel->dateFrom = $this->stringToDate((string) $attributes['termin']); $travel->dateFrom = $this->stringToDate((string) $attributes['termin']);
$travel->dateTo = $this->stringToDate((string) $attributes['bis']); $travel->dateTo = $this->stringToDate((string) $attributes['bis']);
$travel->code = (string) $attributes['code']; $travel->code = (string) $attributes['code'];
@@ -112,7 +115,7 @@ class TravelDataLoader extends AbstractDataLoader
$travel->selectionGroups = $this->getSelectionGroups($xml); $travel->selectionGroups = $this->getSelectionGroups($xml);
$travel->additionalServices = $this->getAdditionalServices($xml); $travel->additionalServices = $this->getAdditionalServices($xml);
$travel->transportationServices = $this->getTransportationServices($xml); $travel->transportationServices = $this->getTransportationServices($xml);
$travel->rooms = $this->getRooms($xml); $travel->rooms = $this->getRooms($hotelXml);
$travel->pickups = $this->getPickups($xml); $travel->pickups = $this->getPickups($xml);
return $travel; return $travel;
@@ -213,11 +216,23 @@ class TravelDataLoader extends AbstractDataLoader
return $pickups; return $pickups;
} }
public function getHotelXml(\SimpleXMLElement $xml, ?int $hotelId): ?\SimpleXMLElement
{
// In case not hotel id is provided, take the first hotel node (which is most probably the only one)
if (null === $hotelId) {
return $xml->hotel;
}
$hotelXml = $xml->xpath(sprintf('//hotel[@idbuspro="%d"]', $hotelId));
return $hotelXml[0] ?? null;
}
public function getRooms(\SimpleXMLElement $xml): array public function getRooms(\SimpleXMLElement $xml): array
{ {
$rooms = []; $rooms = [];
foreach ($xml->hotel->zimmer->preis as $item) { foreach ($xml->zimmer->preis as $item) {
$attributes = $item->attributes(); $attributes = $item->attributes();
$roomId = (int) $attributes['idbuspro_zimmer']; $roomId = (int) $attributes['idbuspro_zimmer'];
+24
View File
@@ -0,0 +1,24 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\DataProvider\CountryDataProvider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class CountryController extends AbstractController
{
public function __construct(private readonly CountryDataProvider $dataProvider)
{
}
#[Route(path: '/countries', name: 'api_country_all')]
public function index(): JsonResponse
{
$countries = $this->dataProvider->getAll();
return $this->json($countries);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class CrmAttributesController extends AbstractController
{
public function __construct(private readonly APiClient $apiClient)
{
}
#[Route('/crm-attributes', name: 'api_crm_attributes', methods: ['GET'])]
public function index(Request $request): JsonResponse
{
$email = $request->headers->get('x-bpn-email');
$password = $request->headers->get('x-bpn-password');
if (null === $email || null === $password) {
return new JsonResponse(['message' => 'Invalid credentials'], Response::HTTP_BAD_REQUEST);
}
try {
$data = $this->apiClient->getCrmAttributes($email, $password);
if ($data instanceof Notification) {
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
}
return $this->json($data);
} catch (ApiClientException $e) {
return new JsonResponse(['message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
}
}
}
+37
View File
@@ -0,0 +1,37 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\XmlLoader\HotelXmlLoader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class HotelController extends AbstractController
{
public function __construct(private readonly HotelXmlLoader $xmlLoader)
{
}
#[Route('/hotels', name: 'api_hotel_all')]
public function index(): JsonResponse
{
$hotels = $this->xmlLoader->loadAll();
return $this->json($hotels, Response::HTTP_OK, [], ['groups' => 'api:list']);
}
#[Route('/hotels/{id}', name: 'api_hotels_single')]
public function single(int $id): JsonResponse
{
$hotel = $this->xmlLoader->loadById($id);
if (null === $hotel) {
return new JsonResponse(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
}
return $this->json($hotel, Response::HTTP_OK, [], ['groups' => 'api:single']);
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class PersonalDataController extends AbstractController
{
public function __construct(private readonly APiClient $apiClient)
{
}
#[Route('/personal-data', name: 'api_personal_data', methods: ['GET'])]
public function index(Request $request): JsonResponse
{
$email = $request->headers->get('x-bpn-email');
$password = $request->headers->get('x-bpn-password');
if (null === $email || null === $password) {
return new JsonResponse(['message' => 'Invalid credentials'], Response::HTTP_BAD_REQUEST);
}
try {
$data = $this->apiClient->getPersonalData($email, $password);
if ($data instanceof Notification) {
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
}
return $this->json($data);
} catch (ApiClientException $e) {
return new JsonResponse(['message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
}
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\XmlLoader\PickupXmlLoader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
#[Route('/api')]
class PickupController extends AbstractController
{
public function __construct(private readonly PickupXmlLoader $xmlLoader)
{
}
#[Route('/pickups', name: 'api_pickup_all')]
public function index(): JsonResponse
{
$pickups = $this->xmlLoader->loadAll();
return $this->json($pickups, Response::HTTP_OK, [], ['groups' => 'api:list']);
}
#[Route('/pickups/{id}', name: 'api_pickup_single')]
public function single(int $id): JsonResponse
{
$pickup = $this->xmlLoader->loadById($id);
return $this->json($pickup, Response::HTTP_OK, [], ['groups' => 'api:list']);
}
}
@@ -0,0 +1,47 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Validator\ValidatorInterface;
#[Route('/api')]
class ResetPasswordController extends AbstractController
{
public function __construct(private readonly ApiClient $apiClient, private readonly ValidatorInterface $validator)
{
}
#[Route('/reset-password', name: 'api_reset_password', methods: ['POST'])]
public function index(Request $request): Response
{
$payload = json_decode($request->getContent(), true);
$email = $payload['email'] ?? null;
if (null === $email) {
return new JsonResponse(['message' => 'Invalid email'], Response::HTTP_BAD_REQUEST);
}
$violationList = $this->validator->validate($email, [new Email([
'mode' => 'strict',
])]);
if (0 < count($violationList)) {
return new JsonResponse(['message' => 'Invalid email'], Response::HTTP_BAD_REQUEST);
}
try {
$this->apiClient->resetPassword($email);
} catch (ApiClientException $e) {
}
return $this->json(['message' => 'Password reset invoked']);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace App\Controller\Api;
use App\BusProNet\XmlLoader\HotelXmlLoader;
use App\BusProNet\XmlLoader\PickupXmlLoader;
use App\BusProNet\XmlLoader\TravelXmlLoader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
#[Route('/api')]
class TravelController extends AbstractController
{
public function __construct(
private readonly TravelXmlLoader $travelXmlLoader,
private readonly HotelXmlLoader $hotelXmlLoader,
private readonly PickupXmlLoader $pickupXmlLoader,
private readonly CacheInterface $cache,
) {
}
#[Route(path: '/travels', name: 'api_travel_mapping')]
public function mapping(): JsonResponse
{
$mapping = $this->travelXmlLoader->generateMapping();
return $this->json($mapping);
}
#[Route(
path: '/travels/{travelId}/{hotelId}',
name: 'api_travel_single',
requirements: ['travelId' => '\d+', 'hotelId' => '\d+']
)]
public function single(int $travelId, ?int $hotelId = null): JsonResponse
{
$cacheKey = sprintf('bpn_travel_%d_%d', $travelId, $hotelId ?? 0);
$travel = $this->cache->get($cacheKey, function (ItemInterface $item) use ($travelId, $hotelId) {
$item->expiresAfter(60);
$travel = $this->travelXmlLoader->loadById($travelId, $hotelId);
if (null === $travel) {
return new JsonResponse(['message' => 'Not found'], Response::HTTP_NOT_FOUND);
}
$this->pickupXmlLoader->patchPickupsDetails($travel);
$this->hotelXmlLoader->patchHotelDetails($travel);
return $travel;
});
return $this->json($travel, Response::HTTP_OK, [], ['groups' => ['api:list', 'api:single']]);
}
}
+7 -7
View File
@@ -3,8 +3,8 @@
namespace App\Controller\Booking; namespace App\Controller\Booking;
use App\BusProNet\ApiClient; use App\BusProNet\ApiClient;
use App\BusProNet\DataLoader\PickupDataLoader; use App\BusProNet\XmlLoader\PickupXmlLoader;
use App\BusProNet\DataLoader\TravelDataLoader; use App\BusProNet\XmlLoader\TravelXmlLoader;
use App\Form\BookingType; use App\Form\BookingType;
use App\Form\Model\BookingData; use App\Form\Model\BookingData;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -19,11 +19,11 @@ use Symfony\Contracts\Cache\ItemInterface;
class EditController extends AbstractController class EditController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ApiClient $apiClient, private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader, private readonly TravelXmlLoader $travelDataLoader,
private readonly PickupDataLoader $pickupDataLoader, private readonly PickupXmlLoader $pickupDataLoader,
private readonly CacheInterface $cache, private readonly CacheInterface $cache,
private readonly Security $security, private readonly Security $security,
) { ) {
} }
+5 -5
View File
@@ -3,7 +3,7 @@
namespace App\Controller\Booking; namespace App\Controller\Booking;
use App\BusProNet\ApiClient; use App\BusProNet\ApiClient;
use App\BusProNet\DataLoader\TravelDataLoader; use App\BusProNet\XmlLoader\TravelXmlLoader;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
@@ -16,10 +16,10 @@ use Symfony\Contracts\Cache\ItemInterface;
class IndexController extends AbstractController class IndexController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly ApiClient $apiClient, private readonly ApiClient $apiClient,
private readonly TravelDataLoader $travelDataLoader, private readonly TravelXmlLoader $travelDataLoader,
private readonly CacheInterface $cache, private readonly CacheInterface $cache,
private readonly Security $security, private readonly Security $security,
) { ) {
} }
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace App\Security;
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\Core\User\UserInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(private readonly array $apiKeys)
{
}
public function supports(Request $request): ?bool
{
return $request->headers->has('X-BPN-API-KEY');
}
public function authenticate(Request $request): Passport
{
$apiKey = $request->headers->get('X-BPN-API-KEY');
if (null === $apiKey) {
throw new CustomUserMessageAuthenticationException('No API key provided');
}
if (false === in_array($apiKey, $this->apiKeys)) {
throw new CustomUserMessageAuthenticationException('Invalid API key');
}
return new SelfValidatingPassport(
new UserBadge($apiKey, function (string $userIdentifier) use ($apiKey): ?UserInterface {
return new ApiUser($apiKey);
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$data = [
'message' => 'Authentication missing or failed',
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
namespace App\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class ApiUser implements UserInterface
{
public function __construct(private readonly string $apiKey)
{
}
public function getRoles(): array
{
return ['ROLE_USER', 'ROLE_API'];
}
public function eraseCredentials(): void
{
}
public function getUserIdentifier(): string
{
return $this->apiKey;
}
}
@@ -2,7 +2,7 @@
namespace App\Tests\BusProNet\DataLoader; namespace App\Tests\BusProNet\DataLoader;
use App\BusProNet\DataLoader\HotelDataLoader; use App\BusProNet\XmlLoader\HotelXmlLoader;
use App\BusProNet\Model\Hotel; use App\BusProNet\Model\Hotel;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -10,7 +10,7 @@ class HotelDataLoaderTest extends TestCase
{ {
public function testParse(): void public function testParse(): void
{ {
$loader = new HotelDataLoader(__DIR__.'/../../Resources'); $loader = new HotelXmlLoader(__DIR__.'/../../Resources');
$hotelId = 163113; $hotelId = 163113;
$xml = $loader->loadById($hotelId, 'hotels_data.xml'); $xml = $loader->loadById($hotelId, 'hotels_data.xml');
@@ -2,7 +2,7 @@
namespace App\Tests\BusProNet\DataLoader; namespace App\Tests\BusProNet\DataLoader;
use App\BusProNet\DataLoader\PickupDataLoader; use App\BusProNet\XmlLoader\PickupXmlLoader;
use App\BusProNet\Model\Pickup; use App\BusProNet\Model\Pickup;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -10,7 +10,7 @@ class PickupDataLoaderTest extends TestCase
{ {
public function testParse(): void public function testParse(): void
{ {
$loader = new PickupDataLoader(); $loader = new PickupXmlLoader();
$pickupId = 1; $pickupId = 1;
$xml = $loader->loadById($pickupId, 'pickups_data.xml'); $xml = $loader->loadById($pickupId, 'pickups_data.xml');
@@ -2,7 +2,7 @@
namespace App\Tests\BusProNet\DataLoader; namespace App\Tests\BusProNet\DataLoader;
use App\BusProNet\DataLoader\TravelDataLoader; use App\BusProNet\XmlLoader\TravelXmlLoader;
use App\BusProNet\Model\Travel; use App\BusProNet\Model\Travel;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
@@ -10,7 +10,7 @@ class TravelDataLoaderTest extends TestCase
{ {
public function testParse(): void public function testParse(): void
{ {
$loader = new TravelDataLoader(''); $loader = new TravelXmlLoader('');
$filename = __DIR__.'/../../Resources/travel_data.xml'; $filename = __DIR__.'/../../Resources/travel_data.xml';
$travelId = 11478; $travelId = 11478;