feat: integrated OAuth2 server
This commit is contained in:
@@ -56,3 +56,9 @@ SFTP_XML_EXPORT_HOST=
|
||||
SFTP_XML_EXPORT_PORT=
|
||||
SFTP_XML_EXPORT_USER=
|
||||
SFTP_XML_EXPORT_PASSWORD=
|
||||
|
||||
###> league/oauth2-server-bundle ###
|
||||
OAUTH_PRIVATE_KEY=%kernel.project_dir%/config/secret/private.key
|
||||
OAUTH_PUBLIC_KEY=%kernel.project_dir%/config/secret/public.key
|
||||
OAUTH_ENCRYPTION_KEY=580084fd179e67399467f59ee96658ac
|
||||
###< league/oauth2-server-bundle ###
|
||||
|
||||
@@ -26,3 +26,8 @@ yarn-error.log
|
||||
###< symfony/webpack-encore-bundle ###
|
||||
|
||||
/http-client.private.env.json
|
||||
|
||||
###> friendsofphp/php-cs-fixer ###
|
||||
/.php-cs-fixer.php
|
||||
/.php-cs-fixer.cache
|
||||
###< friendsofphp/php-cs-fixer ###
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
$finder = (new PhpCsFixer\Finder())
|
||||
->in(__DIR__)
|
||||
->exclude('var')
|
||||
;
|
||||
|
||||
return (new PhpCsFixer\Config())
|
||||
->setRules([
|
||||
'@Symfony' => true,
|
||||
])
|
||||
->setFinder($finder)
|
||||
;
|
||||
@@ -0,0 +1,40 @@
|
||||
### user profile minimum
|
||||
GET https://myep-next.ddev.site/api/userinfo
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_email")}}
|
||||
|
||||
### user profile full
|
||||
GET https://myep-next.ddev.site/api/userinfo
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_profile")}}
|
||||
|
||||
### user crm attributes
|
||||
GET https://myep-next.ddev.site/api/crm-attributes
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_profile")}}
|
||||
|
||||
### API countries
|
||||
GET https://myep-next.ddev.site/api/countries
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API hotels
|
||||
GET https://myep-next.ddev.site/api/hotels
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API hotel
|
||||
GET https://myep-next.ddev.site/api/hotels/198924
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API pickups
|
||||
GET https://myep-next.ddev.site/api/pickups
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
### API pickup
|
||||
GET https://myep-next.ddev.site/api/pickups/1
|
||||
Accept: application/json
|
||||
Authorization: Bearer {{$auth.token("oauth2_api")}}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"knplabs/knp-menu-bundle": "^3.4",
|
||||
"league/flysystem-bundle": "^3.4",
|
||||
"league/flysystem-sftp-v3": "^3.29",
|
||||
"league/oauth2-server-bundle": "^0.11.0",
|
||||
"nelexa/zip": "^4.0",
|
||||
"nesbot/carbon": "^3.8",
|
||||
"phpdocumentor/reflection-docblock": "^5.6",
|
||||
@@ -109,6 +110,7 @@
|
||||
},
|
||||
"require-dev": {
|
||||
"deployer/deployer": "^7.5",
|
||||
"friendsofphp/php-cs-fixer": "^3.75",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"symfony/browser-kit": "6.4.*",
|
||||
"symfony/css-selector": "6.4.*",
|
||||
|
||||
Generated
+1967
-1
File diff suppressed because it is too large
Load Diff
@@ -15,4 +15,5 @@ return [
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
Knp\Bundle\MenuBundle\KnpMenuBundle::class => ['all' => true],
|
||||
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||
League\Bundle\OAuth2ServerBundle\LeagueOAuth2ServerBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
league_oauth2_server:
|
||||
authorization_server:
|
||||
private_key: '%env(resolve:OAUTH_PRIVATE_KEY)%'
|
||||
private_key_passphrase: null
|
||||
encryption_key: '%env(resolve:OAUTH_ENCRYPTION_KEY)%'
|
||||
enable_client_credentials_grant: true
|
||||
enable_auth_code_grant: true
|
||||
enable_refresh_token_grant: false
|
||||
enable_password_grant: false
|
||||
access_token_ttl: PT10M
|
||||
resource_server:
|
||||
public_key: '%env(resolve:OAUTH_PUBLIC_KEY)%'
|
||||
scopes:
|
||||
available: ['email','profile','api']
|
||||
default: ['email']
|
||||
persistence:
|
||||
doctrine: null
|
||||
|
||||
when@test:
|
||||
league_oauth2_server:
|
||||
persistence:
|
||||
in_memory: null
|
||||
@@ -0,0 +1,11 @@
|
||||
services:
|
||||
# Register nyholm/psr7 services for autowiring with PSR-17 (HTTP factories)
|
||||
Psr\Http\Message\RequestFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
Psr\Http\Message\ResponseFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
Psr\Http\Message\ServerRequestFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
Psr\Http\Message\StreamFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
Psr\Http\Message\UploadedFileFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
Psr\Http\Message\UriFactoryInterface: '@nyholm.psr7.psr17_factory'
|
||||
|
||||
nyholm.psr7.psr17_factory:
|
||||
class: Nyholm\Psr7\Factory\Psr17Factory
|
||||
@@ -12,12 +12,14 @@ security:
|
||||
dev:
|
||||
pattern: ^/(_(profiler|wdt)|css|images|js)/
|
||||
security: false
|
||||
api_token:
|
||||
pattern: ^/token$
|
||||
security: false
|
||||
api:
|
||||
lazy: true
|
||||
stateless: true
|
||||
pattern: ^/api
|
||||
custom_authenticator:
|
||||
- App\Security\ApiKeyAuthenticator
|
||||
security: true
|
||||
stateless: true
|
||||
oauth2: true
|
||||
main:
|
||||
lazy: true
|
||||
provider: app_user_provider
|
||||
@@ -30,7 +32,7 @@ security:
|
||||
# Easy way to control access for large sections of your site
|
||||
# Note: Only the *first* access control that matches will be used
|
||||
access_control:
|
||||
- { path: ^/api, roles: ROLE_API }
|
||||
- { path: ^/authorize, roles: IS_AUTHENTICATED_REMEMBERED }
|
||||
|
||||
when@test:
|
||||
security:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
league_oauth2_server:
|
||||
resource: '@LeagueOAuth2ServerBundle/config/routes.php'
|
||||
type: php
|
||||
@@ -27,10 +27,6 @@ services:
|
||||
- '../src/Entity/'
|
||||
- '../src/Kernel.php'
|
||||
|
||||
App\Security\ApiKeyAuthenticator:
|
||||
arguments:
|
||||
$apiKeys: '%env(csv:API_KEYS)%'
|
||||
|
||||
App\BusProNet\ApiClient:
|
||||
arguments:
|
||||
$logger: '@monolog.logger.bpn'
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"dev": {
|
||||
"Security": {
|
||||
"Auth": {
|
||||
"oauth2_email": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://myep-next.ddev.site/authorize",
|
||||
"Token URL": "https://myep-next.ddev.site/token",
|
||||
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
||||
"Scope": "email"
|
||||
},
|
||||
"oauth2_profile": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Authorization Code",
|
||||
"Client ID": "{{oauth2_client_id}}",
|
||||
"Client Secret": "{{oauth2_client_secret}}",
|
||||
"Auth URL": "https://myep-next.ddev.site/authorize",
|
||||
"Token URL": "https://myep-next.ddev.site/token",
|
||||
"Redirect URL": "https://myep-team.ddev.site/auth/check",
|
||||
"Scope": "email profile"
|
||||
},
|
||||
"oauth2_api": {
|
||||
"Type": "OAuth2",
|
||||
"Grant Type": "Client Credentials",
|
||||
"Client ID": "{{oauth2_api_client_id}}",
|
||||
"Client Secret": "{{oauth2_api_client_secret}}",
|
||||
"Auth URL": "https://myep-next.ddev.site/authorize",
|
||||
"Token URL": "https://myep-next.ddev.site/token",
|
||||
"Scope": "api"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250424152547 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE oauth2_access_token (identifier CHAR(80) NOT NULL, client VARCHAR(32) NOT NULL, expiry DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', user_identifier VARCHAR(128) DEFAULT NULL, scopes TEXT DEFAULT NULL COMMENT '(DC2Type:oauth2_scope)', revoked TINYINT(1) NOT NULL, INDEX IDX_454D9673C7440455 (client), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE oauth2_authorization_code (identifier CHAR(80) NOT NULL, client VARCHAR(32) NOT NULL, expiry DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', user_identifier VARCHAR(128) DEFAULT NULL, scopes TEXT DEFAULT NULL COMMENT '(DC2Type:oauth2_scope)', revoked TINYINT(1) NOT NULL, INDEX IDX_509FEF5FC7440455 (client), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE oauth2_client (identifier VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, secret VARCHAR(128) DEFAULT NULL, redirect_uris TEXT DEFAULT NULL COMMENT '(DC2Type:oauth2_redirect_uri)', grants TEXT DEFAULT NULL COMMENT '(DC2Type:oauth2_grant)', scopes TEXT DEFAULT NULL COMMENT '(DC2Type:oauth2_scope)', active TINYINT(1) NOT NULL, allow_plain_text_pkce TINYINT(1) DEFAULT 0 NOT NULL, PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE oauth2_refresh_token (identifier CHAR(80) NOT NULL, access_token CHAR(80) DEFAULT NULL, expiry DATETIME NOT NULL COMMENT '(DC2Type:datetime_immutable)', revoked TINYINT(1) NOT NULL, INDEX IDX_4DD90732B6A2DD68 (access_token), PRIMARY KEY(identifier)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_access_token ADD CONSTRAINT FK_454D9673C7440455 FOREIGN KEY (client) REFERENCES oauth2_client (identifier) ON DELETE CASCADE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_authorization_code ADD CONSTRAINT FK_509FEF5FC7440455 FOREIGN KEY (client) REFERENCES oauth2_client (identifier) ON DELETE CASCADE
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_refresh_token ADD CONSTRAINT FK_4DD90732B6A2DD68 FOREIGN KEY (access_token) REFERENCES oauth2_access_token (identifier) ON DELETE SET NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_access_token DROP FOREIGN KEY FK_454D9673C7440455
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_authorization_code DROP FOREIGN KEY FK_509FEF5FC7440455
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE oauth2_refresh_token DROP FOREIGN KEY FK_4DD90732B6A2DD68
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE oauth2_access_token
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE oauth2_authorization_code
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE oauth2_client
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP TABLE oauth2_refresh_token
|
||||
SQL);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,18 @@
|
||||
"migrations/.gitignore"
|
||||
]
|
||||
},
|
||||
"friendsofphp/php-cs-fixer": {
|
||||
"version": "3.75",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "be2103eb4a20942e28a6dd87736669b757132435"
|
||||
},
|
||||
"files": [
|
||||
".php-cs-fixer.dist.php"
|
||||
]
|
||||
},
|
||||
"knplabs/knp-menu-bundle": {
|
||||
"version": "v3.4.2"
|
||||
},
|
||||
@@ -42,6 +54,31 @@
|
||||
"var/storage/.gitignore"
|
||||
]
|
||||
},
|
||||
"league/oauth2-server-bundle": {
|
||||
"version": "0.11",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "0.11",
|
||||
"ref": "80320e8e61b51f6965b83a7df1cc9d40bcc3fb78"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/league_oauth2_server.yaml",
|
||||
"config/routes/league_oauth2_server.yaml"
|
||||
]
|
||||
},
|
||||
"nyholm/psr7": {
|
||||
"version": "1.8",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "main",
|
||||
"version": "1.0",
|
||||
"ref": "4a8c0345442dcca1d8a2c65633dcf0285dd5a5a2"
|
||||
},
|
||||
"files": [
|
||||
"config/packages/nyholm_psr7.yaml"
|
||||
]
|
||||
},
|
||||
"phpunit/phpunit": {
|
||||
"version": "9.6",
|
||||
"recipe": {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use App\BusProNet\Model\Hotel;
|
||||
use App\BusProNet\XmlLoader\HotelLoader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class HotelDataLoaderTest extends TestCase
|
||||
@@ -27,4 +27,4 @@ class HotelDataLoaderTest extends TestCase
|
||||
$this->assertEquals('Rue des Lacs', $hotel->street);
|
||||
$this->assertEquals('', $hotel->phone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use App\BusProNet\Model\Pickup;
|
||||
use App\BusProNet\XmlLoader\PickupLoader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class PickupDataLoaderTest extends TestCase
|
||||
@@ -25,4 +25,4 @@ class PickupDataLoaderTest extends TestCase
|
||||
$this->assertEquals('48143', $pickup->postalCode);
|
||||
$this->assertEquals('Hafenstr/Ecke Friedrich-Ebert-Str', $pickup->street);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Tests\BusProNet\DataLoader;
|
||||
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use App\BusProNet\Model\Travel;
|
||||
use App\BusProNet\XmlLoader\TravelLoader;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TravelDataLoaderTest extends TestCase
|
||||
|
||||
Reference in New Issue
Block a user