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

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);
}