feat: Version 3.5.2 - Configuration Stripe et gestion des immeubles
- Configuration complète Stripe pour les 3 environnements (DEV/REC/PROD) * DEV: Clés TEST Pierre (mode test) * REC: Clés TEST Client (mode test) * PROD: Clés LIVE Client (mode live) - Ajout de la gestion des bases de données immeubles/bâtiments * Configuration buildings_database pour DEV/REC/PROD * Service BuildingService pour enrichissement des adresses - Optimisations pages et améliorations ergonomie - Mises à jour des dépendances Composer - Nettoyage des fichiers obsolètes 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
533
api/src/Services/EventLogService.php
Normal file
533
api/src/Services/EventLogService.php
Normal file
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use ClientDetector;
|
||||
use Session;
|
||||
|
||||
/**
|
||||
* EventLogService - Système de logs d'événements JSONL
|
||||
*
|
||||
* Enregistre tous les événements métier (authentification, CRUD passages/secteurs/users/entités/opérations)
|
||||
* dans des fichiers JSONL quotidiens pour statistiques et audit.
|
||||
*
|
||||
* Format : Un événement = une ligne JSON
|
||||
* Stockage : /logs/events/YYYY-MM-DD.jsonl
|
||||
* Rétention : 15 mois (compression après 30 jours)
|
||||
*
|
||||
* @see docs/EVENTS-LOG.md
|
||||
*/
|
||||
class EventLogService
|
||||
{
|
||||
/** @var string Chemin du dossier de logs d'événements */
|
||||
private const LOG_DIR = __DIR__ . '/../../logs/events';
|
||||
|
||||
/** @var int Permissions du dossier */
|
||||
private const DIR_PERMISSIONS = 0750;
|
||||
|
||||
/** @var int Permissions des fichiers */
|
||||
private const FILE_PERMISSIONS = 0640;
|
||||
|
||||
// ==================== MÉTHODES D'AUTHENTIFICATION ====================
|
||||
|
||||
/**
|
||||
* Log une connexion réussie
|
||||
*
|
||||
* @param int $userId ID utilisateur (users.id)
|
||||
* @param int|null $entityId ID entité
|
||||
* @param string $username Nom d'utilisateur
|
||||
*/
|
||||
public static function logLoginSuccess(int $userId, ?int $entityId, string $username): void
|
||||
{
|
||||
self::writeEvent('login_success', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une tentative de connexion échouée
|
||||
*
|
||||
* @param string $username Nom d'utilisateur tenté
|
||||
* @param string $reason Raison (invalid_password, user_not_found, account_inactive, blocked_ip)
|
||||
* @param int $attempt Numéro de tentative
|
||||
*/
|
||||
public static function logLoginFailed(string $username, string $reason, int $attempt = 1): void
|
||||
{
|
||||
self::writeEvent('login_failed', [
|
||||
'username' => $username,
|
||||
'reason' => $reason,
|
||||
'attempt' => $attempt
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log une déconnexion
|
||||
*
|
||||
* @param int $userId ID utilisateur
|
||||
* @param int|null $entityId ID entité
|
||||
* @param int $sessionDuration Durée session en secondes
|
||||
*/
|
||||
public static function logLogout(int $userId, ?int $entityId, int $sessionDuration = 0): void
|
||||
{
|
||||
self::writeEvent('logout', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'session_duration' => $sessionDuration
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PASSAGES ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage créé
|
||||
* @param int $operationId ID opération
|
||||
* @param int $sectorId ID secteur
|
||||
* @param float $amount Montant
|
||||
* @param string $paymentType Type paiement (cash, stripe, check, etc.)
|
||||
*/
|
||||
public static function logPassageCreated(
|
||||
int $passageId,
|
||||
int $operationId,
|
||||
int $sectorId,
|
||||
float $amount,
|
||||
string $paymentType
|
||||
): void {
|
||||
self::writeEvent('passage_created', [
|
||||
'passage_id' => $passageId,
|
||||
'operation_id' => $operationId,
|
||||
'sector_id' => $sectorId,
|
||||
'amount' => $amount,
|
||||
'payment_type' => $paymentType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param array $changes Tableau des changements ['field' => ['old' => val, 'new' => val]]
|
||||
*/
|
||||
public static function logPassageUpdated(int $passageId, array $changes): void
|
||||
{
|
||||
self::writeEvent('passage_updated', [
|
||||
'passage_id' => $passageId,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un passage
|
||||
*
|
||||
* @param int $passageId ID du passage
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logPassageDeleted(int $passageId, int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$userId = Session::getUserId();
|
||||
self::writeEvent('passage_deleted', [
|
||||
'passage_id' => $passageId,
|
||||
'operation_id' => $operationId,
|
||||
'deleted_by' => $userId,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES SECTEURS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur créé
|
||||
* @param int $operationId ID opération
|
||||
* @param string $sectorName Nom du secteur
|
||||
*/
|
||||
public static function logSectorCreated(int $sectorId, int $operationId, string $sectorName): void
|
||||
{
|
||||
self::writeEvent('sector_created', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'sector_name' => $sectorName
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur
|
||||
* @param int $operationId ID opération
|
||||
* @param array $changes Tableau des changements
|
||||
*/
|
||||
public static function logSectorUpdated(int $sectorId, int $operationId, array $changes): void
|
||||
{
|
||||
self::writeEvent('sector_updated', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un secteur
|
||||
*
|
||||
* @param int $sectorId ID du secteur
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logSectorDeleted(int $sectorId, int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$userId = Session::getUserId();
|
||||
self::writeEvent('sector_deleted', [
|
||||
'sector_id' => $sectorId,
|
||||
'operation_id' => $operationId,
|
||||
'deleted_by' => $userId,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES USERS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'un utilisateur
|
||||
*
|
||||
* @param int $newUserId ID utilisateur créé
|
||||
* @param int $entityId ID entité
|
||||
* @param int $roleId ID rôle
|
||||
* @param string $username Nom d'utilisateur
|
||||
*/
|
||||
public static function logUserCreated(int $newUserId, int $entityId, int $roleId, string $username): void
|
||||
{
|
||||
$createdBy = Session::getUserId();
|
||||
self::writeEvent('user_created', [
|
||||
'new_user_id' => $newUserId,
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'role_id' => $roleId,
|
||||
'username' => $username
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'un utilisateur
|
||||
*
|
||||
* @param int $userId ID utilisateur modifié
|
||||
* @param array $changes Tableau des changements (booléen pour champs chiffrés)
|
||||
*/
|
||||
public static function logUserUpdated(int $userId, array $changes): void
|
||||
{
|
||||
$updatedBy = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
self::writeEvent('user_updated', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'un utilisateur
|
||||
*
|
||||
* @param int $userId ID utilisateur supprimé
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logUserDeleted(int $userId, bool $softDelete = true): void
|
||||
{
|
||||
$deletedBy = Session::getUserId();
|
||||
$entityId = Session::getEntityId();
|
||||
|
||||
self::writeEvent('user_deleted', [
|
||||
'user_id' => $userId,
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES ENTITÉS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité créée
|
||||
* @param int $entityTypeId Type d'entité
|
||||
* @param string $postalCode Code postal
|
||||
*/
|
||||
public static function logEntityCreated(int $entityId, int $entityTypeId, string $postalCode): void
|
||||
{
|
||||
$createdBy = Session::getUserId() ?? 1; // Super-admin par défaut
|
||||
self::writeEvent('entity_created', [
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'entity_type_id' => $entityTypeId,
|
||||
'postal_code' => $postalCode
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité
|
||||
* @param array $changes Tableau des changements (booléen pour champs chiffrés)
|
||||
*/
|
||||
public static function logEntityUpdated(int $entityId, array $changes): void
|
||||
{
|
||||
$updatedBy = Session::getUserId();
|
||||
self::writeEvent('entity_updated', [
|
||||
'entity_id' => $entityId,
|
||||
'user_id' => $updatedBy,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'une entité
|
||||
*
|
||||
* @param int $entityId ID entité
|
||||
* @param string $reason Raison de la suppression
|
||||
*/
|
||||
public static function logEntityDeleted(int $entityId, string $reason = ''): void
|
||||
{
|
||||
$deletedBy = Session::getUserId() ?? 1;
|
||||
self::writeEvent('entity_deleted', [
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => true,
|
||||
'reason' => $reason
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES OPÉRATIONS ====================
|
||||
|
||||
/**
|
||||
* Log la création d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération créée
|
||||
* @param string $dateStart Date début (YYYY-MM-DD)
|
||||
* @param string $dateEnd Date fin (YYYY-MM-DD)
|
||||
*/
|
||||
public static function logOperationCreated(int $operationId, string $dateStart, string $dateEnd): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$createdBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_created', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'created_by' => $createdBy,
|
||||
'date_start' => $dateStart,
|
||||
'date_end' => $dateEnd
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la modification d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération
|
||||
* @param array $changes Tableau des changements
|
||||
*/
|
||||
public static function logOperationUpdated(int $operationId, array $changes): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$updatedBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_updated', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'updated_by' => $updatedBy,
|
||||
'changes' => $changes
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Log la suppression d'une opération
|
||||
*
|
||||
* @param int $operationId ID opération
|
||||
* @param bool $softDelete Suppression logique ou physique
|
||||
*/
|
||||
public static function logOperationDeleted(int $operationId, bool $softDelete = true): void
|
||||
{
|
||||
$entityId = Session::getEntityId();
|
||||
$deletedBy = Session::getUserId();
|
||||
|
||||
self::writeEvent('operation_deleted', [
|
||||
'operation_id' => $operationId,
|
||||
'entity_id' => $entityId,
|
||||
'deleted_by' => $deletedBy,
|
||||
'soft_delete' => $softDelete
|
||||
]);
|
||||
}
|
||||
|
||||
// ==================== MÉTHODES PRIVÉES ====================
|
||||
|
||||
/**
|
||||
* Méthode centrale d'écriture d'un événement
|
||||
*
|
||||
* @param string $eventName Nom de l'événement
|
||||
* @param array $data Données spécifiques à l'événement
|
||||
*/
|
||||
private static function writeEvent(string $eventName, array $data): void
|
||||
{
|
||||
try {
|
||||
// Enrichir avec timestamp, user_id, entity_id, IP, platform, app_version
|
||||
$event = self::enrichEvent($eventName, $data);
|
||||
|
||||
// Générer le chemin du fichier quotidien
|
||||
$filename = self::LOG_DIR . '/' . date('Y-m-d') . '.jsonl';
|
||||
|
||||
// Créer le dossier si nécessaire
|
||||
self::ensureLogDirectoryExists();
|
||||
|
||||
// Encoder en JSON compact (une ligne)
|
||||
$jsonLine = json_encode($event, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
|
||||
|
||||
// Écrire en mode append
|
||||
if (file_put_contents($filename, $jsonLine, FILE_APPEND | LOCK_EX) === false) {
|
||||
error_log("EventLogService: Impossible d'écrire dans {$filename}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Appliquer les permissions au fichier
|
||||
if (file_exists($filename)) {
|
||||
@chmod($filename, self::FILE_PERMISSIONS);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Ne jamais bloquer l'application si le logging échoue
|
||||
error_log("EventLogService: Erreur lors de l'écriture de l'événement {$eventName}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enrichit un événement avec les métadonnées communes
|
||||
*
|
||||
* @param string $eventName Nom de l'événement
|
||||
* @param array $data Données de l'événement
|
||||
* @return array Événement enrichi
|
||||
*/
|
||||
private static function enrichEvent(string $eventName, array $data): array
|
||||
{
|
||||
// Récupérer les informations client
|
||||
$clientInfo = ClientDetector::getClientInfo();
|
||||
|
||||
// Structure de base
|
||||
$event = [
|
||||
'timestamp' => gmdate('Y-m-d\TH:i:s\Z'), // ISO 8601 UTC
|
||||
'event' => $eventName,
|
||||
];
|
||||
|
||||
// Ajouter user_id si disponible et pas déjà dans $data
|
||||
if (!isset($data['user_id'])) {
|
||||
$userId = Session::getUserId();
|
||||
if ($userId !== null) {
|
||||
$event['user_id'] = $userId;
|
||||
}
|
||||
}
|
||||
|
||||
// Ajouter entity_id si disponible et pas déjà dans $data
|
||||
if (!isset($data['entity_id'])) {
|
||||
$entityId = Session::getEntityId();
|
||||
if ($entityId !== null) {
|
||||
$event['entity_id'] = $entityId;
|
||||
}
|
||||
}
|
||||
|
||||
// Fusionner avec les données spécifiques
|
||||
$event = array_merge($event, $data);
|
||||
|
||||
// Ajouter IP
|
||||
$event['ip'] = $clientInfo['ip'];
|
||||
|
||||
// Ajouter platform
|
||||
$event['platform'] = self::getPlatform($clientInfo);
|
||||
|
||||
// Ajouter app_version si mobile
|
||||
if ($event['platform'] === 'ios' || $event['platform'] === 'android') {
|
||||
$appVersion = self::getAppVersion($clientInfo);
|
||||
if ($appVersion !== null) {
|
||||
$event['app_version'] = $appVersion;
|
||||
}
|
||||
}
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
/**
|
||||
* Détermine la plateforme (ios, android, web)
|
||||
*
|
||||
* @param array $clientInfo Informations client de ClientDetector
|
||||
* @return string Platform (ios|android|web)
|
||||
*/
|
||||
private static function getPlatform(array $clientInfo): string
|
||||
{
|
||||
if ($clientInfo['type'] !== 'mobile') {
|
||||
return 'web';
|
||||
}
|
||||
|
||||
$userAgent = $clientInfo['userAgent'];
|
||||
|
||||
// Détection iOS
|
||||
if (stripos($userAgent, 'iOS') !== false ||
|
||||
stripos($userAgent, 'iPhone') !== false ||
|
||||
stripos($userAgent, 'iPad') !== false) {
|
||||
return 'ios';
|
||||
}
|
||||
|
||||
// Détection Android
|
||||
if (stripos($userAgent, 'Android') !== false) {
|
||||
return 'android';
|
||||
}
|
||||
|
||||
// Par défaut mobile générique = web
|
||||
return 'web';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrait la version de l'application depuis le User-Agent
|
||||
* Format attendu: AppName/VersionNumber ou Platform/Version AppName/Version
|
||||
*
|
||||
* @param array $clientInfo Informations client
|
||||
* @return string|null Version de l'app ou null
|
||||
*/
|
||||
private static function getAppVersion(array $clientInfo): ?string
|
||||
{
|
||||
$userAgent = $clientInfo['userAgent'];
|
||||
|
||||
// Tentative extraction format: GeoSector/3.3.6
|
||||
if (preg_match('/GeoSector\/([0-9\.]+)/', $userAgent, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
// Format alternatif: AppName/Version
|
||||
if (preg_match('/([A-Za-z0-9_]+)\/([0-9\.]+)/', $userAgent, $matches)) {
|
||||
return $matches[2];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée le dossier de logs si nécessaire avec les bonnes permissions
|
||||
*/
|
||||
private static function ensureLogDirectoryExists(): void
|
||||
{
|
||||
if (!is_dir(self::LOG_DIR)) {
|
||||
if (!@mkdir(self::LOG_DIR, self::DIR_PERMISSIONS, true)) {
|
||||
error_log("EventLogService: Impossible de créer le dossier " . self::LOG_DIR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Vérifier les permissions
|
||||
if (!is_writable(self::LOG_DIR)) {
|
||||
@chmod(self::LOG_DIR, self::DIR_PERMISSIONS);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user