| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [x] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Co-authored-by: Matthieu <mtholot19@gmail.com> Reviewed-on: #8 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #8.
This commit is contained in:
@@ -15,6 +15,14 @@ use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserProcessor;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserRbacProcessor;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\Provider\MeProvider;
|
||||
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
|
||||
// Note architecture : User.php utilise SiteInterface (Shared) pour les
|
||||
// type-hints afin de ne pas coupler le module Core au module Sites.
|
||||
// La seule reference concrete a Site subsiste dans les metadonnees ORM
|
||||
// (targetEntity) via FQCN string, ce qui est obligatoire pour Doctrine.
|
||||
// SiteNotAuthorizedException est importee depuis Shared (sa location canonique).
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
use App\Shared\Domain\Contract\SiteInterface;
|
||||
use App\Shared\Domain\Exception\SiteNotAuthorizedException;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -107,6 +115,45 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[Groups(['me:read', 'user:list', 'user:rbac:write', 'user:rbac:read'])]
|
||||
private Collection $directPermissions;
|
||||
|
||||
/**
|
||||
* Sites autorises pour l'utilisateur (ticket 2 du module Sites).
|
||||
*
|
||||
* Relation ManyToMany avec table de jointure `user_site`. Fetch LAZY :
|
||||
* le chargement est defere jusqu'a l'acces explicite a la collection.
|
||||
* MeProvider (ou un futur provider avec JOIN FETCH) est responsable de
|
||||
* precharger cette collection pour /api/me afin d'eviter N+1.
|
||||
*
|
||||
* Le groupe `user:list` a ete retire deliberement (securite : evite
|
||||
* de fuiter la liste des sites de chaque user via GET /api/users).
|
||||
*
|
||||
* @var Collection<int, Site>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: 'App\Module\Sites\Domain\Entity\Site', inversedBy: 'users', fetch: 'LAZY')]
|
||||
#[ORM\JoinTable(name: 'user_site')]
|
||||
#[Groups(['me:read', 'user:rbac:read', 'user:rbac:write'])]
|
||||
private Collection $sites;
|
||||
|
||||
/**
|
||||
* Site courant selectionne par l'utilisateur (ticket 2 du module Sites).
|
||||
*
|
||||
* Relation ManyToOne nullable : un user peut ne pas avoir encore choisi
|
||||
* de site actif (par ex. apres creation avant premier login). La FK porte
|
||||
* `onDelete: SET NULL` pour que la suppression d'un site ne detruise pas
|
||||
* les users qui le pointaient — ils repassent simplement a `null`.
|
||||
*
|
||||
* Doit TOUJOURS pointer vers un site present dans `$sites`. L'invariant
|
||||
* est enforce par UserRbacProcessor qui bascule automatiquement a `null`
|
||||
* si le site courant est retire des sites autorises.
|
||||
*
|
||||
* Fetch LAZY : MeProvider (ou un futur provider avec JOIN FETCH) assure
|
||||
* le prechargement pour /api/me. Le groupe `user:list` a ete retire
|
||||
* deliberement (securite : evite de fuiter le site actif via /api/users).
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: 'App\Module\Sites\Domain\Entity\Site', fetch: 'LAZY')]
|
||||
#[ORM\JoinColumn(name: 'current_site_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
|
||||
#[Groups(['me:read'])]
|
||||
private ?SiteInterface $currentSite = null;
|
||||
|
||||
#[ORM\Column]
|
||||
private ?string $password = null;
|
||||
|
||||
@@ -121,6 +168,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
$this->createdAt = new DateTimeImmutable();
|
||||
$this->rbacRoles = new ArrayCollection();
|
||||
$this->directPermissions = new ArrayCollection();
|
||||
$this->sites = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -313,4 +361,95 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
$this->plainPassword = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Site>
|
||||
*/
|
||||
public function getSites(): Collection
|
||||
{
|
||||
return $this->sites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotent : ajouter deux fois le meme site n'entraine pas de doublon.
|
||||
* Synchronise la collection inverse Site::$users en memoire pour eviter
|
||||
* un etat incoherent entre les deux cotes de la M2M dans une meme
|
||||
* session Doctrine (cf. ticket 2 review point #1).
|
||||
*
|
||||
* Le parametre est type SiteInterface pour eviter le couplage Core → Sites.
|
||||
* En pratique seule App\Module\Sites\Domain\Entity\Site est passee ici.
|
||||
*/
|
||||
public function addSite(SiteInterface $site): static
|
||||
{
|
||||
if (!$this->sites->contains($site)) {
|
||||
$this->sites->add($site);
|
||||
// @phpstan-ignore-next-line : Site concret toujours passe en pratique
|
||||
$site->addUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire un site de la collection + maintient la collection inverse en
|
||||
* memoire (cf. addSite). Attention : ne met PAS a jour `$currentSite`
|
||||
* si le site retire en etait le courant — cet invariant est enforce
|
||||
* par UserRbacProcessor (cote applicatif) ou doit etre maintenu
|
||||
* explicitement par l'appelant. Voir Risque 2 du ticket 2 spec.
|
||||
*/
|
||||
public function removeSite(SiteInterface $site): static
|
||||
{
|
||||
if ($this->sites->removeElement($site)) {
|
||||
// @phpstan-ignore-next-line : Site concret toujours passe en pratique
|
||||
$site->removeUser($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garde applicative rapide : teste la presence d'un site dans la
|
||||
* collection autorisee, via comparaison d'identite d'objet Doctrine.
|
||||
* Utilise par CurrentSiteProcessor pour valider un switch.
|
||||
*/
|
||||
public function hasSite(SiteInterface $site): bool
|
||||
{
|
||||
return $this->sites->contains($site);
|
||||
}
|
||||
|
||||
public function getCurrentSite(): ?SiteInterface
|
||||
{
|
||||
return $this->currentSite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter brut, sans garde. Usage interne pour les flux qui doivent
|
||||
* pouvoir positionner un site arbitraire ou null (reset de coherence
|
||||
* post-PATCH RBAC, fixtures, init). Pour le flux user-facing
|
||||
* "selectionner un site dans la liste autorisee", utiliser
|
||||
* switchCurrentSite() qui porte la garde domaine.
|
||||
*/
|
||||
public function setCurrentSite(?SiteInterface $currentSite): static
|
||||
{
|
||||
$this->currentSite = $currentSite;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Garde domaine du switch utilisateur : refuse un site qui n'est pas
|
||||
* dans la collection autorisee. Levee d'une exception domaine que le
|
||||
* processor HTTP traduit en 403 (pattern aligne sur Role::ensureDeletable
|
||||
* → SystemRoleDeletionException).
|
||||
*
|
||||
* @throws SiteNotAuthorizedException si $site n'appartient pas a $this->sites
|
||||
*/
|
||||
public function switchCurrentSite(SiteInterface $site): void
|
||||
{
|
||||
if (!$this->hasSite($site)) {
|
||||
throw SiteNotAuthorizedException::forSite($site);
|
||||
}
|
||||
|
||||
$this->currentSite = $site;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user