lizenzserver
Dieser Commit ist enthalten in:
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
1
backups/backup_v2docker_20250615_232911_encrypted.sql.gz.enc
Normale Datei
1
backups/backup_v2docker_20250615_232911_encrypted.sql.gz.enc
Normale Datei
Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist
@@ -2743,6 +2743,7 @@ def export_licenses():
|
|||||||
|
|
||||||
# Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
|
# Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht)
|
||||||
include_test = request.args.get('include_test', 'false').lower() == 'true'
|
include_test = request.args.get('include_test', 'false').lower() == 'true'
|
||||||
|
customer_id = request.args.get('customer_id', type=int)
|
||||||
|
|
||||||
query = """
|
query = """
|
||||||
SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email,
|
SELECT l.id, l.license_key, c.name as customer_name, c.email as customer_email,
|
||||||
@@ -2757,12 +2758,23 @@ def export_licenses():
|
|||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Build WHERE clause
|
||||||
|
where_conditions = []
|
||||||
|
params = []
|
||||||
|
|
||||||
if not include_test:
|
if not include_test:
|
||||||
query += " WHERE l.is_test = FALSE"
|
where_conditions.append("l.is_test = FALSE")
|
||||||
|
|
||||||
|
if customer_id:
|
||||||
|
where_conditions.append("l.customer_id = %s")
|
||||||
|
params.append(customer_id)
|
||||||
|
|
||||||
|
if where_conditions:
|
||||||
|
query += " WHERE " + " AND ".join(where_conditions)
|
||||||
|
|
||||||
query += " ORDER BY l.id"
|
query += " ORDER BY l.id"
|
||||||
|
|
||||||
cur.execute(query)
|
cur.execute(query, params)
|
||||||
|
|
||||||
# Spaltennamen
|
# Spaltennamen
|
||||||
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ',
|
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ',
|
||||||
@@ -2963,20 +2975,40 @@ def export_customers():
|
|||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
|
|
||||||
# Alle Kunden mit Lizenzstatistiken (ohne Testdaten)
|
# Check if test data should be included
|
||||||
cur.execute("""
|
include_test = request.args.get('include_test', 'false').lower() == 'true'
|
||||||
SELECT c.id, c.name, c.email, c.created_at,
|
|
||||||
COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
|
# Build query based on test data filter
|
||||||
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
|
if include_test:
|
||||||
COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
|
# Include all customers
|
||||||
FROM customers c
|
query = """
|
||||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
SELECT c.id, c.name, c.email, c.created_at, c.is_test,
|
||||||
GROUP BY c.id, c.name, c.email, c.created_at
|
COUNT(l.id) as total_licenses,
|
||||||
ORDER BY c.id
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses,
|
||||||
""")
|
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
|
||||||
|
FROM customers c
|
||||||
|
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||||
|
GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
|
||||||
|
ORDER BY c.id
|
||||||
|
"""
|
||||||
|
else:
|
||||||
|
# Exclude test customers and test licenses
|
||||||
|
query = """
|
||||||
|
SELECT c.id, c.name, c.email, c.created_at, c.is_test,
|
||||||
|
COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses,
|
||||||
|
COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses,
|
||||||
|
COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses
|
||||||
|
FROM customers c
|
||||||
|
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||||
|
WHERE c.is_test = FALSE
|
||||||
|
GROUP BY c.id, c.name, c.email, c.created_at, c.is_test
|
||||||
|
ORDER BY c.id
|
||||||
|
"""
|
||||||
|
|
||||||
|
cur.execute(query)
|
||||||
|
|
||||||
# Spaltennamen
|
# Spaltennamen
|
||||||
columns = ['ID', 'Name', 'E-Mail', 'Erstellt am',
|
columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten',
|
||||||
'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
|
'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
|
||||||
|
|
||||||
# Daten in DataFrame
|
# Daten in DataFrame
|
||||||
@@ -2986,6 +3018,9 @@ def export_customers():
|
|||||||
# Datumsformatierung
|
# Datumsformatierung
|
||||||
df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
|
df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M')
|
||||||
|
|
||||||
|
# Testdaten formatting
|
||||||
|
df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'})
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|||||||
4
v2_lizenzserver/.env
Normale Datei
4
v2_lizenzserver/.env
Normale Datei
@@ -0,0 +1,4 @@
|
|||||||
|
SECRET_KEY=your-super-secret-key-change-this-in-production-12345
|
||||||
|
DATABASE_URL=postgresql://adminuser:supergeheimespasswort@db:5432/meinedatenbank
|
||||||
|
REDIS_URL=redis://redis:6379
|
||||||
|
DEBUG=False
|
||||||
@@ -1,18 +1,21 @@
|
|||||||
FROM python:3.11-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# Zeitzone setzen
|
|
||||||
ENV TZ=Europe/Berlin
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y tzdata \
|
|
||||||
&& ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
|
|
||||||
&& echo "Europe/Berlin" > /etc/timezone \
|
|
||||||
&& apt-get clean \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Placeholder für Lizenzserver
|
RUN apt-get update && apt-get install -y \
|
||||||
RUN echo "Lizenzserver noch nicht implementiert" > info.txt
|
gcc \
|
||||||
|
postgresql-client \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
CMD ["python", "-c", "print('Lizenzserver Container läuft, aber noch keine Implementierung vorhanden'); import time; time.sleep(86400)"]
|
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"]
|
||||||
335
v2_lizenzserver/LIZENZSERVER_ANLEITUNG.md
Normale Datei
335
v2_lizenzserver/LIZENZSERVER_ANLEITUNG.md
Normale Datei
@@ -0,0 +1,335 @@
|
|||||||
|
# Lizenzserver Nutzungsanleitung
|
||||||
|
|
||||||
|
## Übersicht
|
||||||
|
Der Lizenzserver läuft unter: `https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### 1. Lizenz aktivieren
|
||||||
|
**POST** `/api/license/activate`
|
||||||
|
|
||||||
|
Aktiviert eine Lizenz für eine bestimmte Maschine.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||||
|
"machine_id": "UNIQUE-MACHINE-ID",
|
||||||
|
"hardware_hash": "SHA256-HARDWARE-FINGERPRINT",
|
||||||
|
"os_info": {
|
||||||
|
"os": "Windows",
|
||||||
|
"version": "10.0.19041"
|
||||||
|
},
|
||||||
|
"app_version": "1.0.0"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwort bei Erfolg:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "License activated successfully",
|
||||||
|
"activation_id": 123,
|
||||||
|
"expires_at": "2025-12-31T23:59:59",
|
||||||
|
"features": {
|
||||||
|
"all_features": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Lizenz verifizieren (Heartbeat)
|
||||||
|
**POST** `/api/license/verify`
|
||||||
|
|
||||||
|
Sollte alle 15 Minuten aufgerufen werden.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"license_key": "XXXX-XXXX-XXXX-XXXX",
|
||||||
|
"machine_id": "UNIQUE-MACHINE-ID",
|
||||||
|
"hardware_hash": "SHA256-HARDWARE-FINGERPRINT",
|
||||||
|
"activation_id": 123
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"valid": true,
|
||||||
|
"message": "License is valid",
|
||||||
|
"expires_at": "2025-12-31T23:59:59",
|
||||||
|
"features": {
|
||||||
|
"all_features": true
|
||||||
|
},
|
||||||
|
"requires_update": false,
|
||||||
|
"update_url": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Version prüfen
|
||||||
|
**POST** `/api/version/check`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"current_version": "1.0.0",
|
||||||
|
"license_key": "XXXX-XXXX-XXXX-XXXX"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Antwort:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"latest_version": "1.1.0",
|
||||||
|
"current_version": "1.0.0",
|
||||||
|
"update_available": true,
|
||||||
|
"is_mandatory": false,
|
||||||
|
"download_url": "https://download.example.com/v1.1.0",
|
||||||
|
"release_notes": "Bug fixes and improvements"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Lizenzinfo abrufen
|
||||||
|
**GET** `/api/license/info/{license_key}`
|
||||||
|
|
||||||
|
Liefert detaillierte Informationen über eine Lizenz.
|
||||||
|
|
||||||
|
## Authentifizierung
|
||||||
|
|
||||||
|
Alle API-Anfragen benötigen einen API-Key im Authorization Header:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer YOUR-API-KEY-HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-Integration Beispiel (Python)
|
||||||
|
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
import hashlib
|
||||||
|
import platform
|
||||||
|
import uuid
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
class LicenseClient:
|
||||||
|
def __init__(self, api_key, server_url="https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com"):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.server_url = server_url
|
||||||
|
self.headers = {"Authorization": f"Bearer {api_key}"}
|
||||||
|
|
||||||
|
def get_machine_id(self):
|
||||||
|
"""Eindeutige Maschinen-ID generieren"""
|
||||||
|
return str(uuid.getnode()) # MAC-Adresse
|
||||||
|
|
||||||
|
def get_hardware_hash(self):
|
||||||
|
"""Hardware-Fingerprint erstellen"""
|
||||||
|
machine_id = self.get_machine_id()
|
||||||
|
cpu_info = platform.processor()
|
||||||
|
system_info = f"{platform.system()}-{platform.machine()}"
|
||||||
|
|
||||||
|
combined = f"{machine_id}-{cpu_info}-{system_info}"
|
||||||
|
return hashlib.sha256(combined.encode()).hexdigest()
|
||||||
|
|
||||||
|
def activate_license(self, license_key):
|
||||||
|
"""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(),
|
||||||
|
"machine": platform.machine()
|
||||||
|
},
|
||||||
|
"app_version": "1.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/api/license/activate",
|
||||||
|
headers=self.headers,
|
||||||
|
json=data,
|
||||||
|
verify=True # SSL-Zertifikat prüfen
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def verify_license(self, license_key, activation_id):
|
||||||
|
"""Lizenz verifizieren (Heartbeat)"""
|
||||||
|
data = {
|
||||||
|
"license_key": license_key,
|
||||||
|
"machine_id": self.get_machine_id(),
|
||||||
|
"hardware_hash": self.get_hardware_hash(),
|
||||||
|
"activation_id": activation_id
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/api/license/verify",
|
||||||
|
headers=self.headers,
|
||||||
|
json=data,
|
||||||
|
verify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def check_version(self, license_key, current_version):
|
||||||
|
"""Version prüfen"""
|
||||||
|
data = {
|
||||||
|
"current_version": current_version,
|
||||||
|
"license_key": license_key
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(
|
||||||
|
f"{self.server_url}/api/version/check",
|
||||||
|
headers=self.headers,
|
||||||
|
json=data,
|
||||||
|
verify=True
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
# Verwendungsbeispiel
|
||||||
|
if __name__ == "__main__":
|
||||||
|
client = LicenseClient("YOUR-API-KEY")
|
||||||
|
|
||||||
|
# Lizenz aktivieren
|
||||||
|
result = client.activate_license("USER-LICENSE-KEY")
|
||||||
|
if result["success"]:
|
||||||
|
activation_id = result["activation_id"]
|
||||||
|
print(f"Lizenz aktiviert! Activation ID: {activation_id}")
|
||||||
|
|
||||||
|
# Lizenz verifizieren
|
||||||
|
verify_result = client.verify_license("USER-LICENSE-KEY", activation_id)
|
||||||
|
if verify_result["valid"]:
|
||||||
|
print("Lizenz ist gültig!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Client-Integration Beispiel (C#)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Management;
|
||||||
|
|
||||||
|
public class LicenseClient
|
||||||
|
{
|
||||||
|
private readonly HttpClient httpClient;
|
||||||
|
private readonly string apiKey;
|
||||||
|
private readonly string serverUrl;
|
||||||
|
|
||||||
|
public LicenseClient(string apiKey, string serverUrl = "https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com")
|
||||||
|
{
|
||||||
|
this.apiKey = apiKey;
|
||||||
|
this.serverUrl = serverUrl;
|
||||||
|
|
||||||
|
httpClient = new HttpClient();
|
||||||
|
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetMachineId()
|
||||||
|
{
|
||||||
|
// CPU ID abrufen
|
||||||
|
string cpuId = "";
|
||||||
|
ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT ProcessorId FROM Win32_Processor");
|
||||||
|
foreach (ManagementObject obj in searcher.Get())
|
||||||
|
{
|
||||||
|
cpuId = obj["ProcessorId"].ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return cpuId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHardwareHash()
|
||||||
|
{
|
||||||
|
string machineId = GetMachineId();
|
||||||
|
string systemInfo = Environment.MachineName + Environment.OSVersion;
|
||||||
|
|
||||||
|
using (SHA256 sha256 = SHA256.Create())
|
||||||
|
{
|
||||||
|
byte[] hashBytes = sha256.ComputeHash(Encoding.UTF8.GetBytes(machineId + systemInfo));
|
||||||
|
return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<LicenseActivationResponse> ActivateLicenseAsync(string licenseKey)
|
||||||
|
{
|
||||||
|
var requestData = new
|
||||||
|
{
|
||||||
|
license_key = licenseKey,
|
||||||
|
machine_id = GetMachineId(),
|
||||||
|
hardware_hash = GetHardwareHash(),
|
||||||
|
os_info = new
|
||||||
|
{
|
||||||
|
os = "Windows",
|
||||||
|
version = Environment.OSVersion.Version.ToString()
|
||||||
|
},
|
||||||
|
app_version = "1.0.0"
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<LicenseActivationResponse>(responseContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class LicenseActivationResponse
|
||||||
|
{
|
||||||
|
public bool success { get; set; }
|
||||||
|
public string message { get; set; }
|
||||||
|
public int? activation_id { get; set; }
|
||||||
|
public DateTime? expires_at { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wichtige Hinweise
|
||||||
|
|
||||||
|
### Sicherheit
|
||||||
|
- API-Keys niemals im Quellcode hartcodieren
|
||||||
|
- SSL-Zertifikat immer prüfen (verify=True)
|
||||||
|
- Hardware-Hash lokal zwischenspeichern
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
- Max. 100 Anfragen pro Minute pro API-Key
|
||||||
|
- Heartbeat alle 15 Minuten (nicht öfter)
|
||||||
|
|
||||||
|
### Offline-Betrieb
|
||||||
|
- 7 Tage Grace Period bei Verbindungsproblemen
|
||||||
|
- Letzte gültige Lizenz lokal cachen
|
||||||
|
- Bei Hardware-Änderung: Grace Period nutzen
|
||||||
|
|
||||||
|
### Fehlerbehandlung
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
result = client.verify_license(license_key, activation_id)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
# Offline-Modus: Gecachte Lizenz prüfen
|
||||||
|
if cached_license_valid():
|
||||||
|
continue_execution()
|
||||||
|
else:
|
||||||
|
show_error("Keine Internetverbindung und Lizenz abgelaufen")
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
if e.response.status_code == 401:
|
||||||
|
show_error("Ungültiger API-Key")
|
||||||
|
elif e.response.status_code == 403:
|
||||||
|
show_error("Lizenz ungültig oder abgelaufen")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test-Zugangsdaten
|
||||||
|
|
||||||
|
Für Entwicklungszwecke:
|
||||||
|
- Test API-Key: `test-api-key-12345`
|
||||||
|
- Test Lizenz: `TEST-LICENSE-KEY-12345`
|
||||||
|
|
||||||
|
**WICHTIG:** Für Produktion eigene API-Keys und Lizenzen erstellen!
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Bei Problemen:
|
||||||
|
1. Logs prüfen: `docker logs license-server`
|
||||||
|
2. API-Status: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com/health
|
||||||
|
3. Datenbank prüfen: Lizenzen und Aktivierungen
|
||||||
29
v2_lizenzserver/README.md
Normale Datei
29
v2_lizenzserver/README.md
Normale Datei
@@ -0,0 +1,29 @@
|
|||||||
|
# License Server für v2-Docker
|
||||||
|
|
||||||
|
Dieser License Server ersetzt den Dummy-Container und bietet vollständige Lizenzmanagement-Funktionalität.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
- Lizenzaktivierung mit Hardware-Binding
|
||||||
|
- Heartbeat/Verifizierung alle 15 Minuten
|
||||||
|
- Versionskontrolle mit Update-Erzwingung
|
||||||
|
- Offline Grace Period (7 Tage)
|
||||||
|
- Multi-Aktivierung Support
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
- POST `/api/license/activate` - Lizenz aktivieren
|
||||||
|
- POST `/api/license/verify` - Lizenz verifizieren (Heartbeat)
|
||||||
|
- POST `/api/version/check` - Version prüfen
|
||||||
|
- GET `/api/license/info/{license_key}` - Lizenzinfo abrufen
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
Der Server läuft im v2-Docker Stack und ist über nginx erreichbar:
|
||||||
|
- Extern: https://api-software-undso.z5m7q9dk3ah2v1plx6ju.com
|
||||||
|
- Intern: http://license-server:8443
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
Die Konfiguration erfolgt über die `.env` Datei:
|
||||||
|
- `DATABASE_URL` - Verbindung zur PostgreSQL Datenbank
|
||||||
|
- `SECRET_KEY` - JWT Secret Key für Token-Signierung
|
||||||
|
|
||||||
|
## Integration
|
||||||
|
Siehe `LIZENZSERVER_ANLEITUNG.md` und die Beispiele in `client_examples/` für die Client-Integration.
|
||||||
90
v2_lizenzserver/TODO.md
Normale Datei
90
v2_lizenzserver/TODO.md
Normale Datei
@@ -0,0 +1,90 @@
|
|||||||
|
# TODO - License Server & Admin Panel Erweiterungen
|
||||||
|
|
||||||
|
## 🎯 Phase 1: Admin Panel Integration (Priorität: Hoch)
|
||||||
|
|
||||||
|
### Neue Lizenz-Verwaltung im Admin Panel
|
||||||
|
- [ ] Neuer Menüpunkt "Lizenzen" im Admin Panel
|
||||||
|
- [ ] Dashboard mit Lizenz-Statistiken
|
||||||
|
- [ ] Anzahl aktive/abgelaufene Lizenzen
|
||||||
|
- [ ] Aktivierungen pro Lizenz
|
||||||
|
- [ ] Grafische Auswertungen
|
||||||
|
- [ ] Lizenz-Erstellung Formular
|
||||||
|
- [ ] Lizenzschlüssel generieren
|
||||||
|
- [ ] Ablaufdatum festlegen
|
||||||
|
- [ ] Max. Aktivierungen definieren
|
||||||
|
- [ ] Kundeninformationen speichern
|
||||||
|
- [ ] Lizenz-Übersicht Tabelle
|
||||||
|
- [ ] Suche/Filter-Funktionen
|
||||||
|
- [ ] Status anzeigen (aktiv/abgelaufen/gesperrt)
|
||||||
|
- [ ] Aktivierungen einsehen
|
||||||
|
- [ ] Lizenzen deaktivieren/reaktivieren
|
||||||
|
- [ ] API-Key Verwaltung
|
||||||
|
- [ ] Neue API-Keys generieren
|
||||||
|
- [ ] Bestehende Keys anzeigen/deaktivieren
|
||||||
|
- [ ] Berechtigungen festlegen
|
||||||
|
|
||||||
|
## 🔒 Phase 2: Sicherheits-Optimierungen (Priorität: Mittel)
|
||||||
|
|
||||||
|
### Rate Limiting in Nginx
|
||||||
|
- [ ] API Rate Limiting implementieren (10 Requests/Minute)
|
||||||
|
- [ ] Burst-Handling konfigurieren
|
||||||
|
- [ ] IP-basierte Limits für API-Endpoints
|
||||||
|
|
||||||
|
### Datenbank-Sicherheit
|
||||||
|
- [ ] Separaten Read-Only DB-User für API erstellen
|
||||||
|
- [ ] Admin-User mit eingeschränkten Rechten
|
||||||
|
- [ ] Connection Pooling optimieren
|
||||||
|
|
||||||
|
### GeoIP-Blocking
|
||||||
|
- [ ] GeoIP-Modul in Nginx installieren
|
||||||
|
- [ ] Länder-Blacklist konfigurieren
|
||||||
|
- [ ] VPN-Erkennung implementieren
|
||||||
|
- [ ] Logging verdächtiger Zugriffe
|
||||||
|
|
||||||
|
## 📊 Phase 3: Monitoring & Backup (Priorität: Mittel)
|
||||||
|
|
||||||
|
### Monitoring-System
|
||||||
|
- [ ] Prometheus einrichten
|
||||||
|
- [ ] Grafana Dashboards erstellen
|
||||||
|
- [ ] API-Zugriffe
|
||||||
|
- [ ] Lizenz-Aktivierungen
|
||||||
|
- [ ] System-Ressourcen
|
||||||
|
- [ ] Alerting bei Anomalien
|
||||||
|
- [ ] Zu viele fehlgeschlagene Aktivierungen
|
||||||
|
- [ ] Verdächtige Zugriffsmuster
|
||||||
|
- [ ] System-Ausfälle
|
||||||
|
|
||||||
|
### Backup-Strategie
|
||||||
|
- [ ] Automatische tägliche DB-Backups
|
||||||
|
- [ ] Backup-Rotation (30 Tage aufbewahren)
|
||||||
|
- [ ] Verschlüsselung der Backups
|
||||||
|
- [ ] Offsite-Backup zu Cloud-Storage
|
||||||
|
- [ ] Restore-Tests durchführen
|
||||||
|
|
||||||
|
## 🚀 Phase 4: Performance & Features (Priorität: Niedrig)
|
||||||
|
|
||||||
|
### Performance-Optimierungen
|
||||||
|
- [ ] Redis Cache für häufige Queries
|
||||||
|
- [ ] Database Indexing optimieren
|
||||||
|
- [ ] API Response Caching
|
||||||
|
|
||||||
|
### Erweiterte Features
|
||||||
|
- [ ] Export-Funktionen (CSV/Excel)
|
||||||
|
- [ ] Email-Benachrichtigungen
|
||||||
|
- [ ] Bei Lizenz-Ablauf
|
||||||
|
- [ ] Bei verdächtigen Aktivitäten
|
||||||
|
- [ ] Lizenz-Templates für häufige Konfigurationen
|
||||||
|
- [ ] Bulk-Operationen (mehrere Lizenzen gleichzeitig)
|
||||||
|
|
||||||
|
## 📝 Dokumentation
|
||||||
|
|
||||||
|
- [ ] API-Dokumentation erweitern
|
||||||
|
- [ ] Admin Panel Benutzerhandbuch
|
||||||
|
- [ ] Deployment-Guide aktualisieren
|
||||||
|
- [ ] Troubleshooting-Guide erstellen
|
||||||
|
|
||||||
|
## Prioritäten:
|
||||||
|
1. **Sofort**: Phase 1 - Admin Panel Integration
|
||||||
|
2. **Nächste Woche**: Phase 2 - Sicherheits-Optimierungen
|
||||||
|
3. **Nächster Monat**: Phase 3 - Monitoring & Backup
|
||||||
|
4. **Bei Bedarf**: Phase 4 - Performance & Features
|
||||||
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
|
||||||
209
v2_lizenzserver/app/api/license.py
Normale Datei
209
v2_lizenzserver/app/api/license.py
Normale Datei
@@ -0,0 +1,209 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Dict, Any
|
||||||
|
|
||||||
|
from app.db.database import get_db
|
||||||
|
from app.models.models import License, Activation, Version
|
||||||
|
from app.schemas.license import (
|
||||||
|
LicenseActivationRequest,
|
||||||
|
LicenseActivationResponse,
|
||||||
|
LicenseVerificationRequest,
|
||||||
|
LicenseVerificationResponse
|
||||||
|
)
|
||||||
|
from app.core.security import get_api_key
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
@router.post("/activate", response_model=LicenseActivationResponse)
|
||||||
|
async def activate_license(
|
||||||
|
request: LicenseActivationRequest,
|
||||||
|
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 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 = Depends(get_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 = Depends(get_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
|
||||||
|
]
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
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"
|
||||||
|
|
||||||
|
REDIS_URL: str = "redis://redis:6379"
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
settings = Settings()
|
||||||
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()
|
||||||
65
v2_lizenzserver/app/main.py
Normale Datei
65
v2_lizenzserver/app/main.py
Normale Datei
@@ -0,0 +1,65 @@
|
|||||||
|
from fastapi import FastAPI, Request
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
import uvicorn
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from app.api import license, version
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.db.database import engine, Base
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
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.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
|
||||||
|
)
|
||||||
43
v2_lizenzserver/app/schemas/license.py
Normale Datei
43
v2_lizenzserver/app/schemas/license.py
Normale Datei
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
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")
|
||||||
14
v2_lizenzserver/requirements.txt
Normale Datei
14
v2_lizenzserver/requirements.txt
Normale Datei
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
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())
|
||||||
18
v2_lizenzserver_backup/Dockerfile
Normale Datei
18
v2_lizenzserver_backup/Dockerfile
Normale Datei
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.11-slim
|
||||||
|
|
||||||
|
# Zeitzone setzen
|
||||||
|
ENV TZ=Europe/Berlin
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y tzdata \
|
||||||
|
&& ln -sf /usr/share/zoneinfo/Europe/Berlin /etc/localtime \
|
||||||
|
&& echo "Europe/Berlin" > /etc/timezone \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Placeholder für Lizenzserver
|
||||||
|
RUN echo "Lizenzserver noch nicht implementiert" > info.txt
|
||||||
|
|
||||||
|
CMD ["python", "-c", "print('Lizenzserver Container läuft, aber noch keine Implementierung vorhanden'); import time; time.sleep(86400)"]
|
||||||
In neuem Issue referenzieren
Einen Benutzer sperren