Files
geo/api/src/Controllers/LoginController.php
d6soft 95e9af23e2 feat: création branche singletons - début refactorisation
- Sauvegarde des fichiers critiques
- Préparation transformation ApiService en singleton
- Préparation création CurrentUserService et CurrentAmicaleService
- Objectif: renommer Box users -> user
2025-06-05 15:22:29 +02:00

1279 lines
56 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controllers;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
use PDO;
use PDOException;
use Database;
use AppConfig;
use Request;
use Response;
use Session;
use LogService;
use ApiService;
require_once __DIR__ . '/../Services/LogService.php';
require_once __DIR__ . '/../Services/ApiService.php';
require_once __DIR__ . '/EntiteController.php';
class LoginController {
private PDO $db;
private AppConfig $appConfig;
public function __construct() {
$this->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;
}
}