@mostajs/wagon-engine 0.7.0 → 0.9.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 +48 -0
- package/README.md +7 -7
- package/examples/verifier.mjs +1 -1
- package/llms.txt +7 -6
- package/package.json +1 -1
- package/src/conformite/conformite.test.mjs +5 -4
- package/src/demarrer.mjs +10 -8
- package/src/env.mjs +39 -0
- package/src/index.js +1 -0
- package/src/wagon.mjs +120 -20
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,54 @@
|
|
|
3
3
|
**Auteur** : Dr Hamid MADANI <drmdh@msn.com>
|
|
4
4
|
Format : Keep a Changelog ; versions : SemVer.
|
|
5
5
|
|
|
6
|
+
## [0.9.0] — 2026-10-29
|
|
7
|
+
|
|
8
|
+
### Modifié — les clés du `.env` sans préfixe `WAGON_` (demande du fondateur)
|
|
9
|
+
Le préfixe `WAGON_` ne disait ni la fonction de la clé ni son sens métier, et se mêlait mal au profil `MOSTA_ENV`
|
|
10
|
+
(`PROD_SGBD_URI` n'était pas lu : la base des comptes retombait sur `data/wagon.db`). Les clés portent désormais le
|
|
11
|
+
nom de leur fonction ; le seul préfixe est celui du profil. **Les anciens noms ne sont plus lus** (pas de secours) :
|
|
12
|
+
le démarrage avertit pour chacun qui reste (`signalerClesRetirees`).
|
|
13
|
+
|
|
14
|
+
| retiré | remplacé par |
|
|
15
|
+
|---|---|
|
|
16
|
+
| `WAGON_SGBD`, `WAGON_DB_DIALECT` | `SGBD_URI`, `DB_DIALECT` |
|
|
17
|
+
| `WAGON_BASE_URL` | `BASE_URL` |
|
|
18
|
+
| `WAGON_ADMINS` | `ADMIN_EMAILS` |
|
|
19
|
+
| `WAGON_EMAIL_SERVER_HOST`/`_PORT`/`_USER`/`_PASSWORD`, `WAGON_EMAIL_FROM` | `EMAIL_SERVER_HOST`/`_PORT`/`_USER`/`_PASSWORD`, `EMAIL_FROM` |
|
|
20
|
+
| `WAGON_COURRIEL_FICHIER` | `EMAIL_FICHIER` |
|
|
21
|
+
| `WAGON_STRIPE_RESTRICTED_KEY`, `WAGON_STRIPE_SECRET_KEY`, `WAGON_STRIPE_WEBHOOK_SECRET` | `STRIPE_RESTRICTED_KEY`, `STRIPE_SECRET_KEY`, `STRIPE_WEBHOOK_SECRET` |
|
|
22
|
+
| `WAGON_GREFFE` (suite de conformité) | `GREFFE` |
|
|
23
|
+
|
|
24
|
+
Toutes ces clés sont lues par `getEnv` de `@mostajs/config` (la suite de conformité aussi, qui lisait `process.env`).
|
|
25
|
+
Ajouté : `adminsDeclares(config)` (la liste des administrateurs déclarés, pour les scripts d'administration des
|
|
26
|
+
greffes), `signalerClesRetirees(journal)`, `CLES_RETIREES`.
|
|
27
|
+
|
|
28
|
+
Essai T-ENG-17 (cascade `MOSTA_ENV=PROD` → `PROD_SGBD_URI` ; `WAGON_SGBD` ignoré et signalé) ; 53/53 ; sabotage vérifié
|
|
29
|
+
(relire `WAGON_SGBD` → T-ENG-17 tombe).
|
|
30
|
+
|
|
31
|
+
## [0.8.0] — 2026-10-29
|
|
32
|
+
|
|
33
|
+
### Ajouté — les membres d'un compte (demandé par CaisseTrax : écart E-08 de SPEC-ECRANS-CAISSETRAX)
|
|
34
|
+
- `ctx.utilisateur` (la personne de la session, sans empreinte) et `ctx.role` (son rôle dans le compte) ; `null`
|
|
35
|
+
sans session et pour une clé d'API.
|
|
36
|
+
- `ctx.membres` (session seulement) : `lister()` (courriel, nom, rôle, statut `actif` | `invite` | `suspendu`),
|
|
37
|
+
`inviter({ courriel, nom, role })` → membre du MÊME compte + lien d'activation (`config.invitationJours`, défaut 7),
|
|
38
|
+
`reinviter(id)`, `changerRole(id, role)`, `retirer(id)`. Le lien part par courriel s'il y a un transport et est
|
|
39
|
+
TOUJOURS rendu à la charge (un commerce n'a pas toujours de courriel). La charge décide qui peut s'en servir.
|
|
40
|
+
- `charge.vues.connexion({ lang, action, erreur, oubli })` et `charge.vues.reinitialiser({ lang, jeton, utilisateur, erreur,
|
|
41
|
+
expire, minimum })` : la greffe rend ces deux pages ENTIÈRES (l'écran de SA maquette), erreurs comprises ; le porteur
|
|
42
|
+
d'un jeton d'invitation est lu sans le consommer (« Bienvenue …, vous êtes invité comme … »).
|
|
43
|
+
- `config.apresConnexion` : la page d'arrivée après connexion ou activation (défaut `/compte`).
|
|
44
|
+
|
|
45
|
+
### Corrigé
|
|
46
|
+
- Les jetons de réinitialisation vivaient en mémoire : un redémarrage perdait les liens en cours. Ils sont rangés en
|
|
47
|
+
base (table `wagon_jetons`, empreinte seule).
|
|
48
|
+
- `/connexion/reinitialiser` est servie dès que la connexion est par mot de passe, même sans courriel : le lien d'une
|
|
49
|
+
invitation remis de la main à la main doit fonctionner. La DEMANDE (`/connexion/oubli`) exige toujours un courriel.
|
|
50
|
+
|
|
51
|
+
Essais T-WAG-30 à 35 (test-scripts/unit/membres.test.mjs) ; 52/52 ; sabotage vérifié (jetons remis en mémoire →
|
|
52
|
+
T-WAG-32 tombe).
|
|
53
|
+
|
|
6
54
|
## [0.7.0] — 2026-10-29
|
|
7
55
|
|
|
8
56
|
### Ajouté
|
package/README.md
CHANGED
|
@@ -34,17 +34,17 @@ await demarrer({ config, charge });
|
|
|
34
34
|
| `.env` | rôle |
|
|
35
35
|
|---|---|
|
|
36
36
|
| `PORT`, `HOST` | écoute (4800, 127.0.0.1) |
|
|
37
|
-
| `
|
|
38
|
-
| `
|
|
39
|
-
| `
|
|
40
|
-
| `
|
|
37
|
+
| `BASE_URL` | l'adresse publique (liens des courriels, retours de paiement) |
|
|
38
|
+
| `DB_DIALECT`, `SGBD_URI` | la base : `sqlite` et un chemin, ou `postgres` et `postgresql://…` |
|
|
39
|
+
| `EMAIL_SERVER_HOST`, `…_PORT`, `…_USER`, `…_PASSWORD`, `EMAIL_FROM` | le courriel (SMTP) |
|
|
40
|
+
| `STRIPE_SECRET_KEY` (ou `STRIPE_RESTRICTED_KEY`), `STRIPE_WEBHOOK_SECRET` | Stripe |
|
|
41
41
|
| `CHARGILY_SECRET_KEY` · `SATIM_MERCHANT_ID`, `SATIM_PASSWORD`, `SATIM_TERMINAL_ID` · `PAYPAL_…` | les banques prépayées proposées par `config.paiement.prepaye` |
|
|
42
42
|
| `PAIEMENT_STRIPE=off`, `PAIEMENT_CHARGILY=off`, `PAIEMENT_SATIM=off`, `PAIEMENT_PAYPAL=off` | **coupe un moyen** sans toucher à ses clés |
|
|
43
|
-
| `
|
|
43
|
+
| `ADMIN_EMAILS` | les courriels administrateurs (confirmés par un code reçu) |
|
|
44
44
|
| `PAIEMENT_VENDEUR_DZD`, `PAIEMENT_VENDEUR_EUR`… | 0.6 — **le vendeur de chaque devise** (dénomination, forme, adresse, immatriculations) ; dès qu'une est déclarée, une devise sans vendeur n'est pas vendue, et `{{VENDEUR}}` des CGV les nomme |
|
|
45
45
|
| `PAIEMENT_STRIPE_DEVISES` | 0.6 — la devise de Stripe (défaut `EUR`) |
|
|
46
46
|
|
|
47
|
-
**Le `.env` d'abord.** Toutes les clés passent par `getEnv` de `@mostajs/config` : cascade `MOSTA_ENV` (`PROD_CLÉ` l'emporte sur `CLÉ`), valeur vide = absente, et `loadEnv` n'écrase jamais une variable déjà posée par l'orchestrateur. Si `wagon.config.mjs` lit des variables, il appelle `loadEnv` lui-même, en tête.
|
|
47
|
+
**Le `.env` d'abord.** Toutes les clés passent par `getEnv` de `@mostajs/config` : cascade `MOSTA_ENV` (`PROD_CLÉ` l'emporte sur `CLÉ`), valeur vide = absente, et `loadEnv` n'écrase jamais une variable déjà posée par l'orchestrateur. Une clé porte le nom de sa fonction ; le seul préfixe est celui du profil (`PROD_SGBD_URI`, `DEV_BASE_URL`). Depuis la 0.9, les anciens noms `WAGON_…` ne sont plus lus : le démarrage signale ceux qui restent. Si `wagon.config.mjs` lit des variables, il appelle `loadEnv` lui-même, en tête.
|
|
48
48
|
|
|
49
49
|
**Un vendeur par devise (0.6).** Un SaaS qui vend en dinars par un auto-entrepreneur et en euros par une société pose `PAIEMENT_VENDEUR_DZD=…` et `PAIEMENT_VENDEUR_EUR=…`. Une devise déclarée vide (`PAIEMENT_VENDEUR_EUR=`, la société n'est pas encore immatriculée) n'est pas vendue : Stripe est écarté même avec sa clé. Sans aucune `PAIEMENT_VENDEUR_*`, rien ne change.
|
|
50
50
|
|
|
@@ -54,7 +54,7 @@ await demarrer({ config, charge });
|
|
|
54
54
|
|
|
55
55
|
## Juger une greffe
|
|
56
56
|
|
|
57
|
-
La suite de conformité est publiée avec le moteur. Elle juge la greffe du dossier courant, ou celle que désigne `
|
|
57
|
+
La suite de conformité est publiée avec le moteur. Elle juge la greffe du dossier courant, ou celle que désigne `GREFFE` :
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
npx mjs-unit node_modules/@mostajs/wagon-engine/src/conformite/conformite.test.mjs
|
package/examples/verifier.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { demarrer } from '../src/index.js';
|
|
|
13
13
|
const racine = mkdtempSync(join(tmpdir(), 'greffe-'));
|
|
14
14
|
mkdirSync(join(racine, 'legal'));
|
|
15
15
|
writeFileSync(join(racine, 'legal', 'cgu.fr.md'), '# Conditions\n\n{{SERVICE}} est édité par Exemple SARL.\n');
|
|
16
|
-
writeFileSync(join(racine, '.env'), `PORT=0\
|
|
16
|
+
writeFileSync(join(racine, '.env'), `PORT=0\nSGBD_URI=${join(racine, 'wagon.db')}\nBASE_URL=http://127.0.0.1\nCHARGILY_SECRET_KEY=test_sk_exemple\nPAIEMENT_STRIPE=off\n`);
|
|
17
17
|
|
|
18
18
|
const config = {
|
|
19
19
|
marque: { nom: 'Échos', symbole: 'É', auteur: 'Exemple SARL' }, langues: ['fr', 'en', 'ar'], connexion: 'motdepasse',
|
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
|
|
@@ -17,7 +17,7 @@ Une greffe se configure par SON `.env` (dossier courant de `demarrer`, ou `racin
|
|
|
17
17
|
conteneur) prime sur le fichier.
|
|
18
18
|
3. **Cascade** (première valeur NON VIDE) : `${MOSTA_ENV}_${CLÉ}` → `${CLÉ}` → défaut. Un seul .env, des profils
|
|
19
19
|
`TEST_`/`DEV_`/`PROD_`. Toutes les clés ci-dessous, et celles de `@mostajs/payment`, passent par `getEnv`.
|
|
20
|
-
4. **Vide = absent** : `
|
|
20
|
+
4. **Vide = absent** : `STRIPE_SECRET_KEY=` ne configure rien. Exception voulue : `PAIEMENT_VENDEUR_<DEVISE>=`
|
|
21
21
|
vide DÉCLARE la devise sans la nommer (elle n'est pas vendue).
|
|
22
22
|
5. Les secrets et les informations de l'entreprise sont posés par l'exploitant sur le serveur, jamais commités ;
|
|
23
23
|
`.env.example` de la greffe liste chaque clé, sans valeur.
|
|
@@ -27,13 +27,13 @@ Une greffe se configure par SON `.env` (dossier courant de `demarrer`, ou `racin
|
|
|
27
27
|
- creerWagon({ config, charge, sgbd: string|{dialect,uri}, baseUrl, stripe?, webhookSecret?, transport?, journal?,
|
|
28
28
|
paiement?: { banques: { chargily:'DZD', … }, port?, deviseStripe? }, racine? }) → { moyens, handler, catalogue(), catalogueStripe(), fermer(), … }
|
|
29
29
|
- schemasWagon() (sauvegarde) · sauvegarder({ cibles, driver, seau, recipients, garder }) · suivrePalette(…)
|
|
30
|
-
- '@mostajs/wagon-engine/conformite' = src/conformite/conformite.test.mjs (suite mjs-unit, juge
|
|
30
|
+
- '@mostajs/wagon-engine/conformite' = src/conformite/conformite.test.mjs (suite mjs-unit, juge GREFFE ou cwd)
|
|
31
31
|
|
|
32
32
|
## .env lu par demarrer
|
|
33
|
-
PORT, HOST ·
|
|
34
|
-
|
|
33
|
+
PORT, HOST · BASE_URL · DB_DIALECT (sqlite|postgres…), SGBD_URI · EMAIL_SERVER_HOST/PORT/USER/PASSWORD,
|
|
34
|
+
EMAIL_FROM (ou EMAIL_FICHIER pour les essais) · STRIPE_SECRET_KEY|STRIPE_RESTRICTED_KEY, STRIPE_WEBHOOK_SECRET ·
|
|
35
35
|
CHARGILY_SECRET_KEY|API_KEY · SATIM_MERCHANT_ID, SATIM_PASSWORD, SATIM_TERMINAL_ID · PAYPAL_CLIENT_ID, PAYPAL_SECRET,
|
|
36
|
-
PAYPAL_WEBHOOK_ID · PAIEMENT_<STRIPE|CHARGILY|SATIM|PAYPAL>=off (coupe le moyen) ·
|
|
36
|
+
PAYPAL_WEBHOOK_ID · PAIEMENT_<STRIPE|CHARGILY|SATIM|PAYPAL>=off (coupe le moyen) · ADMIN_EMAILS. Cascade MOSTA_ENV (PROD_SGBD_URI…). 0.9 : plus aucun nom WAGON_… n'est lu (signalé au démarrage).
|
|
37
37
|
0.6 (@mostajs/payment ≥ 2.7) : PAIEMENT_VENDEUR_<DEVISE> (le vendeur de cette devise : dénomination, forme, adresse,
|
|
38
38
|
immatriculations) · PAIEMENT_STRIPE_DEVISES (la devise de Stripe, défaut EUR).
|
|
39
39
|
Moyen ACTIF = clés présentes ET commutateur ≠ off (ET, pour une banque prépayée, proposée par config.paiement.prepaye)
|
|
@@ -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.9.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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @mostajs/wagon-engine — LA SUITE DE CONFORMITÉ : elle dit si une greffe tient.
|
|
2
|
-
// La greffe jugée est celle de
|
|
2
|
+
// La greffe jugée est celle de GREFFE, sinon le dossier courant : sa configuration (wagon.config.mjs), sa
|
|
3
3
|
// charge (charge/index.mjs) et ses textes légaux (legal/). Lancer depuis la greffe :
|
|
4
4
|
// mjs-unit node_modules/@mostajs/wagon-engine/src/conformite/conformite.test.mjs
|
|
5
5
|
// Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
@@ -20,10 +20,11 @@ import { join, resolve } from 'node:path';
|
|
|
20
20
|
import { pathToFileURL } from 'node:url';
|
|
21
21
|
import Stripe from 'stripe';
|
|
22
22
|
import { creerWagon } from '../wagon.mjs';
|
|
23
|
+
import { getEnv } from '@mostajs/config';
|
|
23
24
|
|
|
24
|
-
const GREFFE = resolve(
|
|
25
|
+
const GREFFE = resolve(getEnv('GREFFE', process.cwd()));
|
|
25
26
|
if (!existsSync(join(GREFFE, 'wagon.config.mjs')) || !existsSync(join(GREFFE, 'charge', 'index.mjs'))) {
|
|
26
|
-
throw new Error(`conformité : greffe introuvable dans ${GREFFE} (wagon.config.mjs et charge/index.mjs attendus ;
|
|
27
|
+
throw new Error(`conformité : greffe introuvable dans ${GREFFE} (wagon.config.mjs et charge/index.mjs attendus ; GREFFE la désigne)`);
|
|
27
28
|
}
|
|
28
29
|
const config = (await import(pathToFileURL(join(GREFFE, 'wagon.config.mjs')).href)).default;
|
|
29
30
|
const charge = (await import(pathToFileURL(join(GREFFE, 'charge', 'index.mjs')).href)).default;
|
|
@@ -374,7 +375,7 @@ test('T-WAG-27 — `config.css` : la feuille de l’application est posée sur l
|
|
|
374
375
|
});
|
|
375
376
|
|
|
376
377
|
// 0.5 (audit du 09/10/2026) — la base du wagon n'est plus figée en sqlite : `sgbd: { dialect, uri }` (le .env :
|
|
377
|
-
//
|
|
378
|
+
// DB_DIALECT, SGBD_URI), comme la base de la charge. La chaîne seule reste un chemin sqlite.
|
|
378
379
|
test('T-WAG-19 — la base du wagon se choisit par { dialect, uri } ; la chaîne reste un chemin sqlite', async () => {
|
|
379
380
|
const W2 = await creerWagon({ config: { ...config, dossierLegal: LEGAL_COMPLET }, charge, sgbd: { dialect: 'sqlite', uri: join(atelier, `objet${Math.random()}.db`) }, baseUrl: 'http://127.0.0.1', racine: GREFFE });
|
|
380
381
|
await W2.catalogue();
|
package/src/demarrer.mjs
CHANGED
|
@@ -13,18 +13,19 @@ import { join } from 'node:path';
|
|
|
13
13
|
import { loadEnv, getEnv } from '@mostajs/config';
|
|
14
14
|
import { devisesDuMoyen } from '@mostajs/payment/server';
|
|
15
15
|
import { creerWagon } from './wagon.mjs';
|
|
16
|
+
import { signalerClesRetirees } from './env.mjs';
|
|
16
17
|
|
|
17
18
|
const coupe = (moyen) => getEnv(`PAIEMENT_${moyen.toUpperCase()}`, '') === 'off';
|
|
18
19
|
|
|
19
20
|
/** Les courriels : SMTP (avec DKIM si la clé est publiée), ou un fichier pour les essais. */
|
|
20
21
|
async function transport() {
|
|
21
|
-
const fichier = getEnv('
|
|
22
|
+
const fichier = getEnv('EMAIL_FICHIER', '');
|
|
22
23
|
if (fichier) return { envoyer: async (m) => appendFileSync(fichier, `${JSON.stringify(m)}\n`, { mode: 0o600 }) };
|
|
23
|
-
const host = getEnv('
|
|
24
|
+
const host = getEnv('EMAIL_SERVER_HOST', '');
|
|
24
25
|
if (!host) return null;
|
|
25
26
|
const { createSmtpLiteDriver } = await import('@mostajs/mailer');
|
|
26
|
-
const pilote = createSmtpLiteDriver({ host, port: Number(getEnv('
|
|
27
|
-
const de = getEnv('
|
|
27
|
+
const pilote = createSmtpLiteDriver({ host, port: Number(getEnv('EMAIL_SERVER_PORT', '587')), user: getEnv('EMAIL_SERVER_USER', ''), pass: getEnv('EMAIL_SERVER_PASSWORD', '') });
|
|
28
|
+
const de = getEnv('EMAIL_FROM', getEnv('EMAIL_SERVER_USER', ''));
|
|
28
29
|
return { envoyer: (m) => pilote.send({ from: de, ...m }) };
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -45,14 +46,15 @@ const PRETES = {
|
|
|
45
46
|
*/
|
|
46
47
|
export async function demarrer({ config, charge, racine = process.cwd(), journal = console }) {
|
|
47
48
|
loadEnv({ path: join(racine, '.env') });
|
|
48
|
-
|
|
49
|
+
signalerClesRetirees(journal);
|
|
50
|
+
const cle = coupe('stripe') ? '' : (getEnv('STRIPE_RESTRICTED_KEY', '') || getEnv('STRIPE_SECRET_KEY', ''));
|
|
49
51
|
const banques = Object.fromEntries(Object.entries(config.paiement?.prepaye ?? {}).filter(([b]) => PRETES[b]?.() && !coupe(b)));
|
|
50
52
|
const wagon = await creerWagon({
|
|
51
53
|
config, charge, racine, journal,
|
|
52
|
-
sgbd: { dialect: getEnv('
|
|
53
|
-
baseUrl: getEnv('
|
|
54
|
+
sgbd: { dialect: getEnv('DB_DIALECT', 'sqlite'), uri: getEnv('SGBD_URI', join(racine, 'data', 'wagon.db')) },
|
|
55
|
+
baseUrl: getEnv('BASE_URL', 'http://localhost:4800'),
|
|
54
56
|
stripe: cle ? new (await import('stripe')).default(cle) : null,
|
|
55
|
-
webhookSecret: getEnv('
|
|
57
|
+
webhookSecret: getEnv('STRIPE_WEBHOOK_SECRET', ''),
|
|
56
58
|
transport: await transport(),
|
|
57
59
|
// 0.6 — la devise de Stripe (PAIEMENT_STRIPE_DEVISES, défaut EUR) : creerWagon écarte une devise sans vendeur.
|
|
58
60
|
paiement: { banques, deviseStripe: devisesDuMoyen('stripe', ['EUR'])[0] },
|
package/src/env.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mostajs/wagon-engine — les clés du .env, toujours lues par getEnv de @mostajs/config (cascade MOSTA_ENV :
|
|
3
|
+
* `PROD_CLÉ` l'emporte sur `CLÉ`, valeur vide = absente).
|
|
4
|
+
* @author Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
5
|
+
*
|
|
6
|
+
* 0.9 — les clés portent le nom de leur fonction (SGBD_URI, BASE_URL, ADMIN_EMAILS…) ; le seul préfixe est celui du
|
|
7
|
+
* profil MOSTA_ENV. Les anciens noms `WAGON_…` ne sont PLUS lus : s'il en reste dans le .env, le démarrage le dit.
|
|
8
|
+
*/
|
|
9
|
+
import { getEnv } from '@mostajs/config';
|
|
10
|
+
|
|
11
|
+
/** Les noms retirés en 0.9, et leur remplaçant. */
|
|
12
|
+
export const CLES_RETIREES = {
|
|
13
|
+
WAGON_SGBD: 'SGBD_URI',
|
|
14
|
+
WAGON_DB_DIALECT: 'DB_DIALECT',
|
|
15
|
+
WAGON_BASE_URL: 'BASE_URL',
|
|
16
|
+
WAGON_ADMINS: 'ADMIN_EMAILS',
|
|
17
|
+
WAGON_EMAIL_SERVER_HOST: 'EMAIL_SERVER_HOST',
|
|
18
|
+
WAGON_EMAIL_SERVER_PORT: 'EMAIL_SERVER_PORT',
|
|
19
|
+
WAGON_EMAIL_SERVER_USER: 'EMAIL_SERVER_USER',
|
|
20
|
+
WAGON_EMAIL_SERVER_PASSWORD: 'EMAIL_SERVER_PASSWORD',
|
|
21
|
+
WAGON_EMAIL_FROM: 'EMAIL_FROM',
|
|
22
|
+
WAGON_COURRIEL_FICHIER: 'EMAIL_FICHIER',
|
|
23
|
+
WAGON_STRIPE_RESTRICTED_KEY: 'STRIPE_RESTRICTED_KEY',
|
|
24
|
+
WAGON_STRIPE_SECRET_KEY: 'STRIPE_SECRET_KEY',
|
|
25
|
+
WAGON_STRIPE_WEBHOOK_SECRET: 'STRIPE_WEBHOOK_SECRET',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Signale les anciens noms encore posés : ils sont ignorés. Rend la liste. */
|
|
29
|
+
export function signalerClesRetirees(journal = console) {
|
|
30
|
+
const restes = Object.keys(CLES_RETIREES).filter((k) => getEnv(k, undefined) !== undefined);
|
|
31
|
+
for (const k of restes) journal.warn(`wagon : ${k} n'est plus lue — renommez-la ${CLES_RETIREES[k]} dans le .env (même préfixe MOSTA_ENV)`);
|
|
32
|
+
return restes;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Les courriels administrateurs déclarés (config.admins + ADMIN_EMAILS), en minuscules. */
|
|
36
|
+
export function adminsDeclares(config = {}) {
|
|
37
|
+
return [...new Set([...(config.admins ?? []), ...String(getEnv('ADMIN_EMAILS', '') ?? '').split(',')]
|
|
38
|
+
.map((x) => x.trim().toLowerCase()).filter(Boolean))];
|
|
39
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { creerWagon, schemasWagon } from './wagon.mjs';
|
|
|
5
5
|
export { demarrer } from './demarrer.mjs';
|
|
6
6
|
export { sauvegarder } from './sauvegarde.mjs';
|
|
7
7
|
export { suivrePalette } from './palette.mjs';
|
|
8
|
+
export { adminsDeclares, signalerClesRetirees, CLES_RETIREES } from './env.mjs';
|
package/src/wagon.mjs
CHANGED
|
@@ -19,8 +19,10 @@
|
|
|
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';
|
|
25
|
+
import { adminsDeclares } from './env.mjs';
|
|
24
26
|
import { creerWagonUi, esc } from '@mostajs/wagon-ui';
|
|
25
27
|
import { creerComptes, schemasComptes } from '@mostajs/comptes';
|
|
26
28
|
import { createBilling, createStripeSale, createPrepaidSale, billingSchemas, checkLimits, assertMonotonicPlans } from '@mostajs/subscriptions-plan/server';
|
|
@@ -33,7 +35,6 @@ import { emailCodeEmailHtml, emailCodeEmailText, emailCodeForm, emailLinkConfirm
|
|
|
33
35
|
import { magasinSessions } from '@mostajs/auth-plug';
|
|
34
36
|
import { requeteLegitime, enTetesSecurite, cookieStrict } from '@mostajs/garde-http';
|
|
35
37
|
import { mdToHtml } from '@mostajs/markdown-html';
|
|
36
|
-
import { getEnv } from '@mostajs/config';
|
|
37
38
|
import { suivrePalette } from './palette.mjs';
|
|
38
39
|
|
|
39
40
|
const COURRIEL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
@@ -133,7 +134,7 @@ const CredentialSchema = {
|
|
|
133
134
|
relations: {}, indexes: [{ fields: { user: 'asc' }, unique: true }],
|
|
134
135
|
};
|
|
135
136
|
/**
|
|
136
|
-
* 0.4 (audit du 09/10/2026) — les administrateurs CONFIRMÉS. Un courriel déclaré (config.admins,
|
|
137
|
+
* 0.4 (audit du 09/10/2026) — les administrateurs CONFIRMÉS. Un courriel déclaré (config.admins, ADMIN_EMAILS) ne
|
|
137
138
|
* donne l'administration qu'une fois sa détention prouvée : connexion par code ou par lien reçu à ce courriel, ou
|
|
138
139
|
* confirmation faite sur le serveur (scripts/admin.mjs). Sans cela, quiconque inscrivait ce courriel AVANT son
|
|
139
140
|
* titulaire prenait l'administration.
|
|
@@ -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) => {
|
|
@@ -185,7 +195,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
185
195
|
const https = /^https:/.test(origine);
|
|
186
196
|
const S = Object.fromEntries(schemasWagon().map((s) => [s.name, s]));
|
|
187
197
|
// 0.5 (audit du 09/10/2026) — `sgbd` : un chemin sqlite (comme avant), ou { dialect, uri } d'après le .env
|
|
188
|
-
// (
|
|
198
|
+
// (DB_DIALECT, SGBD_URI) : la base des comptes n'est plus figée en sqlite.
|
|
189
199
|
const base = typeof sgbd === 'string' ? { dialect: 'sqlite', uri: sgbd } : { dialect: sgbd?.dialect ?? 'sqlite', uri: sgbd?.uri };
|
|
190
200
|
const d = await openIsolatedDialect({ ...base, schemaStrategy: 'update' }, schemasWagon());
|
|
191
201
|
// 0.4 — la charge ouvre SA base au démarrage, pas au premier client (DEVRULES-SAAS-APP §5.2).
|
|
@@ -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,
|
|
@@ -307,7 +325,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
307
325
|
} catch (e) { journal.warn(`wagon : paiementRegle de la charge : ${e?.message ?? e}`); return { status: 500, paye: false, meta }; }
|
|
308
326
|
return { status: 200, paye: r.status === 'paid', meta };
|
|
309
327
|
}
|
|
310
|
-
const admins = new Set(
|
|
328
|
+
const admins = new Set(adminsDeclares(config));
|
|
311
329
|
/** Un courriel est administrateur s'il est DÉCLARÉ et CONFIRMÉ. */
|
|
312
330
|
const estAdministrateur = async (courriel) => {
|
|
313
331
|
const c = String(courriel ?? '').toLowerCase();
|
|
@@ -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;
|
|
@@ -684,7 +749,7 @@ export async function creerWagon({ config, charge, sgbd, baseUrl, stripe = null,
|
|
|
684
749
|
if (v.allowed) anonymes.set(k, (anonymes.get(k) ?? 0) + 1);
|
|
685
750
|
return v;
|
|
686
751
|
};
|
|
687
|
-
// 0.3.1 — `admin` : la session est celle d'un administrateur (config.admins +
|
|
752
|
+
// 0.3.1 — `admin` : la session est celle d'un administrateur (config.admins + ADMIN_EMAILS) — pour qu'une
|
|
688
753
|
// charge réserve ses outils (ex. l'outil de l'agence de travel) sans recalculer la liste.
|
|
689
754
|
/** 0.2 (wagon-engine) — un courriel de la charge, par le transport du wagon (SMTP du .env). Rend true s'il est parti,
|
|
690
755
|
* false s'il n'y a pas de transport ou s'il a échoué (journalisé) : jamais une exception dans la route. */
|
|
@@ -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.
|