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)