fix(user) : archivage au lieu de suppression + réparation des références orphelines
Un user supprimé physiquement laissait des références orphelines (task.assignee, time entries, notifications) car les FK vers "user" ont été créées NOT VALID lors du refactor modular-monolith : elles n'ont jamais nettoyé les orphelins legacy. La sérialisation API Platform d'une tâche embarquant un assignee inexistant levait une EntityNotFoundException non rattrapable (HTTP 500 sur tout PATCH/GET de ces tickets). - User::$archived (bool) + migration (soft delete) - Delete de User -> UserArchiveProcessor : archive (archived=true, apiToken vidé) au lieu de supprimer, préservant l'intégrité référentielle - ArchivedUserChecker : login bloqué pour un user archivé (firewalls login + api) - ExcludeArchivedUserExtension : archivés exclus de GET /api/users (assignation), les références existantes restent sérialisées normalement - Commande app:restore-missing-users : recrée (en archivés) les users encore référencés mais supprimés, restaurant l'intégrité sans perte de données. Idempotente, option --dry-run. À lancer une fois en prod après déploiement.
This commit is contained in:
@@ -13,6 +13,7 @@ use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Core\Domain\Enum\ContractType;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\MeProvider;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserArchiveProcessor;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\Processor\UserRbacProcessor;
|
||||
use App\Module\Core\Infrastructure\ApiPlatform\State\UserPasswordHasherProcessor;
|
||||
use App\Module\Core\Infrastructure\Doctrine\DoctrineUserRepository;
|
||||
@@ -47,7 +48,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
),
|
||||
new Post(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
|
||||
new Patch(security: "is_granted('ROLE_ADMIN')", processor: UserPasswordHasherProcessor::class),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||
new Delete(security: "is_granted('ROLE_ADMIN')", processor: UserArchiveProcessor::class),
|
||||
new Get(
|
||||
uriTemplate: '/users/{id}/rbac',
|
||||
security: "is_granted('core.users.manage')",
|
||||
@@ -111,6 +112,15 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedU
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $avatarFileName = null;
|
||||
|
||||
/**
|
||||
* Soft-delete flag. Archived users are kept for referential integrity
|
||||
* (tasks, time entries, notifications…) but cannot log in and are hidden
|
||||
* from selectable user lists.
|
||||
*/
|
||||
#[ORM\Column(options: ['default' => false])]
|
||||
#[Groups(['me:read', 'user:list'])]
|
||||
private bool $archived = false;
|
||||
|
||||
// --- HR / absence management fields (readable only by an admin or the user themselves) ---
|
||||
|
||||
/** Whether this user is an employee subject to absence management. */
|
||||
@@ -228,6 +238,18 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface, SharedU
|
||||
return (string) $this->username;
|
||||
}
|
||||
|
||||
public function isArchived(): bool
|
||||
{
|
||||
return $this->archived;
|
||||
}
|
||||
|
||||
public function setArchived(bool $archived): static
|
||||
{
|
||||
$this->archived = $archived;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
public function getRoles(): array
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user