diff --git a/.env.example b/.env.example index 6677bc7..6ebe8e1 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,6 @@ -# Copy this file to .env and fill in strong secrets before running. -# cp .env.example .env +# OPTIONAL: the stack starts without any .env thanks to preconfigured +# defaults in docker-compose.yml. For production, copy this file to .env +# (or set the variables in Portainer) and override everything below. # openssl rand -hex 32 # use for JWT_SECRET # openssl rand -hex 24 # use for each DB password @@ -9,7 +10,9 @@ POSTGRES_PASSWORD=change-me-postgres-superuser # Password of the restricted application role (sbc_app) the API connects with APP_DB_PASSWORD=change-me-app-db-password -# Secret used to sign session tokens (JWT). MUST be long and random. +# Secret used to sign session tokens (JWT), at least 32 characters. +# If unset, the app generates a random one at startup (sessions are then +# invalidated whenever the container restarts). JWT_SECRET=change-me-64-hex-chars-min # Initial password of the admin account (admin@sluc-businessclub.fr). diff --git a/README.md b/README.md index d783e4a..8bac63c 100644 --- a/README.md +++ b/README.md @@ -34,27 +34,29 @@ le réseau. Supprimez la section `ports:` du service `db` pour la fermer totalem ## Démarrage ```bash -cp .env.example .env -# Éditez .env : générez des secrets forts -# openssl rand -hex 32 → JWT_SECRET -# openssl rand -hex 24 → POSTGRES_PASSWORD et APP_DB_PASSWORD -# puis choisissez ADMIN_INITIAL_PASSWORD / MEMBER_INITIAL_PASSWORD - docker compose up -d --build ``` +Aucune configuration n'est requise : des valeurs par défaut sont préréglées +(mots de passe de base de données, comptes initiaux) et le secret JWT est +généré aléatoirement au démarrage s'il n'est pas fourni. Compatible avec un +déploiement direct depuis Git dans Portainer. + +Pour la production, surchargez ces valeurs via un fichier `.env` +(voir `.env.example`) ou les variables d'environnement de Portainer. + L'application est disponible sur (port configurable via `APP_PORT`). ## Comptes -Les comptes de démonstration sont créés **verrouillés** (aucun mot de passe en dur -dans le dépôt). Au démarrage, l'API leur applique le mot de passe des variables -d'environnement — uniquement s'ils n'en ont pas déjà un : +Les comptes de démonstration sont créés **verrouillés** en base. Au démarrage, +l'API leur applique le mot de passe des variables d'environnement — uniquement +s'ils n'en ont pas déjà un : -| Rôle | Email | Mot de passe initial | -|------|-------|----------------------| -| Admin | `admin@sluc-businessclub.fr` | `ADMIN_INITIAL_PASSWORD` | -| Membre (×12) | email de contact de chaque entreprise de démo, ex. `contact@lorraine-assurances.fr` | `MEMBER_INITIAL_PASSWORD` | +| Rôle | Email | Mot de passe initial (défaut) | +|------|-------|-------------------------------| +| Admin | `admin@sluc-businessclub.fr` | `ADMIN_INITIAL_PASSWORD` (défaut : `SlucAdmin2026!`) | +| Membre (×12) | email de contact de chaque entreprise de démo, ex. `contact@lorraine-assurances.fr` | `MEMBER_INITIAL_PASSWORD` (défaut : `SlucMembre2026!`) | Changez le mot de passe admin après la première connexion (Espace membre/admin → formulaire « Mot de passe », endpoint `POST /api/auth/change-password`). @@ -88,8 +90,8 @@ formulaire « Mot de passe », endpoint `POST /api/auth/change-password`). - **Injection SQL** : 100 % de requêtes paramétrées (`pg`), aucun SQL concaténé. - **Authentification** : bcrypt (coût 12), comparaison à temps constant même si - l'email est inconnu, JWT HS256 signé (secret ≥ 32 caractères exigé au démarrage), - session de 12 h. + l'email est inconnu, JWT HS256 signé (secret fourni via `JWT_SECRET` ou généré + aléatoirement au démarrage), session de 12 h. - **Cookies** : `httpOnly`, `SameSite=Strict`, `Secure` activable (`COOKIE_SECURE=true` derrière HTTPS). - **CSRF** : cookie SameSite=Strict + vérification de l'en-tête `Origin` sur toutes @@ -110,8 +112,10 @@ formulaire « Mot de passe », endpoint `POST /api/auth/change-password`). uniquement sur `127.0.0.1` (inaccessible depuis le réseau). - **Base de données** : l'API se connecte avec un rôle dédié `sbc_app` limité au DML (pas de DDL, pas de superuser). -- **Secrets** : uniquement via `.env` (ignoré par git) ; `docker compose` refuse de - démarrer sans eux ; aucun hash ni mot de passe committé. +- **Secrets** : surchargeables via `.env` / variables d'environnement ; le secret + JWT n'est jamais committé (généré aléatoirement si absent) ; les valeurs par + défaut préréglées ne servent qu'au démarrage clé en main et doivent être + remplacées en production. - **Erreurs** : les détails restent dans les logs serveur, les clients reçoivent un message générique. @@ -119,7 +123,9 @@ formulaire « Mot de passe », endpoint `POST /api/auth/change-password`). - Placez l'application derrière HTTPS (reverse-proxy TLS) et passez `COOKIE_SECURE=true` et `TRUST_PROXY=true`. -- Changez immédiatement les mots de passe initiaux. +- Surchargez les valeurs par défaut (`POSTGRES_PASSWORD`, `APP_DB_PASSWORD`, + `JWT_SECRET`, mots de passe initiaux) et changez le mot de passe admin + après la première connexion. - Sauvegardez les volumes `db_data` (base) et `uploads` (images). ## Développement local (sans Docker) diff --git a/docker-compose.yml b/docker-compose.yml index 9547497..96de83b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,10 @@ services: environment: POSTGRES_DB: sbc POSTGRES_USER: postgres - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} - APP_DB_PASSWORD: ${APP_DB_PASSWORD:?set APP_DB_PASSWORD in .env} + # Preconfigured defaults so the stack deploys without any .env + # (Portainer, etc.). Override them in production. + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-sbc-pg-c7f3a91d284e6b05} + APP_DB_PASSWORD: ${APP_DB_PASSWORD:-sbc-app-9e12d47ab8c3f605} volumes: - db_data:/var/lib/postgresql/data - ./db/init:/docker-entrypoint-initdb.d:ro @@ -28,10 +30,12 @@ services: environment: NODE_ENV: production PORT: "3000" - DATABASE_URL: postgres://sbc_app:${APP_DB_PASSWORD}@db:5432/sbc - JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env} - ADMIN_INITIAL_PASSWORD: ${ADMIN_INITIAL_PASSWORD:-} - MEMBER_INITIAL_PASSWORD: ${MEMBER_INITIAL_PASSWORD:-} + DATABASE_URL: postgres://sbc_app:${APP_DB_PASSWORD:-sbc-app-9e12d47ab8c3f605}@db:5432/sbc + # Empty by default: the app then generates a random ephemeral secret at + # startup. Set a fixed value to keep sessions across restarts. + JWT_SECRET: ${JWT_SECRET:-} + ADMIN_INITIAL_PASSWORD: ${ADMIN_INITIAL_PASSWORD:-SlucAdmin2026!} + MEMBER_INITIAL_PASSWORD: ${MEMBER_INITIAL_PASSWORD:-SlucMembre2026!} COOKIE_SECURE: ${COOKIE_SECURE:-false} TRUST_PROXY: ${TRUST_PROXY:-false} UPLOAD_DIR: /data/uploads diff --git a/server/src/config.js b/server/src/config.js index d0decbc..92e5df2 100644 --- a/server/src/config.js +++ b/server/src/config.js @@ -1,3 +1,5 @@ +import crypto from 'node:crypto'; + const required = (name) => { const v = process.env[name]; if (!v) { @@ -7,10 +9,22 @@ const required = (name) => { return v; }; +// Session-signing secret: taken from the environment when provided, otherwise +// generated randomly at startup (never committed to the repo). With a generated +// secret, sessions simply expire when the container restarts. +let jwtSecret = process.env.JWT_SECRET || ''; +if (jwtSecret.length < 32) { + jwtSecret = crypto.randomBytes(48).toString('hex'); + console.warn( + 'JWT_SECRET absent or shorter than 32 characters: using a random ephemeral secret. ' + + 'Sessions will not survive a restart — set JWT_SECRET to persist them.' + ); +} + export const config = { port: Number(process.env.PORT || 3000), databaseUrl: required('DATABASE_URL'), - jwtSecret: required('JWT_SECRET'), + jwtSecret, cookieSecure: process.env.COOKIE_SECURE === 'true', // set to "true" only when running behind a reverse proxy (TLS termination) trustProxy: process.env.TRUST_PROXY === 'true', @@ -21,8 +35,3 @@ export const config = { sessionTtlSeconds: 12 * 60 * 60, cookieName: 'sbc_session', }; - -if (config.jwtSecret.length < 32) { - console.error('JWT_SECRET must be at least 32 characters long.'); - process.exit(1); -}