Initial commit
Dieser Commit ist enthalten in:
94
lizenzserver/repositories/base.py
Normale Datei
94
lizenzserver/repositories/base.py
Normale Datei
@ -0,0 +1,94 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional, List, Dict, Any
|
||||
import psycopg2
|
||||
from psycopg2.extras import RealDictCursor
|
||||
from contextlib import contextmanager
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class BaseRepository(ABC):
|
||||
"""Base repository with common database operations"""
|
||||
|
||||
def __init__(self, db_url: str):
|
||||
self.db_url = db_url
|
||||
|
||||
@contextmanager
|
||||
def get_db_connection(self):
|
||||
"""Get database connection with automatic cleanup"""
|
||||
conn = None
|
||||
try:
|
||||
conn = psycopg2.connect(self.db_url)
|
||||
yield conn
|
||||
except Exception as e:
|
||||
if conn:
|
||||
conn.rollback()
|
||||
logger.error(f"Database error: {e}")
|
||||
raise
|
||||
finally:
|
||||
if conn:
|
||||
conn.close()
|
||||
|
||||
@contextmanager
|
||||
def get_db_cursor(self, conn):
|
||||
"""Get database cursor with dict results"""
|
||||
cursor = None
|
||||
try:
|
||||
cursor = conn.cursor(cursor_factory=RealDictCursor)
|
||||
yield cursor
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
|
||||
def execute_query(self, query: str, params: tuple = None) -> List[Dict[str, Any]]:
|
||||
"""Execute SELECT query and return results"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
def execute_one(self, query: str, params: tuple = None) -> Optional[Dict[str, Any]]:
|
||||
"""Execute query and return single result"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchone()
|
||||
|
||||
def execute_insert(self, query: str, params: tuple = None) -> Optional[str]:
|
||||
"""Execute INSERT query and return ID"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
cursor.execute(query + " RETURNING id", params)
|
||||
result = cursor.fetchone()
|
||||
conn.commit()
|
||||
return result['id'] if result else None
|
||||
|
||||
def execute_update(self, query: str, params: tuple = None) -> int:
|
||||
"""Execute UPDATE query and return affected rows"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
cursor.execute(query, params)
|
||||
affected = cursor.rowcount
|
||||
conn.commit()
|
||||
return affected
|
||||
|
||||
def execute_delete(self, query: str, params: tuple = None) -> int:
|
||||
"""Execute DELETE query and return affected rows"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
cursor.execute(query, params)
|
||||
affected = cursor.rowcount
|
||||
conn.commit()
|
||||
return affected
|
||||
|
||||
def execute_batch(self, queries: List[tuple]) -> None:
|
||||
"""Execute multiple queries in a transaction"""
|
||||
with self.get_db_connection() as conn:
|
||||
with self.get_db_cursor(conn) as cursor:
|
||||
try:
|
||||
for query, params in queries:
|
||||
cursor.execute(query, params)
|
||||
conn.commit()
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
raise
|
||||
178
lizenzserver/repositories/cache_repo.py
Normale Datei
178
lizenzserver/repositories/cache_repo.py
Normale Datei
@ -0,0 +1,178 @@
|
||||
import redis
|
||||
import json
|
||||
import logging
|
||||
from typing import Optional, Any, Dict, List
|
||||
from datetime import timedelta
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CacheRepository:
|
||||
"""Redis cache repository"""
|
||||
|
||||
def __init__(self, redis_url: str):
|
||||
self.redis_url = redis_url
|
||||
self._connect()
|
||||
|
||||
def _connect(self):
|
||||
"""Connect to Redis"""
|
||||
try:
|
||||
self.redis = redis.from_url(self.redis_url, decode_responses=True)
|
||||
self.redis.ping()
|
||||
logger.info("Connected to Redis")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to Redis: {e}")
|
||||
self.redis = None
|
||||
|
||||
def _make_key(self, prefix: str, *args) -> str:
|
||||
"""Create cache key"""
|
||||
parts = [prefix] + [str(arg) for arg in args]
|
||||
return ":".join(parts)
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""Get value from cache"""
|
||||
if not self.redis:
|
||||
return None
|
||||
|
||||
try:
|
||||
value = self.redis.get(key)
|
||||
if value:
|
||||
return json.loads(value)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Cache get error: {e}")
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any, ttl: int = 300) -> bool:
|
||||
"""Set value in cache with TTL in seconds"""
|
||||
if not self.redis:
|
||||
return False
|
||||
|
||||
try:
|
||||
json_value = json.dumps(value)
|
||||
return self.redis.setex(key, ttl, json_value)
|
||||
except Exception as e:
|
||||
logger.error(f"Cache set error: {e}")
|
||||
return False
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""Delete key from cache"""
|
||||
if not self.redis:
|
||||
return False
|
||||
|
||||
try:
|
||||
return bool(self.redis.delete(key))
|
||||
except Exception as e:
|
||||
logger.error(f"Cache delete error: {e}")
|
||||
return False
|
||||
|
||||
def delete_pattern(self, pattern: str) -> int:
|
||||
"""Delete all keys matching pattern"""
|
||||
if not self.redis:
|
||||
return 0
|
||||
|
||||
try:
|
||||
keys = self.redis.keys(pattern)
|
||||
if keys:
|
||||
return self.redis.delete(*keys)
|
||||
return 0
|
||||
except Exception as e:
|
||||
logger.error(f"Cache delete pattern error: {e}")
|
||||
return 0
|
||||
|
||||
# License-specific cache methods
|
||||
|
||||
def get_license_validation(self, license_key: str, hardware_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached license validation result"""
|
||||
key = self._make_key("license:validation", license_key, hardware_id)
|
||||
return self.get(key)
|
||||
|
||||
def set_license_validation(self, license_key: str, hardware_id: str,
|
||||
result: Dict[str, Any], ttl: int = 300) -> bool:
|
||||
"""Cache license validation result"""
|
||||
key = self._make_key("license:validation", license_key, hardware_id)
|
||||
return self.set(key, result, ttl)
|
||||
|
||||
def get_license_status(self, license_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Get cached license status"""
|
||||
key = self._make_key("license:status", license_id)
|
||||
return self.get(key)
|
||||
|
||||
def set_license_status(self, license_id: str, status: Dict[str, Any],
|
||||
ttl: int = 60) -> bool:
|
||||
"""Cache license status"""
|
||||
key = self._make_key("license:status", license_id)
|
||||
return self.set(key, status, ttl)
|
||||
|
||||
def get_device_list(self, license_id: str) -> Optional[List[Dict[str, Any]]]:
|
||||
"""Get cached device list"""
|
||||
key = self._make_key("license:devices", license_id)
|
||||
return self.get(key)
|
||||
|
||||
def set_device_list(self, license_id: str, devices: List[Dict[str, Any]],
|
||||
ttl: int = 300) -> bool:
|
||||
"""Cache device list"""
|
||||
key = self._make_key("license:devices", license_id)
|
||||
return self.set(key, devices, ttl)
|
||||
|
||||
def invalidate_license_cache(self, license_id: str) -> None:
|
||||
"""Invalidate all cache entries for a license"""
|
||||
patterns = [
|
||||
f"license:validation:*:{license_id}",
|
||||
f"license:status:{license_id}",
|
||||
f"license:devices:{license_id}"
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
self.delete_pattern(pattern)
|
||||
|
||||
# Rate limiting methods
|
||||
|
||||
def check_rate_limit(self, key: str, limit: int, window: int) -> tuple[bool, int]:
|
||||
"""Check if rate limit is exceeded
|
||||
Returns: (is_allowed, current_count)
|
||||
"""
|
||||
if not self.redis:
|
||||
return True, 0
|
||||
|
||||
try:
|
||||
pipe = self.redis.pipeline()
|
||||
now = int(time.time())
|
||||
window_start = now - window
|
||||
|
||||
# Remove old entries
|
||||
pipe.zremrangebyscore(key, 0, window_start)
|
||||
|
||||
# Count requests in current window
|
||||
pipe.zcard(key)
|
||||
|
||||
# Add current request
|
||||
pipe.zadd(key, {str(now): now})
|
||||
|
||||
# Set expiry
|
||||
pipe.expire(key, window + 1)
|
||||
|
||||
results = pipe.execute()
|
||||
current_count = results[1]
|
||||
|
||||
return current_count < limit, current_count + 1
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Rate limit check error: {e}")
|
||||
return True, 0
|
||||
|
||||
def increment_counter(self, key: str, window: int = 3600) -> int:
|
||||
"""Increment counter with expiry"""
|
||||
if not self.redis:
|
||||
return 0
|
||||
|
||||
try:
|
||||
pipe = self.redis.pipeline()
|
||||
pipe.incr(key)
|
||||
pipe.expire(key, window)
|
||||
results = pipe.execute()
|
||||
return results[0]
|
||||
except Exception as e:
|
||||
logger.error(f"Counter increment error: {e}")
|
||||
return 0
|
||||
|
||||
import time # Add this import at the top
|
||||
228
lizenzserver/repositories/license_repo.py
Normale Datei
228
lizenzserver/repositories/license_repo.py
Normale Datei
@ -0,0 +1,228 @@
|
||||
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 {}
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren