db = Database::getInstance(); $this->appConfig = AppConfig::getInstance(); } public function login(): void { try { $data = Request::getJson(); if (!isset($data['username'], $data['password'], $data['type'])) { LogService::log('Tentative de connexion GeoSector échouée : données manquantes', [ 'level' => 'warning', 'username' => $data['username'] ?? 'non fourni' ]); Response::json(['error' => 'Nom d\'utilisateur et mot de passe requis'], 400); return; } $interface = trim($data['type']); $username = trim($data['username']); $encryptedUsername = ApiService::encryptSearchableData($username); // Récupérer le type d'utilisateur // admin accessible uniquement aux fk_role>1 // sinon tout user peut se connecter à l'interface utilisateur $roleCondition = ($interface === 'user') ? '' : 'AND fk_role>1'; // Log pour le debug LogService::log('Tentative de connexion GeoSector', [ 'level' => 'info', 'username' => $username, 'type' => $interface, 'role_condition' => $roleCondition ]); // Requête optimisée: on récupère l'utilisateur et son entité en une seule fois avec LEFT JOIN $stmt = $this->db->prepare( 'SELECT u.id, u.encrypted_email, u.encrypted_user_name, u.encrypted_name, u.user_pass_hash, u.first_name, u.fk_role, u.fk_entite, u.chk_active, u.sect_name, e.id AS entite_id, e.encrypted_name AS entite_encrypted_name, e.adresse1, e.code_postal, e.ville, e.gps_lat, e.gps_lng, e.chk_active AS entite_chk_active FROM users u LEFT JOIN entites e ON u.fk_entite = e.id WHERE u.encrypted_user_name = ? AND u.chk_active != 0 ' . $roleCondition ); $stmt->execute([$encryptedUsername]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { LogService::log('Tentative de connexion GeoSector échouée : utilisateur non trouvé', [ 'level' => 'warning', 'username' => $username ]); Response::json(['error' => 'Identifiants invalides'], 401); return; } // Vérification du mot de passe $passwordValid = password_verify($data['password'], $user['user_pass_hash']); if (!$passwordValid) { LogService::log('Tentative de connexion GeoSector échouée : mot de passe incorrect', [ 'level' => 'warning', 'username' => $username ]); Response::json(['error' => 'Identifiants invalides'], 401); return; } // Vérifier si l'utilisateur a une entité et si elle est active if (!empty($user['fk_entite']) && (!isset($user['entite_chk_active']) || $user['entite_chk_active'] != 1)) { LogService::log('Tentative de connexion GeoSector échouée : entité non active', [ 'level' => 'warning', 'username' => $username, 'entite_id' => $user['fk_entite'] ]); Response::json([ 'status' => 'error', 'message' => 'Votre amicale n\'est pas activée. Veuillez contacter votre administrateur.' ], 403); return; } // Mise à jour de last_login et activation du compte si nécessaire $updateQuery = 'UPDATE users SET updated_at = NOW()' . ($user['chk_active'] == -1 ? ', chk_active = 1' : '') . ($user['chk_active'] == 2 ? ', chk_active = 1' : '') . ' WHERE id = ?'; $updateStmt = $this->db->prepare($updateQuery); $updateStmt->execute([$user['id']]); // Déchiffrement du nom $decryptedName = ApiService::decryptData($user['encrypted_name']); // Déchiffrement de l'email si disponible $email = ''; if (!empty($user['encrypted_email'])) { $email = ApiService::decryptSearchableData($user['encrypted_email']); // Si le déchiffrement échoue, renvoyer une erreur if (empty($email)) { LogService::log('Déchiffrement email échoué', [ 'level' => 'error', 'message' => 'Déchiffrement de l\'email échoué', 'encrypted_email' => $user['encrypted_email'], 'user_id' => $user['id'] ]); Response::json([ 'status' => 'error', 'message' => 'Erreur de déchiffrement de l\'email. Exécutez le script de migration pour résoudre ce problème.', 'debug_info' => [ 'encrypted_email' => $user['encrypted_email'], 'user_id' => $user['id'] ] ], 500); return; } } // Création de la session $sessionData = [ 'id' => $user['id'], 'username' => $username, 'email' => $email, 'name' => $decryptedName, 'first_name' => $user['first_name'] ?? '', 'fk_role' => $user['fk_role'] ?? '0', 'fk_entite' => $user['fk_entite'] ?? '0', // 'interface' supprimée pour se baser uniquement sur le rôle ]; Session::login($sessionData); // Préparation des données utilisateur pour la réponse (uniquement les champs du user) $userData = [ 'id' => $user['id'], 'fk_entite' => $user['fk_entite'] ?? null, 'fk_role' => $user['fk_role'] ?? '0', 'fk_titre' => $user['fk_titre'] ?? null, 'first_name' => $user['first_name'] ?? '', 'sect_name' => $user['sect_name'] ?? '', 'date_naissance' => $user['date_naissance'] ?? null, 'date_embauche' => $user['date_embauche'] ?? null, 'username' => $username, 'name' => $decryptedName ]; // Déchiffrement du téléphone if (!empty($user['encrypted_phone'])) { $userData['phone'] = ApiService::decryptData($user['encrypted_phone']); } else { $userData['phone'] = ''; } // Déchiffrement du mobile if (!empty($user['encrypted_mobile'])) { $userData['mobile'] = ApiService::decryptData($user['encrypted_mobile']); } else { $userData['mobile'] = ''; } // L'email est déjà déchiffré plus haut dans le code $userData['email'] = $email; // Suivant l'interface et le role de l'utilisateur, on lui charge toutes ses données utiles // operations : // Si $interface='user' : on ne récupère que la dernière opération active // Si $interface='admin' et si $user['fk_role']=2 : on récupère les 3 dernières opérations dont celle active // Dans tous les autres cas, operations: [] // secteurs : // On récupère les secteurs de l'opération active trouvée, sinon secteurs: [] // passages : // On récupère les passages du ou des secteurs trouvés, sinon passages: [] // users_sectors : // On récupère les users affectés aux secteurs partagés de l'utilisateur, si pas de secteurs, users_passages: [] // clients : // Si $interface="admin" et si $user['fk_role']=9 // On récupère les entités au complet sauf la entite.id=1 dans un group clients contenant id, name, adresse1, adresse2, code_postal, ville, fk_region, lib_region, fk_type, phone, mobile, email, gps_lat, gps_lng, chk_active // Suivant l'interface et le role de l'utilisateur, on lui charge toutes ses données utiles $operationsData = []; $sectorsData = []; $passagesData = []; $usersSectorsData = []; // 1. Récupération des opérations selon les critères $operationLimit = 0; $activeOperationOnly = false; if ($interface === 'user') { // Interface utilisateur : seulement la dernière opération active $operationLimit = 1; $activeOperationOnly = true; } elseif ($interface === 'admin' && $user['fk_role'] == 2) { // Interface admin avec rôle 2 : les 3 dernières opérations dont l'active $operationLimit = 3; } else { // Autres cas : pas d'opérations $operationLimit = 0; } if ($operationLimit > 0 && !empty($user['fk_entite'])) { $operationQuery = "SELECT id, libelle, date_deb, date_fin FROM operations WHERE fk_entite = ?"; if ($activeOperationOnly) { $operationQuery .= " AND chk_active = 1"; } $operationQuery .= " ORDER BY id DESC LIMIT " . $operationLimit; $operationStmt = $this->db->prepare($operationQuery); $operationStmt->execute([$user['fk_entite']]); $operations = $operationStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($operations)) { // Formater les données des opérations foreach ($operations as $operation) { $operationsData[] = [ 'id' => $operation['id'], 'name' => $operation['libelle'], 'date_deb' => $operation['date_deb'], 'date_fin' => $operation['date_fin'] ]; } // Récupérer l'ID de l'opération active (première opération retournée) $activeOperationId = $operations[0]['id']; // 2. Récupérer les secteurs selon l'interface et le rôle if ($interface === 'user') { // Interface utilisateur : seulement les secteurs affectés à l'utilisateur $sectorsStmt = $this->db->prepare( 'SELECT s.id, s.libelle, s.color, s.sector FROM ope_sectors s JOIN ope_users_sectors us ON s.id = us.fk_sector WHERE us.fk_operation = ? AND us.fk_user = ? AND us.chk_active = 1 AND s.chk_active = 1' ); $sectorsStmt->execute([$activeOperationId, $user['id']]); } elseif ($interface === 'admin' && $user['fk_role'] == 2) { // Interface admin avec rôle 2 : tous les secteurs distincts de l'opération $sectorsStmt = $this->db->prepare( 'SELECT DISTINCT s.id, s.libelle, s.color, s.sector FROM ope_sectors s WHERE s.fk_operation = ? AND s.chk_active = 1' ); $sectorsStmt->execute([$activeOperationId]); } else { // Autres cas : pas de secteurs $sectors = []; $sectorsData = []; } // Récupération des secteurs si une requête a été préparée if (isset($sectorsStmt)) { $sectors = $sectorsStmt->fetchAll(PDO::FETCH_ASSOC); } else { $sectors = []; } if (!empty($sectors)) { $sectorsData = $sectors; // 3. Récupérer les passages selon l'interface et le rôle if ($interface === 'user' && !empty($sectors)) { // Interface utilisateur : passages liés aux secteurs de l'utilisateur $sectorIds = array_column($sectors, 'id'); $sectorIdsString = implode(',', $sectorIds); if (!empty($sectorIdsString)) { $passagesStmt = $this->db->prepare( "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau, gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages FROM ope_pass WHERE fk_operation = ? AND fk_sector IN ($sectorIdsString) AND chk_active = 1" ); $passagesStmt->execute([$activeOperationId]); } } elseif ($interface === 'admin' && $user['fk_role'] == 2) { // Interface admin avec rôle 2 : tous les passages de l'opération $passagesStmt = $this->db->prepare( "SELECT id, fk_operation, fk_sector, fk_user, fk_type, fk_adresse, passed_at, numero, rue, rue_bis, ville, residence, fk_habitat, appt, niveau, gps_lat, gps_lng, nom_recu, encrypted_name, remarque, encrypted_email, encrypted_phone, montant, fk_type_reglement, email_erreur, nb_passages FROM ope_pass WHERE fk_operation = ? AND chk_active = 1" ); $passagesStmt->execute([$activeOperationId]); } else { // Autres cas : pas de passages $passages = []; $passagesData = []; } // Récupération des passages si une requête a été préparée if (isset($passagesStmt)) { $passages = $passagesStmt->fetchAll(PDO::FETCH_ASSOC); } else { $passages = []; } if (!empty($passages)) { // Déchiffrer les données sensibles foreach ($passages as &$passage) { // Déchiffrement du nom $passage['name'] = ''; if (!empty($passage['encrypted_name'])) { $passage['name'] = ApiService::decryptData($passage['encrypted_name']); } unset($passage['encrypted_name']); // Déchiffrement de l'email $passage['email'] = ''; if (!empty($passage['encrypted_email'])) { $decryptedEmail = ApiService::decryptSearchableData($passage['encrypted_email']); if ($decryptedEmail) { $passage['email'] = $decryptedEmail; } } unset($passage['encrypted_email']); // Déchiffrement du téléphone $passage['phone'] = ''; if (!empty($passage['encrypted_phone'])) { $passage['phone'] = ApiService::decryptData($passage['encrypted_phone']); } unset($passage['encrypted_phone']); } $passagesData = $passages; } // 4. Récupérer les utilisateurs des secteurs partagés if (($interface === 'user' || ($interface === 'admin' && $user['fk_role'] == 2)) && !empty($sectors)) { $sectorIds = array_column($sectors, 'id'); $sectorIdsString = implode(',', $sectorIds); if (!empty($sectorIdsString)) { $usersSectorsStmt = $this->db->prepare( "SELECT DISTINCT u.id, u.first_name, u.encrypted_name, u.sect_name, us.fk_sector FROM users u JOIN ope_users_sectors us ON u.id = us.fk_user WHERE us.fk_sector IN ($sectorIdsString) AND us.fk_operation = ? AND us.chk_active = 1 AND u.chk_active = 1 AND u.id != ?" // Exclure l'utilisateur connecté ); $usersSectorsStmt->execute([$activeOperationId, $user['id']]); $usersSectors = $usersSectorsStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($usersSectors)) { // Déchiffrer les noms des utilisateurs foreach ($usersSectors as &$userSector) { if (!empty($userSector['encrypted_name'])) { $userSector['name'] = ApiService::decryptData($userSector['encrypted_name']); unset($userSector['encrypted_name']); } } $usersSectorsData = $usersSectors; } } } else { // Autres cas : pas d'utilisateurs de secteurs $usersSectorsData = []; } } } } // Section "clients" supprimée car redondante avec le nouveau groupe "amicales" // 6. Récupérer les membres (users de l'entité du user) si nécessaire if ($interface === 'admin' && $user['fk_role'] == 2 && !empty($user['fk_entite'])) { $membresStmt = $this->db->prepare( 'SELECT id, fk_role, fk_entite, fk_titre, encrypted_name, first_name, sect_name, encrypted_user_name, encrypted_phone, encrypted_mobile, encrypted_email, date_naissance, date_embauche, chk_active FROM users WHERE fk_entite = ?' ); $membresStmt->execute([$user['fk_entite']]); $membres = $membresStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($membres)) { $membresData = []; foreach ($membres as $membre) { $membreItem = [ 'id' => $membre['id'], 'fk_role' => $membre['fk_role'], 'fk_entite' => $membre['fk_entite'], 'fk_titre' => $membre['fk_titre'], 'first_name' => $membre['first_name'] ?? '', 'sect_name' => $membre['sect_name'] ?? '', 'date_naissance' => $membre['date_naissance'] ?? null, 'date_embauche' => $membre['date_embauche'] ?? null, 'chk_active' => $membre['chk_active'] ]; // Déchiffrement du nom if (!empty($membre['encrypted_name'])) { $membreItem['name'] = ApiService::decryptData($membre['encrypted_name']); } // Déchiffrement du nom d'utilisateur if (!empty($membre['encrypted_user_name'])) { $membreItem['username'] = ApiService::decryptSearchableData($membre['encrypted_user_name']); } // Déchiffrement du téléphone if (!empty($membre['encrypted_phone'])) { $membreItem['phone'] = ApiService::decryptData($membre['encrypted_phone']); } // Déchiffrement du mobile if (!empty($membre['encrypted_mobile'])) { $membreItem['mobile'] = ApiService::decryptData($membre['encrypted_mobile']); } // Déchiffrement de l'email if (!empty($membre['encrypted_email'])) { $decryptedEmail = ApiService::decryptSearchableData($membre['encrypted_email']); if ($decryptedEmail) { $membreItem['email'] = $decryptedEmail; } } $membresData[] = $membreItem; } // Les membres seront ajoutés à la racine de la réponse plus tard // (après la préparation de la réponse) } } // 7. Récupérer les amicales selon le rôle de l'utilisateur $amicalesData = []; if (!empty($user['fk_entite'])) { if ($user['fk_role'] <= 2) { // User normal ou admin avec fk_role=2: uniquement son amicale $amicaleStmt = $this->db->prepare( 'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville, e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile, e.encrypted_email as email, e.gps_lat, e.gps_lng, e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe FROM entites e LEFT JOIN x_regions r ON e.fk_region = r.id WHERE e.id = ? AND e.chk_active = 1' ); $amicaleStmt->execute([$user['fk_entite']]); $amicales = $amicaleStmt->fetchAll(PDO::FETCH_ASSOC); } else { // Admin avec fk_role>2: toutes les amicales sauf id=1 $amicaleStmt = $this->db->prepare( 'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville, e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile, e.encrypted_email as email, e.gps_lat, e.gps_lng, e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe FROM entites e LEFT JOIN x_regions r ON e.fk_region = r.id WHERE e.id != 1 AND e.chk_active = 1' ); $amicaleStmt->execute(); $amicales = $amicaleStmt->fetchAll(PDO::FETCH_ASSOC); } if (!empty($amicales)) { foreach ($amicales as &$amicale) { // Déchiffrement du nom if (!empty($amicale['name'])) { $amicale['name'] = ApiService::decryptData($amicale['name']); } // Déchiffrement de l'email si disponible if (!empty($amicale['email'])) { $decryptedEmail = ApiService::decryptSearchableData($amicale['email']); if ($decryptedEmail) { $amicale['email'] = $decryptedEmail; } } // Déchiffrement du téléphone if (!empty($amicale['phone'])) { $amicale['phone'] = ApiService::decryptData($amicale['phone']); } // Déchiffrement du mobile if (!empty($amicale['mobile'])) { $amicale['mobile'] = ApiService::decryptData($amicale['mobile']); } // Déchiffrement du stripe_id if (!empty($amicale['stripe_id'])) { $amicale['stripe_id'] = ApiService::decryptData($amicale['stripe_id']); } } $amicalesData = $amicales; } } // 8. Récupérer les entités de type 1 pour les utilisateurs avec fk_role > 2 $entitesData = []; if ($user['fk_role'] > 2) { // Admin avec fk_role > 2: toutes les entités de type 1 $entitesStmt = $this->db->prepare( 'SELECT e.id, e.encrypted_name as name, e.adresse1, e.adresse2, e.code_postal, e.ville, e.fk_region, r.libelle AS lib_region, e.fk_type, e.encrypted_phone as phone, e.encrypted_mobile as mobile, e.encrypted_email as email, e.gps_lat, e.gps_lng, e.encrypted_stripe_id as stripe_id, e.chk_demo, e.chk_copie_mail_recu, e.chk_accept_sms, e.chk_active, e.chk_stripe FROM entites e LEFT JOIN x_regions r ON e.fk_region = r.id WHERE e.fk_type = 1 AND e.chk_active = 1' ); $entitesStmt->execute(); $entites = $entitesStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($entites)) { foreach ($entites as &$entite) { // Déchiffrement du nom if (!empty($entite['name'])) { $entite['name'] = ApiService::decryptData($entite['name']); } // Déchiffrement de l'email si disponible if (!empty($entite['email'])) { $decryptedEmail = ApiService::decryptSearchableData($entite['email']); if ($decryptedEmail) { $entite['email'] = $decryptedEmail; } } // Déchiffrement du téléphone if (!empty($entite['phone'])) { $entite['phone'] = ApiService::decryptData($entite['phone']); } // Déchiffrement du mobile if (!empty($entite['mobile'])) { $entite['mobile'] = ApiService::decryptData($entite['mobile']); } // Déchiffrement du stripe_id if (!empty($entite['stripe_id'])) { $entite['stripe_id'] = ApiService::decryptData($entite['stripe_id']); } } $entitesData = $entites; } } // Préparation de la réponse $response = [ 'status' => 'success', 'message' => 'Connexion réussie', 'session_id' => session_id(), 'session_expiry' => date('c', strtotime('+24 hours')), // Ajoute une expiration de 24h 'user' => $userData ]; // Ajout des amicales à la racine de la réponse si disponibles if (!empty($amicalesData)) { // Si c'est un tableau avec un seul élément, on envoie directement l'objet // pour que le client reçoive un objet et non un tableau avec un seul objet if (count($amicalesData) === 1) { $response['amicale'] = $amicalesData[0]; } else { $response['amicale'] = $amicalesData; } } // Ajout des entités à la racine de la réponse sous le nom "clients" (vide pour fk_role <= 2) $response['clients'] = $entitesData; // Ajout des membres à la racine de la réponse si disponibles if (!empty($membresData)) { $response['membres'] = $membresData; } // Ajout des opérations à la racine de la réponse si disponibles if (!empty($operationsData)) { $response['operations'] = $operationsData; } // Ajout des secteurs à la racine de la réponse si disponibles if (!empty($sectorsData)) { $response['sectors'] = $sectorsData; } // Ajout des passages à la racine de la réponse si disponibles if (!empty($passagesData)) { $response['passages'] = $passagesData; } // Ajout des utilisateurs des secteurs à la racine de la réponse si disponibles if (!empty($usersSectorsData)) { $response['users_sectors'] = $usersSectorsData; } // Section "clients" supprimée car redondante avec le nouveau groupe "amicales" // 9. Récupérer les régions selon le rôle de l'utilisateur $regionsData = []; if ($user['fk_role'] <= 2 && !empty($user['fk_entite'])) { // User normal ou admin avec fk_role=2: uniquement sa région basée sur le code postal de son amicale $amicaleStmt = $this->db->prepare('SELECT code_postal FROM entites WHERE id = ?'); $amicaleStmt->execute([$user['fk_entite']]); $amicale = $amicaleStmt->fetch(PDO::FETCH_ASSOC); if (!empty($amicale) && !empty($amicale['code_postal'])) { $departement = substr($amicale['code_postal'], 0, 2); $regionStmt = $this->db->prepare( 'SELECT id, fk_pays, libelle, libelle_long, table_osm, departements, chk_active FROM x_regions WHERE FIND_IN_SET(?, departements) > 0 AND chk_active = 1' ); $regionStmt->execute([$departement]); $regions = $regionStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($regions)) { $regionsData = $regions; } } } else { // Admin avec fk_role>2: toutes les régions $regionStmt = $this->db->prepare( 'SELECT id, fk_pays, libelle, libelle_long, table_osm, departements, chk_active FROM x_regions WHERE chk_active = 1' ); $regionStmt->execute(); $regions = $regionStmt->fetchAll(PDO::FETCH_ASSOC); if (!empty($regions)) { $regionsData = $regions; } } // Ajout des régions à la racine de la réponse si disponibles if (!empty($regionsData)) { $response['regions'] = $regionsData; } // Envoi de la réponse Response::json($response); } catch (PDOException $e) { LogService::log('Erreur base de données lors de la connexion GeoSector', [ 'level' => 'error', 'error' => $e->getMessage(), 'code' => $e->getCode() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur serveur' ], 500); } catch (Exception $e) { LogService::log('Erreur inattendue lors de la connexion GeoSector', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Une erreur inattendue est survenue' ], 500); } } public function lostPassword(): void { try { $data = Request::getJson(); if (!isset($data['email']) || empty($data['email'])) { Response::json([ 'status' => 'error', 'message' => 'Email requis' ], 400); return; } $email = trim($data['email']); // Validation de l'email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { Response::json([ 'status' => 'error', 'message' => 'Format d\'email invalide' ], 400); return; } // Chiffrement de l'email pour la recherche $encryptedEmail = ApiService::encryptSearchableData($email); // Recherche de l'utilisateur $stmt = $this->db->prepare(' SELECT id, encrypted_name, encrypted_user_name, chk_active FROM users WHERE encrypted_email = ? '); $stmt->execute([$encryptedEmail]); $user = $stmt->fetch(PDO::FETCH_ASSOC); if (!$user) { Response::json([ 'status' => 'error', 'message' => 'Aucun compte trouvé avec cet email' ], 404); return; } if ($user['chk_active'] == 0) { Response::json([ 'status' => 'error', 'message' => 'Ce compte est désactivé. Contactez l\'administrateur.' ], 403); return; } // Déchiffrement du nom et du username $name = ApiService::decryptData($user['encrypted_name']); $username = ApiService::decryptSearchableData($user['encrypted_user_name']); // Génération d'un nouveau mot de passe $newPassword = ApiService::generateSecurePassword(); $passwordHash = password_hash($newPassword, PASSWORD_DEFAULT); // Mise à jour du mot de passe $updateStmt = $this->db->prepare(' UPDATE users SET user_pass_hash = ?, updated_at = NOW() WHERE id = ? '); $updateStmt->execute([$passwordHash, $user['id']]); // Envoi de l'email avec le nouveau mot de passe $emailSent = ApiService::sendEmail( $email, $name, 'lostpwd', ['username' => $username, 'password' => $newPassword] ); if ($emailSent) { LogService::log('Réinitialisation mot de passe GeoSector réussie', [ 'level' => 'info', 'userId' => $user['id'], 'email' => $email ]); Response::json([ 'status' => 'success', 'message' => 'Un nouveau mot de passe a été envoyé à votre adresse email' ]); } else { LogService::log('Échec envoi email réinitialisation mot de passe GeoSector', [ 'level' => 'error', 'userId' => $user['id'], 'email' => $email ]); Response::json([ 'status' => 'error', 'message' => 'Impossible d\'envoyer l\'email. Veuillez contacter l\'administrateur.' ], 500); } } catch (Exception $e) { LogService::log('Erreur lors de la réinitialisation du mot de passe GeoSector', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Une erreur est survenue. Veuillez réessayer.' ], 500); } } public function register(): void { try { $data = Request::getJson(); // 1. Validation des données de base if ( !isset($data['email'], $data['name'], $data['amicale_name'], $data['postal_code'], $data['city_name']) || empty($data['email']) || empty($data['name']) || empty($data['amicale_name']) || empty($data['postal_code']) ) { Response::json([ 'status' => 'error', 'message' => 'Tous les champs sont requis' ], 400); return; } // 2. Validation du token et du captcha if (!isset($data['token']) || empty($data['token'])) { Response::json([ 'status' => 'error', 'message' => 'Token de sécurité manquant' ], 400); return; } // Vérification que le token est un timestamp valide et récent // Le frontend envoie un timestamp en millisecondes, donc on le convertit en secondes $tokenTimestamp = intval($data['token']) / 1000; // Conversion millisecondes -> secondes $currentTime = time(); $twoHoursAgo = $currentTime - 7200; // 2 heures = 7200 secondes (plus permissif) // Tolérance de 5 minutes pour les décalages d'horloge $futureTime = $currentTime + 300; // 5 minutes = 300 secondes // Log pour le débogage LogService::log('Vérification du token', [ 'level' => 'info', 'token_ms' => $data['token'], 'token_sec' => $tokenTimestamp, 'current_time' => $currentTime, 'two_hours_ago' => $twoHoursAgo, 'future_time' => $futureTime ]); // Vérification plus permissive if ($tokenTimestamp < $twoHoursAgo || $tokenTimestamp > $futureTime) { LogService::log('Tentative d\'inscription avec un token invalide', [ 'level' => 'warning', 'token' => $data['token'], 'token_sec' => $tokenTimestamp, 'current_time' => $currentTime, 'email' => $data['email'] ?? 'non fourni' ]); Response::json([ 'status' => 'error', 'message' => 'Session expirée, veuillez rafraîchir la page et réessayer' ], 400); return; } if ( !isset($data['captcha_answer'], $data['captcha_expected']) || $data['captcha_answer'] != $data['captcha_expected'] ) { LogService::log('Tentative d\'inscription avec un captcha invalide', [ 'level' => 'warning', 'captcha_answer' => $data['captcha_answer'] ?? 'non fourni', 'captcha_expected' => $data['captcha_expected'] ?? 'non fourni', 'email' => $data['email'] ?? 'non fourni' ]); Response::json([ 'status' => 'error', 'message' => 'Vérification anti-robot échouée' ], 400); return; } $email = trim($data['email']); $name = trim($data['name']); $amicaleName = trim($data['amicale_name']); $postalCode = trim($data['postal_code']); $cityName = trim($data['city_name'] ?? ''); // 3. Validation de l'email if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { Response::json([ 'status' => 'error', 'message' => 'Format d\'email invalide' ], 400); return; } // 4. Vérification de l'existence de l'email $encryptedEmail = ApiService::encryptSearchableData($email); $checkStmt = $this->db->prepare('SELECT id FROM users WHERE encrypted_email = ?'); $checkStmt->execute([$encryptedEmail]); if ($checkStmt->fetch()) { Response::json([ 'status' => 'error', 'message' => 'Cet email est déjà utilisé' ], 409); return; } // 5. Vérification de l'existence du code postal dans la table entites $checkPostalStmt = $this->db->prepare('SELECT id FROM entites WHERE code_postal = ?'); $checkPostalStmt->execute([$postalCode]); if ($checkPostalStmt->fetch()) { Response::json([ 'status' => 'error', 'message' => 'Une amicale est déjà inscrite à ce code postal' ], 409); return; } // 6. Recherche de la région correspondant au code postal $departement = substr($postalCode, 0, 2); $regionStmt = $this->db->prepare(' SELECT id FROM x_regions WHERE FIND_IN_SET(?, departements) > 0 AND chk_active = 1 LIMIT 1 '); $regionStmt->execute([$departement]); $region = $regionStmt->fetch(PDO::FETCH_ASSOC); $regionId = $region ? $region['id'] : null; // 7. Chiffrement des données sensibles $encryptedName = ApiService::encryptData($name); $encryptedAmicaleName = ApiService::encryptData($amicaleName); $encryptedEmail = ApiService::encryptSearchableData($email); // 8. Création de l'entité (amicale) $this->db->beginTransaction(); try { // Insertion de la nouvelle entité $entiteStmt = $this->db->prepare(' INSERT INTO entites ( encrypted_name, code_postal, ville, fk_type, fk_region, encrypted_email, chk_demo, chk_active, created_at ) VALUES (?, ?, ?, 1, ?, ?, 1, 1, NOW()) '); $entiteStmt->execute([ $encryptedAmicaleName, $postalCode, $cityName, $regionId, $encryptedEmail ]); $entiteId = $this->db->lastInsertId(); if (!$entiteId) { throw new Exception('Erreur lors de la création de l\'entité'); } // Recherche des coordonnées GPS de la caserne de pompiers try { $gpsCoordinates = $this->findFireStationCoordinates($postalCode, $cityName); if ($gpsCoordinates) { // Mise à jour des coordonnées GPS de l'entité $updateGpsStmt = $this->db->prepare(' UPDATE entites SET gps_lat = ?, gps_lng = ? WHERE id = ? '); $updateGpsStmt->execute([ $gpsCoordinates['lat'], $gpsCoordinates['lng'], $entiteId ]); LogService::log('Coordonnées GPS de la caserne de pompiers ajoutées', [ 'level' => 'info', 'entiteId' => $entiteId, 'postalCode' => $postalCode, 'cityName' => $cityName, 'lat' => $gpsCoordinates['lat'], 'lng' => $gpsCoordinates['lng'] ]); } else { LogService::log('Aucune caserne de pompiers trouvée', [ 'level' => 'warning', 'entiteId' => $entiteId, 'postalCode' => $postalCode, 'cityName' => $cityName ]); } } catch (Exception $e) { // On ne bloque pas l'inscription si la recherche de coordonnées échoue LogService::log('Erreur lors de la recherche des coordonnées GPS', [ 'level' => 'error', 'entiteId' => $entiteId, 'postalCode' => $postalCode, 'cityName' => $cityName, 'error' => $e->getMessage() ]); } // 9. Génération du nom d'utilisateur et du mot de passe $username = ApiService::generateUserName($this->db, $name, $postalCode, $cityName); $encryptedUsername = ApiService::encryptSearchableData($username); $password = ApiService::generateSecurePassword(); $passwordHash = password_hash($password, PASSWORD_DEFAULT); // 10. Création de l'utilisateur administrateur $userStmt = $this->db->prepare(' INSERT INTO users ( encrypted_user_name, encrypted_email, user_pass_hash, encrypted_name, fk_role, created_at, chk_active, fk_entite ) VALUES (?, ?, ?, ?, 2, NOW(), 1, ?) '); $userStmt->execute([ $encryptedUsername, $encryptedEmail, $passwordHash, $encryptedName, $entiteId ]); $userId = $this->db->lastInsertId(); $this->db->commit(); // Log du succès de l'inscription LogService::log('Inscription GeoSector réussie', [ 'level' => 'info', 'userId' => $userId, 'username' => $username, 'email' => $email, 'role' => 2, 'entiteId' => $entiteId, 'amicaleName' => $amicaleName, 'postalCode' => $postalCode, 'cityName' => $cityName ]); // 11. Envoi des emails // Premier email : bienvenue avec UNIQUEMENT le nom d'utilisateur (sans mot de passe) // Création d'un mot de passe temporaire pour le template (ne sera pas affiché) $tempPassword = "********"; $welcomeResult = ApiService::sendEmail( $email, $name, 'welcome', ['username' => $username, 'password' => $tempPassword] ); // Email de notification aux administrateurs (sans le nom d'utilisateur ni le mot de passe) $notificationMessage = "Nouvelle inscription GeoSector:\n\n" . "Nom: $name\n" . "Email: $email\n" . "Amicale: $amicaleName\n" . "Code postal: $postalCode\n" . "Ville: $cityName\n"; ApiService::sendEmail( "contactgeosector@gmail.com", "Admin GeoSector", 'alert', ['subject' => 'Nouvelle inscription GeoSector', 'message' => $notificationMessage] ); ApiService::sendEmail( "contact@geosector.fr", "Admin GeoSector", 'alert', ['subject' => 'Nouvelle inscription GeoSector', 'message' => $notificationMessage] ); // Attendre un court délai avant d'envoyer le second email (pour éviter les filtres anti-spam) sleep(2); // Second email : UNIQUEMENT le mot de passe $passwordResult = ApiService::sendEmail( $email, $name, 'lostpwd', ['username' => $username, 'password' => $password] ); // Réponse selon le résultat de l'envoi d'email if ($welcomeResult === 0 || $passwordResult === 0) { Response::json([ 'status' => 'warning', 'message' => 'Compte créé avec succès mais impossible de vous envoyer tous les emails. ' . 'Rendez-vous sur la page de login et choisissez mot de passe perdu pour recevoir votre mot de passe.' ], 201); } else { Response::json([ 'status' => 'success', 'message' => 'Votre compte a bien été créé et vous recevrez par email votre identifiant et mot de passe' ], 201); } } catch (Exception $e) { $this->db->rollBack(); LogService::log('Erreur lors de la création du compte GeoSector', [ 'level' => 'error', 'error' => $e->getMessage(), 'email' => $email, 'amicaleName' => $amicaleName, 'postalCode' => $postalCode ]); Response::json([ 'status' => 'error', 'message' => $e->getMessage() ], 500); return; } } catch (PDOException $e) { LogService::log('Erreur serveur lors de l\'inscription GeoSector', [ 'level' => 'error', 'error' => $e->getMessage(), 'code' => $e->getCode(), 'trace' => $e->getTraceAsString() ]); Response::json([ 'status' => 'error', 'message' => 'Erreur lors de la création du compte. Veuillez réessayer.' ], 500); } catch (Exception $e) { LogService::log('Erreur inattendue lors de l\'inscription GeoSector', [ 'level' => 'error', 'error' => $e->getMessage() ]); Response::json([ 'status' => 'error', 'message' => 'Une erreur inattendue est survenue. Veuillez réessayer.' ], 500); } } // Méthodes auxiliaires public function logout(): void { $userId = Session::getUserId() ?? null; $userEmail = Session::getUserEmail() ?? 'anonyme'; Session::logout(); LogService::log('Déconnexion GeoSector réussie', [ 'level' => 'info', 'userId' => $userId, 'email' => $userEmail ]); // Retourner une réponse standardisée Response::json([ 'status' => 'success', 'message' => 'Déconnexion réussie' ]); } /** * Recherche les coordonnées GPS d'une caserne de pompiers dans une ville donnée * * @param string $postalCode Le code postal de la ville * @param string $cityName Le nom de la ville * @return array|null Tableau associatif contenant les coordonnées GPS (lat, lng) ou null si aucune caserne trouvée * @throws Exception En cas d'erreur lors de la requête API */ private function findFireStationCoordinates(string $postalCode, string $cityName): ?array { // Mots-clés pour rechercher une caserne de pompiers $keywords = ['pompiers', 'sdis', 'sapeurs-pompiers', 'caserne', 'centre de secours']; // Formater la ville et le code postal pour la recherche $citySearch = urlencode($cityName . ' ' . $postalCode); foreach ($keywords as $keyword) { // Construire l'URL de recherche pour l'API adresse.gouv.fr $searchUrl = "https://api-adresse.data.gouv.fr/search/?q=" . urlencode($keyword) . "+$citySearch&limit=5"; // Effectuer la requête HTTP $response = @file_get_contents($searchUrl); if ($response === false) { LogService::log('Erreur lors de la requête à l\'API adresse.gouv.fr', [ 'level' => 'error', 'url' => $searchUrl ]); continue; // Essayer avec le mot-clé suivant } // Décoder la réponse JSON $data = json_decode($response, true); if (!$data || !isset($data['features']) || empty($data['features'])) { continue; // Aucun résultat, essayer avec le mot-clé suivant } // Parcourir les résultats pour trouver une caserne de pompiers foreach ($data['features'] as $feature) { $properties = $feature['properties'] ?? []; $name = strtolower($properties['name'] ?? ''); $label = strtolower($properties['label'] ?? ''); // Vérifier si le résultat correspond à une caserne de pompiers if ( strpos($name, 'pompier') !== false || strpos($name, 'sdis') !== false || strpos($label, 'pompier') !== false || strpos($label, 'sdis') !== false || strpos($name, 'caserne') !== false || strpos($label, 'caserne') !== false || strpos($name, 'centre de secours') !== false || strpos($label, 'centre de secours') !== false ) { // Extraire les coordonnées GPS $coordinates = $feature['geometry']['coordinates'] ?? null; if ($coordinates && count($coordinates) >= 2) { // L'API retourne les coordonnées au format [longitude, latitude] return [ 'lng' => $coordinates[0], 'lat' => $coordinates[1] ]; } } } } // Si aucune caserne n'a été trouvée avec les mots-clés, utiliser les coordonnées du centre de la ville $cityUrl = "https://api-adresse.data.gouv.fr/search/?q=$citySearch&limit=1"; $cityResponse = @file_get_contents($cityUrl); if ($cityResponse !== false) { $cityData = json_decode($cityResponse, true); if ($cityData && isset($cityData['features'][0]['geometry']['coordinates'])) { $coordinates = $cityData['features'][0]['geometry']['coordinates']; LogService::log('Utilisation des coordonnées du centre de la ville', [ 'level' => 'info', 'city' => $cityName, 'postalCode' => $postalCode ]); return [ 'lng' => $coordinates[0], 'lat' => $coordinates[1] ]; } } // Aucune coordonnée trouvée return null; } }