Compare commits

...

5 Commits

Author SHA1 Message Date
admin 57faa37e70 0.13.19 : tell apart the two causes of a Stripe signature failure
Build release Docker image / Build Docker Images (push) Successful in 8m27s
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>
2026-07-27 00:39:07 +02:00
admin 87842c7585 0.13.18 : stop leaking password hash and tokens from /users/me
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>
2026-07-27 00:33:05 +02:00
admin 3da128278d 0.13.17 : make the Stripe webhook say when it credits nothing
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>
2026-07-27 00:32:19 +02:00
admin 883b827b0d docs: warn about custom create controllers bypassing files.<attr>
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>
2026-07-27 00:15:35 +02:00
admin 5be8873ad5 docs: note that REST-populated relations need find permission
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>
2026-07-26 19:57:34 +02:00
4 changed files with 79 additions and 5 deletions
+18
View File
@@ -72,6 +72,24 @@ Vue par domaine (34 au total, liste exhaustive dans `src/api/`) :
Toute nouvelle route/action consommée par le front DOIT être ajoutée à ce
fichier (et jamais cochée uniquement dans l'admin, sinon elle sera signalée
comme non déclarée).
- ⚠️ **Les controllers `create` custom qui gèrent des fichiers n'utilisent PAS
la convention `files.<attribut>` de Strapi.** `ad`, `post`, `group` et
`chat-message` lisent directement `ctx.request.files.<attribut>` (donc un
champ multipart nommé `medias`, `media`… et non `files.medias`), uploadent
eux-mêmes puis rattachent les ids. Envoyer `files.<attribut>` à l'un d'eux
ne produit **aucune erreur** : l'entité est créée, les fichiers sont
ignorés en silence. Avant de brancher un formulaire avec upload, lire le
controller du content-type visé. Symptôme : « le contenu s'enregistre mais
pas l'image ».
- ⚠️ **Une relation peuplée via l'API REST exige `find` sur le content-type
cible.** Le sanitizer de l'API REST retire silencieusement les relations
peuplées vers un content-type que le rôle n'a pas le droit de lire — pas
d'erreur, juste un champ vide. C'est ce qui rendait les commentaires
invisibles sur la page de détail d'une publication (corrigé en 0.13.16),
alors qu'ils s'affichaient dans le fil : les controllers custom qui
renvoient via `ctx.send()` ne passent pas par ce sanitizer, et masquent
donc le problème. Symptôme à reconnaître : « ça marche dans le fil, pas
dans le détail ».
- Secrets uniquement en variables d'environnement. `.env` jamais commité ;
`.env.example` maintenu à jour. Depuis le 26/07/2026, `config/plugins.ts`
ne contient plus aucun secret (R2 et SMTP passent par `env()`).
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "harmony-back",
"version": "0.13.16",
"version": "0.13.19",
"private": true,
"description": "A Strapi application",
"scripts": {
+46 -3
View File
@@ -18,7 +18,24 @@ export default factories.createCoreController(
process.env.STRIPE_WEBHOOK_SECRET!,
);
} catch (err: any) {
strapi.log.error(`❌ Erreur de signature Webhook: ${err.message}`);
// Deux causes très différentes se ressemblent ici. Corps brut absent
// (middleware `strapi::body` sans `includeUnparsed`) : Stripe se
// plaint du payload. Corps brut présent mais signature invalide :
// c'est le secret qui ne correspond pas à l'endpoint appelant —
// chaque endpoint Stripe a le sien.
strapi.log.error(
`❌ Erreur de signature Webhook: ${err.message} ` +
`[corps brut: ${
typeof unparsedBody === "string" || Buffer.isBuffer(unparsedBody)
? `présent, ${unparsedBody.length} octets`
: `ABSENT (${typeof unparsedBody})`
}, en-tête stripe-signature: ${sig ? "présent" : "ABSENT"}, ` +
`STRIPE_WEBHOOK_SECRET: ${
process.env.STRIPE_WEBHOOK_SECRET
? `défini (${process.env.STRIPE_WEBHOOK_SECRET.length} car.)`
: "NON DÉFINI"
}]`,
);
return ctx.badRequest(`Webhook Error: ${err.message}`);
}
@@ -42,15 +59,37 @@ export default factories.createCoreController(
premium: 5,
};
if (userId) {
if (!userId) {
// Sans `userId` dans les métadonnées, l'ancien code sortait en
// silence avec un 200 : le paiement aboutissait chez Stripe sans
// qu'aucune commande ni aucun droit n'apparaisse, et sans trace.
strapi.log.error(
`❌ Webhook ${event.type} sans metadata.userId (session ${session.id}) : aucun droit crédité`,
);
} else {
try {
// Les métadonnées portent un `documentId` (cf. le front,
// UserSubscriptionTab). La relation `user` d'une commande attend
// un id numérique : on résout l'utilisateur une fois pour les deux.
const user = await strapi.db
.query("plugin::users-permissions.user")
.findOne({ where: { documentId: userId } });
if (!user) {
strapi.log.error(
`❌ Webhook : aucun utilisateur pour documentId ${userId} (session ${session.id})`,
);
ctx.send({ received: true });
return;
}
// Création de la commande
await strapi.documents("api::order.order").create({
data: {
stripeId: session.id,
amount: session.amount_total ? session.amount_total / 100 : 0,
planType: planType,
user: userId,
user: user.id,
},
});
@@ -74,6 +113,10 @@ export default factories.createCoreController(
// On répond quand même 200 à Stripe pour éviter les retries infinis
}
}
} else {
// Permet de distinguer « Stripe n'appelle pas le webhook » de
// « Stripe appelle mais n'envoie pas checkout.session.completed ».
strapi.log.info(`️ Événement ${event.type} ignoré`);
}
// INDISPENSABLE : Répondre 200 OK à Stripe pour TOUS les événements
@@ -250,8 +250,21 @@ module.exports = (plugin) => {
const stats = await getUserStats(ctx.state.user.id);
// `strapi.db.query` renvoie la ligne brute : sans cette omission, /users/me
// exposait au navigateur le hash du mot de passe et les jetons de
// réinitialisation et de confirmation.
// On retire ces champs explicitement plutôt que de passer par
// `sanitizeUser` : le sanitizer de l'API de contenu retirerait aussi les
// relations peuplées que le front consomme ici.
const {
password: _password,
resetPasswordToken: _resetPasswordToken,
confirmationToken: _confirmationToken,
...safeUser
} = JSON.parse(JSON.stringify(fullUser));
const result = {
...JSON.parse(JSON.stringify(fullUser)),
...safeUser,
stats,
};