feat: minimal locally stored user entity

This commit is contained in:
Björn Fromme
2025-04-24 17:09:05 +02:00
parent 53884e4e12
commit c418ae6399
14 changed files with 217 additions and 200 deletions
-51
View File
@@ -1,51 +0,0 @@
<?php
namespace App\BusProNet\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class User implements UserInterface
{
public function __construct(
private readonly string $email,
private readonly int $personId,
private readonly int $addressId,
private readonly ?string $password = null,
private readonly array $roles = [],
) {
}
public function getEmail(): ?string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getPersonId(): ?int
{
return $this->personId;
}
public function getAddressId(): int
{
return $this->addressId;
}
public function getRoles(): array
{
return ['ROLE_USER', ...$this->roles];
}
public function eraseCredentials(): void
{
}
public function getUserIdentifier(): string
{
return $this->email;
}
}
-60
View File
@@ -1,60 +0,0 @@
<?php
namespace App\BusProNet\Security;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\PersonalData;
use App\Controller\Traits\CredentialsTrait;
use App\Security\Crypt;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
class UserProvider implements UserProviderInterface
{
use CredentialsTrait;
public function __construct(
private readonly ApiClient $apiClient,
private readonly RequestStack $requestStack,
private readonly Crypt $crypt,
) {
}
public function refreshUser(UserInterface $user): UserInterface
{
if (null !== $activeUser = $this->requestStack->getSession()->get('bpn_user')) {
return $activeUser;
}
return $this->loadUserByIdentifier($user->getUserIdentifier());
}
public function supportsClass(string $class): bool
{
return User::class === $class;
}
public function loadUserByIdentifier(string $identifier): UserInterface
{
if (null === $activeUser = $this->requestStack->getSession()->get('bpn_user')) {
throw new UserNotFoundException();
}
try {
$email = $activeUser->getEmail();
$password = $this->crypt->decrypt($activeUser->getPassword());
$response = $this->apiClient->getPersonalData($email, $password);
} catch (ApiClientException $e) {
throw new UserNotFoundException();
}
if (false === $response instanceof PersonalData) {
throw new UserNotFoundException();
}
return $activeUser;
}
}
@@ -5,9 +5,8 @@ namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Security\User;
use App\Controller\Traits\BookingDataTrait;
use App\Controller\Traits\CredentialsTrait;
use App\Entity\User;
use App\Security\Crypt;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -21,7 +20,6 @@ use function Symfony\Component\String\u;
class DownloadController extends AbstractController
{
use CredentialsTrait;
use BookingDataTrait;
public function __construct(
+1 -3
View File
@@ -5,11 +5,10 @@ namespace App\Controller\Booking;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Security\User;
use App\BusProNet\XmlLoader\PickupLoader;
use App\BusProNet\XmlLoader\TravelLoader;
use App\Controller\Traits\BookingDataTrait;
use App\Controller\Traits\CredentialsTrait;
use App\Entity\User;
use App\Form\BookingType;
use App\Form\Model\BookingData;
use App\Security\Crypt;
@@ -25,7 +24,6 @@ use Symfony\Contracts\Cache\CacheInterface;
class EditController extends AbstractController
{
use CredentialsTrait;
use BookingDataTrait;
public function __construct(
+1 -4
View File
@@ -6,9 +6,8 @@ use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\BaseData;
use App\BusProNet\Model\Notification;
use App\BusProNet\Security\User;
use App\BusProNet\XmlLoader\TravelLoader;
use App\Controller\Traits\CredentialsTrait;
use App\Entity\User;
use App\Security\Crypt;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@@ -19,8 +18,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class IndexController extends AbstractController
{
use CredentialsTrait;
public function __construct(
private readonly ApiClient $apiClient,
private readonly TravelLoader $travelDataLoader,
+1 -4
View File
@@ -6,8 +6,7 @@ use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\Notification;
use App\BusProNet\Model\PersonalData;
use App\BusProNet\Security\User;
use App\Controller\Traits\CredentialsTrait;
use App\Entity\User;
use App\Form\PersonalDataType;
use App\Security\Crypt;
use Psr\Log\LoggerInterface;
@@ -19,8 +18,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
class PersonalDataController extends AbstractController
{
use CredentialsTrait;
public function __construct(
private readonly ApiClient $apiClient,
private readonly Crypt $crypt,
@@ -1,16 +0,0 @@
<?php
namespace App\Controller\Traits;
use App\BusProNet\Security\User;
use App\Security\Crypt;
trait CredentialsTrait
{
public function getPasswordPlain(User $bpnUser, string $pathToKeys): string
{
$crypt = new Crypt($pathToKeys);
return $crypt->decrypt($bpnUser->getPassword());
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
#[ORM\Entity]
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private ?string $email;
#[ORM\Column(type: 'text')]
private ?string $password;
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $personId = null;
#[ORM\Column(type: 'integer', nullable: true)]
private ?int $addressId = null;
#[ORM\Column(type: 'json')]
private array $roles = [];
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
private ?\DateTimeImmutable $lastLoginAt = null;
public function __construct(string $email, string $password)
{
$this->email = $email;
$this->password = base64_encode($password);
}
public function getId(): ?int
{
return $this->id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): static
{
$this->email = $email;
return $this;
}
public function getPersonId(): ?int
{
return $this->personId;
}
public function setPersonId(?int $personId): static
{
$this->personId = $personId;
return $this;
}
public function getAddressId(): ?int
{
return $this->addressId;
}
public function setAddressId(?int $addressId): static
{
$this->addressId = $addressId;
return $this;
}
public function getPassword(): ?string
{
return base64_decode($this->password);
}
public function setPassword(?string $password): static
{
$this->password = base64_encode($password);
return $this;
}
public function getRoles(): array
{
return ['ROLE_USER', ...$this->roles];
}
public function setRoles(array $roles): static
{
$this->roles = $roles;
return $this;
}
public function getLastLoginAt(): ?\DateTimeImmutable
{
return $this->lastLoginAt;
}
public function setLastLoginAt(?\DateTimeImmutable $lastLoginAt): static
{
$this->lastLoginAt = $lastLoginAt;
return $this;
}
public function eraseCredentials(): void
{
}
public function getUserIdentifier(): string
{
return $this->email;
}
}
@@ -1,12 +1,13 @@
<?php
namespace App\BusProNet\Security;
namespace App\Security;
use App\BusProNet\ApiClient;
use App\BusProNet\Exception\ApiClientException;
use App\BusProNet\Model\CrmAttributes;
use App\BusProNet\Model\PersonalData;
use App\Security\Crypt;
use App\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -22,11 +23,12 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class Authenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
class BpnAuthenticator extends AbstractLoginFormAuthenticator implements AuthenticationEntryPointInterface
{
public function __construct(
private readonly UrlGeneratorInterface $urlGenerator,
private readonly ApiClient $apiClient,
private readonly EntityManagerInterface $entityManager,
private readonly Crypt $crypt,
private readonly LoggerInterface $logger,
) {
@@ -59,14 +61,7 @@ class Authenticator extends AbstractLoginFormAuthenticator implements Authentica
return new SelfValidatingPassport(
new UserBadge($email, function () use ($email, $password, $response, $request) {
$crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
$roles = $this->collectRoles($crmAttributes);
$encryptedPassword = $this->crypt->encrypt($password);
$user = new User($email, $response->personId, $response->addressId, $encryptedPassword, $roles);
$request->getSession()->set('bpn_user', $user);
return $user;
return $this->createOrUpdateLocalUser($email, $password, $response->personId, $response->addressId);
}),
[
new CsrfTokenBadge('authenticate', $csrfToken),
@@ -75,6 +70,36 @@ class Authenticator extends AbstractLoginFormAuthenticator implements Authentica
);
}
private function createOrUpdateLocalUser(string $email, string $password, ?int $personId, ?int $addressId): User
{
try {
$crmAttributes = $this->apiClient->getCrmAttributes($email, $password);
} catch (ApiClientException $e) {
throw new CustomUserMessageAuthenticationException($e->getMessage());
}
$roles = $this->collectRoles($crmAttributes);
$encryptedPassword = $this->crypt->encrypt($password);
$userRepository = $this->entityManager->getRepository(User::class);
if (null === $user = $userRepository->findOneBy(['email' => $email])) {
$user = new User($email, $encryptedPassword);
$this->entityManager->persist($user);
}
$user
->setPersonId($personId)
->setAddressId($addressId)
->setRoles($roles)
->setLastLoginAt(new \DateTimeImmutable())
;
$this->entityManager->flush();
return $user;
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
$this->logger->info('Login', [