fix(user) : archivage au lieu de suppression + réparation des références orphelines #28
Reference in New Issue
Block a user
Delete Branch "fix/user-soft-delete-orphan-references"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Contexte
Erreur prod (GlitchTip issue 7) :
PATCH /api/tasks/{id}→EntityNotFoundException: User id(7)à la sérialisation (API Platform embarquetask.assignee; le proxy Doctrine d'un user supprimé lève une exception non rattrapable → HTTP 500). 128 tickets étaient cassés, plus 104 time entries et 36 notifications orphelins (users 6, 7, 9, 10 supprimés physiquement).Cause racine : les FK référençant
useront été crééesNOT VALIDlors du refactor modular-monolith → elles enforced les suppressions futures mais ont laissé les orphelins legacy.Correctif (100 % non destructif)
User::$archived(bool) + migration → soft delete.Deletede User →UserArchiveProcessor: archive (archived=true,apiTokenvidé) au lieu de supprimer → plus de nouveaux orphelins, intégrité préservée. Front inchangé.ArchivedUserChecker: login bloqué pour un archivé (firewallslogin+api).ExcludeArchivedUserExtension: archivés exclus deGET /api/users(assignation) ; les références existantes restent sérialisées.app:restore-missing-users: recrée (en archivés) les users référencés mais supprimés → restaure l'intégrité sans perte de données. Idempotente,--dry-run.Validation (dev)
app:restore-missing-users→ users 6/7/9/10 recréés, plus aucun orphelinGET /api/usersexclut les archivés · login archivé → 401 ·DELETE→ archive (ligne conservée)Déploiement prod
archivedautomatique).php bin/console app:restore-missing-users(conteneurlesstime-app).--repo MALIO-DEV/Lesstime ## 🔍 Review — soft-delete user + réparation des orphelins
Verdict : approche correcte et non destructive, mergeable après ajout de tests. Le correctif s'attaque à la vraie cause racine (FK
NOT VALIDlegacy → orphelins → crash de sérialisation). Quelques points à traiter.✅ Points forts (vérifiés)
pg_constraint: robuste et résistant aux évolutions du schéma.user_checkersur les deux firewalls (login+api) → un archivé est bloqué au login et à chaque requête JWT (cookie BEARER encore valide rejeté immédiatement).apiTokenvidé à l'archivage → accès MCP révoqué.ExcludeArchivedUserExtensionauto-taguée et active (vérifié dans le container compilé). Filtre bien uniquement la collection/users→ les références item des archivés continuent de résoudre (tâches/time entries sérialisent). Bon périmètre.RestoreMissingUsersCommandcomplet (vérifié contre le schéma réel : toutes les colonnesNOT NULLsans défaut fournies,created_atviaNOW(),reference_period_start=varchar(5)→'06-01'OK). La commande passe.🟠 À traiter
1. Aucun test ne couvre le nouveau comportement. Pour un fix prod critique, ajouter au minimum un test fonctionnel :
DELETE /api/users/{id}→ archive (ligne conservée,archived=true), pas de suppression ;GET /api/usersexclut les archivés ;PATCH /api/tasks/{id}avec assignee archivé → 200 (le scénario exact du crash GlitchTip).2. Pas de désarchivage + archivés invisibles côté admin = piège irréversible par l'UI. Une fois archivé, l'user disparaît de
GET /users(donc deAdminUserTab) et il n'existe aucune opération/UI pour le désarchiver. ⚠️app:restore-missing-usersn'aide pas : elle ne recrée que les lignes manquantes — une ligne archivée existe déjà, elle est ignorée. Un archivage par erreur n'est annulable qu'en SQL direct. Suggestion : filtre admin?archived=true+ PATCH autorisantarchived=false, ou a minima documenter la procédure manuelle.🟡 Mineurs
RestoreMissingUsersCommand— comptage faux en re-run :++$createdest incrémenté inconditionnellement alors queON CONFLICT (id) DO NOTHINGpeut insérer 0 ligne → message « N user(s) restored » sur-compte au 2ᵉ passage. Utiliser la valeur de retour d'executeStatement(). (L'idempotence elle-même est correcte.)deleted-user-%davecON CONFLICT (id)seulement. Si un usernamedeleted-user-Nexistait avec un autre id, l'INSERT lèverait sur la contrainte uniqueusername. Cas très improbable, à garder en tête.users.deletedpour ce qui est maintenant un archivage : libellé légèrement trompeur (donnée conservée). Cosmétique.- ajoute des tests fonctionnels (archive au DELETE, exclusion de la collection, listing/désarchivage admin, anti-auto-archivage) et un test unitaire du ArchivedUserChecker - expose un filtre BooleanFilter `archived` + bypass admin dans ExcludeArchivedUserExtension pour lister les archivés (?archived=true) - rend `archived` modifiable par un admin (groupe user:write + ApiProperty ROLE_ADMIN) → désarchivage possible via PATCH /api/users/{id} - RestoreMissingUsersCommand : ne compte que les insertions réelles (ON CONFLICT DO NOTHING n'est plus comptabilisé à tort) - relève memory_limit des tests à 512M (boot sérialiseur API Platform)✅ Retours traités (commit
133f205)Tests ajoutés (
UserArchiveApiTest+ArchivedUserCheckerTest) :DELETE /api/users/{id}→ archive (ligne conservée,archived=true,apiTokenvidé), pas de suppression ;GET /api/usersexclut les archivés ;GET /api/users?archived=true(admin) les liste ;PATCH /api/users/{id}{archived:false}.Désarchivage possible :
BooleanFiltersurarchived+ bypass admin dansExcludeArchivedUserExtension(un admin peut lister les archivés via?archived=true), etarchivedrendu modifiable (user:write+ApiPropertyROLE_ADMIN) → restauration via PATCH. Plus besoin de SQL direct.Commande :
RestoreMissingUsersCommandne compte plus que les insertions réelles (ON CONFLICT DO NOTHINGn'est plus comptabilisé à tort).Tests :
memory_limitdu suite relevé à 512M (le boot du sérialiseur API Platform sur la collection non paginée/api/usersdépassait 128M à froid).Suite complète : 225 tests verts.