from typing import Optional, List, Dict, Any from datetime import datetime, timedelta from .base import BaseRepository from ..models import License, LicenseToken, ActivationEvent, EventType import logging logger = logging.getLogger(__name__) class LicenseRepository(BaseRepository): """Repository for license-related database operations""" def get_license_by_key(self, license_key: str) -> Optional[Dict[str, Any]]: """Get license by key""" query = """ SELECT l.*, c.name as customer_name, c.email as customer_email FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.license_key = %s """ return self.execute_one(query, (license_key,)) def get_license_by_id(self, license_id: str) -> Optional[Dict[str, Any]]: """Get license by ID""" query = """ SELECT l.*, c.name as customer_name, c.email as customer_email FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.id = %s """ return self.execute_one(query, (license_id,)) def get_active_devices(self, license_id: str) -> List[Dict[str, Any]]: """Get active devices for a license""" query = """ SELECT DISTINCT ON (hardware_id) hardware_id, ip_address, user_agent, app_version, timestamp as last_seen FROM license_heartbeats WHERE license_id = %s AND timestamp > NOW() - INTERVAL '15 minutes' ORDER BY hardware_id, timestamp DESC """ return self.execute_query(query, (license_id,)) def get_device_count(self, license_id: str) -> int: """Get count of active devices""" query = """ SELECT COUNT(DISTINCT hardware_id) as device_count FROM license_heartbeats WHERE license_id = %s AND timestamp > NOW() - INTERVAL '15 minutes' """ result = self.execute_one(query, (license_id,)) return result['device_count'] if result else 0 def create_license_token(self, license_id: str, hardware_id: str, valid_hours: int = 24) -> Optional[str]: """Create offline validation token""" import secrets token = secrets.token_urlsafe(64) valid_until = datetime.utcnow() + timedelta(hours=valid_hours) query = """ INSERT INTO license_tokens (license_id, token, hardware_id, valid_until) VALUES (%s, %s, %s, %s) RETURNING id """ result = self.execute_insert(query, (license_id, token, hardware_id, valid_until)) return token if result else None def validate_token(self, token: str) -> Optional[Dict[str, Any]]: """Validate offline token""" query = """ SELECT lt.*, l.license_key, l.is_active, l.expires_at FROM license_tokens lt JOIN licenses l ON lt.license_id = l.id WHERE lt.token = %s AND lt.valid_until > NOW() AND l.is_active = true """ result = self.execute_one(query, (token,)) if result: # Update validation count and timestamp update_query = """ UPDATE license_tokens SET validation_count = validation_count + 1, last_validated = NOW() WHERE token = %s """ self.execute_update(update_query, (token,)) return result def record_heartbeat(self, license_id: str, hardware_id: str, ip_address: str = None, user_agent: str = None, app_version: str = None, session_data: Dict = None) -> None: """Record license heartbeat""" query = """ INSERT INTO license_heartbeats (license_id, hardware_id, ip_address, user_agent, app_version, session_data) VALUES (%s, %s, %s, %s, %s, %s) """ import json session_json = json.dumps(session_data) if session_data else None self.execute_insert(query, ( license_id, hardware_id, ip_address, user_agent, app_version, session_json )) def record_activation_event(self, license_id: str, event_type: EventType, hardware_id: str = None, previous_hardware_id: str = None, ip_address: str = None, user_agent: str = None, success: bool = True, error_message: str = None, metadata: Dict = None) -> str: """Record activation event""" query = """ INSERT INTO activation_events (license_id, event_type, hardware_id, previous_hardware_id, ip_address, user_agent, success, error_message, metadata) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id """ import json metadata_json = json.dumps(metadata) if metadata else None return self.execute_insert(query, ( license_id, event_type.value, hardware_id, previous_hardware_id, ip_address, user_agent, success, error_message, metadata_json )) def get_recent_activations(self, license_id: str, hours: int = 24) -> List[Dict[str, Any]]: """Get recent activation events""" query = """ SELECT * FROM activation_events WHERE license_id = %s AND created_at > NOW() - INTERVAL '%s hours' ORDER BY created_at DESC """ return self.execute_query(query, (license_id, hours)) def check_hardware_id_exists(self, license_id: str, hardware_id: str) -> bool: """Check if hardware ID is already registered""" query = """ SELECT 1 FROM activation_events WHERE license_id = %s AND hardware_id = %s AND event_type IN ('activation', 'reactivation') AND success = true LIMIT 1 """ result = self.execute_one(query, (license_id, hardware_id)) return result is not None def deactivate_device(self, license_id: str, hardware_id: str) -> bool: """Deactivate a device""" # Record deactivation event self.record_activation_event( license_id=license_id, event_type=EventType.DEACTIVATION, hardware_id=hardware_id, success=True ) # Remove any active tokens for this device query = """ DELETE FROM license_tokens WHERE license_id = %s AND hardware_id = %s """ affected = self.execute_delete(query, (license_id, hardware_id)) return affected > 0 def transfer_license(self, license_id: str, from_hardware_id: str, to_hardware_id: str, ip_address: str = None) -> bool: """Transfer license from one device to another""" try: # Deactivate old device self.deactivate_device(license_id, from_hardware_id) # Record transfer event self.record_activation_event( license_id=license_id, event_type=EventType.TRANSFER, hardware_id=to_hardware_id, previous_hardware_id=from_hardware_id, ip_address=ip_address, success=True ) return True except Exception as e: logger.error(f"License transfer failed: {e}") return False def get_license_usage_stats(self, license_id: str, days: int = 30) -> Dict[str, Any]: """Get usage statistics for a license""" query = """ WITH daily_stats AS ( SELECT DATE(timestamp) as date, COUNT(*) as validations, COUNT(DISTINCT hardware_id) as unique_devices, COUNT(DISTINCT ip_address) as unique_ips FROM license_heartbeats WHERE license_id = %s AND timestamp > NOW() - INTERVAL '%s days' GROUP BY DATE(timestamp) ) SELECT COUNT(*) as total_days, SUM(validations) as total_validations, AVG(validations) as avg_daily_validations, MAX(unique_devices) as max_devices, MAX(unique_ips) as max_ips FROM daily_stats """ return self.execute_one(query, (license_id, days)) or {}