feat: Version 3.3.4 - Nouvelle architecture pages, optimisations widgets Flutter et API

- Mise à jour VERSION vers 3.3.4
- Optimisations et révisions architecture API (deploy-api.sh, scripts de migration)
- Ajout documentation Stripe Tap to Pay complète
- Migration vers polices Inter Variable pour Flutter
- Optimisations build Android et nettoyage fichiers temporaires
- Amélioration système de déploiement avec gestion backups
- Ajout scripts CRON et migrations base de données

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
pierre
2025-10-05 20:11:15 +02:00
parent 2786252307
commit 570a1fa1f0
212 changed files with 24275 additions and 11321 deletions

18
bao/.gitignore vendored Normal file
View File

@@ -0,0 +1,18 @@
# Configuration avec credentials
config/.env
# Logs
*.log
# Fichiers temporaires
*.tmp
*.swp
*~
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db

504
bao/README.md Normal file
View File

@@ -0,0 +1,504 @@
# BAO - Back-office Admin Operations
Toolkit d'administration pour consulter et gérer les données chiffrées de l'API Geosector.
## 🚀 Scripts disponibles
| Script | Description | Exemple |
|--------|-------------|---------|
| `./bin/bao` | Menu interactif principal | `./bin/bao` |
| `./bin/search-user` | Rechercher un user par nom/prénom/username/secteur | `./bin/search-user rec dupont` |
| `./bin/decrypt-user` | Afficher les infos complètes d'un user | `./bin/decrypt-user rec 123` |
| `./bin/reset-password` | Générer et enregistrer un nouveau mot de passe | `./bin/reset-password rec 123` |
| `./bin/search-email` | Rechercher par email | `./bin/search-email rec contact@example.com` |
| `./bin/list-users` | Lister les utilisateurs | `./bin/list-users dev --entite=5` |
| `./bin/search-entite` | Rechercher une entité (mode interactif) | `./bin/search-entite rec plumeliau` |
| `./bin/decrypt-entite` | Afficher les infos complètes d'une entité | `./bin/decrypt-entite rec 10` |
| `./bin/list-entites` | Lister les entités | `./bin/list-entites dev --stripe` |
| `./bin/list-operations` | Lister les opérations | `./bin/list-operations dev --entite=5` |
| `./bin/list-sectors` | Lister les secteurs d'une opération | `./bin/list-sectors dev --operation=123` |
## 📋 Vue d'ensemble
BAO est un ensemble de scripts **indépendants de l'API** permettant de :
- Consulter les données **déchiffrées** des utilisateurs et entités
- Rechercher dans les champs chiffrés (username, nom, email)
- Réinitialiser les mots de passe utilisateurs
- Se connecter aux **3 environnements** (DEV, REC, PROD) via tunnels SSH
- Vérifier l'intégrité du chiffrement AES-256-CBC
## 🏗️ Architecture
```
PC Bureau (localhost)
↓ SSH Tunnels
├─→ IN3/dva-geo (DEV) → localhost:3306 → geo_app
├─→ IN3/rca-geo (REC) → localhost:3306 → geo_app
└─→ IN4/pra-geo (PROD) → localhost:3306 → pra_geo
```
### Connexions SSH
Les tunnels SSH sont gérés automatiquement via `~/.ssh/config` :
- **vpn-dva-geo** : Container DEV sur IN3 (via VPN)
- **vpn-rca-geo** : Container REC sur IN3 (via VPN)
- **pra-geo** : Container PROD sur IN4 (désactivé)
## 📦 Installation
### 1. Prérequis
```bash
# PHP 8.3+ avec extensions
php -v
php -m | grep -E 'pdo|openssl|mbstring'
# SSH configuré dans ~/.ssh/config
ssh vpn-dva-geo 'echo OK'
ssh vpn-rca-geo 'echo OK'
```
### 2. Configuration
```bash
cd /home/pierre/dev/geosector/bao
# Le fichier .env est déjà créé avec les bonnes valeurs
# Vérifier la configuration
cat config/.env
```
## 🚀 Utilisation
### Menu interactif principal
```bash
cd /home/pierre/dev/geosector/bao
./bin/bao
```
Menu avec :
1. Sélection de l'environnement (DEV/REC/PROD)
2. Actions disponibles (liste, recherche, décryptage)
3. Gestion automatique des tunnels SSH
### Scripts individuels
#### Rechercher un utilisateur
```bash
# Rechercher par nom, prénom, username ou secteur
./bin/search-user dev dupont
./bin/search-user rec pv_admin
./bin/search-user dev jean
```
**Fonctionnalités :**
- Déchiffre **tous** les utilisateurs pour chercher
- Cherche dans : `encrypted_user_name`, `encrypted_name`, `first_name`, `sect_name`
- Affiche un tableau avec les résultats
- Propose d'afficher les détails complets si 1 seul résultat
#### Décrypter un utilisateur
```bash
./bin/decrypt-user dev 123
./bin/decrypt-user rec 456
```
**Affiche :**
- Username, email, nom, prénom (déchiffrés)
- Téléphones (déchiffrés)
- Rôle, entité, dates
#### Réinitialiser un mot de passe
```bash
./bin/reset-password dev 123
./bin/reset-password rec 456
```
**Fonctionnalités :**
- Affiche les infos de l'utilisateur
- Demande confirmation
- Génère un mot de passe sécurisé (12-20 caractères, conforme aux règles API)
- Hash avec bcrypt et met à jour la base
- **Affiche le mot de passe en clair** (à sauvegarder)
#### Décrypter une entité (amicale)
```bash
./bin/decrypt-entite dev 5
./bin/decrypt-entite rec 10
```
**Affiche :**
- Nom, email, téléphones (déchiffrés)
- Adresse complète
- IBAN/BIC (déchiffrés)
- Configuration (Stripe, mots de passe, etc.)
- Statistiques (nb users, opérations)
#### Lister les utilisateurs
```bash
# Tous les utilisateurs (limite 50)
./bin/list-users dev
# Filtrer par entité
./bin/list-users dev --entite=5
# Filtrer par rôle
./bin/list-users rec --role=2
# Limite personnalisée
./bin/list-users dev --limit=100
# Combinaison de filtres
./bin/list-users dev --entite=5 --role=2 --limit=20
```
**Affiche :** Tableau avec username, email, nom (déchiffrés)
#### Rechercher une entité
```bash
# Rechercher par nom, adresse, ville ou email
./bin/search-entite dev plumeliau
./bin/search-entite rec amicale
./bin/search-entite dev appli
```
**Fonctionnalités :**
- Déchiffre **toutes** les entités pour chercher
- Cherche dans : `encrypted_name`, `encrypted_email`, `adresse1`, `adresse2`, `ville`
- Affiche un tableau numéroté avec les résultats
- **Mode interactif** :
1. Détail d'une entité → appelle `decrypt-entite`
2. Opérations d'une entité → appelle `list-operations`
3. Membres d'une entité → appelle `list-users`
#### Lister les entités
```bash
# Toutes les entités
./bin/list-entites dev
# Uniquement celles avec Stripe activé
./bin/list-entites dev --stripe
# Limite personnalisée
./bin/list-entites rec --limit=20
```
**Affiche :** Tableau avec nom, email (déchiffrés), stats
#### Lister les opérations
```bash
# Toutes les opérations
./bin/list-operations dev
# Opérations d'une entité spécifique
./bin/list-operations dev --entite=5
# Limite personnalisée
./bin/list-operations rec --entite=10 --limit=20
```
**Affiche :** Tableau avec entité, libellé, dates, nb passages, nb users, nb secteurs
#### Lister les secteurs d'une opération
```bash
# Secteurs d'une opération spécifique
./bin/list-sectors dev --operation=123
./bin/list-sectors rec --operation=456
```
**Fonctionnalités :**
- Affiche le libellé de l'opération
- Liste tous les secteurs (actifs et inactifs)
- Compte le nombre d'utilisateurs par secteur (depuis `ope_users_sectors`)
- Compte le nombre de passages par secteur
- Affiche des statistiques globales (total users, passages, secteurs actifs)
**Affiche :** Tableau avec ID, libellé, couleur, nb users, nb passages, actif, date création
#### Rechercher par email
```bash
./bin/search-email dev contact@example.com
./bin/search-email rec admin@amicale.fr
```
**Fonctionnalités :**
- Déchiffre **tous** les emails pour chercher
- Affiche **tous** les comptes avec cet email (système autorise les doublons)
- Propose d'afficher les détails complets si 1 seul résultat
### Gestion des tunnels SSH
```bash
# Afficher l'état des tunnels
./bin/_ssh-tunnel.sh status
# Ouvrir un tunnel manuellement
./bin/_ssh-tunnel.sh open dev
./bin/_ssh-tunnel.sh open rec
# Fermer un tunnel
./bin/_ssh-tunnel.sh close dev
# Fermer tous les tunnels
./bin/_ssh-tunnel.sh close-all
```
## 📁 Structure du projet
```
bao/
├── README.md # Cette documentation
├── .gitignore # Exclut config/.env
├── config/
│ ├── .env # Configuration (gitignored)
│ ├── .env.example # Template
│ └── database.php # Classe DatabaseConfig
├── lib/
│ ├── CryptoService.php # Chiffrement/déchiffrement AES-256-CBC
│ ├── DatabaseConnection.php # Connexion PDO aux bases
│ └── helpers.php # Fonctions utilitaires (affichage, couleurs)
└── bin/
├── bao # Script principal (menu interactif)
├── search-user # Rechercher un utilisateur
├── decrypt-user # Décrypter un utilisateur
├── reset-password # Réinitialiser un mot de passe
├── search-email # Rechercher par email
├── list-users # Lister les utilisateurs
├── search-entite # Rechercher une entité (interactif)
├── decrypt-entite # Décrypter une entité
├── list-entites # Lister les entités
├── list-operations # Lister les opérations
├── list-sectors # Lister les secteurs d'une opération
└── _ssh-tunnel.sh # Helper gestion tunnels SSH
```
## 🔐 Sécurité
### Chiffrement
L'API utilise **deux méthodes de chiffrement AES-256-CBC** différentes :
#### 1. Données "Searchable" (recherche)
- **Champs** : `encrypted_user_name`, `encrypted_email`
- **IV** : Fixe (16 bytes de `\0`)
- **Format** : `base64(encrypted_data)`
- **Usage** : Permet la recherche par égalité dans la base
#### 2. Données avec IV aléatoire
- **Champs** : `encrypted_name`, `encrypted_phone`, `encrypted_mobile`, `encrypted_iban`, `encrypted_bic`
- **IV** : Aléatoire (16 bytes)
- **Format** : `base64(IV + encrypted_data)`
- **Usage** : Sécurité maximale, pas de recherche directe
**Clé de chiffrement** : Base64 encodée (32 bytes) partagée avec l'API
### Données chiffrées
**Utilisateurs (`users`) :**
- `encrypted_user_name` → Username (searchable)
- `encrypted_email` → Email (searchable)
- `encrypted_name` → Nom complet (IV aléatoire)
- `encrypted_phone` → Téléphone fixe (IV aléatoire)
- `encrypted_mobile` → Téléphone mobile (IV aléatoire)
**Entités (`entites`) :**
- `encrypted_name` → Nom de l'amicale (IV aléatoire)
- `encrypted_email` → Email (searchable)
- `encrypted_phone` → Téléphone (IV aléatoire)
- `encrypted_mobile` → Mobile (IV aléatoire)
- `encrypted_iban` → Coordonnées bancaires (IV aléatoire)
- `encrypted_bic` → Code BIC (IV aléatoire)
### Bonnes pratiques
**Faire :**
- Garder `.env` hors du versioning (déjà dans `.gitignore`)
- Fermer les tunnels après usage (`Ctrl+C` ou `close-all`)
- Utiliser le menu interactif pour les tâches courantes
**Ne pas faire :**
- Partager la clé de chiffrement (`ENCRYPTION_KEY`)
- Laisser les tunnels ouverts en permanence
- Exporter les données déchiffrées sans précaution
## 🛠️ Dépannage
### Erreur : "Impossible d'ouvrir le tunnel SSH"
```bash
# Vérifier la connexion SSH
ssh dva-geo 'echo OK'
# Vérifier qu'aucun processus n'utilise le port
lsof -i :3307
lsof -i :3308
lsof -i :3309
# Fermer les tunnels zombies
./bin/_ssh-tunnel.sh close-all
```
### Erreur : "Impossible de se connecter à la base"
```bash
# Vérifier que le tunnel est actif
./bin/_ssh-tunnel.sh status
# Tester manuellement
ssh dva-geo 'mysql -u geo_app_user_dev -p geo_app -e "SELECT 1"'
```
### Erreur : "Échec du déchiffrement"
```bash
# Vérifier la clé de chiffrement
grep ENCRYPTION_KEY config/.env
# Comparer avec l'API
grep encryption_key ../api/src/Config/AppConfig.php
```
### Permissions refusées sur les scripts
```bash
# Rendre tous les scripts exécutables
chmod +x bin/*
```
## 📊 Exemples d'utilisation
### Cas d'usage 1 : Rechercher un utilisateur par nom
```bash
./bin/search-user rec dupont
```
Résultat : Tableau des utilisateurs contenant "dupont" (nom, prénom, username ou secteur)
### Cas d'usage 2 : Réinitialiser le mot de passe d'un utilisateur
```bash
./bin/search-user rec martin # Trouver l'ID
./bin/reset-password rec 56930 # Réinitialiser avec l'ID
```
Résultat : Nouveau mot de passe généré et affiché en clair
### Cas d'usage 3 : Trouver tous les comptes d'un email
```bash
./bin/search-email dev contact@amicale.fr
```
Résultat : Liste tous les utilisateurs avec cet email (peut être multiple)
### Cas d'usage 4 : Lister les admins d'une amicale
```bash
./bin/list-users dev --entite=5 --role=2
```
Résultat : Tableau des administrateurs (rôle 2) de l'entité 5
### Cas d'usage 5 : Explorer une entité (mode interactif)
```bash
./bin/search-entite rec plumeliau # Rechercher l'entité
# Sélectionner n° de ligne: 1
# Choisir action: 2 (Opérations)
```
Résultat : Workflow complet pour explorer une entité et ses données liées
### Cas d'usage 6 : Vérifier les entités avec Stripe
```bash
./bin/list-entites dev --stripe
```
Résultat : Toutes les amicales ayant activé Stripe Connect
### Cas d'usage 7 : Lister les opérations d'une entité
```bash
./bin/list-operations rec --entite=662
```
Résultat : Tableau des opérations avec stats (passages, users, secteurs)
### Cas d'usage 8 : Explorer les secteurs d'une opération
```bash
./bin/list-sectors dev --operation=123
```
Résultat : Tableau des secteurs avec nb users/passages + stats globales
### Cas d'usage 9 : Audit complet d'un utilisateur
```bash
./bin/decrypt-user dev 123
```
Résultat : Toutes les informations déchiffrées + métadonnées
## 🔄 Évolutions futures
### À court terme
- [ ] Export CSV avec données déchiffrées
- [ ] Statistiques globales par environnement
- [x] ~~Recherche par nom (déchiffré)~~ ✓ Implémenté (`search-user`)
- [x] ~~Réinitialisation de mots de passe~~ ✓ Implémenté (`reset-password`)
### À moyen terme
- [ ] Vérification HIBP pour `reset-password`
- [ ] Version Go pour performances accrues
- [ ] Cache local des données déchiffrées
- [ ] Interface TUI avec bibliotèque comme `gum`
### À long terme
- [ ] Modification de données complètes (avec validation)
- [ ] Audit trail des accès
- [ ] Synchronisation inter-environnements
## 📝 Notes
### Environnement PROD
⚠️ **Attention :** L'environnement PROD n'est **pas encore créé** (base de données inexistante).
Pour l'activer :
1. Créer la base `pra_geo` sur IN4/maria4
2. Migrer les données depuis REC
3. Changer `PROD_ENABLED=true` dans `config/.env`
### Comptes multiples par email
Le système Geosector **autorise plusieurs comptes avec le même email** (choix client).
Le script `search-email` gère cette particularité en affichant **tous** les comptes trouvés.
## 🤝 Support
Pour toute question ou amélioration :
1. Consulter le TECHBOOK de l'API : `../api/docs/TECHBOOK.md`
2. Vérifier les logs SSH : `~/.ssh/`
3. Tester la connexion directe aux containers
## 📜 Licence
Usage interne - Projet Geosector © 2025

