Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1 @@
# Admin API Service

Datei anzeigen

@ -0,0 +1,666 @@
import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS
import logging
from functools import wraps
from marshmallow import Schema, fields, ValidationError
from datetime import datetime, timedelta
import secrets
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from config import get_config
from repositories.license_repo import LicenseRepository
from repositories.cache_repo import CacheRepository
from events.event_bus import EventBus, Event, EventTypes
from models import EventType, AnomalyType, Severity
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
config = get_config()
app.config.from_object(config)
CORS(app)
# Initialize dependencies
license_repo = LicenseRepository(config.DATABASE_URL)
cache_repo = CacheRepository(config.REDIS_URL)
event_bus = EventBus(config.RABBITMQ_URL)
# Validation schemas
class CreateLicenseSchema(Schema):
customer_id = fields.Str(required=True)
max_devices = fields.Int(missing=1, validate=lambda x: x > 0)
expires_in_days = fields.Int(allow_none=True)
features = fields.List(fields.Str(), missing=[])
is_test = fields.Bool(missing=False)
metadata = fields.Dict(missing={})
class UpdateLicenseSchema(Schema):
max_devices = fields.Int(validate=lambda x: x > 0)
is_active = fields.Bool()
expires_at = fields.DateTime()
features = fields.List(fields.Str())
metadata = fields.Dict()
class DeactivateDeviceSchema(Schema):
hardware_id = fields.Str(required=True)
reason = fields.Str()
class TransferLicenseSchema(Schema):
from_hardware_id = fields.Str(required=True)
to_hardware_id = fields.Str(required=True)
class SearchLicensesSchema(Schema):
customer_id = fields.Str()
is_active = fields.Bool()
is_test = fields.Bool()
created_after = fields.DateTime()
created_before = fields.DateTime()
expires_after = fields.DateTime()
expires_before = fields.DateTime()
page = fields.Int(missing=1, validate=lambda x: x > 0)
per_page = fields.Int(missing=50, validate=lambda x: 0 < x <= 100)
def require_admin_auth(f):
"""Decorator to require admin authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
# Check for admin API key
api_key = request.headers.get('X-Admin-API-Key')
if not api_key:
return jsonify({"error": "Missing admin API key"}), 401
# In production, validate against database
# For now, check environment variable
if api_key != os.getenv('ADMIN_API_KEY', 'admin-key-change-in-production'):
return jsonify({"error": "Invalid admin API key"}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "admin-api",
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/admin/licenses', methods=['POST'])
@require_admin_auth
def create_license():
"""Create new license"""
schema = CreateLicenseSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
# Generate license key
license_key = f"LIC-{secrets.token_urlsafe(16).upper()}"
# Calculate expiration
expires_at = None
if data.get('expires_in_days'):
expires_at = datetime.utcnow() + timedelta(days=data['expires_in_days'])
# Create license in database
query = """
INSERT INTO licenses
(license_key, customer_id, max_devices, is_active, is_test, expires_at, features, metadata)
VALUES (%s, %s, %s, true, %s, %s, %s, %s)
RETURNING id
"""
import json
license_id = license_repo.execute_insert(query, (
license_key,
data['customer_id'],
data['max_devices'],
data['is_test'],
expires_at,
json.dumps(data['features']),
json.dumps(data['metadata'])
))
if not license_id:
return jsonify({"error": "Failed to create license"}), 500
# Publish event
event_bus.publish(Event(
EventTypes.LICENSE_CREATED,
{
"license_id": license_id,
"customer_id": data['customer_id'],
"license_key": license_key
},
"admin-api"
))
return jsonify({
"id": license_id,
"license_key": license_key,
"customer_id": data['customer_id'],
"max_devices": data['max_devices'],
"is_test": data['is_test'],
"expires_at": expires_at.isoformat() if expires_at else None,
"features": data['features']
}), 201
@app.route('/api/v1/admin/licenses/<license_id>', methods=['GET'])
@require_admin_auth
def get_license(license_id):
"""Get license details"""
license = license_repo.get_license_by_id(license_id)
if not license:
return jsonify({"error": "License not found"}), 404
# Get additional statistics
active_devices = license_repo.get_active_devices(license_id)
usage_stats = license_repo.get_license_usage_stats(license_id)
recent_events = license_repo.get_recent_activations(license_id)
# Format response
license['active_devices'] = active_devices
license['usage_stats'] = usage_stats
license['recent_events'] = recent_events
return jsonify(license)
@app.route('/api/v1/admin/licenses/<license_id>', methods=['PATCH'])
@require_admin_auth
def update_license(license_id):
"""Update license"""
schema = UpdateLicenseSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
# Build update query dynamically
updates = []
params = []
if 'max_devices' in data:
updates.append("max_devices = %s")
params.append(data['max_devices'])
if 'is_active' in data:
updates.append("is_active = %s")
params.append(data['is_active'])
if 'expires_at' in data:
updates.append("expires_at = %s")
params.append(data['expires_at'])
if 'features' in data:
updates.append("features = %s")
params.append(json.dumps(data['features']))
if 'metadata' in data:
updates.append("metadata = %s")
params.append(json.dumps(data['metadata']))
if not updates:
return jsonify({"error": "No fields to update"}), 400
# Add updated_at
updates.append("updated_at = NOW()")
# Add license_id to params
params.append(license_id)
query = f"""
UPDATE licenses
SET {', '.join(updates)}
WHERE id = %s
RETURNING *
"""
result = license_repo.execute_one(query, params)
if not result:
return jsonify({"error": "License not found"}), 404
# Invalidate cache
cache_repo.invalidate_license_cache(license_id)
# Publish event
event_bus.publish(Event(
EventTypes.LICENSE_UPDATED,
{
"license_id": license_id,
"changes": list(data.keys())
},
"admin-api"
))
return jsonify(result)
@app.route('/api/v1/admin/licenses/<license_id>', methods=['DELETE'])
@require_admin_auth
def delete_license(license_id):
"""Soft delete license (deactivate)"""
query = """
UPDATE licenses
SET is_active = false, updated_at = NOW()
WHERE id = %s
RETURNING id
"""
result = license_repo.execute_one(query, (license_id,))
if not result:
return jsonify({"error": "License not found"}), 404
# Invalidate cache
cache_repo.invalidate_license_cache(license_id)
# Publish event
event_bus.publish(Event(
EventTypes.LICENSE_DEACTIVATED,
{"license_id": license_id},
"admin-api"
))
return jsonify({"success": True, "message": "License deactivated"})
@app.route('/api/v1/admin/licenses/<license_id>/devices', methods=['GET'])
@require_admin_auth
def get_license_devices(license_id):
"""Get all devices for a license"""
# Get active devices
active_devices = license_repo.get_active_devices(license_id)
# Get all registered devices from activation events
query = """
SELECT DISTINCT ON (hardware_id)
hardware_id,
event_type,
ip_address,
user_agent,
created_at as registered_at,
metadata
FROM activation_events
WHERE license_id = %s
AND event_type IN ('activation', 'reactivation', 'transfer')
AND success = true
ORDER BY hardware_id, created_at DESC
"""
all_devices = license_repo.execute_query(query, (license_id,))
# Mark active devices
active_hw_ids = {d['hardware_id'] for d in active_devices}
for device in all_devices:
device['is_active'] = device['hardware_id'] in active_hw_ids
if device['is_active']:
# Add last_seen from active_devices
active_device = next((d for d in active_devices if d['hardware_id'] == device['hardware_id']), None)
if active_device:
device['last_seen'] = active_device['last_seen']
return jsonify({
"license_id": license_id,
"total_devices": len(all_devices),
"active_devices": len(active_devices),
"devices": all_devices
})
@app.route('/api/v1/admin/licenses/<license_id>/devices/deactivate', methods=['POST'])
@require_admin_auth
def deactivate_device(license_id):
"""Deactivate a device"""
schema = DeactivateDeviceSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
success = license_repo.deactivate_device(license_id, data['hardware_id'])
if not success:
return jsonify({"error": "Failed to deactivate device"}), 500
# Invalidate cache
cache_repo.invalidate_license_cache(license_id)
# Publish event
event_bus.publish(Event(
EventTypes.DEVICE_DEACTIVATED,
{
"license_id": license_id,
"hardware_id": data['hardware_id'],
"reason": data.get('reason', 'Admin action')
},
"admin-api"
))
return jsonify({"success": True, "message": "Device deactivated"})
@app.route('/api/v1/admin/licenses/<license_id>/transfer', methods=['POST'])
@require_admin_auth
def transfer_license(license_id):
"""Transfer license between devices"""
schema = TransferLicenseSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
# Get client IP
ip_address = request.headers.get('X-Forwarded-For', request.remote_addr)
success = license_repo.transfer_license(
license_id,
data['from_hardware_id'],
data['to_hardware_id'],
ip_address
)
if not success:
return jsonify({"error": "Failed to transfer license"}), 500
# Invalidate cache
cache_repo.invalidate_license_cache(license_id)
# Publish event
event_bus.publish(Event(
EventTypes.LICENSE_TRANSFERRED,
{
"license_id": license_id,
"from_hardware_id": data['from_hardware_id'],
"to_hardware_id": data['to_hardware_id']
},
"admin-api"
))
return jsonify({"success": True, "message": "License transferred successfully"})
@app.route('/api/v1/admin/licenses', methods=['GET'])
@require_admin_auth
def search_licenses():
"""Search and list licenses"""
schema = SearchLicensesSchema()
try:
filters = schema.load(request.args)
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
# Build query
where_clauses = []
params = []
if filters.get('customer_id'):
where_clauses.append("customer_id = %s")
params.append(filters['customer_id'])
if 'is_active' in filters:
where_clauses.append("is_active = %s")
params.append(filters['is_active'])
if 'is_test' in filters:
where_clauses.append("is_test = %s")
params.append(filters['is_test'])
if filters.get('created_after'):
where_clauses.append("created_at >= %s")
params.append(filters['created_after'])
if filters.get('created_before'):
where_clauses.append("created_at <= %s")
params.append(filters['created_before'])
if filters.get('expires_after'):
where_clauses.append("expires_at >= %s")
params.append(filters['expires_after'])
if filters.get('expires_before'):
where_clauses.append("expires_at <= %s")
params.append(filters['expires_before'])
where_sql = " AND ".join(where_clauses) if where_clauses else "1=1"
# Count total
count_query = f"SELECT COUNT(*) as total FROM licenses WHERE {where_sql}"
total_result = license_repo.execute_one(count_query, params)
total = total_result['total'] if total_result else 0
# Get paginated results
page = filters['page']
per_page = filters['per_page']
offset = (page - 1) * per_page
query = f"""
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 {where_sql}
ORDER BY l.created_at DESC
LIMIT %s OFFSET %s
"""
params.extend([per_page, offset])
licenses = license_repo.execute_query(query, params)
return jsonify({
"licenses": licenses,
"pagination": {
"total": total,
"page": page,
"per_page": per_page,
"pages": (total + per_page - 1) // per_page
}
})
@app.route('/api/v1/admin/licenses/<license_id>/events', methods=['GET'])
@require_admin_auth
def get_license_events(license_id):
"""Get all events for a license"""
hours = request.args.get('hours', 24, type=int)
events = license_repo.get_recent_activations(license_id, hours)
return jsonify({
"license_id": license_id,
"hours": hours,
"total_events": len(events),
"events": events
})
@app.route('/api/v1/admin/licenses/<license_id>/usage', methods=['GET'])
@require_admin_auth
def get_license_usage(license_id):
"""Get usage statistics for a license"""
days = request.args.get('days', 30, type=int)
stats = license_repo.get_license_usage_stats(license_id, days)
# Get daily breakdown
query = """
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)
ORDER BY date DESC
"""
daily_stats = license_repo.execute_query(query, (license_id, days))
return jsonify({
"license_id": license_id,
"days": days,
"summary": stats,
"daily": daily_stats
})
@app.route('/api/v1/admin/licenses/<license_id>/anomalies', methods=['GET'])
@require_admin_auth
def get_license_anomalies(license_id):
"""Get detected anomalies for a license"""
query = """
SELECT * FROM anomaly_detections
WHERE license_id = %s
ORDER BY detected_at DESC
LIMIT 100
"""
anomalies = license_repo.execute_query(query, (license_id,))
return jsonify({
"license_id": license_id,
"total_anomalies": len(anomalies),
"anomalies": anomalies
})
@app.route('/api/v1/admin/licenses/<license_id>/anomalies/<anomaly_id>/resolve', methods=['POST'])
@require_admin_auth
def resolve_anomaly(license_id, anomaly_id):
"""Mark anomaly as resolved"""
data = request.get_json() or {}
action_taken = data.get('action_taken', 'Resolved by admin')
query = """
UPDATE anomaly_detections
SET resolved = true,
resolved_at = NOW(),
resolved_by = 'admin',
action_taken = %s
WHERE id = %s AND license_id = %s
RETURNING id
"""
result = license_repo.execute_one(query, (action_taken, anomaly_id, license_id))
if not result:
return jsonify({"error": "Anomaly not found"}), 404
return jsonify({"success": True, "message": "Anomaly resolved"})
@app.route('/api/v1/admin/licenses/bulk-create', methods=['POST'])
@require_admin_auth
def bulk_create_licenses():
"""Create multiple licenses at once"""
data = request.get_json()
if not data or 'licenses' not in data:
return jsonify({"error": "Missing licenses array"}), 400
schema = CreateLicenseSchema()
created_licenses = []
errors = []
for idx, license_data in enumerate(data['licenses']):
try:
validated_data = schema.load(license_data)
# Generate license key
license_key = f"LIC-{secrets.token_urlsafe(16).upper()}"
# Calculate expiration
expires_at = None
if validated_data.get('expires_in_days'):
expires_at = datetime.utcnow() + timedelta(days=validated_data['expires_in_days'])
# Create license
query = """
INSERT INTO licenses
(license_key, customer_id, max_devices, is_active, is_test, expires_at, features, metadata)
VALUES (%s, %s, %s, true, %s, %s, %s, %s)
RETURNING id
"""
import json
license_id = license_repo.execute_insert(query, (
license_key,
validated_data['customer_id'],
validated_data['max_devices'],
validated_data['is_test'],
expires_at,
json.dumps(validated_data['features']),
json.dumps(validated_data['metadata'])
))
if license_id:
created_licenses.append({
"id": license_id,
"license_key": license_key,
"customer_id": validated_data['customer_id']
})
except Exception as e:
errors.append({
"index": idx,
"error": str(e)
})
return jsonify({
"created": len(created_licenses),
"failed": len(errors),
"licenses": created_licenses,
"errors": errors
}), 201 if created_licenses else 400
@app.route('/api/v1/admin/statistics', methods=['GET'])
@require_admin_auth
def get_statistics():
"""Get overall license statistics"""
query = """
WITH stats AS (
SELECT
COUNT(*) as total_licenses,
COUNT(*) FILTER (WHERE is_active = true) as active_licenses,
COUNT(*) FILTER (WHERE is_test = true) as test_licenses,
COUNT(*) FILTER (WHERE expires_at < NOW()) as expired_licenses,
COUNT(DISTINCT customer_id) as total_customers
FROM licenses
),
device_stats AS (
SELECT COUNT(DISTINCT hardware_id) as total_devices
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '15 minutes'
),
validation_stats AS (
SELECT
COUNT(*) as validations_today,
COUNT(DISTINCT license_id) as licenses_used_today
FROM license_heartbeats
WHERE timestamp > CURRENT_DATE
)
SELECT * FROM stats, device_stats, validation_stats
"""
stats = license_repo.execute_one(query)
return jsonify(stats or {})
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5004, debug=True)

Datei anzeigen

@ -0,0 +1 @@
# Analytics Service

Datei anzeigen

@ -0,0 +1,478 @@
import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS
import logging
from functools import wraps
from datetime import datetime, timedelta
import asyncio
from concurrent.futures import ThreadPoolExecutor
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from config import get_config
from repositories.license_repo import LicenseRepository
from repositories.cache_repo import CacheRepository
from events.event_bus import EventBus, Event, EventTypes
from models import AnomalyType, Severity
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
config = get_config()
app.config.from_object(config)
CORS(app)
# Initialize dependencies
license_repo = LicenseRepository(config.DATABASE_URL)
cache_repo = CacheRepository(config.REDIS_URL)
event_bus = EventBus(config.RABBITMQ_URL)
# Thread pool for async operations
executor = ThreadPoolExecutor(max_workers=10)
def require_auth(f):
"""Decorator to require authentication"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key:
return jsonify({"error": "Missing API key"}), 401
# Simple validation for now
if not api_key.startswith('sk_'):
return jsonify({"error": "Invalid API key"}), 401
return f(*args, **kwargs)
return decorated_function
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "analytics",
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/analytics/licenses/<license_id>/patterns', methods=['GET'])
@require_auth
def analyze_license_patterns(license_id):
"""Analyze usage patterns for a license"""
days = request.args.get('days', 30, type=int)
# Get usage data
query = """
WITH hourly_usage AS (
SELECT
DATE_TRUNC('hour', timestamp) as hour,
COUNT(*) as validations,
COUNT(DISTINCT hardware_id) as devices,
COUNT(DISTINCT ip_address) as ips
FROM license_heartbeats
WHERE license_id = %s
AND timestamp > NOW() - INTERVAL '%s days'
GROUP BY DATE_TRUNC('hour', timestamp)
),
daily_patterns AS (
SELECT
EXTRACT(DOW FROM hour) as day_of_week,
EXTRACT(HOUR FROM hour) as hour_of_day,
AVG(validations) as avg_validations,
MAX(devices) as max_devices
FROM hourly_usage
GROUP BY day_of_week, hour_of_day
)
SELECT * FROM daily_patterns
ORDER BY day_of_week, hour_of_day
"""
patterns = license_repo.execute_query(query, (license_id, days))
# Detect anomalies
anomalies = detect_usage_anomalies(license_id, patterns)
return jsonify({
"license_id": license_id,
"days_analyzed": days,
"patterns": patterns,
"anomalies": anomalies
})
@app.route('/api/v1/analytics/licenses/<license_id>/anomalies/detect', methods=['POST'])
@require_auth
def detect_anomalies(license_id):
"""Manually trigger anomaly detection for a license"""
# Run multiple anomaly detection checks
anomalies = []
# Check for multiple IPs
ip_anomalies = check_multiple_ips(license_id)
anomalies.extend(ip_anomalies)
# Check for rapid hardware changes
hw_anomalies = check_rapid_hardware_changes(license_id)
anomalies.extend(hw_anomalies)
# Check for concurrent usage
concurrent_anomalies = check_concurrent_usage(license_id)
anomalies.extend(concurrent_anomalies)
# Check for geographic anomalies
geo_anomalies = check_geographic_anomalies(license_id)
anomalies.extend(geo_anomalies)
# Store detected anomalies
for anomaly in anomalies:
store_anomaly(license_id, anomaly)
return jsonify({
"license_id": license_id,
"anomalies_detected": len(anomalies),
"anomalies": anomalies
})
@app.route('/api/v1/analytics/licenses/<license_id>/risk-score', methods=['GET'])
@require_auth
def get_risk_score(license_id):
"""Calculate risk score for a license"""
# Get recent anomalies
query = """
SELECT anomaly_type, severity, detected_at
FROM anomaly_detections
WHERE license_id = %s
AND detected_at > NOW() - INTERVAL '30 days'
AND resolved = false
"""
anomalies = license_repo.execute_query(query, (license_id,))
# Calculate risk score
risk_score = 0
severity_weights = {
'low': 10,
'medium': 25,
'high': 50,
'critical': 100
}
for anomaly in anomalies:
weight = severity_weights.get(anomaly['severity'], 0)
# Recent anomalies have higher weight
days_old = (datetime.utcnow() - anomaly['detected_at']).days
recency_factor = max(0.5, 1 - (days_old / 30))
risk_score += weight * recency_factor
# Normalize to 0-100
risk_score = min(100, risk_score)
# Determine risk level
if risk_score < 20:
risk_level = "low"
elif risk_score < 50:
risk_level = "medium"
elif risk_score < 80:
risk_level = "high"
else:
risk_level = "critical"
return jsonify({
"license_id": license_id,
"risk_score": round(risk_score, 2),
"risk_level": risk_level,
"active_anomalies": len(anomalies),
"factors": anomalies
})
@app.route('/api/v1/analytics/reports/usage', methods=['GET'])
@require_auth
def generate_usage_report():
"""Generate usage report for all licenses"""
days = request.args.get('days', 30, type=int)
query = """
WITH license_stats AS (
SELECT
l.id,
l.license_key,
l.customer_id,
c.name as customer_name,
l.max_devices,
l.is_test,
l.expires_at,
COUNT(DISTINCT lh.hardware_id) as active_devices,
COUNT(lh.*) as total_validations,
MAX(lh.timestamp) as last_validation
FROM licenses l
LEFT JOIN customers c ON l.customer_id = c.id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '%s days'
WHERE l.is_active = true
GROUP BY l.id, l.license_key, l.customer_id, c.name, l.max_devices, l.is_test, l.expires_at
)
SELECT
*,
CASE
WHEN total_validations = 0 THEN 'inactive'
WHEN active_devices > max_devices THEN 'over_limit'
WHEN expires_at < NOW() THEN 'expired'
ELSE 'active'
END as status,
ROUND((active_devices::numeric / NULLIF(max_devices, 0)) * 100, 2) as device_utilization
FROM license_stats
ORDER BY total_validations DESC
"""
report = license_repo.execute_query(query, (days,))
# Summary statistics
summary = {
"total_licenses": len(report),
"active_licenses": len([r for r in report if r['status'] == 'active']),
"inactive_licenses": len([r for r in report if r['status'] == 'inactive']),
"over_limit_licenses": len([r for r in report if r['status'] == 'over_limit']),
"expired_licenses": len([r for r in report if r['status'] == 'expired']),
"total_validations": sum(r['total_validations'] for r in report),
"average_device_utilization": sum(r['device_utilization'] or 0 for r in report) / len(report) if report else 0
}
return jsonify({
"period_days": days,
"generated_at": datetime.utcnow().isoformat(),
"summary": summary,
"licenses": report
})
@app.route('/api/v1/analytics/reports/revenue', methods=['GET'])
@require_auth
def generate_revenue_report():
"""Generate revenue analytics report"""
# This would need pricing information in the database
# For now, return a placeholder
return jsonify({
"message": "Revenue reporting requires pricing data integration",
"placeholder": True
})
def detect_usage_anomalies(license_id, patterns):
"""Detect anomalies in usage patterns"""
anomalies = []
if not patterns:
return anomalies
# Calculate statistics
validations = [p['avg_validations'] for p in patterns]
if validations:
avg_validations = sum(validations) / len(validations)
max_validations = max(validations)
# Detect spikes
for pattern in patterns:
if pattern['avg_validations'] > avg_validations * 3:
anomalies.append({
"type": AnomalyType.SUSPICIOUS_PATTERN.value,
"severity": Severity.MEDIUM.value,
"details": {
"day": pattern['day_of_week'],
"hour": pattern['hour_of_day'],
"validations": pattern['avg_validations'],
"average": avg_validations
}
})
return anomalies
def check_multiple_ips(license_id):
"""Check for multiple IP addresses"""
query = """
SELECT
COUNT(DISTINCT ip_address) as ip_count,
array_agg(DISTINCT ip_address) as ips
FROM license_heartbeats
WHERE license_id = %s
AND timestamp > NOW() - INTERVAL '1 hour'
"""
result = license_repo.execute_one(query, (license_id,))
anomalies = []
if result and result['ip_count'] > config.ANOMALY_MULTIPLE_IPS_THRESHOLD:
anomalies.append({
"type": AnomalyType.MULTIPLE_IPS.value,
"severity": Severity.HIGH.value,
"details": {
"ip_count": result['ip_count'],
"ips": result['ips'][:10], # Limit to 10 IPs
"threshold": config.ANOMALY_MULTIPLE_IPS_THRESHOLD
}
})
return anomalies
def check_rapid_hardware_changes(license_id):
"""Check for rapid hardware ID changes"""
query = """
SELECT
hardware_id,
created_at
FROM activation_events
WHERE license_id = %s
AND event_type IN ('activation', 'transfer')
AND created_at > NOW() - INTERVAL '1 hour'
AND success = true
ORDER BY created_at DESC
"""
events = license_repo.execute_query(query, (license_id,))
anomalies = []
if len(events) > 1:
# Check time between changes
for i in range(len(events) - 1):
time_diff = (events[i]['created_at'] - events[i+1]['created_at']).total_seconds() / 60
if time_diff < config.ANOMALY_RAPID_HARDWARE_CHANGE_MINUTES:
anomalies.append({
"type": AnomalyType.RAPID_HARDWARE_CHANGE.value,
"severity": Severity.HIGH.value,
"details": {
"hardware_ids": [events[i]['hardware_id'], events[i+1]['hardware_id']],
"time_difference_minutes": round(time_diff, 2),
"threshold_minutes": config.ANOMALY_RAPID_HARDWARE_CHANGE_MINUTES
}
})
return anomalies
def check_concurrent_usage(license_id):
"""Check for concurrent usage from different devices"""
query = """
WITH concurrent_sessions AS (
SELECT
h1.hardware_id as hw1,
h2.hardware_id as hw2,
h1.timestamp as time1,
h2.timestamp as time2
FROM license_heartbeats h1
JOIN license_heartbeats h2 ON h1.license_id = h2.license_id
WHERE h1.license_id = %s
AND h2.license_id = %s
AND h1.hardware_id != h2.hardware_id
AND h1.timestamp > NOW() - INTERVAL '15 minutes'
AND h2.timestamp > NOW() - INTERVAL '15 minutes'
AND ABS(EXTRACT(EPOCH FROM h1.timestamp - h2.timestamp)) < 300
)
SELECT COUNT(*) as concurrent_count
FROM concurrent_sessions
"""
result = license_repo.execute_one(query, (license_id, license_id))
anomalies = []
if result and result['concurrent_count'] > 0:
anomalies.append({
"type": AnomalyType.CONCURRENT_USE.value,
"severity": Severity.CRITICAL.value,
"details": {
"concurrent_sessions": result['concurrent_count'],
"timeframe_minutes": 5
}
})
return anomalies
def check_geographic_anomalies(license_id):
"""Check for geographic anomalies (requires IP geolocation)"""
# This would require IP geolocation service integration
# For now, return empty list
return []
def store_anomaly(license_id, anomaly):
"""Store detected anomaly in database"""
query = """
INSERT INTO anomaly_detections
(license_id, anomaly_type, severity, details)
VALUES (%s, %s, %s, %s)
ON CONFLICT (license_id, anomaly_type, details) DO NOTHING
"""
import json
license_repo.execute_insert(query, (
license_id,
anomaly['type'],
anomaly['severity'],
json.dumps(anomaly['details'])
))
# Publish event
event_bus.publish(Event(
EventTypes.ANOMALY_DETECTED,
{
"license_id": license_id,
"anomaly": anomaly
},
"analytics"
))
@app.route('/api/v1/analytics/dashboard', methods=['GET'])
@require_auth
def get_dashboard_data():
"""Get analytics dashboard data"""
query = """
WITH current_stats AS (
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(DISTINCT hardware_id) as active_devices,
COUNT(*) as validations_today
FROM license_heartbeats
WHERE timestamp > CURRENT_DATE
),
anomaly_stats AS (
SELECT
COUNT(*) as total_anomalies,
COUNT(*) FILTER (WHERE severity = 'critical') as critical_anomalies,
COUNT(*) FILTER (WHERE resolved = false) as unresolved_anomalies
FROM anomaly_detections
WHERE detected_at > CURRENT_DATE - INTERVAL '7 days'
),
trend_data AS (
SELECT
DATE(timestamp) as date,
COUNT(*) as validations,
COUNT(DISTINCT license_id) as licenses,
COUNT(DISTINCT hardware_id) as devices
FROM license_heartbeats
WHERE timestamp > CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE(timestamp)
ORDER BY date
)
SELECT
cs.*,
ans.*,
(SELECT json_agg(td.*) FROM trend_data td) as trends
FROM current_stats cs, anomaly_stats ans
"""
dashboard_data = license_repo.execute_one(query)
return jsonify(dashboard_data or {})
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5003, debug=True)

