Add latest changes
Dieser Commit ist enthalten in:
@@ -1,11 +1,12 @@
|
||||
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, Activation, Version
|
||||
from app.models.models import License, DeviceRegistration, LicenseSession, Version
|
||||
from app.schemas.license import (
|
||||
LicenseActivationRequest,
|
||||
LicenseActivationResponse,
|
||||
@@ -47,58 +48,72 @@ async def activate_license(
|
||||
message="License has expired"
|
||||
)
|
||||
|
||||
existing_activations = db.query(Activation).filter(
|
||||
Activation.license_id == license.id,
|
||||
Activation.is_active == True
|
||||
existing_devices = db.query(DeviceRegistration).filter(
|
||||
DeviceRegistration.license_id == license.id,
|
||||
DeviceRegistration.is_active == True
|
||||
).all()
|
||||
|
||||
existing_machine = next(
|
||||
(a for a in existing_activations if a.machine_id == request.machine_id),
|
||||
existing_device = next(
|
||||
(d for d in existing_devices if d.hardware_fingerprint == request.hardware_fingerprint),
|
||||
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
|
||||
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_machine.id,
|
||||
activation_id=existing_device.id,
|
||||
expires_at=license.expires_at,
|
||||
features={"all_features": True}
|
||||
)
|
||||
|
||||
if len(existing_activations) >= license.max_activations:
|
||||
if len(existing_devices) >= license.device_limit:
|
||||
return LicenseActivationResponse(
|
||||
success=False,
|
||||
message=f"Maximum activations ({license.max_activations}) reached"
|
||||
message=f"Device limit ({license.device_limit}) reached"
|
||||
)
|
||||
|
||||
new_activation = Activation(
|
||||
# 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,
|
||||
machine_id=request.machine_id,
|
||||
hardware_hash=request.hardware_hash,
|
||||
os_info=request.os_info,
|
||||
app_version=request.app_version
|
||||
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_activation)
|
||||
db.add(new_device)
|
||||
db.commit()
|
||||
db.refresh(new_activation)
|
||||
db.refresh(new_device)
|
||||
|
||||
return LicenseActivationResponse(
|
||||
success=True,
|
||||
message="License activated successfully",
|
||||
activation_id=new_activation.id,
|
||||
activation_id=new_device.id,
|
||||
expires_at=license.expires_at,
|
||||
features={"all_features": True}
|
||||
)
|
||||
@@ -109,19 +124,20 @@ async def verify_license(
|
||||
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
|
||||
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 activation:
|
||||
if not device:
|
||||
return LicenseVerificationResponse(
|
||||
valid=False,
|
||||
message="Invalid activation"
|
||||
)
|
||||
|
||||
license = activation.license
|
||||
license = device.license
|
||||
|
||||
if not license.is_active:
|
||||
return LicenseVerificationResponse(
|
||||
@@ -135,36 +151,22 @@ async def verify_license(
|
||||
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()
|
||||
# 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 activation.app_version:
|
||||
if latest_version.version_number > activation.app_version:
|
||||
if latest_version and device.app_version:
|
||||
if latest_version.version_number > device.app_version:
|
||||
requires_update = True
|
||||
update_url = latest_version.download_url
|
||||
|
||||
@@ -190,9 +192,9 @@ async def get_license_info(
|
||||
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
|
||||
devices = db.query(DeviceRegistration).filter(
|
||||
DeviceRegistration.license_id == license.id,
|
||||
DeviceRegistration.is_active == True
|
||||
).all()
|
||||
|
||||
return {
|
||||
@@ -202,17 +204,19 @@ async def get_license_info(
|
||||
"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": [
|
||||
"device_limit": license.device_limit,
|
||||
"current_devices": len(devices),
|
||||
"devices": [
|
||||
{
|
||||
"id": a.id,
|
||||
"machine_id": a.machine_id,
|
||||
"activation_date": a.activation_date,
|
||||
"last_heartbeat": a.last_heartbeat,
|
||||
"app_version": a.app_version
|
||||
"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 a in activations
|
||||
for d in devices
|
||||
]
|
||||
}
|
||||
|
||||
@@ -222,12 +226,12 @@ async def start_session(
|
||||
db: Session = Depends(get_db),
|
||||
api_key: str = Depends(validate_api_key)
|
||||
):
|
||||
# Verify API key matches client config
|
||||
# Get client config (API key already validated by dependency)
|
||||
from sqlalchemy import text
|
||||
result = db.execute(text("SELECT api_key, current_version, minimum_version FROM client_configs WHERE client_name = 'Account Forger'")).first()
|
||||
result = db.execute(text("SELECT 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")
|
||||
if not result:
|
||||
raise HTTPException(status_code=404, detail="Client configuration not found")
|
||||
|
||||
# Check if version is supported
|
||||
if request.version < result.minimum_version:
|
||||
@@ -260,47 +264,96 @@ async def start_session(
|
||||
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"),
|
||||
# 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()
|
||||
|
||||
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:
|
||||
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="Es ist nur eine Sitzung erlaubt, stelle sicher, dass nirgendwo sonst das Programm läuft",
|
||||
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, hardware_id, ip_address, client_version, session_token)
|
||||
VALUES (:license_id, :hardware_id, :ip_address, :version, :token)
|
||||
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,
|
||||
"hardware_id": request.hardware_id,
|
||||
"device_id": device.id,
|
||||
"hardware_fingerprint": request.hardware_fingerprint,
|
||||
"ip_address": request.ip_address,
|
||||
"version": request.version,
|
||||
"token": session_token
|
||||
@@ -356,7 +409,7 @@ async def end_session(
|
||||
# Get session info before deleting
|
||||
session_info = db.execute(
|
||||
text("""
|
||||
SELECT license_id, hardware_id, ip_address, client_version, started_at
|
||||
SELECT license_id, hardware_fingerprint, machine_name, ip_address, client_version, started_at
|
||||
FROM license_sessions
|
||||
WHERE session_token = :token
|
||||
"""),
|
||||
@@ -372,21 +425,22 @@ async def end_session(
|
||||
# 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')
|
||||
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_id": session_info.hardware_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
|
||||
}
|
||||
)
|
||||
|
||||
# Delete the session
|
||||
# Mark session as ended instead of deleting it
|
||||
db.execute(
|
||||
text("DELETE FROM license_sessions WHERE session_token = :token"),
|
||||
text("UPDATE license_sessions SET ended_at = CURRENT_TIMESTAMP, end_reason = 'normal' WHERE session_token = :token"),
|
||||
{"token": request.session_token}
|
||||
)
|
||||
|
||||
|
||||
@@ -11,10 +11,25 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
async def validate_api_key(request: Request, db: Session = Depends(get_db)):
|
||||
"""Validate API key from X-API-Key header against system_api_key table"""
|
||||
# Debug: Log all headers
|
||||
headers_dict = dict(request.headers)
|
||||
# logger.warning(f"DEBUG: All headers: {headers_dict}")
|
||||
# logger.warning(f"DEBUG: Header keys: {list(headers_dict.keys())}")
|
||||
|
||||
# Try different variations
|
||||
api_key = request.headers.get("X-API-Key")
|
||||
if not api_key:
|
||||
api_key = request.headers.get("x-api-key")
|
||||
if not api_key:
|
||||
# Try case-insensitive search
|
||||
for key, value in headers_dict.items():
|
||||
if key.lower() == "x-api-key":
|
||||
api_key = value
|
||||
logger.warning(f"DEBUG: Found API key under header: {key}")
|
||||
break
|
||||
|
||||
if not api_key:
|
||||
logger.warning("API request without API key")
|
||||
logger.warning(f"API request without API key. Headers: {list(request.headers.keys())}")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="API key required",
|
||||
@@ -22,13 +37,43 @@ async def validate_api_key(request: Request, db: Session = Depends(get_db)):
|
||||
)
|
||||
|
||||
# Query the system API key
|
||||
cursor = db.execute(
|
||||
text("SELECT api_key FROM system_api_key WHERE id = 1")
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
try:
|
||||
cursor = db.execute(
|
||||
text("SELECT api_key FROM system_api_key WHERE id = 1")
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
# logger.warning(f"DEBUG: DB query result: {result}")
|
||||
except Exception as e:
|
||||
logger.error(f"Database query error: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail="Database error"
|
||||
)
|
||||
|
||||
if not result or result[0] != api_key:
|
||||
logger.warning(f"Invalid API key attempt: {api_key[:8]}...")
|
||||
if not result:
|
||||
logger.warning(f"No API key found in database")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API key"
|
||||
)
|
||||
|
||||
logger.warning(f"DEBUG: Found API key in request: {api_key}")
|
||||
logger.warning(f"DEBUG: API key from DB: {result[0]}")
|
||||
logger.warning(f"DEBUG: API key match: {result[0] == api_key}")
|
||||
logger.warning(f"DEBUG: Types - DB: {type(result[0])}, Request: {type(api_key)}")
|
||||
|
||||
if result[0] != api_key:
|
||||
logger.warning(f"API key mismatch!")
|
||||
logger.warning(f"Expected (DB): '{result[0]}'")
|
||||
logger.warning(f"Got (Request): '{api_key}'")
|
||||
logger.warning(f"API key lengths - DB: {len(result[0])}, Request: {len(api_key)}")
|
||||
|
||||
# Character by character comparison
|
||||
for i, (c1, c2) in enumerate(zip(result[0], api_key)):
|
||||
if c1 != c2:
|
||||
logger.warning(f"First difference at position {i}: DB='{c1}' (ord={ord(c1)}), Request='{c2}' (ord={ord(c2)})")
|
||||
break
|
||||
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API key"
|
||||
|
||||
@@ -10,7 +10,7 @@ class Settings(BaseSettings):
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
DATABASE_URL: str = "postgresql://license_user:license_password@db:5432/license_db"
|
||||
DATABASE_URL: str = "postgresql://adminuser:supergeheimespasswort@postgres:5432/meinedatenbank"
|
||||
|
||||
|
||||
ALLOWED_ORIGINS: List[str] = [
|
||||
|
||||
116
v2_lizenzserver/app/core/scheduler.py
Normale Datei
116
v2_lizenzserver/app/core/scheduler.py
Normale Datei
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
Background scheduler for License Server
|
||||
Handles periodic tasks like session cleanup
|
||||
"""
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.database import SessionLocal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
|
||||
def cleanup_expired_sessions():
|
||||
"""Clean up sessions that haven't sent heartbeat within timeout period"""
|
||||
db: Session = SessionLocal()
|
||||
try:
|
||||
# Get session timeout from config (default 60 seconds)
|
||||
result = db.execute(text("""
|
||||
SELECT session_timeout
|
||||
FROM client_configs
|
||||
WHERE client_name = 'Account Forger'
|
||||
""")).first()
|
||||
|
||||
timeout_seconds = result[0] if result else 60
|
||||
|
||||
# Find expired sessions
|
||||
expired_sessions = db.execute(text(f"""
|
||||
SELECT id, license_id, hardware_fingerprint, session_token
|
||||
FROM license_sessions
|
||||
WHERE ended_at IS NULL
|
||||
AND last_heartbeat < NOW() - INTERVAL '{timeout_seconds} seconds'
|
||||
""")).fetchall()
|
||||
|
||||
if expired_sessions:
|
||||
logger.info(f"Found {len(expired_sessions)} expired sessions to clean up")
|
||||
|
||||
# Mark sessions as ended
|
||||
for session in expired_sessions:
|
||||
db.execute(text("""
|
||||
UPDATE license_sessions
|
||||
SET ended_at = NOW(), end_reason = 'timeout'
|
||||
WHERE id = :session_id
|
||||
"""), {"session_id": session[0]})
|
||||
|
||||
logger.info(f"Ended session {session[0]} for license {session[1]} due to timeout")
|
||||
|
||||
db.commit()
|
||||
logger.info(f"Successfully cleaned up {len(expired_sessions)} expired sessions")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up sessions: {str(e)}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def cleanup_old_sessions():
|
||||
"""Remove old ended sessions from database (older than 30 days)"""
|
||||
db: Session = SessionLocal()
|
||||
try:
|
||||
result = db.execute(text("""
|
||||
DELETE FROM license_sessions
|
||||
WHERE ended_at IS NOT NULL
|
||||
AND ended_at < NOW() - INTERVAL '30 days'
|
||||
"""))
|
||||
|
||||
if result.rowcount > 0:
|
||||
db.commit()
|
||||
logger.info(f"Cleaned up {result.rowcount} old sessions")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error cleaning up old sessions: {str(e)}")
|
||||
db.rollback()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_scheduler():
|
||||
"""Initialize and start the background scheduler"""
|
||||
# Add job to cleanup expired sessions every 30 seconds
|
||||
scheduler.add_job(
|
||||
func=cleanup_expired_sessions,
|
||||
trigger=IntervalTrigger(seconds=30),
|
||||
id='cleanup_expired_sessions',
|
||||
name='Cleanup expired sessions',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# Add job to cleanup old sessions daily at 3 AM
|
||||
scheduler.add_job(
|
||||
func=cleanup_old_sessions,
|
||||
trigger='cron',
|
||||
hour=3,
|
||||
minute=0,
|
||||
id='cleanup_old_sessions',
|
||||
name='Cleanup old sessions',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
scheduler.start()
|
||||
logger.info("Background scheduler started")
|
||||
logger.info("- Session cleanup runs every 30 seconds")
|
||||
logger.info("- Old session cleanup runs daily at 3:00 AM")
|
||||
|
||||
|
||||
def shutdown_scheduler():
|
||||
"""Shutdown the scheduler gracefully"""
|
||||
if scheduler.running:
|
||||
scheduler.shutdown()
|
||||
logger.info("Background scheduler stopped")
|
||||
@@ -9,6 +9,7 @@ from prometheus_client import generate_latest, CONTENT_TYPE_LATEST
|
||||
from app.api import license, version
|
||||
from app.core.config import settings
|
||||
from app.core.metrics import init_metrics, track_request_metrics
|
||||
from app.core.scheduler import init_scheduler, shutdown_scheduler
|
||||
from app.db.database import engine, Base
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
@@ -19,6 +20,9 @@ Base.metadata.create_all(bind=engine)
|
||||
# Initialize metrics
|
||||
init_metrics(version="1.0.0")
|
||||
|
||||
# Initialize scheduler
|
||||
init_scheduler()
|
||||
|
||||
app = FastAPI(
|
||||
title="License Server API",
|
||||
description="API for software license management",
|
||||
@@ -66,6 +70,23 @@ async def metrics():
|
||||
app.include_router(license.router, prefix="/api/license", tags=["license"])
|
||||
app.include_router(version.router, prefix="/api/version", tags=["version"])
|
||||
|
||||
@app.post("/debug/headers")
|
||||
async def debug_headers(request: Request):
|
||||
"""Debug endpoint to check headers"""
|
||||
headers = dict(request.headers)
|
||||
return {
|
||||
"headers": headers,
|
||||
"x-api-key": headers.get("x-api-key"),
|
||||
"X-API-Key": headers.get("X-API-Key"),
|
||||
"all_keys": list(headers.keys())
|
||||
}
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""Cleanup on shutdown"""
|
||||
logger.info("Shutting down scheduler...")
|
||||
shutdown_scheduler()
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
|
||||
@@ -1 +1 @@
|
||||
from .models import License, Activation, Version, ApiKey
|
||||
from .models import License, DeviceRegistration, LicenseSession, Version, ApiKey
|
||||
@@ -13,31 +13,64 @@ class License(Base):
|
||||
customer_email = Column(String, nullable=False)
|
||||
customer_name = Column(String)
|
||||
|
||||
max_activations = Column(Integer, default=1)
|
||||
device_limit = Column(Integer, default=3)
|
||||
concurrent_sessions_limit = Column(Integer, default=1)
|
||||
is_active = Column(Boolean, default=True)
|
||||
expires_at = Column(DateTime, nullable=True)
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, onupdate=func.now())
|
||||
|
||||
activations = relationship("Activation", back_populates="license")
|
||||
device_registrations = relationship("DeviceRegistration", back_populates="license")
|
||||
sessions = relationship("LicenseSession", back_populates="license")
|
||||
|
||||
class Activation(Base):
|
||||
__tablename__ = "activations"
|
||||
class DeviceRegistration(Base):
|
||||
__tablename__ = "device_registrations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
license_id = Column(Integer, ForeignKey("licenses.id"))
|
||||
machine_id = Column(String, nullable=False)
|
||||
hardware_hash = Column(String, nullable=False)
|
||||
hardware_fingerprint = Column(String, nullable=False)
|
||||
device_name = Column(String, nullable=False)
|
||||
device_type = Column(String, default="unknown")
|
||||
operating_system = Column(String)
|
||||
|
||||
activation_date = Column(DateTime, server_default=func.now())
|
||||
last_heartbeat = Column(DateTime, server_default=func.now())
|
||||
first_activated_at = Column(DateTime, server_default=func.now())
|
||||
last_seen_at = Column(DateTime, server_default=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
os_info = Column(JSON)
|
||||
app_version = Column(String)
|
||||
deactivated_at = Column(DateTime, nullable=True)
|
||||
deactivated_by = Column(String, nullable=True)
|
||||
|
||||
license = relationship("License", back_populates="activations")
|
||||
ip_address = Column(String)
|
||||
user_agent = Column(String)
|
||||
app_version = Column(String)
|
||||
device_metadata = Column("metadata", JSON, default={})
|
||||
|
||||
license = relationship("License", back_populates="device_registrations")
|
||||
sessions = relationship("LicenseSession", back_populates="device_registration")
|
||||
|
||||
|
||||
class LicenseSession(Base):
|
||||
__tablename__ = "license_sessions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
license_id = Column(Integer, ForeignKey("licenses.id"))
|
||||
device_registration_id = Column(Integer, ForeignKey("device_registrations.id"))
|
||||
|
||||
session_token = Column(String, unique=True, nullable=False)
|
||||
hardware_fingerprint = Column(String, nullable=False)
|
||||
|
||||
started_at = Column(DateTime, server_default=func.now())
|
||||
last_heartbeat = Column(DateTime, server_default=func.now())
|
||||
ended_at = Column(DateTime, nullable=True)
|
||||
end_reason = Column(String, nullable=True)
|
||||
|
||||
ip_address = Column(String)
|
||||
client_version = Column(String)
|
||||
user_agent = Column(String)
|
||||
|
||||
license = relationship("License", back_populates="sessions")
|
||||
device_registration = relationship("DeviceRegistration", back_populates="sessions")
|
||||
|
||||
class Version(Base):
|
||||
__tablename__ = "versions"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from pydantic import BaseModel, EmailStr, field_validator, model_validator
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
class LicenseActivationRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_hash: str
|
||||
machine_name: str
|
||||
hardware_fingerprint: str
|
||||
os_info: Optional[Dict[str, Any]] = None
|
||||
app_version: Optional[str] = None
|
||||
|
||||
@@ -18,8 +18,8 @@ class LicenseActivationResponse(BaseModel):
|
||||
|
||||
class LicenseVerificationRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_hash: str
|
||||
machine_name: str
|
||||
hardware_fingerprint: str
|
||||
activation_id: int
|
||||
|
||||
class LicenseVerificationResponse(BaseModel):
|
||||
@@ -44,9 +44,8 @@ class VersionCheckResponse(BaseModel):
|
||||
|
||||
class SessionStartRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_id: str
|
||||
hardware_hash: str
|
||||
machine_name: str
|
||||
hardware_fingerprint: str
|
||||
version: str
|
||||
ip_address: Optional[str] = None
|
||||
|
||||
|
||||
@@ -12,4 +12,5 @@ python-dotenv==1.0.0
|
||||
httpx==0.25.2
|
||||
redis==5.0.1
|
||||
packaging==23.2
|
||||
prometheus-client==0.19.0
|
||||
prometheus-client==0.19.0
|
||||
apscheduler==3.10.4
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren