Journal des modifications
Les évolutions de Cohez.io, de la plus récente à la plus ancienne.
173 versions publiées.
Versions 21 à 30 sur 173
0.25.5.0 — 2026-09-25
Ajouts
- Les pièces santé sont rangées par saison, et la fiche adhérent affiche un bloc de conformité par saison active (#317, 2026-09-25). Une pièce appartient désormais à une saison : clé
(member_id, season_id, document_type), une attestation par(adhérent, saison), avec la campagne d'inscription quand elle est connue (member_documents.campaign_id).SeasonHealthProofest l'oracle unique d'une saison (certificat courant + historique) etGoverningCampaignResolverraisonne par saison. Dans une saison, le certificat auvalid_untille plus tardif reste courant ; un dépôt qui expire plus tôt entre en historique, et le remplacement ne supprime jamais de fichier. Le back-office choisit explicitement la saison (et la campagne) au dépôt ; dépôt et retrait sont refusés (404) sur une saison close. Un certificat déjà expiré à la date du dépôt est signalé (CertificateState, statutStaleCertificate). Le tunnel public n'échoue plus sur une seconde attestation pour la même saison. - Page « Archives santé » : consultation motivée et journalisée des pièces des saisons closes (#317, 2026-09-25). Nouvelle permission nominative
view_archived_health_documents, accordée par utilisateur (case « Archives santé » dans les réglages utilisateur) et jamais attribuée par défaut, même au rôleadmin. Ouvrir une pièce archivée exige un motif d'au moins 10 caractères, journalisé dansmember_document_accesses(reason,context = archive) avant l'envoi du premier octet ; 403 sans permission, 422 sans motif, 404 hors phase archivée,AdminUserrefusé. health:unanchored-documentsliste les pièces santé sans saison et permet de les rattacher (--attach) (#317, 2026-09-25). Lecture seule par défaut ; ces pièces ne sont jamais purgées automatiquement.
Modifications
- La conservation des pièces santé est une durée par saison : fin de saison + N ans (#317, 2026-09-25).
tenants.health_retention_years(5 par défaut) etHealthRetentiondéfinissent trois phases — active, archivée, supprimée — utilisées partout (fiche, téléchargements, archives, purge).health:purge-documentsne purge plus à l'expiration du certificat (+ 30 jours) : elle supprime fichiers, lignes et attestations des saisons arrivées en fin de conservation, par lots, tenant par tenant. Le fichier est supprimé avant sa ligne : un échec de stockage laisse la ligne en place et la suppression est retentée la nuit suivante. La commande renvoieFAILUREdès qu'un tenant échoue, et la tâche planifiée est protégée contre le chevauchement. ⚠️ Une date de fin de saison erronée dans le passé déclenche une purge prématurée et irréversible. - Migration de données (#317, 2026-09-25). Les pièces existantes sont rattachées à leur saison, les doublons par saison résolus par supersession, les index uniques posés par saison. La réponse d'attestation « au moins une positive » (
has_positive) est retirée : une réponse positive appelle un certificat.
Notes de déploiement
- En production,
post_build.shrelancePermissionSeederetRoleSeeder: rien à faire. Sur une base de développement non re-seedée, la nouvelle permission manque et la page des réglages utilisateur renvoie une 500 (PermissionDoesNotExist) : relancer ces deux seeders.
0.25.4.0 — 2026-09-25
Ajouts
- Identification d'un adhérent déjà connu dans le funnel d'inscription publique (#296, 2026-09-25). Un adhérent qui a déjà un dossier dans l'association peut désormais s'identifier avant de s'inscrire à une nouvelle proposition : il saisit son e-mail, reçoit un code à usage unique, et le funnel pré-remplit son identité, ses représentants légaux et ses acceptations à partir du dossier retrouvé, sans dédoublonner un nouveau
Person.
Corrections / Sécurité
- Durcissement de bout en bout de l'identification adhérent, à l'issue d'une revue de sécurité de la PR #296 (2026-09-25). L'identité rapprochée est désormais liée côté serveur (aucun identifiant de personne fourni par le client n'est fait confiance) et filtrée par tenant (
applyIdentifiedMember) ; le rapprochement civil passe par unPersonIdentityMatcherpartagé et normalisé (casse, espaces) au lieu de comparaisons dupliquées ; une adhésion active déjà en cours bloque désormais la soumission, avec repli sur l'identité rapprochée quand nécessaire ; l'e-mail d'une personne identifiée n'est ni écrasé ni modifiable côté front tant qu'elle reste identifiée ; un seul contact principal subsiste parmi les responsables légaux, l'ajout d'un nouveau responsable principal rétrogradant l'ancien. Sur le canal d'identification lui-même : le code envoyé est désormais lié au jeton public de la campagne qui l'a émis, réservé atomiquement à chaque tentative (5 maximum), à usage unique une fois vérifié avec succès, vérifié à coût constant (anti-énumération), et un délai minimal de 60 secondes s'impose entre deux envois de code. Un jeton XSRF est relu via un helper partagé (readXsrfToken) plutôt que dupliqué. La déduplication insensible à la casse dePersonDeduplicationService(trouvaille F3, doublons silencieux en base) reste hors périmètre de cette PR et fait l'objet d'un plan séparé.
0.25.3.2 — 2026-09-24
Corrections
- Un certificat médical valide n'est plus écrasé par un dépôt (#310, 2026-09-24).
StoreMemberDocument::persist()archivait le certificat actif et supprimait son fichier sans comparer les dates. Un certificat valide pouvait donc disparaître de trois façons : dépôt d'une pièce plus ancienne, dépôt par un rôle sans accès santé, ou dépôt anonyme via le funnel public quand la dédup rattache un adhérent existant. Désormais, le certificat qui expire le plus tard gagne (valid_until≥ celui de l'actif) ; sinon, la pièce entre directement en historique et le back-office l'indique par un message dédié. Le remplacement ne supprime plus aucun fichier :health:purge-documentspurge à expiration. Seule l'action « Retirer » supprime encore le fichier immédiatement.
0.25.3.1 — 2026-09-24
Modifications
wwwredirige vers le domaine racine (#307, 2026-09-24).www.cohez.ioservait la vitrine en 200 ; le middleware globalRedirectWwwToApexrenvoie désormais une redirection permanente verscohez.io(301 pour GET/HEAD, 308 sinon), en conservant chemin, query et port. Un seul hôte pour la vitrine : l'opt-out Matomo, mémorisé par origine, suit le visiteur, et le contenu n'est plus dupliqué.
0.25.3.0 — 2026-09-23
Ajouts
- Mesure d'audience Matomo sans cookie sur la vitrine (#306, 2026-09-23). Le site public charge le Matomo auto-hébergé (
MATOMO_URL,MATOMO_SITE_ID: URL https et identifiant numérique, sinon mesure désactivée) depuisvitrine.ts, en modedisableCookiespour rester dans l'exemption de consentement CNIL. Global Privacy Control et Do Not Track sont respectés. La CSP n'autorise l'origine Matomo que sur la vitrine, jamais sur un espace tenant ni sur l'admin./confidentialitedécrit le traitement (IP tronquée, conservation 25 mois, données enregistrées, base légale) et propose un opt-out mémorisé dans le stockage local, qui coupe aussi le suivi de la page en cours. Passer Matomo en mode avec cookies exigera d'abord un bandeau de consentement (TODO(consentement)).
0.25.2.2 — 2026-09-23
Ajouts
- Le nom d'une proposition de campagne reprend le nom du cours lié (#305, 2026-09-23). Dans les dialogs d'ajout (
RubriqueCard.vue) et d'édition (PropositionRow.vue) d'une proposition, choisir un cours pré-remplit le champ « Nom » avec le nom du cours, qui reste modifiable. Le nom n'est remplacé que s'il est vide ou encore égal au nom du cours précédemment choisi (propositionNameForCourse(),resources/js/lib/propositionName.ts) : un nom saisi à la main n'est jamais écrasé, et choisir « Aucun cours » ne touche pas au nom.
0.25.2.1 — 2026-09-23
Ajouts
- Accès nominatif aux documents santé, gaté par tenant (#301, 2026-09-23). Un admin peut désormais accorder ou révoquer, par utilisateur, l'accès aux documents santé (
VIEW_MEMBER_HEALTH_DOCUMENTS) indépendamment des rôles — actif uniquement si le tenant a activérequires_health_verification.Tenant::disableHealthVerification()révoque en cascade tous les octrois directs pour qu'aucun accès nominatif ne survive à la désactivation du flag tenant.
0.25.2.0 — 2026-09-18
Ajouts
- Ordre manuel des cours par glisser-déposer (#299, 2026-09-18). Sur
/courses, un bouton « Modifier l'ordre » bascule la liste en mode édition : les lignes se déplacent au glisser-déposer et chaque dépôt est persisté immédiatement dans la nouvelle colonnecourses.position. Réservé au rôleadminvia la permissionreorder_courses, contrôlée parCoursePolicy::reorder()avec vérification de l'appartenance de la saison au tenant. Le mode est refusé quand un filtre est actif ou qu'il n'y a qu'un cours, etCourseController::reorder()sérialise les réordonnancements concurrents d'une même saison en verrouillant laSeasonavant relecture, ce qui évite l'interblocage par ordre de verrouillage inversé sur les lignescourses.
0.25.1.5 — 2026-09-18
Corrections
- Les URLs d'assets perdaient le port de la requête en développement (#298, 2026-09-18).
AppServiceProvider::boot()construisait l'origine des assets avecrequest()->getHost(), qui ampute le port : chaque worktree servant l'application sur un port publié (8770, …) rendait des liens vers le port 80 de l'hôte, où rien n'écoute — assets non chargés, page blanche.getHttpHost()conserve le port et omet les ports standards, donc la production est inchangée.
0.25.1.4 — 2026-09-18
Corrections
- Deux findings mineurs de la revue de la PR #297 sur
draft-ttl-no-fallback.BackfillDraftDeadlinesCommandréimplémentait à la main le calculbase + draft_ttl_days jours, endOfDay()au lieu de réutiliser la logique deCampaign;Campaign::draftExpiresAtFrom(CarbonInterface $base)factorise ce calcul (utilisé pardraftExpiresAt()avecnow()et par le backfill avecregistered_at/created_at), la commande n'a plus sa propre copie. DansPublicRegistrationService::execute(),draftExpiresAt()(calculé avant la transaction),deposit_expires_atetregistered_at(posés dans la transaction) appelaient chacunnow()séparément : sous verrou concurrentiel qui attend, ces appels pouvaient être désynchronisés autour d'un passage de minuit et rogner le délai réel de brouillon jusqu'à environ un jour. Un seul$nowest désormais capturé avant la transaction et réutilisé pour les trois valeurs.