@mostajs/wagon-engine 0.7.0 → 0.8.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,29 @@
3
3
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
4
  Format : Keep a Changelog ; versions : SemVer.
5
5
 
6
+ ## [0.8.0] — 2026-10-29
7
+
8
+ ### Ajouté — les membres d'un compte (demandé par CaisseTrax : écart E-08 de SPEC-ECRANS-CAISSETRAX)
9
+ - `ctx.utilisateur` (la personne de la session, sans empreinte) et `ctx.role` (son rôle dans le compte) ; `null`
10
+ sans session et pour une clé d'API.
11
+ - `ctx.membres` (session seulement) : `lister()` (courriel, nom, rôle, statut `actif` | `invite` | `suspendu`),
12
+ `inviter({ courriel, nom, role })` → membre du MÊME compte + lien d'activation (`config.invitationJours`, défaut 7),
13
+ `reinviter(id)`, `changerRole(id, role)`, `retirer(id)`. Le lien part par courriel s'il y a un transport et est
14
+ TOUJOURS rendu à la charge (un commerce n'a pas toujours de courriel). La charge décide qui peut s'en servir.
15
+ - `charge.vues.connexion({ lang, action, erreur, oubli })` et `charge.vues.reinitialiser({ lang, jeton, utilisateur, erreur,
16
+ expire, minimum })` : la greffe rend ces deux pages ENTIÈRES (l'écran de SA maquette), erreurs comprises ; le porteur
17
+ d'un jeton d'invitation est lu sans le consommer (« Bienvenue …, vous êtes invité comme … »).
18
+ - `config.apresConnexion` : la page d'arrivée après connexion ou activation (défaut `/compte`).
19
+
20
+ ### Corrigé
21
+ - Les jetons de réinitialisation vivaient en mémoire : un redémarrage perdait les liens en cours. Ils sont rangés en
22
+ base (table `wagon_jetons`, empreinte seule).
23
+ - `/connexion/reinitialiser` est servie dès que la connexion est par mot de passe, même sans courriel : le lien d'une
24
+ invitation remis de la main à la main doit fonctionner. La DEMANDE (`/connexion/oubli`) exige toujours un courriel.
25
+
26
+ Essais T-WAG-30 à 35 (test-scripts/unit/membres.test.mjs) ; 52/52 ; sabotage vérifié (jetons remis en mémoire →
27
+ T-WAG-32 tombe).
28
+
6
29
  ## [0.7.0] — 2026-10-29
7
30
 
8
31
  ### Ajouté
package/llms.txt CHANGED
@@ -4,7 +4,7 @@
4
4
  > PayPal), espace client, administration, pages légales, robots/sitemap, sauvegarde, fr/en/ar. Une GREFFE n'apporte que
5
5
  > wagon.config.mjs, charge/, legal/ et son .env. N2 (pack). Extrait du gabarit mosta-wagon 0.11.0 le 26/10/2026.
6
6
 
7
- - Version: 0.6.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
7
+ - Version: 0.8.0 · Licence: AGPL-3.0-or-later · Auteur: Dr Hamid MADANI <drmdh@msn.com>
8
8
  - Chemin: mostajs/mosta-platform-stack/mosta-wagon-engine · gabarit d'une greffe : Entreprise/mosta-wagon
9
9
 
10
10
  ## ⚙️ D'EMBLÉE : LE .env ET LA CASCADE @mostajs/config
@@ -49,6 +49,7 @@ démarrage) · exporterCompte(id), effacerCompte(id) (RGPD) · sitemap() · sauv
49
49
  { '/page': { microphone: 'self' }, '/dossier/*': … }, garde-http ≥ 0.2).
50
50
  ctx : chemin, methode, lang, t, ui, page({ titre, contenu }), envoyer, json, rediriger, formulaire, corpsJson({ strict }),
