diff --git a/package.json b/package.json index 47ab146..a6625bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "harmony-back", - "version": "0.13.19", + "version": "0.13.20", "private": true, "description": "A Strapi application", "scripts": { diff --git a/src/env-check.ts b/src/env-check.ts new file mode 100644 index 0000000..5d61a91 --- /dev/null +++ b/src/env-check.ts @@ -0,0 +1,164 @@ +import type { Core } from "@strapi/strapi"; + +/** + * Contrôle des variables d'environnement au démarrage. + * + * Motivation : 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 s'est manifestée très + * loin de sa cause — un upload qui échoue, un webhook rejeté — et a coûté des + * heures de diagnostic. + * + * Ce module ne vérifie que ce qui est vérifiable sans appel réseau : présence, + * préfixe, longueur, et les altérations de copier-coller (guillemets + * englobants, espaces parasites). Il ne valide évidemment pas qu'une clé est + * la *bonne* — seulement qu'elle a la forme attendue. + * + * Il ne bloque pas le démarrage : une clé SMTP erronée ne doit pas empêcher + * le site de servir. Il journalise en erreur, ce qui suffit à orienter le + * diagnostic dès le boot. + * + * ⚠️ Aucune valeur n'est journalisée, jamais — uniquement des longueurs. + */ + +type EnvRule = { + name: string; + /** Préfixe attendu, le cas échéant. */ + prefix?: string; + /** Longueur exacte attendue. */ + length?: number; + /** Longueur minimale attendue. */ + minLength?: number; + /** Contexte affiché dans le message, pour orienter la correction. */ + usedFor: string; +}; + +const ENV_RULES: EnvRule[] = [ + // --- Paiement + { + name: "STRIPE_SECRET_KEY", + prefix: "sk_", + minLength: 20, + usedFor: "appels à l'API Stripe", + }, + { + name: "STRIPE_WEBHOOK_SECRET", + prefix: "whsec_", + minLength: 30, + usedFor: "vérification de signature des webhooks Stripe", + }, + + // --- Stockage des médias (Cloudflare R2) + { + name: "R2_ACCESS_KEY_ID", + length: 32, + usedFor: "upload des médias", + }, + { + name: "R2_SECRET_ACCESS_KEY", + length: 64, + usedFor: "upload des médias", + }, + { + name: "R2_ENDPOINT", + prefix: "https://", + usedFor: "upload des médias", + }, + { + name: "R2_BUCKET", + minLength: 1, + usedFor: "upload des médias", + }, + { + name: "R2_PUBLIC_URL", + prefix: "https://", + usedFor: "URLs publiques des médias", + }, + + // --- Email transactionnel + { + name: "SMTP_PASSWORD", + minLength: 10, + usedFor: "envoi des emails transactionnels", + }, +]; + +/** Guillemets englobants ou espaces parasites laissés par un copier-coller. */ +const findCopyPasteDamage = (raw: string): string | null => { + if (raw !== raw.trim()) { + return "espace ou retour à la ligne en début ou fin de valeur"; + } + const quoted = + (raw.startsWith('"') && raw.endsWith('"')) || + (raw.startsWith("'") && raw.endsWith("'")); + if (quoted && raw.length > 1) { + return "valeur entourée de guillemets (ils font partie de la valeur)"; + } + return null; +}; + +const checkRule = (rule: EnvRule): string[] => { + const raw = process.env[rule.name]; + + if (raw === undefined || raw === "") { + return [`${rule.name} n'est pas définie — ${rule.usedFor}`]; + } + + const problems: string[] = []; + + const damage = findCopyPasteDamage(raw); + if (damage) { + problems.push(`${rule.name} : ${damage}`); + } + + const value = raw.trim(); + + if (rule.prefix && !value.startsWith(rule.prefix)) { + problems.push( + `${rule.name} ne commence pas par « ${rule.prefix} » — ${rule.usedFor}`, + ); + } + + if (rule.length !== undefined && value.length !== rule.length) { + problems.push( + `${rule.name} fait ${value.length} caractères, ${rule.length} attendus — ${rule.usedFor}`, + ); + } + + if (rule.minLength !== undefined && value.length < rule.minLength) { + problems.push( + `${rule.name} fait ${value.length} caractères, au moins ${rule.minLength} attendus — ${rule.usedFor}`, + ); + } + + return problems; +}; + +/** + * Vérifie la forme des variables d'environnement et journalise les anomalies. + * @returns le nombre d'anomalies détectées + */ +export const checkEnvironment = (strapi: Core.Strapi): number => { + const problems = ENV_RULES.flatMap(checkRule); + + if (problems.length === 0) { + strapi.log.info( + `[env-check] ${ENV_RULES.length} variables vérifiées, aucune anomalie de forme`, + ); + return 0; + } + + strapi.log.error( + `[env-check] ${problems.length} anomalie(s) détectée(s) dans les variables d'environnement :`, + ); + for (const problem of problems) { + strapi.log.error(`[env-check] ✗ ${problem}`); + } + strapi.log.error( + "[env-check] Ces valeurs viennent de l'environnement de déploiement " + + "(Dokploy en production, .env en local). Une variable modifiée n'est " + + "relue qu'au redémarrage du processus.", + ); + + return problems.length; +}; diff --git a/src/index.ts b/src/index.ts index d07bfcb..4185314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ import type { Core } from "@strapi/strapi"; +import { checkEnvironment } from "./env-check"; import { syncRolePermissions } from "./permissions-sync"; export default { @@ -26,6 +27,12 @@ export default { * run jobs, or perform some special logic. */ async bootstrap({ strapi }: { strapi: Core.Strapi }) { + // Contrôle de forme des variables d'environnement (préfixes, longueurs, + // guillemets parasites). Ne bloque pas le démarrage, mais signale au boot + // les erreurs de configuration qui, sinon, ne se manifestent que très + // loin de leur cause — cf. src/env-check.ts. + checkEnvironment(strapi); + // Synchronise les permissions Users & Permissions (source de vérité // versionnée dans src/permissions-sync.ts — additif, idempotent). await syncRolePermissions(strapi);