Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

3
v2_lizenzserver/.env Normale Datei
Datei anzeigen

@ -0,0 +1,3 @@
SECRET_KEY=your-super-secret-key-change-this-in-production-12345
DATABASE_URL=postgresql://adminuser:supergeheimespasswort@db:5432/meinedatenbank
DEBUG=False

Datei anzeigen

@ -0,0 +1,8 @@
# IMPORTANT: Generate a secure secret key using generate-secrets.py
SECRET_KEY=CHANGE_THIS_GENERATE_SECURE_SECRET
# Database connection (password should match v2/.env)
DATABASE_URL=postgresql://adminuser:CHANGE_THIS_STRONG_PASSWORD@db:5432/meinedatenbank
# Production mode
DEBUG=False

21
v2_lizenzserver/Dockerfile Normale Datei
Datei anzeigen

@ -0,0 +1,21 @@
FROM python:3.11-slim
WORKDIR /app
RUN apt-get update && apt-get install -y \
gcc \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app app/
COPY init_db.py .
COPY .env* ./
ENV PYTHONPATH=/app
EXPOSE 8443
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8443"]

Datei anzeigen

@ -0,0 +1 @@
from . import license, version

Datei anzeigen

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

Datei anzeigen

@ -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
}

Datei anzeigen

@ -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

Datei anzeigen

@ -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()

Datei anzeigen

@ -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'
})

Datei anzeigen

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

Datei anzeigen

@ -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
Datei anzeigen

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

Datei anzeigen

@ -0,0 +1 @@
from .models import License, Activation, Version, ApiKey

Datei anzeigen

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

Datei anzeigen

@ -0,0 +1,8 @@
from .license import (
LicenseActivationRequest,
LicenseActivationResponse,
LicenseVerificationRequest,
LicenseVerificationResponse,
VersionCheckRequest,
VersionCheckResponse
)

Datei anzeigen

@ -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

Datei anzeigen

