import hashlib import platform import uuid import requests from datetime import datetime from typing import Optional, Dict from PyQt6.QtCore import QObject, pyqtSignal import os from dotenv import load_dotenv import app.utils.paths as paths # Load environment variables load_dotenv(paths.resource_path(".env")) class LicenseManager(QObject): """Gestionnaire de licences avec liaison matérielle""" license_activated = pyqtSignal(str) # Signal émis lors de l'activation license_expired = pyqtSignal() def __init__(self, settings_manager): super().__init__() self.settings_manager = settings_manager self.licensing_enabled = settings_manager.get_config("enable_licensing") # Si le système de licence est désactivé, initialiser en mode gratuit if not self.licensing_enabled: self.hardware_id = None self.license_key = "" self.license_data = None return # Charger l'URL de l'API depuis .env self.api_url = os.getenv("LICENSE_API_URL") if not self.api_url: raise ValueError("LICENSE_API_URL non définie dans le fichier .env") self.hardware_id = self._get_hardware_id() # Charger la licence sauvegardée self.license_key = settings_manager.settings.value("license_key", "") self.license_data = None if self.license_key: self._load_license_data() def _get_hardware_id(self) -> str: """Génère un identifiant unique basé sur le matériel""" # Combine plusieurs éléments matériels pour un ID unique mac = ':'.join(['{:02x}'.format((uuid.getnode() >> elements) & 0xff) for elements in range(0, 2*6, 2)][::-1]) system = platform.system() machine = platform.machine() processor = platform.processor() # Créer un hash unique unique_string = f"{mac}{system}{machine}{processor}" return hashlib.sha256(unique_string.encode()).hexdigest() def get_hardware_id(self) -> str: """Retourne le hardware ID pour l'afficher à l'utilisateur""" return self.hardware_id def activate_license(self, license_key: str) -> Dict: """ Active une licence avec le serveur Returns: dict: {"success": bool, "message": str, "data": dict} """ try: response = requests.post( f"{self.api_url}/activate", json={ "license_key": license_key, "hardware_id": self.hardware_id, "app_version": self.settings_manager.get_config("app_version"), "platform": platform.system() }, timeout=10 ) data = response.json() if response.status_code == 200 and data.get("success"): # Sauvegarder la licence self.license_key = license_key self.license_data = data.get("license_data", {}) self.settings_manager.settings.setValue("license_key", license_key) self.settings_manager.settings.setValue("license_data", self.license_data) self.license_activated.emit(license_key) return { "success": True, "message": "Licence activée avec succès", "data": self.license_data } else: return { "success": False, "message": data.get("message", "Erreur d'activation"), "data": None } except requests.exceptions.RequestException as e: return { "success": False, "message": f"Erreur de connexion au serveur: {str(e)}", "data": None } def verify_license(self) -> bool: """Vérifie la validité de la licence avec le serveur""" if not self.license_key: return False try: response = requests.post( f"{self.api_url}/verify", json={ "license_key": self.license_key, "hardware_id": self.hardware_id }, timeout=10 ) data = response.json() if response.status_code == 200 and data.get("valid"): self.license_data = data.get("license_data", {}) self.settings_manager.settings.setValue("license_data", self.license_data) return True else: return False except requests.exceptions.RequestException: # En cas d'erreur réseau, utiliser les données en cache return self._verify_offline() def _verify_offline(self) -> bool: """Vérification hors ligne basique""" if not self.license_key or not self.license_data: return False # Vérifier que le hardware_id correspond if self.license_data.get("hardware_id") != self.hardware_id: return False # Vérifier la date d'expiration si applicable expires_at = self.license_data.get("expires_at") if expires_at: expiry_date = datetime.fromisoformat(expires_at) if datetime.now() > expiry_date: self.license_expired.emit() return False return True def _load_license_data(self): """Charge les données de licence depuis les settings""" self.license_data = self.settings_manager.settings.value("license_data", {}) def is_activated(self) -> bool: """Vérifie si l'application est activée""" # Si le système de licence est désactivé, toujours retourner False if not self.licensing_enabled: return False return bool(self.license_key) and self.verify_license() def get_license_type(self) -> str: """Retourne le type de licence (free, premium, etc.)""" # Si le système de licence est désactivé, retourner "free" if not self.licensing_enabled: return "free" if not self.license_data: return "free" return self.license_data.get("type", "free") def is_feature_available(self, feature_id: str) -> bool: """ Vérifie si une fonctionnalité est disponible Args: feature_id: Identifiant de la fonctionnalité (ex: "advanced_export") """ # Si le système de licence est désactivé, toutes les fonctionnalités sont disponibles if not self.licensing_enabled: return True # Si pas de licence, vérifier dans la config des features gratuites if not self.is_activated(): free_features = self.settings_manager.get_config("features_by_license").get("free", []) return feature_id in free_features # Vérifier les features autorisées par la licence license_type = self.get_license_type() features_by_type = self.settings_manager.get_config("features_by_license") allowed_features = features_by_type.get(license_type, []) return feature_id in allowed_features or feature_id in self.settings_manager.get_config("free_features") def get_license_info(self) -> Dict: """Retourne les informations de la licence""" if not self.license_data: return { "type": "free", "status": "inactive", "expires_at": None } return { "type": self.get_license_type(), "status": "active" if self.is_activated() else "inactive", "expires_at": self.license_data.get("expires_at"), "email": self.license_data.get("email"), "activated_at": self.license_data.get("activated_at") }