51
51
  compte, consommer(cle), refuser(v), palier({ compte? }), plafond(cle, n, { compte? }), courriel({ to, subject, text, html? }) → bool, admin.
52
+ 0.8 : ctx.utilisateur, ctx.role ; ctx.membres = { lister(), inviter({ courriel, nom, role }) → { utilisateurId, courriel, role, lien, expiresAt, courrielParti }, reinviter(id), changerRole(id, role), retirer(id) } (session seulement ; la charge garde l'accès) ; config.apresConnexion, config.invitationJours ; jetons en base (wagon_jetons) ; /connexion/reinitialiser servie sans courriel ; charge.vues.connexion(…) / charge.vues.reinitialiser({ jeton, utilisateur, erreur, expire }) → page entière.
52
53
  0.7 : ctx.paiement (null sans banque prépayée active) = { moyens: [{ moyen, devise }], ouvrir({ moyen, montant (unités MINEURES), description, reference, metadata, merci: '/chemin', annule: '/chemin' }) → { ok, url, commande } | { ok: false, raison } } — paiement PONCTUEL de la charge (une place de cours…). La banque confirme à /api/paiement/charge/<moyen> (webhook, signature) ou renvoie l'acheteur à /api/paiement/charge-retour/<moyen> (SATIM) ; le wagon appelle alors charge.paiementRegle({ reference, statut: paid|failed|refunded|…, commande, montant (mineures), devise, moyen, metadata }), à écrire IDEMPOTENT (une banque rejoue). merci/annule : chemins locaux seulement.
53
54
  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.
54
55
  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 /…).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mostajs/wagon-engine",
3
- "version": "0.7.0",
3
+ "version": "0.8.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
@@ -19,6 +19,7 @@
19
19
  * faux ; l'administration est invisible (404) à qui n'est pas administrateur.
20
20
  */
21
21
  import { readFileSync, existsSync } from 'node:fs';
22
+ import { createHash } from 'node:crypto';
22
23
  import { join } from 'node:path';
23
24
  import { openIsolatedDialect } from '@mostajs/data-plug';
24
25
  import { creerWagonUi, esc } from '@mostajs/wagon-ui';
@@ -143,12 +144,21 @@ const AdminSchema = {
143
144
  fields: { courriel: { type: 'string', required: true }, confirmeLe: { type: 'date' }, par: { type: 'string' } },
144
145
  relations: {}, indexes: [{ fields: { courriel: 'asc' }, unique: true }],
145
146
  };
147
+ /**
148
+ * 0.8 — les JETONS à usage unique (réinitialisation, invitation), RANGÉS EN BASE : jusqu'à la 0.7 ils vivaient en
149
+ * mémoire, et un redémarrage perdait le lien qu'un invité n'avait pas encore ouvert. Seule l'empreinte est rangée.
150
+ */
151
+ const JetonSchema = {
152
+ name: 'WagonJeton', collection: 'wagon_jetons', timestamps: false,
153
+ fields: { cle: { type: 'string', required: true }, user: { type: 'string', required: true }, expiresAt: { type: 'number', required: true } },
154
+ relations: {}, indexes: [{ fields: { cle: 'asc' }, unique: true }],
155
+ };
146
156
  export const schemasWagon = () => {
147
157
  const vus = new Set();
148
158
  // `Payment` (@mostajs/payment) : une ligne par session de banque, que le webhook retrouve pour clore la vente.
149
159
  // Déclarée même sans banque prépayée — incident RestoTrax du 19/08/2026 : « no such table: payments » APRÈS
150
160
  // que le client eut engagé son paiement.
151
- return [...schemasComptes(), ...billingSchemas(), PaymentSchema, SessionSchema, CredentialSchema, AdminSchema].filter((s) => (vus.has(s.name) ? false : vus.add(s.name)));
161
+ return [...schemasComptes(), ...billingSchemas(), PaymentSchema, SessionSchema, CredentialSchema, AdminSchema, JetonSchema].filter((s) => (vus.has(s.name) ? false : vus.add(s.name)));
152
162
  };
