Unverified Commit 73f31f2e authored by Kourser's avatar Kourser
Browse files

Installation : un script interactif pour le .env et le démarrage

Le démarrage rapide du README suppose qu'on sache quelles variables
comptent, lesquelles sont obligatoires et laquelle casse une instance si
on la laisse à sa valeur d'exemple. install.sh pose les questions, une
par une, avec un défaut sur chacune.

Le mot de passe PostgreSQL et JWT_SECRET sont générés par openssl plutôt
que proposés à la copie. Ni l'un ni l'autre n'est affiché : le
récapitulatif dit où ils sont écrits, pas ce qu'ils valent. Le fichier
est créé sous umask 077 puis passé en 600.

Les identifiants de base et le mot de passe sont restreints au jeu de
caractères sûr pour un composant d'URL, docker-compose.yml les injectant
tels quels dans DATABASE_URL — un « @ » y casserait la connexion sans
message exploitable.

Le script ne propose que les variables que le compose transmet
réellement au conteneur. Les autres — SMTP_*, SECRETS_KEY,
TRUSTED_PROXIES, les deux rétentions d'audit et d'inscription — sont
listées en commentaire dans le .env généré, avec la raison : les définir
resterait sans effet tant qu'elles ne figurent pas au bloc
« environment: ». L'envoi de courriel se configure de toute façon depuis
la console d'instance, où le mot de passe est chiffré avant stockage.

Le jeu de données de démonstration est proposé en dernier, refusé par
défaut : son mot de passe est publié dans le README.

.gitignore couvre les sauvegardes .env.sauvegarde-* que le script crée
en écrasant une configuration existante ; la règle .env ne les prenait
pas.

