This commit is contained in:
Björn Fromme
2023-07-07 18:19:15 +02:00
parent 1e9d1c5ac4
commit 305699b1ac
28 changed files with 1428 additions and 63 deletions
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace App\BusProNet;
use App\BusProNet\Model\CrmAttributeSelection;
use App\BusProNet\Model\Profile;
use App\BusProNet\Model\ErrorResponse;
use App\BusProNet\Model\Result;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ApiClient
{
private array $config;
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly SerializerInterface $serializer,
array $options
) {
$this->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);
}
}
+7
View File
@@ -0,0 +1,7 @@
<?php
namespace App\BusProNet;
class ApiClientException extends \Exception
{
}
+68
View File
@@ -0,0 +1,68 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\SerializedName;
class Address
{
#[SerializedName('strasse')]
private ?string $street = null;
#[SerializedName('plz')]
private ?string $postCode = null;
#[SerializedName('ort')]
private ?string $city = null;
#[SerializedName('land')]
private ?string $country = null;
public function getStreet(): ?string
{
return $this->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;
}
}
+53
View File
@@ -0,0 +1,53 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\SerializedName;
class Communication
{
#[SerializedName('telefonprivat')]
private ?string $phone = null;
#[SerializedName('telefonmobil')]
private ?string $mobile = null;
#[SerializedName('email')]
private ?string $email = null;
public function getPhone(): ?string
{
return $this->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;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\Ignore;
use Symfony\Component\Serializer\Annotation\SerializedName;
class CrmAttribute
{
#[SerializedName('@id')]
private ?int $id = null;
#[SerializedName('@bezeichnung')]
private ?string $label = null;
#[SerializedName('@auswahl')]
private ?string $selectedAsString = null;
public function getId(): ?int
{
return $this->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;
}
}
@@ -0,0 +1,30 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\SerializedPath;
class CrmAttributeSelection
{
#[SerializedPath('[selektionsmerkmale][selektionsgruppe]')]
private ?array $selectionGroups = null;
/**
* @return CrmAttributeSelectionGroup[]|null
*/
public function getSelectionGroups(): ?array
{
return $this->selectionGroups;
}
/**
* @param CrmAttributeSelectionGroup[]|null $selectionGroups
* @return $this
*/
public function setSelectionGroups(?array $selectionGroups): static
{
$this->selectionGroups = $selectionGroups;
return $this;
}
}
@@ -0,0 +1,45 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\SerializedName;
class CrmAttributeSelectionGroup
{
#[SerializedName('@bezeichnung')]
private ?string $label = null;
#[SerializedName('selektion')]
private ?array $attributes = null;
public function getLabel(): ?string
{
return $this->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;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\SerializedPath;
class ErrorResponse
{
#[SerializedPath('[satz][nr]')]
private ?string $code;
#[SerializedPath('[satz][text]')]
private ?string $type;
public function getCode(): ?string
{
return $this->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;
}
}
+161
View File
@@ -0,0 +1,161 @@
<?php
namespace App\BusProNet\Model;
use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\SerializedPath;
use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer;
class Profile
{
#[SerializedPath('[idadresse]')]
private ?int $addressId = null;
#[SerializedPath('[idperson]')]
private ?int $personId = null;
#[SerializedPath('[adressdaten][name]')]
private ?string $name = null;
#[SerializedPath('[adressdaten][vorname]')]
private ?string $firstName = null;
#[SerializedPath('[adressdaten][anrede]')]
private ?string $salutation = null;
#[SerializedPath('[adressdaten][titel]')]
private ?string $title = null;
#[SerializedPath('[adressdaten][geschlecht]')]
private ?string $gender = null;
#[SerializedPath('[adressdaten][geburtsdatum]')]
#[Context([DateTimeNormalizer::FORMAT_KEY => '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;
}
}
@@ -0,0 +1,26 @@
<?php
namespace App\Controller\Security;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
class LoginController extends AbstractController
{
#[Route('/login', name: 'app_security_login')]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
}
+146
View File
@@ -0,0 +1,146 @@
<?php
namespace App\Entity;
use App\Repository\UserRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity(repositoryClass: UserRepository::class)]
class User implements UserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?int $busProAddressId = null;
#[ORM\Column]
private ?int $busProPersonId = null;
#[ORM\Column(length: 255)]
private ?string $email = null;
#[ORM\Column(length: 255)]
private ?string $firstName = null;
#[ORM\Column(length: 255)]
private ?string $lastName = null;
#[ORM\Column(length: 255)]
private ?string $role = null;
#[ORM\Column(nullable: true)]
private ?\DateTimeImmutable $lastLoginAt = null;
public function getId(): ?int
{
return $this->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;
}
}
+66
View File
@@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\User;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<User>
*
* @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()
// ;
// }
}
+117
View File
@@ -0,0 +1,117 @@
<?php
namespace App\Security;
use App\BusProNet\ApiClient;
use App\BusProNet\ApiClientException;
use App\BusProNet\Model\ErrorResponse;
use App\BusProNet\Model\Profile;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
{
use TargetPathTrait;
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly EntityManagerInterface $entityManager,
private readonly ApiClient $apiClient,
private readonly LoggerInterface $logger
) {
}
protected function getLoginUrl(Request $request): string
{
return $this->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;
}
}