Ajout du dossier api avec la géolocalisation automatique des casernes de pompiers

This commit is contained in:
d6soft
2025-05-16 21:03:04 +02:00
parent 69dcff42f8
commit f4f7882963
143 changed files with 24329 additions and 1 deletions

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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