278
bao/bin/_ssh-tunnel.sh Executable file
View File

@@ -0,0 +1,278 @@
#!/usr/bin/env bash
# =============================================================================
# SSH Tunnel Management Helper
# Gère l'ouverture/fermeture des tunnels SSH vers les containers Incus
# =============================================================================
set -euo pipefail
# Répertoire du script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BAO_ROOT="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$BAO_ROOT/config/.env"
# Couleurs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Fonctions d'affichage
log_info() {
echo -e "${BLUE}${NC} $*"
}
log_success() {
echo -e "${GREEN}${NC} $*"
}
log_error() {
echo -e "${RED}${NC} $*" >&2
}
log_warning() {
echo -e "${YELLOW}${NC} $*"
}
# Charge une variable depuis .env
get_env_var() {
local key="$1"
grep "^${key}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | tr -d ' "'
}
# Vérifie si un tunnel SSH est actif
is_tunnel_active() {
local port="$1"
# Vérifier si le port est en écoute
if command -v ss >/dev/null 2>&1; then
ss -tuln | grep -q ":${port} " && return 0
elif command -v netstat >/dev/null 2>&1; then
netstat -tuln | grep -q ":${port} " && return 0
elif command -v lsof >/dev/null 2>&1; then
lsof -i ":${port}" -sTCP:LISTEN >/dev/null 2>&1 && return 0
fi
return 1
}
# Trouve le PID du processus SSH pour un tunnel
get_tunnel_pid() {
local port="$1"
# Chercher le processus SSH qui utilise ce port local
if command -v lsof >/dev/null 2>&1; then
lsof -ti ":${port}" -sTCP:LISTEN 2>/dev/null | head -n1
else
# Fallback avec ps et grep
ps aux | grep "ssh.*:${port}:localhost:3306" | grep -v grep | awk '{print $2}' | head -n1
fi
}
# Ouvre un tunnel SSH
open_tunnel() {
local env="${1^^}" # Convertir en majuscules
local ssh_host=$(get_env_var "${env}_SSH_HOST")
local local_port=$(get_env_var "${env}_SSH_PORT_LOCAL")
local enabled=$(get_env_var "${env}_ENABLED")
if [[ "$enabled" != "true" ]]; then
log_error "Environnement ${env} désactivé dans .env"
return 1
fi
if [[ -z "$ssh_host" || -z "$local_port" ]]; then
log_error "Configuration ${env} incomplète dans .env"
return 1
fi
# Vérifier si le tunnel est déjà actif
if is_tunnel_active "$local_port"; then
log_warning "Tunnel ${env} déjà actif sur le port ${local_port}"
return 0
fi
log_info "Ouverture du tunnel SSH vers ${ssh_host} (port local ${local_port})..."
# Créer le tunnel en arrière-plan
# -N : Ne pas exécuter de commande distante
# -f : Passer en arrière-plan
# -o ExitOnForwardFailure=yes : Quitter si le tunnel échoue
# -o ServerAliveInterval=60 : Garder la connexion active
ssh -N -f \
-L "${local_port}:localhost:3306" \
-o ExitOnForwardFailure=yes \
-o ServerAliveInterval=60 \
-o ServerAliveCountMax=3 \
"$ssh_host" 2>/dev/null
# Attendre que le tunnel soit actif
local max_attempts=10
local attempt=0
while [[ $attempt -lt $max_attempts ]]; do
if is_tunnel_active "$local_port"; then
log_success "Tunnel ${env} actif sur le port ${local_port}"
return 0
fi
sleep 0.5
((attempt++))
done
log_error "Impossible d'ouvrir le tunnel ${env}"
return 1
}
# Ferme un tunnel SSH
close_tunnel() {
local env="${1^^}"
local local_port=$(get_env_var "${env}_SSH_PORT_LOCAL")
if [[ -z "$local_port" ]]; then
log_error "Port local pour ${env} introuvable dans .env"
return 1
fi
if ! is_tunnel_active "$local_port"; then
log_warning "Tunnel ${env} non actif sur le port ${local_port}"
return 0
fi
local pid=$(get_tunnel_pid "$local_port")
if [[ -z "$pid" ]]; then
log_error "Impossible de trouver le PID du tunnel ${env}"
return 1
fi
log_info "Fermeture du tunnel ${env} (PID ${pid})..."
kill "$pid" 2>/dev/null
sleep 0.5
if ! is_tunnel_active "$local_port"; then
log_success "Tunnel ${env} fermé"
return 0
else
log_error "Impossible de fermer le tunnel ${env}"
return 1
fi
}
# Affiche le statut des tunnels
status_tunnels() {
echo -e "\n${CYAN}╔════════════════════════════════════════╗${NC}"
echo -e "${CYAN}║ État des tunnels SSH ║${NC}"
echo -e "${CYAN}╚════════════════════════════════════════╝${NC}\n"
for env in DEV REC PROD; do
local enabled=$(get_env_var "${env}_ENABLED")
local local_port=$(get_env_var "${env}_SSH_PORT_LOCAL")
local ssh_host=$(get_env_var "${env}_SSH_HOST")
if [[ "$enabled" != "true" ]]; then
echo -e " ${env}: ${YELLOW}Désactivé${NC}"
continue
fi
if is_tunnel_active "$local_port"; then
local pid=$(get_tunnel_pid "$local_port")
echo -e " ${env}: ${GREEN}✓ Actif${NC} (port ${local_port}, PID ${pid})"
else
echo -e " ${env}: ${RED}✗ Inactif${NC} (port ${local_port})"
fi
done
echo ""
}
# Ferme tous les tunnels
close_all_tunnels() {
log_info "Fermeture de tous les tunnels..."
for env in DEV REC PROD; do
local enabled=$(get_env_var "${env}_ENABLED")
if [[ "$enabled" == "true" ]]; then
close_tunnel "$env" 2>/dev/null || true
fi
done
log_success "Tous les tunnels ont été fermés"
}
# Usage
usage() {
cat <<EOF
Usage: $(basename "$0") <command> [environment]
Commandes:
open <env> Ouvre un tunnel SSH vers l'environnement
close <env> Ferme le tunnel SSH
status Affiche l'état de tous les tunnels
close-all Ferme tous les tunnels actifs
Environnements: DEV, REC, PROD
Exemples:
$(basename "$0") open dev
$(basename "$0") close rec
$(basename "$0") status
$(basename "$0") close-all
EOF
}
# Main
main() {
if [[ ! -f "$ENV_FILE" ]]; then
log_error "Fichier .env introuvable: $ENV_FILE"
log_info "Copiez config/.env.example vers config/.env"
exit 1
fi
if [[ $# -eq 0 ]]; then
usage
exit 0
fi
local command="$1"
case "$command" in
open)
if [[ $# -lt 2 ]]; then
log_error "Environnement manquant"
usage
exit 1
fi
open_tunnel "$2"
;;
close)
if [[ $# -lt 2 ]]; then
log_error "Environnement manquant"
usage
exit 1
fi
close_tunnel "$2"
;;
status)
status_tunnels
;;
close-all)
close_all_tunnels
;;
*)
log_error "Commande invalide: $command"
usage
exit 1
;;
esac
}
main "$@"

272
bao/bin/bao Executable file
View File

@@ -0,0 +1,272 @@
#!/usr/bin/env bash
# =============================================================================
# BAO - Back-office Admin Operations
# Menu principal interactif pour gérer les données Geosector
# =============================================================================
set -euo pipefail
# Répertoire du script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BAO_ROOT="$(dirname "$SCRIPT_DIR")"
ENV_FILE="$BAO_ROOT/config/.env"
# Couleurs
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
MAGENTA='\033[0;35m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Fonctions d'affichage
log_info() {
echo -e "${BLUE}${NC} $*"
}
log_success() {
echo -e "${GREEN}✓${NC} $*"
}
log_error() {
echo -e "${RED}✗${NC} $*" >&2
}
log_warning() {
echo -e "${YELLOW}⚠${NC} $*"
}
# Affiche le titre principal
show_header() {
clear
echo -e "${CYAN}"
cat << "EOF"
╔════════════════════════════════════════════════════════════════╗
║ ║
║ ██████╗ █████╗ ██████╗ ║
║ ██╔══██╗██╔══██╗██╔═══██╗ ║
║ ██████╔╝███████║██║ ██║ ║
║ ██╔══██╗██╔══██║██║ ██║ ║
║ ██████╔╝██║ ██║╚██████╔╝ ║
║ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ║
║ ║
║ Back-office Admin Operations - Geosector ║
║ ║
╚════════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}\n"
}
# Charge une variable depuis .env
get_env_var() {
local key="$1"
grep "^${key}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2- | tr -d ' "'
}
# Sélectionne l'environnement
select_environment() {
echo -e "${BOLD}Sélectionnez un environnement :${NC}\n"
local envs=()
local counter=1
for env in DEV REC PROD; do
local enabled=$(get_env_var "${env}_ENABLED")
if [[ "$enabled" == "true" ]]; then
local ssh_host=$(get_env_var "${env}_SSH_HOST")
echo -e " ${GREEN}${counter})${NC} ${BOLD}${env}${NC} (${ssh_host})"
envs+=("$env")
((counter++))
else
echo -e " ${YELLOW}-${NC} ${env} ${YELLOW}(désactivé)${NC}"
fi
done
echo -e " ${RED}q)${NC} Quitter\n"
while true; do
read -p "Votre choix: " choice
if [[ "$choice" == "q" || "$choice" == "Q" ]]; then
echo ""
log_info "Au revoir !"
exit 0
fi
if [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -ge 1 ]] && [[ $choice -le ${#envs[@]} ]]; then
SELECTED_ENV="${envs[$((choice-1))]}"
return 0
fi
log_error "Choix invalide"
done
}
# Affiche le menu des actions
show_action_menu() {
echo -e "\n${BOLD}═══════════════════════════════════════════════════════════════${NC}"
echo -e "${BOLD}Environnement: ${GREEN}${SELECTED_ENV}${NC}"
echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}\n"
echo -e "${BOLD}Actions disponibles :${NC}\n"
echo -e " ${CYAN}1)${NC} Lister les utilisateurs"
echo -e " ${CYAN}2)${NC} Décrypter un utilisateur spécifique"
echo -e " ${CYAN}3)${NC} Rechercher par email"
echo -e " ${CYAN}4)${NC} Lister les entités (amicales)"
echo -e " ${CYAN}5)${NC} Décrypter une entité spécifique"
echo -e " ${CYAN}6)${NC} État des tunnels SSH"
echo -e " ${CYAN}7)${NC} Fermer tous les tunnels"
echo -e " ${YELLOW}8)${NC} Changer d'environnement"
echo -e " ${RED}q)${NC} Quitter\n"
}
# Execute une action
execute_action() {
local action="$1"
case "$action" in
1)
echo ""
read -p "Filtrer par entité ? (ID ou vide): " entite_filter
read -p "Filtrer par rôle ? (ID ou vide): " role_filter
read -p "Limite de résultats ? (défaut: 50): " limit_input
local cmd="$SCRIPT_DIR/list-users $SELECTED_ENV"
[[ -n "$entite_filter" ]] && cmd="$cmd --entite=$entite_filter"
[[ -n "$role_filter" ]] && cmd="$cmd --role=$role_filter"
[[ -n "$limit_input" ]] && cmd="$cmd --limit=$limit_input"
echo ""
$cmd
;;
2)
echo ""
read -p "ID de l'utilisateur: " user_id
if [[ -z "$user_id" || ! "$user_id" =~ ^[0-9]+$ ]]; then
log_error "ID invalide"
return 1
fi
echo ""
"$SCRIPT_DIR/decrypt-user" "$SELECTED_ENV" "$user_id"
;;
3)
echo ""
read -p "Email à rechercher: " email
if [[ -z "$email" ]]; then
log_error "Email vide"
return 1
fi
echo ""
"$SCRIPT_DIR/search-email" "$SELECTED_ENV" "$email"
;;
4)
echo ""
read -p "Uniquement les entités avec Stripe ? (o/N): " stripe_filter
read -p "Limite de résultats ? (défaut: 50): " limit_input
local cmd="$SCRIPT_DIR/list-entites $SELECTED_ENV"
[[ "$stripe_filter" == "o" || "$stripe_filter" == "O" ]] && cmd="$cmd --stripe"
[[ -n "$limit_input" ]] && cmd="$cmd --limit=$limit_input"
echo ""
$cmd
;;
5)
echo ""
read -p "ID de l'entité: " entite_id
if [[ -z "$entite_id" || ! "$entite_id" =~ ^[0-9]+$ ]]; then
log_error "ID invalide"
return 1
fi
echo ""
"$SCRIPT_DIR/decrypt-entite" "$SELECTED_ENV" "$entite_id"
;;
6)
echo ""
"$SCRIPT_DIR/_ssh-tunnel.sh" status
;;
7)
echo ""
"$SCRIPT_DIR/_ssh-tunnel.sh" close-all
;;
8)
return 2 # Signal pour changer d'environnement
;;
q|Q)
echo ""
log_info "Fermeture des tunnels..."
"$SCRIPT_DIR/_ssh-tunnel.sh" close-all
echo ""
log_success "Au revoir !"
exit 0
;;
*)
log_error "Action invalide"
return 1
;;
esac
return 0
}
# Boucle principale
main() {
if [[ ! -f "$ENV_FILE" ]]; then
log_error "Fichier .env introuvable: $ENV_FILE"
log_info "Copiez config/.env.example vers config/.env"
exit 1
fi
# Vérifier que PHP est installé
if ! command -v php >/dev/null 2>&1; then
log_error "PHP n'est pas installé"
exit 1
fi
while true; do
show_header
select_environment
while true; do
show_header
show_action_menu
read -p "Votre choix: " action
execute_action "$action"
local exit_code=$?
if [[ $exit_code -eq 2 ]]; then
# Changer d'environnement
break
fi
echo ""
read -p "Appuyez sur Entrée pour continuer..."
done
done
}
# Gestion de Ctrl+C
trap 'echo ""; log_info "Fermeture des tunnels..."; $SCRIPT_DIR/_ssh-tunnel.sh close-all 2>/dev/null; echo ""; exit 0' INT
main "$@"

