Lien d'inscription protégé par jeton, reçus fiscaux mécénat, infobulles et revue de sécurité
- Lien d'inscription protégé par un jeton (FormTemplate.access_token, /inscription/<token>), avec régénération depuis l'écran de configuration. - Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en mécénat, donateur/montant (chiffres et toutes lettres en français, app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel. - Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée. - Infobulles (HelpTip et détail de cellule de la grille des besoins) repositionnées en position: fixed via JS, bornées à l'écran. - Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une URL (fuyait le jeton d'inscription), sujet retiré du log de remise des emails (pouvait porter un nom de bénévole), photo du formulaire public revalidée par décodage réel avant stockage, export Excel protégé contre l'injection de formule, sondage du statut de paiement limité en fréquence, pyjwt mis à jour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a2b748566b
commit
096b7cd26a
@@ -203,9 +203,40 @@ les tables sauf les tables d'association.
|
||||
sur l'écran de configuration des bénévoles (`ConfigVolunteers.tsx`), même
|
||||
mécanique de téléchargement que les badges/feuilles de poste (blob →
|
||||
`<a download>` généré côté client, voir `BadgesPage.tsx`).
|
||||
- **Reçus fiscaux mécénat** (PDF, `app/mecenat/pdf.py` + `GET
|
||||
/editions/{id}/volunteers/mecenat-receipts`, COORDINATION) : un cerfa
|
||||
11580*05 officiel (`app/mecenat/cerfa-mecenat-particuliers.pdf`, PDF sans
|
||||
champ AcroForm — les informations sont donc dessinées par-dessus la page 2
|
||||
du modèle, aux coordonnées mesurées sur ce document précis) par bénévole
|
||||
ayant payé un type de paiement « mécénat » (repérés par
|
||||
`Volunteer.fee_payment_status == PAID` et `mecenat_address` renseigné —
|
||||
voir `Paiement de la cotisation` plus bas), regroupés dans un seul PDF
|
||||
(2 pages par reçu, comme le cerfa). Seuls l'encadré « Donateur » (nom,
|
||||
prénom, adresse), le montant (en chiffres **et en toutes lettres**,
|
||||
`app/mecenat/number_words.py:amount_in_words` — conversion française pure,
|
||||
testée séparément des coordonnées PDF vu le nombre d'exceptions
|
||||
orthographiques : accord de « quatre-vingts »/« cents », « et onze »
|
||||
irrégulier à 71) et la date du versement sont complétés ; l'encadré
|
||||
« Organisme bénéficiaire » (page 1) reste vierge, à la main,
|
||||
volontairement — pas de données de l'association en base à ce jour.
|
||||
`pypdf` (nouvelle dépendance) fusionne l'overlay ReportLab sur la page du
|
||||
modèle. Bouton « Reçus mécénat (PDF) » sur l'écran de configuration des
|
||||
bénévoles, même mécanique de téléchargement que l'export Excel ;
|
||||
422 `mecenat.no_receipts` si aucun bénévole ne qualifie.
|
||||
- **`FormTemplate`** : configuration du formulaire public d'inscription
|
||||
d'une édition (une ligne, créée à la demande). `is_open` coupe les
|
||||
nouvelles soumissions sans effacer la configuration. `sumup_merchant_code`/
|
||||
nouvelles soumissions sans effacer la configuration. `access_token`
|
||||
(aléatoire, `secrets.token_urlsafe(16)` à la création) protège l'URL du
|
||||
formulaire public : celle-ci est `/inscription/{access_token}`, jamais
|
||||
`/inscription` seul — sans le jeton, impossible de deviner ou de tomber
|
||||
sur le lien (`GET .../form/public`, `GET .../form/logo` et
|
||||
`POST .../form/public/submit` l'exigent en plus de `edition_id`,
|
||||
comparaison en temps constant via `secrets.compare_digest`, 404 générique
|
||||
sinon). `POST .../form/regenerate-token` (COORDINATION) le renouvelle —
|
||||
l'ancien lien cesse aussitôt de fonctionner, à repartager. Les endpoints
|
||||
liés à une soumission en attente de paiement (`submit_mecenat_info`,
|
||||
`get_pending_status`, `retry_pending_payment`, le webhook SumUp) ne sont
|
||||
pas concernés : déjà protégés par le jeton propre à chaque soumission. `sumup_merchant_code`/
|
||||
`sumup_api_key` sont les identifiants du compte marchand SumUp pour le
|
||||
paiement de la cotisation (`app/sumup/`) — `sumup_api_key` est un secret,
|
||||
jamais renvoyé par l'API (`FormTemplateRead.sumup_api_key_set: bool` à la
|
||||
@@ -423,9 +454,12 @@ ni corps**). Config : `SMTP_HOST/PORT/USER/PASSWORD/STARTTLS/SSL/FROM`,
|
||||
clients mail possible). Construite par `build_message`
|
||||
(`app/email/sender.py`), pour **tous** les emails envoyés, sans exception.
|
||||
Logo de l'édition en en-tête si elle en a importé un pour son formulaire
|
||||
public (`app/email/branding.py:edition_logo_url`, `FormTemplate.logo` —
|
||||
lien vers `GET .../form/logo`, public) ; à défaut, un bandeau texte avec
|
||||
le sujet.
|
||||
public (`app/email/branding.py:edition_logo`, `FormTemplate.logo`) ; à
|
||||
défaut, un bandeau texte avec le sujet. Intégré en pièce jointe « inline »
|
||||
(Content-ID, `LOGO_CID`) plutôt que via une URL vers `GET .../form/logo` :
|
||||
cette route exige désormais le `access_token` du formulaire public (§ 2,
|
||||
`FormTemplate`) — une URL externe dans un email l'aurait exposé en clair à
|
||||
quiconque lit la source du message, pas seulement à son destinataire.
|
||||
|
||||
- **Lien magique** : `POST .../auth/magic-link` envoie l'email contenant
|
||||
`PUBLIC_BASE_URL` + `magic_link_path` + `?token=`. Sans SMTP, le jeton (non
|
||||
@@ -570,12 +604,18 @@ positionnement implicite (auto-flow) recréerait la même classe de bug.
|
||||
Vérifié par `scripts/smoke.tsx:checkGridLayout` (inspecte les `grid-row`
|
||||
rendus, pas seulement l'absence de crash — un décalage CSS n'est pas visible
|
||||
dans un simple rendu SSR). Hiérarchie de `z-index` volontairement large et
|
||||
sans égalité (coins figés 20/21 > en-têtes jour/heure 12/11 > libellés de
|
||||
poste 10 > cellules de créneau, chacune isolée dans son propre contexte
|
||||
d'empilement — `relative z-0` — pour que son infobulle au survol, `z-20`,
|
||||
ne rivalise jamais avec les en-têtes figés) : deux éléments sticky à égalité
|
||||
de `z-index` peuvent se chevaucher de façon imprévisible pendant un
|
||||
défilement selon le navigateur. Largeur adaptative par jour (`columnWidthPx`,
|
||||
sans égalité entre les éléments sticky de la grille (coins figés 20/21 >
|
||||
en-têtes jour/heure 12/11 > libellés de poste 10) : deux éléments sticky à
|
||||
égalité de `z-index` peuvent se chevaucher de façon imprévisible pendant un
|
||||
défilement selon le navigateur. L'infobulle au survol d'une cellule (détail
|
||||
du créneau) est en `position: fixed`, positionnée par JS
|
||||
(`lib/useTooltipPosition.ts`, réutilisé par `HelpTip`) plutôt qu'ancrée en
|
||||
`absolute` sur la cellule : un ancrage `absolute` la découpait au bord de la
|
||||
zone de défilement (`overflow-auto`) et la coinçait sous les en-têtes figés
|
||||
quel que soit son propre `z-index` (un enfant ne dépasse jamais le contexte
|
||||
d'empilement de son parent) — bug corrigé une fois (infobulle invisible,
|
||||
« sous tout »), un ancrage `absolute`/`z-index` recréerait la même classe de
|
||||
bug. Largeur adaptative par jour (`columnWidthPx`,
|
||||
en fonction du nombre de tranches) : peu de tranches ⇒ colonnes larges,
|
||||
beaucoup ⇒ largeur compacte avec défilement horizontal — jamais de colonnes
|
||||
étroites par défaut.
|
||||
@@ -657,6 +697,7 @@ l'étape 21.)*
|
||||
│ │ ├── solver/ # suggestions OR-Tools (models, engine, loader)
|
||||
│ │ ├── badges/ # génération PDF des planches (pdf.py, ReportLab)
|
||||
│ │ ├── sheets/ # génération PDF des feuilles de poste (pdf.py)
|
||||
│ │ ├── mecenat/ # reçus fiscaux cerfa (pdf.py, pypdf + ReportLab) + le cerfa officiel
|
||||
│ │ ├── forms/ # hCaptcha (captcha.py) — voir api/forms.py
|
||||
│ │ ├── sumup/ # client API SumUp (client.py) — paiement de la cotisation
|
||||
│ │ ├── bootstrap.py # instance vierge : édition + 1er compte
|
||||
@@ -769,12 +810,17 @@ Aucune dépendance locale requise hors Docker.
|
||||
| 18 | Gestion des référents depuis la fiche bénévole : `PATCH /accounts` (rôle LEAD ↔ VOLUNTEER), `lead_account_id` vérifié (doit être un compte référent de l'édition) | à valider |
|
||||
| 19 | Revue de sécurité avant ouverture au public : refus de démarrer en production avec un `JWT_SECRET` par défaut/vide/court, avertissement si `EMAIL_ENABLED=false` en production, en-têtes de sécurité (nginx + Caddy), pas de jeton dans les logs d'accès du frontend, limiteur de tentatives sur `/auth/login` et `/auth/magic-link` | à valider |
|
||||
| 20 | Planches de badges bénévoles (PDF) : recto personnalisable (fond + champs nom/poste(s)/catégorie/rôle, éditeur glisser-déposer), verso fixe (même image pour tous), écran « Badges » dédié (`/badges`, COORDINATION) | à valider |
|
||||
| 21 | Formulaire public d'inscription (`/inscription`, sans authentification) : champs activables par la coordination (email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir, vœu de compagnon en texte libre, disponibilité/cotisation déclaratives, photo, commentaire), captcha hCaptcha, ouverture/fermeture, lien à partager, écran de configuration dédié (`/inscriptions`, COORDINATION) — les bénévoles créés rejoignent directement la liste | à valider |
|
||||
| 21 | Formulaire public d'inscription (`/inscription/{access_token}`, sans authentification — voir étape 27) : champs activables par la coordination (email, téléphone, vœux de poste — max 2, vœux d'artiste — un par soir, vœu de compagnon en texte libre, disponibilité/cotisation déclaratives, photo, commentaire), captcha hCaptcha, ouverture/fermeture, lien à partager, écran de configuration dédié (`/inscriptions`, COORDINATION) — les bénévoles créés rejoignent directement la liste | à valider |
|
||||
| 22 | Feuilles de poste (PDF, complément des badges) : une page par poste et par jour avec le référent, les créneaux, les bénévoles affectés et leur téléphone — bouton depuis la grille des besoins (COORDINATION) ; photo du bénévole affichée sur son écran et sur l'écran référent (en face du nom de chaque membre de l'équipe) | à valider |
|
||||
| 23 | Créneaux : plus de génération automatique à la création d'un poste — création manuelle à horaire libre (`ConfigGrid.tsx`), génération depuis les créneaux types conservée en option (« Générer les créneaux ») ; grille des besoins redessinée en vrai axe des temps (une colonne = 30 min, bornée à la plage utilisée par jour, cases fusionnées pour chaque créneau — `buildTimeGrid`/`placeInTimeGrid`) | à valider |
|
||||
| 24 | Corrections de stabilité de la grille des besoins (suite à l'étape 23) : positionnement explicite (`gridColumn`/`gridRow`) de chaque cellule (un poste aux créneaux non alignés sur 30 min ne décale plus les postes suivants) ; hiérarchie de `z-index` sans égalité entre les en-têtes figés et isolation de l'infobulle de survol dans son propre contexte d'empilement (ne recouvrait plus la ligne « Affectés / idéal » pendant un défilement) | à valider |
|
||||
| 25 | Paiement de la cotisation via l'API SumUp (remplace le lien statique Yapla) : plusieurs types de paiement configurables par édition (`FormPaymentOption` — libellé + montant, ex. « Cotisation classique », « Mécénat »), inscription en deux temps pour un paiement en ligne (`PendingFormSubmission` : le `Volunteer` n'est créé qu'à la confirmation du paiement, jamais avant), étape d'adresse postale pour un type « mécénat » (cerfa), page de confirmation en direct au retour de SumUp avec relance possible après échec, tous les champs activés désormais obligatoires, statut (`PENDING`/`PAID`/`FAILED`/`EXPIRED`) tenu à jour par webhook et visible (libellé + montant + statut) sur la fiche du bénévole, email de confirmation d'inscription à la création du bénévole (tenant lieu de reçu quand un paiement est confirmé), mise en forme HTML au thème de l'appli (logo de l'édition en en-tête) pour tous les emails envoyés | à valider |
|
||||
| 26 | Export Excel de la liste des bénévoles (`GET /editions/{id}/volunteers/export`, COORDINATION, `app/exporting/xlsx.py`) : coordonnées, vœux (résolus en noms lisibles), cotisation et mécénat — bouton dédié sur l'écran de configuration des bénévoles | à valider |
|
||||
| 27 | Lien d'inscription protégé par jeton (`FormTemplate.access_token`, voir § 2) : l'URL du formulaire public devient `/inscription/{access_token}` (`/inscription` seul ne mène plus nulle part), bouton « Régénérer » sur l'écran de configuration (`/inscriptions`) pour révoquer un lien qui aurait fuité | à valider |
|
||||
| 28 | Reçus fiscaux mécénat (`GET /editions/{id}/volunteers/mecenat-receipts`, COORDINATION, `app/mecenat/pdf.py`) : un cerfa 11580*05 officiel par bénévole ayant payé en mécénat, donateur/montant (chiffres et toutes lettres en français, `number_words.py`)/date complétés par-dessus le modèle (`pypdf` + ReportLab), organisme bénéficiaire volontairement laissé vierge — bouton dédié sur l'écran de configuration des bénévoles | à valider |
|
||||
| 29 | Jeu de démonstration : 8 bénévoles avec une cotisation classique payée, 4 avec une cotisation mécénat payée (adresse complète) — `app/seed.py:_add_demo_payments` — pour que l'export Excel et les reçus fiscaux mécénat (étape 28) aient de quoi montrer quelque chose d'emblée | à valider |
|
||||
| 30 | Infobulles (`HelpTip` et détail de cellule de la grille des besoins) repositionnées en `position: fixed` via JS (`lib/useTooltipPosition.ts`), bornées à l'intérieur de l'écran et basculant de côté si la place manque — corrige un centrage `absolute` qui les faisait déborder de l'écran près d'un bord, et une isolation `z-index` qui rendait celle de la grille invisible (coincée sous les en-têtes figés) | à valider |
|
||||
| 31 | Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une URL (fuyait le `access_token` du formulaire public à tout destinataire — régression de l'étape 27, aucun test ne couvrait le rendu HTML du logo), sujet retiré du log de remise des emails (pouvait porter le nom d'un bénévole via un modèle personnalisé), image du formulaire public revalidée par décodage réel (`PIL`, pas seulement le `Content-Type` déclaré) avant stockage — évite qu'une fausse image fasse échouer la génération des badges/feuilles de poste, export Excel des bénévoles protégé contre l'injection de formule (CWE-1236, `app/exporting/xlsx.py:_safe_text`), sondage du statut de paiement en attente limité en fréquence par jeton (`pending_status_limiter`, évite un abus de l'API SumUp), `pyjwt` mis à jour (CVE listées non exploitables ici — HS256 à secret statique uniquement, jamais de JWKS — mais autant ne pas rester sur une version vulnérable) | à valider |
|
||||
|
||||
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
|
||||
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
|
||||
|
||||
Reference in New Issue
Block a user