@mostajs/wagon-engine 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,18 @@
3
3
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
4
  Format : Keep a Changelog ; versions : SemVer.
5
5
 
6
+ ## [0.4.0] — 2026-10-27
7
+
8
+ ### Ajouté
9
+ - **`?suite=`** sur `/inscription` et `/connexion` : la page LOCALE où revenir une fois connecté. Une charge y envoie
10
+ un visiteur sans compte (Speaking : `/inscription?suite=/parcours`). Après l'inscription, s'il n'y a rien à montrer
11
+ (pas de clé d'API, pas de paiement), le visiteur y repart connecté (303) ; une clé d'API ne se montrant qu'une fois,
12
+ la page « inscription faite » reste alors, avec un bouton « Continuer » vers la suite ; un paiement à faire passe
13
+ avant. Après la connexion par mot de passe : 303 vers la suite au lieu de `/compte`. Seul un chemin du site est
14
+ suivi : `//hôte`, `/\hôte`, une adresse absolue, un chemin relatif ou un caractère hors liste sont ignorés
15
+ (redirection ouverte). La connexion par code ou lien de courriel n'en tient pas compte (à venir). Essai T-ENG-10 ;
16
+ sabotages consignés (suite ignorée, garde retirée → T-ENG-10).
17
+
6
18
  ## [0.3.0] — 2026-10-27
7
19
 
8
20
  ### Ajouté
package/README.md CHANGED
@@ -60,6 +60,11 @@ Elle contrôle les comptes, le quota, le paiement (signatures Stripe réelles, b
60
60
  node examples/verifier.mjs # une greffe de vingt lignes démarre : paiement, paliers, texte légal, quota
61
61
  ```
62
62
 
63
+ ## Revenir où l'on était
64
+
65
+ Une page de la charge qui demande un compte envoie le visiteur vers `/inscription?suite=/ma-page` (ou
66
+ `/connexion?suite=/ma-page`) : une fois inscrit ou connecté, il y revient. Seul un chemin du site est suivi.
67
+
63
68
  ## Référence
64
69
 
65
70
  L'API et le contrat complet de la charge sont dans `llms.txt`.
package/llms.txt CHANGED
@@ -28,6 +28,7 @@ démarrage) · exporterCompte(id), effacerCompte(id) (RGPD) · sitemap() · sauv
28
28
  { '/page': { microphone: 'self' }, '/dossier/*': … }, garde-http ≥ 0.2).
29
29
  ctx : chemin, methode, lang, t, ui, page({ titre, contenu }), envoyer, json, rediriger, formulaire, corpsJson({ strict }),
30
30
  compte, consommer(cle), refuser(v), palier({ compte? }), plafond(cle, n, { compte? }), courriel({ to, subject, text, html? }) → bool, admin.
31
+ 0.4 : /inscription?suite=/chemin et /connexion?suite=/chemin — retour à une page LOCALE après inscription (si ni clé ni paiement à montrer ; sinon bouton « Continuer ») ou connexion par mot de passe ; //hôte et adresses absolues ignorés.
31
32
  0.3 : ctx.page({ disposition: 'vitrine'|'application'|'console'|'nu'|<fournie>, application: { retour, titre, avatar, onglets, langues }, console: { menu, contexte, utilisateur } }) (wagon-ui 0.10) ; config.jetons { rayon, police, policeTitres, ombre, fond } ; config.marque.logo / logoClair (SVG en ligne ou chemin /…).
32
33
  0.2 : { compte } = identifiant d'un compte DÉSIGNÉ (requête sans session bornée par le palier d'un compte rattaché par la charge).
33
34
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/wagon-engine",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Le wagon d'un SaaS, une fois pour toutes : comptes, connexion, paliers, quotas, paiement (Stripe, Chargily, SATIM), espace client, administration, pages légales, fr/en/ar. Une greffe n'apporte que wagon.config.mjs, charge/, legal/ et son .env ; la suite de conformité publiée juge la greffe.",
5
5
  "keywords": [
6
6
  "mostajs",
package/src/wagon.mjs CHANGED
@@ -69,6 +69,17 @@ const TEXTES_PAIEMENT = {
69
69
  * 0.5 (audit du 09/10/2026) — LE COMPTE DU CLIENT : changer son mot de passe, le retrouver, partir avec ses données,
70
70
  * supprimer son compte (RGPD), se déconnecter par un POST. Textes propres au gabarit, en trois langues.
71
71
  */
72
+ /**
73
+ * 0.4 — `?suite=` : la page LOCALE où revenir après l'inscription ou la connexion (la charge y renvoie un visiteur
74
+ * sans compte, par ex. /parcours de Speaking). Seul un chemin du site est suivi — jamais `//hôte`, `/\\hôte`, ni une
75
+ * adresse absolue (redirection ouverte) ; sinon le comportement d'avant.
76
+ */
77
+ const suiteSure = (v) => {
78
+ const s = String(v ?? '');
79
+ return s.length <= 300 && /^\/(?![\/\\])[A-Za-z0-9\-._~\/?=&%+:@!$()*,;]*$/.test(s) ? s : null;
80
+ };
81
+ const avecSuite = (chemin, suite) => (suite ? `${chemin}?suite=${encodeURIComponent(suite)}` : chemin);
82
+
72
83
  const TEXTES_COMPTE = {
73
84
  fr: { gerer: 'Mon compte', motDePasse: 'Changer mon mot de passe', actuel: 'Mot de passe actuel', nouveau: 'Nouveau mot de passe', confirmer: 'Le même, encore',
74
85
  enregistrer: 'Enregistrer', mauvais: 'Mot de passe actuel incorrect.', differents: 'Les deux saisies diffèrent.', oublie: 'Mot de passe oublié ?',
@@ -79,7 +90,7 @@ const TEXTES_COMPTE = {
79
90
  exporter: 'Télécharger mes données', supprimer: 'Supprimer mon compte', supprimerAide: 'La suppression efface votre identité et vos données ; les factures sont conservées, anonymisées, le temps exigé par la loi. Pour confirmer, saisissez votre courriel.',
80
91
  confirmation: 'Votre courriel', supprimerBouton: 'Supprimer définitivement', confirmationFausse: 'Le courriel saisi ne correspond pas.',
81
92
  abonnementActif: 'Résiliez d’abord votre abonnement (« Gérer mon abonnement »).', deconnexion: 'Se déconnecter', deconnexionAide: 'Fermer votre session sur cet appareil ?',
82
- trop: 'Trop d’inscriptions depuis cette adresse : réessayez dans quelques minutes.' },
93
+ trop: 'Trop d’inscriptions depuis cette adresse : réessayez dans quelques minutes.' , continuer: 'Continuer' },
83
94
  en: { gerer: 'My account', motDePasse: 'Change my password', actuel: 'Current password', nouveau: 'New password', confirmer: 'The same, again',
84
95
  enregistrer: 'Save', mauvais: 'Current password is incorrect.', differents: 'The two entries differ.', oublie: 'Forgot your password?',
85
96
  oubliTitre: 'Recover access', oubliAide: 'Enter your email: if an account matches, you will receive a link to choose a new password.',
@@ -89,7 +100,7 @@ const TEXTES_COMPTE = {
89
100
  exporter: 'Download my data', supprimer: 'Delete my account', supprimerAide: 'Deletion erases your identity and your data; invoices are kept, anonymised, for the period required by law. To confirm, type your email.',
90
101
  confirmation: 'Your email', supprimerBouton: 'Delete for good', confirmationFausse: 'The email typed does not match.',
91
102
  abonnementActif: 'Cancel your subscription first (“Manage my subscription”).', deconnexion: 'Sign out', deconnexionAide: 'Close your session on this device?',
92
- trop: 'Too many sign-ups from this address: try again in a few minutes.' },
103
+ trop: 'Too many sign-ups from this address: try again in a few minutes.' , continuer: 'Continue' },
93
104
  ar: { gerer: 'حسابي', motDePasse: 'تغيير كلمة المرور', actuel: 'كلمة المرور الحالية', nouveau: 'كلمة المرور الجديدة', confirmer: 'نفسها مرة أخرى',
94
105
  enregistrer: 'حفظ', mauvais: 'كلمة المرور الحالية غير صحيحة.', differents: 'الإدخالان مختلفان.', oublie: 'نسيت كلمة المرور؟',
95
106
  oubliTitre: 'استعادة الوصول', oubliAide: 'أدخل بريدك الإلكتروني: إن كان له حساب، ستتلقى رابطًا لاختيار كلمة مرور جديدة.',
@@ -99,7 +110,7 @@ const TEXTES_COMPTE = {
99
110
  exporter: 'تنزيل بياناتي', supprimer: 'حذف حسابي', supprimerAide: 'يمحو الحذف هويتك وبياناتك؛ وتُحفظ الفواتير مجهولة الهوية المدة التي يفرضها القانون. للتأكيد، اكتب بريدك الإلكتروني.',
100
111
  confirmation: 'بريدك الإلكتروني', supprimerBouton: 'حذف نهائي', confirmationFausse: 'البريد المكتوب غير مطابق.',
101
112
  abonnementActif: 'ألغِ اشتراكك أولًا («إدارة اشتراكي»).', deconnexion: 'تسجيل الخروج', deconnexionAide: 'إغلاق جلستك على هذا الجهاز؟',
102
- trop: 'تسجيلات كثيرة من هذا العنوان: أعد المحاولة بعد دقائق.' },
113
+ trop: 'تسجيلات كثيرة من هذا العنوان: أعد المحاولة بعد دقائق.' , continuer: 'متابعة' },
103
114
  };
104
115
  const DOMAINES_BANQUES = {
105
116
  chargily: 'https://pay.chargily.net https://pay.chargily.com',
@@ -591,12 +602,14 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
591
602
  if (chemin === '/inscription') {
592
603
  const tous = await paliersEnBase();
593
604
  const palierDe = (slug) => tous.find((p) => p.slug === slug && p.active !== false) ?? tous.find((p) => p.slug === (config.planGratuit ?? 'gratuit'));
594
- if (methode === 'GET') return envoyer(200, page({ titre: t('inscription.titre'), contenu: ui.inscription({ lang, palier: palierDe(url.searchParams.get('plan')), methode: methodeConnexion, minMotDePasse: motsDePasse.policy.minLength }) }));
605
+ const suite = suiteSure(url.searchParams.get('suite'));
606
+ const action = avecSuite('/inscription', suite);
607
+ if (methode === 'GET') return envoyer(200, page({ titre: t('inscription.titre'), contenu: ui.inscription({ lang, palier: palierDe(url.searchParams.get('plan')), methode: methodeConnexion, minMotDePasse: motsDePasse.policy.minLength, action }) }));
595
608
  if (!freinInscription.take(`ip:${ipClient(req)}`).ok) return envoyer(429, page({ titre: t('inscription.titre'), contenu: `<p class="msg">${esc((TEXTES_COMPTE[lang] ?? TEXTES_COMPTE.fr).trop)}</p>` }), { 'retry-after': '60' });
596
609
  const f = await formulaire();
597
610
  const palier = palierDe(f.plan);
598
611
  const courriel = String(f.courriel ?? '').trim().toLowerCase();
599
- const refus = (m) => envoyer(200, page({ titre: t('inscription.titre'), contenu: ui.inscription({ lang, palier, erreur: m, methode: methodeConnexion, minMotDePasse: motsDePasse.policy.minLength }) }));
612
+ const refus = (m) => envoyer(200, page({ titre: t('inscription.titre'), contenu: ui.inscription({ lang, palier, erreur: m, methode: methodeConnexion, minMotDePasse: motsDePasse.policy.minLength, action }) }));
600
613
  if (!COURRIEL.test(courriel) || !String(f.nom ?? '').trim()) return refus(t('inscription.courrielAide'));
601
614
  if (await d.findOne(S.User, { email: courriel })) return refus(t('inscription.deja'));
602
615
  if (methodeConnexion === 'motdepasse') {
@@ -610,19 +623,25 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
610
623
  let cle = null;
611
624
  try { cle = (await comptes.cles.creer(String(c.id), { label: t('espace.vosCles'), portees: {} })).full; } catch (e) { if (e.code !== 'PLAFOND_CLES') throw e; }
612
625
  const paiement = Number(palier?.price) > 0 ? { compteId: String(c.id), planSlug: palier.slug } : null;
613
- const contenu = ui.inscriptionFaite({ lang, cle, base: origine, paiement });
626
+ // 0.4 — ?suite= : rien à montrer (ni clé, ni paiement) → on y repart, connecté ; une clé se montre UNE fois, donc
627
+ // la page reste, avec « Continuer » vers la suite. Un paiement à faire passe avant la suite.
628
+ if (suite && !cle && !paiement && methodeConnexion === 'motdepasse') return ouvrirSession(utilisateur.id, suite);
629
+ const continuer = suite && !paiement ? `<p><a class="btn" href="${esc(suite)}">${esc((TEXTES_COMPTE[lang] ?? TEXTES_COMPTE.fr).continuer)}</a></p>` : '';
630
+ const contenu = ui.inscriptionFaite({ lang, cle, base: origine, paiement }) + continuer;
614
631
  if (methodeConnexion === 'code') return envoyer(200, page({ titre: t('inscription.faite'), contenu }));
615
632
  return envoyer(200, page({ titre: t('inscription.faite'), contenu }), { 'set-cookie': cookieStrict('wagon_session', await sessions.ouvrir(utilisateur.id), { maxAge: 30 * 86400, https }) });
616
633
  }
617
634
 
618
635
  // La connexion : mot de passe, ou code + lien par courriel. Un refus ne dit jamais lequel était faux.
619
636
  if (chemin === '/connexion') {
620
- if (methode === 'GET') return envoyer(200, page({ titre: t('espace.seConnecter'), contenu: ui.connexion({ lang, methode: methodeConnexion }) + (oubliPossible ? `<p class="hint"><a href="/connexion/oubli">${esc(TC.oublie)}</a></p>` : '') }));
637
+ const suite = suiteSure(url.searchParams.get('suite'));
638
+ const action = avecSuite('/connexion', suite);
639
+ if (methode === 'GET') return envoyer(200, page({ titre: t('espace.seConnecter'), contenu: ui.connexion({ lang, methode: methodeConnexion, action }) + (oubliPossible ? `<p class="hint"><a href="/connexion/oubli">${esc(TC.oublie)}</a></p>` : '') }));
621
640
  const f = await formulaire();
622
641
  if (methodeConnexion === 'motdepasse') {
623
642
  const r = await motsDePasse.login(String(f.courriel ?? ''), String(f.motdepasse ?? ''), { ip: ipClient(req) });
624
- if (r.ok) return ouvrirSession(r.userId);
625
- return envoyer(r.reason === 'throttled' ? 429 : 200, page({ titre: t('espace.seConnecter'), contenu: ui.connexion({ lang, erreur: t('espace.identifiantsFaux') }) }));
643
+ if (r.ok) return ouvrirSession(r.userId, suite ?? '/compte');
644
+ return envoyer(r.reason === 'throttled' ? 429 : 200, page({ titre: t('espace.seConnecter'), contenu: ui.connexion({ lang, erreur: t('espace.identifiantsFaux'), action }) }));
626
645
  }
627
646
  const courriel = String(f.courriel ?? '').trim().toLowerCase();
628
647
  const dfi = await codes.request(courriel, { ip: ipClient(req) });