diff --git a/.ddev/config.yaml b/.ddev/config.yaml new file mode 100644 index 0000000..9a1c5af --- /dev/null +++ b/.ddev/config.yaml @@ -0,0 +1,258 @@ +name: myep-team +type: php +docroot: public +php_version: "8.1" +webserver_type: apache-fpm +router_http_port: "80" +router_https_port: "443" +xdebug_enabled: false +additional_hostnames: [] +additional_fqdns: [] +database: + type: mariadb + version: "10.4" +nfs_mount_enabled: false +mutagen_enabled: false +use_dns_when_possible: true +composer_version: "2" +web_environment: [] +nodejs_version: "16" +omit_containers: [dba] + +# Key features of ddev's config.yaml: + +# name: # Name of the project, automatically provides +# http://projectname.ddev.site and https://projectname.ddev.site + +# type: # drupal6/7/8, backdrop, typo3, wordpress, php + +# docroot: # Relative path to the directory containing index.php. + +# php_version: "7.4" # PHP version to use, "5.6", "7.0", "7.1", "7.2", "7.3", "7.4", "8.0", "8.1", "8.2" + +# You can explicitly specify the webimage but this +# is not recommended, as the images are often closely tied to ddev's' behavior, +# so this can break upgrades. + +# webimage: # nginx/php docker image. + +# database: +# type: # mysql, mariadb +# version: # database version, like "10.3" or "8.0" +# Note that mariadb_version or mysql_version from v1.18 and earlier +# will automatically be converted to this notation with just a "ddev config --auto" + +# router_http_port: # Port to be used for http (defaults to port 80) +# router_https_port: # Port for https (defaults to 443) + +# xdebug_enabled: false # Set to true to enable xdebug and "ddev start" or "ddev restart" +# Note that for most people the commands +# "ddev xdebug" to enable xdebug and "ddev xdebug off" to disable it work better, +# as leaving xdebug enabled all the time is a big performance hit. + +# xhprof_enabled: false # Set to true to enable xhprof and "ddev start" or "ddev restart" +# Note that for most people the commands +# "ddev xhprof" to enable xhprof and "ddev xhprof off" to disable it work better, +# as leaving xhprof enabled all the time is a big performance hit. + +# webserver_type: nginx-fpm # or apache-fpm + +# timezone: Europe/Berlin +# This is the timezone used in the containers and by PHP; +# it can be set to any valid timezone, +# see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# For example Europe/Dublin or MST7MDT + +# composer_root: +# Relative path to the composer root directory from the project root. This is +# the directory which contains the composer.json and where all Composer related +# commands are executed. + +# composer_version: "2" +# You can set it to "" or "2" (default) for Composer v2 or "1" for Composer v1 +# to use the latest major version available at the time your container is built. +# It is also possible to use each other Composer version channel. This includes: +# - 2.2 (latest Composer LTS version) +# - stable +# - preview +# - snapshot +# Alternatively, an explicit Composer version may be specified, for example "2.2.18". +# To reinstall Composer after the image was built, run "ddev debug refresh". + +# nodejs_version: "16" +# change from the default system Node.js version to another supported version, like 12, 14, 17, 18. +# Note that you can use 'ddev nvm' or nvm inside the web container to provide nearly any +# Node.js version, including v6, etc. + +# additional_hostnames: +# - somename +# - someothername +# would provide http and https URLs for "somename.ddev.site" +# and "someothername.ddev.site". + +# additional_fqdns: +# - example.com +# - sub1.example.com +# would provide http and https URLs for "example.com" and "sub1.example.com" +# Please take care with this because it can cause great confusion. + +# upload_dir: custom/upload/dir +# would set the destination path for ddev import-files to /custom/upload/dir +# When mutagen is enabled this path is bind-mounted so that all the files +# in the upload_dir don't have to be synced into mutagen + +# working_dir: +# web: /var/www/html +# db: /home +# would set the default working directory for the web and db services. +# These values specify the destination directory for ddev ssh and the +# directory in which commands passed into ddev exec are run. + +# omit_containers: [db, dba, ddev-ssh-agent] +# Currently only these containers are supported. Some containers can also be +# omitted globally in the ~/.ddev/global_config.yaml. Note that if you omit +# the "db" container, several standard features of ddev that access the +# database container will be unusable. In the global configuration it is also +# possible to omit ddev-router, but not here. + +# nfs_mount_enabled: false +# Great performance improvement but requires host configuration first. +# See https://ddev.readthedocs.io/en/latest/users/install/performance/#nfs + +# mutagen_enabled: false +# Performance improvement using mutagen asynchronous updates. +# See https://ddev.readthedocs.io/en/latest/users/install/performance/#mutagen + +# fail_on_hook_fail: False +# Decide whether 'ddev start' should be interrupted by a failing hook + +# host_https_port: "59002" +# The host port binding for https can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_webserver_port: "59001" +# The host port binding for the ddev-webserver can be explicitly specified. It is +# dynamic unless otherwise specified. +# This is not used by most people, most people use the *router* instead +# of the localhost port. + +# host_db_port: "59002" +# The host port binding for the ddev-dbserver can be explicitly specified. It is dynamic +# unless explicitly specified. + +# phpmyadmin_port: "8036" +# phpmyadmin_https_port: "8037" +# The PHPMyAdmin ports can be changed from the default 8036 and 8037 + +# host_phpmyadmin_port: "8036" +# The phpmyadmin (dba) port is not normally bound on the host at all, instead being routed +# through ddev-router, but it can be specified and bound. + +# mailhog_port: "8025" +# mailhog_https_port: "8026" +# The MailHog ports can be changed from the default 8025 and 8026 + +# host_mailhog_port: "8025" +# The mailhog port is not normally bound on the host at all, instead being routed +# through ddev-router, but it can be bound directly to localhost if specified here. + +# webimage_extra_packages: [php7.4-tidy, php-bcmath] +# Extra Debian packages that are needed in the webimage can be added here + +# dbimage_extra_packages: [telnet,netcat] +# Extra Debian packages that are needed in the dbimage can be added here + +# use_dns_when_possible: true +# If the host has internet access and the domain configured can +# successfully be looked up, DNS will be used for hostname resolution +# instead of editing /etc/hosts +# Defaults to true + +# project_tld: ddev.site +# The top-level domain used for project URLs +# The default "ddev.site" allows DNS lookup via a wildcard +# If you prefer you can change this to "ddev.local" to preserve +# pre-v1.9 behavior. + +# ngrok_args: --basic-auth username:pass1234 +# Provide extra flags to the "ngrok http" command, see +# https://ngrok.com/docs#http or run "ngrok http -h" + +# disable_settings_management: false +# If true, ddev will not create CMS-specific settings files like +# Drupal's settings.php/settings.ddev.php or TYPO3's AdditionalConfiguration.php +# In this case the user must provide all such settings. + +# You can inject environment variables into the web container with: +# web_environment: +# - SOMEENV=somevalue +# - SOMEOTHERENV=someothervalue + +# no_project_mount: false +# (Experimental) If true, ddev will not mount the project into the web container; +# the user is responsible for mounting it manually or via a script. +# This is to enable experimentation with alternate file mounting strategies. +# For advanced users only! + +# bind_all_interfaces: false +# If true, host ports will be bound on all network interfaces, +# not just the localhost interface. This means that ports +# will be available on the local network if the host firewall +# allows it. + +# default_container_timeout: 120 +# The default time that ddev waits for all containers to become ready can be increased from +# the default 120. This helps in importing huge databases, for example. + +#web_extra_exposed_ports: +#- name: nodejs +# container_port: 3000 +# http_port: 2999 +# https_port: 3000 +#- name: something +# container_port: 4000 +# https_port: 4000 +# http_port: 3999 +# Allows a set of extra ports to be exposed via ddev-router +# The port behavior on the ddev-webserver must be arranged separately, for example +# using web_extra_daemons. +# For example, with a web app on port 3000 inside the container, this config would +# expose that web app on https://.ddev.site:9999 and http://.ddev.site:9998 +# web_extra_exposed_ports: +# - container_port: 3000 +# http_port: 9998 +# https_port: 9999 + +#web_extra_daemons: +#- name: "http-1" +# command: "/var/www/html/node_modules/.bin/http-server -p 3000" +# directory: /var/www/html +#- name: "http-2" +# command: "/var/www/html/node_modules/.bin/http-server /var/www/html/sub -p 3000" +# directory: /var/www/html + +# override_config: false +# By default, config.*.yaml files are *merged* into the configuration +# But this means that some things can't be overridden +# For example, if you have 'nfs_mount_enabled: true'' you can't override it with a merge +# and you can't erase existing hooks or all environment variables. +# However, with "override_config: true" in a particular config.*.yaml file, +# 'nfs_mount_enabled: false' can override the existing values, and +# hooks: +# post-start: [] +# or +# web_environment: [] +# or +# additional_hostnames: [] +# can have their intended affect. 'override_config' affects only behavior of the +# config.*.yaml file it exists in. + +# Many ddev commands can be extended to run tasks before or after the +# ddev command is executed, for example "post-start", "post-import-db", +# "pre-composer", "post-composer" +# See https://ddev.readthedocs.io/en/stable/users/extend/custom-commands/ for more +# information on the commands that can be extended and the tasks you can define +# for them. Example: +#hooks: diff --git a/.env b/.env index cfa2d45..6ff6389 100644 --- a/.env +++ b/.env @@ -39,3 +39,7 @@ MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0 ###> symfony/mailer ### # MAILER_DSN=null://null ###< symfony/mailer ### + +APP_BPN_USER= +APP_BPN_PASSWORD= +APP_BPN_ENDPOINT= diff --git a/Beschreibung XMLAnfrage.pdf b/Beschreibung XMLAnfrage.pdf new file mode 100644 index 0000000..811ae4e Binary files /dev/null and b/Beschreibung XMLAnfrage.pdf differ diff --git a/buspronet.http b/buspronet.http new file mode 100644 index 0000000..df27824 --- /dev/null +++ b/buspronet.http @@ -0,0 +1,2 @@ +https://www.ep-reisen.de/buchung/xmlanfrage.php? + operation=Kuschick1a1c8a7235369a0ea2313444c84c5375Adressdatenmail@fromme.orged0ac03a0f0f1eeb11249cc9edbab6cb \ No newline at end of file diff --git a/composer.json b/composer.json index 1db4b95..ceb587a 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/orm": "^2.15", "phpdocumentor/reflection-docblock": "^5.3", "phpstan/phpdoc-parser": "^1.22", + "symfony/apache-pack": "^1.0", "symfony/asset": "6.3.*", "symfony/console": "6.3.*", "symfony/doctrine-messenger": "6.3.*", @@ -85,7 +86,7 @@ }, "extra": { "symfony": { - "allow-contrib": false, + "allow-contrib": true, "require": "6.3.*" } }, diff --git a/composer.lock b/composer.lock index 1fcee7e..fa403d1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5625360c4bf0ca4c404a3a47e7b43354", + "content-hash": "321e741fbc0979c63e04752cd30aba64", "packages": [ { "name": "doctrine/cache", @@ -2008,6 +2008,32 @@ }, "time": "2021-07-14T16:46:02+00:00" }, + { + "name": "symfony/apache-pack", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/apache-pack.git", + "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/apache-pack/zipball/3aa5818d73ad2551281fc58a75afd9ca82622e6c", + "reference": "3aa5818d73ad2551281fc58a75afd9ca82622e6c", + "shasum": "" + }, + "type": "symfony-pack", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A pack for Apache support in Symfony", + "support": { + "issues": "https://github.com/symfony/apache-pack/issues", + "source": "https://github.com/symfony/apache-pack/tree/master" + }, + "time": "2017-12-12T01:46:35+00:00" + }, { "name": "symfony/asset", "version": "v6.3.0", diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 367af25..54747e3 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,37 +1,28 @@ security: - # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' - # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider + providers: - users_in_memory: { memory: null } + bpn_user_provider: + entity: + class: App\Entity\User + property: email + firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true - provider: users_in_memory - - # activate different ways to authenticate - # https://symfony.com/doc/current/security.html#the-firewall - - # https://symfony.com/doc/current/security/impersonating_user.html - # switch_user: true - - # Easy way to control access for large sections of your site - # Note: Only the *first* access control that matches will be used + provider: bpn_user_provider + custom_authenticators: + - App\Security\BpnAuthenticator access_control: - # - { path: ^/admin, roles: ROLE_ADMIN } - # - { path: ^/profile, roles: ROLE_USER } + - { path: ^/profile, roles: ROLE_USER } when@test: security: password_hashers: - # By default, password hashers are resource intensive and take time. This is - # important to generate secure password hashes. In tests however, secure hashes - # are not important, waste resources and increase test times. The following - # reduces the work factor to the lowest possible values. Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: algorithm: auto cost: 4 # Lowest possible value for bcrypt diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..bf03a27 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -1,9 +1,7 @@ -# This file is the entry point to configure your own services. -# Files in the packages/ subdirectory configure your dependencies. - -# Put parameters here that don't need to change on each machine where the app is deployed -# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + bpn_url: '%env(APP_BPN_ENDPOINT)%' + bpn_username: '%env(APP_BPN_USER)%' + bpn_password: '%env(APP_BPN_PASSWORD)%' services: # default configuration for services in *this* file @@ -20,5 +18,6 @@ services: - '../src/Entity/' - '../src/Kernel.php' - # add more service definitions when explicit configuration is needed - # please note that last definitions always *replace* previous ones + App\BusProNet\ApiClient: + arguments: + $options: { 'bpn_username': '%bpn_username%', 'bpn_password': '%bpn_password%', 'bpn_url': '%bpn_url%' } \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml deleted file mode 100644 index f4f0a6c..0000000 --- a/docker-compose.override.yml +++ /dev/null @@ -1,14 +0,0 @@ -version: '3' - -services: -###> doctrine/doctrine-bundle ### - database: - ports: - - "5432" -###< doctrine/doctrine-bundle ### - -###> symfony/mailer ### - mailer: - image: schickling/mailcatcher - ports: ["1025", "1080"] -###< symfony/mailer ### diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 1067b9c..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,21 +0,0 @@ -version: '3' - -services: -###> doctrine/doctrine-bundle ### - database: - image: postgres:${POSTGRES_VERSION:-15}-alpine - environment: - POSTGRES_DB: ${POSTGRES_DB:-app} - # You should definitely change the password in production - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!} - POSTGRES_USER: ${POSTGRES_USER:-app} - volumes: - - database_data:/var/lib/postgresql/data:rw - # You may use a bind-mounted host directory instead, so that it is harder to accidentally remove the volume and lose all your data! - # - ./docker/db/data:/var/lib/postgresql/data:rw -###< doctrine/doctrine-bundle ### - -volumes: -###> doctrine/doctrine-bundle ### - database_data: -###< doctrine/doctrine-bundle ### diff --git a/migrations/Version20230707115553.php b/migrations/Version20230707115553.php new file mode 100644 index 0000000..92a7712 --- /dev/null +++ b/migrations/Version20230707115553.php @@ -0,0 +1,33 @@ +addSql('CREATE TABLE user (id INT AUTO_INCREMENT NOT NULL, bus_pro_address_id INT NOT NULL, email VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL, last_name VARCHAR(255) NOT NULL, role VARCHAR(255) NOT NULL, last_login_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + $this->addSql('CREATE TABLE messenger_messages (id BIGINT AUTO_INCREMENT NOT NULL, body LONGTEXT NOT NULL, headers LONGTEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', available_at DATETIME NOT NULL COMMENT \'(DC2Type:datetime_immutable)\', delivered_at DATETIME DEFAULT NULL COMMENT \'(DC2Type:datetime_immutable)\', INDEX IDX_75EA56E0FB7336F0 (queue_name), INDEX IDX_75EA56E0E3BD61CE (available_at), INDEX IDX_75EA56E016BA31DB (delivered_at), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE user'); + $this->addSql('DROP TABLE messenger_messages'); + } +} diff --git a/migrations/Version20230707135208.php b/migrations/Version20230707135208.php new file mode 100644 index 0000000..b02614e --- /dev/null +++ b/migrations/Version20230707135208.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE user ADD bus_pro_person_id INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE user DROP bus_pro_person_id'); + } +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..3853fa9 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,70 @@ +# Use the front controller as index file. It serves as a fallback solution when +# every other rewrite/redirect fails (e.g. in an aliased environment without +# mod_rewrite). Additionally, this reduces the matching process for the +# start page (path "/") because otherwise Apache will apply the rewriting rules +# to each configured DirectoryIndex file (e.g. index.php, index.html, index.pl). +DirectoryIndex index.php + +# By default, Apache does not evaluate symbolic links if you did not enable this +# feature in your server configuration. Uncomment the following line if you +# install assets as symlinks or if you experience problems related to symlinks +# when compiling LESS/Sass/CoffeScript assets. +# Options +FollowSymlinks + +# Disabling MultiViews prevents unwanted negotiation, e.g. "/index" should not resolve +# to the front controller "/index.php" but be rewritten to "/index.php/index". + + Options -MultiViews + + + + # This Option needs to be enabled for RewriteRule, otherwise it will show an error like + # 'Options FollowSymLinks or SymLinksIfOwnerMatch is off which implies that RewriteRule directive is forbidden' + Options +FollowSymlinks + + RewriteEngine On + + # Determine the RewriteBase automatically and set it as environment variable. + # If you are using Apache aliases to do mass virtual hosting or installed the + # project in a subdirectory, the base path will be prepended to allow proper + # resolution of the index.php file and to redirect to the correct URI. It will + # work in environments without path prefix as well, providing a safe, one-size + # fits all solution. But as you do not need it in this case, you can comment + # the following 2 lines to eliminate the overhead. + RewriteCond %{REQUEST_URI}::$0 ^(/.+)/(.*)::\2$ + RewriteRule .* - [E=BASE:%1] + + # Sets the HTTP_AUTHORIZATION header removed by Apache + RewriteCond %{HTTP:Authorization} .+ + RewriteRule ^ - [E=HTTP_AUTHORIZATION:%0] + + # Redirect to URI without front controller to prevent duplicate content + # (with and without `/index.php`). Only do this redirect on the initial + # rewrite by Apache and not on subsequent cycles. Otherwise we would get an + # endless redirect loop (request -> rewrite to front controller -> + # redirect -> request -> ...). + # So in case you get a "too many redirects" error or you always get redirected + # to the start page because your Apache does not expose the REDIRECT_STATUS + # environment variable, you have 2 choices: + # - disable this feature by commenting the following 2 lines or + # - use Apache >= 2.3.9 and replace all L flags by END flags and remove the + # following RewriteCond (best solution) + RewriteCond %{ENV:REDIRECT_STATUS} ="" + RewriteRule ^index\.php(?:/(.*)|$) %{ENV:BASE}/$1 [R=301,L] + + # If the requested filename exists, simply serve it. + # We only want to let Apache serve files and not directories. + # Rewrite all other queries to the front controller. + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ %{ENV:BASE}/index.php [L] + + + + + # When mod_rewrite is not available, we instruct a temporary redirect of + # the start page to the front controller explicitly so that the website + # and the generated links can still be used. + RedirectMatch 307 ^/$ /index.php/ + # RedirectTemp cannot be used instead + + diff --git a/src/BusProNet/ApiClient.php b/src/BusProNet/ApiClient.php new file mode 100644 index 0000000..048fe38 --- /dev/null +++ b/src/BusProNet/ApiClient.php @@ -0,0 +1,131 @@ +config = $this->resolveOptions($options); + } + + /** + * @throws ApiClientException + */ + public function getProfile(string $email, string $password): mixed + { + $data = [ + 'anfrage' => [ + 'user' => $this->config['bpn_username'], + 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], 'KUNDENKONTO'), + 'satz' => ['@typ' => 'KUNDENKONTO'], + 'art' => 'Adressdaten', + 'email' => $email, + 'passwort' => md5($password), + ], + ]; + + $body = $this + ->serializer + ->serialize($data, 'xml') + ; + + try { + $response = $this->httpClient->request('GET', $this->config['bpn_url'], [ + 'query' => [ + 'operation' => $body, + ] + ]); + + $xml = $response->getContent(); + + if (str_contains($xml, 'HINWEIS')) { + return $this->serializer->deserialize($xml, ErrorResponse::class, 'xml'); + } + + return $this->serializer->deserialize($xml, Profile::class, 'xml'); + } catch (\Throwable $e) { + } + + throw new ApiClientException($e->getMessage()); + } + + public function updateProfile(): void + {} + + /** + * @throws ApiClientException + */ + public function getCrmSelection(string $email, string $password): mixed + { + $data = [ + 'anfrage' => [ + 'user' => $this->config['bpn_username'], + 'key' => $this->createKey($this->config['bpn_username'], $this->config['bpn_password'], 'KUNDENKONTO'), + 'satz' => ['@typ' => 'KUNDENKONTO'], + 'art' => 'SelektionCRM', + 'email' => $email, + 'passwort' => md5($password), + ], + ]; + + $body = $this + ->serializer + ->serialize($data, 'xml') + ; + + try { + $response = $this->httpClient->request('GET', $this->config['bpn_url'], [ + 'query' => [ + 'operation' => $body, + ] + ]); + + $xml = $response->getContent(); + + if (str_contains($xml, 'HINWEIS')) { + return $this->serializer->deserialize($xml, ErrorResponse::class, 'xml'); + } + + return $this->serializer->deserialize($xml, CrmAttributeSelection::class, 'xml'); + } catch (\Throwable $e) { + } + + throw new ApiClientException($e->getMessage()); + } + + /** + * @Creates key for BusPro API access according to documentation + */ + private function createKey(string $username, string $password, string $type): string + { + $date = (new \DateTimeImmutable())->format('Ymd'); + + return md5($username.$password.$date.$type); + } + + private function resolveOptions(array $options): array + { + $optionsResolver = new OptionsResolver(); + $optionsResolver->setRequired(['bpn_url', 'bpn_username', 'bpn_password']); + + return $optionsResolver->resolve($options); + } +} \ No newline at end of file diff --git a/src/BusProNet/ApiClientException.php b/src/BusProNet/ApiClientException.php new file mode 100644 index 0000000..1015460 --- /dev/null +++ b/src/BusProNet/ApiClientException.php @@ -0,0 +1,7 @@ +street; + } + + public function setStreet(?string $street): static + { + $this->street = $street; + + return $this; + } + + public function getPostCode(): ?string + { + return $this->postCode; + } + + public function setPostCode(?string $postCode): static + { + $this->postCode = $postCode; + + return $this; + } + + public function getCity(): ?string + { + return $this->city; + } + + public function setCity(?string $city): static + { + $this->city = $city; + + return $this; + } + + public function getCountry(): ?string + { + return $this->country; + } + + public function setCountry(?string $country): static + { + $this->country = $country; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/Communication.php b/src/BusProNet/Model/Communication.php new file mode 100644 index 0000000..8193d30 --- /dev/null +++ b/src/BusProNet/Model/Communication.php @@ -0,0 +1,53 @@ +phone; + } + + public function setPhone(?string $phone): static + { + $this->phone = $phone; + + return $this; + } + + public function getMobile(): ?string + { + return $this->mobile; + } + + public function setMobile(?string $mobile): static + { + $this->mobile = $mobile; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(?string $email): static + { + $this->email = $email; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/CrmAttribute.php b/src/BusProNet/Model/CrmAttribute.php new file mode 100644 index 0000000..1559c4b --- /dev/null +++ b/src/BusProNet/Model/CrmAttribute.php @@ -0,0 +1,66 @@ +id; + } + + public function setId(?int $id): static + { + $this->id = $id; + + return $this; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(?string $label): static + { + $this->label = $label; + + return $this; + } + + public function getSelectedAsString(): ?string + { + return $this->selectedAsString; + } + + public function setSelectedAsString(?string $selectedAsString): static + { + $this->selectedAsString = $selectedAsString; + + return $this; + } + + public function isSelected(): bool + { + return 'true' === strtolower($this->selectedAsString); + } + + public function setSelected(bool $selected): static + { + $this->selectedAsString = $selected ? 'True' : 'False'; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/CrmAttributeSelection.php b/src/BusProNet/Model/CrmAttributeSelection.php new file mode 100644 index 0000000..8fbd199 --- /dev/null +++ b/src/BusProNet/Model/CrmAttributeSelection.php @@ -0,0 +1,30 @@ +selectionGroups; + } + + /** + * @param CrmAttributeSelectionGroup[]|null $selectionGroups + * @return $this + */ + public function setSelectionGroups(?array $selectionGroups): static + { + $this->selectionGroups = $selectionGroups; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/CrmAttributeSelectionGroup.php b/src/BusProNet/Model/CrmAttributeSelectionGroup.php new file mode 100644 index 0000000..5f054bb --- /dev/null +++ b/src/BusProNet/Model/CrmAttributeSelectionGroup.php @@ -0,0 +1,45 @@ +label; + } + + public function setLabel(?string $label): static + { + $this->label = $label; + + return $this; + } + + /** + * @return CrmAttribute[]|null + */ + public function getAttributes(): ?array + { + return $this->attributes; + } + + /** + * @param CrmAttribute[]|null $attributes + * @return $this + */ + public function setAttributes(?array $attributes): static + { + $this->attributes = $attributes; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/ErrorResponse.php b/src/BusProNet/Model/ErrorResponse.php new file mode 100644 index 0000000..14c5f00 --- /dev/null +++ b/src/BusProNet/Model/ErrorResponse.php @@ -0,0 +1,38 @@ +code; + } + + public function setCode(?string $code): static + { + $this->code = $code; + + return $this; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(?string $type): static + { + $this->type = $type; + + return $this; + } +} \ No newline at end of file diff --git a/src/BusProNet/Model/Profile.php b/src/BusProNet/Model/Profile.php new file mode 100644 index 0000000..0cb966e --- /dev/null +++ b/src/BusProNet/Model/Profile.php @@ -0,0 +1,161 @@ + 'd.m.Y'])] + private ?\DateTimeImmutable $dateOfBirth = null; + + #[SerializedPath('[adressdaten][anschrift]')] + private ?Address $address = null; + + #[SerializedPath('[adressdaten][kommunikation]')] + private ?Communication $communication = null; + + public function getAddressId(): ?int + { + return $this->addressId; + } + + public function setAddressId(?int $addressId): static + { + $this->addressId = $addressId; + + return $this; + } + + public function getPersonId(): ?int + { + return $this->personId; + } + + public function setPersonId(?int $personId): static + { + $this->personId = $personId; + + return $this; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(?string $name): static + { + $this->name = $name; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(?string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getSalutation(): ?string + { + return $this->salutation; + } + + public function setSalutation(?string $salutation): static + { + $this->salutation = $salutation; + + return $this; + } + + public function getTitle(): ?string + { + return $this->title; + } + + public function setTitle(?string $title): static + { + $this->title = $title; + + return $this; + } + + public function getGender(): ?string + { + return $this->gender; + } + + public function setGender(?string $gender): static + { + $this->gender = $gender; + + return $this; + } + + public function getDateOfBirth(): \DateTimeImmutable + { + return $this->dateOfBirth; + } + + public function setDateOfBirth(?\DateTimeImmutable $dateOfBirth): static + { + $this->dateOfBirth = $dateOfBirth; + + return $this; + } + + public function getAddress(): ?Address + { + return $this->address; + } + + public function setAddress(?Address $address): static + { + $this->address = $address; + + return $this; + } + + public function getCommunication(): ?Communication + { + return $this->communication; + } + + public function setCommunication(?Communication $communication): static + { + $this->communication = $communication; + + return $this; + } +} \ No newline at end of file diff --git a/src/Controller/Security/LoginController.php b/src/Controller/Security/LoginController.php new file mode 100644 index 0000000..a0cf172 --- /dev/null +++ b/src/Controller/Security/LoginController.php @@ -0,0 +1,26 @@ +getLastAuthenticationError(); + + // last username entered by the user + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('security/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } +} \ No newline at end of file diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..058858d --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,146 @@ +id; + } + + public function getBusProAddressId(): ?int + { + return $this->busProAddressId; + } + + public function setBusProAddressId(int $busProAddressId): static + { + $this->busProAddressId = $busProAddressId; + + return $this; + } + + public function getBusProPersonId(): ?int + { + return $this->busProPersonId; + } + + public function setBusProPersonId(int $busProPersonId): static + { + $this->busProPersonId = $busProPersonId; + + return $this; + } + + public function getEmail(): ?string + { + return $this->email; + } + + public function setEmail(string $email): static + { + $this->email = $email; + + return $this; + } + + public function getFirstName(): ?string + { + return $this->firstName; + } + + public function setFirstName(string $firstName): static + { + $this->firstName = $firstName; + + return $this; + } + + public function getLastName(): ?string + { + return $this->lastName; + } + + public function setLastName(string $lastName): static + { + $this->lastName = $lastName; + + return $this; + } + + public function getRole(): ?string + { + return $this->role; + } + + public function setRole(string $role): static + { + $this->role = $role; + + return $this; + } + + public function getLastLoginAt(): ?\DateTimeImmutable + { + return $this->lastLoginAt; + } + + public function setLastLoginAt(?\DateTimeImmutable $lastLoginAt): static + { + $this->lastLoginAt = $lastLoginAt; + + return $this; + } + + public function getDefaultRoute(): string + { + return 'app_index'; + } + + public function getRoles(): array + { + return ['ROLE_USER']; + } + + public function eraseCredentials(): void + { + } + + public function getUserIdentifier(): string + { + return $this->email; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..d1f2be2 --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,66 @@ + + * + * @method User|null find($id, $lockMode = null, $lockVersion = null) + * @method User|null findOneBy(array $criteria, array $orderBy = null) + * @method User[] findAll() + * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } + + public function save(User $entity, bool $flush = false): void + { + $this->getEntityManager()->persist($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + + public function remove(User $entity, bool $flush = false): void + { + $this->getEntityManager()->remove($entity); + + if ($flush) { + $this->getEntityManager()->flush(); + } + } + +// /** +// * @return User[] Returns an array of User objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('u.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?User +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Security/BpnAuthenticator.php b/src/Security/BpnAuthenticator.php new file mode 100644 index 0000000..a437164 --- /dev/null +++ b/src/Security/BpnAuthenticator.php @@ -0,0 +1,117 @@ +urlGenerator->generate('app_security_login'); + } + + public function authenticate(Request $request): Passport + { + $email = trim($request->request->get('_username', '')); + $password = trim($request->request->get('_password', '')); + $csrfToken = $request->request->get('_csrf_token', ''); + + return new SelfValidatingPassport( + new UserBadge($email, function () use ($email, $password) { + try { + $response = $this->apiClient->getProfile($email, $password); + } catch (ApiClientException $e) { + return null; + } + + if ($response instanceof ErrorResponse) { + return null; + } + + return $this->getOrCreateLocalUser($response, $email, $password); + }), + [new CsrfTokenBadge('authenticate', $csrfToken)] + ); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response + { + /** @var User $user */ + $user = $token->getUser(); + + $user->setLastLoginAt(new \DateTimeImmutable()); + + $this->entityManager->flush(); + + if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) { + return new RedirectResponse($targetPath); + } + + $url = $this->urlGenerator->generate($user->getDefaultRoute()); + + return new RedirectResponse($url); + } + + private function getOrCreateLocalUser(Profile $profile, string $email, string $password): User + { + $repository = $this->entityManager->getRepository(User::class); + + $user = $repository->findOneBy([ + 'busProAddressId' => $profile->getAddressId(), + 'busProPersonId' => $profile->getPersonId(), + ]); + + if (null !== $user) { + return $user; + } + + try { + $response = $this->apiClient->getCrmSelection($email, $password); + } catch (ApiClientException $e) { + } + + $user = new User(); + $user + ->setBusProAddressId($profile->getAddressId()) + ->setBusProPersonId($profile->getPersonId()) + ->setRole('ROLE_FOO') + ->setEmail($profile->getCommunication()->getEmail()) + ->setFirstName($profile->getFirstName()) + ->setLastName($profile->getName()) + ; + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $user; + } +} diff --git a/symfony.lock b/symfony.lock index 00d2268..d020c1b 100644 --- a/symfony.lock +++ b/symfony.lock @@ -40,6 +40,18 @@ "tests/bootstrap.php" ] }, + "symfony/apache-pack": { + "version": "1.0", + "recipe": { + "repo": "github.com/symfony/recipes-contrib", + "branch": "main", + "version": "1.0", + "ref": "efb318193e48384eb5c5aadff15396ed698f8ffc" + }, + "files": [ + "public/.htaccess" + ] + }, "symfony/console": { "version": "6.3", "recipe": { diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..9501342 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,19 @@ +{% extends 'base.html.twig' %} + +{% block body %} + {% if error %} +
{{ error.messageKey|trans(error.messageData, 'security') }}
+ {% endif %} + +
+ + + + + + + + + +
+{% endblock %} \ No newline at end of file