Datei anzeigen

@ -0,0 +1,25 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 5001
# Run with gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5001", "--workers", "4", "--timeout", "120", "app:app"]

Datei anzeigen

@ -0,0 +1,279 @@
import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
from datetime import datetime, timedelta
import logging
from functools import wraps
from prometheus_flask_exporter import PrometheusMetrics
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from config import get_config
from repositories.base import BaseRepository
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
config = get_config()
app.config.from_object(config)
CORS(app)
# Initialize Prometheus metrics
metrics = PrometheusMetrics(app)
metrics.info('auth_service_info', 'Auth Service Information', version='1.0.0')
# Initialize repository
db_repo = BaseRepository(config.DATABASE_URL)
def create_token(payload: dict, expires_delta: timedelta) -> str:
"""Create JWT token"""
to_encode = payload.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire, "iat": datetime.utcnow()})
return jwt.encode(
to_encode,
config.JWT_SECRET,
algorithm=config.JWT_ALGORITHM
)
def decode_token(token: str) -> dict:
"""Decode and validate JWT token"""
try:
payload = jwt.decode(
token,
config.JWT_SECRET,
algorithms=[config.JWT_ALGORITHM]
)
return payload
except jwt.ExpiredSignatureError:
raise ValueError("Token has expired")
except jwt.InvalidTokenError:
raise ValueError("Invalid token")
def require_api_key(f):
"""Decorator to require API key"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key:
return jsonify({"error": "Missing API key"}), 401
# Validate API key
query = """
SELECT id, client_name, allowed_endpoints
FROM api_clients
WHERE api_key = %s AND is_active = true
"""
client = db_repo.execute_one(query, (api_key,))
if not client:
return jsonify({"error": "Invalid API key"}), 401
# Check if endpoint is allowed
endpoint = request.endpoint
allowed = client.get('allowed_endpoints', [])
if allowed and endpoint not in allowed:
return jsonify({"error": "Endpoint not allowed"}), 403
# Add client info to request
request.api_client = client
return f(*args, **kwargs)
return decorated_function
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "auth",
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/auth/token', methods=['POST'])
@require_api_key
def create_access_token():
"""Create access token for license validation"""
data = request.get_json()
if not data or 'license_id' not in data:
return jsonify({"error": "Missing license_id"}), 400
license_id = data['license_id']
hardware_id = data.get('hardware_id')
# Verify license exists and is active
query = """
SELECT id, is_active, max_devices
FROM licenses
WHERE id = %s
"""
license = db_repo.execute_one(query, (license_id,))
if not license:
return jsonify({"error": "License not found"}), 404
if not license['is_active']:
return jsonify({"error": "License is not active"}), 403
# Create token payload
payload = {
"sub": license_id,
"hwid": hardware_id,
"client_id": request.api_client['id'],
"type": "access"
}
# Add features and limits based on license
payload["features"] = data.get('features', [])
payload["limits"] = {
"api_calls": config.DEFAULT_RATE_LIMIT_PER_HOUR,
"concurrent_sessions": config.MAX_CONCURRENT_SESSIONS
}
# Create tokens
access_token = create_token(payload, config.JWT_ACCESS_TOKEN_EXPIRES)
# Create refresh token
refresh_payload = {
"sub": license_id,
"client_id": request.api_client['id'],
"type": "refresh"
}
refresh_token = create_token(refresh_payload, config.JWT_REFRESH_TOKEN_EXPIRES)
return jsonify({
"access_token": access_token,
"refresh_token": refresh_token,
"token_type": "Bearer",
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
})
@app.route('/api/v1/auth/refresh', methods=['POST'])
def refresh_access_token():
"""Refresh access token"""
data = request.get_json()
if not data or 'refresh_token' not in data:
return jsonify({"error": "Missing refresh_token"}), 400
try:
# Decode refresh token
payload = decode_token(data['refresh_token'])
if payload.get('type') != 'refresh':
return jsonify({"error": "Invalid token type"}), 400
license_id = payload['sub']
# Verify license still active
query = "SELECT is_active FROM licenses WHERE id = %s"
license = db_repo.execute_one(query, (license_id,))
if not license or not license['is_active']:
return jsonify({"error": "License is not active"}), 403
# Create new access token
access_payload = {
"sub": license_id,
"client_id": payload['client_id'],
"type": "access"
}
access_token = create_token(access_payload, config.JWT_ACCESS_TOKEN_EXPIRES)
return jsonify({
"access_token": access_token,
"token_type": "Bearer",
"expires_in": int(config.JWT_ACCESS_TOKEN_EXPIRES.total_seconds())
})
except ValueError as e:
return jsonify({"error": str(e)}), 401
@app.route('/api/v1/auth/verify', methods=['POST'])
def verify_token():
"""Verify token validity"""
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"error": "Missing or invalid authorization header"}), 401
token = auth_header.split(' ')[1]
try:
payload = decode_token(token)
return jsonify({
"valid": True,
"license_id": payload['sub'],
"expires_at": datetime.fromtimestamp(payload['exp']).isoformat()
})
except ValueError as e:
return jsonify({
"valid": False,
"error": str(e)
}), 401
@app.route('/api/v1/auth/api-key', methods=['POST'])
def create_api_key():
"""Create new API key (admin only)"""
# This endpoint should be protected by admin authentication
# For now, we'll use a simple secret header
admin_secret = request.headers.get('X-Admin-Secret')
if admin_secret != os.getenv('ADMIN_SECRET', 'change-this-admin-secret'):
return jsonify({"error": "Unauthorized"}), 401
data = request.get_json()
if not data or 'client_name' not in data:
return jsonify({"error": "Missing client_name"}), 400
import secrets
api_key = f"sk_{secrets.token_urlsafe(32)}"
secret_key = secrets.token_urlsafe(64)
query = """
INSERT INTO api_clients (client_name, api_key, secret_key, allowed_endpoints)
VALUES (%s, %s, %s, %s)
RETURNING id
"""
allowed_endpoints = data.get('allowed_endpoints', [])
client_id = db_repo.execute_insert(
query,
(data['client_name'], api_key, secret_key, allowed_endpoints)
)
if not client_id:
return jsonify({"error": "Failed to create API key"}), 500
return jsonify({
"client_id": client_id,
"api_key": api_key,
"secret_key": secret_key,
"client_name": data['client_name']
}), 201
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001, debug=True)

Datei anzeigen

@ -0,0 +1,15 @@
import os
from datetime import timedelta
def get_config():
"""Get configuration from environment variables"""
return {
'DATABASE_URL': os.getenv('DATABASE_URL', 'postgresql://postgres:password@postgres:5432/v2_adminpanel'),
'REDIS_URL': os.getenv('REDIS_URL', 'redis://redis:6379/1'),
'JWT_SECRET': os.getenv('JWT_SECRET', 'dev-secret-key'),
'JWT_ALGORITHM': 'HS256',
'ACCESS_TOKEN_EXPIRE_MINUTES': 30,
'REFRESH_TOKEN_EXPIRE_DAYS': 7,
'FLASK_ENV': os.getenv('FLASK_ENV', 'production'),
'LOG_LEVEL': os.getenv('LOG_LEVEL', 'INFO'),
}

Datei anzeigen

@ -0,0 +1,9 @@
flask==3.0.0
flask-cors==4.0.0
pyjwt==2.8.0
psycopg2-binary==2.9.9
redis==5.0.1
python-dotenv==1.0.0
gunicorn==21.2.0
marshmallow==3.20.1
prometheus-flask-exporter==0.23.0

Datei anzeigen

@ -0,0 +1,25 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser
# Expose port
EXPOSE 5002
# Run with gunicorn
CMD ["gunicorn", "--bind", "0.0.0.0:5002", "--workers", "4", "--timeout", "120", "app:app"]

Datei anzeigen

@ -0,0 +1,409 @@
import os
import sys
from flask import Flask, request, jsonify
from flask_cors import CORS
import jwt
from datetime import datetime, timedelta
import logging
from functools import wraps
from marshmallow import Schema, fields, ValidationError
# Add parent directory to path for imports
sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from config import get_config
from repositories.license_repo import LicenseRepository
from repositories.cache_repo import CacheRepository
from events.event_bus import EventBus, Event, EventTypes
from models import EventType, ValidationRequest, ValidationResponse
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize Flask app
app = Flask(__name__)
config = get_config()
app.config.from_object(config)
CORS(app)
# Initialize dependencies
license_repo = LicenseRepository(config.DATABASE_URL)
cache_repo = CacheRepository(config.REDIS_URL)
event_bus = EventBus(config.RABBITMQ_URL)
# Validation schemas
class ValidateSchema(Schema):
license_key = fields.Str(required=True)
hardware_id = fields.Str(required=True)
app_version = fields.Str()
class ActivateSchema(Schema):
license_key = fields.Str(required=True)
hardware_id = fields.Str(required=True)
device_name = fields.Str()
os_info = fields.Dict()
class HeartbeatSchema(Schema):
session_data = fields.Dict()
class OfflineTokenSchema(Schema):
duration_hours = fields.Int(missing=24, validate=lambda x: 0 < x <= 72)
def require_api_key(f):
"""Decorator to require API key"""
@wraps(f)
def decorated_function(*args, **kwargs):
api_key = request.headers.get('X-API-Key')
if not api_key:
return jsonify({"error": "Missing API key"}), 401
# For now, accept any API key starting with 'sk_'
# In production, validate against database
if not api_key.startswith('sk_'):
return jsonify({"error": "Invalid API key"}), 401
return f(*args, **kwargs)
return decorated_function
def require_auth_token(f):
"""Decorator to require JWT token"""
@wraps(f)
def decorated_function(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({"error": "Missing or invalid authorization header"}), 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(
token,
config.JWT_SECRET,
algorithms=[config.JWT_ALGORITHM]
)
request.token_payload = payload
return f(*args, **kwargs)
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token has expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
return decorated_function
def get_client_ip():
"""Get client IP address"""
if request.headers.get('X-Forwarded-For'):
return request.headers.get('X-Forwarded-For').split(',')[0]
return request.remote_addr
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
"status": "healthy",
"service": "license-api",
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/license/validate', methods=['POST'])
@require_api_key
def validate_license():
"""Validate license key with hardware ID"""
schema = ValidateSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
license_key = data['license_key']
hardware_id = data['hardware_id']
app_version = data.get('app_version')
# Check cache first
cached_result = cache_repo.get_license_validation(license_key, hardware_id)
if cached_result:
logger.info(f"Cache hit for license validation: {license_key[:8]}...")
return jsonify(cached_result)
# Get license from database
license = license_repo.get_license_by_key(license_key)
if not license:
event_bus.publish(Event(
EventTypes.LICENSE_VALIDATION_FAILED,
{"license_key": license_key, "reason": "not_found"},
"license-api"
))
return jsonify({
"valid": False,
"error": "License not found",
"error_code": "LICENSE_NOT_FOUND"
}), 404
# Check if license is active
if not license['is_active']:
event_bus.publish(Event(
EventTypes.LICENSE_VALIDATION_FAILED,
{"license_id": license['id'], "reason": "inactive"},
"license-api"
))
return jsonify({
"valid": False,
"error": "License is not active",
"error_code": "LICENSE_INACTIVE"
}), 403
# Check expiration
if license['expires_at'] and datetime.utcnow() > license['expires_at']:
event_bus.publish(Event(
EventTypes.LICENSE_EXPIRED,
{"license_id": license['id']},
"license-api"
))
return jsonify({
"valid": False,
"error": "License has expired",
"error_code": "LICENSE_EXPIRED"
}), 403
# Check device limit
device_count = license_repo.get_device_count(license['id'])
if device_count >= license['max_devices']:
# Check if this device is already registered
if not license_repo.check_hardware_id_exists(license['id'], hardware_id):
return jsonify({
"valid": False,
"error": "Device limit exceeded",
"error_code": "DEVICE_LIMIT_EXCEEDED",
"current_devices": device_count,
"max_devices": license['max_devices']
}), 403
# Record heartbeat
license_repo.record_heartbeat(
license_id=license['id'],
hardware_id=hardware_id,
ip_address=get_client_ip(),
user_agent=request.headers.get('User-Agent'),
app_version=app_version
)
# Create response
response = {
"valid": True,
"license_id": license['id'],
"expires_at": license['expires_at'].isoformat() if license['expires_at'] else None,
"features": license.get('features', []),
"limits": {
"max_devices": license['max_devices'],
"current_devices": device_count
}
}
# Cache the result
cache_repo.set_license_validation(
license_key,
hardware_id,
response,
config.CACHE_TTL_VALIDATION
)
# Publish success event
event_bus.publish(Event(
EventTypes.LICENSE_VALIDATED,
{
"license_id": license['id'],
"hardware_id": hardware_id,
"ip_address": get_client_ip()
},
"license-api"
))
return jsonify(response)
@app.route('/api/v1/license/activate', methods=['POST'])
@require_api_key
def activate_license():
"""Activate license on a new device"""
schema = ActivateSchema()
try:
data = schema.load(request.get_json())
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
license_key = data['license_key']
hardware_id = data['hardware_id']
device_name = data.get('device_name')
os_info = data.get('os_info', {})
# Get license
license = license_repo.get_license_by_key(license_key)
if not license:
return jsonify({
"error": "License not found",
"error_code": "LICENSE_NOT_FOUND"
}), 404
if not license['is_active']:
return jsonify({
"error": "License is not active",
"error_code": "LICENSE_INACTIVE"
}), 403
# Check if already activated on this device
if license_repo.check_hardware_id_exists(license['id'], hardware_id):
return jsonify({
"error": "License already activated on this device",
"error_code": "ALREADY_ACTIVATED"
}), 400
# Check device limit
device_count = license_repo.get_device_count(license['id'])
if device_count >= license['max_devices']:
return jsonify({
"error": "Device limit exceeded",
"error_code": "DEVICE_LIMIT_EXCEEDED",
"current_devices": device_count,
"max_devices": license['max_devices']
}), 403
# Record activation
license_repo.record_activation_event(
license_id=license['id'],
event_type=EventType.ACTIVATION,
hardware_id=hardware_id,
ip_address=get_client_ip(),
user_agent=request.headers.get('User-Agent'),
success=True,
metadata={
"device_name": device_name,
"os_info": os_info
}
)
# Invalidate cache
cache_repo.invalidate_license_cache(license['id'])
# Publish event
event_bus.publish(Event(
EventTypes.LICENSE_ACTIVATED,
{
"license_id": license['id'],
"hardware_id": hardware_id,
"device_name": device_name
},
"license-api"
))
return jsonify({
"success": True,
"license_id": license['id'],
"message": "License activated successfully"
}), 201
@app.route('/api/v1/license/heartbeat', methods=['POST'])
@require_auth_token
def heartbeat():
"""Record license heartbeat"""
schema = HeartbeatSchema()
try:
data = schema.load(request.get_json() or {})
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
license_id = request.token_payload['sub']
hardware_id = request.token_payload.get('hwid')
# Record heartbeat
license_repo.record_heartbeat(
license_id=license_id,
hardware_id=hardware_id,
ip_address=get_client_ip(),
user_agent=request.headers.get('User-Agent'),
session_data=data.get('session_data', {})
)
return jsonify({
"success": True,
"timestamp": datetime.utcnow().isoformat()
})
@app.route('/api/v1/license/offline-token', methods=['POST'])
@require_auth_token
def create_offline_token():
"""Create offline validation token"""
schema = OfflineTokenSchema()
try:
data = schema.load(request.get_json() or {})
except ValidationError as e:
return jsonify({"error": "Invalid request", "details": e.messages}), 400
license_id = request.token_payload['sub']
hardware_id = request.token_payload.get('hwid')
duration_hours = data['duration_hours']
if not hardware_id:
return jsonify({"error": "Hardware ID required"}), 400
# Create offline token
token = license_repo.create_license_token(
license_id=license_id,
hardware_id=hardware_id,
valid_hours=duration_hours
)
if not token:
return jsonify({"error": "Failed to create token"}), 500
valid_until = datetime.utcnow() + timedelta(hours=duration_hours)
return jsonify({
"token": token,
"valid_until": valid_until.isoformat(),
"duration_hours": duration_hours
})
@app.route('/api/v1/license/validate-offline', methods=['POST'])
def validate_offline_token():
"""Validate offline token"""
data = request.get_json()
if not data or 'token' not in data:
return jsonify({"error": "Missing token"}), 400
# Validate token
result = license_repo.validate_token(data['token'])
if not result:
return jsonify({
"valid": False,
"error": "Invalid or expired token"
}), 401
return jsonify({
"valid": True,
"license_id": result['license_id'],
"hardware_id": result['hardware_id'],
"expires_at": result['valid_until'].isoformat()
})
@app.errorhandler(404)
def not_found(error):
return jsonify({"error": "Not found"}), 404
@app.errorhandler(500)
def internal_error(error):
logger.error(f"Internal error: {error}")
return jsonify({"error": "Internal server error"}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5002, debug=True)

Datei anzeigen

@ -0,0 +1,10 @@
flask==3.0.0
flask-cors==4.0.0
pyjwt==2.8.0
psycopg2-binary==2.9.9
redis==5.0.1
pika==1.3.2
python-dotenv==1.0.0
gunicorn==21.2.0
marshmallow==3.20.1
requests==2.31.0