114
bao/bin/decrypt-entite Executable file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de décryptage d'une entité (amicale) spécifique
* Usage: ./decrypt-entite <environment> <entite_id>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <entite_id>");
error("Exemple: " . basename($argv[0]) . " dev 5");
exit(1);
}
$environment = strtoupper($argv[1]);
$entiteId = (int)$argv[2];
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche de l'entité #$entiteId...\n");
// Requête pour récupérer l'entité
$stmt = $pdo->prepare("
SELECT
e.*,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE e.id = :entite_id
GROUP BY e.id
");
$stmt->execute(['entite_id' => $entiteId]);
$entite = $stmt->fetch();
if (!$entite) {
error("Entité #$entiteId introuvable");
exit(1);
}
// Déchiffrer les données
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$entite['name'] = $crypto->decryptWithIV($entite['encrypted_name']);
$entite['email'] = $crypto->decryptSearchable($entite['encrypted_email']);
$entite['phone'] = $crypto->decryptWithIV($entite['encrypted_phone']);
$entite['mobile'] = $crypto->decryptWithIV($entite['encrypted_mobile']);
$entite['iban'] = $crypto->decryptWithIV($entite['encrypted_iban']);
$entite['bic'] = $crypto->decryptWithIV($entite['encrypted_bic']);
// Affichage
title("ENTITÉ (AMICALE) #" . $entite['id']);
echo color("Identité\n", 'bold');
display("Nom", $entite['name']);
display("Email", $entite['email']);
display("Téléphone", $entite['phone']);
display("Mobile", $entite['mobile']);
echo "\n" . color("Adresse\n", 'bold');
display("Adresse 1", $entite['adresse1'] ?: '-');
display("Adresse 2", $entite['adresse2'] ?: '-');
display("Code postal", $entite['code_postal'] ?: '-');
display("Ville", $entite['ville'] ?: '-');
echo "\n" . color("Coordonnées bancaires\n", 'bold');
display("IBAN", $entite['iban'] ?: '-');
display("BIC", $entite['bic'] ?: '-');
echo "\n" . color("Configuration\n", 'bold');
display("Stripe activé", $entite['chk_stripe'] ? 'Oui' : 'Non');
display("Gestion MDP manuelle", $entite['chk_mdp_manuel'] ? 'Oui' : 'Non');
display("Gestion username manuelle", $entite['chk_username_manuel'] ? 'Oui' : 'Non');
display("Copie mail reçu", $entite['chk_copie_mail_recu'] ? 'Oui' : 'Non');
display("Accepte SMS", $entite['chk_accept_sms'] ? 'Oui' : 'Non');
echo "\n" . color("Statistiques\n", 'bold');
display("Nombre d'utilisateurs", (string)$entite['nb_users']);
display("Nombre d'opérations", (string)$entite['nb_operations']);
echo "\n" . color("Dates\n", 'bold');
display("Date création", formatDate($entite['created_at']));
display("Dernière modif", formatDate($entite['updated_at']));
echo "\n";
success("Entité déchiffrée avec succès");
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

119
bao/bin/decrypt-user Executable file
View File

@@ -0,0 +1,119 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de décryptage d'un utilisateur spécifique
* Usage: ./decrypt-user <environment> <user_id>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <user_id>");
error("Exemple: " . basename($argv[0]) . " dev 123");
exit(1);
}
$environment = strtoupper($argv[1]);
$userId = (int)$argv[2];
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche de l'utilisateur #$userId...\n");
// Requête pour récupérer l'utilisateur
$stmt = $pdo->prepare("
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name,
u.encrypted_phone,
u.encrypted_mobile,
u.sect_name,
u.fk_role,
u.fk_entite,
u.fk_titre,
u.date_naissance,
u.date_embauche,
u.created_at,
u.updated_at,
r.libelle as role_name,
e.encrypted_name as entite_encrypted_name
FROM users u
LEFT JOIN x_users_roles r ON u.fk_role = r.id
LEFT JOIN entites e ON u.fk_entite = e.id
WHERE u.id = :user_id
");
$stmt->execute(['user_id' => $userId]);
$user = $stmt->fetch();
if (!$user) {
error("Utilisateur #$userId introuvable");
exit(1);
}
// Déchiffrer les données
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$user['user_name'] = $crypto->decryptSearchable($user['encrypted_user_name']);
$user['email'] = $crypto->decryptSearchable($user['encrypted_email']);
$user['name'] = $crypto->decryptWithIV($user['encrypted_name']);
$user['phone'] = $crypto->decryptWithIV($user['encrypted_phone']);
$user['mobile'] = $crypto->decryptWithIV($user['encrypted_mobile']);
$user['entite_name'] = $crypto->decryptWithIV($user['entite_encrypted_name']);
// Affichage
title("UTILISATEUR #" . $user['id']);
echo color("Identité\n", 'bold');
display("Username", $user['user_name']);
display("Prénom", $user['first_name']);
display("Nom", $user['name']);
display("Email", $user['email']);
display("Téléphone", $user['phone']);
display("Mobile", $user['mobile']);
echo "\n" . color("Fonction\n", 'bold');
display("Rôle", $user['role_name'] . " (ID: " . $user['fk_role'] . ")");
display("Secteur", $user['sect_name'] ?: '-');
display("Titre", $user['fk_titre'] ? "#" . $user['fk_titre'] : '-');
echo "\n" . color("Amicale\n", 'bold');
display("ID Entité", (string)$user['fk_entite']);
display("Nom Entité", $user['entite_name']);
echo "\n" . color("Dates\n", 'bold');
display("Date naissance", formatDate($user['date_naissance']));
display("Date embauche", formatDate($user['date_embauche']));
display("Date création", formatDate($user['created_at']));
display("Dernière modif", formatDate($user['updated_at']));
echo "\n";
success("Utilisateur déchiffré avec succès");
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

132
bao/bin/list-entites Executable file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de listing des entités (amicales) avec données déchiffrées
* Usage: ./list-entites <environment> [--stripe] [--limit=N]
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 2) {
error("Usage: " . basename($argv[0]) . " <environment> [--stripe] [--limit=N]");
error("Exemple: " . basename($argv[0]) . " dev --stripe");
exit(1);
}
$environment = strtoupper($argv[1]);
$stripeOnly = false;
$limit = 50;
// Parser les options
for ($i = 2; $i < $argc; $i++) {
if ($argv[$i] === '--stripe') {
$stripeOnly = true;
} elseif (preg_match('/--limit=(\d+)/', $argv[$i], $matches)) {
$limit = (int)$matches[1];
}
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
if ($stripeOnly) {
info("Filtre: Stripe activé uniquement");
}
info("Limite: $limit\n");
// Construction de la requête
$sql = "
SELECT
e.id,
e.encrypted_name,
e.encrypted_email,
e.encrypted_phone,
e.ville,
e.chk_stripe,
e.chk_mdp_manuel,
e.chk_username_manuel,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE 1=1
";
if ($stripeOnly) {
$sql .= " AND e.chk_stripe = 1";
}
$sql .= " GROUP BY e.id ORDER BY e.id DESC LIMIT :limit";
$stmt = $pdo->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$entites = $stmt->fetchAll();
if (empty($entites)) {
warning("Aucune entité trouvée");
exit(0);
}
// Déchiffrer les données
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$decryptedEntites = [];
foreach ($entites as $entite) {
$decryptedEntites[] = [
'id' => $entite['id'],
'name' => truncate($crypto->decryptWithIV($entite['encrypted_name']) ?? '-', 30),
'ville' => truncate($entite['ville'] ?? '-', 20),
'email' => truncate($crypto->decryptSearchable($entite['encrypted_email']) ?? '-', 30),
'phone' => $crypto->decryptWithIV($entite['encrypted_phone']) ?? '-',
'users' => $entite['nb_users'],
'ops' => $entite['nb_operations'],
'stripe' => $entite['chk_stripe'] ? '✓' : '✗',
];
}
// Affichage
title("LISTE DES ENTITÉS (AMICALES) - " . count($decryptedEntites) . " résultat(s)");
table(
[
'id' => 'ID',
'name' => 'Nom',
'ville' => 'Ville',
'email' => 'Email',
'phone' => 'Téléphone',
'users' => 'Users',
'ops' => 'Ops',
'stripe' => 'Stripe',
],
$decryptedEntites,
true
);
success("Affichage terminé");
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

156
bao/bin/list-operations Executable file
View File

@@ -0,0 +1,156 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de listage des opérations
* Usage: ./list-operations <environment> [--entite=<id>] [--limit=<n>]
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 2) {
error("Usage: " . basename($argv[0]) . " <environment> [--entite=<id>] [--limit=<n>]");
error("Exemple: " . basename($argv[0]) . " dev");
error(" " . basename($argv[0]) . " rec --entite=5");
error(" " . basename($argv[0]) . " dev --entite=10 --limit=20");
exit(1);
}
$environment = strtoupper($argv[1]);
// Parser les options
$filters = [];
$limit = 50;
for ($i = 2; $i < $argc; $i++) {
if (preg_match('/^--entite=(\d+)$/', $argv[$i], $matches)) {
$filters['entite'] = (int)$matches[1];
} elseif (preg_match('/^--limit=(\d+)$/', $argv[$i], $matches)) {
$limit = (int)$matches[1];
} else {
error("Option invalide: " . $argv[$i]);
exit(1);
}
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
if (!empty($filters)) {
info("Filtres: " . json_encode($filters));
}
info("Limite: $limit\n");
// Construction de la requête
$sql = "
SELECT
o.id,
o.fk_entite,
o.libelle,
o.date_deb,
o.date_fin,
o.chk_distinct_sectors,
o.chk_active,
o.created_at,
o.updated_at,
e.encrypted_name as entite_encrypted_name,
COUNT(DISTINCT p.id) as nb_passages,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT s.id) as nb_sectors
FROM operations o
LEFT JOIN entites e ON e.id = o.fk_entite
LEFT JOIN ope_pass p ON p.fk_operation = o.id
LEFT JOIN ope_users u ON u.fk_operation = o.id
LEFT JOIN ope_sectors s ON s.fk_operation = o.id
WHERE 1=1
";
$params = [];
if (isset($filters['entite'])) {
$sql .= " AND o.fk_entite = :entite";
$params['entite'] = $filters['entite'];
}
$sql .= " GROUP BY o.id";
$sql .= " ORDER BY o.date_deb DESC";
$sql .= " LIMIT :limit";
$stmt = $pdo->prepare($sql);
// Bind des paramètres
foreach ($params as $key => $value) {
$stmt->bindValue(':' . $key, $value, PDO::PARAM_INT);
}
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
$operations = $stmt->fetchAll();
if (empty($operations)) {
warning("\nAucune opération trouvée");
exit(0);
}
// Déchiffrer les noms d'entités
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$operationsData = [];
foreach ($operations as $op) {
$operationsData[] = [
'id' => $op['id'],
'entite' => truncate($crypto->decryptWithIV($op['entite_encrypted_name']) ?? '-', 25),
'libelle' => truncate($op['libelle'], 35),
'date_deb' => date('d/m/Y', strtotime($op['date_deb'])),
'date_fin' => date('d/m/Y', strtotime($op['date_fin'])),
'passages' => $op['nb_passages'],
'users' => $op['nb_users'],
'sectors' => $op['nb_sectors'],
'actif' => $op['chk_active'] ? '✓' : '✗',
];
}
// Affichage
title("LISTE DES OPÉRATIONS - " . count($operationsData) . " résultat(s)");
table(
[
'id' => 'ID',
'entite' => 'Entité',
'libelle' => 'Libellé',
'date_deb' => 'Début',
'date_fin' => 'Fin',
'passages' => 'Passages',
'users' => 'Users',
'sectors' => 'Secteurs',
'actif' => 'Actif',
],
$operationsData,
true
);
success("Opérations listées avec succès");
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

