Dateien
Hetzner-Backup/v2_lizenzserver/app/api/license.py
2025-07-03 20:38:33 +00:00

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"
)