Ajout du dossier api avec la géolocalisation automatique des casernes de pompiers
This commit is contained in:
379
api/src/Config/AppConfig.php
Normal file
379
api/src/Config/AppConfig.php
Normal file
@@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Configuration de l'application Geosector
|
||||
*
|
||||
* Ce fichier contient la configuration de l'application Geosector pour les trois environnements :
|
||||
* - Production (app.geosector.fr)
|
||||
* - Recette (rapp.geosector.fr)
|
||||
* - Développement (dapp.geosector.fr)
|
||||
*
|
||||
* Il inclut les paramètres de base de données, les informations SMTP,
|
||||
* les clés de chiffrement et les configurations des services externes (Mapbox, Stripe, SMS OVH).
|
||||
*/
|
||||
class AppConfig {
|
||||
private static ?self $instance = null;
|
||||
private array $headers;
|
||||
private array $config;
|
||||
private string $currentHost;
|
||||
private string $clientIp;
|
||||
|
||||
private function __construct() {
|
||||
// Récupération du host directement depuis SERVER_NAME ou HTTP_HOST
|
||||
$this->currentHost = $_SERVER['SERVER_NAME'] ?? $_SERVER['HTTP_HOST'] ?? '';
|
||||
|
||||
// Récupérer les autres en-têtes pour une utilisation ultérieure si nécessaire
|
||||
$this->headers = getallheaders();
|
||||
|
||||
// Déterminer l'adresse IP du client
|
||||
$this->clientIp = $this->getClientIpAddress();
|
||||
|
||||
$this->initConfig();
|
||||
$this->validateApp();
|
||||
}
|
||||
|
||||
public static function getInstance(): self {
|
||||
if (self::$instance === null) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
private function initConfig(): void {
|
||||
// Configuration de base commune à tous les environnements
|
||||
$baseConfig = [
|
||||
'name' => 'geosector',
|
||||
'encryption_key' => 'Qga2M8Ov6tyx2fIQRWHQ1U6oMK/bAFdTL7A8VRtiDhk=',
|
||||
'smtp' => [
|
||||
'host' => 'barbotte.o2switch.net',
|
||||
'auth' => true,
|
||||
'user' => 'noreply@geosector.fr',
|
||||
'pass' => '@G83^[OMSo^Q',
|
||||
'secure' => 'ssl',
|
||||
'port' => 465,
|
||||
],
|
||||
'email' => [
|
||||
'from' => 'noreply@geosector.fr',
|
||||
'contact' => 'contact@geosector.fr',
|
||||
'hourly_limit' => 1500, // Limite de 1500 emails/heure comme mentionné dans le cahier des charges
|
||||
],
|
||||
'mapbox' => [
|
||||
'api_key' => '', // À remplir avec la clé API Mapbox
|
||||
],
|
||||
'stripe' => [
|
||||
'api_key' => '', // À remplir avec la clé API Stripe
|
||||
'webhook_secret' => '', // À remplir avec le secret du webhook Stripe
|
||||
],
|
||||
'sms' => [
|
||||
'provider' => 'ovh', // Comme mentionné dans le cahier des charges
|
||||
'api_key' => '', // À remplir avec la clé API SMS OVH
|
||||
'api_secret' => '', // À remplir avec le secret API SMS OVH
|
||||
],
|
||||
];
|
||||
|
||||
// Configuration PRODUCTION
|
||||
$this->config['app.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'production',
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_prod',
|
||||
'password' => 'QO:96-SrHJ6k7-df*?k{4W6m',
|
||||
],
|
||||
]);
|
||||
|
||||
// Configuration RECETTE
|
||||
$this->config['rapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'recette',
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_rec',
|
||||
'password' => 'QO:96df*?k-dS3KiO-{4W6m',
|
||||
],
|
||||
// Vous pouvez remplacer d'autres paramètres spécifiques à l'environnement de recette ici
|
||||
]);
|
||||
|
||||
// Configuration DÉVELOPPEMENT
|
||||
$this->config['dapp.geosector.fr'] = array_merge($baseConfig, [
|
||||
'env' => 'development',
|
||||
'database' => [
|
||||
'host' => 'localhost',
|
||||
'name' => 'geo_app',
|
||||
'username' => 'geo_app_user_dev',
|
||||
'password' => '34GOz-X5gJu-oH@Fa3$#Z',
|
||||
],
|
||||
// Vous pouvez activer des fonctionnalités de débogage en développement
|
||||
'debug' => true,
|
||||
// Configurez des endpoints de test pour Stripe, etc.
|
||||
'stripe' => [
|
||||
'api_key' => 'pk_test_...', // Clé de test Stripe
|
||||
'webhook_secret' => 'whsec_test_...', // Secret de test
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function validateApp(): void {
|
||||
// Si l'hôte est vide, utiliser une solution de secours (développement par défaut)
|
||||
if (empty($this->currentHost)) {
|
||||
// Journaliser cette situation anormale
|
||||
error_log("WARNING: No host detected, falling back to development environment");
|
||||
$this->currentHost = 'dapp.geosector.fr';
|
||||
}
|
||||
|
||||
// Si l'hôte n'existe pas dans la configuration, tenter une correction
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
// Essayer de faire correspondre avec l'un des hôtes connus
|
||||
$knownHosts = array_keys($this->config);
|
||||
foreach ($knownHosts as $host) {
|
||||
if (strpos($this->currentHost, str_replace(['app.', 'rapp.', 'dapp.'], '', $host)) !== false) {
|
||||
// Correspondance trouvée, utiliser cette configuration
|
||||
$this->currentHost = $host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si toujours pas de correspondance, utiliser l'environnement de développement par défaut
|
||||
if (!isset($this->config[$this->currentHost])) {
|
||||
error_log("WARNING: Unknown host '{$this->currentHost}', falling back to development environment");
|
||||
$this->currentHost = 'dapp.geosector.fr';
|
||||
}
|
||||
}
|
||||
|
||||
// Journaliser l'environnement détecté
|
||||
$environment = $this->config[$this->currentHost]['env'] ?? 'unknown';
|
||||
error_log("INFO: Environment detected: {$environment} (Host: {$this->currentHost}, IP: {$this->clientIp})");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le type de client (web, mobile, etc.)
|
||||
*
|
||||
* @return string Le type de client ou 'unknown' si non défini
|
||||
*/
|
||||
public function getClientType(): string {
|
||||
return $this->headers['X-Client-Type'] ?? $_SERVER['HTTP_X_CLIENT_TYPE'] ?? 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'identifiant de l'application basé sur l'hôte
|
||||
*
|
||||
* @return string L'identifiant de l'application (app.geosector.fr, rapp.geosector.fr, dapp.geosector.fr)
|
||||
*/
|
||||
public function getAppIdentifier(): string {
|
||||
return $this->currentHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'environnement actuel (production, recette, development)
|
||||
*
|
||||
* @return string L'environnement actuel
|
||||
*/
|
||||
public function getEnvironment(): string {
|
||||
return $this->getCurrentConfig()['env'] ?? 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'application est en mode développement
|
||||
*
|
||||
* @return bool True si l'application est en mode développement
|
||||
*/
|
||||
public function isDevelopment(): bool {
|
||||
return $this->getEnvironment() === 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'application est en mode recette
|
||||
*
|
||||
* @return bool True si l'application est en mode recette
|
||||
*/
|
||||
public function isRecette(): bool {
|
||||
return $this->getEnvironment() === 'recette';
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si l'application est en mode production
|
||||
*
|
||||
* @return bool True si l'application est en mode production
|
||||
*/
|
||||
public function isProduction(): bool {
|
||||
return $this->getEnvironment() === 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration complète de l'environnement actuel
|
||||
*
|
||||
* @return array Configuration de l'environnement
|
||||
*/
|
||||
public function getCurrentConfig(): array {
|
||||
return $this->config[$this->currentHost];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne le nom de l'application
|
||||
*
|
||||
* @return string Nom de l'application (geosector)
|
||||
*/
|
||||
public function getName(): string {
|
||||
return $this->getCurrentConfig()['name'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration de la base de données
|
||||
*
|
||||
* @return array Configuration de la base de données
|
||||
*/
|
||||
public function getDatabaseConfig(): array {
|
||||
return $this->getCurrentConfig()['database'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la clé de chiffrement
|
||||
*
|
||||
* @return string Clé de chiffrement
|
||||
*/
|
||||
public function getEncryptionKey(): string {
|
||||
return $this->getCurrentConfig()['encryption_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration SMTP
|
||||
*
|
||||
* @return array Configuration SMTP
|
||||
*/
|
||||
public function getSmtpConfig(): array {
|
||||
return $this->getCurrentConfig()['smtp'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration email
|
||||
*
|
||||
* @return array Configuration email
|
||||
*/
|
||||
public function getEmailConfig(): array {
|
||||
return $this->getCurrentConfig()['email'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration Mapbox
|
||||
*
|
||||
* @return array Configuration Mapbox
|
||||
*/
|
||||
public function getMapboxConfig(): array {
|
||||
return $this->getCurrentConfig()['mapbox'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration Stripe
|
||||
*
|
||||
* @return array Configuration Stripe
|
||||
*/
|
||||
public function getStripeConfig(): array {
|
||||
return $this->getCurrentConfig()['stripe'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration SMS
|
||||
*
|
||||
* @return array Configuration SMS
|
||||
*/
|
||||
public function getSmsConfig(): array {
|
||||
return $this->getCurrentConfig()['sms'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne si le mode debug est activé
|
||||
*
|
||||
* @return bool True si le mode debug est activé
|
||||
*/
|
||||
public function isDebugEnabled(): bool {
|
||||
return $this->getCurrentConfig()['debug'] ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la liste des origines autorisées (domaines)
|
||||
*
|
||||
* @return array Liste des origines autorisées
|
||||
*/
|
||||
public function getAllowedOrigins(): array {
|
||||
return array_keys($this->config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si le client est d'un type spécifique
|
||||
*
|
||||
* @param string $type Type de client à vérifier
|
||||
* @return bool True si le client est du type spécifié
|
||||
*/
|
||||
public function isClientType(string $type): bool {
|
||||
return $this->getClientType() === $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne la configuration complète pour l'utilisation externe
|
||||
*
|
||||
* @return array Configuration complète
|
||||
*/
|
||||
public function getFullConfig(): array {
|
||||
return [
|
||||
'environment' => $this->getEnvironment(),
|
||||
'database' => $this->getDatabaseConfig(),
|
||||
'api' => [
|
||||
'allowed_origins' => $this->getAllowedOrigins(),
|
||||
'current_site' => $this->getName(),
|
||||
],
|
||||
'debug' => $this->isDebugEnabled()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retourne l'adresse IP du client
|
||||
*
|
||||
* @return string L'adresse IP du client
|
||||
*/
|
||||
public function getClientIp(): string {
|
||||
return $this->clientIp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine l'adresse IP du client en tenant compte des proxys et load balancers
|
||||
*
|
||||
* @return string L'adresse IP du client
|
||||
*/
|
||||
private function getClientIpAddress(): string {
|
||||
// Vérifier les en-têtes courants pour l'IP client
|
||||
$ipSources = [
|
||||
'HTTP_X_REAL_IP', // Nginx proxy
|
||||
'HTTP_CLIENT_IP', // Proxy partagé
|
||||
'HTTP_X_FORWARDED_FOR', // Proxy ou load balancer courant
|
||||
'HTTP_X_FORWARDED', // Proxy générique
|
||||
'HTTP_X_CLUSTER_CLIENT_IP', // Reverse proxy
|
||||
'HTTP_FORWARDED_FOR', // Proxies précédents
|
||||
'HTTP_FORWARDED', // Format standardisé (RFC 7239)
|
||||
'REMOTE_ADDR', // Fallback direct
|
||||
];
|
||||
|
||||
foreach ($ipSources as $source) {
|
||||
if (!empty($_SERVER[$source])) {
|
||||
// Pour des en-têtes comme X-Forwarded-For qui peuvent contenir plusieurs IPs séparées par des virgules
|
||||
// (format: "client, proxy1, proxy2")
|
||||
if ($source === 'HTTP_X_FORWARDED_FOR' || $source === 'HTTP_FORWARDED_FOR') {
|
||||
$ips = explode(',', $_SERVER[$source]);
|
||||
$clientIp = trim($ips[0]); // Prendre la première adresse (client original)
|
||||
} else {
|
||||
$clientIp = $_SERVER[$source];
|
||||
}
|
||||
|
||||
// Valider l'IP pour éviter les injections
|
||||
$filteredIp = filter_var($clientIp, FILTER_VALIDATE_IP);
|
||||
if ($filteredIp !== false) {
|
||||
return $filteredIp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune adresse IP valide n'est trouvée, retourner une valeur par défaut
|
||||
return '0.0.0.0';
|
||||
}
|
||||
}
|
||||
632
api/src/Controllers/EntiteController.php
Normal file
632
api/src/Controllers/EntiteController.php
Normal file
@@ -0,0 +1,632 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use Exception;
|
||||
|
||||
class EntiteController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle entité (amicale) si elle n'existe pas déjà avec le code postal spécifié
|
||||
*
|
||||
* @param string $name Nom de l'amicale
|
||||
* @param string $postalCode Code postal
|
||||
* @param string $cityName Nom de la ville
|
||||
* @return array|false Tableau contenant l'ID de l'entité créée ou false en cas d'erreur
|
||||
* @throws Exception Si une entité existe déjà avec ce code postal
|
||||
*/
|
||||
public function createEntite(string $name, string $postalCode, string $cityName): array|false {
|
||||
try {
|
||||
// Vérification que le code postal n'existe pas déjà
|
||||
$stmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?');
|
||||
$stmt->execute([$postalCode]);
|
||||
|
||||
if ($stmt->fetch()) {
|
||||
throw new Exception('Une amicale existe déjà sur ce code postal');
|
||||
}
|
||||
|
||||
// Chiffrement du nom
|
||||
$encryptedName = ApiService::encryptData($name);
|
||||
|
||||
// Insertion de la nouvelle entité
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO entites (
|
||||
encrypted_name,
|
||||
code_postal,
|
||||
ville,
|
||||
fk_type,
|
||||
created_at,
|
||||
chk_active
|
||||
) VALUES (?, ?, ?, 1, NOW(), 1)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$encryptedName,
|
||||
$postalCode,
|
||||
$cityName
|
||||
]);
|
||||
|
||||
$entiteId = $this->db->lastInsertId();
|
||||
|
||||
if (!$entiteId) {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la création de l\'entité GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode,
|
||||
'cityName' => $cityName
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une entité par son ID
|
||||
*
|
||||
* @param int $id ID de l'entité
|
||||
* @return array|false Données de l'entité ou false si non trouvée
|
||||
*/
|
||||
public function getEntiteById(int $id): array|false {
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
||||
FROM entites
|
||||
WHERE id = ? AND chk_active = 1
|
||||
');
|
||||
|
||||
$stmt->execute([$id]);
|
||||
$entite = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$entite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Déchiffrement du nom
|
||||
$entite['name'] = ApiService::decryptData($entite['encrypted_name']);
|
||||
unset($entite['encrypted_name']);
|
||||
|
||||
return $entite;
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération de l\'entité GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'id' => $id
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère une entité par son code postal
|
||||
*
|
||||
* @param string $postalCode Code postal
|
||||
* @return array|false Données de l'entité ou false si non trouvée
|
||||
*/
|
||||
public function getEntiteByPostalCode(string $postalCode): array|false {
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
||||
FROM entites
|
||||
WHERE code_postal = ? AND chk_active = 1
|
||||
');
|
||||
|
||||
$stmt->execute([$postalCode]);
|
||||
$entite = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$entite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Déchiffrement du nom
|
||||
$entite['name'] = ApiService::decryptData($entite['encrypted_name']);
|
||||
unset($entite['encrypted_name']);
|
||||
|
||||
return $entite;
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération de l\'entité GeoSector par code postal', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une entité existe avec le code postal spécifié, et en crée une nouvelle si nécessaire
|
||||
*
|
||||
* @param string $name Nom de l'amicale
|
||||
* @param string $postalCode Code postal
|
||||
* @return int ID de l'entité créée ou existante
|
||||
* @throws Exception Si une entité existe déjà avec ce code postal
|
||||
*/
|
||||
public function getOrCreateEntiteByPostalCode(string $name, string $postalCode): int {
|
||||
try {
|
||||
// Vérification que le code postal n'existe pas déjà
|
||||
$stmt = $this->db->prepare('SELECT COUNT(*) as count FROM entites WHERE code_postal = ?');
|
||||
$stmt->execute([$postalCode]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result && $result['count'] > 0) {
|
||||
throw new Exception('Une amicale est déjà inscrite à ce code postal');
|
||||
}
|
||||
|
||||
// Chiffrement du nom
|
||||
$encryptedName = ApiService::encryptData($name);
|
||||
|
||||
// Insertion de la nouvelle entité
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO entites (
|
||||
encrypted_name,
|
||||
code_postal,
|
||||
ville,
|
||||
fk_type,
|
||||
created_at,
|
||||
chk_active
|
||||
) VALUES (?, ?, "", 1, NOW(), 1)
|
||||
');
|
||||
|
||||
$stmt->execute([
|
||||
$encryptedName,
|
||||
$postalCode
|
||||
]);
|
||||
|
||||
$entiteId = (int)$this->db->lastInsertId();
|
||||
|
||||
if (!$entiteId) {
|
||||
throw new Exception('Erreur lors de la création de l\'entité');
|
||||
}
|
||||
|
||||
LogService::log('Création d\'une nouvelle entité GeoSector via getOrCreateEntiteByPostalCode', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
return $entiteId;
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la vérification/création de l\'entité GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'name' => $name,
|
||||
'postalCode' => $postalCode
|
||||
]);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère toutes les entités actives
|
||||
*
|
||||
* @return array Liste des entités
|
||||
*/
|
||||
public function getEntites(): void {
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, encrypted_name, code_postal, ville, fk_type, chk_active
|
||||
FROM entites
|
||||
WHERE chk_active = 1
|
||||
ORDER BY code_postal ASC
|
||||
');
|
||||
|
||||
$stmt->execute();
|
||||
$entites = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
$result = [];
|
||||
foreach ($entites as $entite) {
|
||||
// Déchiffrement du nom pour chaque entité
|
||||
$entite['name'] = ApiService::decryptData($entite['encrypted_name']);
|
||||
unset($entite['encrypted_name']);
|
||||
$result[] = $entite;
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'entites' => $result
|
||||
], 200);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la récupération des entités GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la récupération des entités'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche les coordonnées GPS d'une caserne de pompiers à partir d'une adresse
|
||||
*
|
||||
* @param string $address Adresse complète (adresse + code postal + ville)
|
||||
* @param string $postalCode Code postal
|
||||
* @param string $city Ville
|
||||
* @return array|null Tableau contenant les coordonnées GPS [lat, lng] ou null si non trouvé
|
||||
*/
|
||||
private function findFireStationCoordinates(string $address, string $postalCode, string $city): ?array {
|
||||
try {
|
||||
// Construire l'adresse complète
|
||||
$fullAddress = urlencode($address . ' ' . $postalCode . ' ' . $city);
|
||||
|
||||
// Mots-clés pour rechercher une caserne de pompiers
|
||||
$keywords = ['pompiers', 'sapeurs-pompiers', 'sdis', 'caserne', 'centre de secours'];
|
||||
|
||||
foreach ($keywords as $keyword) {
|
||||
// Construire l'URL de recherche
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=" . urlencode($keyword) . "&postcode=$postalCode&limit=1";
|
||||
|
||||
// Effectuer la requête
|
||||
$response = file_get_contents($searchUrl);
|
||||
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
|
||||
// Vérifier si des résultats ont été trouvés
|
||||
if (isset($data['features']) && count($data['features']) > 0) {
|
||||
$feature = $data['features'][0];
|
||||
|
||||
// Vérifier si les coordonnées sont disponibles
|
||||
if (isset($feature['geometry']['coordinates'])) {
|
||||
$coordinates = $feature['geometry']['coordinates'];
|
||||
|
||||
// Les coordonnées sont au format [longitude, latitude]
|
||||
return [
|
||||
'lat' => $coordinates[1],
|
||||
'lng' => $coordinates[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucune caserne n'a été trouvée, essayer avec l'adresse de la mairie
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=mairie&postcode=$postalCode&limit=1";
|
||||
$response = file_get_contents($searchUrl);
|
||||
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (isset($data['features']) && count($data['features']) > 0) {
|
||||
$feature = $data['features'][0];
|
||||
|
||||
if (isset($feature['geometry']['coordinates'])) {
|
||||
$coordinates = $feature['geometry']['coordinates'];
|
||||
|
||||
return [
|
||||
'lat' => $coordinates[1],
|
||||
'lng' => $coordinates[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Si toujours rien, essayer avec l'adresse complète
|
||||
$searchUrl = "https://api-adresse.data.gouv.fr/search/?q=$fullAddress&limit=1";
|
||||
$response = file_get_contents($searchUrl);
|
||||
|
||||
if ($response) {
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if (isset($data['features']) && count($data['features']) > 0) {
|
||||
$feature = $data['features'][0];
|
||||
|
||||
if (isset($feature['geometry']['coordinates'])) {
|
||||
$coordinates = $feature['geometry']['coordinates'];
|
||||
|
||||
return [
|
||||
'lat' => $coordinates[1],
|
||||
'lng' => $coordinates[0]
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Aucune coordonnée trouvée
|
||||
return null;
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la recherche des coordonnées GPS', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'address' => $address,
|
||||
'postalCode' => $postalCode,
|
||||
'city' => $city
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour une entité existante avec les données fournies
|
||||
* Seuls les administrateurs (rôle > 2) peuvent modifier certains champs
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function updateEntite(): void {
|
||||
try {
|
||||
// Vérifier l'authentification et les droits d'accès
|
||||
$userId = Session::getUserId();
|
||||
if (!$userId) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous devez être connecté pour effectuer cette action'
|
||||
], 401);
|
||||
return;
|
||||
}
|
||||
|
||||
// Récupérer le rôle de l'utilisateur
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$userRole = (int)$user['fk_role'];
|
||||
$isAdmin = $userRole > 2;
|
||||
|
||||
// Récupérer les données de la requête
|
||||
$data = Request::getJson();
|
||||
|
||||
if (!isset($data['id']) || empty($data['id'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'ID de l\'entité requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$entiteId = (int)$data['id'];
|
||||
|
||||
// Récupérer les données actuelles de l'entité pour vérifier si l'adresse a changé
|
||||
$stmt = $this->db->prepare('SELECT adresse1, adresse2, code_postal, ville FROM entites WHERE id = ?');
|
||||
$stmt->execute([$entiteId]);
|
||||
$currentEntite = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$currentEntite) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Entité non trouvée'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Vérifier si l'adresse a changé
|
||||
$addressChanged = false;
|
||||
$newAdresse1 = $data['adresse1'] ?? $currentEntite['adresse1'];
|
||||
$newAdresse2 = $data['adresse2'] ?? $currentEntite['adresse2'];
|
||||
$newCodePostal = $data['code_postal'] ?? $currentEntite['code_postal'];
|
||||
$newVille = $data['ville'] ?? $currentEntite['ville'];
|
||||
|
||||
// Vérifier si l'adresse a changé
|
||||
if (
|
||||
$newAdresse1 !== $currentEntite['adresse1'] ||
|
||||
$newAdresse2 !== $currentEntite['adresse2'] ||
|
||||
$newCodePostal !== $currentEntite['code_postal'] ||
|
||||
$newVille !== $currentEntite['ville']
|
||||
) {
|
||||
$addressChanged = true;
|
||||
}
|
||||
|
||||
// Si l'adresse a changé, recalculer les coordonnées GPS
|
||||
if ($addressChanged) {
|
||||
// Construire l'adresse complète
|
||||
$fullAddress = $newAdresse1;
|
||||
if (!empty($newAdresse2)) {
|
||||
$fullAddress .= ' ' . $newAdresse2;
|
||||
}
|
||||
|
||||
// Rechercher les coordonnées GPS de la caserne de pompiers
|
||||
$coordinates = $this->findFireStationCoordinates($fullAddress, $newCodePostal, $newVille);
|
||||
|
||||
// Si des coordonnées ont été trouvées, les ajouter aux champs à mettre à jour
|
||||
if ($coordinates) {
|
||||
$data['gps_lat'] = $coordinates['lat'];
|
||||
$data['gps_lng'] = $coordinates['lng'];
|
||||
|
||||
LogService::log('Coordonnées GPS mises à jour suite à un changement d\'adresse', [
|
||||
'level' => 'info',
|
||||
'entiteId' => $entiteId,
|
||||
'lat' => $coordinates['lat'],
|
||||
'lng' => $coordinates['lng']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
// Préparer les champs à mettre à jour
|
||||
$updateFields = [];
|
||||
$params = [];
|
||||
|
||||
// Champs modifiables par tous les utilisateurs
|
||||
if (isset($data['name']) && !empty($data['name'])) {
|
||||
$updateFields[] = 'encrypted_name = ?';
|
||||
$params[] = ApiService::encryptData($data['name']);
|
||||
}
|
||||
|
||||
if (isset($data['adresse1'])) {
|
||||
$updateFields[] = 'adresse1 = ?';
|
||||
$params[] = $data['adresse1'];
|
||||
}
|
||||
|
||||
if (isset($data['adresse2'])) {
|
||||
$updateFields[] = 'adresse2 = ?';
|
||||
$params[] = $data['adresse2'];
|
||||
}
|
||||
|
||||
if (isset($data['code_postal']) && !empty($data['code_postal'])) {
|
||||
$updateFields[] = 'code_postal = ?';
|
||||
$params[] = $data['code_postal'];
|
||||
}
|
||||
|
||||
if (isset($data['ville'])) {
|
||||
$updateFields[] = 'ville = ?';
|
||||
$params[] = $data['ville'];
|
||||
}
|
||||
|
||||
if (isset($data['fk_region'])) {
|
||||
$updateFields[] = 'fk_region = ?';
|
||||
$params[] = $data['fk_region'];
|
||||
}
|
||||
|
||||
if (isset($data['phone'])) {
|
||||
$updateFields[] = 'encrypted_phone = ?';
|
||||
$params[] = ApiService::encryptData($data['phone']);
|
||||
}
|
||||
|
||||
if (isset($data['mobile'])) {
|
||||
$updateFields[] = 'encrypted_mobile = ?';
|
||||
$params[] = ApiService::encryptData($data['mobile']);
|
||||
}
|
||||
|
||||
if (isset($data['email']) && !empty($data['email'])) {
|
||||
$updateFields[] = 'encrypted_email = ?';
|
||||
$params[] = ApiService::encryptSearchableData($data['email']);
|
||||
}
|
||||
|
||||
if (isset($data['chk_copie_mail_recu'])) {
|
||||
$updateFields[] = 'chk_copie_mail_recu = ?';
|
||||
$params[] = $data['chk_copie_mail_recu'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_accept_sms'])) {
|
||||
$updateFields[] = 'chk_accept_sms = ?';
|
||||
$params[] = $data['chk_accept_sms'] ? 1 : 0;
|
||||
}
|
||||
|
||||
// Champs modifiables uniquement par les administrateurs
|
||||
if ($isAdmin) {
|
||||
if (isset($data['gps_lat'])) {
|
||||
$updateFields[] = 'gps_lat = ?';
|
||||
$params[] = $data['gps_lat'];
|
||||
}
|
||||
|
||||
if (isset($data['gps_lng'])) {
|
||||
$updateFields[] = 'gps_lng = ?';
|
||||
$params[] = $data['gps_lng'];
|
||||
}
|
||||
|
||||
if (isset($data['stripe_id'])) {
|
||||
$updateFields[] = 'encrypted_stripe_id = ?';
|
||||
$params[] = ApiService::encryptData($data['stripe_id']);
|
||||
}
|
||||
|
||||
if (isset($data['chk_demo'])) {
|
||||
$updateFields[] = 'chk_demo = ?';
|
||||
$params[] = $data['chk_demo'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_active'])) {
|
||||
$updateFields[] = 'chk_active = ?';
|
||||
$params[] = $data['chk_active'] ? 1 : 0;
|
||||
}
|
||||
|
||||
if (isset($data['chk_stripe'])) {
|
||||
$updateFields[] = 'chk_stripe = ?';
|
||||
$params[] = $data['chk_stripe'] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Si aucun champ à mettre à jour, retourner une erreur
|
||||
if (empty($updateFields)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Aucune donnée à mettre à jour'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ajouter la date de mise à jour
|
||||
$updateFields[] = 'updated_at = NOW()';
|
||||
|
||||
// Construire la requête SQL
|
||||
$sql = 'UPDATE entites SET ' . implode(', ', $updateFields) . ' WHERE id = ?';
|
||||
$params[] = $entiteId;
|
||||
|
||||
// Exécuter la requête
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
// Vérifier si la mise à jour a réussi
|
||||
if ($stmt->rowCount() === 0) {
|
||||
Response::json([
|
||||
'status' => 'warning',
|
||||
'message' => 'Aucune modification effectuée'
|
||||
], 200);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Mise à jour d\'une entité GeoSector', [
|
||||
'level' => 'info',
|
||||
'userId' => $userId,
|
||||
'entiteId' => $entiteId,
|
||||
'isAdmin' => $isAdmin
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Entité mise à jour avec succès'
|
||||
], 200);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la mise à jour de l\'entité GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur lors de la mise à jour de l\'entité'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
161
api/src/Controllers/LogController.php
Normal file
161
api/src/Controllers/LogController.php
Normal file
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
class LogController {
|
||||
private const REQUIRED_FIELDS = [
|
||||
'metadata' => [
|
||||
'side',
|
||||
'version',
|
||||
'level',
|
||||
'timestamp',
|
||||
'environment',
|
||||
'client' => [
|
||||
'ip',
|
||||
'browser' => ['name', 'version'],
|
||||
'os' => ['name', 'version'],
|
||||
'screenResolution',
|
||||
'userAgent'
|
||||
]
|
||||
],
|
||||
'message'
|
||||
];
|
||||
|
||||
public function index(): void {
|
||||
try {
|
||||
// Récupérer la configuration de l'application
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$appName = $appConfig->getName();
|
||||
$clientType = ClientDetector::getClientType();
|
||||
|
||||
// Récupérer et décoder le JSON
|
||||
$data = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
// Vérifier si le JSON est valide
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception('Invalid JSON format');
|
||||
}
|
||||
|
||||
// Valider la structure des données
|
||||
$this->validateData($data);
|
||||
|
||||
// Ajouter le type de client aux métadonnées
|
||||
$data['metadata']['client_type'] = $clientType;
|
||||
|
||||
// Si c'est une app mobile, ajouter l'identifiant de l'app
|
||||
if ($clientType === 'mobile') {
|
||||
$data['metadata']['app_identifier'] = ClientDetector::getAppIdentifier();
|
||||
}
|
||||
|
||||
|
||||
// Définir le chemin du dossier logs à la racine du projet
|
||||
$logDir = __DIR__ . '/../../logs';
|
||||
|
||||
// Créer le dossier logs s'il n'existe pas
|
||||
if (!is_dir($logDir)) {
|
||||
if (!mkdir($logDir, 0777, true)) {
|
||||
throw new \Exception("Impossible de créer le dossier de logs: {$logDir}");
|
||||
}
|
||||
// S'assurer que les permissions sont correctes
|
||||
chmod($logDir, 0777);
|
||||
}
|
||||
|
||||
// Vérifier si le dossier est accessible en écriture
|
||||
if (!is_writable($logDir)) {
|
||||
throw new \Exception("Le dossier de logs n'est pas accessible en écriture: {$logDir}");
|
||||
}
|
||||
|
||||
// Récupérer l'environnement défini dans la configuration
|
||||
$environment = $appConfig->getEnvironment();
|
||||
|
||||
// Créer le nom du fichier basé sur l'environnement et la date
|
||||
// Format: geosector-production-2025-03-28.log, geosector-recette-2025-03-28.log, geosector-development-2025-03-28.log
|
||||
$filename = $logDir . '/geosector-' . $environment . '-' . date('Y-m-d') . '.log';
|
||||
|
||||
// Formater la ligne de log au format plat demandé
|
||||
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
|
||||
$timestamp = date('Y-m-d\TH:i:s');
|
||||
$browserInfo = $data['metadata']['client']['browser']['name'] . '@' . $data['metadata']['client']['browser']['version'];
|
||||
$osInfo = $data['metadata']['client']['os']['name'] . '@' . $data['metadata']['client']['os']['version'];
|
||||
$clientType = $data['metadata']['client_type'];
|
||||
|
||||
// Extraire le niveau de log
|
||||
$level = isset($data['metadata']['level']) ? (is_array($data['metadata']['level']) ? 'info' : $data['metadata']['level']) : 'info';
|
||||
|
||||
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format et les informations client)
|
||||
$additionalMetadata = [];
|
||||
foreach ($data['metadata'] as $key => $value) {
|
||||
// Exclure les informations client, type client, side, version, level et environment
|
||||
if (!in_array($key, ['client', 'client_type', 'side', 'version', 'level', 'environment'])) {
|
||||
if (is_array($value)) {
|
||||
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$additionalMetadata[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Joindre les métadonnées supplémentaires avec des virgules
|
||||
$metadataStr = !empty($additionalMetadata) ? implode(',', array_map(function($k, $v) {
|
||||
return $k . '=' . $v;
|
||||
}, array_keys($additionalMetadata), $additionalMetadata)) : '-';
|
||||
|
||||
// Construire la ligne de log au format demandé
|
||||
$logLine = implode(';', [
|
||||
$timestamp,
|
||||
$browserInfo,
|
||||
$osInfo,
|
||||
$clientType,
|
||||
$level,
|
||||
$metadataStr,
|
||||
$data['message']
|
||||
]) . "\n";
|
||||
|
||||
// Écrire dans le fichier avec vérification complète
|
||||
if (file_put_contents($filename, $logLine, FILE_APPEND) === false) {
|
||||
throw new \Exception("Impossible d'écrire dans le fichier de logs: {$filename}");
|
||||
}
|
||||
|
||||
// Retourner 204 No Content en cas de succès
|
||||
http_response_code(204);
|
||||
} catch (\Exception $e) {
|
||||
Response::json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
private function validateData($data): void {
|
||||
if (!isset($data['metadata']) || !isset($data['message'])) {
|
||||
throw new \Exception('Missing required root fields');
|
||||
}
|
||||
|
||||
// Valider la structure metadata
|
||||
foreach (self::REQUIRED_FIELDS['metadata'] as $key => $value) {
|
||||
if (is_array($value)) {
|
||||
if (!isset($data['metadata'][$key])) {
|
||||
throw new \Exception("Missing metadata field: {$key}");
|
||||
}
|
||||
foreach ($value as $subKey => $subValue) {
|
||||
if (is_array($subValue)) {
|
||||
foreach ($subValue as $field) {
|
||||
if (!isset($data['metadata'][$key][$subKey][$field])) {
|
||||
throw new \Exception("Missing metadata field: {$key}.{$subKey}.{$field}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isset($data['metadata'][$key][$subValue])) {
|
||||
throw new \Exception("Missing metadata field: {$key}.{$subValue}");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!isset($data['metadata'][$value])) {
|
||||
throw new \Exception("Missing metadata field: {$value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1276
api/src/Controllers/LoginController.php
Normal file
1276
api/src/Controllers/LoginController.php
Normal file
File diff suppressed because it is too large
Load Diff
605
api/src/Controllers/UserController.php
Normal file
605
api/src/Controllers/UserController.php
Normal file
@@ -0,0 +1,605 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
|
||||
class UserController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public function getUsers(): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur)
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$userId = Session::getUserId();
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$userId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
|
||||
if ($userRole != '1' && $userRole != '2') { // Supposons que 1 et 2 sont des rôles admin
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
u.id,
|
||||
u.encrypt_email,
|
||||
u.encrypted_name,
|
||||
u.first_name,
|
||||
u.fk_role as role,
|
||||
u.fk_entite,
|
||||
u.chk_active,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
e.encrypted_name as entite_name
|
||||
FROM users u
|
||||
LEFT JOIN entites e ON u.fk_entite = e.id
|
||||
ORDER BY u.created_at DESC
|
||||
');
|
||||
$stmt->execute();
|
||||
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Déchiffrement des données sensibles pour chaque utilisateur
|
||||
foreach ($users as &$user) {
|
||||
$user['email'] = ApiService::decryptSearchableData($user['encrypt_email']);
|
||||
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
||||
|
||||
if (!empty($user['entite_name'])) {
|
||||
$user['entite_name'] = ApiService::decryptData($user['entite_name']);
|
||||
}
|
||||
|
||||
// Suppression des champs chiffrés
|
||||
unset($user['encrypt_email']);
|
||||
unset($user['encrypted_name']);
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'users' => $users
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la récupération des utilisateurs GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUserById(string $id): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur ou utilisateur lui-même)
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$currentUserId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
|
||||
if ($userRole != '1' && $userRole != '2' && $currentUserId != $id) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT
|
||||
u.id,
|
||||
u.encrypt_email,
|
||||
u.encrypted_name,
|
||||
u.first_name,
|
||||
u.sect_name,
|
||||
u.encrypt_phone,
|
||||
u.encrypt_mobile,
|
||||
u.fk_role as role,
|
||||
u.fk_entite,
|
||||
u.infos,
|
||||
u.chk_alert_email,
|
||||
u.chk_suivi,
|
||||
u.date_naissance,
|
||||
u.date_embauche,
|
||||
u.matricule,
|
||||
u.chk_active,
|
||||
u.created_at,
|
||||
u.updated_at,
|
||||
e.encrypted_name as entite_name,
|
||||
e.adresse1,
|
||||
e.adresse2,
|
||||
e.cp,
|
||||
e.ville,
|
||||
e.fk_region
|
||||
FROM users u
|
||||
LEFT JOIN entites e ON u.fk_entite = e.id
|
||||
WHERE u.id = ?
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
$user = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$user) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Déchiffrement des données sensibles
|
||||
$user['email'] = ApiService::decryptSearchableData($user['encrypt_email']);
|
||||
$user['name'] = ApiService::decryptData($user['encrypted_name']);
|
||||
$user['phone'] = ApiService::decryptData($user['encrypt_phone'] ?? '');
|
||||
$user['mobile'] = ApiService::decryptData($user['encrypt_mobile'] ?? '');
|
||||
|
||||
if (!empty($user['entite_name'])) {
|
||||
$user['entite_name'] = ApiService::decryptData($user['entite_name']);
|
||||
}
|
||||
|
||||
// Suppression des champs chiffrés
|
||||
unset($user['encrypt_email']);
|
||||
unset($user['encrypted_name']);
|
||||
unset($user['encrypt_phone']);
|
||||
unset($user['encrypt_mobile']);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'user' => $user
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la récupération de l\'utilisateur GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'userId' => $id
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function createUser(): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur)
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$currentUserId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
|
||||
if ($userRole != '1' && $userRole != '2') {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Validation des données requises
|
||||
if (!isset($data['email'], $data['name'])) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Email et nom requis'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
$email = trim(strtolower($data['email']));
|
||||
$name = trim($data['name']);
|
||||
$firstName = isset($data['first_name']) ? trim($data['first_name']) : '';
|
||||
$role = isset($data['role']) ? trim($data['role']) : '1';
|
||||
$entiteId = isset($data['fk_entite']) ? (int)$data['fk_entite'] : 1;
|
||||
|
||||
// Vérification des longueurs d'entrée
|
||||
if (strlen($email) > 255 || strlen($name) > 255) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Email ou nom trop long'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validation de l'email
|
||||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Format d\'email invalide'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Chiffrement des données sensibles
|
||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||
$encryptedName = ApiService::encryptData($name);
|
||||
|
||||
// Vérification de l'existence de l'email
|
||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypt_email = ?');
|
||||
$checkStmt->execute([$encryptedEmail]);
|
||||
if ($checkStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cet email est déjà utilisé'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
|
||||
// Génération du mot de passe
|
||||
$password = ApiService::generateSecurePassword();
|
||||
$passwordHash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
// Préparation des champs optionnels
|
||||
$phone = isset($data['phone']) ? ApiService::encryptData(trim($data['phone'])) : null;
|
||||
$mobile = isset($data['mobile']) ? ApiService::encryptData(trim($data['mobile'])) : null;
|
||||
$sectName = isset($data['sect_name']) ? trim($data['sect_name']) : '';
|
||||
$infos = isset($data['infos']) ? trim($data['infos']) : '';
|
||||
$alertEmail = isset($data['chk_alert_email']) ? (int)$data['chk_alert_email'] : 1;
|
||||
$suivi = isset($data['chk_suivi']) ? (int)$data['chk_suivi'] : 0;
|
||||
$dateNaissance = isset($data['date_naissance']) ? $data['date_naissance'] : null;
|
||||
$dateEmbauche = isset($data['date_embauche']) ? $data['date_embauche'] : null;
|
||||
$matricule = isset($data['matricule']) ? trim($data['matricule']) : '';
|
||||
|
||||
// Insertion en base de données
|
||||
$stmt = $this->db->prepare('
|
||||
INSERT INTO users (
|
||||
encrypt_email, user_pswd, encrypted_name, first_name,
|
||||
sect_name, encrypt_phone, encrypt_mobile, fk_role,
|
||||
fk_entite, infos, chk_alert_email, chk_suivi,
|
||||
date_naissance, date_embauche, matricule,
|
||||
created_at, fk_user_creat, chk_active
|
||||
) VALUES (
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?, ?,
|
||||
?, ?, ?,
|
||||
NOW(), ?, 1
|
||||
)
|
||||
');
|
||||
$stmt->execute([
|
||||
$encryptedEmail,
|
||||
$passwordHash,
|
||||
$encryptedName,
|
||||
$firstName,
|
||||
$sectName,
|
||||
$phone,
|
||||
$mobile,
|
||||
$role,
|
||||
$entiteId,
|
||||
$infos,
|
||||
$alertEmail,
|
||||
$suivi,
|
||||
$dateNaissance,
|
||||
$dateEmbauche,
|
||||
$matricule,
|
||||
$currentUserId
|
||||
]);
|
||||
$userId = $this->db->lastInsertId();
|
||||
|
||||
// Envoi de l'email avec les identifiants
|
||||
ApiService::sendEmail($email, $name, 'welcome', ['password' => $password]);
|
||||
|
||||
LogService::log('Utilisateur GeoSector créé', [
|
||||
'level' => 'info',
|
||||
'createdBy' => $currentUserId,
|
||||
'newUserId' => $userId,
|
||||
'email' => $email
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Utilisateur créé avec succès',
|
||||
'id' => $userId
|
||||
], 201);
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la création d\'un utilisateur GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode()
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function updateUser(string $id): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur ou utilisateur lui-même)
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$currentUserId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
|
||||
if ($userRole != '1' && $userRole != '2' && $currentUserId != $id) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$data = Request::getJson();
|
||||
|
||||
// Vérification qu'il y a des données à mettre à jour
|
||||
if (empty($data)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Aucune donnée à mettre à jour'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Construction de la requête UPDATE dynamique
|
||||
$updateFields = [];
|
||||
$params = ['id' => $id];
|
||||
|
||||
// Traitement des champs à chiffrer
|
||||
if (isset($data['email'])) {
|
||||
// Vérification que l'email n'est pas déjà utilisé par un autre utilisateur
|
||||
$email = trim(strtolower($data['email']));
|
||||
$encryptedEmail = ApiService::encryptSearchableData($email);
|
||||
|
||||
$checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypt_email = ? AND id != ?');
|
||||
$checkStmt->execute([$encryptedEmail, $id]);
|
||||
if ($checkStmt->fetch()) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Cet email est déjà utilisé par un autre utilisateur'
|
||||
], 409);
|
||||
return;
|
||||
}
|
||||
|
||||
$updateFields[] = "encrypt_email = :encrypt_email";
|
||||
$params['encrypt_email'] = $encryptedEmail;
|
||||
}
|
||||
|
||||
if (isset($data['name'])) {
|
||||
$updateFields[] = "encrypted_name = :encrypted_name";
|
||||
$params['encrypted_name'] = ApiService::encryptData(trim($data['name']));
|
||||
}
|
||||
|
||||
if (isset($data['phone'])) {
|
||||
$updateFields[] = "encrypt_phone = :encrypt_phone";
|
||||
$params['encrypt_phone'] = ApiService::encryptData(trim($data['phone']));
|
||||
}
|
||||
|
||||
if (isset($data['mobile'])) {
|
||||
$updateFields[] = "encrypt_mobile = :encrypt_mobile";
|
||||
$params['encrypt_mobile'] = ApiService::encryptData(trim($data['mobile']));
|
||||
}
|
||||
|
||||
// Traitement des champs non chiffrés
|
||||
$nonEncryptedFields = [
|
||||
'first_name',
|
||||
'sect_name',
|
||||
'fk_role',
|
||||
'fk_entite',
|
||||
'infos',
|
||||
'chk_alert_email',
|
||||
'chk_suivi',
|
||||
'date_naissance',
|
||||
'date_embauche',
|
||||
'matricule',
|
||||
'chk_active'
|
||||
];
|
||||
|
||||
foreach ($nonEncryptedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updateFields[] = "$field = :$field";
|
||||
$params[$field] = is_string($data[$field]) ? trim($data[$field]) : $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// Mise à jour du mot de passe si fourni
|
||||
if (isset($data['password']) && !empty($data['password'])) {
|
||||
if (strlen($data['password']) < 8) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le mot de passe doit contenir au moins 8 caractères'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
$updateFields[] = "user_pswd = :password";
|
||||
$params['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
// Ajout des champs de mise à jour
|
||||
$updateFields[] = "updated_at = NOW()";
|
||||
$updateFields[] = "fk_user_modif = :modifier_id";
|
||||
$params['modifier_id'] = $currentUserId;
|
||||
|
||||
if (!empty($updateFields)) {
|
||||
$sql = 'UPDATE users SET ' . implode(', ', $updateFields) . ' WHERE id = :id';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->execute($params);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
Response::json([
|
||||
'status' => 'warning',
|
||||
'message' => 'Aucune modification effectuée'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector mis à jour', [
|
||||
'level' => 'info',
|
||||
'modifiedBy' => $currentUserId,
|
||||
'userId' => $id,
|
||||
'fields' => array_keys($data)
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Utilisateur mis à jour avec succès'
|
||||
]);
|
||||
} else {
|
||||
Response::json([
|
||||
'status' => 'warning',
|
||||
'message' => 'Aucune donnée valide à mettre à jour'
|
||||
]);
|
||||
}
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la mise à jour d\'un utilisateur GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'userId' => $id
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
public function deleteUser(string $id): void {
|
||||
Session::requireAuth();
|
||||
|
||||
// Vérification des droits d'accès (rôle administrateur)
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Récupérer le rôle de l'utilisateur depuis la base de données
|
||||
$stmt = $this->db->prepare('SELECT fk_role FROM users WHERE id = ?');
|
||||
$stmt->execute([$currentUserId]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
$userRole = $result ? $result['fk_role'] : null;
|
||||
|
||||
if ($userRole != '1' && $userRole != '2') {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Accès non autorisé'
|
||||
], 403);
|
||||
return;
|
||||
}
|
||||
|
||||
$currentUserId = Session::getUserId();
|
||||
|
||||
// Empêcher la suppression de son propre compte
|
||||
if ($currentUserId == $id) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Vous ne pouvez pas supprimer votre propre compte'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Désactivation de l'utilisateur plutôt que suppression
|
||||
$stmt = $this->db->prepare('
|
||||
UPDATE users
|
||||
SET chk_active = 0,
|
||||
updated_at = NOW(),
|
||||
fk_user_modif = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([$currentUserId, $id]);
|
||||
|
||||
if ($stmt->rowCount() === 0) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Utilisateur non trouvé'
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
LogService::log('Utilisateur GeoSector désactivé', [
|
||||
'level' => 'info',
|
||||
'deactivatedBy' => $currentUserId,
|
||||
'userId' => $id
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'message' => 'Utilisateur désactivé avec succès'
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
LogService::log('Erreur lors de la désactivation d\'un utilisateur GeoSector', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'userId' => $id
|
||||
]);
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur serveur'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Méthodes auxiliaires
|
||||
private function validateUpdateData(array $data): ?string {
|
||||
// Validation de l'email
|
||||
if (isset($data['email'])) {
|
||||
if (!filter_var(trim($data['email']), FILTER_VALIDATE_EMAIL)) {
|
||||
return 'Format d\'email invalide';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation du nom
|
||||
if (isset($data['name']) && strlen(trim($data['name'])) < 2) {
|
||||
return 'Le nom doit contenir au moins 2 caractères';
|
||||
}
|
||||
|
||||
// Validation du téléphone
|
||||
if (isset($data['phone']) && !empty($data['phone'])) {
|
||||
if (!preg_match('/^[0-9+\s()-]{6,20}$/', trim($data['phone']))) {
|
||||
return 'Format de téléphone invalide';
|
||||
}
|
||||
}
|
||||
|
||||
// Validation du mobile
|
||||
if (isset($data['mobile']) && !empty($data['mobile'])) {
|
||||
if (!preg_match('/^[0-9+\s()-]{6,20}$/', trim($data['mobile']))) {
|
||||
return 'Format de mobile invalide';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
99
api/src/Controllers/VilleController.php
Normal file
99
api/src/Controllers/VilleController.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
require_once __DIR__ . '/../Services/LogService.php';
|
||||
require_once __DIR__ . '/../Services/ApiService.php';
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use Database;
|
||||
use AppConfig;
|
||||
use Request;
|
||||
use Response;
|
||||
use Session;
|
||||
use LogService;
|
||||
use ApiService;
|
||||
use Exception;
|
||||
|
||||
class VilleController {
|
||||
private PDO $db;
|
||||
private AppConfig $appConfig;
|
||||
|
||||
public function __construct() {
|
||||
$this->db = Database::getInstance();
|
||||
$this->appConfig = AppConfig::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche les villes dont le code postal commence par les chiffres saisis
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function searchVillesByPostalCode(): void {
|
||||
try {
|
||||
// Récupérer le paramètre code_postal de la requête
|
||||
$postalCode = Request::getValue('code_postal');
|
||||
|
||||
if (empty($postalCode) || strlen($postalCode) < 3) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le code postal doit contenir au moins 3 chiffres'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Valider que le code postal ne contient que des chiffres
|
||||
if (!ctype_digit($postalCode)) {
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'message' => 'Le code postal doit contenir uniquement des chiffres'
|
||||
], 400);
|
||||
return;
|
||||
}
|
||||
|
||||
// Rechercher les villes dont le code postal commence par les chiffres saisis
|
||||
$stmt = $this->db->prepare('
|
||||
SELECT id, fk_departement, libelle, code_postal
|
||||
FROM x_villes
|
||||
WHERE code_postal LIKE ?
|
||||
ORDER BY libelle ASC
|
||||
LIMIT 20
|
||||
');
|
||||
|
||||
$stmt->execute([$postalCode . '%']);
|
||||
$villes = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Renommer les champs pour une meilleure lisibilité côté client
|
||||
$result = [];
|
||||
foreach ($villes as $ville) {
|
||||
$result[] = [
|
||||
'id' => $ville['id'],
|
||||
'departement_id' => $ville['fk_departement'],
|
||||
'nom' => $ville['libelle'],
|
||||
'code_postal' => $ville['code_postal']
|
||||
];
|
||||
}
|
||||
|
||||
Response::json([
|
||||
'status' => 'success',
|
||||
'success' => true,
|
||||
'data' => $result
|
||||
], 200);
|
||||
} catch (Exception $e) {
|
||||
LogService::log('Erreur lors de la recherche de villes par code postal', [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'postalCode' => $postalCode ?? 'non défini'
|
||||
]);
|
||||
|
||||
Response::json([
|
||||
'status' => 'error',
|
||||
'success' => false,
|
||||
'message' => 'Erreur lors de la recherche de villes'
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
api/src/Core/Database.php
Normal file
39
api/src/Core/Database.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Database {
|
||||
private static ?PDO $instance = null;
|
||||
private static array $config;
|
||||
|
||||
public static function init(array $config): void {
|
||||
self::$config = $config;
|
||||
}
|
||||
|
||||
public static function getInstance(): PDO {
|
||||
if (self::$instance === null) {
|
||||
try {
|
||||
$dsn = sprintf("mysql:host=%s;dbname=%s;charset=utf8mb4",
|
||||
self::$config['host'],
|
||||
self::$config['name']
|
||||
);
|
||||
|
||||
$options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
];
|
||||
|
||||
self::$instance = new PDO(
|
||||
$dsn,
|
||||
self::$config['username'],
|
||||
self::$config['password'],
|
||||
$options
|
||||
);
|
||||
} catch (PDOException $e) {
|
||||
throw new RuntimeException("Database connection failed: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
}
|
||||
22
api/src/Core/Request.php
Normal file
22
api/src/Core/Request.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Request {
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getJson(): array {
|
||||
$json = file_get_contents('php://input');
|
||||
$data = json_decode($json, true) ?? [];
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new RuntimeException('Invalid JSON payload');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function getValue(string $key, mixed $default = null): mixed {
|
||||
return $_REQUEST[$key] ?? $default;
|
||||
}
|
||||
}
|
||||
97
api/src/Core/Response.php
Normal file
97
api/src/Core/Response.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
class Response {
|
||||
public static function json(array $data, int $status = 200): void {
|
||||
// Nettoyer tout buffer existant
|
||||
while (ob_get_level() > 0) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
// Headers CORS pour permettre les requêtes cross-origin (applications mobiles)
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
|
||||
|
||||
// Configurer les headers CORS
|
||||
header("Access-Control-Allow-Origin: $origin");
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Origin, Content-Type, X-Auth-Token, Authorization, X-Requested-With, X-App-Identifier, X-Client-Type');
|
||||
header('Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision');
|
||||
|
||||
// Définir les headers de réponse
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
|
||||
// Définir le code de statut
|
||||
http_response_code($status);
|
||||
|
||||
// Ajouter status et message à la réponse si non présents
|
||||
if (!isset($data['status'])) {
|
||||
if ($status >= 200 && $status < 300) {
|
||||
$data['status'] = 'success';
|
||||
} else {
|
||||
$data['status'] = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
if (!isset($data['message']) && isset($data['error'])) {
|
||||
$data['message'] = $data['error'];
|
||||
}
|
||||
|
||||
// Sanitize data to ensure valid UTF-8 before encoding
|
||||
$sanitizedData = self::sanitizeForJson($data);
|
||||
|
||||
// Encoder et envoyer la réponse
|
||||
$jsonResponse = json_encode($sanitizedData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
// Vérifier si l'encodage a échoué
|
||||
if ($jsonResponse === false) {
|
||||
error_log('Erreur d\'encodage JSON: ' . json_last_error_msg());
|
||||
$jsonResponse = json_encode([
|
||||
'status' => 'error',
|
||||
'message' => 'Erreur d\'encodage de la réponse',
|
||||
'debug_info' => json_last_error_msg()
|
||||
]);
|
||||
}
|
||||
|
||||
// Log de débogage
|
||||
error_log('Envoi de la réponse JSON: ' . $jsonResponse);
|
||||
|
||||
// Envoyer la réponse
|
||||
echo $jsonResponse;
|
||||
|
||||
// S'assurer que tout est envoyé
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize data recursively to ensure valid UTF-8 encoding for JSON
|
||||
*
|
||||
* @param mixed $data The data to sanitize
|
||||
* @return mixed The sanitized data
|
||||
*/
|
||||
private static function sanitizeForJson($data) {
|
||||
if (is_string($data)) {
|
||||
// Replace invalid UTF-8 characters
|
||||
if (!mb_check_encoding($data, 'UTF-8')) {
|
||||
// Try to convert from other encodings
|
||||
$encodings = ['ISO-8859-1', 'Windows-1252'];
|
||||
foreach ($encodings as $encoding) {
|
||||
$converted = mb_convert_encoding($data, 'UTF-8', $encoding);
|
||||
if (mb_check_encoding($converted, 'UTF-8')) {
|
||||
return $converted;
|
||||
}
|
||||
}
|
||||
// If conversion fails, strip invalid characters
|
||||
return mb_convert_encoding($data, 'UTF-8', 'UTF-8');
|
||||
}
|
||||
return $data;
|
||||
} else if (is_array($data)) {
|
||||
// Recursively sanitize array elements
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = self::sanitizeForJson($value);
|
||||
}
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
208
api/src/Core/Router.php
Normal file
208
api/src/Core/Router.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Router {
|
||||
// Préfixe fixe de l'API (toujours 'api')
|
||||
private const API_PREFIX = 'api';
|
||||
private array $routes = [];
|
||||
private array $publicEndpoints = [
|
||||
'login',
|
||||
'register',
|
||||
'lostpassword',
|
||||
'log',
|
||||
'villes', // Ajout de la route villes comme endpoint public pour l'autocomplétion du code postal
|
||||
];
|
||||
|
||||
public function __construct() {
|
||||
// Pas besoin de récupérer AppConfig puisque nous utilisons une constante pour le préfixe API
|
||||
$this->configureRoutes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure toutes les routes de l'application
|
||||
*/
|
||||
private function configureRoutes(): void {
|
||||
// Routes publiques
|
||||
$this->post('login', ['LoginController', 'login']);
|
||||
$this->post('register', ['LoginController', 'register']);
|
||||
$this->post('lostpassword', ['LoginController', 'lostPassword']);
|
||||
|
||||
// Route pour les logs
|
||||
$this->post('log', ['LogController', 'index']);
|
||||
|
||||
// Routes privées utilisateurs
|
||||
$this->get('users', ['UserController', 'getUsers']);
|
||||
$this->get('users/:id', ['UserController', 'getUserById']);
|
||||
$this->post('users', ['UserController', 'createUser']);
|
||||
$this->put('users/:id', ['UserController', 'updateUser']);
|
||||
$this->delete('users/:id', ['UserController', 'deleteUser']);
|
||||
$this->post('logout', ['LoginController', 'logout']);
|
||||
|
||||
// Routes entités
|
||||
$this->get('entites', ['EntiteController', 'getEntites']);
|
||||
$this->get('entites/:id', ['EntiteController', 'getEntiteById']);
|
||||
$this->get('entites/postal/:code', ['EntiteController', 'getEntiteByPostalCode']);
|
||||
|
||||
// Routes villes
|
||||
$this->get('villes', ['VilleController', 'searchVillesByPostalCode']);
|
||||
}
|
||||
|
||||
public function handle(): void {
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$uri = $this->normalizeUri($_SERVER['REQUEST_URI']);
|
||||
|
||||
error_log("Initial URI: $uri");
|
||||
|
||||
// Handle CORS preflight
|
||||
if ($method === 'OPTIONS') {
|
||||
header('HTTP/1.1 200 OK');
|
||||
exit();
|
||||
}
|
||||
|
||||
// Prendre le préfixe API à partir de la constante
|
||||
$apiPrefix = self::API_PREFIX;
|
||||
|
||||
// Vérifier si l'URI commence bien par le préfixe API
|
||||
$prefixMatch = strpos($uri, $apiPrefix) === 0;
|
||||
|
||||
if (!$prefixMatch) {
|
||||
Response::json([
|
||||
'error' => 'Invalid API prefix',
|
||||
'path' => $uri,
|
||||
'expected_prefix' => $apiPrefix
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extraire l'endpoint en retirant le préfixe API
|
||||
$endpoint = substr($uri, strlen($apiPrefix) + 1); // +1 pour le slash
|
||||
$endpoint = trim($endpoint, '/');
|
||||
|
||||
// Check if endpoint is public
|
||||
if ($this->isPublicEndpoint($endpoint)) {
|
||||
error_log("Public endpoint found: $endpoint");
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
error_log("Private endpoint: $endpoint");
|
||||
// Private route - check auth first
|
||||
Session::requireAuth();
|
||||
|
||||
$route = $this->findRoute($method, $endpoint);
|
||||
if ($route) {
|
||||
$this->executeRoute($route);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No route found
|
||||
Response::json([
|
||||
'error' => 'Route not found',
|
||||
'endpoint' => $endpoint,
|
||||
'uri' => $uri
|
||||
], 404);
|
||||
}
|
||||
|
||||
private function normalizeUri(string $uri): string {
|
||||
return trim(preg_replace('#/+#', '/', parse_url($uri, PHP_URL_PATH)), '/');
|
||||
}
|
||||
|
||||
private function isPublicEndpoint(string $endpoint): bool {
|
||||
return in_array($endpoint, $this->publicEndpoints);
|
||||
}
|
||||
|
||||
private function executeRoute(array $route): void {
|
||||
[$controllerName, $method] = $route['handler'];
|
||||
|
||||
// Essayer de trouver le contrôleur en tenant compte des namespaces possibles
|
||||
$classNames = [
|
||||
$controllerName, // Sans namespace
|
||||
"\\App\\Controllers\\$controllerName", // Avec namespace complet
|
||||
"\\$controllerName" // Avec namespace racine
|
||||
];
|
||||
|
||||
$controllerClass = null;
|
||||
foreach ($classNames as $className) {
|
||||
if (class_exists($className)) {
|
||||
$controllerClass = $className;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($controllerClass === null) {
|
||||
// Classe non trouvée, gérer l'erreur
|
||||
Response::json([
|
||||
'error' => 'Controller not found',
|
||||
'controller' => $controllerName,
|
||||
'status' => 'error',
|
||||
'message' => 'Controller not found',
|
||||
'tried_namespaces' => implode(', ', $classNames)
|
||||
], 404);
|
||||
return;
|
||||
}
|
||||
|
||||
$controller = new $controllerClass();
|
||||
|
||||
if (!empty($route['params'])) {
|
||||
$controller->$method(...$route['params']);
|
||||
} else {
|
||||
$controller->$method();
|
||||
}
|
||||
}
|
||||
|
||||
public function get(string $path, array $handler): void {
|
||||
$this->addRoute('GET', $path, $handler);
|
||||
}
|
||||
|
||||
public function post(string $path, array $handler): void {
|
||||
$this->addRoute('POST', $path, $handler);
|
||||
}
|
||||
|
||||
public function put(string $path, array $handler): void {
|
||||
$this->addRoute('PUT', $path, $handler);
|
||||
}
|
||||
|
||||
public function delete(string $path, array $handler): void {
|
||||
$this->addRoute('DELETE', $path, $handler);
|
||||
}
|
||||
|
||||
private function addRoute(string $method, string $path, array $handler): void {
|
||||
// Normalize the path
|
||||
$path = trim($path, '/');
|
||||
$this->routes[$method][$path] = $handler;
|
||||
}
|
||||
|
||||
private function findRoute(string $method, string $uri): ?array {
|
||||
if (!isset($this->routes[$method])) {
|
||||
error_log("Méthode $method non trouvée dans les routes");
|
||||
return null;
|
||||
}
|
||||
|
||||
$uri = trim($uri, '/');
|
||||
error_log("Recherche de route pour: méthode=$method, uri=$uri");
|
||||
error_log("Routes disponibles pour $method: " . implode(', ', array_keys($this->routes[$method])));
|
||||
|
||||
foreach ($this->routes[$method] as $route => $handler) {
|
||||
$pattern = preg_replace('/{[^}]+}/', '([^/]+)', $route);
|
||||
$pattern = "@^" . $pattern . "$@D";
|
||||
error_log("Test pattern: $pattern contre uri: $uri");
|
||||
|
||||
if (preg_match($pattern, $uri, $matches)) {
|
||||
error_log("Route trouvée! Pattern: $pattern, Handler: {$handler[0]}::{$handler[1]}");
|
||||
array_shift($matches);
|
||||
return [
|
||||
'handler' => $handler,
|
||||
'params' => $matches
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
error_log("Aucune route trouvée pour $method $uri");
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
135
api/src/Core/Session.php
Normal file
135
api/src/Core/Session.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class Session {
|
||||
public static function start(): void {
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
// Configuration des sessions adaptée pour les applications mobiles
|
||||
ini_set('session.use_strict_mode', '1');
|
||||
ini_set('session.cookie_httponly', '1');
|
||||
|
||||
// Permettre les connexions non-HTTPS en développement
|
||||
$isProduction = (getenv('APP_ENV') === 'production');
|
||||
ini_set('session.cookie_secure', $isProduction ? '1' : '0');
|
||||
|
||||
// SameSite None pour permettre les requêtes cross-origin (applications mobiles)
|
||||
ini_set('session.cookie_samesite', 'None');
|
||||
ini_set('session.gc_maxlifetime', '86400'); // 24 heures
|
||||
|
||||
// Récupérer le session_id du Bearer token si présent
|
||||
self::getSessionFromBearer();
|
||||
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
public static function login(array $userData): void {
|
||||
$_SESSION['user_id'] = $userData['id'];
|
||||
$_SESSION['user_email'] = $userData['email'] ?? '';
|
||||
$_SESSION['authenticated'] = true;
|
||||
$_SESSION['last_activity'] = time();
|
||||
|
||||
// Régénère l'ID de session pour éviter la fixation de session
|
||||
session_regenerate_id(true);
|
||||
}
|
||||
|
||||
public static function logout(): void {
|
||||
session_unset();
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
public static function isAuthenticated(): bool {
|
||||
return isset($_SESSION['authenticated']) && $_SESSION['authenticated'] === true;
|
||||
}
|
||||
|
||||
public static function getUserId(): ?int {
|
||||
return $_SESSION['user_id'] ?? null;
|
||||
}
|
||||
|
||||
public static function getUserEmail(): ?string {
|
||||
return $_SESSION['user_email'] ?? null;
|
||||
}
|
||||
|
||||
public static function requireAuth(): void {
|
||||
if (!self::isAuthenticated()) {
|
||||
// Log détaillé pour le debug
|
||||
$logFile = __DIR__ . '/../../logs/auth_' . date('Y-m-d') . '.log';
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? 'No Authorization header';
|
||||
$appId = isset($_SERVER['HTTP_X_APP_IDENTIFIER']) ? $_SERVER['HTTP_X_APP_IDENTIFIER'] : 'No App Identifier';
|
||||
$method = $_SERVER['REQUEST_METHOD'] ?? 'Unknown Method';
|
||||
$uri = $_SERVER['REQUEST_URI'] ?? 'Unknown URI';
|
||||
|
||||
$logMessage = "\n===== AUTHENTICATION FAILURE =====\n";
|
||||
$logMessage .= "Date: " . date('Y-m-d H:i:s') . "\n";
|
||||
$logMessage .= "Method: $method\n";
|
||||
$logMessage .= "URI: $uri\n";
|
||||
$logMessage .= "App ID: $appId\n";
|
||||
$logMessage .= "Auth Header: $authHeader\n";
|
||||
$logMessage .= "Session data: " . (isset($_SESSION) ? json_encode($_SESSION) : 'No session') . "\n";
|
||||
$logMessage .= "================================\n";
|
||||
|
||||
file_put_contents($logFile, $logMessage, FILE_APPEND);
|
||||
|
||||
Response::json(['error' => 'Non authentifié - Veuillez vous connecter'], 401);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérification optionnelle de l'activité
|
||||
public static function checkActivity(): void {
|
||||
$inactiveTime = 3600; // 1 heure
|
||||
if (
|
||||
isset($_SESSION['last_activity']) &&
|
||||
(time() - $_SESSION['last_activity'] > $inactiveTime)
|
||||
) {
|
||||
self::logout();
|
||||
Response::json(['error' => 'Session expirée'], 440);
|
||||
exit;
|
||||
}
|
||||
$_SESSION['last_activity'] = time();
|
||||
}
|
||||
|
||||
// Récupère le session_id du Bearer token et le définit comme session_id courant
|
||||
private static function getSessionFromBearer(): void {
|
||||
// Vérifier si le header Authorization est présent
|
||||
$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
|
||||
|
||||
// Mettre toutes les erreurs dans un fichier de log dédié
|
||||
$logFile = __DIR__ . '/../../logs/session_' . date('Y-m-d') . '.log';
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Auth Header: " . $authHeader . "\n", FILE_APPEND);
|
||||
|
||||
// Nettoyage du header d'autorisation
|
||||
$authHeader = trim($authHeader);
|
||||
|
||||
// Support de plusieurs formats possibles
|
||||
if (strpos($authHeader, 'Bearer ') === 0) {
|
||||
// Format standard "Bearer token"
|
||||
$sessionId = substr($authHeader, 7);
|
||||
} elseif (strpos(strtolower($authHeader), 'bearer ') === 0) {
|
||||
// Cas insensible à la casse
|
||||
$sessionId = substr($authHeader, 7);
|
||||
} elseif (preg_match('/^bearer\s+(.*)$/i', $authHeader, $matches)) {
|
||||
// Utilisation de l'expression régulière
|
||||
$sessionId = $matches[1];
|
||||
} else {
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - No Bearer token found in Authorization header\n", FILE_APPEND);
|
||||
return;
|
||||
}
|
||||
|
||||
// Nettoyage du token
|
||||
$sessionId = trim($sessionId);
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Session ID extracted: " . $sessionId . "\n", FILE_APPEND);
|
||||
|
||||
// Vérifier que le session_id a un format valide (alphanumerique avec quelques caractères spéciaux)
|
||||
// Attention: les sessions en PHP peuvent contenir des caractères non-alphanumériques
|
||||
// Assouplir les règles de validation si nécessaire
|
||||
if (!empty($sessionId) && strlen($sessionId) <= 128) {
|
||||
// Définir l'ID de session avant de démarrer la session
|
||||
session_id($sessionId);
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Session ID set: " . $sessionId . "\n", FILE_APPEND);
|
||||
} else {
|
||||
file_put_contents($logFile, date('Y-m-d H:i:s') . " - Invalid session ID format in Bearer token\n", FILE_APPEND);
|
||||
}
|
||||
}
|
||||
}
|
||||
300
api/src/Services/ApiService.php
Normal file
300
api/src/Services/ApiService.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
use PHPMailer\PHPMailer\SMTP;
|
||||
use PHPMailer\PHPMailer\Exception;
|
||||
|
||||
require_once __DIR__ . '/EmailTemplates.php';
|
||||
|
||||
class ApiService {
|
||||
|
||||
/**
|
||||
* Envoie un email selon un type prédéfini
|
||||
*
|
||||
* @param string $email Email du destinataire
|
||||
* @param string $name Nom du destinataire
|
||||
* @param string $type Type d'email ('welcome', 'lostpwd', etc.)
|
||||
* @param array $data Données supplémentaires pour le template
|
||||
* @return int 1 si succès, 0 si échec
|
||||
*/
|
||||
public static function sendEmail(string $email, string $name, string $type, array $data = []): int {
|
||||
require_once __DIR__ . '/../../vendor/autoload.php';
|
||||
|
||||
$name = ucwords($name);
|
||||
try {
|
||||
$mail = new PHPMailer(true);
|
||||
|
||||
// Récupération des paramètres SMTP depuis la configuration
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$smtpConfig = $appConfig->getSmtpConfig();
|
||||
$emailConfig = $appConfig->getEmailConfig();
|
||||
|
||||
// Configuration du serveur
|
||||
$mail->isSMTP();
|
||||
$mail->Host = $smtpConfig['host'];
|
||||
$mail->SMTPAuth = $smtpConfig['auth'];
|
||||
$mail->Username = $smtpConfig['user'];
|
||||
$mail->Password = $smtpConfig['pass'];
|
||||
$mail->SMTPSecure = $smtpConfig['secure'];
|
||||
$mail->Port = $smtpConfig['port'];
|
||||
|
||||
// Configuration de base
|
||||
$mail->setFrom($emailConfig['from'], 'GEOSECTOR');
|
||||
$mail->addAddress($email, $name);
|
||||
$mail->isHTML(true);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
// Configuration selon le type d'email
|
||||
switch ($type) {
|
||||
case 'welcome':
|
||||
$mail->Subject = 'Bienvenue sur GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getWelcomeTemplate($name, $data['username'] ?? '', $data['password']);
|
||||
break;
|
||||
|
||||
case 'lostpwd':
|
||||
$mail->Subject = 'Réinitialisation de votre mot de passe GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getLostPasswordTemplate($name, $data['username'] ?? '', $data['password']);
|
||||
break;
|
||||
|
||||
case 'alert':
|
||||
$mail->Subject = $data['subject'] ?? 'Alerte GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getAlertTemplate($data['subject'] ?? 'Alerte', $data['message'] ?? '');
|
||||
break;
|
||||
|
||||
case 'receipt':
|
||||
$mail->Subject = 'Reçu de passage GEOSECTOR';
|
||||
$mail->Body = EmailTemplates::getReceiptTemplate(
|
||||
$name,
|
||||
$data['date'] ?? date('d/m/Y'),
|
||||
$data['address'] ?? '',
|
||||
$data['amount'] ?? '0',
|
||||
$data['paymentMethod'] ?? 'Espèces'
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Type d'email non reconnu");
|
||||
}
|
||||
|
||||
$mail->send();
|
||||
LogService::log("Email '$type' envoyé avec succès", [
|
||||
'level' => 'info',
|
||||
'email' => $email,
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
return 1;
|
||||
} catch (Exception $e) {
|
||||
LogService::log("Échec d'envoi d'email", [
|
||||
'level' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'email' => $email,
|
||||
'type' => $type
|
||||
]);
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Pour les données qui servent de clé de recherche (comme l'email)
|
||||
public static function encryptSearchableData(string $data): string {
|
||||
if (empty($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Forcer un padding cohérent en ajoutant un caractère spécial de contrôle
|
||||
$data = $data . "\x01"; // Garantit que même un bloc parfait reçoit du padding
|
||||
|
||||
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
||||
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
||||
$iv = str_repeat("\0", 16); // IV fixe
|
||||
|
||||
// Expliciter les options de padding
|
||||
$options = 0; // PKCS7 padding par défaut
|
||||
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, $options, $iv);
|
||||
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
|
||||
// Pour les données qui ne servent pas de clé de recherche (comme le nom)
|
||||
public static function encryptData(string $data): string {
|
||||
if (empty($data)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
||||
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
||||
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('AES-256-CBC'));
|
||||
$encrypted = openssl_encrypt($data, 'AES-256-CBC', $key, 0, $iv);
|
||||
return base64_encode($iv . $encrypted);
|
||||
}
|
||||
|
||||
public static function decryptSearchableData(string $encryptedData): string {
|
||||
if (empty($encryptedData)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Décoder la chaîne base64
|
||||
$encrypted = base64_decode($encryptedData);
|
||||
|
||||
// Vérifier que le décodage base64 a fonctionné
|
||||
if ($encrypted === false) {
|
||||
return ''; // Échec du décodage, retourner une chaîne vide
|
||||
}
|
||||
|
||||
// Méthode simple et robuste utilisant le même IV fixe que pour le chiffrement
|
||||
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
||||
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
||||
$iv = str_repeat("\0", 16); // IV fixe identique à celui utilisé pour le chiffrement
|
||||
|
||||
// Déchiffrer avec la méthode standard
|
||||
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
|
||||
|
||||
// Si le déchiffrement a échoué, retourner une chaîne vide
|
||||
if ($decrypted === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Supprimer uniquement le caractère de contrôle ajouté
|
||||
if (substr($decrypted, -1) === "\x01") {
|
||||
return substr($decrypted, 0, -1);
|
||||
}
|
||||
|
||||
return $decrypted; // Pour la rétrocompatibilité avec les anciennes données
|
||||
}
|
||||
|
||||
public static function decryptData(string $encryptedData): string {
|
||||
if (empty($encryptedData)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$keyBase64 = AppConfig::getInstance()->getEncryptionKey();
|
||||
$key = base64_decode($keyBase64); // Décoder la clé base64 en binaire
|
||||
|
||||
$data = base64_decode($encryptedData);
|
||||
|
||||
// Vérifier que le décodage base64 a fonctionné
|
||||
if ($data === false) {
|
||||
return $encryptedData; // Retourner la chaîne d'origine en cas d'échec
|
||||
}
|
||||
|
||||
$ivLength = openssl_cipher_iv_length('AES-256-CBC');
|
||||
|
||||
// Vérifier que les données sont assez longues pour contenir l'IV
|
||||
if (strlen($data) <= $ivLength) {
|
||||
return $encryptedData; // Retourner la chaîne d'origine en cas d'échec
|
||||
}
|
||||
|
||||
$iv = substr($data, 0, $ivLength);
|
||||
$encrypted = substr($data, $ivLength);
|
||||
|
||||
$decrypted = openssl_decrypt($encrypted, 'AES-256-CBC', $key, 0, $iv);
|
||||
|
||||
// Si le déchiffrement échoue, retourner la chaîne d'origine
|
||||
return $decrypted !== false ? $decrypted : $encryptedData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un nom d'utilisateur unique à partir du nom, du code postal et de la ville
|
||||
*
|
||||
* @param PDO $db Instance de la base de données
|
||||
* @param string $name Nom de l'utilisateur
|
||||
* @param string $postalCode Code postal
|
||||
* @param string $cityName Nom de la ville
|
||||
* @param int $minLength Longueur minimale du nom d'utilisateur (par défaut 10)
|
||||
* @return string Nom d'utilisateur généré
|
||||
*/
|
||||
public static function generateUserName(PDO $db, string $name, string $postalCode, string $cityName, int $minLength = 10): string {
|
||||
// Nettoyer et préparer les chaînes
|
||||
$name = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($name));
|
||||
$postalCode = preg_replace('/[^0-9]/', '', $postalCode);
|
||||
$cityName = preg_replace('/[^a-zA-Z0-9]/', '', strtolower($cityName));
|
||||
|
||||
// Extraire les premières lettres du nom (2 à 5 caractères)
|
||||
$nameLength = min(5, max(2, rand(2, strlen($name))));
|
||||
$namePart = substr($name, 0, $nameLength);
|
||||
|
||||
// Extraire une partie du code postal (2 à 3 chiffres)
|
||||
$postalLength = min(3, max(2, rand(2, strlen($postalCode))));
|
||||
$postalPart = substr($postalCode, 0, $postalLength);
|
||||
|
||||
// Extraire une partie du nom de la ville (2 à 4 caractères)
|
||||
$cityLength = min(4, max(2, rand(2, strlen($cityName))));
|
||||
$cityPart = substr($cityName, 0, $cityLength);
|
||||
|
||||
// Combiner les parties avec un séparateur aléatoire
|
||||
$separators = ['', '.', '_', '-'];
|
||||
$separator1 = $separators[array_rand($separators)];
|
||||
$separator2 = $separators[array_rand($separators)];
|
||||
|
||||
// Ajouter un nombre aléatoire pour garantir l'unicité
|
||||
$randomNum = rand(10, 999);
|
||||
|
||||
// Construire le nom d'utilisateur
|
||||
$username = $namePart . $separator1 . $postalPart . $separator2 . $cityPart . $randomNum;
|
||||
|
||||
// S'assurer que la longueur minimale est respectée
|
||||
while (strlen($username) < $minLength) {
|
||||
$username .= rand(0, 9);
|
||||
}
|
||||
|
||||
// Vérifier l'unicité du nom d'utilisateur dans la base de données
|
||||
$isUnique = false;
|
||||
$attempts = 0;
|
||||
$originalUsername = $username;
|
||||
|
||||
while (!$isUnique && $attempts < 10) {
|
||||
// Chiffrer le nom d'utilisateur pour la recherche
|
||||
$encryptedUsername = self::encryptSearchableData($username);
|
||||
|
||||
// Vérifier si le nom d'utilisateur existe déjà
|
||||
$stmt = $db->prepare('SELECT COUNT(*) as count FROM users WHERE encrypted_user_name = ?');
|
||||
$stmt->execute([$encryptedUsername]);
|
||||
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if ($result && $result['count'] == 0) {
|
||||
$isUnique = true;
|
||||
} else {
|
||||
// Ajouter un nombre aléatoire supplémentaire
|
||||
$username = $originalUsername . rand(100, 999);
|
||||
$attempts++;
|
||||
}
|
||||
}
|
||||
|
||||
return $username;
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un mot de passe sécurisé aléatoire
|
||||
*
|
||||
* @param int $minLength Longueur minimale du mot de passe (par défaut 12)
|
||||
* @param int $maxLength Longueur maximale du mot de passe (par défaut 16)
|
||||
* @return string Mot de passe généré
|
||||
*/
|
||||
public static function generateSecurePassword(int $minLength = 12, int $maxLength = 16): string {
|
||||
$lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
||||
$uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$numbers = '0123456789';
|
||||
$special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
|
||||
$length = rand($minLength, $maxLength);
|
||||
$password = '';
|
||||
|
||||
// Au moins un de chaque type
|
||||
$password .= $lowercase[rand(0, strlen($lowercase) - 1)];
|
||||
$password .= $uppercase[rand(0, strlen($uppercase) - 1)];
|
||||
$password .= $numbers[rand(0, strlen($numbers) - 1)];
|
||||
$password .= $special[rand(0, strlen($special) - 1)];
|
||||
|
||||
// Compléter avec des caractères aléatoires
|
||||
$all = $lowercase . $uppercase . $numbers . $special;
|
||||
for ($i = strlen($password); $i < $length; $i++) {
|
||||
$password .= $all[rand(0, strlen($all) - 1)];
|
||||
}
|
||||
|
||||
// Mélanger le mot de passe
|
||||
return str_shuffle($password);
|
||||
}
|
||||
}
|
||||
76
api/src/Services/EmailTemplates.php
Normal file
76
api/src/Services/EmailTemplates.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class EmailTemplates {
|
||||
/**
|
||||
* Template d'email de bienvenue
|
||||
*/
|
||||
public static function getWelcomeTemplate(string $name, string $username, string $password): string {
|
||||
return "
|
||||
Bonjour $name,<br><br>
|
||||
Votre compte a été créé avec succès sur <b>GeoSector</b>.<br><br>
|
||||
<b>Identifiant :</b> $username<br>
|
||||
<b>Mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter dès maintenant sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
|
||||
/**
|
||||
* Template d'email pour mot de passe perdu
|
||||
*/
|
||||
public static function getLostPasswordTemplate(string $name, string $username, string $password): string {
|
||||
return "
|
||||
Bonjour $name,<br><br>
|
||||
Vous avez demandé la réinitialisation de votre mot de passe sur <b>GeoSector</b>.<br><br>
|
||||
<b>Nouveau mot de passe :</b> $password<br><br>
|
||||
Vous pouvez vous connecter avec ce nouveau mot de passe sur <a href=\"https://app.geosector.fr\">app.geosector.fr</a><br><br>
|
||||
À très bientôt,<br>
|
||||
L'équipe GeoSector";
|
||||
}
|
||||
|
||||
/**
|
||||
* Template d'email pour alerte
|
||||
*/
|
||||
public static function getAlertTemplate(string $subject, string $message): string {
|
||||
return "
|
||||
<h2>$subject</h2>
|
||||
<p>$message</p>
|
||||
<br>
|
||||
<p>L'équipe GeoSector</p>";
|
||||
}
|
||||
|
||||
/**
|
||||
* Template de reçu de passage
|
||||
*/
|
||||
public static function getReceiptTemplate(string $name, string $date, string $address, string $amount, string $paymentMethod): string {
|
||||
return "
|
||||
<h2>Reçu de passage GeoSector</h2>
|
||||
<p>Bonjour $name,</p>
|
||||
<p>Nous vous remercions pour votre contribution lors de notre passage.</p>
|
||||
<br>
|
||||
<table style='width:100%; border-collapse: collapse;'>
|
||||
<tr>
|
||||
<td style='padding:8px; border:1px solid #ddd;'><b>Date :</b></td>
|
||||
<td style='padding:8px; border:1px solid #ddd;'>$date</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding:8px; border:1px solid #ddd;'><b>Adresse :</b></td>
|
||||
<td style='padding:8px; border:1px solid #ddd;'>$address</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding:8px; border:1px solid #ddd;'><b>Montant :</b></td>
|
||||
<td style='padding:8px; border:1px solid #ddd;'>$amount €</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style='padding:8px; border:1px solid #ddd;'><b>Mode de paiement :</b></td>
|
||||
<td style='padding:8px; border:1px solid #ddd;'>$paymentMethod</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
<p>Votre soutien est précieux pour notre amicale. Nous vous en remercions chaleureusement.</p>
|
||||
<p>À bientôt,</p>
|
||||
<p>L'équipe GeoSector</p>";
|
||||
}
|
||||
}
|
||||
117
api/src/Services/LogService.php
Normal file
117
api/src/Services/LogService.php
Normal file
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
class LogService {
|
||||
public static function log(string $message, array $metadata = []): void {
|
||||
// Obtenir les informations client via ClientDetector
|
||||
$clientInfo = ClientDetector::getClientInfo();
|
||||
$clientType = $clientInfo['type'];
|
||||
|
||||
$defaultMetadata = [
|
||||
'level' => 'info',
|
||||
'client' => [
|
||||
'ip' => $clientInfo['ip'],
|
||||
'browser' => $clientInfo['browser'],
|
||||
'os' => $clientInfo['os'],
|
||||
'screenResolution' => 'N/A',
|
||||
'userAgent' => $clientInfo['userAgent']
|
||||
],
|
||||
'client_type' => $clientType
|
||||
];
|
||||
|
||||
// Si c'est une app mobile, ajouter l'identifiant de l'app
|
||||
if ($clientType === 'mobile' && isset($clientInfo['appIdentifier'])) {
|
||||
$defaultMetadata['app_identifier'] = $clientInfo['appIdentifier'];
|
||||
}
|
||||
|
||||
$metadata = array_merge_recursive($defaultMetadata, $metadata);
|
||||
|
||||
$logData = [
|
||||
'metadata' => $metadata,
|
||||
'message' => $message
|
||||
];
|
||||
|
||||
try {
|
||||
// Récupérer la configuration de l'application
|
||||
$appConfig = AppConfig::getInstance();
|
||||
$appName = $appConfig->getName();
|
||||
|
||||
// Récupérer l'environnement défini dans la configuration
|
||||
$environment = $appConfig->getEnvironment();
|
||||
|
||||
// Définir le chemin du dossier logs à la racine du projet
|
||||
$logDir = __DIR__ . '/../../logs';
|
||||
|
||||
// Créer le nom du fichier basé sur l'application et l'environnement
|
||||
// Format: geosector-production-2025-03-28.log, geosector-recette-2025-03-28.log, geosector-development-2025-03-28.log
|
||||
$filename = $logDir . '/' . $appName . '-' . $environment . '-' . date('Y-m-d') . '.log';
|
||||
|
||||
// Créer le dossier logs s'il n'existe pas
|
||||
if (!is_dir($logDir)) {
|
||||
if (!mkdir($logDir, 0777, true)) {
|
||||
error_log("Impossible de créer le dossier de logs: {$logDir}");
|
||||
return; // Sortir de la fonction si on ne peut pas créer le dossier
|
||||
}
|
||||
// S'assurer que les permissions sont correctes
|
||||
chmod($logDir, 0777);
|
||||
}
|
||||
|
||||
// Vérifier si le dossier est accessible en écriture
|
||||
if (!is_writable($logDir)) {
|
||||
error_log("Le dossier de logs n'est pas accessible en écriture: {$logDir}");
|
||||
return; // Sortir de la fonction si on ne peut pas écrire dans le dossier
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Erreur lors de la configuration des logs: " . $e->getMessage());
|
||||
return; // Sortir de la fonction en cas d'erreur
|
||||
}
|
||||
|
||||
try {
|
||||
// Formater la ligne de log au format plat demandé
|
||||
// timestamp;browser.name@browser.version;os.name@os.version;client_type;$metadata;$message
|
||||
$timestamp = date('Y-m-d\TH:i:s');
|
||||
$browserInfo = $clientInfo['browser']['name'] . '@' . $clientInfo['browser']['version'];
|
||||
$osInfo = $clientInfo['os']['name'] . '@' . $clientInfo['os']['version'];
|
||||
|
||||
// Extraire le niveau de log
|
||||
$level = isset($metadata['level']) ? (is_array($metadata['level']) ? 'info' : $metadata['level']) : 'info';
|
||||
|
||||
// Préparer les métadonnées supplémentaires (exclure celles déjà incluses dans le format)
|
||||
$additionalMetadata = [];
|
||||
foreach ($metadata as $key => $value) {
|
||||
if (!in_array($key, ['browser', 'os', 'client_type', 'side', 'version', 'level', 'environment', 'client'])) {
|
||||
if (is_array($value)) {
|
||||
$additionalMetadata[$key] = json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
} else {
|
||||
$additionalMetadata[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Joindre les métadonnées supplémentaires avec des virgules
|
||||
$metadataStr = !empty($additionalMetadata) ? implode(',', array_map(function($k, $v) {
|
||||
return $k . '=' . $v;
|
||||
}, array_keys($additionalMetadata), $additionalMetadata)) : '-';
|
||||
|
||||
// Construire la ligne de log au format demandé
|
||||
$logLine = implode(';', [
|
||||
$timestamp,
|
||||
$browserInfo,
|
||||
$osInfo,
|
||||
$clientType,
|
||||
$level,
|
||||
$metadataStr,
|
||||
$message
|
||||
]) . "\n";
|
||||
|
||||
// Écrire dans le fichier avec gestion d'erreur
|
||||
if (file_put_contents($filename, $logLine, FILE_APPEND) === false) {
|
||||
error_log("Impossible d'écrire dans le fichier de logs: {$filename}");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
error_log("Erreur lors de l'écriture des logs: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
97
api/src/Utils/ClientDetector.php
Normal file
97
api/src/Utils/ClientDetector.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
class ClientDetector {
|
||||
/**
|
||||
* Détecte le type de client basé sur l'User-Agent
|
||||
*
|
||||
* @return string 'mobile', 'web' ou 'unknown'
|
||||
*/
|
||||
public static function getClientType(): string {
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
// Détection des applications mobiles natives
|
||||
if (preg_match('/(Android|iOS)\/[0-9\.]+\s+\w+\/[0-9\.]+/', $userAgent)) {
|
||||
return 'mobile';
|
||||
}
|
||||
|
||||
// Détection des navigateurs mobiles
|
||||
if (preg_match('/(Android|iPhone|iPad|iPod|Windows Phone)/i', $userAgent)) {
|
||||
return 'mobile';
|
||||
}
|
||||
|
||||
// Détection des navigateurs web
|
||||
if (preg_match('/(Mozilla|Chrome|Safari|Firefox|Edge|MSIE|Trident)/i', $userAgent)) {
|
||||
return 'web';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère l'identifiant de l'application mobile depuis l'User-Agent
|
||||
* Format attendu: AppName/VersionNumber (Platform/Version)
|
||||
*
|
||||
* @return string Identifiant de l'application ou 'unknown'
|
||||
*/
|
||||
public static function getAppIdentifier(): string {
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
// Extraction de l'identifiant de l'application
|
||||
if (preg_match('/^([A-Za-z0-9_]+)\/[0-9\.]+/', $userAgent, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Extraction depuis un format alternatif
|
||||
if (preg_match('/\b(GeoSector|Prokov|Resalice)\/[0-9\.]+\b/i', $userAgent, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère des informations détaillées sur le client
|
||||
*
|
||||
* @return array Informations sur le client
|
||||
*/
|
||||
public static function getClientInfo(): array {
|
||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
||||
|
||||
// Information par défaut
|
||||
$clientInfo = [
|
||||
'type' => self::getClientType(),
|
||||
'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
|
||||
'userAgent' => $userAgent,
|
||||
'browser' => [
|
||||
'name' => 'unknown',
|
||||
'version' => 'unknown'
|
||||
],
|
||||
'os' => [
|
||||
'name' => 'unknown',
|
||||
'version' => 'unknown'
|
||||
]
|
||||
];
|
||||
|
||||
// Détection du navigateur
|
||||
if (preg_match('/(Chrome|Safari|Firefox|Edge|MSIE|Trident)[\s\/]([0-9\.]+)/i', $userAgent, $matches)) {
|
||||
$clientInfo['browser']['name'] = $matches[1];
|
||||
$clientInfo['browser']['version'] = $matches[2];
|
||||
}
|
||||
|
||||
// Détection du système d'exploitation
|
||||
if (preg_match('/(Android|iOS|iPhone OS|iPad|iPod|Windows NT|Mac OS X|Linux)[\s\/]([0-9\._]+)/i', $userAgent, $matches)) {
|
||||
$clientInfo['os']['name'] = $matches[1];
|
||||
$clientInfo['os']['version'] = $matches[2];
|
||||
}
|
||||
|
||||
// Si c'est une application mobile, ajouter l'identifiant
|
||||
if ($clientInfo['type'] === 'mobile') {
|
||||
$clientInfo['appIdentifier'] = self::getAppIdentifier();
|
||||
}
|
||||
|
||||
return $clientInfo;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user