Files
v2-Docker/lizenzserver/services/license_api/app.py
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

409 Zeilen
12 KiB
Python

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)