452 Zeilen
15 KiB
Python
452 Zeilen
15 KiB
Python
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"
|
|
) |