diff --git a/.env.ci b/.env.ci index 7aa89ff9a..220adebbe 100644 --- a/.env.ci +++ b/.env.ci @@ -1,6 +1,6 @@ APP_NAME=publiq-platform APP_ENV=local -APP_KEY= +APP_KEY=base64:5f6ivEq1QCl8ylgkFEzMpU6npqDi5tGYRuG1LYzilb4= APP_DEBUG=true APP_URL=http://localhost APP_SERVICE=laravel @@ -74,6 +74,17 @@ AUTH0_LOGIN_CLIENT_ID=*** AUTH0_LOGIN_CLIENT_SECRET=*** AUTH0_LOGIN_REDIRECT_URI=http://localhost/auth/callback +KEYCLOAK_CREATION_ENABLED=true +KEYCLOAK_LOGIN_ENABLED=false + +KEYCLOAK_LOGIN_DOMAIN=account-keycloak-acc.uitid.be +KEYCLOAK_LOGIN_MANAGEMENT_DOMAIN=account-keycloak-acc.uitid.be +KEYCLOAK_LOGIN_CLIENT_ID= +KEYCLOAK_LOGIN_CLIENT_SECRET +KEYCLOAK_LOGIN_REDIRECT_URI=http://localhost/auth/callback +KEYCLOAK_LOGIN_PARAMETERS="locale=nl&referrer=publiq-platform&prompt=login&skip_verify_legacy=true&product_display_name=publiq platform" +KEYCLOAK_LOGIN_REALM_NAME= + AUTH0_CLIENT_CREATION_ENABLED=true # MUST always be set to DEV tenant config except for the .env for the production app! diff --git a/app/Auth0/Auth0ServiceProvider.php b/app/Auth0/Auth0ServiceProvider.php index d2cc79a87..e82458486 100644 --- a/app/Auth0/Auth0ServiceProvider.php +++ b/app/Auth0/Auth0ServiceProvider.php @@ -93,7 +93,7 @@ public function register(): void ); }); - if (config(KeycloakConfig::IS_ENABLED)) { + if (config(KeycloakConfig::KEYCLOAK_CREATION_ENABLED)) { // By default, the Auth0 integration is enabled. For testing purposes this can be disabled inside the .env file. // May always be registered even if there are no configured tenants, because in that case the cluster SDK will diff --git a/app/Domain/Auth/Controllers/LogoutController.php b/app/Domain/Auth/Controllers/LogoutController.php index 433009837..26459c560 100644 --- a/app/Domain/Auth/Controllers/LogoutController.php +++ b/app/Domain/Auth/Controllers/LogoutController.php @@ -4,6 +4,7 @@ namespace App\Domain\Auth\Controllers; +use App\Keycloak\KeycloakConfig; use Auth0\SDK\Auth0; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\Auth; @@ -17,12 +18,27 @@ private function getLogoutLink(): string /** @var Auth0 $auth0 */ $auth0 = app(Auth0::class); + $idtoken = $auth0->getIdToken(); + if (Auth::check()) { $auth0->logout(); Auth::guard(config('nova.guard'))->logout(); } - return $auth0->authentication()->getLogoutLink(config('app.url')); + $url = config('app.url'); + + if (config(KeycloakConfig::KEYCLOAK_LOGIN_ENABLED)) { + return sprintf( + 'https://%s/realms/%s/protocol/openid-connect/logout?client_id=%s&post_logout_redirect_uri=%s&id_token_hint=%s', + config(KeycloakConfig::KEYCLOAK_DOMAIN), + config(KeycloakConfig::KEYCLOAK_REALM_NAME), + config(KeycloakConfig::KEYCLOAK_CLIENT_ID), + $url, + $idtoken + ); + } + + return $auth0->authentication()->getLogoutLink($url); } public function adminLogout(): JsonResponse diff --git a/app/Domain/Integrations/Controllers/IntegrationController.php b/app/Domain/Integrations/Controllers/IntegrationController.php index ac8cb1c34..72cd433ac 100644 --- a/app/Domain/Integrations/Controllers/IntegrationController.php +++ b/app/Domain/Integrations/Controllers/IntegrationController.php @@ -24,6 +24,7 @@ use App\Domain\Integrations\IntegrationUrl; use App\Domain\Integrations\KeyVisibility; use App\Domain\Integrations\Mappers\OrganizationMapper; +use App\Domain\Integrations\Mappers\OrganizerMapper; use App\Domain\Integrations\Mappers\StoreContactMapper; use App\Domain\Integrations\Mappers\StoreIntegrationMapper; use App\Domain\Integrations\Mappers\StoreIntegrationUrlMapper; @@ -33,6 +34,7 @@ use App\Domain\Integrations\Mappers\UpdateIntegrationUrlsMapper; use App\Domain\Integrations\Repositories\IntegrationRepository; use App\Domain\Integrations\Repositories\IntegrationUrlRepository; +use App\Domain\Integrations\Repositories\OrganizerRepository; use App\Domain\KeyVisibilityUpgrades\KeyVisibilityUpgrade; use App\Domain\Organizations\Repositories\OrganizationRepository; use App\Domain\Subscriptions\Repositories\SubscriptionRepository; @@ -63,6 +65,7 @@ public function __construct( private readonly ContactRepository $contactRepository, private readonly ContactKeyVisibilityRepository $contactKeyVisibilityRepository, private readonly OrganizationRepository $organizationRepository, + private readonly OrganizerRepository $organizerRepository, private readonly CouponRepository $couponRepository, private readonly Auth0ClientRepository $auth0ClientRepository, private readonly UiTiDv1ConsumerRepository $uitidV1ConsumerRepository, @@ -172,6 +175,7 @@ public function show(string $id): Response 'oldCredentialsExpirationDate' => $oldCredentialsExpirationDate, 'email' => Auth::user()?->email, 'subscriptions' => $this->subscriptionRepository->all(), + 'organizers' => session('organizers'), ]); } @@ -288,6 +292,9 @@ public function requestActivation(string $id, RequestActivationRequest $request) $organization = OrganizationMapper::mapActivationRequest($request); $this->organizationRepository->save($organization); + $organizers = OrganizerMapper::map($request, $id); + $this->organizerRepository->create(...$organizers); + $this->integrationRepository->requestActivation(Uuid::fromString($id), $organization->id, $request->input('coupon')); return Redirect::back(); diff --git a/app/Domain/Integrations/Controllers/OrganizerController.php b/app/Domain/Integrations/Controllers/OrganizerController.php new file mode 100644 index 000000000..2aee7d1f0 --- /dev/null +++ b/app/Domain/Integrations/Controllers/OrganizerController.php @@ -0,0 +1,42 @@ +input('name'); + + $data = $this->searchService->searchUiTPASOrganizer($organizerName)->getMember()?->getItems() ?? []; + + return new JsonResponse( + array_map(function (Organizer $organizer) { + return [ + 'id' => $organizer->getCdbid(), + 'name' => $organizer->getName()?->getValues(), + ]; + }, $data) + ); + + } catch (\Exception $e) { + return new JsonResponse( + ['exception' => $e->getMessage()] + ); + ; + } + } +} diff --git a/app/Domain/Integrations/FormRequests/GetOrganizersRequest.php b/app/Domain/Integrations/FormRequests/GetOrganizersRequest.php new file mode 100644 index 000000000..6ccad2338 --- /dev/null +++ b/app/Domain/Integrations/FormRequests/GetOrganizersRequest.php @@ -0,0 +1,20 @@ + + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string'], + ]; + } +} diff --git a/app/Domain/Integrations/FormRequests/RequestActivationRequest.php b/app/Domain/Integrations/FormRequests/RequestActivationRequest.php index 7258a3be5..265009220 100644 --- a/app/Domain/Integrations/FormRequests/RequestActivationRequest.php +++ b/app/Domain/Integrations/FormRequests/RequestActivationRequest.php @@ -4,6 +4,7 @@ namespace App\Domain\Integrations\FormRequests; +use App\Domain\Integrations\Integration; use App\Domain\Integrations\IntegrationType; use App\Domain\Integrations\Repositories\IntegrationRepository; use App\Domain\Subscriptions\Repositories\SubscriptionRepository; @@ -24,20 +25,33 @@ public function rules(): array $rules = collect([ ...(new CreateOrganizationRequest())->rules(), 'coupon' => ['nullable', 'string', 'max:255'], + 'organizers' => ['required','array'], + 'organizers.*.name' => ['required', 'string'], + 'organizers.*.id' => ['required', 'string'], ]); - if (!$this->isAccountingInfoRequired()) { + if (!$this->isAccountingInfoRequired() || $this->isUITPAS()) { $rules->forget(['organization.invoiceEmail', 'organization.vat', 'coupon']); } + if (!$this->isUITPAS()) { + $rules->forget(['organizers']); + } + return $rules->toArray(); } - private function isAccountingInfoRequired(): bool + private function fetchIntegration(): Integration { /** @var IntegrationRepository $integrationRepository */ $integrationRepository = App::get(IntegrationRepository::class); - $integration = $integrationRepository->getById(Uuid::fromString($this->id)); + return $integrationRepository->getById(Uuid::fromString($this->id)); + } + + + private function isAccountingInfoRequired(): bool + { + $integration = $this->fetchIntegration(); /** @var SubscriptionRepository $subscriptionRepository */ $subscriptionRepository = App::get(SubscriptionRepository::class); @@ -45,4 +59,10 @@ private function isAccountingInfoRequired(): bool return $integration->type !== IntegrationType::EntryApi || $subscription->price > 0.0; } + + private function isUITPAS(): bool + { + $integration = $this->fetchIntegration(); + return $integration->type === IntegrationType::UiTPAS; + } } diff --git a/app/Domain/Integrations/FormRequests/StoreIntegrationRequest.php b/app/Domain/Integrations/FormRequests/StoreIntegrationRequest.php index 75bd9c3f1..950d89bae 100644 --- a/app/Domain/Integrations/FormRequests/StoreIntegrationRequest.php +++ b/app/Domain/Integrations/FormRequests/StoreIntegrationRequest.php @@ -4,7 +4,9 @@ namespace App\Domain\Integrations\FormRequests; +use App\Domain\Integrations\IntegrationType; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; final class StoreIntegrationRequest extends FormRequest { @@ -26,6 +28,7 @@ public function rules(): array 'lastNameTechnicalContact' => ['required', 'string', 'max:255'], 'emailTechnicalContact' => ['required', 'string', 'email', 'max:255'], 'agreement' => ['required', 'string'], + 'uitpasAgreement' => [Rule::requiredIf($this->input('integrationType') === IntegrationType::UiTPAS->value), 'nullable', 'string'], 'coupon' => ['nullable', 'string'], ]; } diff --git a/app/Domain/Integrations/FormRequests/UpdateIntegrationRequest.php b/app/Domain/Integrations/FormRequests/UpdateIntegrationRequest.php index 8a181134f..267aae0c3 100644 --- a/app/Domain/Integrations/FormRequests/UpdateIntegrationRequest.php +++ b/app/Domain/Integrations/FormRequests/UpdateIntegrationRequest.php @@ -4,6 +4,7 @@ namespace App\Domain\Integrations\FormRequests; +use App\Domain\Integrations\IntegrationType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -18,7 +19,7 @@ public function rules(): array 'integrationName' => ['required_without:description', 'string', 'max:255'], 'description' => ['required_without:integrationName', 'string', 'max:255'], 'website' => [ - Rule::requiredIf($this->input('integrationType') === 'uitpas'), + Rule::requiredIf($this->input('integrationType') === IntegrationType::UiTPAS->value), 'nullable', 'url:http,https', 'max:255', diff --git a/app/Domain/Integrations/Mappers/OrganizerMapper.php b/app/Domain/Integrations/Mappers/OrganizerMapper.php new file mode 100644 index 000000000..bbd35facc --- /dev/null +++ b/app/Domain/Integrations/Mappers/OrganizerMapper.php @@ -0,0 +1,33 @@ +input('organizers') ?? [] as $organizer) { + $organizers[] = new Organizer( + Uuid::uuid4(), + Uuid::fromString($id), + Uuid::fromString($organizer['id']) + ); + } + + return $organizers; + } +} diff --git a/app/Domain/Integrations/Repositories/EloquentOrganizerRepository.php b/app/Domain/Integrations/Repositories/EloquentOrganizerRepository.php index a3610d229..3714224c7 100644 --- a/app/Domain/Integrations/Repositories/EloquentOrganizerRepository.php +++ b/app/Domain/Integrations/Repositories/EloquentOrganizerRepository.php @@ -6,15 +6,20 @@ use App\Domain\Integrations\Models\OrganizerModel; use App\Domain\Integrations\Organizer; +use Illuminate\Support\Facades\DB; final class EloquentOrganizerRepository implements OrganizerRepository { - public function create(Organizer $organizer): void + public function create(Organizer ...$organizers): void { - OrganizerModel::query()->create([ - 'id' => $organizer->id->toString(), - 'integration_id' => $organizer->integrationId->toString(), - 'organizer_id' => $organizer->organizerId->toString(), - ]); + DB::transaction(function () use ($organizers): void { + foreach ($organizers as $organizer) { + OrganizerModel::query()->create([ + 'id' => $organizer->id->toString(), + 'integration_id' => $organizer->integrationId->toString(), + 'organizer_id' => $organizer->organizerId->toString(), + ]); + } + }); } } diff --git a/app/Domain/Integrations/Repositories/OrganizerRepository.php b/app/Domain/Integrations/Repositories/OrganizerRepository.php index b234244ec..12be40211 100644 --- a/app/Domain/Integrations/Repositories/OrganizerRepository.php +++ b/app/Domain/Integrations/Repositories/OrganizerRepository.php @@ -8,5 +8,5 @@ interface OrganizerRepository { - public function create(Organizer $organizer): void; + public function create(Organizer ...$organizer): void; } diff --git a/app/Keycloak/Converters/IntegrationUrlConverter.php b/app/Keycloak/Converters/IntegrationUrlConverter.php index 0ffd47831..5f6dbf23a 100644 --- a/app/Keycloak/Converters/IntegrationUrlConverter.php +++ b/app/Keycloak/Converters/IntegrationUrlConverter.php @@ -54,6 +54,6 @@ public static function buildLogoutUrls(Integration $integration, Environment $en } $logoutUrls = $integration->urlsForTypeAndEnvironment(IntegrationUrlType::Logout, $environment); - return implode('#', array_map(static fn ($url) => $url->url, $logoutUrls)); + return implode('##', array_map(static fn ($url) => $url->url, $logoutUrls)); } } diff --git a/app/Keycloak/KeycloakConfig.php b/app/Keycloak/KeycloakConfig.php index 84876f3f9..32c8efffa 100644 --- a/app/Keycloak/KeycloakConfig.php +++ b/app/Keycloak/KeycloakConfig.php @@ -6,5 +6,10 @@ final class KeycloakConfig { - public const IS_ENABLED = 'keycloak.enabled'; + public const KEYCLOAK_LOGIN_ENABLED = 'keycloak.loginEnabled'; + public const KEYCLOAK_CREATION_ENABLED = 'keycloak.creationEnabled'; + public const KEYCLOAK_DOMAIN = 'keycloak.login.domain'; + public const KEYCLOAK_CLIENT_ID = 'keycloak.login.clientId'; + public const KEYCLOAK_REALM_NAME = 'keycloak.login.realmName'; + public const KEYCLOAK_LOGIN_PARAMETERS = 'keycloak.login.parameters'; } diff --git a/app/Keycloak/KeycloakServiceProvider.php b/app/Keycloak/KeycloakServiceProvider.php index c5a77d801..7cb2c888f 100644 --- a/app/Keycloak/KeycloakServiceProvider.php +++ b/app/Keycloak/KeycloakServiceProvider.php @@ -67,7 +67,7 @@ public function register(): void private function bootstrapEventHandling(): void { - if (!config(KeycloakConfig::IS_ENABLED)) { + if (!config(KeycloakConfig::KEYCLOAK_CREATION_ENABLED)) { return; } diff --git a/app/Nova/Resources/Integration.php b/app/Nova/Resources/Integration.php index 441976a20..f6ac92260 100644 --- a/app/Nova/Resources/Integration.php +++ b/app/Nova/Resources/Integration.php @@ -67,7 +67,7 @@ public static function searchableColumns(): array $output[] = new SearchableRelation('auth0Clients', 'auth0_client_id'); } - if (config(KeycloakConfig::IS_ENABLED)) { + if (config(KeycloakConfig::KEYCLOAK_CREATION_ENABLED)) { $output[] = new SearchableRelation('keycloakClients', 'client_id'); } @@ -204,7 +204,7 @@ function (Text $field, NovaRequest $request, FormData $formData) { $fields[] = HasMany::make('UiTiD v2 Client Credentials (Auth0)', 'auth0Clients', Auth0Client::class); } - if (config(KeycloakConfig::IS_ENABLED)) { + if (config(KeycloakConfig::KEYCLOAK_CREATION_ENABLED)) { $fields[] = HasMany::make('Keycloak client Credentials', 'keycloakClients', KeycloakClient::class); } @@ -289,7 +289,7 @@ public function actions(NovaRequest $request): array ->canRun(fn (Request $request, IntegrationModel $model) => $model->hasMissingAuth0Clients()); } - if (config(KeycloakConfig::IS_ENABLED)) { + if (config(KeycloakConfig::KEYCLOAK_CREATION_ENABLED)) { $actions[] = (new CreateMissingKeycloakClients()) ->withName('Create missing Keycloak clients') ->exceptOnIndex() diff --git a/app/Nova/Resources/KeycloakClient.php b/app/Nova/Resources/KeycloakClient.php index 8e739c421..90b618767 100644 --- a/app/Nova/Resources/KeycloakClient.php +++ b/app/Nova/Resources/KeycloakClient.php @@ -86,7 +86,7 @@ public function fields(NovaRequest $request): array Text::make('Open', function (KeycloakClientModel $model) { $client = $model->toDomain(); $realm = App::get(Realms::class)->getRealmByEnvironment($client->environment); - $url = $realm->baseUrl . 'admin/master/console/#/' . $realm->internalName . '/clients/' . $client->id->toString() . '/settings'; + $url = $realm->baseUrl . 'dashboard/' . $realm->internalName . '/clients/' . urlencode($client->clientId) . '/settings'; return sprintf('Open in Keycloak', $url); })->asHtml(), diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 211d368bb..b4f304122 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -19,12 +19,13 @@ use App\Domain\Integrations\Policies\IntegrationPolicy; use App\Domain\Integrations\Policies\IntegrationUrlPolicy; use App\Domain\Integrations\Policies\OrganizerPolicy; +use App\Domain\KeyVisibilityUpgrades\Models\KeyVisibilityUpgradeModel; +use App\Domain\KeyVisibilityUpgrades\Policies\KeyVisibilityUpgradePolicy; use App\Domain\Organizations\Models\OrganizationModel; use App\Domain\Organizations\Policies\OrganizationPolicy; use App\Domain\Subscriptions\Models\SubscriptionModel; use App\Domain\Subscriptions\Policies\SubscriptionPolicy; -use App\Domain\KeyVisibilityUpgrades\Models\KeyVisibilityUpgradeModel; -use App\Domain\KeyVisibilityUpgrades\Policies\KeyVisibilityUpgradePolicy; +use App\Keycloak\KeycloakConfig; use App\Keycloak\Models\KeycloakClientModel; use App\Keycloak\Policies\KeycloakClientPolicy; use App\UiTiDv1\Models\UiTiDv1ConsumerModel; @@ -63,14 +64,29 @@ public function boot(): void $this->app->singleton( Auth0::class, - static fn (): Auth0 => new Auth0(new SdkConfiguration(config('auth0'))) - ); + static function (): Auth0 { + if (config(KeycloakConfig::KEYCLOAK_LOGIN_ENABLED)) { + return new Auth0(new SdkConfiguration(config('keycloak.login'))); + } - $auth0LoginParameters = []; - parse_str(config('auth0.login_parameters'), $auth0LoginParameters); + return new Auth0(new SdkConfiguration(config('auth0'))); + } + ); $this->app->when(LoginController::class) ->needs('$loginParams') - ->give($auth0LoginParameters); + ->give($this->getLoginParameters()); + } + + private function getLoginParameters(): array + { + $auth0LoginParameters = []; + if (config(KeycloakConfig::KEYCLOAK_LOGIN_ENABLED)) { + parse_str(config(KeycloakConfig::KEYCLOAK_LOGIN_PARAMETERS), $auth0LoginParameters); + return $auth0LoginParameters; + } + + parse_str(config('auth0.login_parameters'), $auth0LoginParameters); + return $auth0LoginParameters; } } diff --git a/app/Search/Sapi3/Sapi3SearchService.php b/app/Search/Sapi3/Sapi3SearchService.php index fb169697b..a9c0abfee 100644 --- a/app/Search/Sapi3/Sapi3SearchService.php +++ b/app/Search/Sapi3/Sapi3SearchService.php @@ -4,7 +4,7 @@ namespace App\Search\Sapi3; -use CultuurNet\SearchV3\Parameter\Label; +use CultuurNet\SearchV3\Parameter\Query; use CultuurNet\SearchV3\SearchClientInterface; use CultuurNet\SearchV3\SearchQuery; use CultuurNet\SearchV3\ValueObjects\PagedCollection; @@ -18,8 +18,9 @@ public function __construct(private SearchClientInterface $searchClient) public function searchUiTPASOrganizer(string $name): PagedCollection { $searchQuery = new SearchQuery(); - $searchQuery->addParameter(new Label('UiTPAS')); + $searchQuery->addParameter(new Query('labels:UiTPAS*')); $searchQuery->addParameter(new Name($name)); + $searchQuery->setLimit(5); $searchQuery->setEmbed(true); return $this->searchClient->searchOrganizers($searchQuery); diff --git a/composer.lock b/composer.lock index 460880229..ec3d6d77d 100644 --- a/composer.lock +++ b/composer.lock @@ -3316,16 +3316,16 @@ }, { "name": "monolog/monolog", - "version": "3.6.0", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654" + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", - "reference": "4b18b21a5527a3d5ffdac2fd35d3ab25a9597654", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", "shasum": "" }, "require": { @@ -3401,7 +3401,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.6.0" + "source": "https://github.com/Seldaek/monolog/tree/3.7.0" }, "funding": [ { @@ -3413,7 +3413,7 @@ "type": "tidelift" } ], - "time": "2024-04-12T21:02:21+00:00" + "time": "2024-06-28T09:40:51+00:00" }, { "name": "nesbot/carbon", @@ -6190,16 +6190,16 @@ }, { "name": "symfony/console", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91" + "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/be5854cee0e8c7b110f00d695d11debdfa1a2a91", - "reference": "be5854cee0e8c7b110f00d695d11debdfa1a2a91", + "url": "https://api.github.com/repos/symfony/console/zipball/6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", + "reference": "6edb5363ec0c78ad4d48c5128ebf4d083d89d3a9", "shasum": "" }, "require": { @@ -6264,7 +6264,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.8" + "source": "https://github.com/symfony/console/tree/v6.4.9" }, "funding": [ { @@ -6280,7 +6280,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-06-28T09:49:33+00:00" }, { "name": "symfony/css-selector", @@ -6416,16 +6416,16 @@ }, { "name": "symfony/error-handler", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc" + "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", - "reference": "ef836152bf13472dc5fb5b08b0c0c4cfeddc0fcc", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c9b7cc075b3ab484239855622ca05cb0b99c13ec", + "reference": "c9b7cc075b3ab484239855622ca05cb0b99c13ec", "shasum": "" }, "require": { @@ -6471,7 +6471,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v6.4.8" + "source": "https://github.com/symfony/error-handler/tree/v6.4.9" }, "funding": [ { @@ -6487,7 +6487,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-06-21T16:04:15+00:00" }, { "name": "symfony/event-dispatcher", @@ -6788,16 +6788,16 @@ }, { "name": "symfony/http-kernel", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1" + "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", - "reference": "6c519aa3f32adcfd1d1f18d923f6b227d9acf3c1", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", + "reference": "cc4a9bec6e1bdd2405f40277a68a6ed1bb393005", "shasum": "" }, "require": { @@ -6882,7 +6882,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v6.4.8" + "source": "https://github.com/symfony/http-kernel/tree/v6.4.9" }, "funding": [ { @@ -6898,20 +6898,20 @@ "type": "tidelift" } ], - "time": "2024-06-02T16:06:25+00:00" + "time": "2024-06-28T11:48:06+00:00" }, { "name": "symfony/mailer", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "76326421d44c07f7824b19487cfbf87870b37efc" + "reference": "e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/76326421d44c07f7824b19487cfbf87870b37efc", - "reference": "76326421d44c07f7824b19487cfbf87870b37efc", + "url": "https://api.github.com/repos/symfony/mailer/zipball/e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45", + "reference": "e2d56f180f5b8c5e7c0fbea872bb1f529b6d6d45", "shasum": "" }, "require": { @@ -6962,7 +6962,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v6.4.8" + "source": "https://github.com/symfony/mailer/tree/v6.4.9" }, "funding": [ { @@ -6978,20 +6978,20 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-06-28T07:59:05+00:00" }, { "name": "symfony/mime", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33" + "reference": "7d048964877324debdcb4e0549becfa064a20d43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/618597ab8b78ac86d1c75a9d0b35540cda074f33", - "reference": "618597ab8b78ac86d1c75a9d0b35540cda074f33", + "url": "https://api.github.com/repos/symfony/mime/zipball/7d048964877324debdcb4e0549becfa064a20d43", + "reference": "7d048964877324debdcb4e0549becfa064a20d43", "shasum": "" }, "require": { @@ -7005,7 +7005,7 @@ "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", "symfony/mailer": "<5.4", - "symfony/serializer": "<6.3.2" + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" }, "require-dev": { "egulias/email-validator": "^2.1.10|^3.1|^4", @@ -7015,7 +7015,7 @@ "symfony/process": "^5.4|^6.4|^7.0", "symfony/property-access": "^5.4|^6.0|^7.0", "symfony/property-info": "^5.4|^6.0|^7.0", - "symfony/serializer": "^6.3.2|^7.0" + "symfony/serializer": "^6.4.3|^7.0.3" }, "type": "library", "autoload": { @@ -7047,7 +7047,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v6.4.8" + "source": "https://github.com/symfony/mime/tree/v6.4.9" }, "funding": [ { @@ -7063,7 +7063,7 @@ "type": "tidelift" } ], - "time": "2024-06-01T07:50:16+00:00" + "time": "2024-06-28T09:49:33+00:00" }, { "name": "symfony/options-resolver", @@ -8238,16 +8238,16 @@ }, { "name": "symfony/string", - "version": "v7.1.1", + "version": "v7.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "60bc311c74e0af215101235aa6f471bcbc032df2" + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/60bc311c74e0af215101235aa6f471bcbc032df2", - "reference": "60bc311c74e0af215101235aa6f471bcbc032df2", + "url": "https://api.github.com/repos/symfony/string/zipball/14221089ac66cf82e3cf3d1c1da65de305587ff8", + "reference": "14221089ac66cf82e3cf3d1c1da65de305587ff8", "shasum": "" }, "require": { @@ -8305,7 +8305,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.1.1" + "source": "https://github.com/symfony/string/tree/v7.1.2" }, "funding": [ { @@ -8321,7 +8321,7 @@ "type": "tidelift" } ], - "time": "2024-06-04T06:40:14+00:00" + "time": "2024-06-28T09:27:18+00:00" }, { "name": "symfony/translation", @@ -8666,16 +8666,16 @@ }, { "name": "symfony/var-dumper", - "version": "v6.4.8", + "version": "v6.4.9", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25" + "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/ad23ca4312395f0a8a8633c831ef4c4ee542ed25", - "reference": "ad23ca4312395f0a8a8633c831ef4c4ee542ed25", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c31566e4ca944271cc8d8ac6887cbf31b8c6a172", + "reference": "c31566e4ca944271cc8d8ac6887cbf31b8c6a172", "shasum": "" }, "require": { @@ -8731,7 +8731,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v6.4.8" + "source": "https://github.com/symfony/var-dumper/tree/v6.4.9" }, "funding": [ { @@ -8747,7 +8747,7 @@ "type": "tidelift" } ], - "time": "2024-05-31T14:49:08+00:00" + "time": "2024-06-27T13:23:14+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -9626,16 +9626,16 @@ }, { "name": "laravel/sail", - "version": "v1.29.3", + "version": "v1.30.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "e35b3ffe1b9ea598246d7e99197ee8799f6dc2e5" + "reference": "e08b594052385ab9891dd86047e52da8a953c827" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/e35b3ffe1b9ea598246d7e99197ee8799f6dc2e5", - "reference": "e35b3ffe1b9ea598246d7e99197ee8799f6dc2e5", + "url": "https://api.github.com/repos/laravel/sail/zipball/e08b594052385ab9891dd86047e52da8a953c827", + "reference": "e08b594052385ab9891dd86047e52da8a953c827", "shasum": "" }, "require": { @@ -9685,7 +9685,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2024-06-12T16:24:41+00:00" + "time": "2024-06-18T17:36:56+00:00" }, { "name": "maximebf/debugbar", @@ -10260,16 +10260,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "10.1.14", + "version": "10.1.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b" + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", - "reference": "e3f51450ebffe8e0efdf7346ae966a656f7d5e5b", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", + "reference": "5da8b1728acd1e6ffdf2ff32ffbdfd04307f26ae", "shasum": "" }, "require": { @@ -10326,7 +10326,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.14" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.15" }, "funding": [ { @@ -10334,7 +10334,7 @@ "type": "github" } ], - "time": "2024-03-12T15:33:41+00:00" + "time": "2024-06-29T08:25:15+00:00" }, { "name": "phpunit/php-file-iterator", @@ -10581,16 +10581,16 @@ }, { "name": "phpunit/phpunit", - "version": "10.5.21", + "version": "10.5.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "ac837816fa52078f7a5e17ed774f256a72a51af6" + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ac837816fa52078f7a5e17ed774f256a72a51af6", - "reference": "ac837816fa52078f7a5e17ed774f256a72a51af6", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5f124e3e3e561006047b532fd0431bf5bb6b9015", + "reference": "5f124e3e3e561006047b532fd0431bf5bb6b9015", "shasum": "" }, "require": { @@ -10662,7 +10662,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.21" + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.24" }, "funding": [ { @@ -10678,7 +10678,7 @@ "type": "tidelift" } ], - "time": "2024-06-15T09:13:15+00:00" + "time": "2024-06-20T13:09:54+00:00" }, { "name": "publiq/php-cs-fixer-config", diff --git a/config/keycloak.php b/config/keycloak.php index ba4d878b3..f23ad83f5 100644 --- a/config/keycloak.php +++ b/config/keycloak.php @@ -2,8 +2,25 @@ declare(strict_types=1); +use Auth0\SDK\Configuration\SdkConfiguration; + return [ - 'enabled' => env('KEYCLOAK_ENABLED', false), + + 'loginEnabled' => env('KEYCLOAK_LOGIN_ENABLED', false), + 'creationEnabled' => env('KEYCLOAK_CREATION_ENABLED', false), + 'login' => [ + 'strategy' => env('AUTH0_LOGIN_STRATEGY', SdkConfiguration::STRATEGY_REGULAR), + 'domain' => env('KEYCLOAK_LOGIN_DOMAIN'), + 'managementDomain' => env('KEYCLOAK_LOGIN_MANAGEMENT_DOMAIN'), + 'clientId' => env('KEYCLOAK_LOGIN_CLIENT_ID'), + 'clientSecret' => env('KEYCLOAK_LOGIN_CLIENT_SECRET'), + 'audience' => env('KEYCLOAK_LOGIN_AUDIENCE'), + 'realmName' => env('KEYCLOAK_LOGIN_REALM_NAME'), + 'parameters' => env('KEYCLOAK_LOGIN_PARAMETERS'), + 'cookieSecret' => env('KEYCLOAK_LOGIN_COOKIE_SECRET', env('APP_KEY')), + 'cookieExpires' => env('COOKIE_EXPIRES', 0), + 'redirectUri' => env('KEYCLOAK_LOGIN_REDIRECT_URI', env('APP_URL') . '/callback'), + ], 'environments' => [ 'acc' => [ 'internalName' => env('KEYCLOAK_ACC_REALM_NAME', ''), diff --git a/lang/en/validation.php b/lang/en/validation.php index 01d7332cf..05732014d 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -196,6 +196,7 @@ 'organization.address.zip' => 'postcode', 'organization.address.city' => 'city', 'organization.vat' => 'VAT or company number', + 'organizers' => 'organizers' ], ]; diff --git a/lang/nl/validation.php b/lang/nl/validation.php index 53eaa9c61..e2c3be749 100644 --- a/lang/nl/validation.php +++ b/lang/nl/validation.php @@ -199,6 +199,7 @@ 'lastName' => 'achternaam', 'email' => 'email', 'agreement' => 'gebruikersvoorwaarden', + 'uitpasAgreement' => 'verwerkingsvoorwaarden van UiTPAS', 'functional.email' => 'email', 'functional.lastName' => 'achternaam', 'functional.firstName' => 'voornaam', @@ -211,6 +212,7 @@ 'organization.address.zip' => 'postcode', 'organization.address.city' => 'gemeente', 'organization.vat' => 'BTW of ondernemingsnummer', + 'organizers' => 'organisaties' ], ]; diff --git a/package-lock.json b/package-lock.json index e4610f186..528cd81dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,14 +23,14 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.1", - "@playwright/test": "^1.44.1", + "@playwright/test": "^1.45.0", "@tailwindcss/forms": "^0.5.7", - "@types/lodash": "^4.17.5", + "@types/lodash": "^4.17.6", "@types/node": "^20.14.8", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.12.0", - "@typescript-eslint/parser": "^7.12.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "axios": "^1.7.2", @@ -43,7 +43,7 @@ "prettier": "3.3.2", "tailwindcss": "^3.4.4", "typescript": "5.5.2", - "vite": "^5.3.1" + "vite": "^5.3.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1186,18 +1186,18 @@ } }, "node_modules/@playwright/test": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz", - "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.0.tgz", + "integrity": "sha512-TVYsfMlGAaxeUllNkywbwek67Ncf8FRGn8ZlRdO291OL3NjG9oMbfVhyP82HQF0CZLMrYsvesqoUekxdWuF9Qw==", "dev": true, "dependencies": { - "playwright": "1.44.1" + "playwright": "1.45.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -1570,9 +1570,9 @@ "dev": true }, "node_modules/@types/lodash": { - "version": "4.17.5", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", - "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "version": "4.17.6", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", + "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", "dev": true }, "node_modules/@types/node": { @@ -1610,16 +1610,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.12.0.tgz", - "integrity": "sha512-7F91fcbuDf/d3S8o21+r3ZncGIke/+eWk0EpO21LXhDfLahriZF9CGj4fbAetEjlaBdjdSm9a6VeXbpbT6Z40Q==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", + "integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/type-utils": "7.12.0", - "@typescript-eslint/utils": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/type-utils": "7.14.1", + "@typescript-eslint/utils": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1643,15 +1643,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.12.0.tgz", - "integrity": "sha512-dm/J2UDY3oV3TKius2OUZIFHsomQmpHtsV0FTh1WO8EKgHLQ1QCADUqscPgTpU+ih1e21FQSRjXckHn3txn6kQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4" }, "engines": { @@ -1671,13 +1671,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.12.0.tgz", - "integrity": "sha512-itF1pTnN6F3unPak+kutH9raIkL3lhH1YRPGgt7QQOh43DQKVJXmWkpb+vpc/TiDHs6RSd9CTbDsc/Y+Ygq7kg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", + "integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0" + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1688,13 +1688,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.12.0.tgz", - "integrity": "sha512-lib96tyRtMhLxwauDWUp/uW3FMhLA6D0rJ8T7HmH7x23Gk1Gwwu8UZ94NMXBvOELn6flSPiBrCKlehkiXyaqwA==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", + "integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.12.0", - "@typescript-eslint/utils": "7.12.0", + "@typescript-eslint/typescript-estree": "7.14.1", + "@typescript-eslint/utils": "7.14.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1715,9 +1715,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.12.0.tgz", - "integrity": "sha512-o+0Te6eWp2ppKY3mLCU+YA9pVJxhUJE15FV7kxuD9jgwIAa+w/ycGJBMrYDTpVGUM/tgpa9SeMOugSabWFq7bg==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", + "integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1728,13 +1728,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.12.0.tgz", - "integrity": "sha512-5bwqLsWBULv1h6pn7cMW5dXX/Y2amRqLaKqsASVwbBHMZSnHqE/HN4vT4fE0aFsiwxYvr98kqOWh1a8ZKXalCQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", + "integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/visitor-keys": "7.12.0", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/visitor-keys": "7.14.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1756,15 +1756,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.12.0.tgz", - "integrity": "sha512-Y6hhwxwDx41HNpjuYswYp6gDbkiZ8Hin9Bf5aJQn1bpTs3afYY4GX+MPYxma8jtoIV2GRwTM/UJm/2uGCVv+DQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", + "integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.12.0", - "@typescript-eslint/types": "7.12.0", - "@typescript-eslint/typescript-estree": "7.12.0" + "@typescript-eslint/scope-manager": "7.14.1", + "@typescript-eslint/types": "7.14.1", + "@typescript-eslint/typescript-estree": "7.14.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1778,12 +1778,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.12.0.tgz", - "integrity": "sha512-uZk7DevrQLL3vSnfFl5bj4sL75qC9D6EdjemIdbtkuUmIheWpuiiylSY01JxJE7+zGrOWDZrp1WxOuDntvKrHQ==", + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", + "integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.12.0", + "@typescript-eslint/types": "7.14.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4752,33 +4752,33 @@ } }, "node_modules/playwright": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz", - "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.0.tgz", + "integrity": "sha512-4z3ac3plDfYzGB6r0Q3LF8POPR20Z8D0aXcxbJvmfMgSSq1hkcgvFRXJk9rUq5H/MJ0Ktal869hhOdI/zUTeLA==", "dev": true, "dependencies": { - "playwright-core": "1.44.1" + "playwright-core": "1.45.0" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.44.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz", - "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==", + "version": "1.45.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.0.tgz", + "integrity": "sha512-lZmHlFQ0VYSpAs43dRq1/nJ9G/6SiTI7VPqidld9TDefL9tX87bTKExWZZUF5PeRyqtXqd8fQi2qmfIedkwsNQ==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/playwright/node_modules/fsevents": { @@ -6010,9 +6010,9 @@ "dev": true }, "node_modules/vite": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz", - "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", + "integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", "dev": true, "dependencies": { "esbuild": "^0.21.3", diff --git a/package.json b/package.json index 05bb575d9..f45407071 100644 --- a/package.json +++ b/package.json @@ -30,14 +30,14 @@ }, "devDependencies": { "@faker-js/faker": "^8.4.1", - "@playwright/test": "^1.44.1", + "@playwright/test": "^1.45.0", "@tailwindcss/forms": "^0.5.7", - "@types/lodash": "^4.17.5", + "@types/lodash": "^4.17.6", "@types/node": "^20.14.8", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.12.0", - "@typescript-eslint/parser": "^7.12.0", + "@typescript-eslint/eslint-plugin": "^7.14.1", + "@typescript-eslint/parser": "^7.14.1", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.19", "axios": "^1.7.2", @@ -50,6 +50,6 @@ "prettier": "3.3.2", "tailwindcss": "^3.4.4", "typescript": "5.5.2", - "vite": "^5.3.1" + "vite": "^5.3.2" } } diff --git a/resources/translations/en.json b/resources/translations/en.json index 6cf490257..54c463c25 100644 --- a/resources/translations/en.json +++ b/resources/translations/en.json @@ -133,6 +133,10 @@ }, "error": "Not all mandatory fields were filled in correctly. Check your input.", "agree": "I agree to the <1>terms of use and the <2>privacy policy", + "uitpasAgreement": { + "label": "I agree with the <1>processing conditions of UiTPAS<1>", + "link": "https://www.publiq.be/en/projects/publiq-platform/processing-conditions" + }, "terms_of_use_link": "https://docs.publiq.be/docs/uitdatabank/terms-of-use/terms-of-use", "privacy_link": "https://www.publiq.be/en/privacy", "coupon": "I have a coupon", @@ -184,6 +188,14 @@ "coupon": "Coupon", "plan": "Subscription", "year": "year" + }, + "uitpas": { + "partner": "Partner", + "organizers": { + "title": "UiTdatabank organizers", + "info": "Enter the UiTdatabank organizers for which you want to execute actions in UiTPAS.", + "label": "Organizers" + } } }, "documentation": { diff --git a/resources/translations/nl.json b/resources/translations/nl.json index 2e1ee65a0..907c2f04a 100644 --- a/resources/translations/nl.json +++ b/resources/translations/nl.json @@ -133,6 +133,10 @@ }, "error": "Niet alle verplichte velden werden correct ingevuld. Controleer je invoer.", "agree": "Ik ga akkoord met de <1>gebruiksvoorwaarden en de <2>privacyverklaring", + "uitpasAgreement": { + "label": "Ik ga akkoord met de <1>verwerkingsvoorwaarden van UiTPAS<1>", + "link": "https://www.publiq.be/nl/projecten/publiq-platform/algemene-verwerkingsvoorwaarden" + }, "terms_of_use_link": "https://docs.publiq.be/docs/uitdatabank/terms-of-use/terms-of-use", "privacy_link": "https://www.publiq.be/nl/privacy", "coupon": "Ik heb een coupon", @@ -184,6 +188,14 @@ "coupon": "Coupon", "plan": "Abonnement", "year": "jaar" + }, + "uitpas": { + "partner": "Partner", + "organizers": { + "title": "UiTdatabank organisaties", + "info": "Geef de UiTdatabank organisaties op waarvoor je acties in UiTPAS wilt uitvoeren.", + "label": "Organisaties" + } } }, "documentation": { diff --git a/resources/ts/Components/ActivationDialog.tsx b/resources/ts/Components/ActivationDialog.tsx index 3c1cafb06..18e094e1c 100644 --- a/resources/ts/Components/ActivationDialog.tsx +++ b/resources/ts/Components/ActivationDialog.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useRef, useState } from "react"; import { Dialog } from "./Dialog"; import { ButtonSecondary } from "./ButtonSecondary"; import { ButtonPrimary } from "./ButtonPrimary"; @@ -13,6 +13,12 @@ import type { PricingPlan } from "../hooks/useGetPricingPlans"; import { formatCurrency } from "../utils/formatCurrency"; import { Heading } from "./Heading"; import { CouponInfoContext } from "../Context/CouponInfo"; +import { faTrash } from "@fortawesome/free-solid-svg-icons"; +import { ButtonIcon } from "./ButtonIcon"; +import { debounce } from "lodash"; +import type { Organization } from "../types/Organization"; +import type { UiTPASOrganizer } from "../types/UiTPASOrganizer"; +import { Alert } from "./Alert"; const PriceOverview = ({ coupon, @@ -81,6 +87,12 @@ type Props = { email: string; }; +type InitialValues = { + organization: Omit; + organizers: UiTPASOrganizer[]; + coupon: string; +}; + export const ActivationDialog = ({ isVisible, onClose, @@ -93,7 +105,7 @@ export const ActivationDialog = ({ const isMobile = useIsMobile(); - const initialValuesOrganization = { + const initialValuesOrganization: InitialValues = { organization: { name: "", invoiceEmail: "", @@ -105,6 +117,7 @@ export const ActivationDialog = ({ country: "Belgium", }, }, + organizers: [], coupon: "", }; @@ -121,10 +134,90 @@ export const ActivationDialog = ({ string | undefined >; + const isBillingInfoAndPriceOverviewVisible = + type !== IntegrationType.EntryApi && type !== IntegrationType.UiTPAS; + + const [isSearchListVisible, setIsSearchListVisible] = useState(false); + const [organizerList, setOrganizerList] = useState([]); + const [organizerError, setOrganizerError] = useState(false); + + const organizersInputRef = useRef(null); + + const handleGetOrganizers = debounce( + async (e: React.ChangeEvent) => { + const response = await fetch(`/organizers?name=${e.target.value}`); + const data = await response.json(); + if ("exception" in data) { + setOrganizerError(true); + return; + } + const organizers = data.map( + (organizer: { name: string | { nl: string }; id: string }) => { + if (typeof organizer.name === "object" && "nl" in organizer.name) { + return { name: organizer.name.nl, id: organizer.id }; + } + return organizer; + } + ); + setOrganizerList(organizers); + if (organizerError) { + setOrganizerError(false); + } + }, + 750 + ); + + const handleAddOrganizers = (organizer: UiTPASOrganizer) => { + const isDuplicate = + organizationForm.data.organizers.length > 0 && + organizationForm.data.organizers.some( + (existingOrganizer) => existingOrganizer.id === organizer.id + ); + if (!isDuplicate) { + organizationForm.setData("organizers", [ + ...organizationForm.data.organizers, + organizer, + ]); + setIsSearchListVisible(false); + setOrganizerList([]); + if (organizersInputRef.current) { + organizersInputRef.current.value = ""; + } + } + }; + + const handleDeleteOrganizer = (deletedOrganizer: string) => { + const updatedOrganizers = organizationForm.data.organizers.filter( + (organizer) => organizer.name !== deletedOrganizer + ); + organizationForm.setData("organizers", updatedOrganizers); + }; + if (!isVisible) { return null; } + const handleInputOnChange = async ( + e: React.ChangeEvent + ) => { + if (e.target.value !== "") { + await handleGetOrganizers(e); + setIsSearchListVisible(true); + } else { + setIsSearchListVisible(false); + setOrganizerList([]); + } + }; + + const handleKeyDown = ( + event: React.KeyboardEvent, + organizer: UiTPASOrganizer + ) => { + if (event.key === "Enter") { + handleAddOrganizers(organizer); + } + }; + return ( <> + {type === IntegrationType.UiTPAS && ( + + {t("integrations.activation_dialog.uitpas.partner")} + + )} - {type !== IntegrationType.EntryApi && ( + {type === IntegrationType.UiTPAS && ( + <> +
+ + {t("integrations.activation_dialog.uitpas.organizers.title")} + + + {t("integrations.activation_dialog.uitpas.organizers.info")} + +
+
+ {organizationForm.data.organizers.length > 0 && + organizationForm.data.organizers.map((organizer, index) => ( +
+

{organizer.name}

+ handleDeleteOrganizer(organizer.name)} + /> +
+ ))} +
+ {organizerError && ( + {t("dialog.invite_error")} + )} + + { + await handleInputOnChange(e); + }} + /> + {organizerList && + organizerList.length > 0 && + isSearchListVisible && ( +
    + {organizerList.map((organizer) => ( +
  • handleAddOrganizers(organizer)} + onKeyDown={(e) => handleKeyDown(e, organizer)} + className="border-b px-3 py-1 hover:bg-gray-100" + > + {organizer.name} +
  • + ))} +
+ )} + + } + /> + + )} + {isBillingInfoAndPriceOverviewVisible && ( <> )} - {type !== IntegrationType.EntryApi && ( + {isBillingInfoAndPriceOverviewVisible && (
& { inputId?: string; }; -export const Input = ({ - children, - className, - iconBack, - disabled, - inputId, - ...props -}: Props) => { +const InputComponent = ( + { children, className, iconBack, disabled, inputId, ...props }: Props, + ref: ForwardedRef +) => { return (
{children} @@ -40,3 +37,5 @@ export const Input = ({
); }; + +export const Input = forwardRef(InputComponent); diff --git a/resources/ts/Components/Integrations/Detail/Credentials.tsx b/resources/ts/Components/Integrations/Detail/Credentials.tsx index 5429f3d29..98d40bd82 100644 --- a/resources/ts/Components/Integrations/Detail/Credentials.tsx +++ b/resources/ts/Components/Integrations/Detail/Credentials.tsx @@ -32,6 +32,7 @@ export const Credentials = ({ subscription, type, keyVisibility, + keyVisibilityUpgrade, legacyAuthConsumers, authClients, oldCredentialsExpirationDate, @@ -40,7 +41,9 @@ export const Credentials = ({ const hasAnyCredentials = Boolean( legacyAuthConsumers.length || authClients.length ); - usePolling(!hasAnyCredentials, { only: ["integration"] }); + const isV1Upgraded = + keyVisibility === KeyVisibility.v1 && !!keyVisibilityUpgrade; + usePolling(!hasAnyCredentials || isV1Upgraded, { only: ["integration"] }); const credentials = useMemo( () => ({ legacyTestConsumer: legacyAuthConsumers.find( @@ -96,6 +99,7 @@ export const Credentials = ({ type={type} subscription={subscription} keyVisibility={keyVisibility} + keyVisibilityUpgrade={keyVisibilityUpgrade} /> ); diff --git a/resources/ts/Components/Integrations/Detail/CredentialsAuthClients.tsx b/resources/ts/Components/Integrations/Detail/CredentialsAuthClients.tsx index 71905700c..d6d078ca8 100644 --- a/resources/ts/Components/Integrations/Detail/CredentialsAuthClients.tsx +++ b/resources/ts/Components/Integrations/Detail/CredentialsAuthClients.tsx @@ -11,10 +11,16 @@ import type { Integration } from "../../../types/Integration"; import { KeyVisibility } from "../../../types/KeyVisibility"; import { router } from "@inertiajs/react"; import { Link } from "../../Link"; +import { Alert } from "../../Alert"; type Props = Pick< Integration, - "id" | "status" | "subscription" | "type" | "keyVisibility" + | "id" + | "status" + | "subscription" + | "type" + | "keyVisibility" + | "keyVisibilityUpgrade" > & Credentials & { email: string }; @@ -27,8 +33,10 @@ export const CredentialsAuthClients = ({ subscription, type, keyVisibility, + keyVisibilityUpgrade, }: Props) => { const { t } = useTranslation(); + const isKeyVisibilityV1 = keyVisibility === KeyVisibility.v1; const auth0TestClientWithLabels = [ { @@ -62,27 +70,32 @@ export const CredentialsAuthClients = ({ {t("details.credentials.uitid_v2")} - {keyVisibility === KeyVisibility.v1 ? ( -
-
- , - ]} - /> + {isKeyVisibilityV1 ? ( + keyVisibilityUpgrade ? ( + {t("integrations.pending_credentials")} + ) : ( +
+
+ , + ]} + /> +
+ + + {t("details.credentials.action_uitid")} +
- - {t("details.credentials.action_uitid")} - -
+ ) ) : (
diff --git a/resources/ts/Pages/Integrations/New.tsx b/resources/ts/Pages/Integrations/New.tsx index 3bde64454..627e05507 100644 --- a/resources/ts/Pages/Integrations/New.tsx +++ b/resources/ts/Pages/Integrations/New.tsx @@ -1,6 +1,5 @@ import type { FormEvent, ReactNode } from "react"; -import React from "react"; -import { useState } from "react"; +import React, { useEffect, useState } from "react"; import { router, useForm } from "@inertiajs/react"; import Layout from "../../layouts/Layout"; import { Heading } from "../../Components/Heading"; @@ -33,12 +32,6 @@ const New = ({ subscriptions }: Props) => { const { t } = useTranslation(); const { i18n } = useTranslation(); - const freeSubscriptionId = subscriptions.find( - (subscription) => - subscription.integrationType === IntegrationType.EntryApi && - subscription.price === 0 - )?.id; - const basicSubscriptionIds = subscriptions .filter( (subscription) => subscription.category === SubscriptionCategory.Basic @@ -55,10 +48,7 @@ const New = ({ subscriptions }: Props) => { const initialFormValues = { integrationType: activeType, - subscriptionId: - activeType === IntegrationType.EntryApi && !!freeSubscriptionId - ? freeSubscriptionId - : "", + subscriptionId: "", integrationName: "", description: "", website: "", @@ -71,6 +61,7 @@ const New = ({ subscriptions }: Props) => { lastNameTechnicalContact: "", emailTechnicalContact: "", agreement: "", + uitpasAgreement: "", coupon: "", }; @@ -79,6 +70,17 @@ const New = ({ subscriptions }: Props) => { const { data, setData, errors, hasErrors, post, processing } = useForm(initialFormValues); + useEffect(() => { + const freeSubscriptionId = subscriptions.find( + (subscription) => + subscription.integrationType === activeType && + subscription.category === SubscriptionCategory.Free + )?.id; + + setData("subscriptionId", freeSubscriptionId ?? ""); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeType, subscriptions]); + function handleSubmit(e: FormEvent) { e.preventDefault(); post("/integrations", { @@ -379,6 +381,42 @@ const New = ({ subscriptions }: Props) => { } error={errors.agreement} /> + {activeType === IntegrationType.UiTPAS && ( + + ), + }} + /> + } + labelPosition="right" + labelSize="base" + labelWeight="normal" + component={ + + setData( + "uitpasAgreement", + data.uitpasAgreement === "true" ? "" : "true" + ) + } + /> + } + error={errors.uitpasAgreement} + /> + )} {isCouponFieldVisible && ( <> ; @@ -32,4 +37,5 @@ export type Integration = { authClients: AuthClient[]; legacyAuthConsumers: LegacyAuthConsumer[]; keyVisibility: KeyVisibility; + keyVisibilityUpgrade: KeyVisibilityUpgrade | null; }; diff --git a/resources/ts/types/UiTPASOrganizer.ts b/resources/ts/types/UiTPASOrganizer.ts new file mode 100644 index 000000000..449366547 --- /dev/null +++ b/resources/ts/types/UiTPASOrganizer.ts @@ -0,0 +1,4 @@ +export type UiTPASOrganizer = { + name: string; + id: string; +}; diff --git a/routes/web.php b/routes/web.php index b831e7478..582cec288 100644 --- a/routes/web.php +++ b/routes/web.php @@ -6,6 +6,7 @@ use App\Domain\Auth\Controllers\LoginController; use App\Domain\Auth\Controllers\LogoutController; use App\Domain\Integrations\Controllers\IntegrationController; +use App\Domain\Integrations\Controllers\OrganizerController; use App\Domain\Newsletter\Controllers\NewsletterController; use Illuminate\Support\Facades\Route; use App\Router\TranslatedRoute; @@ -71,6 +72,8 @@ Route::post('/integrations', [IntegrationController::class, 'store']); + Route::get('/organizers', [OrganizerController::class, 'index']); + Route::group(['middleware' => 'can:access-integration,id'], static function () { TranslatedRoute::get( [ diff --git a/tests/Keycloak/Converters/IntegrationToKeycloakClientConverterTest.php b/tests/Keycloak/Converters/IntegrationToKeycloakClientConverterTest.php index b3948b906..67e2a3dd2 100644 --- a/tests/Keycloak/Converters/IntegrationToKeycloakClientConverterTest.php +++ b/tests/Keycloak/Converters/IntegrationToKeycloakClientConverterTest.php @@ -83,7 +83,7 @@ public function test_combining_keycloak_convert_with_configured_uris(): void $this->assertEquals([ 'origin' => 'publiq-platform', 'use.refresh.tokens' => true, - 'post.logout.redirect.uris' => 'https://example.com/logout1#https://example.com/logout2', + 'post.logout.redirect.uris' => 'https://example.com/logout1##https://example.com/logout2', ], $convertedData['attributes']); $this->assertEquals('https://example.com/login1', $convertedData['baseUrl']); $this->assertEquals(['https://example.com/callback1', 'https://example.com/callback2'], $convertedData['redirectUris']); diff --git a/tests/Keycloak/Converters/IntegrationUrlConverterTest.php b/tests/Keycloak/Converters/IntegrationUrlConverterTest.php index ba92aabb5..d4843b632 100644 --- a/tests/Keycloak/Converters/IntegrationUrlConverterTest.php +++ b/tests/Keycloak/Converters/IntegrationUrlConverterTest.php @@ -99,7 +99,7 @@ public function test_convert_for_first_party_logout_urls(): void new IntegrationUrl(Uuid::uuid4(), $integration->id, Environment::Production, IntegrationUrlType::Logout, 'https://wrong.com/'), ); $this->assertSame( - 'https://example.com/logout1#https://example.com/logout2', + 'https://example.com/logout1##https://example.com/logout2', IntegrationUrlConverter::buildLogoutUrls($integration, $this->client->environment) ); }