from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session 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, Activation, 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_activations = db.query(Activation).filter( Activation.license_id == license.id, Activation.is_active == True ).all() existing_machine = next( (a for a in existing_activations if a.machine_id == request.machine_id), None ) if existing_machine: if existing_machine.hardware_hash != request.hardware_hash: return LicenseActivationResponse( success=False, message="Hardware mismatch for this machine" ) existing_machine.last_heartbeat = datetime.utcnow() existing_machine.app_version = request.app_version existing_machine.os_info = request.os_info db.commit() return LicenseActivationResponse( success=True, message="License reactivated successfully", activation_id=existing_machine.id, expires_at=license.expires_at, features={"all_features": True} ) if len(existing_activations) >= license.max_activations: return LicenseActivationResponse( success=False, message=f"Maximum activations ({license.max_activations}) reached" ) new_activation = Activation( license_id=license.id, machine_id=request.machine_id, hardware_hash=request.hardware_hash, os_info=request.os_info, app_version=request.app_version ) db.add(new_activation) db.commit() db.refresh(new_activation) return LicenseActivationResponse( success=True, message="License activated successfully", activation_id=new_activation.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) ): activation = db.query(Activation).filter( Activation.id == request.activation_id, Activation.machine_id == request.machine_id, Activation.is_active == True ).first() if not activation: return LicenseVerificationResponse( valid=False, message="Invalid activation" ) license = activation.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 activation.hardware_hash != request.hardware_hash: grace_period = datetime.utcnow() - timedelta(days=settings.OFFLINE_GRACE_PERIOD_DAYS) if activation.last_heartbeat < grace_period: return LicenseVerificationResponse( valid=False, message="Hardware mismatch and grace period expired" ) else: return LicenseVerificationResponse( valid=True, message="Hardware mismatch but within grace period", expires_at=license.expires_at, features={"all_features": True} ) if license.expires_at and license.expires_at < datetime.utcnow(): return LicenseVerificationResponse( valid=False, message="License has expired" ) activation.last_heartbeat = 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 activation.app_version: if latest_version.version_number > activation.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") activations = db.query(Activation).filter( Activation.license_id == license.id, Activation.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, "max_activations": license.max_activations, "current_activations": len(activations), "activations": [ { "id": a.id, "machine_id": a.machine_id, "activation_date": a.activation_date, "last_heartbeat": a.last_heartbeat, "app_version": a.app_version } for a in activations ] } @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) ): # Verify API key matches client config from sqlalchemy import text result = db.execute(text("SELECT api_key, current_version, minimum_version FROM client_configs WHERE client_name = 'Account Forger'")).first() if not result or result.api_key != api_key: raise HTTPException(status_code=401, detail="Invalid API key") # 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 ) # Check for existing active session existing_session_result = db.execute( text("SELECT session_token, hardware_id FROM license_sessions WHERE license_id = :license_id"), {"license_id": license.id} ).first() if existing_session_result: if existing_session_result.hardware_id == request.hardware_id: # Same device, update heartbeat db.execute( text("UPDATE license_sessions SET last_heartbeat = CURRENT_TIMESTAMP WHERE session_token = :token"), {"token": existing_session_result.session_token} ) db.commit() return SessionStartResponse( success=True, message="Existing session resumed", session_token=existing_session_result.session_token, requires_update=request.version < result.current_version, update_url=None, whats_new=None ) else: return SessionStartResponse( success=False, message="Es ist nur eine Sitzung erlaubt, stelle sicher, dass nirgendwo sonst das Programm läuft", session_token=None ) # Create new session session_token = str(uuid.uuid4()) db.execute( text(""" INSERT INTO license_sessions (license_id, hardware_id, ip_address, client_version, session_token) VALUES (:license_id, :hardware_id, :ip_address, :version, :token) """), { "license_id": license.id, "hardware_id": request.hardware_id, "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_id, 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_id, ip_address, client_version, started_at, ended_at, end_reason) VALUES (:license_id, :hardware_id, :ip_address, :version, :started, CURRENT_TIMESTAMP, 'normal') """), { "license_id": session_info.license_id, "hardware_id": session_info.hardware_id, "ip_address": session_info.ip_address, "version": session_info.client_version, "started": session_info.started_at } ) # Delete the session db.execute( text("DELETE FROM license_sessions WHERE session_token = :token"), {"token": request.session_token} ) db.commit() return SessionEndResponse( success=True, message="Session ended" )