153
163
 
154
164
  const lireCorps = (req, max = 1_000_000) => new Promise((ok, ko) => {
@@ -218,13 +228,21 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
218
228
  const purgerSessions = () => sessions.purger?.().catch((e) => journal.warn(`wagon : purge des sessions : ${e?.message ?? e}`));
219
229
  purgerSessions(); setInterval(purgerSessions, 6 * 3600 * 1000).unref?.();
220
230
  const freinInscription = memoryLimiter({ limits: { byIp: config.freinInscription ?? { capacity: 10, refillPerSec: 1 / 60 } } });
221
- const motsDePasse = createPasswordAuth({
222
- credentials: {
223
- get: async (u) => { const r = await d.findOne(S.WagonCredential, { user: String(u) }); return r ? { hash: r.hash, updatedAt: r.updatedAt } : null; },
224
- set: async (u, rec) => { const r = await d.findOne(S.WagonCredential, { user: String(u) }); if (r) await d.update(S.WagonCredential, String(r.id), rec); else await d.create(S.WagonCredential, { user: String(u), ...rec }); },
225
- },
226
- findUser: async (l) => { const u = await d.findOne(S.User, { email: l }); return u ? { id: String(u.id) } : null; },
227
- });
231
+ const identifiants = {
232
+ get: async (u) => { const r = await d.findOne(S.WagonCredential, { user: String(u) }); return r ? { hash: r.hash, updatedAt: r.updatedAt } : null; },
233
+ set: async (u, rec) => { const r = await d.findOne(S.WagonCredential, { user: String(u) }); if (r) await d.update(S.WagonCredential, String(r.id), rec); else await d.create(S.WagonCredential, { user: String(u), ...rec }); },
234
+ };
235
+ const trouverUtilisateur = async (l) => { const u = await d.findOne(S.User, { email: l }); return u ? { id: String(u.id) } : null; };
236
+ // 0.8 — les jetons en base (voir JetonSchema), partagés par la réinitialisation et l'invitation.
237
+ const jetons = {
238
+ get: async (cle) => { const r = await d.findOne(S.WagonJeton, { cle }); return r ? { userId: String(r.user), expiresAt: Number(r.expiresAt) } : null; },
239
+ put: async (cle, e) => { await d.create(S.WagonJeton, { cle, user: String(e.userId), expiresAt: Number(e.expiresAt) }); },
240
+ delete: async (cle) => { for (const r of await d.find(S.WagonJeton, { cle })) await d.delete(S.WagonJeton, String(r.id)); },
241
+ };
242
+ const motsDePasse = createPasswordAuth({ credentials: identifiants, findUser: trouverUtilisateur, resetTokens: jetons });
243
+ // L'invitation : même magasin, durée plus longue (un invité n'ouvre pas son courriel dans la demi-heure).
244
+ const invitations = createPasswordAuth({ credentials: identifiants, findUser: trouverUtilisateur, resetTokens: jetons, limiter: false,
245
+ resetTtlMs: (config.invitationJours ?? 7) * 86400000 });
228
246
  const codes = createEmailCodeAuth({ findUser: async (l) => { const u = await d.findOne(S.User, { email: l }); return u ? { id: String(u.id) } : null; } });
229
247
  const vente = stripe ? createStripeSale({
230
248
  billing: facturation, stripe, webhookSecret, metadataKeys: META,
@@ -494,6 +512,53 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
494
512
  });
495
513
  }
496
514
 
515
+ /**
516
+ * 0.8 — les MEMBRES du compte d'une session : la charge décide QUI peut s'en servir (le wagon ne connaît pas ses
517
+ * rôles). Inviter crée un membre du MÊME compte, au mot de passe impossible, et rend un lien d'activation (sept
518
+ * jours, `config.invitationJours`) : envoyé par courriel s'il y a un transport, toujours rendu à la charge, pour
519
+ * qu'elle le montre (un commerce n'a pas toujours de courriel).
520
+ */
521
+ const membresDe = (compteSession) => {
522
+ const cid = String(compteSession.id);
523
+ const lienDe = (token) => `${origine}/connexion/reinitialiser?t=${encodeURIComponent(token)}`;
524
+ const envoyerInvitation = async (courriel, nom, lien) => {
525
+ if (!transport) return false;
526
+ try {
527
+ await transport.envoyer({ to: courriel, subject: `${config.marque.nom} — invitation`,
528
+ text: [config.marque.nom, '', `${nom ? `${nom}, v` : 'V'}ous êtes invité(e). Choisissez votre mot de passe :`, lien, '', `Ce lien vaut ${config.invitationJours ?? 7} jours.`, ''].join('\n') });
529
+ return true;
530
+ } catch (e) { journal.warn(`wagon : invitation non partie : ${e?.message ?? e}`); return false; }
531
+ };
532
+ return {
533
+ async lister() {
534
+ const out = [];
535
+ for (const m of await comptes.membres.lister(cid)) {
536
+ const u = await d.findById(S.User, m.utilisateurId).catch(() => null);
537
+ out.push({ ...m, nom: u?.firstName ?? null, statut: u?.status !== 'active' ? 'suspendu' : (await motsDePasse.hasPassword(m.utilisateurId)) ? 'actif' : 'invite' });
538
+ }
539
+ return out;
540
+ },
541
+ async inviter({ courriel, nom = null, role = 'membre' } = {}) {
542
+ const r = await comptes.membres.ajouter(cid, courriel, String(role || 'membre'));
543
+ const n = String(nom ?? '').trim();
544
+ if (n) await d.update(S.User, r.utilisateurId, { firstName: n });
545
+ const { token, expiresAt } = await invitations.issueToken(r.utilisateurId);
546
+ const lien = lienDe(token);
547
+ return { ...r, nom: n || null, lien, expiresAt: new Date(expiresAt).toISOString(), courrielParti: await envoyerInvitation(r.courriel, n, lien) };
548
+ },
549
+ async reinviter(utilisateurId) {
550
+ const m = (await comptes.membres.lister(cid)).find((x) => x.utilisateurId === String(utilisateurId));
551
+ if (!m) throw Object.assign(new Error('Cet utilisateur n’est pas membre de ce compte.'), { code: 'NON_MEMBRE' });
552
+ const u = await d.findById(S.User, m.utilisateurId).catch(() => null);
553
+ const { token, expiresAt } = await invitations.issueToken(m.utilisateurId);
554
+ const lien = lienDe(token);
555
+ return { ...m, lien, expiresAt: new Date(expiresAt).toISOString(), courrielParti: await envoyerInvitation(m.courriel, u?.firstName, lien) };
556
+ },
557
+ changerRole: (utilisateurId, role) => comptes.membres.changerRole(cid, String(utilisateurId), String(role)),
558
+ retirer: (utilisateurId) => comptes.membres.retirer(cid, String(utilisateurId)),
559
+ };
560
+ };
561
+
497
562
  async function handler(req, res) {
498
563
  const url = new URL(req.url, 'http://wagon.local');
499
564
  const chemin = url.pathname;
@@ -693,7 +758,10 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
693
758
  try { await transport.envoyer(m); return true; } catch (e) { journal.warn(`wagon : courriel de la charge non parti : ${e?.message ?? e}`); return false; }
694
759
  };
695
760
  const ctx = { chemin, methode, lang, t, ui, page, envoyer, json, rediriger, formulaire, corpsJson, compte, consommer, refuser, palier, plafond, courriel, admin: estAdmin && !compteCle,
696
- paiement: paiementCharge }; // 0.7 — null sans banque prépayée active
761
+ paiement: paiementCharge, // 0.7 — null sans banque prépayée active
762
+ // 0.8 — la PERSONNE de la session (sans empreinte) et son rôle dans le compte ; les membres du compte.
763
+ utilisateur: compteCle ? null : (qui?.utilisateur ?? null), role: compteCle ? null : (qui?.role ?? null),
764
+ membres: qui && !compteCle ? membresDe(qui.compte) : null };
697
765
  if (charge.routes && (await charge.routes(req, res, ctx))) return true;