@ -0,0 +1,534 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace LicenseClient
{
/// <summary>
/// Vollständige Lizenzserver-Integration für .NET-Anwendungen
/// </summary>
public class LicenseManager : IDisposable
{
private readonly HttpClient httpClient;
private readonly string apiKey;
private readonly string appVersion;
private readonly string serverUrl = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com";
private readonly string cacheFilePath;
private string licenseKey;
private int? activationId;
private DateTime? expiresAt;
private bool isValid;
private Timer heartbeatTimer;
private readonly SemaphoreSlim semaphore = new SemaphoreSlim(1, 1);
public LicenseManager(string apiKey, string appVersion = "1.0.0")
{
this.apiKey = apiKey;
this.appVersion = appVersion;
// HttpClient Setup
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true // Für Entwicklung
};
httpClient = new HttpClient(handler);
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
httpClient.Timeout = TimeSpan.FromSeconds(30);
// Cache-Verzeichnis
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
string appFolder = Path.Combine(appDataPath, "MyApp", "License");
Directory.CreateDirectory(appFolder);
cacheFilePath = Path.Combine(appFolder, "license.json");
}
/// <summary>
/// Eindeutige Maschinen-ID generieren
/// </summary>
private string GetMachineId()
{
try
{
// CPU-ID abrufen
string cpuId = "";
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor"))
{
foreach (ManagementObject obj in searcher.Get())
{
cpuId = obj["ProcessorId"]?.ToString() ?? "";
break;
}
}
// Motherboard Serial Number
string motherboardId = "";
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BaseBoard"))
{
foreach (ManagementObject obj in searcher.Get())
{
motherboardId = obj["SerialNumber"]?.ToString() ?? "";
break;
}
}
return $"{cpuId}-{motherboardId}";
}
catch
{
// Fallback: Machine Name + User
return $"{Environment.MachineName}-{Environment.UserName}";
}
}
/// <summary>
/// Hardware-Fingerprint erstellen
/// </summary>
private string GetHardwareHash()
{
var components = new List<string>
{
GetMachineId(),
Environment.MachineName,
Environment.OSVersion.ToString(),
Environment.ProcessorCount.ToString()
};
// MAC-Adressen hinzufügen
try
{
using (var searcher = new ManagementObjectSearcher("SELECT MACAddress FROM Win32_NetworkAdapter WHERE MACAddress IS NOT NULL"))
{
foreach (ManagementObject obj in searcher.Get())
{
string mac = obj["MACAddress"]?.ToString();
if (!string.IsNullOrEmpty(mac))
components.Add(mac);
}
}
}
catch { }
string combined = string.Join("-", components);
using (SHA256 sha256 = SHA256.Create())
{
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(combined));
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
}
}
/// <summary>
/// Lizenz-Cache speichern
/// </summary>
private async Task SaveLicenseCacheAsync()
{
var cacheData = new
{
license_key = licenseKey,
activation_id = activationId,
expires_at = expiresAt?.ToString("O"),
hardware_hash = GetHardwareHash(),
last_verified = DateTime.UtcNow.ToString("O")
};
string json = JsonSerializer.Serialize(cacheData, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(cacheFilePath, json);
}
/// <summary>
/// Lizenz-Cache laden
/// </summary>
private async Task<LicenseCache> LoadLicenseCacheAsync()
{
if (!File.Exists(cacheFilePath))
return null;
try
{
string json = await File.ReadAllTextAsync(cacheFilePath);
return JsonSerializer.Deserialize<LicenseCache>(json);
}
catch
{
return null;
}
}
/// <summary>
/// Lizenz aktivieren
/// </summary>
public async Task<(bool Success, string Message)> ActivateLicenseAsync(string licenseKey)
{
await semaphore.WaitAsync();
try
{
var requestData = new
{
license_key = licenseKey,
machine_id = GetMachineId(),
hardware_hash = GetHardwareHash(),
os_info = new
{
os = "Windows",
version = Environment.OSVersion.Version.ToString(),
platform = Environment.OSVersion.Platform.ToString(),
service_pack = Environment.OSVersion.ServicePack
},
app_version = appVersion
};
var json = JsonSerializer.Serialize(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/license/activate", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonSerializer.Deserialize<ActivationResponse>(responseContent);
if (result.success)
{
this.licenseKey = licenseKey;
this.activationId = result.activation_id;
this.isValid = true;
if (!string.IsNullOrEmpty(result.expires_at))
this.expiresAt = DateTime.Parse(result.expires_at);
await SaveLicenseCacheAsync();
StartHeartbeat();
return (true, result.message ?? "Lizenz erfolgreich aktiviert");
}
else
{
return (false, result.message ?? "Aktivierung fehlgeschlagen");
}
}
else
{
return (false, $"Server-Fehler: {response.StatusCode}");
}
}
catch (HttpRequestException ex)
{
return (false, $"Verbindungsfehler: {ex.Message}");
}
catch (Exception ex)
{
return (false, $"Fehler: {ex.Message}");
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// Lizenz verifizieren (Heartbeat)
/// </summary>
public async Task<(bool Valid, string Message)> VerifyLicenseAsync()
{
if (string.IsNullOrEmpty(licenseKey) || !activationId.HasValue)
return (false, "Keine aktive Lizenz");
await semaphore.WaitAsync();
try
{
var requestData = new
{
license_key = licenseKey,
machine_id = GetMachineId(),
hardware_hash = GetHardwareHash(),
activation_id = activationId.Value
};
var json = JsonSerializer.Serialize(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/license/verify", content);
var responseContent = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var result = JsonSerializer.Deserialize<VerificationResponse>(responseContent);
isValid = result.valid;
if (isValid)
{
await SaveLicenseCacheAsync();
if (result.requires_update)
{
OnUpdateAvailable?.Invoke(result.update_url);
}
}
return (isValid, result.message ?? "");
}
else
{
return (false, $"Server-Fehler: {response.StatusCode}");
}
}
catch (HttpRequestException)
{
// Offline-Verifizierung
return await VerifyOfflineAsync();
}
catch (Exception ex)
{
return (false, $"Fehler: {ex.Message}");
}
finally
{
semaphore.Release();
}
}
/// <summary>
/// Offline-Verifizierung mit Grace Period
/// </summary>
private async Task<(bool Valid, string Message)> VerifyOfflineAsync()
{
var cache = await LoadLicenseCacheAsync();
if (cache == null)
return (false, "Keine gecachte Lizenz vorhanden");
// Hardware-Hash prüfen
if (cache.hardware_hash != GetHardwareHash())
{
// Grace Period bei Hardware-Änderung
var lastVerified = DateTime.Parse(cache.last_verified);
var gracePeriod = TimeSpan.FromDays(7);
if (DateTime.UtcNow - lastVerified > gracePeriod)
return (false, "Hardware geändert - Grace Period abgelaufen");
}
// Ablaufdatum prüfen
if (!string.IsNullOrEmpty(cache.expires_at))
{
var expiresAt = DateTime.Parse(cache.expires_at);
if (DateTime.UtcNow > expiresAt)
return (false, "Lizenz abgelaufen");
}
licenseKey = cache.license_key;
activationId = cache.activation_id;
isValid = true;
return (true, "Offline-Modus (gecachte Lizenz)");
}
/// <summary>
/// Nach Updates suchen
/// </summary>
public async Task<UpdateInfo> CheckForUpdatesAsync()
{
if (string.IsNullOrEmpty(licenseKey))
return null;
try
{
var requestData = new
{
current_version = appVersion,
license_key = licenseKey
};
var json = JsonSerializer.Serialize(requestData);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync($"{serverUrl}/api/version/check", content);
if (response.IsSuccessStatusCode)
{
var responseContent = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<UpdateInfo>(responseContent);
}
}
catch { }
return null;
}
/// <summary>
/// Heartbeat starten
/// </summary>
private void StartHeartbeat()
{
heartbeatTimer?.Dispose();
// Alle 15 Minuten
heartbeatTimer = new Timer(async _ =>
{
var (valid, message) = await VerifyLicenseAsync();
if (!valid)
{
OnLicenseInvalid?.Invoke(message);
}
}, null, TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15));
}
/// <summary>
/// Lizenz-Informationen abrufen
/// </summary>
public LicenseInfo GetLicenseInfo()
{
return new LicenseInfo
{
IsValid = isValid,
LicenseKey = string.IsNullOrEmpty(licenseKey) ? null : licenseKey.Substring(0, 4) + "****",
ExpiresAt = expiresAt,
MachineId = GetMachineId()
};
}
// Events
public event Action<string> OnUpdateAvailable;
public event Action<string> OnLicenseInvalid;
public void Dispose()
{
heartbeatTimer?.Dispose();
httpClient?.Dispose();
semaphore?.Dispose();
}
}
// Hilfsklassen
public class LicenseCache
{
public string license_key { get; set; }
public int? activation_id { get; set; }
public string expires_at { get; set; }
public string hardware_hash { get; set; }
public string last_verified { get; set; }
}
public class ActivationResponse
{
public bool success { get; set; }
public string message { get; set; }
public int? activation_id { get; set; }
public string expires_at { get; set; }
}
public class VerificationResponse
{
public bool valid { get; set; }
public string message { get; set; }
public string expires_at { get; set; }
public bool requires_update { get; set; }
public string update_url { get; set; }
}
public class UpdateInfo
{
public string latest_version { get; set; }
public string current_version { get; set; }
public bool update_available { get; set; }
public bool is_mandatory { get; set; }
public string download_url { get; set; }
public string release_notes { get; set; }
}
public class LicenseInfo
{
public bool IsValid { get; set; }
public string LicenseKey { get; set; }
public DateTime? ExpiresAt { get; set; }
public string MachineId { get; set; }
}
// Beispiel-Anwendung
class Program
{
static async Task Main(string[] args)
{
// API-Key aus Umgebungsvariable oder Konfiguration
string apiKey = Environment.GetEnvironmentVariable("LICENSE_API_KEY") ?? "your-api-key-here";
using (var licenseManager = new LicenseManager(apiKey, "1.0.0"))
{
// Event-Handler registrieren
licenseManager.OnUpdateAvailable += url => Console.WriteLine($"Update verfügbar: {url}");
licenseManager.OnLicenseInvalid += msg => Console.WriteLine($"Lizenz ungültig: {msg}");
// Lizenz prüfen/aktivieren
var cache = await licenseManager.LoadLicenseCacheAsync();
if (cache != null)
{
Console.WriteLine("Gecachte Lizenz gefunden, verifiziere...");
var (valid, message) = await licenseManager.VerifyLicenseAsync();
if (!valid)
{
Console.WriteLine($"Lizenz ungültig: {message}");
await ActivateNewLicense(licenseManager);
}
else
{
Console.WriteLine($"✓ Lizenz gültig: {message}");
}
}
else
{
await ActivateNewLicense(licenseManager);
}
// Update-Check
var updateInfo = await licenseManager.CheckForUpdatesAsync();
if (updateInfo?.update_available == true)
{
Console.WriteLine($"Update verfügbar: {updateInfo.latest_version}");
if (updateInfo.is_mandatory)
Console.WriteLine("⚠️ Dies ist ein Pflicht-Update!");
}
// Lizenz-Info anzeigen
var info = licenseManager.GetLicenseInfo();
Console.WriteLine($"\nLizenz-Status:");
Console.WriteLine($"- Gültig: {info.IsValid}");
Console.WriteLine($"- Ablauf: {info.ExpiresAt}");
Console.WriteLine($"- Maschine: {info.MachineId}");
// App läuft...
Console.WriteLine("\n✓ Anwendung gestartet");
Console.WriteLine("Drücken Sie eine Taste zum Beenden...");
Console.ReadKey();
}
}
static async Task ActivateNewLicense(LicenseManager licenseManager)
{
Console.Write("Bitte Lizenzschlüssel eingeben: ");
string licenseKey = Console.ReadLine();
var (success, message) = await licenseManager.ActivateLicenseAsync(licenseKey);
if (success)
{
Console.WriteLine($"✓ {message}");
}
else
{
Console.WriteLine($"✗ {message}");
Environment.Exit(1);
}
}
}
}

Datei anzeigen

