Prérègle les secrets : déploiement sans configuration (Portainer)

- docker-compose : valeurs par défaut pour POSTGRES_PASSWORD,
  APP_DB_PASSWORD et les mots de passe initiaux (admin SlucAdmin2026!,
  membres SlucMembre2026!) — surchargeables par variables d'environnement
- JWT_SECRET devient optionnel : l'application génère un secret aléatoire
  au démarrage s'il est absent (les sessions expirent alors au redémarrage
  du conteneur), aucun secret de signature n'est committé
- .env.example et README mis à jour (démarrage clé en main, consignes de
  surcharge pour la production)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fkg15RCxNgUys4ru73He2o
This commit is contained in:
Claude committed 2026-07-10 20:02:55 +00:00
1 parent 499b753c22
commit 3f35ea584f
4 files changed
+55 -33

No files matched your search

+6 -3
View File
@@ -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).
+24 -18
View File
@@ -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 <http://localhost:8321> (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)
+10 -6
View File
@@ -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
+15 -6
View File
@@ -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);
}