Aller au contenu
Candidater

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). SeasonHealthProof est l'oracle unique d'une saison (certificat courant + historique) et GoverningCampaignResolver raisonne par saison. Dans une saison, le certificat au valid_until le 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, statut StaleCertificate). 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ôle admin. Ouvrir une pièce archivée exige un motif d'au moins 10 caractères, journalisé dans member_document_accesses (reason, context = archive) avant l'envoi du premier octet ; 403 sans permission, 422 sans motif, 404 hors phase archivée, AdminUser refusé.
  • health:unanchored-documents liste 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) et HealthRetention définissent trois phases — active, archivée, supprimée — utilisées partout (fiche, téléchargements, archives, purge). health:purge-documents ne 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 renvoie FAILURE dè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.sh relance PermissionSeeder et RoleSeeder : 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 un PersonIdentityMatcher partagé 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 de PersonDeduplicationService (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-documents purge à expiration. Seule l'action « Retirer » supprime encore le fichier immédiatement.

0.25.3.1 — 2026-09-24

Modifications

  • www redirige vers le domaine racine (#307, 2026-09-24). www.cohez.io servait la vitrine en 200 ; le middleware global RedirectWwwToApex renvoie désormais une redirection permanente vers cohez.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) depuis vitrine.ts, en mode disableCookies pour 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. /confidentialite dé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 colonne courses.position. Réservé au rôle admin via la permission reorder_courses, contrôlée par CoursePolicy::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, et CourseController::reorder() sérialise les réordonnancements concurrents d'une même saison en verrouillant la Season avant relecture, ce qui évite l'interblocage par ordre de verrouillage inversé sur les lignes courses.

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 avec request()->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. BackfillDraftDeadlinesCommand réimplémentait à la main le calcul base + draft_ttl_days jours, endOfDay() au lieu de réutiliser la logique de Campaign ; Campaign::draftExpiresAtFrom(CarbonInterface $base) factorise ce calcul (utilisé par draftExpiresAt() avec now() et par le backfill avec registered_at/created_at), la commande n'a plus sa propre copie. Dans PublicRegistrationService::execute(), draftExpiresAt() (calculé avant la transaction), deposit_expires_at et registered_at (posés dans la transaction) appelaient chacun now() 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 $now est désormais capturé avant la transaction et réutilisé pour les trois valeurs.