@ -0,0 +1,331 @@
#!/usr/bin/env python3
"""
Vollständiges Beispiel für die Integration des Lizenzservers in eine Python-Anwendung
"""
import requests
import hashlib
import platform
import uuid
import json
import os
import time
import threading
from datetime import datetime, timedelta
from pathlib import Path
class LicenseManager:
def __init__(self, api_key, app_version="1.0.0"):
self.api_key = api_key
self.app_version = app_version
self.server_url = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com"
self.headers = {"Authorization": f"Bearer {api_key}"}
# Cache-Verzeichnis
self.cache_dir = Path.home() / ".myapp" / "license"
self.cache_dir.mkdir(parents=True, exist_ok=True)
self.cache_file = self.cache_dir / "license.json"
# Lizenz-Status
self.license_key = None
self.activation_id = None
self.is_valid = False
self.expires_at = None
# Heartbeat Thread
self.heartbeat_thread = None
self.stop_heartbeat = False
def get_machine_id(self):
"""Eindeutige Maschinen-ID basierend auf MAC-Adresse"""
mac = uuid.getnode()
return f"MAC-{mac:012X}"
def get_hardware_hash(self):
"""Hardware-Fingerprint aus verschiedenen Systeminfos"""
components = [
self.get_machine_id(),
platform.processor(),
platform.system(),
platform.machine(),
platform.node()
]
combined = "-".join(components)
return hashlib.sha256(combined.encode()).hexdigest()
def save_license_cache(self):
"""Lizenzinfo lokal speichern für Offline-Betrieb"""
cache_data = {
"license_key": self.license_key,
"activation_id": self.activation_id,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"hardware_hash": self.get_hardware_hash(),
"last_verified": datetime.now().isoformat()
}
with open(self.cache_file, 'w') as f:
json.dump(cache_data, f)
def load_license_cache(self):
"""Gespeicherte Lizenz laden"""
if not self.cache_file.exists():
return None
try:
with open(self.cache_file, 'r') as f:
return json.load(f)
except:
return None
def activate_license(self, license_key):
"""Neue Lizenz aktivieren"""
data = {
"license_key": license_key,
"machine_id": self.get_machine_id(),
"hardware_hash": self.get_hardware_hash(),
"os_info": {
"os": platform.system(),
"version": platform.version(),
"release": platform.release(),
"machine": platform.machine()
},
"app_version": self.app_version
}
try:
response = requests.post(
f"{self.server_url}/api/license/activate",
headers=self.headers,
json=data,
timeout=10,
verify=True
)
if response.status_code == 200:
result = response.json()
if result.get("success"):
self.license_key = license_key
self.activation_id = result.get("activation_id")
self.is_valid = True
if result.get("expires_at"):
self.expires_at = datetime.fromisoformat(
result["expires_at"].replace("Z", "+00:00")
)
self.save_license_cache()
self.start_heartbeat()
return True, result.get("message", "Lizenz aktiviert")
else:
return False, result.get("message", "Aktivierung fehlgeschlagen")
else:
return False, f"Server-Fehler: {response.status_code}"
except requests.exceptions.RequestException as e:
return False, f"Verbindungsfehler: {str(e)}"
def verify_license(self):
"""Lizenz verifizieren (Heartbeat)"""
if not self.license_key or not self.activation_id:
return False, "Keine aktive Lizenz"
data = {
"license_key": self.license_key,
"machine_id": self.get_machine_id(),
"hardware_hash": self.get_hardware_hash(),
"activation_id": self.activation_id
}
try:
response = requests.post(
f"{self.server_url}/api/license/verify",
headers=self.headers,
json=data,
timeout=10,
verify=True
)
if response.status_code == 200:
result = response.json()
self.is_valid = result.get("valid", False)
if self.is_valid:
self.save_license_cache()
# Update-Check
if result.get("requires_update"):
print(f"Update verfügbar: {result.get('update_url')}")
return self.is_valid, result.get("message", "")
else:
return False, f"Server-Fehler: {response.status_code}"
except requests.exceptions.RequestException:
# Offline-Modus: Cache prüfen
return self.verify_offline()
def verify_offline(self):
"""Offline-Verifizierung mit Grace Period"""
cache = self.load_license_cache()
if not cache:
return False, "Keine gecachte Lizenz vorhanden"
# Hardware-Hash prüfen
if cache.get("hardware_hash") != self.get_hardware_hash():
# Grace Period bei Hardware-Änderung
last_verified = datetime.fromisoformat(cache.get("last_verified"))
grace_period = timedelta(days=7)
if datetime.now() - last_verified > grace_period:
return False, "Hardware geändert - Grace Period abgelaufen"
# Ablaufdatum prüfen
if cache.get("expires_at"):
expires_at = datetime.fromisoformat(cache.get("expires_at"))
if datetime.now() > expires_at:
return False, "Lizenz abgelaufen"
self.license_key = cache.get("license_key")
self.activation_id = cache.get("activation_id")
self.is_valid = True
return True, "Offline-Modus (gecachte Lizenz)"
def check_for_updates(self):
"""Nach Updates suchen"""
if not self.license_key:
return None
data = {
"current_version": self.app_version,
"license_key": self.license_key
}
try:
response = requests.post(
f"{self.server_url}/api/version/check",
headers=self.headers,
json=data,
timeout=10,
verify=True
)
if response.status_code == 200:
return response.json()
except:
pass
return None
def heartbeat_worker(self):
"""Background-Thread für regelmäßige Lizenzprüfung"""
while not self.stop_heartbeat:
time.sleep(900) # 15 Minuten
if self.stop_heartbeat:
break
valid, message = self.verify_license()
if not valid:
print(f"Lizenz-Warnung: {message}")
# Hier könnte die App reagieren (z.B. Features deaktivieren)
def start_heartbeat(self):
"""Heartbeat-Thread starten"""
if self.heartbeat_thread and self.heartbeat_thread.is_alive():
return
self.stop_heartbeat = False
self.heartbeat_thread = threading.Thread(
target=self.heartbeat_worker,
daemon=True
)
self.heartbeat_thread.start()
def stop_heartbeat_thread(self):
"""Heartbeat-Thread beenden"""
self.stop_heartbeat = True
if self.heartbeat_thread:
self.heartbeat_thread.join(timeout=1)
def get_license_info(self):
"""Aktuelle Lizenzinformationen"""
return {
"valid": self.is_valid,
"license_key": self.license_key[:4] + "****" if self.license_key else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"machine_id": self.get_machine_id()
}
# Beispiel-Anwendung
def main():
# API-Key sollte sicher gespeichert werden (z.B. verschlüsselt)
API_KEY = os.environ.get("LICENSE_API_KEY", "your-api-key-here")
# License Manager initialisieren
license_mgr = LicenseManager(API_KEY, app_version="1.0.0")
# Versuche gecachte Lizenz zu laden
cache = license_mgr.load_license_cache()
if cache:
print("Gecachte Lizenz gefunden, verifiziere...")
valid, message = license_mgr.verify_license()
if valid:
print(f"✓ Lizenz gültig: {message}")
else:
print(f"✗ Lizenz ungültig: {message}")
# Neue Lizenz erforderlich
license_key = input("Bitte Lizenzschlüssel eingeben: ")
success, message = license_mgr.activate_license(license_key)
if success:
print(f"{message}")
else:
print(f"{message}")
return
else:
# Erste Aktivierung
print("Keine Lizenz gefunden.")
license_key = input("Bitte Lizenzschlüssel eingeben: ")
success, message = license_mgr.activate_license(license_key)
if success:
print(f"{message}")
else:
print(f"{message}")
return
# Update-Check
print("\nPrüfe auf Updates...")
update_info = license_mgr.check_for_updates()
if update_info and update_info.get("update_available"):
print(f"Update verfügbar: {update_info.get('latest_version')}")
if update_info.get("is_mandatory"):
print("⚠️ Dies ist ein Pflicht-Update!")
# Lizenzinfo anzeigen
info = license_mgr.get_license_info()
print(f"\nLizenz-Status:")
print(f"- Gültig: {info['valid']}")
print(f"- Ablauf: {info['expires_at']}")
print(f"- Maschine: {info['machine_id']}")
# App läuft...
print("\n✓ Anwendung gestartet mit gültiger Lizenz")
try:
# Simuliere App-Laufzeit
while True:
time.sleep(1)
except KeyboardInterrupt:
print("\nBeende Anwendung...")
license_mgr.stop_heartbeat_thread()
if __name__ == "__main__":
main()

