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:
188
bao/lib/CryptoService.php
Normal file
188
bao/lib/CryptoService.php
Normal 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);
|
||||
}
|
||||
}
|
||||
114
bao/lib/DatabaseConnection.php
Normal file
114
bao/lib/DatabaseConnection.php
Normal 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
226
bao/lib/helpers.php
Normal 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) . '...';
|
||||
}
|
||||
48
bao/lib/init-connection.php
Normal file
48
bao/lib/init-connection.php
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user