Le controller forçait `state = "pending_user_approval"` quel que soit le
sens — commentaire à l'appui : « On force le statut en attente peu
importe le type d'invité ». Une candidature déposée par un utilisateur
ressortait donc en invitation, et il recevait un email « vous avez été
invité » pour une chorale qu'il venait lui-même de demander à rejoindre.
- une adhésion créée par un utilisateur pour lui-même est une
candidature : `pending_admin_approval`, à valider par un administrateur
- rôle imposé à `member` dans ce cas : sans cela, une candidature pouvait
se déclarer `owner` de la chorale visée
- aucun email d'invitation n'est envoyé pour une candidature
- l'invitation, elle, garde son comportement : `pending_user_approval`,
token pour un email externe, notification pour un compte existant
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le créateur d'une chorale n'avait aucune adhésion : la chorale
n'apparaissait pas dans « Mes chorales » et son créateur n'en était pas
propriétaire — il n'avait donc aucun droit sur ce qu'il venait de créer.
Seule la recherche la montrait.
Le controller crée désormais une choral-membership `owner` / `active`
après la création, et journalise l'anomalie si l'id de la chorale n'est
pas exploitable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Trois erreurs de configuration successives — clés R2 pointant sur
l'ancien MinIO, `wwhsec_` au lieu de `whsec_`, `k_test_` amputé de son
`s` — n'ont produit aucun message exploitable. Chacune se manifestait
très loin de sa cause : un upload qui échoue, un webhook rejeté.
src/env-check.ts vérifie au bootstrap la forme des variables sensibles
(Stripe, R2, SMTP) : présence, préfixe, longueur, et les altérations de
copier-coller (guillemets englobants, espaces parasites). Les trois
erreurs ci-dessus sont détectées.
Ne bloque pas le démarrage — une clé SMTP erronée ne doit pas empêcher
le site de servir — mais journalise en erreur, une ligne par anomalie.
Aucune valeur n'est journalisée, uniquement des longueurs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un corps brut absent et un secret de signature erroné produisent des
messages Stripe proches, alors que les correctifs sont opposés. Le log
d'échec indique désormais si le corps brut est présent et sa taille, si
l'en-tête stripe-signature est là, et si STRIPE_WEBHOOK_SECRET est défini
(sa longueur, jamais sa valeur).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Le controller `me` renvoyait la ligne brute de `strapi.db.query`, sans
passer par le sanitizer utilisé par `findOne` : le hash du mot de passe,
`resetPasswordToken` et `confirmationToken` étaient envoyés au navigateur
à chaque appel — et /users/me est appelé sur presque toutes les pages.
Les trois champs sont retirés explicitement plutôt que via
`sanitizeUser` : le sanitizer de l'API de contenu retirerait aussi les
relations peuplées que le front consomme dans cette réponse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un abonnement premium souscrit en test aboutissait chez Stripe sans
qu'aucune commande ni aucun droit n'apparaisse en base — et sans la
moindre trace côté serveur.
- `metadata.userId` absent faisait sortir le handler en silence avec un
200 : c'est désormais journalisé en erreur, avec l'id de session Stripe
- l'utilisateur est résolu une fois par `documentId` (ce que le front
envoie) : la relation `user` d'une commande attend un id numérique,
elle recevait un documentId
- un utilisateur introuvable est journalisé au lieu d'échouer plus loin
- les types d'événements ignorés sont tracés, pour distinguer « Stripe
n'appelle pas » de « Stripe appelle sans checkout.session.completed »
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Piège rencontré sur la publication d'annonce : ad/post/group/chat-message
lisent ctx.request.files.<attribut> directement. Envoyer files.<attribut>
n'échoue pas, les fichiers sont simplement ignorés.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Règle apprise en corrigeant les commentaires invisibles sur la page de
détail d'une publication (0.13.16) : le sanitizer de l'API REST retire
sans erreur les relations peuplées vers un content-type non lisible par
le rôle. Les controllers custom qui répondent via ctx.send() ne passent
pas par ce sanitizer et masquent le problème.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Les commentaires n'apparaissaient pas sur la page de détail d'une
publication, alors qu'ils s'affichent dans le fil.
`comment` n'avait aucune permission déclarée. Le fil passe par le
controller custom `feed`, qui renvoie ses résultats via ctx.send() sans
sanitizer d'API ; la page de détail passe par l'API REST standard, dont
le sanitizer retire les relations peuplées vers un content-type que le
rôle n'a pas le droit de lire.
Lecture seule : l'écriture reste passée par `post.addComment`, qui
contrôle l'auteur.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`configuration.privacy.profileVisibility` n'avait pas de valeur par
défaut : un composant `privacy` créé sans choix explicite laissait le
champ nul, que la recherche d'utilisateurs traitait comme non-public.
L'écran de confidentialité du front affiche « public » dans ce cas —
le schéma s'aligne.
Ne rétroagit pas sur les utilisateurs existants, qui n'ont aucun
composant `privacy` : c'est le front (0.18.3) qui les traite comme
publics.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tous les uploads échouaient depuis mai 2026 sur « Credential access key
has length 5, should be 32 ».
config/env/production/plugins.ts surchargeait l'upload avec l'ancien
MinIO auto-hébergé (accessKeyId "admin", 5 caractères, endpoint
container.harmonylab.ovh aujourd'hui injoignable). Strapi fusionnant les
surcharges d'environnement en profondeur, la config effective en prod
n'était ni l'une ni l'autre : endpoint R2 hérité de config/plugins.ts,
mais identifiants MinIO, le provider lisant s3Options.credentials en
priorité sur les options à plat.
- surcharge production supprimée : une seule config plugins, les
différences d'environnement passent par les variables d'environnement
- upload en forme s3Options attendue par @strapi/provider-upload-aws-s3 5.x
(à plat, le provider n'accepte plus qu'au prix d'une dépréciation)
- secrets R2 et SMTP sortis du fichier vers env(), .env.example à jour
- CSP de production : les médias étaient autorisés depuis
container.harmonylab.ovh et 192.168.0.211:9000, jamais depuis le domaine
R2 — l'hôte est désormais dérivé de R2_PUBLIC_URL
- la surcharge production des middlewares réduisait strapi::body à sa forme
par défaut, perdant includeUnparsed nécessaire au webhook Stripe
⚠️ Déploiement : poser R2_* et SMTP_PASSWORD dans Dokploy AVANT de
déployer. Les clés historiques restent dans l'historique git : rotation
R2 et ZeptoMail à faire.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- webhook : crédite maxChoirs selon le plan (pro=1, premium=5), écrit
uniquement des champs existants du schéma (isPremium supprimé,
subscriptionId -> stripeSubscriptionId)
- schéma user : ajout stripeCustomerId / stripeSubscriptionId (private)
- controller choral.create : contrôle du crédit côté serveur + décrément
après création
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>