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

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