Datei anzeigen

@ -0,0 +1,48 @@
version: '3.8'
services:
license-server:
build: .
container_name: license-server
restart: unless-stopped
ports:
- "8443:8443"
environment:
- DATABASE_URL=postgresql://license_user:license_password@db:5432/license_db
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
networks:
- license-network
db:
image: postgres:15-alpine
container_name: license-db
restart: unless-stopped
environment:
POSTGRES_USER: license_user
POSTGRES_PASSWORD: license_password
POSTGRES_DB: license_db
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- license-network
redis:
image: redis:7-alpine
container_name: license-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- license-network
volumes:
postgres_data:
redis_data:
networks:
license-network:
driver: bridge

44
v2_lizenzserver/init_db.py Normale Datei
Datei anzeigen

@ -0,0 +1,44 @@
import sys
sys.path.append('/app')
from app.db.database import engine, Base
from app.models.models import License, Activation, Version, ApiKey
from sqlalchemy.orm import Session
import uuid
from datetime import datetime, timedelta
print("Creating database tables...")
Base.metadata.create_all(bind=engine)
with Session(engine) as db:
# Create a test API key
api_key = ApiKey(
key="test-api-key-12345",
name="Test API Key",
is_active=True
)
db.add(api_key)
# Create a test license
test_license = License(
license_key="TEST-LICENSE-KEY-12345",
product_id="software-v1",
customer_email="test@example.com",
customer_name="Test Customer",
max_activations=5,
expires_at=datetime.utcnow() + timedelta(days=365)
)
db.add(test_license)
# Create initial version
initial_version = Version(
version_number="1.0.0",
release_notes="Initial release",
is_mandatory=False
)
db.add(initial_version)
db.commit()
print("Database initialized successfully!")
print(f"Test API Key: test-api-key-12345")
print(f"Test License Key: TEST-LICENSE-KEY-12345")

Datei anzeigen

@ -0,0 +1,15 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.6
pydantic==2.5.0
pydantic-settings==2.1.0
alembic==1.12.1
python-dotenv==1.0.0
httpx==0.25.2
redis==5.0.1
packaging==23.2
prometheus-client==0.19.0

Datei anzeigen

@ -0,0 +1,29 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 admin && chown -R admin:admin /app
USER admin
# Expose port
EXPOSE 5004
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5004/health').raise_for_status()"
# Run the application
CMD ["python", "app.py"]

Datei anzeigen

@ -0,0 +1 @@
# Admin API Service Package

Datei anzeigen

