PythonApplicationTemplate/app/core/license_manager.py
2025-10-30 18:10:23 +01:00

220 lines
8.0 KiB
Python

import hashlib
import platform
import uuid
import requests
from datetime import datetime
from typing import 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 None
if not self.license_data:
return None
return self.license_data.get("type", None)
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(None, [])
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
def get_license_info(self) -> Dict:
"""Retourne les informations de la licence"""
if not self.license_data:
return {
"type": None,
"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")
}