Compare commits
5 Commits
5a8a7d9f66
...
57faa37e70
| Author | SHA1 | Date | |
|---|---|---|---|
| 57faa37e70 | |||
| 87842c7585 | |||
| 3da128278d | |||
| 883b827b0d | |||
| 5be8873ad5 |
@@ -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
|
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
|
fichier (et jamais cochée uniquement dans l'admin, sinon elle sera signalée
|
||||||
comme non déclaré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é ;
|
- Secrets uniquement en variables d'environnement. `.env` jamais commité ;
|
||||||
`.env.example` maintenu à jour. Depuis le 26/07/2026, `config/plugins.ts`
|
`.env.example` maintenu à jour. Depuis le 26/07/2026, `config/plugins.ts`
|
||||||
ne contient plus aucun secret (R2 et SMTP passent par `env()`).
|
ne contient plus aucun secret (R2 et SMTP passent par `env()`).
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "harmony-back",
|
"name": "harmony-back",
|
||||||
"version": "0.13.16",
|
"version": "0.13.19",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "A Strapi application",
|
"description": "A Strapi application",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -18,7 +18,24 @@ export default factories.createCoreController(
|
|||||||
process.env.STRIPE_WEBHOOK_SECRET!,
|
process.env.STRIPE_WEBHOOK_SECRET!,
|
||||||
);
|
);
|
||||||
} catch (err: any) {
|
} 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}`);
|
return ctx.badRequest(`Webhook Error: ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,15 +59,37 @@ export default factories.createCoreController(
|
|||||||
premium: 5,
|
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 {
|
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
|
// Création de la commande
|
||||||
await strapi.documents("api::order.order").create({
|
await strapi.documents("api::order.order").create({
|
||||||
data: {
|
data: {
|
||||||
stripeId: session.id,
|
stripeId: session.id,
|
||||||
amount: session.amount_total ? session.amount_total / 100 : 0,
|
amount: session.amount_total ? session.amount_total / 100 : 0,
|
||||||
planType: planType,
|
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
|
// 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
|
// 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);
|
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 = {
|
const result = {
|
||||||
...JSON.parse(JSON.stringify(fullUser)),
|
...safeUser,
|
||||||
stats,
|
stats,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user