Compare commits

..

10 Commits

Author SHA1 Message Date
Serreau Jovann
ecc180ca0d chore: ajouter scripts de backup et restore pour migrations
All checks were successful
Symfony CD - Scheduled Deploy / 🚀 Deploy to Production (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 12:49:32 +02:00
Serreau Jovann
5ae97c8887 fix: désactiver secure cookie pour compatibilité
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:22:08 +01:00
Serreau Jovann
1fb0cc6f3f revert: remettre le calculateur de frais de livraison
Restauration complète du système d'estimation de livraison :
- Page publique /estimer-la-livraison + liens navigation
- Calcul automatique livraison dans FlowController (admin)
- Champs distance/prix + carte Leaflet dans la vue admin flow
- Estimation livraison dans la confirmation de réservation
- Ligne "Frais de livraison" sur les devis générés

Seules les modifications CGV (suppression section 7.2 rayon 30km) sont conservées.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:20:35 +01:00
Serreau Jovann
553d12aac8 fix: supprimer le calculateur de frais de livraison et la section 7.2 des CGV
Suppression complète du système de calcul de frais de livraison (rayon 30km depuis Danizy) :
- Route /estimer-la-livraison et template estimate_delivery.twig
- Calcul automatique livraison dans FlowController et ReserverController
- Champs distance/prix livraison dans la vue admin flow
- Ligne "Frais de livraison" sur les devis générés
- Section 7.2 (mise en relation + rayon 30km) dans les CGV (twig + PDF contrat/devis)
- Liens navigation "Estimer la livraison" (desktop + mobile)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:03:16 +01:00
Serreau Jovann
31b28e5df2 fix: ajouter log détaillé sur SSO InvalidState pour diagnostic
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:46:49 +01:00
Serreau Jovann
6b882639b1 fix: utiliser Redis pour les sessions PHP au lieu des fichiers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:38:37 +01:00
Serreau Jovann
23c6a6fc1d fix: remplacer session save par session start avant le redirect OAuth
Le save() fermait la session prématurément. Le start() garantit que
la session est initialisée avant que le state OAuth y soit stocké.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:24:18 +01:00
Serreau Jovann
0109c690ad fix: rediriger vers la page login au lieu du SSO pour éviter boucle infinie
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:11:11 +01:00
Serreau Jovann
2c43d8f0ce fix: forcer session save et retry automatique pour SSO invalid state
Sauvegarde explicite de la session avant la redirection OAuth pour
garantir la persistance du state parameter. Retry automatique du
flow SSO en cas d'InvalidStateAuthenticationException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:03:23 +01:00
Serreau Jovann
9a04b30913 fix: ajouter cookie_samesite lax pour corriger SSO invalid state parameter
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 08:50:00 +01:00
11 changed files with 477 additions and 17 deletions

206
bin/migrate-backup.sh Executable file
View File

@@ -0,0 +1,206 @@
#!/usr/bin/env bash
# ============================================================================
# migrate-backup.sh
# ----------------------------------------------------------------------------
# Crée une archive complète de l'application (BDD + fichiers + config) afin
# de migrer le CRM Ludikevent d'un serveur vers un autre.
#
# Contenu de l'archive :
# - dump PostgreSQL (pg_dump --format=custom)
# - dump Redis (RDB) si redis-cli est disponible
# - dossiers d'uploads : public/media, public/images, public/pdf, public/seo
# - var/storage, sauvegarde
# - fichiers de config : .env.local, google.json
# - manifeste (versions, date, hostname)
#
# Usage :
# ./bin/migrate-backup.sh # archive dans ./sauvegarde/
# ./bin/migrate-backup.sh -o /tmp/backup.tar.gz # chemin de sortie explicite
# ./bin/migrate-backup.sh --no-redis # ignorer Redis
# ./bin/migrate-backup.sh --docker # forcer l'utilisation de docker
# ============================================================================
set -euo pipefail
# --- Couleurs ----------------------------------------------------------------
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; RESET='\033[0m'
log() { printf "${CYAN}[backup]${RESET} %s\n" "$*"; }
ok() { printf "${GREEN}[ ok ]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[warn ]${RESET} %s\n" "$*"; }
err() { printf "${RED}[error]${RESET} %s\n" "$*" >&2; }
# --- Args --------------------------------------------------------------------
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUTPUT=""
SKIP_REDIS=0
FORCE_DOCKER=0
while [[ $# -gt 0 ]]; do
case "$1" in
-o|--output) OUTPUT="$2"; shift 2 ;;
--no-redis) SKIP_REDIS=1; shift ;;
--docker) FORCE_DOCKER=1; shift ;;
-h|--help)
sed -n '2,22p' "$0"; exit 0 ;;
*) err "Argument inconnu : $1"; exit 1 ;;
esac
done
cd "$PROJECT_DIR"
TIMESTAMP="$(date +%Y-%m-%d_%H-%M-%S)"
HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)"
[[ -z "$OUTPUT" ]] && OUTPUT="$PROJECT_DIR/sauvegarde/migrate_${HOSTNAME_SHORT}_${TIMESTAMP}.tar.gz"
mkdir -p "$(dirname "$OUTPUT")"
WORK_DIR="$(mktemp -d -t crm-backup-XXXXXX)"
trap 'rm -rf "$WORK_DIR"' EXIT
log "Projet : $PROJECT_DIR"
log "Destination : $OUTPUT"
log "Workdir : $WORK_DIR"
# --- Lecture des variables d'environnement ----------------------------------
ENV_FILE=""
for f in .env.local .env; do
[[ -f "$PROJECT_DIR/$f" ]] && ENV_FILE="$PROJECT_DIR/$f" && break
done
[[ -z "$ENV_FILE" ]] && { err "Aucun fichier .env trouvé."; exit 1; }
log "Env source : $ENV_FILE"
# Récupère DATABASE_URL (dernière valeur définie, sans guillemets)
DATABASE_URL="$(grep -E '^DATABASE_URL=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
[[ -z "$DATABASE_URL" ]] && { err "DATABASE_URL introuvable dans $ENV_FILE"; exit 1; }
# postgresql://user:pass@host:port/db?...
re='^postgres(ql)?://([^:]+):([^@]*)@([^:/]+)(:([0-9]+))?/([^?]+)'
if [[ ! "$DATABASE_URL" =~ $re ]]; then
err "DATABASE_URL non reconnue : $DATABASE_URL"; exit 1
fi
PG_USER="${BASH_REMATCH[2]}"
PG_PASS="${BASH_REMATCH[3]}"
PG_HOST="${BASH_REMATCH[4]}"
PG_PORT="${BASH_REMATCH[6]:-5432}"
PG_DB="${BASH_REMATCH[7]}"
log "PostgreSQL : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB"
# --- Détection du mode (natif ou docker) ------------------------------------
USE_DOCKER=0
if [[ $FORCE_DOCKER -eq 1 ]]; then
USE_DOCKER=1
elif ! command -v pg_dump >/dev/null 2>&1; then
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -q '^crm_db$'; then
USE_DOCKER=1
warn "pg_dump non installé, fallback sur docker exec crm_db"
fi
fi
# --- Dump PostgreSQL ---------------------------------------------------------
log "Dump PostgreSQL en cours…"
DUMP_FILE="$WORK_DIR/database.dump"
if [[ $USE_DOCKER -eq 1 ]]; then
docker exec -e PGPASSWORD="$PG_PASS" crm_db \
pg_dump -U "$PG_USER" -d "$PG_DB" --format=custom --no-owner --no-acl > "$DUMP_FILE"
else
PGPASSWORD="$PG_PASS" pg_dump \
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
--format=custom --no-owner --no-acl -f "$DUMP_FILE"
fi
DUMP_SIZE="$(du -h "$DUMP_FILE" | cut -f1)"
ok "Dump BDD : $DUMP_SIZE"
# --- Dump Redis (best effort) ------------------------------------------------
if [[ $SKIP_REDIS -eq 0 ]]; then
REDIS_DSN="$(grep -E '^REDIS_DSN=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
if [[ -n "$REDIS_DSN" ]]; then
# redis://[password@]host[:port]
rre='^redis://(([^@]*)@)?([^:/]+)(:([0-9]+))?'
if [[ "$REDIS_DSN" =~ $rre ]]; then
R_PASS="${BASH_REMATCH[2]}"
R_HOST="${BASH_REMATCH[3]}"
R_PORT="${BASH_REMATCH[5]:-6379}"
REDIS_FILE="$WORK_DIR/redis.rdb"
log "Dump Redis ($R_HOST:$R_PORT)…"
if command -v redis-cli >/dev/null 2>&1; then
if [[ -n "$R_PASS" ]]; then
redis-cli -h "$R_HOST" -p "$R_PORT" -a "$R_PASS" --no-auth-warning --rdb "$REDIS_FILE" >/dev/null \
&& ok "Dump Redis : $(du -h "$REDIS_FILE" | cut -f1)" \
|| warn "Échec dump Redis (ignoré)"
else
redis-cli -h "$R_HOST" -p "$R_PORT" --rdb "$REDIS_FILE" >/dev/null \
&& ok "Dump Redis : $(du -h "$REDIS_FILE" | cut -f1)" \
|| warn "Échec dump Redis (ignoré)"
fi
else
warn "redis-cli absent, dump Redis ignoré"
fi
fi
fi
fi
# --- Copie des fichiers (uploads + config) -----------------------------------
FILES_DIR="$WORK_DIR/files"
mkdir -p "$FILES_DIR"
PATHS=(
"public/media"
"public/images"
"public/pdf"
"public/seo"
"public/tmp-sign"
"var/storage"
"sauvegarde"
".env.local"
"google.json"
)
log "Archivage des fichiers…"
for p in "${PATHS[@]}"; do
if [[ -e "$PROJECT_DIR/$p" ]]; then
mkdir -p "$FILES_DIR/$(dirname "$p")"
cp -a "$PROJECT_DIR/$p" "$FILES_DIR/$p"
ok " + $p"
else
warn " - $p (absent)"
fi
done
# Évite de réembarquer l'archive courante si elle est dans sauvegarde/
if [[ "$OUTPUT" == "$PROJECT_DIR/sauvegarde/"* && -e "$FILES_DIR/sauvegarde/$(basename "$OUTPUT")" ]]; then
rm -f "$FILES_DIR/sauvegarde/$(basename "$OUTPUT")"
fi
# --- Manifeste ---------------------------------------------------------------
GIT_COMMIT="$(git -C "$PROJECT_DIR" rev-parse HEAD 2>/dev/null || echo "n/a")"
GIT_BRANCH="$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "n/a")"
PHP_VER="$(php -r 'echo PHP_VERSION;' 2>/dev/null || echo "n/a")"
cat > "$WORK_DIR/MANIFEST.txt" <<EOF
Migration backup CRM Ludikevent
================================
Date : $(date -Iseconds)
Hostname : $(hostname)
Project dir : $PROJECT_DIR
Git branch : $GIT_BRANCH
Git commit : $GIT_COMMIT
PHP version : $PHP_VER
DB target : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB
Mode : $([[ $USE_DOCKER -eq 1 ]] && echo "docker" || echo "native")
EOF
# --- Création de l'archive ---------------------------------------------------
log "Compression de l'archive…"
tar -czf "$OUTPUT" -C "$WORK_DIR" .
ARCHIVE_SIZE="$(du -h "$OUTPUT" | cut -f1)"
ok "Archive prête : $OUTPUT ($ARCHIVE_SIZE)"
cat <<EOF
${GREEN}✔ Backup terminé.${RESET}
Pour migrer vers un autre serveur :
scp "$OUTPUT" user@nouveau-serveur:/tmp/
ssh user@nouveau-serveur
cd /chemin/vers/crm
./bin/migrate-restore.sh /tmp/$(basename "$OUTPUT")
EOF

