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