M0: Skelett + Auth-Grundgerüst (Keycloak-OIDC + API-Token)
- Symfony 7.4 LTS, PHP 8.4, FrankenPHP, SQLite (3-Layer src/UI|Logic|Data) - Auth: manueller OidcClient (Authorization-Code) im UI-Layer, kein Security-Bundle/league; Session-Gate via SessionAuthSubscriber - API-Guard via statischem X-Api-Token (ApiTokenSubscriber); ExceptionSubscriber - UserEntity (Auto-Provision) + Init-Migration - Docker (php:8.4-frankenphp) + compose (essen.ljh31.de, traefik-net external) - SmokeTests (7) grün: Redirect-Fälle, API-Token, Auth-Render, Logout Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Auth\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'essens_user')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_email', columns: ['email'])]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_kc_sub', columns: ['kc_sub'])]
|
||||
final class UserEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $username = '';
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255)]
|
||||
private string $email = '';
|
||||
|
||||
#[ORM\Column(type: 'string', length: 64)]
|
||||
private string $kcSub = '';
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $createdAm;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $updatedAm;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$this->createdAm = $now;
|
||||
$this->updatedAm = $now;
|
||||
}
|
||||
|
||||
public function getId(): ?int { return $this->id; }
|
||||
|
||||
public function getUsername(): string { return $this->username; }
|
||||
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): string { return $this->email; }
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getKcSub(): string { return $this->kcSub; }
|
||||
|
||||
public function setKcSub(string $sub): self
|
||||
{
|
||||
$this->kcSub = $sub;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAm(): \DateTimeImmutable { return $this->createdAm; }
|
||||
|
||||
public function getUpdatedAm(): \DateTimeImmutable { return $this->updatedAm; }
|
||||
|
||||
public function setUpdatedAm(\DateTimeImmutable $dt): self
|
||||
{
|
||||
$this->updatedAm = $dt;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Infrastructure\Doctrine;
|
||||
|
||||
use App\Logic\Common\TransactionManagerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* Bridge von Doctrine\EntityManager auf `TransactionManagerInterface`.
|
||||
*
|
||||
* Verwendet Doctrine 3 `doInTransaction()` (commit + rollback).
|
||||
*/
|
||||
final class EntityManagerTransactionManager implements TransactionManagerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
) {
|
||||
}
|
||||
|
||||
public function execute(\Closure $work): mixed
|
||||
{
|
||||
try {
|
||||
return $this->em->transactional(static fn () => $work($work));
|
||||
} catch (\Throwable $e) {
|
||||
$this->em->clear();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Data\Infrastructure;
|
||||
|
||||
use App\Logic\Common\EventDispatcherInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface as SymfonyEventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* Bridge zurSymfony-Event-Dispatcher (kein eigenes Queue / kein Kafka nötig).
|
||||
*
|
||||
* Events sollten final/ReadOnly-Klassen sein (State-Carry).
|
||||
*/
|
||||
final class SymfonyEventDispatcherBridge implements EventDispatcherInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SymfonyEventDispatcherInterface $dispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function dispatch(object $event): void
|
||||
{
|
||||
$this->dispatcher->dispatch($event);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user