Files
geo/api/src/Services/EventLogService.php
Pierre 232940b1eb feat: Version 3.6.2 - Correctifs tâches #17-20
- #17: Amélioration gestion des secteurs et statistiques
- #18: Optimisation services API et logs
- #19: Corrections Flutter widgets et repositories
- #20: Fix création passage - détection automatique ope_users.id vs users.id

Suppression dossier web/ (migration vers app Flutter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 14:11:15 +01:00

669 lines
21 KiB
PHP

<?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 STRIPE ====================
/**
* Log la création d'un PaymentIntent
*
* @param string $paymentIntentId ID Stripe du PaymentIntent
* @param int $passageId ID du passage
* @param int $amount Montant en centimes
* @param string $method Méthode (tap_to_pay, qr_code, web)
*/
public static function logStripePaymentCreated(
string $paymentIntentId,
int $passageId,
int $amount,
string $method
): void {
$entityId = Session::getEntityId();
self::writeEvent('stripe_payment_created', [
'payment_intent_id' => $paymentIntentId,
'passage_id' => $passageId,
'entity_id' => $entityId,
'amount' => $amount,
'method' => $method
]);
}
/**
* Log un paiement Stripe réussi
*
* @param string $paymentIntentId ID Stripe du PaymentIntent
* @param int $passageId ID du passage
* @param int $amount Montant en centimes
* @param string $method Méthode (tap_to_pay, qr_code, web)
*/
public static function logStripePaymentSuccess(
string $paymentIntentId,
int $passageId,
int $amount,
string $method
): void {
$entityId = Session::getEntityId();
self::writeEvent('stripe_payment_success', [
'payment_intent_id' => $paymentIntentId,
'passage_id' => $passageId,
'entity_id' => $entityId,
'amount' => $amount,
'method' => $method
]);
}
/**
* Log un paiement Stripe échoué
*
* @param string|null $paymentIntentId ID Stripe (peut être null si création échouée)
* @param int|null $passageId ID du passage (peut être null)
* @param int|null $amount Montant en centimes (peut être null)
* @param string $method Méthode tentée
* @param string $errorCode Code d'erreur
* @param string $errorMessage Message d'erreur
*/
public static function logStripePaymentFailed(
?string $paymentIntentId,
?int $passageId,
?int $amount,
string $method,
string $errorCode,
string $errorMessage
): void {
$entityId = Session::getEntityId();
$data = [
'entity_id' => $entityId,
'method' => $method,
'error_code' => $errorCode,
'error_message' => $errorMessage
];
if ($paymentIntentId !== null) {
$data['payment_intent_id'] = $paymentIntentId;
}
if ($passageId !== null) {
$data['passage_id'] = $passageId;
}
if ($amount !== null) {
$data['amount'] = $amount;
}
self::writeEvent('stripe_payment_failed', $data);
}
/**
* Log l'annulation d'un paiement Stripe
*
* @param string $paymentIntentId ID Stripe du PaymentIntent
* @param int|null $passageId ID du passage (peut être null)
* @param string $reason Raison (user_cancelled, timeout, error, etc.)
*/
public static function logStripePaymentCancelled(
string $paymentIntentId,
?int $passageId,
string $reason
): void {
$entityId = Session::getEntityId();
$data = [
'payment_intent_id' => $paymentIntentId,
'entity_id' => $entityId,
'reason' => $reason
];
if ($passageId !== null) {
$data['passage_id'] = $passageId;
}
self::writeEvent('stripe_payment_cancelled', $data);
}
/**
* Log une erreur du Terminal Tap to Pay
*
* @param string $errorCode Code d'erreur (cardReadTimedOut, device_not_compatible, etc.)
* @param string $errorMessage Message d'erreur
* @param array $metadata Métadonnées supplémentaires (device_model, is_simulated, etc.)
*/
public static function logStripeTerminalError(
string $errorCode,
string $errorMessage,
array $metadata = []
): void {
$entityId = Session::getEntityId();
self::writeEvent('stripe_terminal_error', array_merge([
'entity_id' => $entityId,
'error_code' => $errorCode,
'error_message' => $errorMessage
], $metadata));
}
// ==================== 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);
}
}
}