Co-Authored-By: Claude (RCA)
parent 99b693e2
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -8,3 +8,4 @@ coverage/
.env.local
.DS_Store
apps/api/prisma/*.db
.env.sauvegarde-*

install.sh

0 → 100755
+341 −0
Original line number Diff line number Diff line
#!/usr/bin/env bash
#
# Installation interactive d'Enkadenn.
#   1. construit le fichier .env attendu par docker-compose.yml
#   2. lance la pile (PostgreSQL + application)
#
# Usage : ./install.sh
#
# Le script ne renseigne que les variables reellement transmises au conteneur
# par docker-compose.yml. La configuration SMTP se fait depuis la console
# d'instance, une fois l'application demarree.

set -euo pipefail

RACINE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$RACINE"
FICHIER_ENV="$RACINE/.env"

# --------------------------------------------------------------- affichage --
info() { printf '\033[36m[enkadenn]\033[0m %s\n' "$*"; }
alerte() { printf '\033[33m[enkadenn]\033[0m %s\n' "$*"; }
erreur() { printf '\033[31m[enkadenn]\033[0m %s\n' "$*" >&2; }
titre() { printf '\n\033[1m== %s\033[0m\n' "$*"; }

# -------------------------------------------------------------- garde-fous --
if [ ! -t 0 ]; then
  erreur "Ce script est interactif : lancez-le depuis un terminal."
  exit 1
fi

requis() {
  command -v "$1" >/dev/null 2>&1 ||
    { erreur $1 » est requis et introuvable dans le PATH."; exit 1; }
}
requis docker
requis openssl

if ! docker compose version >/dev/null 2>&1; then
  erreur "Docker Compose v2 requis (« docker compose version » echoue)."
  exit 1
fi
if ! docker info >/dev/null 2>&1; then
  erreur "Le demon Docker ne repond pas. Demarrez Docker, puis relancez."
  exit 1
fi
if [ ! -f "$RACINE/docker-compose.yml" ]; then
  erreur "docker-compose.yml introuvable a cote du script."
  exit 1
fi

# ------------------------------------------------------------------ saisie --
REPONSE=''

# demande <question> <defaut>  ->  REPONSE
demande() {
  local question="$1" defaut="${2:-}" saisie=''
  if [ -n "$defaut" ]; then
    printf '  %s\n    [%s] > ' "$question" "$defaut"
  else
    printf '  %s\n    [vide] > ' "$question"
  fi
  read -r saisie
  REPONSE="${saisie:-$defaut}"
}

# oui_non <question> <defaut: o|n>  ->  code retour
oui_non() {
  local question="$1" defaut="$2" saisie='' indice='[o/N]'
  [ "$defaut" = 'o' ] && indice='[O/n]'
  while :; do
    printf '  %s %s > ' "$question" "$indice"
    read -r saisie
    saisie="${saisie:-$defaut}"
    case "$saisie" in
      o | O | oui | Oui | OUI | y | Y | yes) return 0 ;;
      n | N | non | Non | NON | no) return 1 ;;
      *) alerte "Repondez par o ou n." ;;
    esac
  done
}

# nombre <question> <defaut> <min> <max>  ->  REPONSE
nombre() {
  local question="$1" defaut="$2" min="$3" max="$4"
  while :; do
    demande "$question" "$defaut"
    case "$REPONSE" in
      '' | *[!0-9]*)
        alerte "Valeur numerique attendue."
        continue
        ;;
    esac
    if [ "${#REPONSE}" -gt 10 ]; then
      alerte "Valeur hors limites (max $max)."
      continue
    fi
    if [ "$REPONSE" -lt "$min" ] || [ "$REPONSE" -gt "$max" ]; then
      alerte "Valeur attendue entre $min et $max."
      continue
    fi
    return 0
  done
}

# identifiant <question> <defaut>  ->  REPONSE
# Restreint au jeu de caracteres sur pour un composant d'URL, ces valeurs
# etant injectees telles quelles dans DATABASE_URL par docker-compose.yml.
identifiant() {
  while :; do
    demande "$1" "$2"
    case "$REPONSE" in
      '' | *[!A-Za-z0-9_-]*) alerte "Lettres, chiffres, tiret et souligne uniquement." ;;
      *) return 0 ;;
    esac
  done
}

# ------------------------------------------------------------------- .env ---
titre 'Fichier de configuration'

if [ -e "$FICHIER_ENV" ]; then
  alerte "Un fichier .env existe deja dans $RACINE."
  if ! oui_non "Le remplacer ? Une sauvegarde horodatee sera creee." n; then
    info "Abandon : .env laisse intact, rien n'a ete modifie."
    exit 0
  fi
  SAUVEGARDE="$FICHIER_ENV.sauvegarde-$(date +%Y%m%d-%H%M%S)"
  cp -p "$FICHIER_ENV" "$SAUVEGARDE"
  chmod 600 "$SAUVEGARDE"
  info "Sauvegarde : $(basename "$SAUVEGARDE") — contient vos anciens secrets, ne la diffusez pas."
else
  info "Aucun .env existant, creation d'un fichier neuf."
fi

# --------------------------------------------------------------- PostgreSQL -
titre 'Base de donnees PostgreSQL (service « db »)'

identifiant "Nom de l'utilisateur PostgreSQL" 'enkadenn'
PG_USER="$REPONSE"
identifiant "Nom de la base de donnees" 'enkadenn'
PG_DB="$REPONSE"

if oui_non "Saisir vous-meme le mot de passe ? (non = genere aleatoirement)" n; then
  while :; do
    printf '  Mot de passe (saisie masquee) > '
    read -rs MDP_A
    printf '\n'
    printf '  Confirmation                  > '
    read -rs MDP_B
    printf '\n'
    if [ "$MDP_A" != "$MDP_B" ]; then
      alerte "Les deux saisies different."
      continue
    fi
    if [ "${#MDP_A}" -lt 12 ]; then
      alerte "12 caracteres minimum."
      continue
    fi
    case "$MDP_A" in
      *[!A-Za-z0-9._~-]*)
        alerte "Caracteres autorises : lettres, chiffres, point, souligne, tilde, tiret."
        alerte "Le mot de passe est insere dans DATABASE_URL ; le reste demanderait un encodage."
        continue
        ;;
    esac
    PG_PASSWORD="$MDP_A"
    break
  done
  unset MDP_A MDP_B
else
  PG_PASSWORD="$(openssl rand -hex 24)"
  info "Mot de passe PostgreSQL genere (48 caracteres hexadecimaux)."
fi

# ----------------------------------------------------------------- secrets --
titre 'Secret de session'

JWT_SECRET="$(openssl rand -hex 32)"
info "JWT_SECRET genere (64 caracteres hexadecimaux)."
alerte "Il signe les sessions ET derive la clef de chiffrement des secrets stockes."
alerte "Le changer plus tard deconnecte tout le monde et rend illisibles les"
alerte "secrets deja chiffres en base (mot de passe SMTP) : ils seront a ressaisir."

# ------------------------------------------------------------------ reseau --
titre 'Reseau'

nombre "Port d'ecoute sur la machine hote" '3000' 1 65535
PORT="$REPONSE"

demande "URL publique de l'instance, telle que vue par les joueurs" "http://localhost:$PORT"
APP_URL="$REPONSE"

demande "URL d'un wiki public a lier depuis la page de connexion (vide = aucun lien)" ''
DOCS_URL="$REPONSE"

nombre "Nombre de proxys de confiance devant l'application (0 = aucun)" '0' 0 10
TRUSTED_PROXIES="$REPONSE"

# -------------------------------------------------------------------- RGPD --
titre 'Conservation des donnees (RGPD)'

info "Purge automatique des exercices archives. 0 desactive toute purge."
nombre "Duree de conservation, en jours" '0' 0 3650
RETENTION_DAYS="$REPONSE"

# ----------------------------------------------------------- appels vocaux --
titre 'Appels vocaux (optionnel)'

info "Sans serveur ICE/TURN, aucune donnee ne sort de l'instance : la liaison"
info "ne s'etablit qu'entre navigateurs d'un meme reseau."
while :; do
  demande "Serveurs ICE au format JSON, ex. [{\"urls\":\"turn:turn.exemple.fr:3478\"}] (vide = aucun)" ''
  case "$REPONSE" in
    *\'*) alerte "L'apostrophe n'est pas supportee dans cette valeur." ;;
    *) break ;;
  esac
done
WEBRTC_ICE_SERVERS="$REPONSE"

if [ -n "$WEBRTC_ICE_SERVERS" ]; then
  ICE_LIGNE="WEBRTC_ICE_SERVERS='$WEBRTC_ICE_SERVERS'"
else
  ICE_LIGNE="WEBRTC_ICE_SERVERS="
fi

# --------------------------------------------------------------- ecriture ---
titre 'Ecriture du fichier .env'

umask 077
cat > "$FICHIER_ENV" <<ENV
# Genere par install.sh le $(date '+%Y-%m-%d a %H:%M:%S').
#
# CE FICHIER CONTIENT DES SECRETS. Il est couvert par .gitignore : ne l'en
# sortez pas, ne le copiez pas dans une image, ne le collez pas dans un ticket.

# --- PostgreSQL ------------------------------------------------------------
POSTGRES_USER=$PG_USER
POSTGRES_PASSWORD=$PG_PASSWORD
POSTGRES_DB=$PG_DB

# --- Secret de session -----------------------------------------------------
# Signe les jetons d'authentification et derive la clef de chiffrement des
# secrets stockes en base. Le modifier deconnecte les sessions en cours et
# rend illisibles les secrets deja chiffres.
JWT_SECRET=$JWT_SECRET

# --- Reseau ----------------------------------------------------------------
PORT=$PORT
APP_URL=$APP_URL
DOCS_URL=$DOCS_URL

# --- Conservation des donnees (RGPD) ---------------------------------------
# Purge des exercices archives apres N jours. 0 = purge desactivee.
RETENTION_DAYS=$RETENTION_DAYS

# --- Appels vocaux ---------------------------------------------------------
$ICE_LIGNE

# --- Non transmis au conteneur en l'etat -----------------------------------
# docker-compose.yml n'expose pas les variables ci-dessous au service « app ».
# Les definir ici resterait sans effet : ajoutez-les d'abord au bloc
# « environment: » du compose si vous en avez besoin.
#   TRUSTED_PROXIES=$TRUSTED_PROXIES
#   SMTP_HOST= SMTP_PORT= SMTP_SECURE= SMTP_USER= SMTP_PASSWORD= MAIL_FROM=
#   SECRETS_KEY= AUDIT_RETENTION_DAYS= SIGNUP_RETENTION_DAYS=
# L'envoi de courriel se configure depuis la console d'instance, dans
# l'application : le mot de passe SMTP y est chiffre avant stockage.
ENV

chmod 600 "$FICHIER_ENV"
info "Ecrit : .env (permissions 600)"

# ------------------------------------------------------------ recapitulatif -
titre 'Recapitulatif'
printf '  PostgreSQL           : utilisateur %s, base %s\n' "$PG_USER" "$PG_DB"
printf '  Mot de passe base    : ecrit dans .env, non affiche\n'
printf '  JWT_SECRET           : ecrit dans .env, non affiche\n'
printf '  Port hote            : %s\n' "$PORT"
printf '  URL publique         : %s\n' "$APP_URL"
printf '  Wiki public          : %s\n' "${DOCS_URL:-aucun}"
if [ "$RETENTION_DAYS" = '0' ]; then
  printf '  Conservation         : purge desactivee\n'
else
  printf '  Conservation         : %s jours\n' "$RETENTION_DAYS"
fi
printf '  Serveurs ICE         : %s\n' "${WEBRTC_ICE_SERVERS:-aucun}"

# --------------------------------------------------------------- demarrage --
titre 'Demarrage de la pile Docker'

if ! oui_non "Lancer « docker compose up --build » maintenant ?" o; then
  info "Configuration prete. Pour demarrer plus tard :"
  printf '    docker compose up --build -d\n'
  exit 0
fi

if ! oui_non "Demarrer en arriere-plan (detache) ?" o; then
  info "Mode attache : les journaux defilent, Ctrl+C arrete les conteneurs."
  exec docker compose up --build
fi

docker compose up --build -d

# ------------------------------------------------------------ verification --
titre 'Verification'

if command -v curl >/dev/null 2>&1; then
  info "Attente de l'API sur http://localhost:$PORT/api/health (2 min max)..."
  ESSAIS=60
  while [ "$ESSAIS" -gt 0 ]; do
    if curl -fsS "http://localhost:$PORT/api/health" >/dev/null 2>&1; then
      info "API operationnelle."
      break
    fi
    ESSAIS=$((ESSAIS - 1))
    sleep 2
  done
  if [ "$ESSAIS" -eq 0 ]; then
    alerte "Pas de reponse apres 2 minutes. Journaux : docker compose logs -f app"
  fi
else
  alerte "curl absent : verifiez vous-meme http://localhost:$PORT/api/health"
fi

# ---------------------------------------------------------- demonstration ---
titre 'Jeu de donnees de demonstration (optionnel)'

alerte "Ce jeu cree des comptes dont le mot de passe est publie dans le README."
alerte "A reserver a un poste de test, jamais sur une instance accessible."
if oui_non "Injecter les donnees de demonstration ?" n; then
  docker compose exec app node_modules/.bin/ts-node apps/api/prisma/seed.ts ||
    alerte "Injection en echec. A relancer manuellement si besoin."
fi

# ----------------------------------------------------------------- la fin ---
titre 'Termine'
printf '  Application : %s\n' "$APP_URL"
printf '  Journaux    : docker compose logs -f app\n'
printf '  Arret       : docker compose down\n'
printf '  Arret et suppression des donnees : docker compose down -v\n'
alerte "« down -v » detruit la base et les fichiers televerses, sans confirmation."