Files
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

398 Zeilen
13 KiB
Python

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