from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from sqlalchemy import text from datetime import datetime, timedelta from typing import Dict, Any import uuid from app.db.database import get_db from app.models.models import License, DeviceRegistration, LicenseSession, Version from app.schemas.license import ( LicenseActivationRequest, LicenseActivationResponse, LicenseVerificationRequest, LicenseVerificationResponse, SessionStartRequest, SessionStartResponse, SessionHeartbeatRequest, SessionHeartbeatResponse, SessionEndRequest, SessionEndResponse ) from app.core.security import get_api_key from app.core.config import settings from app.core.api_key_auth import validate_api_key router = APIRouter() @router.post("/activate", response_model=LicenseActivationResponse) async def activate_license( request: LicenseActivationRequest, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): license = db.query(License).filter( License.license_key == request.license_key, License.is_active == True ).first() if not license: return LicenseActivationResponse( success=False, message="Invalid license key" ) if license.expires_at and license.expires_at < datetime.utcnow(): return LicenseActivationResponse( success=False, message="License has expired" ) existing_devices = db.query(DeviceRegistration).filter( DeviceRegistration.license_id == license.id, DeviceRegistration.is_active == True ).all() existing_device = next( (d for d in existing_devices if d.hardware_fingerprint == request.hardware_fingerprint), None ) if existing_device: # Update existing device info existing_device.last_seen_at = datetime.utcnow() existing_device.app_version = request.app_version existing_device.device_metadata = request.os_info existing_device.device_name = request.machine_name db.commit() return LicenseActivationResponse( success=True, message="License reactivated successfully", activation_id=existing_device.id, expires_at=license.expires_at, features={"all_features": True} ) if len(existing_devices) >= license.device_limit: return LicenseActivationResponse( success=False, message=f"Device limit ({license.device_limit}) reached" ) # Determine device type from OS info device_type = 'unknown' if request.os_info: os_string = str(request.os_info).lower() if 'windows' in os_string: device_type = 'desktop' elif 'mac' in os_string or 'darwin' in os_string: device_type = 'desktop' elif 'linux' in os_string: device_type = 'desktop' elif 'android' in os_string: device_type = 'mobile' elif 'ios' in os_string: device_type = 'mobile' new_device = DeviceRegistration( license_id=license.id, device_name=request.machine_name, hardware_fingerprint=request.hardware_fingerprint, device_type=device_type, operating_system=request.os_info.get('os', 'unknown') if isinstance(request.os_info, dict) else str(request.os_info), metadata=request.os_info, app_version=request.app_version, ip_address=request.ip_address if hasattr(request, 'ip_address') else None ) db.add(new_device) db.commit() db.refresh(new_device) return LicenseActivationResponse( success=True, message="License activated successfully", activation_id=new_device.id, expires_at=license.expires_at, features={"all_features": True} ) @router.post("/verify", response_model=LicenseVerificationResponse) async def verify_license( request: LicenseVerificationRequest, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): device = db.query(DeviceRegistration).filter( DeviceRegistration.id == request.activation_id, DeviceRegistration.device_name == request.machine_name, DeviceRegistration.hardware_fingerprint == request.hardware_fingerprint, DeviceRegistration.is_active == True ).first() if not device: return LicenseVerificationResponse( valid=False, message="Invalid activation" ) license = device.license if not license.is_active: return LicenseVerificationResponse( valid=False, message="License is no longer active" ) if license.license_key != request.license_key: return LicenseVerificationResponse( valid=False, message="License key mismatch" ) if license.expires_at and license.expires_at < datetime.utcnow(): return LicenseVerificationResponse( valid=False, message="License has expired" ) # Update last seen timestamp device.last_seen_at = datetime.utcnow() db.commit() latest_version = db.query(Version).order_by(Version.release_date.desc()).first() requires_update = False update_url = None if latest_version and device.app_version: if latest_version.version_number > device.app_version: requires_update = True update_url = latest_version.download_url return LicenseVerificationResponse( valid=True, message="License is valid", expires_at=license.expires_at, features={"all_features": True}, requires_update=requires_update, update_url=update_url ) @router.get("/info/{license_key}") async def get_license_info( license_key: str, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): license = db.query(License).filter( License.license_key == license_key ).first() if not license: raise HTTPException(status_code=404, detail="License not found") devices = db.query(DeviceRegistration).filter( DeviceRegistration.license_id == license.id, DeviceRegistration.is_active == True ).all() return { "license_key": license.license_key, "product_id": license.product_id, "customer_email": license.customer_email, "customer_name": license.customer_name, "is_active": license.is_active, "expires_at": license.expires_at, "device_limit": license.device_limit, "current_devices": len(devices), "devices": [ { "id": d.id, "hardware_fingerprint": d.hardware_fingerprint, "device_name": d.device_name, "device_type": d.device_type, "first_activated_at": d.first_activated_at, "last_seen_at": d.last_seen_at, "app_version": d.app_version } for d in devices ] } @router.post("/session/start", response_model=SessionStartResponse) async def start_session( request: SessionStartRequest, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): # Get client config (API key already validated by dependency) from sqlalchemy import text result = db.execute(text("SELECT current_version, minimum_version FROM client_configs WHERE client_name = 'Account Forger'")).first() if not result: raise HTTPException(status_code=404, detail="Client configuration not found") # Check if version is supported if request.version < result.minimum_version: return SessionStartResponse( success=False, message=f"Version {request.version} is too old. Minimum required: {result.minimum_version}", session_token=None, requires_update=True, update_url=None, whats_new=None ) # Verify license exists and is active license = db.query(License).filter( License.license_key == request.license_key, License.is_active == True ).first() if not license: return SessionStartResponse( success=False, message="Invalid or inactive license key", session_token=None ) if license.expires_at and license.expires_at < datetime.utcnow(): return SessionStartResponse( success=False, message="License has expired", session_token=None ) # Get license details with concurrent_sessions_limit license_details = db.execute( text("SELECT concurrent_sessions_limit FROM licenses WHERE id = :license_id"), {"license_id": license.id} ).first() max_concurrent_sessions = license_details.concurrent_sessions_limit if license_details else 1 # Check if device is registered device = db.query(DeviceRegistration).filter( DeviceRegistration.license_id == license.id, DeviceRegistration.hardware_fingerprint == request.hardware_fingerprint, DeviceRegistration.is_active == True ).first() if not device: # Register new device if under limit device_count = db.query(DeviceRegistration).filter( DeviceRegistration.license_id == license.id, DeviceRegistration.is_active == True ).count() if device_count >= license.device_limit: return SessionStartResponse( success=False, message=f"Device limit ({license.device_limit}) reached", session_token=None ) # Register device device = DeviceRegistration( license_id=license.id, hardware_fingerprint=request.hardware_fingerprint, device_name=request.machine_name, device_type='desktop', app_version=request.version, ip_address=request.ip_address ) db.add(device) db.commit() db.refresh(device) # Check if this device already has an active session existing_session = db.execute( text("SELECT session_token FROM license_sessions WHERE license_id = :license_id AND device_registration_id = :device_id AND ended_at IS NULL"), {"license_id": license.id, "device_id": device.id} ).first() if existing_session: # Same device, update heartbeat db.execute( text("UPDATE license_sessions SET last_heartbeat = CURRENT_TIMESTAMP WHERE session_token = :token"), {"token": existing_session.session_token} ) db.commit() return SessionStartResponse( success=True, message="Existing session resumed", session_token=existing_session.session_token, requires_update=request.version < result.current_version, update_url=None, whats_new=None ) # Count active sessions for this license active_sessions_count = db.execute( text("SELECT COUNT(*) FROM license_sessions WHERE license_id = :license_id AND ended_at IS NULL"), {"license_id": license.id} ).scalar() if active_sessions_count >= max_concurrent_sessions: return SessionStartResponse( success=False, message=f"Maximum concurrent sessions ({max_concurrent_sessions}) reached. Please close another session first.", session_token=None ) # Create new session session_token = str(uuid.uuid4()) db.execute( text(""" INSERT INTO license_sessions (license_id, device_registration_id, hardware_fingerprint, ip_address, client_version, session_token) VALUES (:license_id, :device_id, :hardware_fingerprint, :ip_address, :version, :token) """), { "license_id": license.id, "device_id": device.id, "hardware_fingerprint": request.hardware_fingerprint, "ip_address": request.ip_address, "version": request.version, "token": session_token } ) db.commit() return SessionStartResponse( success=True, message="Session started successfully", session_token=session_token, requires_update=request.version < result.current_version, update_url=None, whats_new=None ) @router.post("/session/heartbeat", response_model=SessionHeartbeatResponse) async def session_heartbeat( request: SessionHeartbeatRequest, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): # Update heartbeat result = db.execute( text(""" UPDATE license_sessions SET last_heartbeat = CURRENT_TIMESTAMP WHERE session_token = :token RETURNING id """), {"token": request.session_token} ).first() if not result: return SessionHeartbeatResponse( success=False, message="Invalid or expired session" ) db.commit() return SessionHeartbeatResponse( success=True, message="Heartbeat received" ) @router.post("/session/end", response_model=SessionEndResponse) async def end_session( request: SessionEndRequest, db: Session = Depends(get_db), api_key: str = Depends(validate_api_key) ): # Get session info before deleting session_info = db.execute( text(""" SELECT license_id, hardware_fingerprint, machine_name, ip_address, client_version, started_at FROM license_sessions WHERE session_token = :token """), {"token": request.session_token} ).first() if not session_info: return SessionEndResponse( success=False, message="Session not found" ) # Log to session history db.execute( text(""" INSERT INTO session_history (license_id, hardware_fingerprint, machine_name, ip_address, client_version, started_at, ended_at, end_reason) VALUES (:license_id, :hardware_fingerprint, :machine_name, :ip_address, :version, :started, CURRENT_TIMESTAMP, 'normal') """), { "license_id": session_info.license_id, "hardware_fingerprint": session_info.hardware_fingerprint, "machine_name": session_info.machine_name if session_info.machine_name else None, "ip_address": session_info.ip_address, "version": session_info.client_version, "started": session_info.started_at } ) # Mark session as ended instead of deleting it db.execute( text("UPDATE license_sessions SET ended_at = CURRENT_TIMESTAMP, end_reason = 'normal' WHERE session_token = :token"), {"token": request.session_token} ) db.commit() return SessionEndResponse( success=True, message="Session ended" )