@ -0,0 +1,744 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from datetime import datetime, timedelta
import os
import psycopg2
from psycopg2.extras import RealDictCursor
from psycopg2.pool import SimpleConnectionPool
import redis
import json
import logging
from functools import wraps
import jwt
import uuid
from typing import List, Dict, Optional
import bcrypt
from prometheus_flask_exporter import PrometheusMetrics
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# Initialize Prometheus metrics
metrics = PrometheusMetrics(app)
metrics.info('admin_api_service_info', 'Admin API Service Information', version='1.0.0')
# Configuration
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres@postgres:5432/v2_adminpanel')
REDIS_URL = os.environ.get('REDIS_URL', 'redis://redis:6379/3')
JWT_SECRET = os.environ.get('JWT_SECRET', 'your-secret-key')
SERVICE_PORT = 5004
# Database connection pool
db_pool = SimpleConnectionPool(1, 20, DATABASE_URL)
# Redis client
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
# JWT validation decorator with admin check
def require_admin_auth(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
# Check if user has admin privileges
if payload.get('type') not in ['admin_access', 'analytics_access']:
return jsonify({'error': 'Insufficient privileges'}), 403
request.jwt_payload = payload
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return wrapper
# Database query helpers
def execute_query(query, params=None, fetchall=True):
conn = db_pool.getconn()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(query, params)
if query.strip().upper().startswith(('INSERT', 'UPDATE', 'DELETE')):
conn.commit()
return cur.rowcount
if fetchall:
return cur.fetchall()
return cur.fetchone()
finally:
db_pool.putconn(conn)
def execute_batch(query, data):
conn = db_pool.getconn()
try:
with conn.cursor() as cur:
cur.executemany(query, data)
conn.commit()
return cur.rowcount
finally:
db_pool.putconn(conn)
# Audit logging
def log_admin_action(action: str, entity_type: str, entity_id: str, details: Dict, user_id: str = None):
"""Log admin actions to audit trail"""
query = """
INSERT INTO audit_log (username, action, timestamp, ip_address, additional_info)
VALUES (%s, %s, %s, %s, %s)
"""
username = user_id or request.jwt_payload.get('sub', 'system')
ip_address = request.headers.get('X-Real-IP', request.remote_addr)
additional_info = json.dumps({
'entity_type': entity_type,
'entity_id': entity_id,
'details': details
})
execute_query(query, [username, action, datetime.utcnow(), ip_address, additional_info])
# API Routes
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'service': 'admin-api-service',
'timestamp': datetime.utcnow().isoformat()
})
# License Management
@app.route('/api/v1/admin/licenses', methods=['GET'])
@require_admin_auth
def list_licenses():
"""List all licenses with filtering and pagination"""
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 50))
customer_id = request.args.get('customer_id')
is_active = request.args.get('is_active')
license_type = request.args.get('license_type')
offset = (page - 1) * per_page
# Build query with filters
query = """
SELECT l.*, c.name as customer_name, c.email as customer_email,
COUNT(DISTINCT lh.hardware_id) as active_devices,
MAX(lh.timestamp) as last_activity
FROM licenses l
LEFT JOIN customers c ON l.customer_id = c.id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '24 hours'
WHERE 1=1
"""
params = []
if customer_id:
query += " AND l.customer_id = %s"
params.append(customer_id)
if is_active is not None:
query += " AND l.is_active = %s"
params.append(is_active == 'true')
if license_type:
query += " AND l.license_type = %s"
params.append(license_type)
query += """
GROUP BY l.id, c.name, c.email
ORDER BY l.created_at DESC
LIMIT %s OFFSET %s
"""
params.extend([per_page, offset])
licenses = execute_query(query, params)
# Get total count
count_query = "SELECT COUNT(*) as total FROM licenses WHERE 1=1"
count_params = []
if customer_id:
count_query += " AND customer_id = %s"
count_params.append(customer_id)
if is_active is not None:
count_query += " AND is_active = %s"
count_params.append(is_active == 'true')
if license_type:
count_query += " AND license_type = %s"
count_params.append(license_type)
total = execute_query(count_query, count_params, fetchall=False)['total']
return jsonify({
'success': True,
'data': licenses,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'pages': (total + per_page - 1) // per_page
}
})
@app.route('/api/v1/admin/licenses/<license_id>', methods=['GET'])
@require_admin_auth
def get_license(license_id):
"""Get detailed license information"""
query = """
SELECT l.*, c.name as customer_name, c.email as customer_email,
array_agg(DISTINCT lh.hardware_id) as hardware_ids,
COUNT(DISTINCT lh.hardware_id) as device_count,
MIN(lh.timestamp) as first_activation,
MAX(lh.timestamp) as last_activity
FROM licenses l
LEFT JOIN customers c ON l.customer_id = c.id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
WHERE l.id = %s
GROUP BY l.id, c.name, c.email
"""
license_data = execute_query(query, [license_id], fetchall=False)
if not license_data:
return jsonify({'error': 'License not found'}), 404
# Get recent activity
activity_query = """
SELECT hardware_id, ip_address, timestamp, user_agent
FROM license_heartbeats
WHERE license_id = %s
ORDER BY timestamp DESC
LIMIT 20
"""
recent_activity = execute_query(activity_query, [license_id])
license_data['recent_activity'] = recent_activity
return jsonify({
'success': True,
'data': license_data
})
@app.route('/api/v1/admin/licenses', methods=['POST'])
@require_admin_auth
def create_license():
"""Create a new license"""
data = request.get_json()
required_fields = ['customer_id', 'license_type', 'device_limit']
if not all(field in data for field in required_fields):
return jsonify({'error': 'Missing required fields'}), 400
license_id = str(uuid.uuid4())
license_key = f"{data['license_type'].upper()}-{uuid.uuid4().hex[:8].upper()}-{uuid.uuid4().hex[:8].upper()}"
query = """
INSERT INTO licenses (id, customer_id, license_key, license_type,
device_limit, is_active, expires_at, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
expires_at = None
if data.get('expires_at'):
expires_at = datetime.fromisoformat(data['expires_at'])
params = [
license_id,
data['customer_id'],
license_key,
data['license_type'],
data['device_limit'],
data.get('is_active', True),
expires_at,
datetime.utcnow()
]
new_license = execute_query(query, params, fetchall=False)
log_admin_action('create_license', 'license', license_id, {
'license_key': license_key,
'customer_id': data['customer_id'],
'license_type': data['license_type']
})
return jsonify({
'success': True,
'data': new_license
}), 201
@app.route('/api/v1/admin/licenses/<license_id>', methods=['PUT'])
@require_admin_auth
def update_license(license_id):
"""Update license information"""
data = request.get_json()
# Build dynamic update query
update_fields = []
params = []
allowed_fields = ['is_active', 'device_limit', 'expires_at', 'notes']
for field in allowed_fields:
if field in data:
update_fields.append(f"{field} = %s")
params.append(data[field])
if not update_fields:
return jsonify({'error': 'No fields to update'}), 400
query = f"""
UPDATE licenses
SET {', '.join(update_fields)}, updated_at = %s
WHERE id = %s
RETURNING *
"""
params.extend([datetime.utcnow(), license_id])
updated_license = execute_query(query, params, fetchall=False)
if not updated_license:
return jsonify({'error': 'License not found'}), 404
log_admin_action('update_license', 'license', license_id, data)
# Clear cache
redis_client.delete(f"license:{license_id}")
return jsonify({
'success': True,
'data': updated_license
})
@app.route('/api/v1/admin/licenses/<license_id>', methods=['DELETE'])
@require_admin_auth
def delete_license(license_id):
"""Delete a license (soft delete by deactivating)"""
query = """
UPDATE licenses
SET is_active = false, updated_at = %s
WHERE id = %s
RETURNING *
"""
deleted_license = execute_query(query, [datetime.utcnow(), license_id], fetchall=False)
if not deleted_license:
return jsonify({'error': 'License not found'}), 404
log_admin_action('delete_license', 'license', license_id, {
'license_key': deleted_license['license_key']
})
# Clear cache
redis_client.delete(f"license:{license_id}")
return jsonify({
'success': True,
'message': 'License deactivated successfully'
})
# Batch Operations
@app.route('/api/v1/admin/licenses/batch', methods=['POST'])
@require_admin_auth
def batch_create_licenses():
"""Create multiple licenses at once"""
data = request.get_json()
if 'licenses' not in data or not isinstance(data['licenses'], list):
return jsonify({'error': 'Invalid request format'}), 400
created_licenses = []
for license_data in data['licenses']:
license_id = str(uuid.uuid4())
license_key = f"{license_data['license_type'].upper()}-{uuid.uuid4().hex[:8].upper()}-{uuid.uuid4().hex[:8].upper()}"
query = """
INSERT INTO licenses (id, customer_id, license_key, license_type,
device_limit, is_active, expires_at, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING *
"""
params = [
license_id,
license_data['customer_id'],
license_key,
license_data['license_type'],
license_data.get('device_limit', 1),
license_data.get('is_active', True),
license_data.get('expires_at'),
datetime.utcnow()
]
new_license = execute_query(query, params, fetchall=False)
created_licenses.append(new_license)
log_admin_action('batch_create_licenses', 'license', None, {
'count': len(created_licenses),
'customer_ids': list(set(l['customer_id'] for l in created_licenses))
})
return jsonify({
'success': True,
'data': created_licenses,
'count': len(created_licenses)
}), 201
@app.route('/api/v1/admin/licenses/batch/activate', methods=['POST'])
@require_admin_auth
def batch_activate_licenses():
"""Batch activate/deactivate licenses"""
data = request.get_json()
if 'license_ids' not in data or 'is_active' not in data:
return jsonify({'error': 'Missing required fields'}), 400
query = """
UPDATE licenses
SET is_active = %s, updated_at = %s
WHERE id = ANY(%s)
"""
affected = execute_query(
query,
[data['is_active'], datetime.utcnow(), data['license_ids']]
)
log_admin_action('batch_update_licenses', 'license', None, {
'action': 'activate' if data['is_active'] else 'deactivate',
'count': affected,
'license_ids': data['license_ids']
})
# Clear cache for all affected licenses
for license_id in data['license_ids']:
redis_client.delete(f"license:{license_id}")
return jsonify({
'success': True,
'affected': affected
})
# Customer Management
@app.route('/api/v1/admin/customers', methods=['GET'])
@require_admin_auth
def list_customers():
"""List all customers with stats"""
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 50))
search = request.args.get('search')
offset = (page - 1) * per_page
query = """
SELECT c.*,
COUNT(DISTINCT l.id) as license_count,
COUNT(DISTINCT CASE WHEN l.is_active THEN l.id END) as active_licenses,
MAX(lh.timestamp) as last_activity
FROM customers c
LEFT JOIN licenses l ON c.id = l.customer_id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '30 days'
"""
params = []
if search:
query += " WHERE c.name ILIKE %s OR c.email ILIKE %s"
params.extend([f'%{search}%', f'%{search}%'])
query += """
GROUP BY c.id
ORDER BY c.created_at DESC
LIMIT %s OFFSET %s
"""
params.extend([per_page, offset])
customers = execute_query(query, params)
# Get total count
count_query = "SELECT COUNT(*) as total FROM customers"
if search:
count_query += " WHERE name ILIKE %s OR email ILIKE %s"
total = execute_query(count_query, [f'%{search}%', f'%{search}%'], fetchall=False)['total']
else:
total = execute_query(count_query, fetchall=False)['total']
return jsonify({
'success': True,
'data': customers,
'pagination': {
'page': page,
'per_page': per_page,
'total': total,
'pages': (total + per_page - 1) // per_page
}
})
# System Configuration
@app.route('/api/v1/admin/config/feature-flags', methods=['GET'])
@require_admin_auth
def list_feature_flags():
"""List all feature flags"""
query = "SELECT * FROM feature_flags ORDER BY name"
flags = execute_query(query)
return jsonify({
'success': True,
'data': flags
})
@app.route('/api/v1/admin/config/feature-flags/<flag_id>', methods=['PUT'])
@require_admin_auth
def update_feature_flag(flag_id):
"""Update feature flag status"""
data = request.get_json()
if 'enabled' not in data:
return jsonify({'error': 'Missing enabled field'}), 400
query = """
UPDATE feature_flags
SET enabled = %s, updated_at = %s
WHERE id = %s
RETURNING *
"""
updated_flag = execute_query(
query,
[data['enabled'], datetime.utcnow(), flag_id],
fetchall=False
)
if not updated_flag:
return jsonify({'error': 'Feature flag not found'}), 404
log_admin_action('update_feature_flag', 'feature_flag', flag_id, {
'name': updated_flag['name'],
'enabled': data['enabled']
})
# Clear feature flag cache
redis_client.delete('feature_flags:all')
return jsonify({
'success': True,
'data': updated_flag
})
# API Key Management
@app.route('/api/v1/admin/api-keys', methods=['GET'])
@require_admin_auth
def list_api_keys():
"""List all API keys"""
query = """
SELECT ak.*, arl.requests_per_minute, arl.requests_per_hour
FROM api_clients ak
LEFT JOIN api_rate_limits arl ON ak.api_key = arl.api_key
ORDER BY ak.created_at DESC
"""
api_keys = execute_query(query)
return jsonify({
'success': True,
'data': api_keys
})
@app.route('/api/v1/admin/api-keys', methods=['POST'])
@require_admin_auth
def create_api_key():
"""Create new API key"""
data = request.get_json()
if 'name' not in data:
return jsonify({'error': 'Missing name field'}), 400
api_key = f"sk_{uuid.uuid4().hex}"
# Create API client
client_query = """
INSERT INTO api_clients (api_key, name, is_active, created_at)
VALUES (%s, %s, %s, %s)
RETURNING *
"""
new_client = execute_query(
client_query,
[api_key, data['name'], True, datetime.utcnow()],
fetchall=False
)
# Create rate limits
rate_query = """
INSERT INTO api_rate_limits (api_key, requests_per_minute, requests_per_hour, requests_per_day)
VALUES (%s, %s, %s, %s)
"""
execute_query(
rate_query,
[
api_key,
data.get('requests_per_minute', 60),
data.get('requests_per_hour', 1000),
data.get('requests_per_day', 10000)
]
)
log_admin_action('create_api_key', 'api_key', api_key, {
'name': data['name']
})
return jsonify({
'success': True,
'data': new_client
}), 201
# Audit Log
@app.route('/api/v1/admin/audit-log', methods=['GET'])
@require_admin_auth
def get_audit_log():
"""Get audit log entries"""
page = int(request.args.get('page', 1))
per_page = int(request.args.get('per_page', 100))
action = request.args.get('action')
username = request.args.get('username')
start_date = request.args.get('start_date')
end_date = request.args.get('end_date')
offset = (page - 1) * per_page
query = "SELECT * FROM audit_log WHERE 1=1"
params = []
if action:
query += " AND action = %s"
params.append(action)
if username:
query += " AND username = %s"
params.append(username)
if start_date:
query += " AND timestamp >= %s"
params.append(datetime.fromisoformat(start_date))
if end_date:
query += " AND timestamp <= %s"
params.append(datetime.fromisoformat(end_date))
query += " ORDER BY timestamp DESC LIMIT %s OFFSET %s"
params.extend([per_page, offset])
entries = execute_query(query, params)
return jsonify({
'success': True,
'data': entries
})
# Device Management
@app.route('/api/v1/admin/licenses/<license_id>/devices', methods=['GET'])
@require_admin_auth
def list_license_devices(license_id):
"""List all devices for a license"""
query = """
SELECT DISTINCT hardware_id,
MIN(timestamp) as first_seen,
MAX(timestamp) as last_seen,
COUNT(*) as total_heartbeats,
array_agg(DISTINCT ip_address) as ip_addresses
FROM license_heartbeats
WHERE license_id = %s
GROUP BY hardware_id
ORDER BY last_seen DESC
"""
devices = execute_query(query, [license_id])
return jsonify({
'success': True,
'data': devices
})
@app.route('/api/v1/admin/licenses/<license_id>/devices/<hardware_id>', methods=['DELETE'])
@require_admin_auth
def remove_device(license_id, hardware_id):
"""Remove a device from a license"""
# Mark device as inactive in activation events
query = """
INSERT INTO activation_events
(id, license_id, event_type, hardware_id, success, created_at)
VALUES (%s, %s, 'deactivation', %s, true, %s)
"""
execute_query(
query,
[str(uuid.uuid4()), license_id, hardware_id, datetime.utcnow()]
)
log_admin_action('remove_device', 'license', license_id, {
'hardware_id': hardware_id
})
return jsonify({
'success': True,
'message': 'Device removed successfully'
})
# System Stats
@app.route('/api/v1/admin/stats/overview', methods=['GET'])
@require_admin_auth
def get_system_overview():
"""Get system overview statistics"""
stats = {}
# License stats
license_stats = execute_query("""
SELECT
COUNT(*) as total_licenses,
COUNT(CASE WHEN is_active THEN 1 END) as active_licenses,
COUNT(CASE WHEN expires_at < NOW() THEN 1 END) as expired_licenses,
COUNT(CASE WHEN is_test THEN 1 END) as test_licenses
FROM licenses
""", fetchall=False)
stats['licenses'] = license_stats
# Customer stats
customer_stats = execute_query("""
SELECT
COUNT(*) as total_customers,
COUNT(CASE WHEN created_at > NOW() - INTERVAL '30 days' THEN 1 END) as new_customers
FROM customers
""", fetchall=False)
stats['customers'] = customer_stats
# Activity stats
activity_stats = execute_query("""
SELECT
COUNT(DISTINCT license_id) as active_licenses_24h,
COUNT(DISTINCT hardware_id) as active_devices_24h,
COUNT(*) as total_validations_24h
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '24 hours'
""", fetchall=False)
stats['activity'] = activity_stats
# Anomaly stats
anomaly_stats = execute_query("""
SELECT
COUNT(*) as total_anomalies,
COUNT(CASE WHEN resolved = false THEN 1 END) as unresolved_anomalies,
COUNT(CASE WHEN severity = 'critical' AND resolved = false THEN 1 END) as critical_anomalies
FROM anomaly_detections
WHERE detected_at > NOW() - INTERVAL '7 days'
""", fetchall=False)
stats['anomalies'] = anomaly_stats
return jsonify({
'success': True,
'data': stats,
'timestamp': datetime.utcnow().isoformat()
})
if __name__ == '__main__':
logger.info(f"Starting Admin API Service on port {SERVICE_PORT}")
app.run(host='0.0.0.0', port=SERVICE_PORT, debug=os.environ.get('FLASK_ENV') == 'development')

Datei anzeigen

@ -0,0 +1,10 @@
Flask==3.0.0
flask-cors==4.0.0
psycopg2-binary==2.9.9
redis==5.0.1
PyJWT==2.8.0
bcrypt==4.1.2
requests==2.31.0
python-dotenv==1.0.0
gunicorn==21.2.0
prometheus-flask-exporter==0.23.0

Datei anzeigen

@ -0,0 +1,29 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN useradd -m -u 1000 analytics && chown -R analytics:analytics /app
USER analytics
# Expose port
EXPOSE 5003
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD python -c "import requests; requests.get('http://localhost:5003/health').raise_for_status()"
# Run the application
CMD ["python", "app.py"]

Datei anzeigen

@ -0,0 +1 @@
# Analytics Service Package

Datei anzeigen

@ -0,0 +1,465 @@
from flask import Flask, jsonify, request
from flask_cors import CORS
from datetime import datetime, timedelta
import os
import psycopg2
from psycopg2.extras import RealDictCursor
from psycopg2.pool import SimpleConnectionPool
import redis
import json
import logging
from functools import wraps
import jwt
from collections import defaultdict
import numpy as np
from prometheus_flask_exporter import PrometheusMetrics
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = Flask(__name__)
CORS(app)
# Initialize Prometheus metrics
metrics = PrometheusMetrics(app)
metrics.info('analytics_service_info', 'Analytics Service Information', version='1.0.0')
# Configuration
DATABASE_URL = os.environ.get('DATABASE_URL', 'postgresql://postgres:postgres@postgres:5432/v2_adminpanel')
REDIS_URL = os.environ.get('REDIS_URL', 'redis://redis:6379/2')
JWT_SECRET = os.environ.get('JWT_SECRET', 'your-secret-key')
SERVICE_PORT = 5003
# Database connection pool
db_pool = SimpleConnectionPool(1, 20, DATABASE_URL)
# Redis client
redis_client = redis.from_url(REDIS_URL, decode_responses=True)
# Cache decorator
def cache_result(ttl=300):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
cache_key = f"analytics:{f.__name__}:{str(args)}:{str(kwargs)}"
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
result = f(*args, **kwargs)
redis_client.setex(cache_key, ttl, json.dumps(result, default=str))
return result
return wrapper
return decorator
# JWT validation decorator
def require_auth(f):
@wraps(f)
def wrapper(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid authorization header'}), 401
token = auth_header.split(' ')[1]
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
request.jwt_payload = payload
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return wrapper
# Database query helper
def execute_query(query, params=None, fetchall=True):
conn = db_pool.getconn()
try:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute(query, params)
if fetchall:
return cur.fetchall()
return cur.fetchone()
finally:
db_pool.putconn(conn)
# Analytics calculations
class AnalyticsService:
@staticmethod
@cache_result(ttl=60)
def get_usage_statistics(customer_id=None, days=30):
"""Get usage statistics for licenses"""
base_query = """
SELECT
DATE(lh.timestamp) as date,
COUNT(DISTINCT lh.license_id) as active_licenses,
COUNT(DISTINCT lh.hardware_id) as active_devices,
COUNT(*) as total_heartbeats,
COUNT(DISTINCT lh.session_data->>'app_version') as app_versions
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
WHERE lh.timestamp >= NOW() - INTERVAL '%s days'
"""
params = [days]
if customer_id:
base_query += " AND l.customer_id = %s"
params.append(customer_id)
base_query += " GROUP BY DATE(lh.timestamp) ORDER BY date DESC"
return execute_query(base_query, params)
@staticmethod
@cache_result(ttl=300)
def get_performance_metrics(days=7):
"""Get system performance metrics"""
query = """
SELECT
DATE_TRUNC('hour', timestamp) as hour,
AVG(EXTRACT(EPOCH FROM (timestamp - LAG(timestamp) OVER (PARTITION BY license_id ORDER BY timestamp)))) as avg_heartbeat_interval,
COUNT(*) as validation_count,
COUNT(DISTINCT license_id) as unique_licenses,
COUNT(DISTINCT hardware_id) as unique_devices
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '%s days'
GROUP BY DATE_TRUNC('hour', timestamp)
ORDER BY hour DESC
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=120)
def get_anomaly_statistics(days=30):
"""Get anomaly detection statistics"""
query = """
SELECT
anomaly_type,
severity,
COUNT(*) as count,
COUNT(CASE WHEN resolved = false THEN 1 END) as unresolved_count,
AVG(CASE WHEN resolved = true THEN EXTRACT(EPOCH FROM (resolved_at - detected_at))/3600 END) as avg_resolution_hours
FROM anomaly_detections
WHERE detected_at >= NOW() - INTERVAL '%s days'
GROUP BY anomaly_type, severity
ORDER BY count DESC
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=300)
def get_license_distribution():
"""Get license distribution statistics"""
query = """
SELECT
l.license_type,
l.is_test,
COUNT(*) as total_count,
COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_count,
COUNT(CASE WHEN lh.timestamp >= NOW() - INTERVAL '1 hour' THEN 1 END) as recently_active,
AVG(l.device_limit) as avg_device_limit
FROM licenses l
LEFT JOIN LATERAL (
SELECT timestamp
FROM license_heartbeats
WHERE license_id = l.id
ORDER BY timestamp DESC
LIMIT 1
) lh ON true
GROUP BY l.license_type, l.is_test
"""
return execute_query(query)
@staticmethod
def get_revenue_impact(days=30):
"""Calculate revenue impact from license usage"""
query = """
WITH license_activity AS (
SELECT
l.id,
l.customer_id,
l.license_type,
l.price,
COUNT(DISTINCT DATE(lh.timestamp)) as active_days,
COUNT(DISTINCT lh.hardware_id) as devices_used
FROM licenses l
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp >= NOW() - INTERVAL '%s days'
WHERE l.is_test = false
GROUP BY l.id, l.customer_id, l.license_type, l.price
)
SELECT
license_type,
COUNT(*) as total_licenses,
SUM(price) as total_revenue,
AVG(active_days) as avg_active_days,
AVG(devices_used) as avg_devices_used,
SUM(CASE WHEN active_days > 0 THEN price ELSE 0 END) as active_revenue,
SUM(CASE WHEN active_days = 0 THEN price ELSE 0 END) as inactive_revenue
FROM license_activity
GROUP BY license_type
"""
return execute_query(query, [days])
@staticmethod
@cache_result(ttl=600)
def get_geographic_distribution():
"""Get geographic distribution of license usage"""
query = """
SELECT
lh.ip_address::text,
COUNT(DISTINCT lh.license_id) as license_count,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(*) as total_validations,
MAX(lh.timestamp) as last_seen
FROM license_heartbeats lh
WHERE lh.timestamp >= NOW() - INTERVAL '24 hours'
AND lh.ip_address IS NOT NULL
GROUP BY lh.ip_address
ORDER BY total_validations DESC
LIMIT 100
"""
return execute_query(query)
@staticmethod
def get_usage_patterns(license_id=None):
"""Analyze usage patterns for predictive analytics"""
base_query = """
WITH hourly_usage AS (
SELECT
EXTRACT(HOUR FROM timestamp) as hour_of_day,
EXTRACT(DOW FROM timestamp) as day_of_week,
COUNT(*) as usage_count
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '30 days'
"""
params = []
if license_id:
base_query += " AND license_id = %s"
params.append(license_id)
base_query += """
GROUP BY hour_of_day, day_of_week
)
SELECT
hour_of_day,
day_of_week,
usage_count,
AVG(usage_count) OVER (PARTITION BY hour_of_day) as avg_hourly_usage,
AVG(usage_count) OVER (PARTITION BY day_of_week) as avg_daily_usage
FROM hourly_usage
ORDER BY day_of_week, hour_of_day
"""
return execute_query(base_query, params)
@staticmethod
def calculate_churn_risk():
"""Calculate churn risk based on usage patterns"""
query = """
WITH recent_activity AS (
SELECT
l.id,
l.customer_id,
l.expires_at,
MAX(lh.timestamp) as last_activity,
COUNT(DISTINCT DATE(lh.timestamp)) as active_days_30d,
COUNT(DISTINCT DATE(lh.timestamp)) FILTER (WHERE lh.timestamp >= NOW() - INTERVAL '7 days') as active_days_7d
FROM licenses l
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp >= NOW() - INTERVAL '30 days'
WHERE l.is_test = false
GROUP BY l.id, l.customer_id, l.expires_at
)
SELECT
customer_id,
COUNT(*) as total_licenses,
AVG(EXTRACT(EPOCH FROM (NOW() - last_activity))/86400) as avg_days_since_activity,
AVG(active_days_30d) as avg_active_days_30d,
AVG(active_days_7d) as avg_active_days_7d,
MIN(expires_at) as next_expiry,
CASE
WHEN AVG(active_days_7d) = 0 AND AVG(active_days_30d) > 0 THEN 'high'
WHEN AVG(active_days_30d) < 5 THEN 'medium'
ELSE 'low'
END as churn_risk
FROM recent_activity
GROUP BY customer_id
HAVING COUNT(*) > 0
ORDER BY churn_risk DESC, avg_days_since_activity DESC
"""
return execute_query(query)
# API Routes
@app.route('/health', methods=['GET'])
def health_check():
"""Health check endpoint"""
return jsonify({
'status': 'healthy',
'service': 'analytics-service',
'timestamp': datetime.utcnow().isoformat()
})
@app.route('/api/v1/analytics/usage', methods=['GET'])
@require_auth
def get_usage_stats():
"""Get usage statistics"""
customer_id = request.args.get('customer_id')
days = int(request.args.get('days', 30))
stats = AnalyticsService.get_usage_statistics(customer_id, days)
return jsonify({
'success': True,
'data': stats,
'period_days': days,
'customer_id': customer_id
})
@app.route('/api/v1/analytics/performance', methods=['GET'])
@require_auth
def get_performance():
"""Get performance metrics"""
days = int(request.args.get('days', 7))
metrics = AnalyticsService.get_performance_metrics(days)
return jsonify({
'success': True,
'data': metrics,
'period_days': days
})
@app.route('/api/v1/analytics/anomalies', methods=['GET'])
@require_auth
def get_anomalies():
"""Get anomaly statistics"""
days = int(request.args.get('days', 30))
anomalies = AnalyticsService.get_anomaly_statistics(days)
return jsonify({
'success': True,
'data': anomalies,
'period_days': days
})
@app.route('/api/v1/analytics/distribution', methods=['GET'])
@require_auth
def get_distribution():
"""Get license distribution"""
distribution = AnalyticsService.get_license_distribution()
return jsonify({
'success': True,
'data': distribution
})
@app.route('/api/v1/analytics/revenue', methods=['GET'])
@require_auth
def get_revenue():
"""Get revenue impact analysis"""
days = int(request.args.get('days', 30))
revenue = AnalyticsService.get_revenue_impact(days)
return jsonify({
'success': True,
'data': revenue,
'period_days': days
})
@app.route('/api/v1/analytics/geographic', methods=['GET'])
@require_auth
def get_geographic():
"""Get geographic distribution"""
geo_data = AnalyticsService.get_geographic_distribution()
return jsonify({
'success': True,
'data': geo_data
})
@app.route('/api/v1/analytics/patterns', methods=['GET'])
@require_auth
def get_patterns():
"""Get usage patterns"""
license_id = request.args.get('license_id')
patterns = AnalyticsService.get_usage_patterns(license_id)
return jsonify({
'success': True,
'data': patterns,
'license_id': license_id
})
@app.route('/api/v1/analytics/churn-risk', methods=['GET'])
@require_auth
def get_churn_risk():
"""Get churn risk analysis"""
churn_data = AnalyticsService.calculate_churn_risk()
return jsonify({
'success': True,
'data': churn_data
})
@app.route('/api/v1/analytics/summary/<customer_id>', methods=['GET'])
@require_auth
def get_customer_summary(customer_id):
"""Get comprehensive analytics summary for a customer"""
usage = AnalyticsService.get_usage_statistics(customer_id, 30)
# Calculate summary metrics
total_heartbeats = sum(day['total_heartbeats'] for day in usage)
active_days = len([day for day in usage if day['active_licenses'] > 0])
return jsonify({
'success': True,
'customer_id': customer_id,
'summary': {
'total_heartbeats_30d': total_heartbeats,
'active_days_30d': active_days,
'average_daily_devices': np.mean([day['active_devices'] for day in usage]) if usage else 0,
'usage_trend': usage[:7] if len(usage) >= 7 else usage
}
})
# Real-time analytics endpoint (for websocket in future)
@app.route('/api/v1/analytics/realtime', methods=['GET'])
@require_auth
def get_realtime_stats():
"""Get real-time statistics for dashboard"""
# Get stats from last 5 minutes
query = """
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(DISTINCT hardware_id) as active_devices,
COUNT(*) as validations_5min,
COUNT(*) / 5.0 as validations_per_minute
FROM license_heartbeats
WHERE timestamp >= NOW() - INTERVAL '5 minutes'
"""
realtime = execute_query(query, fetchall=False)
# Get current anomalies
anomaly_query = """
SELECT COUNT(*) as unresolved_anomalies
FROM anomaly_detections
WHERE resolved = false
"""
anomalies = execute_query(anomaly_query, fetchall=False)
return jsonify({
'success': True,
'timestamp': datetime.utcnow().isoformat(),
'data': {
'active_licenses': realtime['active_licenses'] or 0,
'active_devices': realtime['active_devices'] or 0,
'validations_5min': realtime['validations_5min'] or 0,
'validations_per_minute': float(realtime['validations_per_minute'] or 0),
'unresolved_anomalies': anomalies['unresolved_anomalies'] or 0
}
})
if __name__ == '__main__':
logger.info(f"Starting Analytics Service on port {SERVICE_PORT}")
app.run(host='0.0.0.0', port=SERVICE_PORT, debug=os.environ.get('FLASK_ENV') == 'development')

Datei anzeigen

@ -0,0 +1,10 @@
Flask==3.0.0
flask-cors==4.0.0
psycopg2-binary==2.9.9
redis==5.0.1
PyJWT==2.8.0
numpy==1.26.2
requests==2.31.0
python-dotenv==1.0.0
gunicorn==21.2.0
prometheus-flask-exporter==0.23.0

39
v2_lizenzserver/test_api.py Normale Datei
Datei anzeigen

@ -0,0 +1,39 @@
import httpx
import asyncio
API_URL = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com"
API_KEY = "test-api-key-12345"
async def test_license_server():
async with httpx.AsyncClient(verify=False) as client:
# Test root endpoint
print("Testing root endpoint...")
response = await client.get(f"{API_URL}/")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}\n")
# Test health endpoint
print("Testing health endpoint...")
response = await client.get(f"{API_URL}/health")
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}\n")
# Test license activation (will fail without valid API key)
print("Testing license activation...")
headers = {"Authorization": f"Bearer {API_KEY}"}
data = {
"license_key": "TEST-LICENSE-KEY-12345",
"machine_id": "MACHINE001",
"hardware_hash": "abc123def456",
"app_version": "1.0.0"
}
response = await client.post(
f"{API_URL}/api/license/activate",
headers=headers,
json=data
)
print(f"Status: {response.status_code}")
print(f"Response: {response.json()}\n")
if __name__ == "__main__":
asyncio.run(test_license_server())