242
bin/migrate-restore.sh Executable file
View File

@@ -0,0 +1,242 @@
#!/usr/bin/env bash
# ============================================================================
# migrate-restore.sh
# ----------------------------------------------------------------------------
# Restaure une archive produite par bin/migrate-backup.sh sur un nouveau
# serveur : BDD PostgreSQL, fichiers, config, Redis (si présent).
#
# Usage :
# ./bin/migrate-restore.sh <archive.tar.gz>
# ./bin/migrate-restore.sh <archive.tar.gz> --yes # sans confirmation
# ./bin/migrate-restore.sh <archive.tar.gz> --docker # forcer docker
# ./bin/migrate-restore.sh <archive.tar.gz> --no-redis # ignorer Redis
# ./bin/migrate-restore.sh <archive.tar.gz> --no-files # restaurer la BDD seulement
# ./bin/migrate-restore.sh <archive.tar.gz> --no-db # restaurer les fichiers seulement
#
# ⚠️ ATTENTION : la restauration ÉCRASE la base de données et les fichiers
# de destination. Une sauvegarde locale est créée dans /tmp avant action.
# ============================================================================
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; RESET='\033[0m'
log() { printf "${CYAN}[restore]${RESET} %s\n" "$*"; }
ok() { printf "${GREEN}[ ok ]${RESET} %s\n" "$*"; }
warn() { printf "${YELLOW}[ warn ]${RESET} %s\n" "$*"; }
err() { printf "${RED}[ error ]${RESET} %s\n" "$*" >&2; }
# --- Args --------------------------------------------------------------------
ARCHIVE=""
ASSUME_YES=0
FORCE_DOCKER=0
SKIP_REDIS=0
SKIP_FILES=0
SKIP_DB=0
while [[ $# -gt 0 ]]; do
case "$1" in
-y|--yes) ASSUME_YES=1; shift ;;
--docker) FORCE_DOCKER=1; shift ;;
--no-redis) SKIP_REDIS=1; shift ;;
--no-files) SKIP_FILES=1; shift ;;
--no-db) SKIP_DB=1; shift ;;
-h|--help) sed -n '2,20p' "$0"; exit 0 ;;
-*) err "Option inconnue : $1"; exit 1 ;;
*) ARCHIVE="$1"; shift ;;
esac
done
[[ -z "$ARCHIVE" ]] && { err "Usage : $0 <archive.tar.gz> [--yes] [--docker] [--no-redis|--no-files|--no-db]"; exit 1; }
[[ ! -f "$ARCHIVE" ]] && { err "Archive introuvable : $ARCHIVE"; exit 1; }
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$PROJECT_DIR"
WORK_DIR="$(mktemp -d -t crm-restore-XXXXXX)"
trap 'rm -rf "$WORK_DIR"' EXIT
log "Projet : $PROJECT_DIR"
log "Archive : $ARCHIVE"
log "Workdir : $WORK_DIR"
# --- Extraction --------------------------------------------------------------
log "Extraction de l'archive…"
tar -xzf "$ARCHIVE" -C "$WORK_DIR"
if [[ -f "$WORK_DIR/MANIFEST.txt" ]]; then
echo "----- MANIFEST -----"
cat "$WORK_DIR/MANIFEST.txt"
echo "--------------------"
fi
# --- Lecture de la config destination ---------------------------------------
ENV_FILE=""
for f in .env.local .env; do
[[ -f "$PROJECT_DIR/$f" ]] && ENV_FILE="$PROJECT_DIR/$f" && break
done
[[ -z "$ENV_FILE" ]] && { err "Aucun .env trouvé sur le serveur cible."; exit 1; }
log "Env cible : $ENV_FILE"
DATABASE_URL="$(grep -E '^DATABASE_URL=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
re='^postgres(ql)?://([^:]+):([^@]*)@([^:/]+)(:([0-9]+))?/([^?]+)'
if [[ ! "$DATABASE_URL" =~ $re ]]; then
err "DATABASE_URL non reconnue : $DATABASE_URL"; exit 1
fi
PG_USER="${BASH_REMATCH[2]}"
PG_PASS="${BASH_REMATCH[3]}"
PG_HOST="${BASH_REMATCH[4]}"
PG_PORT="${BASH_REMATCH[6]:-5432}"
PG_DB="${BASH_REMATCH[7]}"
log "Cible BDD : $PG_USER@$PG_HOST:$PG_PORT/$PG_DB"
# --- Confirmation ------------------------------------------------------------
if [[ $ASSUME_YES -eq 0 ]]; then
echo
warn "Cette opération VA ÉCRASER :"
[[ $SKIP_DB -eq 0 ]] && echo " - la base $PG_DB sur $PG_HOST:$PG_PORT"
[[ $SKIP_FILES -eq 0 ]] && echo " - les dossiers public/media, public/pdf, public/images, public/seo, var/storage, sauvegarde, .env.local"
echo
read -r -p "Confirmer ? (tapez 'oui') : " ANSWER
[[ "$ANSWER" == "oui" ]] || { err "Annulé."; exit 1; }
fi
# --- Détection du mode -------------------------------------------------------
USE_DOCKER=0
if [[ $FORCE_DOCKER -eq 1 ]]; then
USE_DOCKER=1
elif ! command -v psql >/dev/null 2>&1 || ! command -v pg_restore >/dev/null 2>&1; then
if command -v docker >/dev/null 2>&1 && docker ps --format '{{.Names}}' | grep -q '^crm_db$'; then
USE_DOCKER=1
warn "psql/pg_restore absents, fallback docker exec crm_db"
fi
fi
# --- Helpers psql / pg_restore ----------------------------------------------
run_psql() {
if [[ $USE_DOCKER -eq 1 ]]; then
docker exec -i -e PGPASSWORD="$PG_PASS" crm_db psql -U "$PG_USER" -d "$1" -v ON_ERROR_STOP=1
else
PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$1" -v ON_ERROR_STOP=1
fi
}
run_pg_restore() {
local dump_file="$1"
if [[ $USE_DOCKER -eq 1 ]]; then
docker exec -i -e PGPASSWORD="$PG_PASS" crm_db \
pg_restore -U "$PG_USER" -d "$PG_DB" --no-owner --no-acl --clean --if-exists < "$dump_file"
else
PGPASSWORD="$PG_PASS" pg_restore \
-h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
--no-owner --no-acl --clean --if-exists "$dump_file"
fi
}
# --- Sauvegarde de sécurité avant écrasement --------------------------------
SAFETY_DIR="/tmp/crm-restore-safety-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$SAFETY_DIR"
log "Sauvegarde de sécurité dans : $SAFETY_DIR"
if [[ $SKIP_DB -eq 0 ]]; then
log "Snapshot BDD courante…"
if [[ $USE_DOCKER -eq 1 ]]; then
docker exec -e PGPASSWORD="$PG_PASS" crm_db \
pg_dump -U "$PG_USER" -d "$PG_DB" --format=custom --no-owner --no-acl > "$SAFETY_DIR/database.dump" 2>/dev/null \
|| warn "Snapshot BDD impossible (base inexistante ?)"
else
PGPASSWORD="$PG_PASS" pg_dump -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" \
--format=custom --no-owner --no-acl -f "$SAFETY_DIR/database.dump" 2>/dev/null \
|| warn "Snapshot BDD impossible (base inexistante ?)"
fi
fi
# --- Restauration BDD --------------------------------------------------------
if [[ $SKIP_DB -eq 0 ]]; then
DUMP_FILE="$WORK_DIR/database.dump"
[[ ! -f "$DUMP_FILE" ]] && { err "database.dump absent de l'archive."; exit 1; }
log "Création/réinitialisation de la base $PG_DB"
# Création si inexistante (depuis postgres)
if [[ $USE_DOCKER -eq 1 ]]; then
docker exec -e PGPASSWORD="$PG_PASS" crm_db psql -U "$PG_USER" -d postgres -tc \
"SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 \
|| docker exec -e PGPASSWORD="$PG_PASS" crm_db createdb -U "$PG_USER" "$PG_DB"
else
PGPASSWORD="$PG_PASS" psql -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" -d postgres -tc \
"SELECT 1 FROM pg_database WHERE datname='$PG_DB'" | grep -q 1 \
|| PGPASSWORD="$PG_PASS" createdb -h "$PG_HOST" -p "$PG_PORT" -U "$PG_USER" "$PG_DB"
fi
log "pg_restore en cours…"
run_pg_restore "$DUMP_FILE" || warn "pg_restore a renvoyé des warnings (souvent bénins)."
ok "BDD restaurée"
fi
# --- Restauration des fichiers ----------------------------------------------
if [[ $SKIP_FILES -eq 0 && -d "$WORK_DIR/files" ]]; then
log "Sauvegarde des dossiers existants…"
for p in public/media public/images public/pdf public/seo public/tmp-sign var/storage sauvegarde; do
if [[ -e "$PROJECT_DIR/$p" ]]; then
mkdir -p "$SAFETY_DIR/$(dirname "$p")"
cp -a "$PROJECT_DIR/$p" "$SAFETY_DIR/$p"
fi
done
[[ -f "$PROJECT_DIR/.env.local" ]] && cp -a "$PROJECT_DIR/.env.local" "$SAFETY_DIR/.env.local"
log "Copie des fichiers depuis l'archive…"
# rsync si dispo (delta + permissions), sinon cp -a
if command -v rsync >/dev/null 2>&1; then
rsync -a "$WORK_DIR/files/" "$PROJECT_DIR/"
else
cp -a "$WORK_DIR/files/." "$PROJECT_DIR/"
fi
ok "Fichiers restaurés"
fi
# --- Restauration Redis ------------------------------------------------------
if [[ $SKIP_REDIS -eq 0 && -f "$WORK_DIR/redis.rdb" ]]; then
REDIS_DSN="$(grep -E '^REDIS_DSN=' "$ENV_FILE" | tail -n1 | cut -d= -f2- | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
rre='^redis://(([^@]*)@)?([^:/]+)(:([0-9]+))?'
if [[ -n "$REDIS_DSN" && "$REDIS_DSN" =~ $rre ]]; then
R_PASS="${BASH_REMATCH[2]}"
R_HOST="${BASH_REMATCH[3]}"
R_PORT="${BASH_REMATCH[5]:-6379}"
log "Restauration Redis ($R_HOST:$R_PORT)…"
if command -v redis-cli >/dev/null 2>&1; then
REDIS_AUTH=()
[[ -n "$R_PASS" ]] && REDIS_AUTH=(-a "$R_PASS" --no-auth-warning)
redis-cli -h "$R_HOST" -p "$R_PORT" "${REDIS_AUTH[@]}" FLUSHALL >/dev/null || true
# Restaure clé par clé via DEBUG RELOAD impossible à distance ;
# on se contente d'un FLUSHALL : la majorité des données Redis (cache, sessions)
# se reconstruit à l'usage. Le RDB reste dans l'archive si besoin manuel.
warn "Redis vidé. Le fichier redis.rdb reste dans $WORK_DIR pour usage manuel."
warn "Pour réinjecter : copier redis.rdb dans le datadir Redis et redémarrer."
cp "$WORK_DIR/redis.rdb" "$SAFETY_DIR/redis.rdb"
else
warn "redis-cli absent, étape Redis ignorée"
fi
fi
fi
# --- Tâches Symfony post-restauration ---------------------------------------
log "Tâches Symfony post-restauration…"
if [[ -x "$PROJECT_DIR/bin/console" ]]; then
php "$PROJECT_DIR/bin/console" cache:clear --no-interaction || warn "cache:clear KO"
php "$PROJECT_DIR/bin/console" doctrine:migrations:migrate --no-interaction --allow-no-migration || warn "migrate KO"
ok "Console Symfony OK"
else
warn "bin/console introuvable, étape ignorée"
fi
cat <<EOF
${GREEN}✔ Restauration terminée.${RESET}
Sauvegarde de sécurité (rollback possible) :
$SAFETY_DIR
Étapes de validation conseillées :
- Vérifier l'accès web (curl -I https://votre-domaine)
- Vérifier les permissions : sudo chown -R bot:www-data var public/media public/pdf
- Relancer supervisor / worker messenger si nécessaire
EOF

