Initial commit
Dieser Commit ist enthalten in:
1
v2_lizenzserver/app/api/__init__.py
Normale Datei
1
v2_lizenzserver/app/api/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
from . import license, version
|
||||
398
v2_lizenzserver/app/api/license.py
Normale Datei
398
v2_lizenzserver/app/api/license.py
Normale Datei
@ -0,0 +1,398 @@
|
||||
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"
|
||||
)
|
||||
84
v2_lizenzserver/app/api/version.py
Normale Datei
84
v2_lizenzserver/app/api/version.py
Normale Datei
@ -0,0 +1,84 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from packaging import version
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.models.models import Version, License
|
||||
from app.schemas.license import VersionCheckRequest, VersionCheckResponse
|
||||
from app.core.security import get_api_key
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/check", response_model=VersionCheckResponse)
|
||||
async def check_version(
|
||||
request: VersionCheckRequest,
|
||||
db: Session = Depends(get_db),
|
||||
api_key = Depends(get_api_key)
|
||||
):
|
||||
license = db.query(License).filter(
|
||||
License.license_key == request.license_key,
|
||||
License.is_active == True
|
||||
).first()
|
||||
|
||||
if not license:
|
||||
return VersionCheckResponse(
|
||||
latest_version=request.current_version,
|
||||
current_version=request.current_version,
|
||||
update_available=False,
|
||||
is_mandatory=False
|
||||
)
|
||||
|
||||
latest_version = db.query(Version).order_by(Version.release_date.desc()).first()
|
||||
|
||||
if not latest_version:
|
||||
return VersionCheckResponse(
|
||||
latest_version=request.current_version,
|
||||
current_version=request.current_version,
|
||||
update_available=False,
|
||||
is_mandatory=False
|
||||
)
|
||||
|
||||
current_ver = version.parse(request.current_version)
|
||||
latest_ver = version.parse(latest_version.version_number)
|
||||
|
||||
update_available = latest_ver > current_ver
|
||||
is_mandatory = False
|
||||
|
||||
if update_available and latest_version.is_mandatory:
|
||||
if latest_version.min_version:
|
||||
min_ver = version.parse(latest_version.min_version)
|
||||
is_mandatory = current_ver < min_ver
|
||||
else:
|
||||
is_mandatory = True
|
||||
|
||||
return VersionCheckResponse(
|
||||
latest_version=latest_version.version_number,
|
||||
current_version=request.current_version,
|
||||
update_available=update_available,
|
||||
is_mandatory=is_mandatory,
|
||||
download_url=latest_version.download_url if update_available else None,
|
||||
release_notes=latest_version.release_notes if update_available else None
|
||||
)
|
||||
|
||||
@router.get("/latest")
|
||||
async def get_latest_version(
|
||||
db: Session = Depends(get_db),
|
||||
api_key = Depends(get_api_key)
|
||||
):
|
||||
latest_version = db.query(Version).order_by(Version.release_date.desc()).first()
|
||||
|
||||
if not latest_version:
|
||||
return {
|
||||
"version": "1.0.0",
|
||||
"release_date": None,
|
||||
"release_notes": "Initial release"
|
||||
}
|
||||
|
||||
return {
|
||||
"version": latest_version.version_number,
|
||||
"release_date": latest_version.release_date,
|
||||
"is_mandatory": latest_version.is_mandatory,
|
||||
"min_version": latest_version.min_version,
|
||||
"download_url": latest_version.download_url,
|
||||
"release_notes": latest_version.release_notes
|
||||
}
|
||||
46
v2_lizenzserver/app/core/api_key_auth.py
Normale Datei
46
v2_lizenzserver/app/core/api_key_auth.py
Normale Datei
@ -0,0 +1,46 @@
|
||||
from fastapi import HTTPException, Request, Depends
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import text
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from app.db.database import get_db
|
||||
|
||||
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"""
|
||||
api_key = request.headers.get("X-API-Key")
|
||||
|
||||
if not api_key:
|
||||
logger.warning("API request without API key")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="API key required",
|
||||
headers={"WWW-Authenticate": "ApiKey"}
|
||||
)
|
||||
|
||||
# Query the system API key
|
||||
cursor = db.execute(
|
||||
text("SELECT api_key FROM system_api_key WHERE id = 1")
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
|
||||
if not result or result[0] != api_key:
|
||||
logger.warning(f"Invalid API key attempt: {api_key[:8]}...")
|
||||
raise HTTPException(
|
||||
status_code=401,
|
||||
detail="Invalid API key"
|
||||
)
|
||||
|
||||
# Update usage statistics
|
||||
db.execute(text("""
|
||||
UPDATE system_api_key
|
||||
SET last_used_at = CURRENT_TIMESTAMP,
|
||||
usage_count = usage_count + 1
|
||||
WHERE id = 1
|
||||
"""))
|
||||
db.commit()
|
||||
|
||||
return api_key
|
||||
32
v2_lizenzserver/app/core/config.py
Normale Datei
32
v2_lizenzserver/app/core/config.py
Normale Datei
@ -0,0 +1,32 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from typing import List
|
||||
|
||||
class Settings(BaseSettings):
|
||||
PROJECT_NAME: str = "License Server"
|
||||
VERSION: str = "1.0.0"
|
||||
API_PREFIX: str = "/api"
|
||||
|
||||
SECRET_KEY: str = "your-secret-key-change-this-in-production"
|
||||
ALGORITHM: str = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
|
||||
|
||||
DATABASE_URL: str = "postgresql://license_user:license_password@db:5432/license_db"
|
||||
|
||||
|
||||
ALLOWED_ORIGINS: List[str] = [
|
||||
"https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com",
|
||||
"https://admin-panel-undso.z5m7q9dk3ah2v1plx6ju.com"
|
||||
]
|
||||
|
||||
DEBUG: bool = False
|
||||
|
||||
MAX_ACTIVATIONS_PER_LICENSE: int = 5
|
||||
HEARTBEAT_INTERVAL_MINUTES: int = 15
|
||||
OFFLINE_GRACE_PERIOD_DAYS: int = 7
|
||||
|
||||
class Config:
|
||||
env_file = ".env"
|
||||
case_sensitive = True
|
||||
extra = "ignore" # Ignore extra environment variables
|
||||
|
||||
settings = Settings()
|
||||
175
v2_lizenzserver/app/core/metrics.py
Normale Datei
175
v2_lizenzserver/app/core/metrics.py
Normale Datei
@ -0,0 +1,175 @@
|
||||
from prometheus_client import Counter, Histogram, Gauge, Info
|
||||
from functools import wraps
|
||||
import time
|
||||
|
||||
# License validation metrics
|
||||
license_validation_total = Counter(
|
||||
'license_validation_total',
|
||||
'Total number of license validations',
|
||||
['result', 'license_type']
|
||||
)
|
||||
|
||||
license_validation_errors_total = Counter(
|
||||
'license_validation_errors_total',
|
||||
'Total number of license validation errors',
|
||||
['error_type']
|
||||
)
|
||||
|
||||
license_validation_duration_seconds = Histogram(
|
||||
'license_validation_duration_seconds',
|
||||
'License validation duration in seconds',
|
||||
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0]
|
||||
)
|
||||
|
||||
# Active licenses gauge
|
||||
active_licenses_total = Gauge(
|
||||
'active_licenses_total',
|
||||
'Total number of active licenses',
|
||||
['license_type']
|
||||
)
|
||||
|
||||
# Heartbeat metrics
|
||||
license_heartbeat_total = Counter(
|
||||
'license_heartbeat_total',
|
||||
'Total number of license heartbeats received'
|
||||
)
|
||||
|
||||
# Activation metrics
|
||||
license_activation_total = Counter(
|
||||
'license_activation_total',
|
||||
'Total number of license activations',
|
||||
['result']
|
||||
)
|
||||
|
||||
# Anomaly detection metrics
|
||||
anomaly_detections_total = Counter(
|
||||
'anomaly_detections_total',
|
||||
'Total number of anomalies detected',
|
||||
['anomaly_type', 'severity']
|
||||
)
|
||||
|
||||
# Concurrent sessions gauge
|
||||
concurrent_sessions_total = Gauge(
|
||||
'concurrent_sessions_total',
|
||||
'Total number of concurrent active sessions'
|
||||
)
|
||||
|
||||
# Database connection pool metrics
|
||||
db_connection_pool_size = Gauge(
|
||||
'db_connection_pool_size',
|
||||
'Database connection pool size'
|
||||
)
|
||||
|
||||
db_connection_pool_used = Gauge(
|
||||
'db_connection_pool_used',
|
||||
'Database connections currently in use'
|
||||
)
|
||||
|
||||
# API client metrics
|
||||
api_requests_total = Counter(
|
||||
'api_requests_total',
|
||||
'Total number of API requests',
|
||||
['method', 'endpoint', 'status']
|
||||
)
|
||||
|
||||
api_request_duration_seconds = Histogram(
|
||||
'api_request_duration_seconds',
|
||||
'API request duration in seconds',
|
||||
['method', 'endpoint'],
|
||||
buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
|
||||
)
|
||||
|
||||
# Cache metrics
|
||||
cache_hits_total = Counter(
|
||||
'cache_hits_total',
|
||||
'Total number of cache hits',
|
||||
['cache_type']
|
||||
)
|
||||
|
||||
cache_misses_total = Counter(
|
||||
'cache_misses_total',
|
||||
'Total number of cache misses',
|
||||
['cache_type']
|
||||
)
|
||||
|
||||
# System info
|
||||
system_info = Info(
|
||||
'license_server_info',
|
||||
'License server information'
|
||||
)
|
||||
|
||||
def track_request_metrics(method: str, endpoint: str):
|
||||
"""Decorator to track API request metrics"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
status = "success"
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
return result
|
||||
except Exception as e:
|
||||
status = "error"
|
||||
raise
|
||||
finally:
|
||||
duration = time.time() - start_time
|
||||
api_requests_total.labels(
|
||||
method=method,
|
||||
endpoint=endpoint,
|
||||
status=status
|
||||
).inc()
|
||||
api_request_duration_seconds.labels(
|
||||
method=method,
|
||||
endpoint=endpoint
|
||||
).observe(duration)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
def track_validation_metrics():
|
||||
"""Decorator to track license validation metrics"""
|
||||
def decorator(func):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
result = await func(*args, **kwargs)
|
||||
|
||||
# Extract result type from the validation result
|
||||
if result.get('valid'):
|
||||
result_type = 'success'
|
||||
elif result.get('error') == 'expired':
|
||||
result_type = 'expired'
|
||||
elif result.get('error') == 'invalid':
|
||||
result_type = 'invalid'
|
||||
else:
|
||||
result_type = 'error'
|
||||
|
||||
license_type = result.get('license_type', 'unknown')
|
||||
license_validation_total.labels(
|
||||
result=result_type,
|
||||
license_type=license_type
|
||||
).inc()
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
license_validation_errors_total.labels(
|
||||
error_type=type(e).__name__
|
||||
).inc()
|
||||
raise
|
||||
finally:
|
||||
duration = time.time() - start_time
|
||||
license_validation_duration_seconds.observe(duration)
|
||||
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# Initialize system info
|
||||
def init_metrics(version: str = "1.0.0"):
|
||||
"""Initialize system metrics"""
|
||||
system_info.info({
|
||||
'version': version,
|
||||
'service': 'license-server'
|
||||
})
|
||||
52
v2_lizenzserver/app/core/security.py
Normale Datei
52
v2_lizenzserver/app/core/security.py
Normale Datei
@ -0,0 +1,52 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import HTTPException, Security, Depends
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.database import get_db
|
||||
from app.models.models import ApiKey
|
||||
from app.core.config import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
security = HTTPBearer()
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
|
||||
token = credentials.credentials
|
||||
try:
|
||||
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
||||
return payload
|
||||
except JWTError:
|
||||
raise HTTPException(status_code=403, detail="Invalid token")
|
||||
|
||||
def verify_api_key(api_key: str, db: Session):
|
||||
key = db.query(ApiKey).filter(
|
||||
ApiKey.key == api_key,
|
||||
ApiKey.is_active == True
|
||||
).first()
|
||||
|
||||
if not key:
|
||||
raise HTTPException(status_code=401, detail="Invalid API key")
|
||||
|
||||
key.last_used = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return key
|
||||
|
||||
def get_api_key(
|
||||
credentials: HTTPAuthorizationCredentials = Security(security),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
api_key = credentials.credentials
|
||||
return verify_api_key(api_key, db)
|
||||
16
v2_lizenzserver/app/db/database.py
Normale Datei
16
v2_lizenzserver/app/db/database.py
Normale Datei
@ -0,0 +1,16 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
75
v2_lizenzserver/app/main.py
Normale Datei
75
v2_lizenzserver/app/main.py
Normale Datei
@ -0,0 +1,75 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
import uvicorn
|
||||
import logging
|
||||
from datetime import datetime
|
||||
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.db.database import engine, Base
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
# Initialize metrics
|
||||
init_metrics(version="1.0.0")
|
||||
|
||||
app = FastAPI(
|
||||
title="License Server API",
|
||||
description="API for software license management",
|
||||
version="1.0.0",
|
||||
docs_url="/docs" if settings.DEBUG else None,
|
||||
redoc_url="/redoc" if settings.DEBUG else None,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.ALLOWED_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request: Request, exc: Exception):
|
||||
logger.error(f"Global exception: {str(exc)}", exc_info=True)
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error"}
|
||||
)
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"status": "online",
|
||||
"service": "License Server",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
|
||||
@app.get("/metrics")
|
||||
async def metrics():
|
||||
"""Prometheus metrics endpoint"""
|
||||
return Response(content=generate_latest(), media_type=CONTENT_TYPE_LATEST)
|
||||
|
||||
app.include_router(license.router, prefix="/api/license", tags=["license"])
|
||||
app.include_router(version.router, prefix="/api/version", tags=["version"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host="0.0.0.0",
|
||||
port=8443,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
1
v2_lizenzserver/app/models/__init__.py
Normale Datei
1
v2_lizenzserver/app/models/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
from .models import License, Activation, Version, ApiKey
|
||||
65
v2_lizenzserver/app/models/models.py
Normale Datei
65
v2_lizenzserver/app/models/models.py
Normale Datei
@ -0,0 +1,65 @@
|
||||
from sqlalchemy import Column, String, Integer, Boolean, DateTime, ForeignKey, Text, JSON
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.database import Base
|
||||
import uuid
|
||||
|
||||
class License(Base):
|
||||
__tablename__ = "licenses"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
license_key = Column(String, unique=True, index=True, default=lambda: str(uuid.uuid4()))
|
||||
product_id = Column(String, nullable=False)
|
||||
customer_email = Column(String, nullable=False)
|
||||
customer_name = Column(String)
|
||||
|
||||
max_activations = 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")
|
||||
|
||||
class Activation(Base):
|
||||
__tablename__ = "activations"
|
||||
|
||||
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)
|
||||
|
||||
activation_date = Column(DateTime, server_default=func.now())
|
||||
last_heartbeat = Column(DateTime, server_default=func.now())
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
os_info = Column(JSON)
|
||||
app_version = Column(String)
|
||||
|
||||
license = relationship("License", back_populates="activations")
|
||||
|
||||
class Version(Base):
|
||||
__tablename__ = "versions"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
version_number = Column(String, unique=True, nullable=False)
|
||||
release_date = Column(DateTime, server_default=func.now())
|
||||
is_mandatory = Column(Boolean, default=False)
|
||||
min_version = Column(String)
|
||||
|
||||
download_url = Column(String)
|
||||
release_notes = Column(Text)
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
class ApiKey(Base):
|
||||
__tablename__ = "api_keys"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
key = Column(String, unique=True, index=True, default=lambda: str(uuid.uuid4()))
|
||||
name = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
last_used = Column(DateTime)
|
||||
8
v2_lizenzserver/app/schemas/__init__.py
Normale Datei
8
v2_lizenzserver/app/schemas/__init__.py
Normale Datei
@ -0,0 +1,8 @@
|
||||
from .license import (
|
||||
LicenseActivationRequest,
|
||||
LicenseActivationResponse,
|
||||
LicenseVerificationRequest,
|
||||
LicenseVerificationResponse,
|
||||
VersionCheckRequest,
|
||||
VersionCheckResponse
|
||||
)
|
||||
74
v2_lizenzserver/app/schemas/license.py
Normale Datei
74
v2_lizenzserver/app/schemas/license.py
Normale Datei
@ -0,0 +1,74 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
class LicenseActivationRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_hash: str
|
||||
os_info: Optional[Dict[str, Any]] = None
|
||||
app_version: Optional[str] = None
|
||||
|
||||
class LicenseActivationResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
activation_id: Optional[int] = None
|
||||
expires_at: Optional[datetime] = None
|
||||
features: Optional[Dict[str, Any]] = None
|
||||
|
||||
class LicenseVerificationRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_hash: str
|
||||
activation_id: int
|
||||
|
||||
class LicenseVerificationResponse(BaseModel):
|
||||
valid: bool
|
||||
message: str
|
||||
expires_at: Optional[datetime] = None
|
||||
features: Optional[Dict[str, Any]] = None
|
||||
requires_update: bool = False
|
||||
update_url: Optional[str] = None
|
||||
|
||||
class VersionCheckRequest(BaseModel):
|
||||
current_version: str
|
||||
license_key: str
|
||||
|
||||
class VersionCheckResponse(BaseModel):
|
||||
latest_version: str
|
||||
current_version: str
|
||||
update_available: bool
|
||||
is_mandatory: bool
|
||||
download_url: Optional[str] = None
|
||||
release_notes: Optional[str] = None
|
||||
|
||||
class SessionStartRequest(BaseModel):
|
||||
license_key: str
|
||||
machine_id: str
|
||||
hardware_id: str
|
||||
hardware_hash: str
|
||||
version: str
|
||||
ip_address: Optional[str] = None
|
||||
|
||||
class SessionStartResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
session_token: Optional[str] = None
|
||||
requires_update: bool = False
|
||||
update_url: Optional[str] = None
|
||||
whats_new: Optional[str] = None
|
||||
|
||||
class SessionHeartbeatRequest(BaseModel):
|
||||
session_token: str
|
||||
license_key: str
|
||||
|
||||
class SessionHeartbeatResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
class SessionEndRequest(BaseModel):
|
||||
session_token: str
|
||||
|
||||
class SessionEndResponse(BaseModel):
|
||||
success: bool
|
||||
message: str
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren