Skip to content

release: livraison develop → main (5 PR, unités encadrées, rôles et âge, email réservé à l ADMIN) - #186

Merged
mathiscapart merged 18 commits into
mainfrom
develop
Oct 8, 2026
Merged

mathiscapart merged 18 commits into
mainfrom
develop

Conversation

@mathiscapart

@mathiscapart mathiscapart commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Origine

Livraison develop → main : 5 PR (#176, #182, #183, #184, #185), 12 commits hors merges. Closes #128.

⚠️ Avant de déployer

  1. Sauvegarde de la base avant le déploiement. 1 migration Prisma : 20261007163359_unit_lead. Elle crée la table UnitLead et insère des données : un UnitLead(userId, unit) pour chaque compte Chef qui a une unité (comptes anonymisés exclus), plus une ligne d'audit par compte repris. Idempotente (INSERT OR IGNORE), sans effet sur une base vierge.
  2. Aucune variable d'environnement nouvelle, aucun changement de docker-compose.yml, .env.example ni package.json.
  3. Après le déploiement : saisir à la main les compagnons qui encadrent déjà une autre unité, avec le nouvel écran « Unités encadrées » (/admin/utilisateurs/[id]/modifier). La migration ne reprend que l'unité d'appartenance des chefs actuels : ils gardent exactement leurs droits, mais aucun compagnon n'encadre une autre unité tant que ce n'est pas saisi.

Ce qui change

Pourquoi

Un compagnon ne pouvait pas encadrer une unité, parce que le périmètre d'un chef reposait sur son unité d'appartenance, qui est unique (#128). Et la faille de #176 contournait le choix de #173 (mot de passe réservé à l'ADMIN).

Preuves

Invariants

  • Mutations sous withAudit() : vérifié PR par PR (setUserLeadUnits avec avant/après)
  • Actions/pages sensibles gardées par can(user, "…") : user.manage, user.email.set
  • Migration Prisma : 20261007163359_unit_lead
  • CSP Traefik mise à jour si nouveau domaine externe : Sans objet
  • Données de mineurs / RGPD touchées : oui. Un compagnon mineur peut être chef (D-044). L'anonymisation supprime les UnitLead. Le seed contient un chef de 17 ans.

Décisions prises

Entrées DECISIONS.md : D-044 (l'âge ne restreint plus les rôles) et D-045 (le périmètre d'un chef est UnitLead). Choix A validé : un RG ou une secrétaire peut s'attribuer CHEF et des unités encadrées, avec trace dans l'audit et sans notification.

Risques et points à vérifier

  • Aucune recette sur copie de la base de prod. La migration insère des lignes : à tester sur une copie si possible.
  • Asymétrie assumée, dette connue (TODO(ROLES-06)) : le Chef est borné par ledUnits, les autres rôles par User.unit. La matrice de rôles la résoudra.
  • Un chef qui perd toutes ses unités encadrées garde le rôle Chef sans périmètre : plus d'accès aux salons ni aux actions de son unité.
  • Points faibles laissés par les audits : le flux SSE n'est pas réévalué après le retrait d'une unité, leaderIds ignore canLogin, et les libellés d'unité ne sont pas uniformes dans la liste admin.
  • La saisie manuelle des compagnons qui encadrent déjà (voir « Avant de déployer », point 3) n'est plus suivie par une issue : c'est une action de production, pas du développement.

mathiscapart and others added 18 commits September 25, 2026 14:14
…DMIN

Changer l'email d'un compte permettait de s'y connecter : « mot de passe
oublié » envoie le lien à la nouvelle adresse. Le RG et la secrétaire
(user.manage) contournaient ainsi user.password.set, réservé à l'ADMIN
depuis #173.

- Nouvelle action `user.email.set`, ADMIN seul.
- `updateUserAccount` refuse un email modifié sans ce droit ; nom,
  prénom et téléphone restent modifiables.
- Formulaire : champ email en lecture seule avec explication pour les
  non-ADMIN.

Vérifié en build de prod : RG et secrétaire refusés même en forçant le
champ côté navigateur, base inchangée ; téléphone modifiable ; l'ADMIN
change l'email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Suite de revue : sans `user.email.set`, la transaction garde l'email relu
en base au lieu de celui du formulaire. Un enregistrement du RG ou de la
secrétaire sur une page ouverte avant un changement d'email par l'ADMIN
n'écrase plus ce changement. Le message de refus le mentionne.

Vérifié en build de prod : l'ADMIN change l'email pendant que le RG a la
fiche ouverte ; l'enregistrement du RG est refusé, l'email de l'ADMIN est
conservé.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…le RG, parents prévenus

Décision : seul un compte qui se connecte déjà est réservé à l'ADMIN.
Un compte sans connexion (compte enfant) peut recevoir sa première
adresse de qui gère les comptes, pour garder l'activation d'un jeune qui
atteint 15 ans sans passer par l'ADMIN.

- `canChangeAccountEmail` (src/lib/permissions.ts) : `user.email.set`
  (ADMIN) pour tout compte, `user.manage` pour un compte sans connexion.
- Compte enfant qui reçoit une vraie adresse : alerte forcée
  (SECURITY_ALERT) à chaque parent rattaché.
- Audit USER_ACCOUNT_UPDATED : ancienne et nouvelle adresse quand l'email
  change, effacées à l'anonymisation (liste blanche, test ajouté).

Vérifié en build de prod : secrétaire refusée sur un compte qui se
connecte (base inchangée) ; secrétaire attribue l'email d'un compte
enfant de 16 ans (connexion activée, alerte au parent, audit
ancienne → nouvelle) ; le compte devenu connectable repasse en lecture
seule ; l'ADMIN change l'email d'un adulte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…able hors ADMIN

Revue : retirer ses rôles à un adulte puis lui donner une date de moins
de 15 ans le passait en canLogin=false sans changer son adresse réelle ;
la secrétaire ou le RG pouvait alors la remplacer par la sienne et se
connecter via « mot de passe oublié ».

- `canChangeAccountEmail` exige en plus une adresse provisoire
  (`PLACEHOLDER_EMAIL_SUFFIX`, déplacé dans src/lib/permissions.ts).
- Changement d'email comparé sans la casse des deux côtés : plus d'audit
  ni d'alerte aux parents sur une simple différence de majuscules.
- Messages : « une adresse email déjà attribuée ».

Vérifié en build de prod : la secrétaire est refusée sur un adulte en
canLogin=false avec adresse réelle (base inchangée) ; elle attribue
toujours l'adresse d'un compte enfant de 16 ans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e hors ADMIN

Suite de revue :
- `canChangeAccountEmail` exclut les comptes supprimés (anonymisés) :
  leur adresse est aussi provisoire, et y remettre une adresse réelle
  réintroduisait de la PII dans une fiche effacée, sans expurgation de
  l'audit.
- `canSetEmailTo` : hors ADMIN, une nouvelle adresse ne peut pas finir
  par @piloti.invalid. Poser `deleted+<id>@piloti.invalid` d'un autre
  compte bloquait son anonymisation (contrainte d'unicité).

Vérifié en build de prod : secrétaire refusée sur un compte supprimé et
sur une adresse provisoire saisie à la main (base inchangée) ; elle
attribue toujours l'adresse réelle d'un compte enfant de 16 ans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Abandonne la règle « un mineur ne reçoit que le rôle Jeune » (#122,
PR #126). Un ADMIN ou un RG peut attribuer n'importe quel rôle à
n'importe quel compte, y compris à un mineur.

- supprime assignableRolesForBirthDate et ses tests
- approveUser, setUserRoles : plus de contrôle d'âge
- setUserBirthDate : ne bloque plus une date « mineure » sur un compte
  qui porte un rôle d'encadrement (la coupure de connexion sous 15 ans
  reste en place)
- RolesEditor, ApproveDialog : plus de filtre par âge (le badge Mineur
  reste affiché)
- DECISIONS.md : D-044

Les verrous canEnableLogin/MIN_LOGIN_AGE, dm-policy, consentement
parental et comptes enfants sont inchangés.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fix(admin): #128 — l'âge ne restreint plus les rôles attribuables
# Conflicts:
#	src/app/(app)/admin/utilisateurs/[id]/modifier/page.tsx
fix(roles): une adresse email déjà attribuée n'est modifiable que par l'ADMIN
- Nouvelle table UnitLead (userId, unit) : unités ENCADRÉES, distinctes de
  l'unité d'appartenance User.unit. La migration reprend les chefs actuels.
- Le périmètre d'un CHEF (inUnitScope, canActOnUnit, scopedUnits,
  canReadPedagoNotes) devient ses UnitLead (ledUnits, chargé par
  getCurrentUser). Fail-closed : un CHEF sans UnitLead n'encadre rien. Les
  rôles transverses gardent leur périmètre d'avant (unité d'appartenance).
- Un seul point d'écriture (writeLeadUnits, admin/actions.ts), verrouillé par
  un test statique. approveUser et setUserRoles gardent CHEF et UnitLead
  cohérents ; setUserUnit ne touche pas au périmètre ; l'anonymisation
  supprime les UnitLead.
- Seed : un compagnon CHEF qui encadre les Louveteaux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Décision de Mathis : seul le CHEF est borné par ledUnits, les autres rôles
restent sur User.unit. Dette connue, à supprimer par la matrice de rôles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…OLES-06)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(admin): #128 — unités encadrées (UnitLead), périmètre d'un chef
…modération et pédagogie