698
766
  if (chemin.startsWith('/api/')) return json(404, { ok: false, error: 'introuvable' });
699
767
 
@@ -729,6 +797,10 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
729
797
  // 0.5 — « mot de passe oublié » n'existe que si un courriel peut partir : un accusé de réception d'un message que
730
798
  // personne n'enverra serait pire qu'une page absente (auth-plug T-KIT-5).
731
799
  const oubliPossible = methodeConnexion === 'motdepasse' && !!transport;
800
+ // 0.8 — la page qui consomme un jeton est servie dès que l'on se connecte par mot de passe : le lien d'une
801
+ // INVITATION remis de la main à la main (sans courriel) doit fonctionner. Seule la DEMANDE (/connexion/oubli)
802
+ // exige un courriel possible.
803
+ const reinitPossible = methodeConnexion === 'motdepasse';
732
804
  if (chemin === '/inscription') {
733
805
  const tous = await paliersEnBase();
734
806
  const palierDe = (slug) => tous.find((p) => p.slug === slug && p.active !== false) ?? tous.find((p) => p.slug === (config.planGratuit ?? 'gratuit'));
@@ -775,11 +847,18 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
775
847
  if (chemin === '/connexion') {
776
848
  const suite = suiteSure(url.searchParams.get('suite'));
777
849
  const action = avecSuite('/connexion', suite);
778
- 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>` : '') }));
850
+ // 0.8 — `charge.vues.connexion({ lang, action, erreur, oubli })` rend la page ENTIÈRE à la place de wagon-ui
851
+ // (champs « courriel » et « motdepasse », POST vers `action`) : l'écran de la maquette de la greffe.
852
+ const vueConnexion = methodeConnexion === 'motdepasse' && typeof charge.vues?.connexion === 'function' ? charge.vues.connexion : null;
853
+ if (methode === 'GET') {
854
+ if (vueConnexion) return envoyer(200, await vueConnexion({ lang, action, erreur: null, oubli: oubliPossible ? '/connexion/oubli' : null }));
855
+ 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>` : '') }));
856
+ }
779
857
  const f = await formulaire();
780
858
  if (methodeConnexion === 'motdepasse') {
781
859
  const r = await motsDePasse.login(String(f.courriel ?? ''), String(f.motdepasse ?? ''), { ip: ipClient(req) });
782
- if (r.ok) return ouvrirSession(r.userId, suite ?? '/compte');
860
+ if (r.ok) return ouvrirSession(r.userId, suite ?? config.apresConnexion ?? '/compte');
861
+ if (vueConnexion) return envoyer(r.reason === 'throttled' ? 429 : 200, await vueConnexion({ lang, action, erreur: t('espace.identifiantsFaux'), oubli: oubliPossible ? '/connexion/oubli' : null }));
783
862
  return envoyer(r.reason === 'throttled' ? 429 : 200, page({ titre: t('espace.seConnecter'), contenu: ui.connexion({ lang, erreur: t('espace.identifiantsFaux'), action }) }));
784
863
  }
785
864
  const courriel = String(f.courriel ?? '').trim().toLowerCase();
@@ -826,15 +905,36 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
826
905
  }
827
906
  return true;
828
907
  }
829
- if (oubliPossible && chemin === '/connexion/reinitialiser') {
908
+ if (reinitPossible && chemin === '/connexion/reinitialiser') {
830
909
  const formulaireReinit = (tk, erreur = '') => `<h1>${esc(TC.reinit)}</h1>${erreur ? `<p class="msg">${esc(erreur)}</p>` : ''}<form method="post" action="/connexion/reinitialiser"><input type="hidden" name="t" value="${esc(tk)}"><p><label>${esc(TC.nouveau)} <input type="password" name="nouveau" required autocomplete="new-password"></label></p><p><label>${esc(TC.confirmer)} <input type="password" name="confirmer" required autocomplete="new-password"></label></p><button class="bouton">${esc(TC.enregistrer)}</button></form>`;
831
- if (methode === 'GET') return envoyer(200, page({ titre: TC.reinit, contenu: formulaireReinit(url.searchParams.get('t') ?? '') }));
910
+ // 0.8 — `charge.vues.reinitialiser({ lang, jeton, utilisateur, erreur, expire })` rend la page ENTIÈRE (champs
911
+ // « t », « nouveau », « confirmer ») : une invitation s'y présente (« Bienvenue …, vous êtes invité comme … »).
912
+ // `utilisateur` (id, courriel, prenom) est lu sur le jeton SANS le consommer ; null si le lien ne vaut plus.
913
+ const vueReinit = typeof charge.vues?.reinitialiser === 'function' ? charge.vues.reinitialiser : null;
914
+ const porteurDe = async (tk) => {
915
+ const e = tk ? await jetons.get(createHash('sha256').update(String(tk)).digest('base64url')) : null;
916
+ if (!e || e.expiresAt < Date.now()) return null;
917
+ const u = await d.findById(S.User, e.userId).catch(() => null);
918
+ return u ? { id: String(u.id), courriel: u.email, prenom: u.firstName ?? null } : null;
919
+ };
920
+ const vueR = async (code, tk, erreur) => {
921
+ const u = await porteurDe(tk);
922
+ return envoyer(code, await vueReinit({ lang, jeton: tk, utilisateur: u, erreur, expire: !u, minimum: motsDePasse.policy.minLength }));
923
+ };
924
+ if (methode === 'GET') {
925
+ if (vueReinit) return vueR(200, url.searchParams.get('t') ?? '', null);
926
+ return envoyer(200, page({ titre: TC.reinit, contenu: formulaireReinit(url.searchParams.get('t') ?? '') }));
927
+ }
832
928
  const f = await formulaire();
833
- if (String(f.nouveau ?? '') !== String(f.confirmer ?? '')) return envoyer(400, page({ titre: TC.reinit, contenu: formulaireReinit(String(f.t ?? ''), TC.differents) }));
929
+ if (String(f.nouveau ?? '') !== String(f.confirmer ?? '')) {
930
+ if (vueReinit) return vueR(400, String(f.t ?? ''), TC.differents);
931
+ return envoyer(400, page({ titre: TC.reinit, contenu: formulaireReinit(String(f.t ?? ''), TC.differents) }));
932
+ }
834
933
  const r = await motsDePasse.consumeReset(String(f.t ?? ''), String(f.nouveau ?? ''));
934
+ if (!r.ok && vueReinit) return vueR(400, String(f.t ?? ''), r.reason === 'policy' ? t('espace.faible', { motifs: (r.issues ?? []).join(', ') }) : TC.lienMort);
835
935
  if (!r.ok) return envoyer(400, page({ titre: TC.reinit, contenu: r.reason === 'policy' ? formulaireReinit(String(f.t ?? ''), t('espace.faible', { motifs: (r.issues ?? []).join(', ') })) : `<h1>${esc(TC.reinit)}</h1><p class="msg">${esc(TC.lienMort)}</p><p><a href="/connexion/oubli">${esc(TC.oublie)}</a></p>` }));
836
936
  await sessions.fermerToutes(String(r.userId));
837
- return ouvrirSession(r.userId);
937
+ return ouvrirSession(r.userId, config.apresConnexion ?? '/compte');
838
938
  }
839
939
 
840
940
  // L'espace du client.