feat: integrated OAuth2 server
This commit is contained in:
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\BusProNet;
|
||||
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\BusProNet\DataProcessor\BookingDataProcessor;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
@@ -12,6 +11,8 @@ use App\BusProNet\Model\BookingUpdate;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\BusProNet\Traits\ApiClientTrait;
|
||||
use App\BusProNet\XmlParser\ApiResponseParser;
|
||||
use App\Form\Model\BookingData;
|
||||
use App\Form\Model\RegistrationData;
|
||||
use League\Flysystem\FilesystemException;
|
||||
@@ -39,7 +40,7 @@ class ApiClient
|
||||
private readonly ApiResponseParser $responseParser,
|
||||
private readonly FilesystemOperator $xmlDump,
|
||||
private readonly LoggerInterface $logger,
|
||||
array $options
|
||||
array $options,
|
||||
) {
|
||||
$this->config = $this->resolveOptions($options);
|
||||
}
|
||||
@@ -107,7 +108,7 @@ class ApiClient
|
||||
string $email,
|
||||
string $password,
|
||||
PersonalData $personalData,
|
||||
bool $debug = false
|
||||
bool $debug = false,
|
||||
): Notification|PersonalData {
|
||||
$data = [
|
||||
'user' => $this->config['bpn_username'],
|
||||
@@ -333,7 +334,7 @@ class ApiClient
|
||||
private function dumpXmlToFile(string $type, string $requestId, string $body): void
|
||||
{
|
||||
try {
|
||||
$this->xmlDump->write($requestId . '_' . $type . '.xml', $body);
|
||||
$this->xmlDump->write($requestId.'_'.$type.'.xml', $body);
|
||||
} catch (FilesystemException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace App\BusProNet\DataProvider;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Country;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
@@ -12,7 +11,8 @@ use Symfony\Contracts\Cache\ItemInterface;
|
||||
class CountryDataProvider
|
||||
{
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly CacheInterface $cache)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
public function getAll(): array
|
||||
{
|
||||
@@ -39,4 +39,4 @@ class CountryDataProvider
|
||||
|
||||
return $this->getAll()[$token] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,4 @@ namespace App\BusProNet\Exception;
|
||||
|
||||
class ResponseParserException extends \Exception
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,10 @@ use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
|
||||
class CountryChoiceLoader implements ChoiceLoaderInterface
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $countries, private readonly string $property)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
public function loadChoiceList(callable $value = null): ChoiceListInterface
|
||||
public function loadChoiceList(?callable $value = null): ChoiceListInterface
|
||||
{
|
||||
$choices = [];
|
||||
/** @var Country[] $countries */
|
||||
@@ -25,16 +26,15 @@ class CountryChoiceLoader implements ChoiceLoaderInterface
|
||||
}
|
||||
|
||||
return new ArrayChoiceList($choices);
|
||||
|
||||
}
|
||||
|
||||
public function loadChoicesForValues(array $values, callable $value = null): array
|
||||
public function loadChoicesForValues(array $values, ?callable $value = null): array
|
||||
{
|
||||
return $values;
|
||||
}
|
||||
|
||||
public function loadValuesForChoices(array $choices, callable $value = null): array
|
||||
public function loadValuesForChoices(array $choices, ?callable $value = null): array
|
||||
{
|
||||
return $choices;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
class CountryType extends AbstractType
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $countries)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
@@ -35,4 +36,4 @@ class CountryType extends AbstractType
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,4 @@ class Availability implements \JsonSerializable
|
||||
'available' => $this->available,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ namespace App\BusProNet\Model;
|
||||
class BaseData
|
||||
{
|
||||
public function __construct(private readonly array $items)
|
||||
{}
|
||||
{
|
||||
}
|
||||
|
||||
public function getItems(): array
|
||||
{
|
||||
@@ -16,4 +17,4 @@ class BaseData
|
||||
{
|
||||
return $this->items[$key] ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,4 +8,4 @@ class Country
|
||||
public ?string $name = null;
|
||||
public ?string $token = null;
|
||||
public ?string $nationality = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,4 +12,4 @@ class CrmAttributes
|
||||
public bool $teamer = false;
|
||||
public ?string $hotelCode = null;
|
||||
public array $roles = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,4 +17,4 @@ class CrmSelection
|
||||
|
||||
#[Groups(['api:single'])]
|
||||
public bool $selected = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,8 @@ class CrmSelectionGroup
|
||||
{
|
||||
/**
|
||||
* Maps BusPro IDs of CRM selection groups representing
|
||||
* included services
|
||||
* included services.
|
||||
*
|
||||
* @var array|string[]
|
||||
*/
|
||||
public static array $includedServicesMapping = [
|
||||
|
||||
@@ -100,4 +100,4 @@ class Error
|
||||
985 => 'Zustieg mit ID #1# nicht fürs Internet buchbar',
|
||||
999 => 'Systemfehler: #1#',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ class File
|
||||
public ?string $filename = null;
|
||||
public ?string $content = null;
|
||||
public ?string $mimeType = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ class Guide
|
||||
{
|
||||
public ?string $name = null;
|
||||
public ?string $phone = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,4 +26,4 @@ class Hotel
|
||||
|
||||
#[Groups(['api:list', 'api:single'])]
|
||||
public ?string $country = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,4 +21,4 @@ class MutableData
|
||||
public ?string $category = null;
|
||||
public bool $mutable = false;
|
||||
public ?\DateTimeImmutable $mutableBefore = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,4 +64,36 @@ class PersonalData
|
||||
'bemerkung' => $this->remarks,
|
||||
];
|
||||
}
|
||||
|
||||
public function getClaims(): array
|
||||
{
|
||||
return [
|
||||
'email' => $this->communication->email,
|
||||
'profile' => [
|
||||
'first_name' => $this->firstName,
|
||||
'last_name' => $this->name,
|
||||
'title' => $this->title,
|
||||
'salutation' => $this->salutation,
|
||||
'gender' => $this->gender,
|
||||
'nationality' => $this->nationality,
|
||||
'address' => [
|
||||
'street' => $this->address->street,
|
||||
'postcode' => $this->address->postCode,
|
||||
'city' => $this->address->city,
|
||||
'disctrict' => $this->address->district,
|
||||
'country' => $this->address->country,
|
||||
],
|
||||
'communication' => [
|
||||
'email' => $this->communication->email,
|
||||
'mobile' => $this->communication->mobile,
|
||||
'phone' => $this->communication->phone,
|
||||
'newsletter' => $this->communication->newsletter,
|
||||
],
|
||||
'height' => $this->height,
|
||||
'weight' => $this->weight,
|
||||
'shoe_size' => $this->shoeSize,
|
||||
'remarks' => $this->remarks,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,4 @@ class Pickup
|
||||
|
||||
#[Groups(['api:booking'])]
|
||||
public array $mapping = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,4 +63,4 @@ class Room
|
||||
|
||||
#[Groups(['booking'])]
|
||||
public array $individualPrice = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class Service
|
||||
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;
|
||||
|
||||
@@ -8,4 +8,4 @@ class Surcharge
|
||||
public array $mapping = [];
|
||||
public ?float $totalPrice = null;
|
||||
public array $individualPrice = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet;
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
|
||||
@@ -56,7 +56,7 @@ trait ApiClientTrait
|
||||
private function send($socket, string $data): void
|
||||
{
|
||||
// message length is prepended to actual message
|
||||
$send = sprintf('%010s', strlen($data)) . $data;
|
||||
$send = sprintf('%010s', strlen($data)).$data;
|
||||
fwrite($socket, $send);
|
||||
}
|
||||
|
||||
@@ -75,4 +75,4 @@ trait ApiClientTrait
|
||||
{
|
||||
@fclose($socket);
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet;
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Carbon\Exceptions\InvalidFormatException;
|
||||
@@ -52,6 +52,7 @@ trait TypeConversionTrait
|
||||
|
||||
try {
|
||||
$date = Carbon::createFromFormat('d.m.Y', $string)->startOfDay();
|
||||
|
||||
return $date->toDateTimeImmutable();
|
||||
} catch (InvalidFormatException $e) {
|
||||
return null;
|
||||
@@ -66,6 +67,7 @@ trait TypeConversionTrait
|
||||
|
||||
try {
|
||||
$dateTime = Carbon::createFromFormat('d.m.Y H:i', substr($string, 0, 16))->startOfDay();
|
||||
|
||||
return $dateTime->toDateTimeImmutable();
|
||||
} catch (InvalidFormatException $e) {
|
||||
return null;
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace App\BusProNet;
|
||||
namespace App\BusProNet\Traits;
|
||||
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
@@ -15,4 +15,4 @@ trait XmlParserTrait
|
||||
{
|
||||
return 0 < $node->count() ? (int) $node->text() : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\BusProNet\XmlLoader;
|
||||
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
use App\BusProNet\XmlParserTrait;
|
||||
use App\BusProNet\Traits\TypeConversionTrait;
|
||||
use App\BusProNet\Traits\XmlParserTrait;
|
||||
use League\Flysystem\FilesystemOperator;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class TravelLoader extends AbstractLoader
|
||||
private readonly HotelLoader $hotelDataLoader,
|
||||
CacheInterface $cache,
|
||||
private readonly string $travelInfoBaseUrl,
|
||||
FileSystemOperator $xmlExport,
|
||||
FilesystemOperator $xmlExport,
|
||||
) {
|
||||
parent::__construct($cache, $xmlExport);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\BusProNet\XmlParser;
|
||||
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
use App\BusProNet\Traits\TypeConversionTrait;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
abstract class AbstractParser
|
||||
@@ -47,4 +47,4 @@ abstract class AbstractParser
|
||||
|
||||
return $this->stringToArray($node->text(), $separator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +61,4 @@ class ApiResponseParser extends AbstractParser
|
||||
|
||||
throw new ResponseParserException('Unable to parse XML response');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ class AvailabilitiesParser extends AbstractParser
|
||||
|
||||
return new BaseData($availabilities);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,4 +38,4 @@ class BookingsParser extends AbstractParser
|
||||
|
||||
return new BaseData($bookings);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,4 +27,4 @@ class CountriesParser
|
||||
|
||||
return new BaseData($countries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use App\BusProNet\Model\CrmAction;
|
||||
use App\BusProNet\Model\CrmAttributes;
|
||||
use App\BusProNet\Model\CrmSelection;
|
||||
use App\BusProNet\Model\CrmSelectionGroup;
|
||||
use App\BusProNet\TypeConversionTrait;
|
||||
use App\BusProNet\Traits\TypeConversionTrait;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
class CrmAttributesResponseParser
|
||||
|
||||
@@ -37,8 +37,7 @@ class DocumentsParser
|
||||
});
|
||||
|
||||
return new File('reisedokumente.zip', $zip->outputAsString(), 'application/zip');
|
||||
}
|
||||
catch (ZipException $e) {
|
||||
} catch (ZipException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -57,4 +56,4 @@ class DocumentsParser
|
||||
|
||||
return [$filename, $pdfData];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ class MutableDataParser extends AbstractParser
|
||||
|
||||
return new BaseData($mutableFields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,4 +14,4 @@ class NotificationParser extends AbstractParser
|
||||
|
||||
return new Notification($number, $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,8 @@ class PersonalDataParser extends AbstractParser
|
||||
$address = new Address();
|
||||
$address->street = $this->getStringOrNullValue($addressNode->filterXPath('//strasse'));
|
||||
$address->postCode = $this->getStringOrNullValue($addressNode->filterXPath('//plz'));
|
||||
$address->city = $this->getStringOrNullValue($addressNode->filterXPath('//ort')) ;
|
||||
$address->country = $this->getStringOrNullValue($addressNode->filterXPath('//land')) ;
|
||||
$address->city = $this->getStringOrNullValue($addressNode->filterXPath('//ort'));
|
||||
$address->country = $this->getStringOrNullValue($addressNode->filterXPath('//land'));
|
||||
|
||||
$personalData->address = $address;
|
||||
}
|
||||
@@ -63,4 +63,4 @@ class PersonalDataParser extends AbstractParser
|
||||
|
||||
return $personalData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,4 +33,4 @@ class PickupsParser extends AbstractParser
|
||||
|
||||
return $pickups;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ class RoomsParser extends AbstractParser
|
||||
|
||||
$result->each(function (Crawler $node) use (&$rooms) {
|
||||
$room = new Room();
|
||||
$room->id = (int) $node->attr('idzimmer');;
|
||||
$room->id = (int) $node->attr('idzimmer');
|
||||
$room->label = $node->attr('zimmer');
|
||||
$room->category = $node->attr('kategorie');
|
||||
$room->boardId = (int) $node->attr('idverpflegung');
|
||||
@@ -38,4 +38,4 @@ class RoomsParser extends AbstractParser
|
||||
|
||||
return $rooms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,4 +39,4 @@ class ServicesParser extends AbstractParser
|
||||
|
||||
return $services;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,4 +30,4 @@ class SurchargesParser extends AbstractParser
|
||||
|
||||
return $surcharges;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[AsCommand(
|
||||
name: 'app:cleanup:xml-dumps',
|
||||
description: 'Removes XML dumps of BPN requests/responses older than a week')
|
||||
]
|
||||
description: 'Removes XML dumps of BPN requests/responses older than a week'
|
||||
)]
|
||||
class CleanupXMLDumpsCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
@@ -33,11 +33,11 @@ class CleanupXMLDumpsCommand extends Command
|
||||
$files = $this
|
||||
->xmlDump
|
||||
->listContents('.')
|
||||
->filter(fn(StorageAttributes $attributes) => $attributes->isFile() && $attributes->lastModified() < $maxDate)
|
||||
->map(fn(StorageAttributes $attributes) => $attributes->path())
|
||||
->filter(fn (StorageAttributes $attributes) => $attributes->isFile() && $attributes->lastModified() < $maxDate)
|
||||
->map(fn (StorageAttributes $attributes) => $attributes->path())
|
||||
->toArray();
|
||||
} catch (FilesystemException $e) {
|
||||
$output->writeln('<error>' . $e->getMessage() . '</error>');
|
||||
$output->writeln('<error>'.$e->getMessage().'</error>');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ use App\BusProNet\DataProvider\CountryDataProvider;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class CountryController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly CountryDataProvider $dataProvider)
|
||||
@@ -21,4 +23,4 @@ class CountryController extends AbstractController
|
||||
|
||||
return $this->json($countries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-11
@@ -5,28 +5,29 @@ namespace App\Controller\Api;
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\Entity\User;
|
||||
use App\Security\Crypt;
|
||||
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\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
class CrmAttributesController extends AbstractController
|
||||
#[IsGranted('ROLE_OAUTH2_PROFILE')]
|
||||
class CrmAttributeController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly APiClient $apiClient)
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly Crypt $crypt)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/crm-attributes', name: 'api_crm_attributes', methods: ['GET'])]
|
||||
public function index(Request $request): JsonResponse
|
||||
public function index(): 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);
|
||||
}
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$email = $user->getEmail();
|
||||
$password = $this->crypt->decrypt($user->getPassword());
|
||||
|
||||
try {
|
||||
$data = $this->apiClient->getCrmAttributes($email, $password);
|
||||
@@ -40,4 +41,4 @@ class CrmAttributesController extends AbstractController
|
||||
return new JsonResponse(['message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class HotelController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly HotelLoader $xmlLoader)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/hotels', name: 'api_hotel_all')]
|
||||
#[Route('/hotels', name: 'api_hotels_all')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$hotels = $this->xmlLoader->loadAll();
|
||||
@@ -34,4 +36,4 @@ class HotelController extends AbstractController
|
||||
|
||||
return $this->json($hotel, Response::HTTP_OK, [], ['groups' => 'api:single']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class PickupController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly PickupLoader $xmlLoader)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/pickups', name: 'api_pickup_all')]
|
||||
#[Route('/pickups', name: 'api_pickups_all')]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$pickups = $this->xmlLoader->loadAll();
|
||||
@@ -23,11 +25,11 @@ class PickupController extends AbstractController
|
||||
return $this->json($pickups, Response::HTTP_OK, [], ['groups' => 'api:list']);
|
||||
}
|
||||
|
||||
#[Route('/pickups/{id}', name: 'api_pickup_single')]
|
||||
#[Route('/pickups/{id}', name: 'api_pickups_single')]
|
||||
public function single(int $id): JsonResponse
|
||||
{
|
||||
$pickup = $this->xmlLoader->loadById($id);
|
||||
|
||||
return $this->json($pickup, Response::HTTP_OK, [], ['groups' => 'api:list']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?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']);
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,18 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
use Symfony\Contracts\Cache\ItemInterface;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_API')]
|
||||
class TravelController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly TravelLoader $travelXmlLoader,
|
||||
private readonly HotelLoader $hotelXmlLoader,
|
||||
private readonly PickupLoader $pickupXmlLoader,
|
||||
private readonly TravelLoader $travelXmlLoader,
|
||||
private readonly HotelLoader $hotelXmlLoader,
|
||||
private readonly PickupLoader $pickupXmlLoader,
|
||||
private readonly CacheInterface $cache,
|
||||
) {
|
||||
}
|
||||
@@ -90,4 +92,4 @@ class TravelController extends AbstractController
|
||||
|
||||
return $travel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Api;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Model\Notification;
|
||||
use App\BusProNet\Model\PersonalData;
|
||||
use App\Entity\User;
|
||||
use App\Security\Crypt;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
#[Route('/api')]
|
||||
#[IsGranted('ROLE_OAUTH2_EMAIL')]
|
||||
class UserinfoController extends AbstractController
|
||||
{
|
||||
public function __construct(private readonly ApiClient $apiClient, private readonly Crypt $crypt)
|
||||
{
|
||||
}
|
||||
|
||||
#[Route('/userinfo', name: 'api_userinfo', methods: ['GET'])]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
// basic scopes applicable to all authenticated users
|
||||
$scopes = ['email'];
|
||||
|
||||
// extend scopes depending on granted permissions
|
||||
if ($this->isGranted('ROLE_OAUTH2_PROFILE')) {
|
||||
$scopes[] = 'profile';
|
||||
}
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$email = $user->getEmail();
|
||||
$password = $this->crypt->decrypt($user->getPassword());
|
||||
|
||||
try {
|
||||
$data = $this->apiClient->getPersonalData($email, $password);
|
||||
|
||||
if ($data instanceof Notification) {
|
||||
return new JsonResponse(['message' => $data->message, 'code' => $data->code], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
|
||||
// extract userdata for resulting claims
|
||||
$userData = $this->getClaims($data, $scopes);
|
||||
|
||||
return $this->json($userData);
|
||||
} catch (ApiClientException $e) {
|
||||
return new JsonResponse(['message' => $e->getMessage()], Response::HTTP_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
||||
private function getClaims(PersonalData $data, array $scopes): array
|
||||
{
|
||||
// get all available claims
|
||||
$allClaims = $data->getClaims();
|
||||
$keys = array_keys($allClaims);
|
||||
$claims = [];
|
||||
|
||||
// filter claims by provided scopes
|
||||
foreach ($scopes as $scope) {
|
||||
if (false === in_array($scope, $keys)) {
|
||||
continue;
|
||||
}
|
||||
$claims[$scope] = $allClaims[$scope];
|
||||
}
|
||||
|
||||
return $claims;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
use Symfony\Contracts\Cache\CacheInterface;
|
||||
|
||||
use function Symfony\Component\String\u;
|
||||
|
||||
class DownloadController extends AbstractController
|
||||
@@ -42,7 +43,7 @@ class DownloadController extends AbstractController
|
||||
requirements: ['id' => '\d+'],
|
||||
defaults: ['fileType' => 'invoice']
|
||||
)]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function documents(int $id, string $fileType): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
|
||||
@@ -38,7 +38,7 @@ class EditController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/bookings/{id}/edit', name: 'app_booking_edit', requirements: ['id' => '\d+'])]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function edit(int $id, Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
|
||||
@@ -27,7 +27,7 @@ class IndexController extends AbstractController
|
||||
}
|
||||
|
||||
#[Route('/bookings', name: 'app_bookings')]
|
||||
#[IsGranted("ROLE_USER")]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
/** @var User $user */
|
||||
|
||||
@@ -26,7 +26,7 @@ class RegistrationController extends AbstractController
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$registrationData = new RegistrationData();
|
||||
$form = $this->createForm(RegistrationType::class, $registrationData,[
|
||||
$form = $this->createForm(RegistrationType::class, $registrationData, [
|
||||
'action' => $this->generateUrl('app_registration'),
|
||||
]);
|
||||
$form->handleRequest($request);
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace App\Controller;
|
||||
|
||||
use App\BusProNet\ApiClient;
|
||||
use App\BusProNet\Exception\ApiClientException;
|
||||
use App\BusProNet\Exception\ResponseParserException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
@@ -38,7 +37,7 @@ class ResetPasswordController extends AbstractController
|
||||
new Email([
|
||||
'mode' => 'strict',
|
||||
'message' => 'Bitte eine gültige E-Mail Adresse angeben',
|
||||
])
|
||||
]),
|
||||
],
|
||||
])
|
||||
->getForm()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
@@ -12,7 +13,7 @@ class SecurityController extends AbstractController
|
||||
{
|
||||
#[Route('/', name: 'app_login')]
|
||||
#[IsGranted('PUBLIC_ACCESS')]
|
||||
public function login(AuthenticationUtils $authenticationUtils): Response
|
||||
public function login(AuthenticationUtils $authenticationUtils, Request $request): Response
|
||||
{
|
||||
if (null !== $this->getUser()) {
|
||||
return $this->redirectToRoute('app_personal_data');
|
||||
@@ -21,6 +22,15 @@ class SecurityController extends AbstractController
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
$lastUsername = $authenticationUtils->getLastUsername();
|
||||
|
||||
// flag this request in the session as external OAuth2 request if
|
||||
// applicable to immediately logout the current user after successful
|
||||
// authentication (see App\EventListener\AuthorizationCodeListener).
|
||||
$session = $request->getSession();
|
||||
$targetPath = $session->get('_security.main.target_path');
|
||||
if (str_contains($targetPath, '/authorize')) {
|
||||
$session->set('_oauth2', true);
|
||||
}
|
||||
|
||||
return $this->render('security/login.html.twig', [
|
||||
'last_username' => $lastUsername,
|
||||
'error' => $error,
|
||||
@@ -30,5 +40,6 @@ class SecurityController extends AbstractController
|
||||
#[Route('/logout', name: 'app_logout')]
|
||||
#[IsGranted('ROLE_USER')]
|
||||
public function logout(): void
|
||||
{}
|
||||
}
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Entity\User;
|
||||
use League\Bundle\OAuth2ServerBundle\Event\AuthorizationRequestResolveEvent;
|
||||
use League\Bundle\OAuth2ServerBundle\OAuth2Events;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[AsEventListener(event: OAuth2Events::AUTHORIZATION_REQUEST_RESOLVE, method: 'onAuthorizationRequestResolve')]
|
||||
class AuthorizationCodeListener
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly RequestStack $requestStack,
|
||||
private readonly Security $security,
|
||||
private readonly LoggerInterface $authenticationLogger,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Approves or denies authorization by checking for a current user session and redirects anonymous
|
||||
* users to login page. We purposely don't check consents since these are irrelevant for our use case.
|
||||
* We do check though if the current user is allowed to login to the requesting client.
|
||||
*/
|
||||
public function onAuthorizationRequestResolve(AuthorizationRequestResolveEvent $event): void
|
||||
{
|
||||
$request = $this->requestStack->getMainRequest();
|
||||
|
||||
/** @var User $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (null === $user) {
|
||||
$response = new RedirectResponse(
|
||||
$this->urlGenerator->generate('app_login', $request->query->all()),
|
||||
Response::HTTP_TEMPORARY_REDIRECT
|
||||
);
|
||||
$event->setResponse($response);
|
||||
$this->authenticationLogger->info('Authorization request without session');
|
||||
} else {
|
||||
$event->resolveAuthorization(AuthorizationRequestResolveEvent::AUTHORIZATION_APPROVED);
|
||||
// in case this authorization request has been flagged as external in login controller,
|
||||
// immediately logout the current user (see App\Controller\Core\Security\LoginController).
|
||||
if (true === $request->getSession()->get('_oauth2', false)) {
|
||||
$this->security->logout(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,4 +19,4 @@ class RegistrationData
|
||||
#[Assert\NotBlank(message: 'Bitte angeben')]
|
||||
#[Assert\Email(message: 'Bitte eine gültige E-Mail Adresse angeben', mode: 'strict')]
|
||||
public ?string $email = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ class ParticipantType extends AbstractType
|
||||
'label' => 'Vorname',
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('lastName', TextType::class, [
|
||||
'label' => 'Nachname',
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('dateOfBirth', BirthdayType::class, [
|
||||
'label' => 'Geburtsdatum',
|
||||
@@ -54,7 +54,7 @@ class ParticipantType extends AbstractType
|
||||
'input' => 'datetime_immutable',
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('gender', ChoiceType::class, [
|
||||
'label' => 'Geschlecht',
|
||||
@@ -68,7 +68,7 @@ class ParticipantType extends AbstractType
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('nationality', CountryType::class, [
|
||||
'label' => 'Nationalität',
|
||||
@@ -77,21 +77,21 @@ class ParticipantType extends AbstractType
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
'style' => false === $personalDataMutable ? 'pointer-events: none' : null,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'E-Mail',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('mobile', TextType::class, [
|
||||
'label' => 'Telefon (mobil)',
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'readonly' => false === $personalDataMutable,
|
||||
]
|
||||
],
|
||||
])
|
||||
->add('height', ChoiceType::class, [
|
||||
'label' => 'Körpergröße',
|
||||
@@ -144,7 +144,8 @@ class ParticipantType extends AbstractType
|
||||
return $service->label;
|
||||
}
|
||||
|
||||
return sprintf('%s (%s €)',
|
||||
return sprintf(
|
||||
'%s (%s €)',
|
||||
$service->label,
|
||||
number_format($price, 2, ',', '.')
|
||||
);
|
||||
@@ -189,8 +190,8 @@ class ParticipantType extends AbstractType
|
||||
// remove choices only available in booking data and
|
||||
// which are not mapped to the current participant
|
||||
if (
|
||||
Service::SOURCE_TRAVEL === $service->source ||
|
||||
Service::CATEGORY_ADDITIONAL !== $service->category
|
||||
Service::SOURCE_TRAVEL === $service->source
|
||||
|| Service::CATEGORY_ADDITIONAL !== $service->category
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
@@ -293,7 +294,8 @@ class ParticipantType extends AbstractType
|
||||
return $pickupLabel;
|
||||
}
|
||||
|
||||
return sprintf('%s %s€',
|
||||
return sprintf(
|
||||
'%s %s€',
|
||||
$pickupLabel,
|
||||
number_format($price, 2, ',', '.')
|
||||
);
|
||||
@@ -308,7 +310,7 @@ class ParticipantType extends AbstractType
|
||||
},
|
||||
'attr' => [
|
||||
'readonly' => false === $options['pickups_mutable'],
|
||||
]
|
||||
],
|
||||
]);
|
||||
})
|
||||
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($options) {
|
||||
@@ -331,8 +333,9 @@ class ParticipantType extends AbstractType
|
||||
// forcibly select mandatory services that potentially have been disabled in PRE_SET_DATA
|
||||
$mandatoryServices = array_filter($options['selectable_services'], function (Service $service) use ($form) {
|
||||
$participantIndex = $form->getData()->index;
|
||||
return true === $service->mandatory ||
|
||||
(Service::SOURCE_BOOKING === $service->source && in_array($participantIndex, $service->mapping));
|
||||
|
||||
return true === $service->mandatory
|
||||
|| (Service::SOURCE_BOOKING === $service->source && in_array($participantIndex, $service->mapping));
|
||||
});
|
||||
|
||||
if (0 < count($mandatoryServices)) {
|
||||
|
||||
@@ -4,8 +4,6 @@ namespace App\Form;
|
||||
|
||||
use App\BusProNet\Form\CountryType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\BirthdayType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
@@ -43,4 +43,4 @@ class RegistrationType extends AbstractType
|
||||
'anti_xss' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,4 @@ class MenuBuilder
|
||||
|
||||
return $menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,12 @@ 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;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
use Symfony\Component\Security\Http\Util\TargetPathTrait;
|
||||
|
||||
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
|
||||
{
|
||||
use TargetPathTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly ApiClient $apiClient,
|
||||
@@ -60,7 +63,7 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
$csrfToken = $request->request->getString('_csrf_token');
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($email, function () use ($email, $password, $response, $request) {
|
||||
new UserBadge($email, function () use ($email, $password, $response) {
|
||||
return $this->createOrUpdateLocalUser($email, $password, $response->personId, $response->addressId);
|
||||
}),
|
||||
[
|
||||
@@ -106,6 +109,10 @@ class BpnAuthenticator extends AbstractLoginFormAuthenticator implements Authent
|
||||
'email' => $token->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
|
||||
return new RedirectResponse($targetPath);
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('app_personal_data'));
|
||||
}
|
||||
|
||||
|
||||
@@ -13,28 +13,28 @@ class Crypt
|
||||
|
||||
public function encrypt(string $message): string
|
||||
{
|
||||
$privateKey = PrivateKey::fromFile($this->path . '/private.key');
|
||||
$privateKey = PrivateKey::fromFile($this->path.'/private.key');
|
||||
|
||||
return $privateKey->encrypt($message);
|
||||
}
|
||||
|
||||
public function decrypt(string $message): string
|
||||
{
|
||||
$publicKey = PublicKey::fromFile($this->path . '/public.key');
|
||||
$publicKey = PublicKey::fromFile($this->path.'/public.key');
|
||||
|
||||
return $publicKey->decrypt($message);
|
||||
}
|
||||
|
||||
public function sign(string $message): string
|
||||
{
|
||||
$privateKey = PrivateKey::fromFile($this->path . '/private.key');
|
||||
$privateKey = PrivateKey::fromFile($this->path.'/private.key');
|
||||
|
||||
return $privateKey->sign($message);
|
||||
}
|
||||
|
||||
public function verify(string $message, string $signature): bool
|
||||
{
|
||||
$publicKey = PublicKey::fromFile($this->path . '/public.key');
|
||||
$publicKey = PublicKey::fromFile($this->path.'/public.key');
|
||||
|
||||
return $publicKey->verify($message, $signature);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ class AppRuntime implements RuntimeExtensionInterface
|
||||
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
$factor = floor((strlen($bytes) - 1) / 3);
|
||||
|
||||
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)) . @$size[$factor];
|
||||
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)).@$size[$factor];
|
||||
}
|
||||
|
||||
public function formatMoney(int $amount): string
|
||||
|
||||
Reference in New Issue
Block a user