View File

@@ -8,9 +8,7 @@ framework:
stale_while_revalidate: 3600
stale_if_error: 3600
session:
name: crm_session
cookie_lifetime: 3600
cookie_secure: auto
handler_id: '%env(REDIS_DSN)%'
#esi: true
#fragments: true

View File

@@ -1271,7 +1271,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param;
* lifetime?: int|Param, // Default: 31536000
* path?: scalar|Param|null, // Default: "/"
* domain?: scalar|Param|null, // Default: null
* secure?: true|false|"auto"|Param, // Default: true
* secure?: true|false|"auto"|Param, // Default: false
* httponly?: bool|Param, // Default: true
* samesite?: null|"lax"|"strict"|"none"|Param, // Default: null
* always_remember_me?: bool|Param, // Default: false

View File

@@ -1077,8 +1077,10 @@ class EtlController extends AbstractController
}
#[Route('/etl/connect/keycloak', name: 'connect_keycloak_etl_start')]
public function connectKeycloakEtlStart(ClientRegistry $clientRegistry): Response
public function connectKeycloakEtlStart(ClientRegistry $clientRegistry, Request $request): Response
{
$request->getSession()->start();
$response = $clientRegistry
->getClient('keycloak_etl')
->redirect(['openid', 'profile', 'email']);

View File

@@ -26,8 +26,10 @@ class HomeController extends AbstractController
{
#[Route('/intranet/connect/keycloak', name: 'connect_keycloak_start')]
public function connect(ClientRegistry $clientRegistry): Response
public function connect(ClientRegistry $clientRegistry, Request $request): Response
{
$request->getSession()->start();
$response = $clientRegistry
->getClient('keycloak')
->redirect(['email', 'profile', 'openid'], []);

View File

@@ -17,6 +17,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Uid\Uuid;
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
class EtlKeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
@@ -89,6 +90,10 @@ class EtlKeycloakAuthenticator extends OAuth2Authenticator implements Authentica
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if ($exception instanceof InvalidStateAuthenticationException) {
return new RedirectResponse($this->router->generate('etl_login'));
}
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}

View File

@@ -18,18 +18,22 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Uid\Uuid;
use KnpU\OAuth2ClientBundle\Security\Exception\InvalidStateAuthenticationException;
use Psr\Log\LoggerInterface;
class KeycloakAuthenticator extends OAuth2Authenticator implements AuthenticationEntryPointInterface
{
private $clientRegistry;
private $entityManager;
private $router;
private $logger;
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router)
public function __construct(ClientRegistry $clientRegistry, EntityManagerInterface $entityManager, RouterInterface $router, LoggerInterface $logger)
{
$this->clientRegistry = $clientRegistry;
$this->entityManager = $entityManager;
$this->router = $router;
$this->logger = $logger;
}
public function supports(Request $request): ?bool
@@ -91,6 +95,15 @@ class KeycloakAuthenticator extends OAuth2Authenticator implements Authenticatio
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
if ($exception instanceof InvalidStateAuthenticationException) {
$this->logger->error('SSO Invalid State', [
'session_id' => $request->getSession()->getId(),
'has_session' => $request->hasSession(),
'state_param' => $request->query->get('state'),
]);
return new RedirectResponse($this->router->generate('app_home'));
}
$message = strtr($exception->getMessageKey(), $exception->getMessageData());
return new Response($message, Response::HTTP_FORBIDDEN);
}

View File

@@ -239,7 +239,7 @@ class ContratPdfService extends Fpdf
"ARTICLE 6 CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
"ARTICLE 8 ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",

View File

@@ -383,7 +383,7 @@ class DevisPdfService extends Fpdf
"ARTICLE 6 CAUTION DE GARANTIE" => "Une caution de garantie peut être exigée selon le type et la valeur du matériel loué :\n• Structures professionnelles Lilian SEGARD - Ludikevent | Selon devis | Restitution immédiate après état des lieux de fin de location et contrôle de conformité\n• Matériel mis en relation (propriétaires privés) | Selon convention propriétaire | Restitution selon accord entre parties\nLa caution peut être conservée totalement ou partiellement en cas de : dégradation du matériel, salissure importante nécessitant un nettoyage approfondi, perte d'éléments ou d'accessoires, non-respect des conditions d'utilisation ayant entraîné des dommages.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.\n7.2 Matériel en mise en relation :\nLilian SEGARD - Ludikevent peut assurer l'installation et la récupération pour le compte du propriétaire privé. Le Client locataire reste tenu d'assurer une surveillance permanente et constante. Des frais de déplacement supplémentaires peuvent s'appliquer au-delà d'un rayon de 30 km depuis Danizy.",
"ARTICLE 7 LIVRAISON, INSTALLATION ET RÉCUPÉRATION" => "7.1 Structures professionnelles Lilian SEGARD - Ludikevent :\nInstallation : L'installation est réalisée exclusivement par le personnel qualifié de Lilian SEGARD - Ludikevent. Le Client doit fournir un terrain plat, propre, dégagé, avec une alimentation électrique 220V à moins de 50 mètres. L'accès doit être praticable pour les véhicules.\nRécupération : La récupération du matériel est effectuée par Lilian SEGARD - Ludikevent aux horaires convenus.",
"ARTICLE 8 ÉTATS DES LIEUX ET TRANSFERT DE RESPONSABILITÉ" => "8.1 État des lieux d'installation : Un état des lieux contradictoire est OBLIGATOIREMENT réalisé. La signature par le Client vaut reconnaissance de conformité et TRANSFERT COMPLET DE LA RESPONSABILITÉ DU MATÉRIEL AU CLIENT. Le Client assume l'ENTIÈRE et EXCLUSIVE responsabilité du matériel, de son utilisation, de sa surveillance et des dommages qui pourraient en résulter.\n8.2 État des lieux de fin de location : Un état des lieux contradictoire est réalisé lors de la récupération. Sa signature vaut RETOUR DE LA RESPONSABILITÉ À Lilian SEGARD - Ludikevent.\n8.3 Absence de signature : En cas d'absence ou de refus de signature, l'état des lieux établi par Lilian SEGARD - Ludikevent fera foi. Le matériel sera réputé avoir été livré en parfait état et la responsabilité du Client reste pleine et entière durant toute la période de location.",

View File

@@ -510,14 +510,6 @@
</div>
</div>
{# Frais de déplacement #}
<div class="flex items-center gap-4 p-5 bg-amber-50 rounded-2xl border border-amber-100">
<span class="text-2xl">🌍</span>
<p class="text-xs md:text-sm font-bold text-amber-900 leading-tight">
Livraison incluse dans un rayon de <span class="underline decoration-amber-400 underline-offset-4">30 km depuis Danizy</span>.
Des frais de déplacement s'appliquent au-delà.
</p>
</div>
</div>
</div>