145
bao/bin/list-sectors Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de listage des secteurs d'une opération
* Usage: ./list-sectors <environment> --operation=<id>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> --operation=<id>");
error("Exemple: " . basename($argv[0]) . " dev --operation=123");
exit(1);
}
$environment = strtoupper($argv[1]);
// Parser les options
$operationId = null;
for ($i = 2; $i < $argc; $i++) {
if (preg_match('/^--operation=(\d+)$/', $argv[$i], $matches)) {
$operationId = (int)$matches[1];
} else {
error("Option invalide: " . $argv[$i]);
exit(1);
}
}
if ($operationId === null) {
error("Le paramètre --operation=<id> est obligatoire");
exit(1);
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Opération: #$operationId\n");
// Vérifier que l'opération existe
$stmtOp = $pdo->prepare("SELECT libelle FROM operations WHERE id = :operation_id");
$stmtOp->execute(['operation_id' => $operationId]);
$operation = $stmtOp->fetch();
if (!$operation) {
error("Opération #$operationId introuvable");
exit(1);
}
info("Libellé opération: " . $operation['libelle'] . "\n");
// Récupérer les secteurs avec le nombre d'utilisateurs et de passages
$stmt = $pdo->prepare("
SELECT
s.id,
s.libelle,
s.color,
s.chk_active,
s.created_at,
COUNT(DISTINCT us.fk_user) as nb_users,
COUNT(DISTINCT p.id) as nb_passages
FROM ope_sectors s
LEFT JOIN ope_users_sectors us ON us.fk_sector = s.id AND us.chk_active = 1
LEFT JOIN ope_pass p ON p.fk_sector = s.id AND p.chk_active = 1
WHERE s.fk_operation = :operation_id
GROUP BY s.id
ORDER BY s.id
");
$stmt->execute(['operation_id' => $operationId]);
$sectors = $stmt->fetchAll();
if (empty($sectors)) {
warning("\nAucun secteur trouvé pour cette opération");
exit(0);
}
// Préparer les données pour le tableau
$sectorsData = [];
foreach ($sectors as $sector) {
$sectorsData[] = [
'id' => $sector['id'],
'libelle' => truncate($sector['libelle'], 40),
'color' => $sector['color'],
'users' => $sector['nb_users'],
'passages' => $sector['nb_passages'],
'actif' => $sector['chk_active'] ? '✓' : '✗',
'created' => date('d/m/Y', strtotime($sector['created_at'])),
];
}
// Affichage
title("SECTEURS - Opération #$operationId - " . count($sectorsData) . " secteur(s)");
table(
[
'id' => 'ID',
'libelle' => 'Libellé',
'color' => 'Couleur',
'users' => 'Users',
'passages' => 'Passages',
'actif' => 'Actif',
'created' => 'Créé le',
],
$sectorsData,
true
);
success("Secteurs listés avec succès");
// Afficher les statistiques globales
$totalUsers = array_sum(array_column($sectorsData, 'users'));
$totalPassages = array_sum(array_column($sectorsData, 'passages'));
$activeSectors = count(array_filter($sectorsData, fn($s) => $s['actif'] === '✓'));
echo "\n";
echo color("Statistiques:\n", 'bold');
display("Secteurs actifs", "$activeSectors / " . count($sectorsData));
display("Total utilisateurs", (string)$totalUsers);
display("Total passages", (string)$totalPassages);
echo "\n";
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

149
bao/bin/list-users Executable file
View File

@@ -0,0 +1,149 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de listing des utilisateurs avec données déchiffrées
* Usage: ./list-users <environment> [--entite=X] [--role=Y] [--limit=N]
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 2) {
error("Usage: " . basename($argv[0]) . " <environment> [--entite=X] [--role=Y] [--limit=N]");
error("Exemple: " . basename($argv[0]) . " dev --entite=5 --limit=20");
exit(1);
}
$environment = strtoupper($argv[1]);
$filters = [];
$limit = 50;
// Parser les options
for ($i = 2; $i < $argc; $i++) {
if (preg_match('/--entite=(\d+)/', $argv[$i], $matches)) {
$filters['entite'] = (int)$matches[1];
} elseif (preg_match('/--role=(\d+)/', $argv[$i], $matches)) {
$filters['role'] = (int)$matches[1];
} elseif (preg_match('/--limit=(\d+)/', $argv[$i], $matches)) {
$limit = (int)$matches[1];
}
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
if (!empty($filters)) {
info("Filtres: " . json_encode($filters));
}
info("Limite: $limit\n");
// Construction de la requête
$sql = "
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name,
u.fk_role,
u.fk_entite,
r.libelle as role_name,
e.encrypted_name as entite_encrypted_name
FROM users u
LEFT JOIN x_users_roles r ON u.fk_role = r.id
LEFT JOIN entites e ON u.fk_entite = e.id
WHERE 1=1
";
$params = [];
if (isset($filters['entite'])) {
$sql .= " AND u.fk_entite = :entite";
$params['entite'] = $filters['entite'];
}
if (isset($filters['role'])) {
$sql .= " AND u.fk_role = :role";
$params['role'] = $filters['role'];
}
$sql .= " ORDER BY u.id DESC LIMIT :limit";
$params['limit'] = $limit;
$stmt = $pdo->prepare($sql);
// Bind des paramètres
foreach ($params as $key => $value) {
if ($key === 'limit') {
$stmt->bindValue(':' . $key, $value, PDO::PARAM_INT);
} else {
$stmt->bindValue(':' . $key, $value);
}
}
$stmt->execute();
$users = $stmt->fetchAll();
if (empty($users)) {
warning("Aucun utilisateur trouvé");
exit(0);
}
// Déchiffrer les données
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$decryptedUsers = [];
foreach ($users as $user) {
$decryptedUsers[] = [
'id' => $user['id'],
'username' => truncate($crypto->decryptSearchable($user['encrypted_user_name']) ?? '-', 20),
'email' => truncate($crypto->decryptSearchable($user['encrypted_email']) ?? '-', 30),
'prenom' => truncate($user['first_name'] ?? '-', 15),
'nom' => truncate($crypto->decryptWithIV($user['encrypted_name']) ?? '-', 20),
'role' => $user['role_name'] ?? '-',
'entite' => truncate($crypto->decryptWithIV($user['entite_encrypted_name']) ?? '-', 25),
];
}
// Affichage
title("LISTE DES UTILISATEURS - " . count($decryptedUsers) . " résultat(s)");
table(
[
'id' => 'ID',
'username' => 'Username',
'prenom' => 'Prénom',
'nom' => 'Nom',
'email' => 'Email',
'role' => 'Rôle',
'entite' => 'Entité',
],
$decryptedUsers,
true
);
success("Affichage terminé");
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

174
bao/bin/reset-password Executable file
View File

@@ -0,0 +1,174 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de réinitialisation du mot de passe d'un utilisateur
* Génère un nouveau mot de passe sécurisé et l'enregistre
* Usage: ./reset-password <environment> <user_id>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <user_id>");
error("Exemple: " . basename($argv[0]) . " dev 123");
exit(1);
}
$environment = strtoupper($argv[1]);
$userId = (int)$argv[2];
/**
* Génère un mot de passe sécurisé selon les règles de l'API
* Règles conformes à PasswordSecurityService de l'API
*
* @param int $length Longueur du mot de passe (12-20)
* @return string Mot de passe généré
*/
function generateSecurePassword(int $length = 14): string {
// Limiter la longueur entre 12 et 20 (règles API)
$length = max(12, min(20, $length));
// Caractères autorisés (sans ambiguïté visuelle - règles API)
$lowercase = 'abcdefghijkmnopqrstuvwxyz'; // sans l
$uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ'; // sans I, O
$numbers = '23456789'; // sans 0, 1
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
$password = '';
// Garantir au moins un caractère de chaque type (règle API)
$password .= $lowercase[random_int(0, strlen($lowercase) - 1)];
$password .= $uppercase[random_int(0, strlen($uppercase) - 1)];
$password .= $numbers[random_int(0, strlen($numbers) - 1)];
$password .= $special[random_int(0, strlen($special) - 1)];
// Compléter avec des caractères aléatoires
$allChars = $lowercase . $uppercase . $numbers . $special;
for ($i = strlen($password); $i < $length; $i++) {
$password .= $allChars[random_int(0, strlen($allChars) - 1)];
}
// Mélanger les caractères (règle API)
$passwordArray = str_split($password);
shuffle($passwordArray);
return implode('', $passwordArray);
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche de l'utilisateur #$userId...\n");
// Vérifier que l'utilisateur existe
$stmt = $pdo->prepare("
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name
FROM users u
WHERE u.id = :user_id
");
$stmt->execute(['user_id' => $userId]);
$user = $stmt->fetch();
if (!$user) {
error("Utilisateur #$userId introuvable");
exit(1);
}
// Déchiffrer les données pour affichage
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$userName = $crypto->decryptSearchable($user['encrypted_user_name']);
$email = $crypto->decryptSearchable($user['encrypted_email']);
$name = $crypto->decryptWithIV($user['encrypted_name']);
$firstName = $user['first_name'];
// Afficher les infos utilisateur
title("RÉINITIALISATION DU MOT DE PASSE");
echo color("Utilisateur\n", 'bold');
display("ID", (string)$user['id']);
display("Username", $userName);
display("Prénom", $firstName);
display("Nom", $name);
display("Email", $email);
echo "\n";
// Demander confirmation
if (!confirm("Confirmer la réinitialisation du mot de passe ?")) {
warning("Opération annulée");
exit(0);
}
// Générer un nouveau mot de passe
$newPassword = generateSecurePassword(14);
$passwordHash = password_hash($newPassword, PASSWORD_BCRYPT);
info("\nGénération du nouveau mot de passe...");
// Mettre à jour la base de données
$updateStmt = $pdo->prepare("
UPDATE users
SET user_pass_hash = :password_hash,
updated_at = CURRENT_TIMESTAMP
WHERE id = :user_id
");
$updateStmt->execute([
'password_hash' => $passwordHash,
'user_id' => $userId
]);
if ($updateStmt->rowCount() === 0) {
error("Erreur lors de la mise à jour du mot de passe");
exit(1);
}
// Afficher le résultat
echo "\n";
success("Mot de passe réinitialisé avec succès !");
echo "\n";
echo color("═══════════════════════════════════════════\n", 'cyan');
echo color(" NOUVEAU MOT DE PASSE\n", 'bold');
echo color("═══════════════════════════════════════════\n", 'cyan');
echo "\n";
echo color(" ", 'green') . color($newPassword, 'yellow') . "\n";
echo "\n";
echo color("═══════════════════════════════════════════\n", 'cyan');
echo "\n";
warning("⚠ Conservez ce mot de passe en lieu sûr !");
warning(" Il ne sera pas possible de le récupérer.");
echo "\n";
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

131
bao/bin/search-email Executable file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de recherche par email (déchiffré)
* Usage: ./search-email <environment> <email>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <email>");
error("Exemple: " . basename($argv[0]) . " dev contact@example.com");
exit(1);
}
$environment = strtoupper($argv[1]);
$searchEmail = strtolower(trim($argv[2]));
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche de l'email: $searchEmail\n");
// Récupérer TOUS les utilisateurs (on doit déchiffrer pour chercher)
$stmt = $pdo->query("
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name,
u.fk_role,
u.fk_entite,
r.libelle as role_name,
e.encrypted_name as entite_encrypted_name
FROM users u
LEFT JOIN x_users_roles r ON u.fk_role = r.id
LEFT JOIN entites e ON u.fk_entite = e.id
ORDER BY u.id
");
$allUsers = $stmt->fetchAll();
// Déchiffrer et filtrer
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$matchedUsers = [];
info("Analyse de " . count($allUsers) . " utilisateurs...");
foreach ($allUsers as $user) {
$email = $crypto->decryptSearchable($user['encrypted_email']);
if ($email && strtolower($email) === $searchEmail) {
$matchedUsers[] = [
'id' => $user['id'],
'username' => $crypto->decryptSearchable($user['encrypted_user_name']),
'email' => $email,
'prenom' => $user['first_name'],
'nom' => $crypto->decryptWithIV($user['encrypted_name']),
'role' => $user['role_name'],
'entite_id' => $user['fk_entite'],
'entite' => $crypto->decryptWithIV($user['entite_encrypted_name']),
];
}
}
if (empty($matchedUsers)) {
warning("\nAucun utilisateur trouvé avec l'email: $searchEmail");
exit(0);
}
// Affichage
echo "\n";
title("RÉSULTATS DE LA RECHERCHE - " . count($matchedUsers) . " utilisateur(s) trouvé(s)");
if (count($matchedUsers) > 1) {
warning("Attention: Plusieurs comptes utilisent le même email (autorisé par le système)");
echo "\n";
}
foreach ($matchedUsers as $index => $user) {
echo color("═══ Utilisateur #" . $user['id'] . " ═══", 'cyan') . "\n";
display("Username", $user['username']);
display("Prénom", $user['prenom']);
display("Nom", $user['nom']);
display("Email", $user['email']);
display("Rôle", $user['role']);
display("Entité ID", (string)$user['entite_id']);
display("Entité Nom", $user['entite']);
echo "\n";
}
success("Recherche terminée");
// Proposer d'afficher les détails complets
if (count($matchedUsers) === 1) {
echo "\n";
if (confirm("Afficher les détails complets de cet utilisateur ?")) {
echo "\n";
$userId = $matchedUsers[0]['id'];
$decryptUserScript = __DIR__ . '/decrypt-user';
passthru("$decryptUserScript $environment $userId");
}
}
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

316
bao/bin/search-entite Executable file
View File

@@ -0,0 +1,316 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de recherche d'entité par nom/adresse/ville/email
* Usage: ./search-entite <environment> <search_term>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <search_term>");
error("Exemple: " . basename($argv[0]) . " dev plumeliau");
error("Exemple: " . basename($argv[0]) . " rec amicale");
exit(1);
}
$environment = strtoupper($argv[1]);
$searchTerm = strtolower(trim($argv[2]));
if (strlen($searchTerm) < 2) {
error("La recherche doit contenir au moins 2 caractères");
exit(1);
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche: \"$searchTerm\"");
info("Champs: nom, email, adresse1, adresse2, ville\n");
// Pré-filtrage sur les champs en clair (adresse1, adresse2, ville)
$stmt = $pdo->prepare("
SELECT
e.id,
e.encrypted_name,
e.encrypted_email,
e.adresse1,
e.adresse2,
e.code_postal,
e.ville,
e.chk_stripe,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE e.chk_active = 1
AND (LOWER(e.adresse1) LIKE :search1
OR LOWER(e.adresse2) LIKE :search2
OR LOWER(e.ville) LIKE :search3)
GROUP BY e.id
ORDER BY e.id
");
$searchPattern = '%' . $searchTerm . '%';
$stmt->execute([
'search1' => $searchPattern,
'search2' => $searchPattern,
'search3' => $searchPattern
]);
$preFilteredEntites = $stmt->fetchAll();
// Récupérer TOUTES les entités pour chercher dans les champs chiffrés
$stmtAll = $pdo->query("
SELECT
e.id,
e.encrypted_name,
e.encrypted_email,
e.adresse1,
e.adresse2,
e.code_postal,
e.ville,
e.chk_stripe,
COUNT(DISTINCT u.id) as nb_users,
COUNT(DISTINCT o.id) as nb_operations
FROM entites e
LEFT JOIN users u ON u.fk_entite = e.id
LEFT JOIN operations o ON o.fk_entite = e.id
WHERE e.chk_active = 1
GROUP BY e.id
ORDER BY e.id
");
$allEntites = $stmtAll->fetchAll();
info("Analyse de " . count($allEntites) . " entités actives...\n");
// Déchiffrer et filtrer
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$matchedEntites = [];
$seenIds = [];
// Ajouter d'abord les résultats pré-filtrés (adresse1, adresse2, ville)
foreach ($preFilteredEntites as $entite) {
$name = $crypto->decryptWithIV($entite['encrypted_name']);
$email = $crypto->decryptSearchable($entite['encrypted_email']);
// Vérifier aussi dans les champs chiffrés
$matches = false;
$matchedFields = [];
if (stripos($entite['adresse1'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'adresse1';
}
if (stripos($entite['adresse2'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'adresse2';
}
if (stripos($entite['ville'], $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'ville';
}
if ($name && stripos($name, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($email && stripos($email, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'email';
}
if ($matches) {
$matchedEntites[] = [
'id' => $entite['id'],
'name' => $name ?? '-',
'email' => $email ?? '-',
'adresse1' => $entite['adresse1'] ?? '-',
'code_postal' => $entite['code_postal'] ?? '-',
'ville' => $entite['ville'] ?? '-',
'stripe' => $entite['chk_stripe'] ? 'Oui' : 'Non',
'nb_users' => $entite['nb_users'],
'nb_operations' => $entite['nb_operations'],
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$entite['id']] = true;
}
}
// Chercher dans les entités restantes (pour nom et email chiffrés)
foreach ($allEntites as $entite) {
if (isset($seenIds[$entite['id']])) {
continue; // Déjà trouvé
}
$name = $crypto->decryptWithIV($entite['encrypted_name']);
$email = $crypto->decryptSearchable($entite['encrypted_email']);
$matches = false;
$matchedFields = [];
if ($name && stripos($name, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($email && stripos($email, $searchTerm) !== false) {
$matches = true;
$matchedFields[] = 'email';
}
if ($matches) {
$matchedEntites[] = [
'id' => $entite['id'],
'name' => $name ?? '-',
'email' => $email ?? '-',
'adresse1' => $entite['adresse1'] ?? '-',
'code_postal' => $entite['code_postal'] ?? '-',
'ville' => $entite['ville'] ?? '-',
'stripe' => $entite['chk_stripe'] ? 'Oui' : 'Non',
'nb_users' => $entite['nb_users'],
'nb_operations' => $entite['nb_operations'],
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$entite['id']] = true;
}
}
if (empty($matchedEntites)) {
warning("\nAucune entité trouvée avec: \"$searchTerm\"");
exit(0);
}
// Affichage
title("RÉSULTATS - " . count($matchedEntites) . " entité(s) trouvée(s)");
// Préparer les données pour le tableau
$tableData = [];
$lineNumber = 1;
foreach ($matchedEntites as $entite) {
$tableData[] = [
'line' => $lineNumber++,
'id' => $entite['id'],
'name' => truncate($entite['name'], 30),
'ville' => truncate($entite['ville'], 20),
'cp' => $entite['code_postal'],
'users' => $entite['nb_users'],
'ops' => $entite['nb_operations'],
'stripe' => $entite['stripe'],
'match' => $entite['matched_in'],
];
}
table(
[
'line' => '#',
'id' => 'ID',
'name' => 'Nom',
'ville' => 'Ville',
'cp' => 'CP',
'users' => 'Users',
'ops' => 'Ops',
'stripe' => 'Stripe',
'match' => 'Trouvé dans',
],
$tableData,
true
);
success("Recherche terminée");
// Menu interactif
if (count($matchedEntites) > 0) {
echo "\n";
echo color("═══════════════════════════════════════\n", 'cyan');
echo color(" Actions disponibles\n", 'bold');
echo color("═══════════════════════════════════════\n", 'cyan');
echo " 1. Détail d'une entité\n";
echo " 2. Opérations d'une entité\n";
echo " 3. Membres d'une entité\n";
echo " 0. Quitter\n";
echo color("═══════════════════════════════════════\n", 'cyan');
echo color("\nVotre choix: ", 'yellow');
$handle = fopen('php://stdin', 'r');
$choice = trim(fgets($handle));
if ($choice === '0' || $choice === '') {
echo "\n";
info("Au revoir !");
exit(0);
}
// Demander le numéro de ligne (sauf si une seule trouvée)
$entiteId = null;
if (count($matchedEntites) === 1) {
$entiteId = $matchedEntites[0]['id'];
info("\nEntité sélectionnée: #$entiteId - " . $matchedEntites[0]['name']);
} else {
echo color("\nEntrez le n° de ligne (1-" . count($matchedEntites) . "): ", 'yellow');
$lineInput = trim(fgets($handle));
if (!is_numeric($lineInput) || (int)$lineInput < 1 || (int)$lineInput > count($matchedEntites)) {
fclose($handle);
error("Numéro de ligne invalide (doit être entre 1 et " . count($matchedEntites) . ")");
exit(1);
}
$lineNumber = (int)$lineInput;
$entiteId = $matchedEntites[$lineNumber - 1]['id'];
info("\nEntité sélectionnée: #$entiteId - " . $matchedEntites[$lineNumber - 1]['name']);
}
fclose($handle);
echo "\n";
// Exécuter l'action choisie
switch ($choice) {
case '1':
// Détail de l'entité
$decryptEntiteScript = __DIR__ . '/decrypt-entite';
passthru("$decryptEntiteScript $environment $entiteId");
break;
case '2':
// Opérations de l'entité
$listOperationsScript = __DIR__ . '/list-operations';
passthru("$listOperationsScript $environment --entite=$entiteId");
break;
case '3':
// Membres de l'entité
$listUsersScript = __DIR__ . '/list-users';
passthru("$listUsersScript $environment --entite=$entiteId");
break;
default:
error("Choix invalide: $choice");
exit(1);
}
}
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

254
bao/bin/search-user Executable file
View File

@@ -0,0 +1,254 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
/**
* Script de recherche d'utilisateurs par chaîne
* Recherche dans : username, nom, prénom, secteur
* Usage: ./search-user <environment> <search_string>
*/
require_once __DIR__ . '/../lib/CryptoService.php';
require_once __DIR__ . '/../lib/DatabaseConnection.php';
require_once __DIR__ . '/../lib/helpers.php';
// Vérifier les arguments
if ($argc < 3) {
error("Usage: " . basename($argv[0]) . " <environment> <search_string>");
error("Exemple: " . basename($argv[0]) . " dev dupont");
error(" " . basename($argv[0]) . " dev secteur_a");
error(" " . basename($argv[0]) . " dev j.dupont");
exit(1);
}
$environment = strtoupper($argv[1]);
$searchString = strtolower(trim($argv[2]));
if (strlen($searchString) < 2) {
error("La recherche doit contenir au moins 2 caractères");
exit(1);
}
try {
// Ouvrir le tunnel SSH si nécessaire
$tunnelScript = __DIR__ . '/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
exit(1);
}
// Connexion à la base de données
$db = new DatabaseConnection($environment);
$pdo = $db->connect();
info("Environnement: $environment");
info("Recherche de: '$searchString'");
info("Champs: username, nom, prénom, secteur\n");
// Récupérer les utilisateurs avec sect_name (en clair) et first_name (en clair)
// On peut filtrer directement sur ces deux champs
$stmt = $pdo->prepare("
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name,
u.sect_name,
u.fk_role,
u.fk_entite,
r.libelle as role_name,
e.encrypted_name as entite_encrypted_name
FROM users u
LEFT JOIN x_users_roles r ON u.fk_role = r.id
LEFT JOIN entites e ON u.fk_entite = e.id
WHERE LOWER(u.first_name) LIKE :search1
OR LOWER(u.sect_name) LIKE :search2
ORDER BY u.id
");
$searchPattern = '%' . $searchString . '%';
$stmt->execute([
'search1' => $searchPattern,
'search2' => $searchPattern
]);
$preFilteredUsers = $stmt->fetchAll();
// Récupérer TOUS les utilisateurs pour chercher dans les champs chiffrés
$stmtAll = $pdo->query("
SELECT
u.id,
u.encrypted_user_name,
u.encrypted_email,
u.encrypted_name,
u.first_name,
u.sect_name,
u.fk_role,
u.fk_entite,
r.libelle as role_name,
e.encrypted_name as entite_encrypted_name
FROM users u
LEFT JOIN x_users_roles r ON u.fk_role = r.id
LEFT JOIN entites e ON u.fk_entite = e.id
ORDER BY u.id
");
$allUsers = $stmtAll->fetchAll();
info("Analyse de " . count($allUsers) . " utilisateurs...\n");
// Déchiffrer et filtrer
$config = DatabaseConfig::getInstance();
$crypto = new CryptoService($config->getEncryptionKey());
$matchedUsers = [];
$seenIds = [];
// Ajouter d'abord les résultats pré-filtrés (sect_name, first_name)
foreach ($preFilteredUsers as $user) {
$username = $crypto->decryptSearchable($user['encrypted_user_name']);
$name = $crypto->decryptWithIV($user['encrypted_name']);
// Vérifier aussi dans les champs chiffrés
$matches = false;
$matchedFields = [];
if (stripos($user['first_name'], $searchString) !== false) {
$matches = true;
$matchedFields[] = 'prénom';
}
if (stripos($user['sect_name'], $searchString) !== false) {
$matches = true;
$matchedFields[] = 'secteur';
}
if ($username && stripos($username, $searchString) !== false) {
$matches = true;
$matchedFields[] = 'username';
}
if ($name && stripos($name, $searchString) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($matches) {
$matchedUsers[] = [
'id' => $user['id'],
'username' => $username ?? '-',
'prenom' => $user['first_name'] ?? '-',
'nom' => $name ?? '-',
'secteur' => $user['sect_name'] ?? '-',
'role' => $user['role_name'] ?? '-',
'entite' => $crypto->decryptWithIV($user['entite_encrypted_name']) ?? '-',
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$user['id']] = true;
}
}
// Chercher dans les utilisateurs restants (pour username et nom chiffrés)
foreach ($allUsers as $user) {
if (isset($seenIds[$user['id']])) {
continue; // Déjà trouvé
}
$username = $crypto->decryptSearchable($user['encrypted_user_name']);
$name = $crypto->decryptWithIV($user['encrypted_name']);
$matches = false;
$matchedFields = [];
if ($username && stripos($username, $searchString) !== false) {
$matches = true;
$matchedFields[] = 'username';
}
if ($name && stripos($name, $searchString) !== false) {
$matches = true;
$matchedFields[] = 'nom';
}
if ($matches) {
$matchedUsers[] = [
'id' => $user['id'],
'username' => $username ?? '-',
'prenom' => $user['first_name'] ?? '-',
'nom' => $name ?? '-',
'secteur' => $user['sect_name'] ?? '-',
'role' => $user['role_name'] ?? '-',
'entite' => $crypto->decryptWithIV($user['entite_encrypted_name']) ?? '-',
'matched_in' => implode(', ', $matchedFields),
];
$seenIds[$user['id']] = true;
}
}
if (empty($matchedUsers)) {
warning("\nAucun utilisateur trouvé avec: '$searchString'");
exit(0);
}
// Affichage
title("RÉSULTATS DE LA RECHERCHE - " . count($matchedUsers) . " utilisateur(s) trouvé(s)");
// Préparer les données pour le tableau
$tableData = [];
foreach ($matchedUsers as $user) {
$tableData[] = [
'id' => $user['id'],
'username' => truncate($user['username'], 20),
'prenom' => truncate($user['prenom'], 15),
'nom' => truncate($user['nom'], 20),
'secteur' => truncate($user['secteur'], 15),
'role' => truncate($user['role'], 12),
'match' => $user['matched_in'],
];
}
table(
[
'id' => 'ID',
'username' => 'Username',
'prenom' => 'Prénom',
'nom' => 'Nom',
'secteur' => 'Secteur',
'role' => 'Rôle',
'match' => 'Trouvé dans',
],
$tableData,
true
);
success("Recherche terminée");
// Proposer d'afficher les détails complets
if (count($matchedUsers) === 1) {
echo "\n";
if (confirm("Afficher les détails complets de cet utilisateur ?")) {
echo "\n";
$userId = $matchedUsers[0]['id'];
$decryptUserScript = __DIR__ . '/decrypt-user';
passthru("$decryptUserScript $environment $userId");
}
} elseif (count($matchedUsers) > 1 && count($matchedUsers) <= 10) {
echo "\n";
if (confirm("Afficher les détails d'un utilisateur spécifique ?")) {
echo color("\nEntrez l'ID de l'utilisateur: ", 'yellow');
$handle = fopen('php://stdin', 'r');
$userId = trim(fgets($handle));
fclose($handle);
if (is_numeric($userId) && (int)$userId > 0) {
echo "\n";
$decryptUserScript = __DIR__ . '/decrypt-user';
passthru("$decryptUserScript $environment $userId");
}
}
}
} catch (Exception $e) {
error("Erreur: " . $e->getMessage());
exit(1);
}

89
bao/config/database.php Normal file
View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* Configuration de la base de données pour BAO
* Charge les paramètres depuis le fichier .env
*/
class DatabaseConfig {
private static ?self $instance = null;
private array $config;
private function __construct() {
$this->loadEnv();
}
public static function getInstance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function loadEnv(): void {
$envPath = __DIR__ . '/.env';
if (!file_exists($envPath)) {
throw new RuntimeException("Fichier .env introuvable. Copiez .env.example vers .env et configurez-le.");
}
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
// Ignorer les commentaires
if (strpos(trim($line), '#') === 0) {
continue;
}
// Parser les variables
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
$key = trim($key);
$value = trim($value);
$this->config[$key] = $value;
}
}
}
public function get(string $key, $default = null) {
return $this->config[$key] ?? $default;
}
public function getEncryptionKey(): string {
$key = $this->get('ENCRYPTION_KEY');
if (empty($key)) {
throw new RuntimeException("ENCRYPTION_KEY manquante dans .env");
}
return $key;
}
public function getEnvironmentConfig(string $env): array {
$env = strtoupper($env);
$enabled = $this->get("{$env}_ENABLED", 'false');
if ($enabled !== 'true') {
throw new RuntimeException("Environnement {$env} désactivé dans .env");
}
return [
'use_vpn' => $this->get("{$env}_USE_VPN", 'false') === 'true',
'ssh_host' => $this->get("{$env}_SSH_HOST"),
'ssh_port_local' => (int)$this->get("{$env}_SSH_PORT_LOCAL"),
'host' => $this->get("{$env}_DB_HOST"),
'port' => (int)$this->get("{$env}_DB_PORT"),
'name' => $this->get("{$env}_DB_NAME"),
'user' => $this->get("{$env}_DB_USER"),
'pass' => $this->get("{$env}_DB_PASS"),
];
}
public function getAvailableEnvironments(): array {
$envs = [];
foreach (['DEV', 'REC', 'PROD'] as $env) {
if ($this->get("{$env}_ENABLED") === 'true') {
$envs[] = $env;
}
}
return $envs;
}
}

View File

View File

View File

View File

View File

474
bao/geo_app.sql Executable file
View File

@@ -0,0 +1,474 @@
-- -------------------------------------------------------------
-- TablePlus 6.4.8(608)
--
-- https://tableplus.com/
--
-- Database: geo_app
-- Generation Time: 2025-06-09 18:03:43.5140
-- -------------------------------------------------------------
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
-- Tables préfixées "chat_"
CREATE TABLE chat_rooms (
id VARCHAR(36) PRIMARY KEY,
title VARCHAR(255),
type ENUM('private', 'group', 'broadcast'),
created_at TIMESTAMP,
created_by INT
);
CREATE TABLE chat_messages (
id VARCHAR(36) PRIMARY KEY,
room_id VARCHAR(36),
content TEXT,
sender_id INT,
sent_at TIMESTAMP,
FOREIGN KEY (room_id) REFERENCES chat_rooms(id)
);
CREATE TABLE chat_participants (
room_id VARCHAR(36),
user_id INT,
role INT,
entite_id INT,
joined_at TIMESTAMP,
PRIMARY KEY (room_id, user_id)
);
CREATE TABLE chat_read_receipts (
message_id VARCHAR(36),
user_id INT,
read_at TIMESTAMP,
PRIMARY KEY (message_id, user_id)
);
CREATE TABLE `email_counter` (
`id` int(10) unsigned NOT NULL DEFAULT 1,
`hour_start` timestamp NULL DEFAULT NULL,
`count` int(10) unsigned DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `email_queue` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
`to_email` varchar(255) DEFAULT NULL,
`subject` varchar(255) DEFAULT NULL,
`body` text DEFAULT NULL,
`headers` text DEFAULT NULL,
`created_at` timestamp NULL DEFAULT current_timestamp(),
`status` enum('pending','sent','failed') DEFAULT 'pending',
`attempts` int(10) unsigned DEFAULT 0,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `entites` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`encrypted_name` varchar(255) DEFAULT NULL,
`adresse1` varchar(45) DEFAULT '',
`adresse2` varchar(45) DEFAULT '',
`code_postal` varchar(5) DEFAULT '',
`ville` varchar(45) DEFAULT '',
`fk_region` int(10) unsigned DEFAULT NULL,
`fk_type` int(10) unsigned DEFAULT 1,
`encrypted_phone` varchar(128) DEFAULT '',
`encrypted_mobile` varchar(128) DEFAULT '',
`encrypted_email` varchar(255) DEFAULT '',
`gps_lat` varchar(20) NOT NULL DEFAULT '',
`gps_lng` varchar(20) NOT NULL DEFAULT '',
`chk_stripe` tinyint(1) unsigned DEFAULT 0,
`encrypted_stripe_id` varchar(255) DEFAULT '',
`encrypted_iban` varchar(255) DEFAULT '',
`encrypted_bic` varchar(128) DEFAULT '',
`chk_demo` tinyint(1) unsigned DEFAULT 1,
`chk_mdp_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des mots de passe manuelle (1) ou automatique (0)',
`chk_username_manuel` tinyint(1) unsigned NOT NULL DEFAULT 0 COMMENT 'Gestion des usernames manuelle (1) ou automatique (0)',
`chk_copie_mail_recu` tinyint(1) unsigned NOT NULL DEFAULT 0,
`chk_accept_sms` tinyint(1) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
KEY `entites_ibfk_1` (`fk_region`),
KEY `entites_ibfk_2` (`fk_type`),
CONSTRAINT `entites_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE,
CONSTRAINT `entites_ibfk_2` FOREIGN KEY (`fk_type`) REFERENCES `x_entites_types` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1230 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `medias` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`support` varchar(45) NOT NULL DEFAULT '' COMMENT 'Type de support (entite, user, operation, passage)',
`support_id` int(10) unsigned NOT NULL DEFAULT 0 COMMENT 'ID de élément associé',
`fichier` varchar(250) NOT NULL DEFAULT '' COMMENT 'Nom du fichier stocké',
`file_type` varchar(50) DEFAULT NULL COMMENT 'Extension du fichier (pdf, jpg, xlsx, etc.)',
`file_category` varchar(50) DEFAULT NULL COMMENT 'export, logo, carte, etc.',
`file_size` int(10) unsigned DEFAULT NULL COMMENT 'Taille du fichier en octets',
`mime_type` varchar(100) DEFAULT NULL COMMENT 'Type MIME du fichier',
`original_name` varchar(255) DEFAULT NULL COMMENT 'Nom original du fichier uploadé',
`fk_entite` int(10) unsigned DEFAULT NULL COMMENT 'ID de entité propriétaire',
`fk_operation` int(10) unsigned DEFAULT NULL COMMENT 'ID de opération (pour passages)',
`file_path` varchar(500) DEFAULT NULL COMMENT 'Chemin complet du fichier',
`original_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur originale de image',
`original_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur originale de image',
`processed_width` int(10) unsigned DEFAULT NULL COMMENT 'Largeur après traitement',
`processed_height` int(10) unsigned DEFAULT NULL COMMENT 'Hauteur après traitement',
`is_processed` tinyint(1) unsigned DEFAULT 0 COMMENT 'Image redimensionnée (1) ou originale (0)',
`description` varchar(100) NOT NULL DEFAULT '' COMMENT 'Description du fichier',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp(),
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `idx_entite` (`fk_entite`),
KEY `idx_operation` (`fk_operation`),
KEY `idx_support_type` (`support`, `support_id`),
KEY `idx_file_type` (`file_type`),
KEY `idx_file_category` (`file_category`),
CONSTRAINT `fk_medias_entite` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE ON DELETE CASCADE,
CONSTRAINT `fk_medias_operation` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE `ope_pass` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
`fk_sector` int(10) unsigned DEFAULT 0,
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
`fk_adresse` varchar(25) DEFAULT '' COMMENT 'adresses.cp??.id',
`passed_at` timestamp NULL DEFAULT NULL COMMENT 'Date du passage',
`fk_type` int(10) unsigned DEFAULT 0,
`numero` varchar(10) NOT NULL DEFAULT '',
`rue` varchar(75) NOT NULL DEFAULT '',
`rue_bis` varchar(1) NOT NULL DEFAULT '',
`ville` varchar(75) NOT NULL DEFAULT '',
`fk_habitat` int(10) unsigned DEFAULT 1,
`appt` varchar(5) DEFAULT '',
`niveau` varchar(5) DEFAULT '',
`residence` varchar(75) DEFAULT '',
`gps_lat` varchar(20) NOT NULL DEFAULT '',
`gps_lng` varchar(20) NOT NULL DEFAULT '',
`encrypted_name` varchar(255) NOT NULL DEFAULT '',
`montant` decimal(7,2) NOT NULL DEFAULT 0.00,
`fk_type_reglement` int(10) unsigned DEFAULT 1,
`remarque` text DEFAULT '',
`encrypted_email` varchar(255) DEFAULT '',
`nom_recu` varchar(50) DEFAULT NULL,
`date_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de réception',
`date_creat_recu` timestamp NULL DEFAULT NULL COMMENT 'Date de création du reçu',
`date_sent_recu` timestamp NULL DEFAULT NULL COMMENT 'Date envoi du reçu',
`email_erreur` varchar(30) DEFAULT '',
`chk_email_sent` tinyint(1) unsigned NOT NULL DEFAULT 0,
`encrypted_phone` varchar(128) NOT NULL DEFAULT '',
`is_striped` tinyint(1) unsigned NOT NULL DEFAULT 0,
`docremis` tinyint(1) unsigned DEFAULT 0,
`date_repasser` timestamp NULL DEFAULT NULL COMMENT 'Date prévue pour repasser',
`nb_passages` int(11) DEFAULT 1 COMMENT 'Nb passages pour les a repasser',
`chk_gps_maj` tinyint(1) unsigned DEFAULT 0,
`chk_map_create` tinyint(1) unsigned DEFAULT 0,
`chk_mobile` tinyint(1) unsigned DEFAULT 0,
`chk_synchro` tinyint(1) unsigned DEFAULT 1 COMMENT 'chk synchro entre web et appli',
`chk_api_adresse` tinyint(1) unsigned DEFAULT 0,
`chk_maj_adresse` tinyint(1) unsigned DEFAULT 0,
`anomalie` tinyint(1) unsigned DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned DEFAULT NULL,
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `fk_operation` (`fk_operation`),
KEY `fk_sector` (`fk_sector`),
KEY `fk_user` (`fk_user`),
KEY `fk_type` (`fk_type`),
KEY `fk_type_reglement` (`fk_type_reglement`),
KEY `email` (`encrypted_email`),
CONSTRAINT `ope_pass_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_pass_ibfk_2` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_pass_ibfk_3` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_pass_ibfk_4` FOREIGN KEY (`fk_type_reglement`) REFERENCES `x_types_reglements` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=19499566 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `ope_pass_histo` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_pass` int(10) unsigned NOT NULL DEFAULT 0,
`date_histo` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date historique',
`sujet` varchar(50) DEFAULT NULL,
`remarque` varchar(250) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
KEY `ope_pass_histo_fk_pass_IDX` (`fk_pass`) USING BTREE,
KEY `ope_pass_histo_date_histo_IDX` (`date_histo`) USING BTREE,
CONSTRAINT `ope_pass_histo_ibfk_1` FOREIGN KEY (`fk_pass`) REFERENCES `ope_pass` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=6752 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `ope_sectors` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
`fk_old_sector` int(10) unsigned DEFAULT NULL,
`libelle` varchar(75) NOT NULL DEFAULT '',
`sector` text NOT NULL DEFAULT '',
`color` varchar(7) NOT NULL DEFAULT '#4B77BE',
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `fk_operation` (`fk_operation`),
CONSTRAINT `ope_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=27675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `ope_users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned DEFAULT NULL,
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `ope_users_ibfk_1` (`fk_operation`),
KEY `ope_users_ibfk_2` (`fk_user`),
CONSTRAINT `ope_users_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_users_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=199006 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `ope_users_sectors` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_operation` int(10) unsigned NOT NULL DEFAULT 0,
`fk_user` int(10) unsigned NOT NULL DEFAULT 0,
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`),
KEY `fk_operation` (`fk_operation`),
KEY `fk_user` (`fk_user`),
KEY `fk_sector` (`fk_sector`),
CONSTRAINT `ope_users_sectors_ibfk_1` FOREIGN KEY (`fk_operation`) REFERENCES `operations` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_users_sectors_ibfk_2` FOREIGN KEY (`fk_user`) REFERENCES `users` (`id`) ON UPDATE CASCADE,
CONSTRAINT `ope_users_sectors_ibfk_3` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=48082 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `operations` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_entite` int(10) unsigned NOT NULL DEFAULT 1,
`libelle` varchar(75) NOT NULL DEFAULT '',
`date_deb` date NOT NULL DEFAULT '0000-00-00',
`date_fin` date NOT NULL DEFAULT '0000-00-00',
`chk_distinct_sectors` tinyint(1) unsigned NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned NOT NULL DEFAULT 0,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned NOT NULL DEFAULT 0,
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (`id`),
KEY `fk_entite` (`fk_entite`),
KEY `date_deb` (`date_deb`),
CONSTRAINT `operations_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=3121 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `params` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(35) NOT NULL DEFAULT '',
`valeur` varchar(255) NOT NULL DEFAULT '',
`aide` varchar(150) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `sectors_adresses` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_adresse` varchar(25) DEFAULT NULL COMMENT 'adresses.cp??.id',
`osm_id` int(10) unsigned NOT NULL DEFAULT 0,
`fk_sector` int(10) unsigned NOT NULL DEFAULT 0,
`osm_name` varchar(50) NOT NULL DEFAULT '',
`numero` varchar(5) NOT NULL DEFAULT '',
`rue_bis` varchar(5) NOT NULL DEFAULT '',
`rue` varchar(60) NOT NULL DEFAULT '',
`cp` varchar(5) NOT NULL DEFAULT '',
`ville` varchar(60) NOT NULL DEFAULT '',
`gps_lat` varchar(20) NOT NULL DEFAULT '',
`gps_lng` varchar(20) NOT NULL DEFAULT '',
`osm_date_creat` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
PRIMARY KEY (`id`),
KEY `sectors_adresses_fk_sector_index` (`fk_sector`),
KEY `sectors_adresses_numero_index` (`numero`),
KEY `sectors_adresses_rue_index` (`rue`),
KEY `sectors_adresses_ville_index` (`ville`),
CONSTRAINT `sectors_adresses_ibfk_1` FOREIGN KEY (`fk_sector`) REFERENCES `ope_sectors` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=1562946 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `users` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_entite` int(10) unsigned DEFAULT 1,
`fk_role` int(10) unsigned DEFAULT 1,
`fk_titre` int(10) unsigned DEFAULT 1,
`encrypted_name` varchar(255) DEFAULT NULL,
`first_name` varchar(45) DEFAULT NULL,
`sect_name` varchar(60) DEFAULT '',
`encrypted_user_name` varchar(128) DEFAULT '',
`user_pass_hash` varchar(60) DEFAULT NULL,
`encrypted_phone` varchar(128) DEFAULT NULL,
`encrypted_mobile` varchar(128) DEFAULT NULL,
`encrypted_email` varchar(255) DEFAULT '',
`chk_alert_email` tinyint(1) unsigned DEFAULT 1,
`chk_suivi` tinyint(1) unsigned DEFAULT 0,
`date_naissance` date DEFAULT NULL,
`date_embauche` date DEFAULT NULL,
`created_at` timestamp NOT NULL DEFAULT current_timestamp() COMMENT 'Date de création',
`fk_user_creat` int(10) unsigned DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL ON UPDATE current_timestamp() COMMENT 'Date de modification',
`fk_user_modif` int(10) unsigned DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
KEY `fk_entite` (`fk_entite`),
KEY `username` (`encrypted_user_name`),
KEY `users_ibfk_2` (`fk_role`),
KEY `users_ibfk_3` (`fk_titre`),
CONSTRAINT `users_ibfk_1` FOREIGN KEY (`fk_entite`) REFERENCES `entites` (`id`) ON UPDATE CASCADE,
CONSTRAINT `users_ibfk_2` FOREIGN KEY (`fk_role`) REFERENCES `x_users_roles` (`id`) ON UPDATE CASCADE,
CONSTRAINT `users_ibfk_3` FOREIGN KEY (`fk_titre`) REFERENCES `x_users_titres` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=10027748 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_departements` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`code` varchar(3) DEFAULT NULL,
`fk_region` int(10) unsigned DEFAULT 1,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `x_departements_ibfk_1` (`fk_region`),
CONSTRAINT `x_departements_ibfk_1` FOREIGN KEY (`fk_region`) REFERENCES `x_regions` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=105 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_devises` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`code` varchar(3) DEFAULT NULL,
`symbole` varchar(6) DEFAULT NULL,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_entites_types` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_pays` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`code` varchar(3) DEFAULT NULL,
`fk_continent` int(10) unsigned DEFAULT NULL,
`fk_devise` int(10) unsigned DEFAULT 1,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `x_pays_ibfk_1` (`fk_devise`),
CONSTRAINT `x_pays_ibfk_1` FOREIGN KEY (`fk_devise`) REFERENCES `x_devises` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Table des pays avec leurs codes' `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_regions` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_pays` int(10) unsigned DEFAULT 1,
`libelle` varchar(45) DEFAULT NULL,
`libelle_long` varchar(45) DEFAULT NULL,
`table_osm` varchar(45) DEFAULT NULL,
`departements` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `x_regions_ibfk_1` (`fk_pays`),
CONSTRAINT `x_regions_ibfk_1` FOREIGN KEY (`fk_pays`) REFERENCES `x_pays` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_types_passages` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(10) DEFAULT NULL,
`color_button` varchar(15) DEFAULT NULL,
`color_mark` varchar(15) DEFAULT NULL,
`color_table` varchar(15) DEFAULT NULL,
`chk_active` tinyint(1) unsigned NOT NULL DEFAULT 1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_types_reglements` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_users_roles` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents rôles des utilisateurs' `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_users_titres` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`libelle` varchar(45) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Les différents titres des utilisateurs' `PAGE_COMPRESSED`='ON';
CREATE TABLE `x_villes` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`fk_departement` int(10) unsigned DEFAULT 1,
`libelle` varchar(65) DEFAULT NULL,
`code_postal` varchar(5) DEFAULT NULL,
`code_insee` varchar(5) DEFAULT NULL,
`chk_active` tinyint(1) unsigned DEFAULT 1,
PRIMARY KEY (`id`),
UNIQUE KEY `id_UNIQUE` (`id`),
KEY `x_villes_ibfk_1` (`fk_departement`),
CONSTRAINT `x_villes_ibfk_1` FOREIGN KEY (`fk_departement`) REFERENCES `x_departements` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB AUTO_INCREMENT=38950 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE TABLE `z_sessions` (
`sid` text NOT NULL,
`fk_user` int(11) NOT NULL,
`role` varchar(10) DEFAULT NULL,
`date_modified` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
`ip` varchar(50) NOT NULL,
`browser` varchar(150) NOT NULL,
`data` mediumtext DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci `PAGE_COMPRESSED`='ON';
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `chat_conversations_unread` AS select `r`.`id` AS `id`,`r`.`type` AS `type`,`r`.`title` AS `title`,`r`.`date_creation` AS `date_creation`,`r`.`fk_user` AS `fk_user`,`r`.`fk_entite` AS `fk_entite`,`r`.`statut` AS `statut`,`r`.`description` AS `description`,`r`.`reply_permission` AS `reply_permission`,`r`.`is_pinned` AS `is_pinned`,`r`.`expiry_date` AS `expiry_date`,`r`.`updated_at` AS `updated_at`,count(distinct `m`.`id`) AS `total_messages`,count(distinct `rm`.`id`) AS `read_messages`,count(distinct `m`.`id`) - count(distinct `rm`.`id`) AS `unread_messages`,(select `geo_app`.`chat_messages`.`date_sent` from `chat_messages` where `geo_app`.`chat_messages`.`fk_room` = `r`.`id` order by `geo_app`.`chat_messages`.`date_sent` desc limit 1) AS `last_message_date` from ((`chat_rooms` `r` left join `chat_messages` `m` on(`r`.`id` = `m`.`fk_room`)) left join `chat_read_messages` `rm` on(`m`.`id` = `rm`.`fk_message`)) group by `r`.`id`;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

188
bao/lib/CryptoService.php Normal file
View File

@@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
/**
* Service de chiffrement/déchiffrement AES-256-CBC
* Compatible avec le système de chiffrement de l'API Geosector
*/
class CryptoService {
private string $encryptionKey;
private string $cipher = 'AES-256-CBC';
public function __construct(string $encryptionKey) {
// Décoder la clé base64
$this->encryptionKey = base64_decode($encryptionKey);
if (strlen($this->encryptionKey) !== 32) {
throw new RuntimeException("La clé de chiffrement doit faire 32 bytes (256 bits)");
}
}
/**
* Déchiffre les données "searchable" (encrypted_user_name, encrypted_email)
* Format: base64 simple avec IV fixe
*/
public function decryptSearchable(?string $encryptedData): ?string {
if (empty($encryptedData)) {
return null;
}
$encrypted = base64_decode($encryptedData);
if ($encrypted === false) {
return null;
}
$iv = str_repeat("\0", 16); // IV fixe
$decrypted = openssl_decrypt($encrypted, $this->cipher, $this->encryptionKey, 0, $iv);
if ($decrypted === false) {
return null;
}
// Supprimer le caractère de contrôle ajouté
if (substr($decrypted, -1) === "\x01") {
return substr($decrypted, 0, -1);
}
return $decrypted;
}
/**
* Déchiffre les données avec IV aléatoire (encrypted_name, encrypted_phone, etc.)
* Format: base64(IV + encrypted)
*/
public function decryptWithIV(?string $encryptedData): ?string {
if (empty($encryptedData)) {
return null;
}
$data = base64_decode($encryptedData);
if ($data === false) {
return null;
}
$ivLength = openssl_cipher_iv_length($this->cipher);
if (strlen($data) <= $ivLength) {
return null;
}
$iv = substr($data, 0, $ivLength);
$encrypted = substr($data, $ivLength);
$decrypted = openssl_decrypt($encrypted, $this->cipher, $this->encryptionKey, 0, $iv);
return $decrypted !== false ? $decrypted : null;
}
/**
* Déchiffre une valeur chiffrée
*
* @param string|null $encryptedValue Valeur chiffrée (format base64:iv:data)
* @return string|null Valeur déchiffrée ou null
*/
public function decrypt(?string $encryptedValue): ?string {
if (empty($encryptedValue)) {
return null;
}
// Le format de l'API est : base64:iv:encrypted_data
$parts = explode(':', $encryptedValue);
if (count($parts) !== 3 || $parts[0] !== 'base64') {
// Format invalide, peut-être déjà en clair
return $encryptedValue;
}
$iv = base64_decode($parts[1]);
$encrypted = base64_decode($parts[2]);
if ($iv === false || $encrypted === false) {
throw new RuntimeException("Impossible de décoder les données chiffrées");
}
$decrypted = openssl_decrypt(
$encrypted,
$this->cipher,
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv
);
if ($decrypted === false) {
throw new RuntimeException("Échec du déchiffrement : " . openssl_error_string());
}
return $decrypted;
}
/**
* Chiffre une valeur
*
* @param string $value Valeur à chiffrer
* @return string Valeur chiffrée (format base64:iv:data)
*/
public function encrypt(string $value): string {
$ivLength = openssl_cipher_iv_length($this->cipher);
$iv = openssl_random_pseudo_bytes($ivLength);
$encrypted = openssl_encrypt(
$value,
$this->cipher,
$this->encryptionKey,
OPENSSL_RAW_DATA,
$iv
);
if ($encrypted === false) {
throw new RuntimeException("Échec du chiffrement : " . openssl_error_string());
}
return 'base64:' . base64_encode($iv) . ':' . base64_encode($encrypted);
}
/**
* Déchiffre plusieurs colonnes d'un tableau
*
* @param array $row Ligne de base de données
* @param array $encryptedColumns Liste des colonnes à déchiffrer (sans le préfixe encrypted_)
* @return array Tableau avec colonnes déchiffrées
*/
public function decryptRow(array $row, array $encryptedColumns): array {
$decrypted = $row;
foreach ($encryptedColumns as $column) {
$encryptedColumn = 'encrypted_' . $column;
if (isset($row[$encryptedColumn])) {
$decrypted[$column] = $this->decrypt($row[$encryptedColumn]);
}
}
return $decrypted;
}
/**
* Déchiffre les colonnes encrypted_* d'un utilisateur
*
* @param array $user Données utilisateur
* @return array Utilisateur avec données déchiffrées
*/
public function decryptUser(array $user): array {
$columns = ['user_name', 'email', 'name', 'phone', 'mobile'];
return $this->decryptRow($user, $columns);
}
/**
* Déchiffre les colonnes encrypted_* d'une entité
*
* @param array $entite Données entité
* @return array Entité avec données déchiffrées
*/
public function decryptEntite(array $entite): array {
$columns = ['name', 'email', 'phone', 'mobile', 'iban', 'bic'];
return $this->decryptRow($entite, $columns);
}
}

View File

@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
require_once __DIR__ . '/../config/database.php';
/**
* Gestion de la connexion à la base de données
*/
class DatabaseConnection {
private ?PDO $pdo = null;
private array $config;
private string $environment;
public function __construct(string $environment) {
$this->environment = strtoupper($environment);
$dbConfig = DatabaseConfig::getInstance();
$this->config = $dbConfig->getEnvironmentConfig($this->environment);
}
/**
* Établit la connexion PDO
*/
public function connect(): PDO {
if ($this->pdo !== null) {
return $this->pdo;
}
try {
$dsn = sprintf(
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
$this->config['host'],
$this->config['port'],
$this->config['name']
);
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci",
];
$this->pdo = new PDO(
$dsn,
$this->config['user'],
$this->config['pass'],
$options
);
return $this->pdo;
} catch (PDOException $e) {
throw new RuntimeException(
"Impossible de se connecter à la base {$this->environment}: " . $e->getMessage()
);
}
}
/**
* Retourne la connexion PDO active
*/
public function getPdo(): PDO {
if ($this->pdo === null) {
$this->connect();
}
return $this->pdo;
}
/**
* Ferme la connexion
*/
public function close(): void {
$this->pdo = null;
}
/**
* Retourne le nom de l'environnement
*/
public function getEnvironment(): string {
return $this->environment;
}
/**
* Retourne la configuration SSH pour le tunnel
*/
public function getSshConfig(): array {
return [
'host' => $this->config['ssh_host'],
'port_local' => $this->config['ssh_port_local'],
'port_remote' => 3306,
];
}
/**
* Vérifie si l'environnement utilise le VPN (pas besoin de tunnel SSH)
*/
public function usesVpn(): bool {
return $this->config['use_vpn'] ?? false;
}
/**
* Teste la connexion
*/
public function testConnection(): bool {
try {
$pdo = $this->connect();
$stmt = $pdo->query('SELECT 1');
return $stmt !== false;
} catch (Exception $e) {
return false;
}
}
}

226
bao/lib/helpers.php Normal file
View File

@@ -0,0 +1,226 @@
<?php
declare(strict_types=1);
/**
* Fonctions utilitaires pour BAO
*/
/**
* Affiche un message coloré dans le terminal
*/
function color(string $text, string $color = 'default'): string {
static $colors = [
'default' => "\033[0m",
'black' => "\033[0;30m",
'red' => "\033[0;31m",
'green' => "\033[0;32m",
'yellow' => "\033[0;33m",
'blue' => "\033[0;34m",
'magenta' => "\033[0;35m",
'cyan' => "\033[0;36m",
'white' => "\033[0;37m",
'bold' => "\033[1m",
'underline' => "\033[4m",
];
$config = DatabaseConfig::getInstance();
$colorsEnabled = $config->get('COLORS_ENABLED', 'true') === 'true';
if (!$colorsEnabled || !isset($colors[$color])) {
return $text;
}
return $colors[$color] . $text . $colors['default'];
}
/**
* Affiche un titre encadré
*/
function title(string $text): void {
$length = strlen($text);
$border = str_repeat('═', $length + 4);
echo color("\n{$border}\n", 'cyan');
echo color("{$text}\n", 'cyan');
echo color("{$border}\n\n", 'cyan');
}
/**
* Affiche un message de succès
*/
function success(string $message): void {
echo color("", 'green') . color($message, 'white') . "\n";
}
/**
* Affiche un message d'erreur
*/
function error(string $message): void {
echo color("", 'red') . color($message, 'white') . "\n";
}
/**
* Affiche un message d'avertissement
*/
function warning(string $message): void {
echo color("", 'yellow') . color($message, 'white') . "\n";
}
/**
* Affiche un message d'information
*/
function info(string $message): void {
echo color(" ", 'blue') . color($message, 'white') . "\n";
}
/**
* Affiche un label et sa valeur
*/
function display(string $label, ?string $value, bool $encrypted = false): void {
$labelColored = color($label . ':', 'yellow');
if ($value === null) {
$valueColored = color('(null)', 'magenta');
} elseif ($encrypted && strpos($value, 'base64:') === 0) {
$valueColored = color('[ENCRYPTED]', 'red');
} else {
$valueColored = color($value, 'white');
}
echo " {$labelColored} {$valueColored}\n";
}
/**
* Affiche une ligne de séparation
*/
function separator(int $length = 80): void {
echo color(str_repeat('─', $length), 'cyan') . "\n";
}
/**
* Demande une confirmation à l'utilisateur
*/
function confirm(string $question, bool $default = false): bool {
$suffix = $default ? '[O/n]' : '[o/N]';
echo color("{$question} {$suffix}: ", 'yellow');
$handle = fopen('php://stdin', 'r');
$line = trim(fgets($handle));
fclose($handle);
if (empty($line)) {
return $default;
}
return in_array(strtolower($line), ['o', 'oui', 'y', 'yes']);
}
/**
* Demande un choix parmi plusieurs options
*/
function choice(string $question, array $options, $default = null): string {
echo color("\n{$question}\n", 'yellow');
foreach ($options as $key => $label) {
$prefix = ($key === $default) ? color('*', 'green') : ' ';
echo " {$prefix} " . color((string)$key, 'cyan') . ") {$label}\n";
}
echo color("\nVotre choix: ", 'yellow');
$handle = fopen('php://stdin', 'r');
$line = trim(fgets($handle));
fclose($handle);
if (empty($line) && $default !== null) {
return (string)$default;
}
if (!isset($options[$line])) {
error("Choix invalide");
return choice($question, $options, $default);
}
return $line;
}
/**
* Affiche un tableau formaté
*/
function table(array $headers, array $rows, bool $showIndex = true): void {
if (empty($rows)) {
warning("Aucune donnée à afficher");
return;
}
// Calculer les largeurs de colonnes
$widths = [];
if ($showIndex) {
$widths['#'] = max(strlen((string)count($rows)), 1) + 1;
}
foreach ($headers as $key => $label) {
$widths[$key] = max(
strlen($label),
max(array_map(fn($row) => strlen((string)($row[$key] ?? '')), $rows))
) + 2;
}
// En-tête
separator();
if ($showIndex) {
echo color(str_pad('#', $widths['#']), 'bold');
}
foreach ($headers as $key => $label) {
echo color(str_pad($label, $widths[$key]), 'bold');
}
echo "\n";
separator();
// Lignes
foreach ($rows as $index => $row) {
if ($showIndex) {
echo color(str_pad((string)($index + 1), $widths['#']), 'cyan');
}
foreach ($headers as $key => $label) {
$value = $row[$key] ?? '';
echo str_pad((string)$value, $widths[$key]);
}
echo "\n";
}
separator();
}
/**
* Formate une date MySQL en français
*/
function formatDate(?string $date): string {
if (empty($date) || $date === '0000-00-00' || $date === '0000-00-00 00:00:00') {
return '-';
}
try {
$dt = new DateTime($date);
return $dt->format('d/m/Y H:i');
} catch (Exception $e) {
return $date;
}
}
/**
* Tronque une chaîne si elle est trop longue
*/
function truncate(string $text, int $length = 50): string {
if (strlen($text) <= $length) {
return $text;
}
return substr($text, 0, $length - 3) . '...';
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/**
* Initialisation de la connexion base de données
* Gère automatiquement les tunnels SSH ou connexion VPN directe
*/
require_once __DIR__ . '/../config/database.php';
require_once __DIR__ . '/DatabaseConnection.php';
require_once __DIR__ . '/helpers.php';
/**
* Initialise la connexion pour un environnement
* Ouvre le tunnel SSH si nécessaire (mode non-VPN)
*
* @param string $environment Environnement (DEV, REC, PROD)
* @return DatabaseConnection Connexion initialisée
* @throws RuntimeException Si la connexion échoue
*/
function initConnection(string $environment): DatabaseConnection {
$db = new DatabaseConnection($environment);
// Si on utilise le VPN, pas besoin de tunnel SSH
if ($db->usesVpn()) {
info("Mode VPN détecté - connexion directe à la base");
return $db;
}
// Mode tunnel SSH
info("Mode tunnel SSH - ouverture du tunnel...");
$tunnelScript = __DIR__ . '/../bin/_ssh-tunnel.sh';
exec("$tunnelScript open $environment 2>&1", $output, $exitCode);
if ($exitCode !== 0) {
error("Impossible d'ouvrir le tunnel SSH");
if (!empty($output)) {
foreach ($output as $line) {
error(" " . $line);
}
}
throw new RuntimeException("Échec de l'ouverture du tunnel SSH");
}
return $db;
}