Initial commit
Dieser Commit ist enthalten in:
3
v2_lizenzserver/.env
Normale Datei
3
v2_lizenzserver/.env
Normale Datei
@ -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
|
||||
8
v2_lizenzserver/.env.production.template
Normale Datei
8
v2_lizenzserver/.env.production.template
Normale Datei
@ -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
21
v2_lizenzserver/Dockerfile
Normale Datei
@ -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"]
|
||||
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
|
||||
534
v2_lizenzserver/client_examples/csharp_client.cs
Normale Datei
534
v2_lizenzserver/client_examples/csharp_client.cs
Normale Datei
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
v2_lizenzserver/client_examples/python_client.py
Normale Datei
331
v2_lizenzserver/client_examples/python_client.py
Normale Datei
@ -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()
|
||||
48
v2_lizenzserver/docker-compose.yml
Normale Datei
48
v2_lizenzserver/docker-compose.yml
Normale Datei
@ -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
44
v2_lizenzserver/init_db.py
Normale Datei
@ -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")
|
||||
15
v2_lizenzserver/requirements.txt
Normale Datei
15
v2_lizenzserver/requirements.txt
Normale Datei
@ -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
|
||||
29
v2_lizenzserver/services/admin/Dockerfile
Normale Datei
29
v2_lizenzserver/services/admin/Dockerfile
Normale Datei
@ -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"]
|
||||
1
v2_lizenzserver/services/admin/__init__.py
Normale Datei
1
v2_lizenzserver/services/admin/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
# Admin API Service Package
|
||||
744
v2_lizenzserver/services/admin/app.py
Normale Datei
744
v2_lizenzserver/services/admin/app.py
Normale Datei
@ -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')
|
||||
10
v2_lizenzserver/services/admin/requirements.txt
Normale Datei
10
v2_lizenzserver/services/admin/requirements.txt
Normale Datei
@ -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
|
||||
29
v2_lizenzserver/services/analytics/Dockerfile
Normale Datei
29
v2_lizenzserver/services/analytics/Dockerfile
Normale Datei
@ -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"]
|
||||
1
v2_lizenzserver/services/analytics/__init__.py
Normale Datei
1
v2_lizenzserver/services/analytics/__init__.py
Normale Datei
@ -0,0 +1 @@
|
||||
# Analytics Service Package
|
||||
465
v2_lizenzserver/services/analytics/app.py
Normale Datei
465
v2_lizenzserver/services/analytics/app.py
Normale Datei
@ -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')
|
||||
10
v2_lizenzserver/services/analytics/requirements.txt
Normale Datei
10
v2_lizenzserver/services/analytics/requirements.txt
Normale Datei
@ -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
39
v2_lizenzserver/test_api.py
Normale Datei
@ -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())
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren