@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 +23 -0
- package/llms.txt +2 -1
- package/package.json +1 -1
- package/src/wagon.mjs +115 -15
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.
|
|
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.
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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 ?? ''))
|
|
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.
|