- dm-policy : « chef de l'unité du jeune » = CHEF qui encadre son unité
  (ledUnits) ; la règle entre mineurs de 15 à 17 ans passe avant.
- Signalements : notification et file des CHEF par unités encadrées,
  fail-closed sans encadrement.
- 2e validation pédagogique : notifie les chefs qui encadrent l'unité.
- setUserRoles ne recrée plus d'UnitLead sur un compte DELETED.
- D-045 amendée.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(communication): #128 — chef d'unité par UnitLead en messagerie, modération et pédagogie
…d'un chef

- Salons : un chef accède aux salons des unités qu'il encadre ; exclusion
  (excludeUnits) seulement si toutes ses unités sont exclues.
- Annonces, notifications d'événement et iCal : un chef reçoit aussi ceux
  des unités qu'il encadre. Relances et cotisations restent à l'appartenance.
- Bilan des présences : unité présélectionnée parmi les unités encadrées.
- /admin/utilisateurs/[id]/modifier : éditeur « Unités encadrées »
  (setUserLeadUnits), message clair pour un compte non Chef ; audit avec
  l'avant et l'après. Unités encadrées affichées dans la liste (avec filtre)
  et sur la fiche membre.
- CLAUDE.md : unité d'appartenance ≠ unités encadrées. D-045 amendée.

Refs #128

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…alogue

Relecture : la première unité encadrée dépendait de l'ordre de chargement
des UnitLead. Libellé « Encadre : … » calculé une fois par ligne.

Refs #128

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
feat(admin): #128 — unités encadrées à l'écran et dans la visibilité d'un chef
@mathiscapart
mathiscapart merged commit 8b63dc8 into main Oct 8, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor(comptes): un compagnon peut être chef — le modèle ne gère ni plusieurs rôles d'unité ni plusieurs unités

2 participants