Gerätelimit drin
Dieser Commit ist enthalten in:
@@ -60,7 +60,8 @@
|
|||||||
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"href=[''\"\"][/]?(dashboard)?[''\"\"]\" --type html)",
|
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"href=[''\"\"][/]?(dashboard)?[''\"\"]\" --type html)",
|
||||||
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"Dashboard\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/resources.html)",
|
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"Dashboard\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/resources.html)",
|
||||||
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"Dashboard\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/profile.html /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/resource_metrics.html)",
|
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"Dashboard\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/profile.html /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/templates/resource_metrics.html)",
|
||||||
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"BACKUP|LOGIN_2FA_SUCCESS\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/app.py)"
|
"Bash(/home/rac00n/.npm-global/lib/node_modules/@anthropic-ai/claude-code/vendor/ripgrep/x64-linux/rg -n \"BACKUP|LOGIN_2FA_SUCCESS\" /mnt/c/Users/Administrator/Documents/GitHub/v2-Docker/v2_adminpanel/app.py)",
|
||||||
|
"Bash(sed:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
44
JOURNAL.md
44
JOURNAL.md
@@ -1,5 +1,49 @@
|
|||||||
# v2-Docker Projekt Journal
|
# v2-Docker Projekt Journal
|
||||||
|
|
||||||
|
## Letzte Änderungen (06.01.2025)
|
||||||
|
|
||||||
|
### Gerätelimit-Feature implementiert
|
||||||
|
- **Datenbank-Schema erweitert**:
|
||||||
|
- Neue Spalte `device_limit` in `licenses` Tabelle (Standard: 3, Range: 1-10)
|
||||||
|
- Neue Tabelle `device_registrations` für Hardware-ID Tracking
|
||||||
|
- Indizes für Performance-Optimierung hinzugefügt
|
||||||
|
|
||||||
|
- **UI-Anpassungen**:
|
||||||
|
- Einzellizenz-Formular: Dropdown für Gerätelimit (1-10 Geräte)
|
||||||
|
- Batch-Formular: Gerätelimit pro Lizenz auswählbar
|
||||||
|
- Lizenz-Bearbeitung: Gerätelimit änderbar
|
||||||
|
- Lizenz-Anzeige: Zeigt aktive Geräte (z.B. "💻 2/3")
|
||||||
|
|
||||||
|
- **Backend-Änderungen**:
|
||||||
|
- Lizenz-Erstellung speichert device_limit
|
||||||
|
- Batch-Erstellung berücksichtigt device_limit
|
||||||
|
- Lizenz-Update kann device_limit ändern
|
||||||
|
- API-Endpoints liefern Geräteinformationen
|
||||||
|
|
||||||
|
- **Migration**:
|
||||||
|
- Skript `migrate_device_limit.sql` erstellt
|
||||||
|
- Setzt device_limit = 3 für alle bestehenden Lizenzen
|
||||||
|
|
||||||
|
### Vollständig implementiert:
|
||||||
|
✅ Device Management UI (Geräte pro Lizenz anzeigen/verwalten)
|
||||||
|
✅ Device Validation Logic (Prüfung bei Geräte-Registrierung)
|
||||||
|
✅ API-Endpoints für Geräte-Registrierung/Deregistrierung
|
||||||
|
|
||||||
|
### API-Endpoints:
|
||||||
|
- `GET /api/license/<id>/devices` - Listet alle Geräte einer Lizenz
|
||||||
|
- `POST /api/license/<id>/register-device` - Registriert ein neues Gerät
|
||||||
|
- `POST /api/license/<id>/deactivate-device/<device_id>` - Deaktiviert ein Gerät
|
||||||
|
|
||||||
|
### Features:
|
||||||
|
- Geräte-Registrierung mit Hardware-ID Validierung
|
||||||
|
- Automatische Prüfung des Gerätelimits
|
||||||
|
- Reaktivierung deaktivierter Geräte möglich
|
||||||
|
- Geräte-Verwaltung UI mit Modal-Dialog
|
||||||
|
- Anzeige von Gerätename, OS, IP, Registrierungsdatum
|
||||||
|
- Admin kann Geräte manuell deaktivieren
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Projektübersicht
|
## Projektübersicht
|
||||||
Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker-basierter Architektur.
|
Lizenzmanagement-System für Social Media Account-Erstellungssoftware mit Docker-basierter Architektur.
|
||||||
|
|
||||||
|
|||||||
@@ -1485,6 +1485,7 @@ def create_license():
|
|||||||
domain_count = int(request.form.get("domain_count", 1))
|
domain_count = int(request.form.get("domain_count", 1))
|
||||||
ipv4_count = int(request.form.get("ipv4_count", 1))
|
ipv4_count = int(request.form.get("ipv4_count", 1))
|
||||||
phone_count = int(request.form.get("phone_count", 1))
|
phone_count = int(request.form.get("phone_count", 1))
|
||||||
|
device_limit = int(request.form.get("device_limit", 3))
|
||||||
|
|
||||||
conn = get_connection()
|
conn = get_connection()
|
||||||
cur = conn.cursor()
|
cur = conn.cursor()
|
||||||
@@ -1536,11 +1537,11 @@ def create_license():
|
|||||||
# Lizenz hinzufügen
|
# Lizenz hinzufügen
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
||||||
domain_count, ipv4_count, phone_count, is_test)
|
domain_count, ipv4_count, phone_count, device_limit, is_test)
|
||||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
""", (license_key, customer_id, license_type, valid_from, valid_until,
|
||||||
domain_count, ipv4_count, phone_count, is_test))
|
domain_count, ipv4_count, phone_count, device_limit, is_test))
|
||||||
license_id = cur.fetchone()[0]
|
license_id = cur.fetchone()[0]
|
||||||
|
|
||||||
# Ressourcen zuweisen
|
# Ressourcen zuweisen
|
||||||
@@ -1652,6 +1653,7 @@ def create_license():
|
|||||||
'license_type': license_type,
|
'license_type': license_type,
|
||||||
'valid_from': valid_from,
|
'valid_from': valid_from,
|
||||||
'valid_until': valid_until,
|
'valid_until': valid_until,
|
||||||
|
'device_limit': device_limit,
|
||||||
'is_test': is_test
|
'is_test': is_test
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1711,6 +1713,7 @@ def batch_licenses():
|
|||||||
domain_count = int(request.form.get("domain_count", 1))
|
domain_count = int(request.form.get("domain_count", 1))
|
||||||
ipv4_count = int(request.form.get("ipv4_count", 1))
|
ipv4_count = int(request.form.get("ipv4_count", 1))
|
||||||
phone_count = int(request.form.get("phone_count", 1))
|
phone_count = int(request.form.get("phone_count", 1))
|
||||||
|
device_limit = int(request.form.get("device_limit", 3))
|
||||||
|
|
||||||
# Sicherheitslimit
|
# Sicherheitslimit
|
||||||
if quantity < 1 or quantity > 100:
|
if quantity < 1 or quantity > 100:
|
||||||
@@ -1803,11 +1806,11 @@ def batch_licenses():
|
|||||||
cur.execute("""
|
cur.execute("""
|
||||||
INSERT INTO licenses (license_key, customer_id, license_type, is_test,
|
INSERT INTO licenses (license_key, customer_id, license_type, is_test,
|
||||||
valid_from, valid_until, is_active,
|
valid_from, valid_until, is_active,
|
||||||
domain_count, ipv4_count, phone_count)
|
domain_count, ipv4_count, phone_count, device_limit)
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s)
|
VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
""", (license_key, customer_id, license_type, is_test, valid_from, valid_until,
|
""", (license_key, customer_id, license_type, is_test, valid_from, valid_until,
|
||||||
domain_count, ipv4_count, phone_count))
|
domain_count, ipv4_count, phone_count, device_limit))
|
||||||
license_id = cur.fetchone()[0]
|
license_id = cur.fetchone()[0]
|
||||||
|
|
||||||
# Ressourcen für diese Lizenz zuweisen
|
# Ressourcen für diese Lizenz zuweisen
|
||||||
@@ -1983,7 +1986,7 @@ def edit_license(license_id):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
# Alte Werte für Audit-Log abrufen
|
# Alte Werte für Audit-Log abrufen
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT license_key, license_type, valid_from, valid_until, is_active, is_test
|
SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit
|
||||||
FROM licenses WHERE id = %s
|
FROM licenses WHERE id = %s
|
||||||
""", (license_id,))
|
""", (license_id,))
|
||||||
old_license = cur.fetchone()
|
old_license = cur.fetchone()
|
||||||
@@ -1995,13 +1998,14 @@ def edit_license(license_id):
|
|||||||
valid_until = request.form["valid_until"]
|
valid_until = request.form["valid_until"]
|
||||||
is_active = request.form.get("is_active") == "on"
|
is_active = request.form.get("is_active") == "on"
|
||||||
is_test = request.form.get("is_test") == "on"
|
is_test = request.form.get("is_test") == "on"
|
||||||
|
device_limit = int(request.form.get("device_limit", 3))
|
||||||
|
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE licenses
|
UPDATE licenses
|
||||||
SET license_key = %s, license_type = %s, valid_from = %s,
|
SET license_key = %s, license_type = %s, valid_from = %s,
|
||||||
valid_until = %s, is_active = %s, is_test = %s
|
valid_until = %s, is_active = %s, is_test = %s, device_limit = %s
|
||||||
WHERE id = %s
|
WHERE id = %s
|
||||||
""", (license_key, license_type, valid_from, valid_until, is_active, is_test, license_id))
|
""", (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id))
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
@@ -2013,7 +2017,8 @@ def edit_license(license_id):
|
|||||||
'valid_from': str(old_license[2]),
|
'valid_from': str(old_license[2]),
|
||||||
'valid_until': str(old_license[3]),
|
'valid_until': str(old_license[3]),
|
||||||
'is_active': old_license[4],
|
'is_active': old_license[4],
|
||||||
'is_test': old_license[5]
|
'is_test': old_license[5],
|
||||||
|
'device_limit': old_license[6]
|
||||||
},
|
},
|
||||||
new_values={
|
new_values={
|
||||||
'license_key': license_key,
|
'license_key': license_key,
|
||||||
@@ -2021,7 +2026,8 @@ def edit_license(license_id):
|
|||||||
'valid_from': valid_from,
|
'valid_from': valid_from,
|
||||||
'valid_until': valid_until,
|
'valid_until': valid_until,
|
||||||
'is_active': is_active,
|
'is_active': is_active,
|
||||||
'is_test': is_test
|
'is_test': is_test,
|
||||||
|
'device_limit': device_limit
|
||||||
})
|
})
|
||||||
|
|
||||||
cur.close()
|
cur.close()
|
||||||
@@ -2048,7 +2054,7 @@ def edit_license(license_id):
|
|||||||
# Get license data
|
# Get license data
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
SELECT l.id, l.license_key, c.name, c.email, l.license_type,
|
||||||
l.valid_from, l.valid_until, l.is_active, c.id, l.is_test
|
l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
JOIN customers c ON l.customer_id = c.id
|
JOIN customers c ON l.customer_id = c.id
|
||||||
WHERE l.id = %s
|
WHERE l.id = %s
|
||||||
@@ -2343,7 +2349,9 @@ def customers_licenses():
|
|||||||
END as status,
|
END as status,
|
||||||
l.domain_count,
|
l.domain_count,
|
||||||
l.ipv4_count,
|
l.ipv4_count,
|
||||||
l.phone_count
|
l.phone_count,
|
||||||
|
l.device_limit,
|
||||||
|
(SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
WHERE l.customer_id = %s
|
WHERE l.customer_id = %s
|
||||||
ORDER BY l.created_at DESC, l.id DESC
|
ORDER BY l.created_at DESC, l.id DESC
|
||||||
@@ -2384,7 +2392,9 @@ def api_customer_licenses(customer_id):
|
|||||||
END as status,
|
END as status,
|
||||||
l.domain_count,
|
l.domain_count,
|
||||||
l.ipv4_count,
|
l.ipv4_count,
|
||||||
l.phone_count
|
l.phone_count,
|
||||||
|
l.device_limit,
|
||||||
|
(SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices
|
||||||
FROM licenses l
|
FROM licenses l
|
||||||
WHERE l.customer_id = %s
|
WHERE l.customer_id = %s
|
||||||
ORDER BY l.created_at DESC, l.id DESC
|
ORDER BY l.created_at DESC, l.id DESC
|
||||||
@@ -2434,6 +2444,8 @@ def api_customer_licenses(customer_id):
|
|||||||
'domain_count': row[7],
|
'domain_count': row[7],
|
||||||
'ipv4_count': row[8],
|
'ipv4_count': row[8],
|
||||||
'phone_count': row[9],
|
'phone_count': row[9],
|
||||||
|
'device_limit': row[10],
|
||||||
|
'active_devices': row[11],
|
||||||
'resources': resources
|
'resources': resources
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -3711,6 +3723,218 @@ def bulk_deactivate_licenses():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'success': False, 'message': str(e)}), 500
|
return jsonify({'success': False, 'message': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route("/api/license/<int:license_id>/devices")
|
||||||
|
@login_required
|
||||||
|
def get_license_devices(license_id):
|
||||||
|
"""Hole alle registrierten Geräte einer Lizenz"""
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Prüfe ob Lizenz existiert und hole device_limit
|
||||||
|
cur.execute("""
|
||||||
|
SELECT device_limit FROM licenses WHERE id = %s
|
||||||
|
""", (license_id,))
|
||||||
|
license_data = cur.fetchone()
|
||||||
|
|
||||||
|
if not license_data:
|
||||||
|
return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
|
||||||
|
|
||||||
|
device_limit = license_data[0]
|
||||||
|
|
||||||
|
# Hole alle Geräte für diese Lizenz
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, hardware_id, device_name, operating_system,
|
||||||
|
first_seen, last_seen, is_active, ip_address
|
||||||
|
FROM device_registrations
|
||||||
|
WHERE license_id = %s
|
||||||
|
ORDER BY is_active DESC, last_seen DESC
|
||||||
|
""", (license_id,))
|
||||||
|
|
||||||
|
devices = []
|
||||||
|
for row in cur.fetchall():
|
||||||
|
devices.append({
|
||||||
|
'id': row[0],
|
||||||
|
'hardware_id': row[1],
|
||||||
|
'device_name': row[2] or 'Unbekanntes Gerät',
|
||||||
|
'operating_system': row[3] or 'Unbekannt',
|
||||||
|
'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '',
|
||||||
|
'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '',
|
||||||
|
'is_active': row[6],
|
||||||
|
'ip_address': row[7] or '-'
|
||||||
|
})
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'devices': devices,
|
||||||
|
'device_limit': device_limit,
|
||||||
|
'active_count': sum(1 for d in devices if d['is_active'])
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500
|
||||||
|
|
||||||
|
@app.route("/api/license/<int:license_id>/register-device", methods=["POST"])
|
||||||
|
def register_device(license_id):
|
||||||
|
"""Registriere ein neues Gerät für eine Lizenz"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
hardware_id = data.get('hardware_id')
|
||||||
|
device_name = data.get('device_name', '')
|
||||||
|
operating_system = data.get('operating_system', '')
|
||||||
|
|
||||||
|
if not hardware_id:
|
||||||
|
return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400
|
||||||
|
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Prüfe ob Lizenz existiert und aktiv ist
|
||||||
|
cur.execute("""
|
||||||
|
SELECT device_limit, is_active, valid_until
|
||||||
|
FROM licenses
|
||||||
|
WHERE id = %s
|
||||||
|
""", (license_id,))
|
||||||
|
license_data = cur.fetchone()
|
||||||
|
|
||||||
|
if not license_data:
|
||||||
|
return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404
|
||||||
|
|
||||||
|
device_limit, is_active, valid_until = license_data
|
||||||
|
|
||||||
|
# Prüfe ob Lizenz aktiv und gültig ist
|
||||||
|
if not is_active:
|
||||||
|
return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403
|
||||||
|
|
||||||
|
if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date():
|
||||||
|
return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403
|
||||||
|
|
||||||
|
# Prüfe ob Gerät bereits registriert ist
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id, is_active FROM device_registrations
|
||||||
|
WHERE license_id = %s AND hardware_id = %s
|
||||||
|
""", (license_id, hardware_id))
|
||||||
|
existing_device = cur.fetchone()
|
||||||
|
|
||||||
|
if existing_device:
|
||||||
|
device_id, is_device_active = existing_device
|
||||||
|
if is_device_active:
|
||||||
|
# Gerät ist bereits aktiv, update last_seen
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE device_registrations
|
||||||
|
SET last_seen = CURRENT_TIMESTAMP,
|
||||||
|
ip_address = %s,
|
||||||
|
user_agent = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id})
|
||||||
|
else:
|
||||||
|
# Gerät war deaktiviert, prüfe ob wir es reaktivieren können
|
||||||
|
cur.execute("""
|
||||||
|
SELECT COUNT(*) FROM device_registrations
|
||||||
|
WHERE license_id = %s AND is_active = TRUE
|
||||||
|
""", (license_id,))
|
||||||
|
active_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
if active_count >= device_limit:
|
||||||
|
return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
|
||||||
|
|
||||||
|
# Reaktiviere das Gerät
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE device_registrations
|
||||||
|
SET is_active = TRUE,
|
||||||
|
last_seen = CURRENT_TIMESTAMP,
|
||||||
|
deactivated_at = NULL,
|
||||||
|
deactivated_by = NULL,
|
||||||
|
ip_address = %s,
|
||||||
|
user_agent = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (get_client_ip(), request.headers.get('User-Agent', ''), device_id))
|
||||||
|
conn.commit()
|
||||||
|
return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id})
|
||||||
|
|
||||||
|
# Neues Gerät - prüfe Gerätelimit
|
||||||
|
cur.execute("""
|
||||||
|
SELECT COUNT(*) FROM device_registrations
|
||||||
|
WHERE license_id = %s AND is_active = TRUE
|
||||||
|
""", (license_id,))
|
||||||
|
active_count = cur.fetchone()[0]
|
||||||
|
|
||||||
|
if active_count >= device_limit:
|
||||||
|
return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403
|
||||||
|
|
||||||
|
# Registriere neues Gerät
|
||||||
|
cur.execute("""
|
||||||
|
INSERT INTO device_registrations
|
||||||
|
(license_id, hardware_id, device_name, operating_system, ip_address, user_agent)
|
||||||
|
VALUES (%s, %s, %s, %s, %s, %s)
|
||||||
|
RETURNING id
|
||||||
|
""", (license_id, hardware_id, device_name, operating_system,
|
||||||
|
get_client_ip(), request.headers.get('User-Agent', '')))
|
||||||
|
device_id = cur.fetchone()[0]
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Audit Log
|
||||||
|
log_audit('DEVICE_REGISTER', 'device', device_id,
|
||||||
|
new_values={'license_id': license_id, 'hardware_id': hardware_id})
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500
|
||||||
|
|
||||||
|
@app.route("/api/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
|
||||||
|
@login_required
|
||||||
|
def deactivate_device(license_id, device_id):
|
||||||
|
"""Deaktiviere ein registriertes Gerät"""
|
||||||
|
try:
|
||||||
|
conn = get_connection()
|
||||||
|
cur = conn.cursor()
|
||||||
|
|
||||||
|
# Prüfe ob das Gerät zu dieser Lizenz gehört
|
||||||
|
cur.execute("""
|
||||||
|
SELECT id FROM device_registrations
|
||||||
|
WHERE id = %s AND license_id = %s AND is_active = TRUE
|
||||||
|
""", (device_id, license_id))
|
||||||
|
|
||||||
|
if not cur.fetchone():
|
||||||
|
return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404
|
||||||
|
|
||||||
|
# Deaktiviere das Gerät
|
||||||
|
cur.execute("""
|
||||||
|
UPDATE device_registrations
|
||||||
|
SET is_active = FALSE,
|
||||||
|
deactivated_at = CURRENT_TIMESTAMP,
|
||||||
|
deactivated_by = %s
|
||||||
|
WHERE id = %s
|
||||||
|
""", (session['username'], device_id))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Audit Log
|
||||||
|
log_audit('DEVICE_DEACTIVATE', 'device', device_id,
|
||||||
|
old_values={'is_active': True},
|
||||||
|
new_values={'is_active': False})
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
|
||||||
|
return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500
|
||||||
|
|
||||||
@app.route("/api/licenses/bulk-delete", methods=["POST"])
|
@app.route("/api/licenses/bulk-delete", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def bulk_delete_licenses():
|
def bulk_delete_licenses():
|
||||||
|
|||||||
@@ -173,6 +173,38 @@ BEGIN
|
|||||||
END IF;
|
END IF;
|
||||||
END $$;
|
END $$;
|
||||||
|
|
||||||
|
-- Erweiterung der licenses Tabelle um device_limit
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'licenses' AND column_name = 'device_limit') THEN
|
||||||
|
ALTER TABLE licenses
|
||||||
|
ADD COLUMN device_limit INTEGER DEFAULT 3 CHECK (device_limit >= 1 AND device_limit <= 10);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- Tabelle für Geräte-Registrierungen
|
||||||
|
CREATE TABLE IF NOT EXISTS device_registrations (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
license_id INTEGER REFERENCES licenses(id) ON DELETE CASCADE,
|
||||||
|
hardware_id TEXT NOT NULL,
|
||||||
|
device_name TEXT,
|
||||||
|
operating_system TEXT,
|
||||||
|
first_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_seen TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
is_active BOOLEAN DEFAULT TRUE,
|
||||||
|
deactivated_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
deactivated_by TEXT,
|
||||||
|
ip_address TEXT,
|
||||||
|
user_agent TEXT,
|
||||||
|
UNIQUE(license_id, hardware_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indizes für device_registrations
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_device_license ON device_registrations(license_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_device_hardware ON device_registrations(hardware_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_device_active ON device_registrations(license_id, is_active) WHERE is_active = TRUE;
|
||||||
|
|
||||||
-- Indizes für Performance
|
-- Indizes für Performance
|
||||||
CREATE INDEX IF NOT EXISTS idx_resource_status ON resource_pools(status);
|
CREATE INDEX IF NOT EXISTS idx_resource_status ON resource_pools(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_resource_type_status ON resource_pools(resource_type, status);
|
CREATE INDEX IF NOT EXISTS idx_resource_type_status ON resource_pools(resource_type, status);
|
||||||
|
|||||||
13
v2_adminpanel/migrate_device_limit.sql
Normale Datei
13
v2_adminpanel/migrate_device_limit.sql
Normale Datei
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: Setze device_limit für bestehende Test-Lizenzen auf 3
|
||||||
|
-- Dieses Script wird nur einmal ausgeführt, um bestehende Lizenzen zu aktualisieren
|
||||||
|
|
||||||
|
-- Setze device_limit = 3 für alle bestehenden Lizenzen, die noch keinen Wert haben
|
||||||
|
UPDATE licenses
|
||||||
|
SET device_limit = 3
|
||||||
|
WHERE device_limit IS NULL;
|
||||||
|
|
||||||
|
-- Bestätige die Änderung
|
||||||
|
SELECT COUNT(*) as updated_licenses,
|
||||||
|
COUNT(CASE WHEN is_test = TRUE THEN 1 END) as test_licenses_updated
|
||||||
|
FROM licenses
|
||||||
|
WHERE device_limit = 3;
|
||||||
@@ -147,6 +147,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Device Limit -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-laptop"></i> Gerätelimit pro Lizenz
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="deviceLimit" class="form-label">
|
||||||
|
Maximale Anzahl Geräte pro Lizenz
|
||||||
|
</label>
|
||||||
|
<select class="form-select" id="deviceLimit" name="device_limit" required>
|
||||||
|
{% for i in range(1, 11) %}
|
||||||
|
<option value="{{ i }}" {% if i == 3 %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Jede generierte Lizenz kann auf maximal dieser Anzahl von Geräten gleichzeitig aktiviert werden.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Test Data Checkbox -->
|
<!-- Test Data Checkbox -->
|
||||||
<div class="form-check mt-3">
|
<div class="form-check mt-3">
|
||||||
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||||
|
|||||||
@@ -168,18 +168,24 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if license[9] > 0 %}
|
{% if license[9] > 0 %}
|
||||||
<div class="d-inline-block" data-bs-toggle="tooltip" title="Telefonnummern">
|
<div class="d-inline-block me-2" data-bs-toggle="tooltip" title="Telefonnummern">
|
||||||
📱 {{ license[9] }}
|
📱 {{ license[9] }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<div class="d-inline-block" data-bs-toggle="tooltip" title="Geräte">
|
||||||
|
💻 {{ license[11] }}/{{ license[10] }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})">
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus({{ license[0] }}, {{ license[5] }})" title="Aktivieren/Deaktivieren">
|
||||||
<i class="bi bi-power"></i>
|
<i class="bi bi-power"></i>
|
||||||
</button>
|
</button>
|
||||||
<a href="/license/edit/{{ license[0] }}{% if request.args.get('show_test') %}?ref=customers-licenses&show_test=true{% endif %}" class="btn btn-outline-secondary">
|
<button class="btn btn-outline-info" onclick="showDeviceManagement({{ license[0] }})" title="Geräte verwalten">
|
||||||
|
<i class="bi bi-laptop"></i>
|
||||||
|
</button>
|
||||||
|
<a href="/license/edit/{{ license[0] }}{% if request.args.get('show_test') %}?ref=customers-licenses&show_test=true{% endif %}" class="btn btn-outline-secondary" title="Bearbeiten">
|
||||||
<i class="bi bi-pencil"></i>
|
<i class="bi bi-pencil"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -253,6 +259,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal für Geräte-Verwaltung -->
|
||||||
|
<div class="modal fade" id="deviceManagementModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Geräte verwalten</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="deviceManagementContent">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Lädt...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.customer-item {
|
.customer-item {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
@@ -433,6 +461,11 @@ function updateLicenseView(customerId, licenses) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Geräte-Anzeige hinzufügen
|
||||||
|
resourcesHtml += `<div class="resource-group">
|
||||||
|
<span class="resource-icon" data-bs-toggle="tooltip" title="Geräte">💻 ${license.active_devices}/${license.device_limit}</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
html += `
|
html += `
|
||||||
<tr data-license-id="${license.id}">
|
<tr data-license-id="${license.id}">
|
||||||
<td>
|
<td>
|
||||||
@@ -453,7 +486,10 @@ function updateLicenseView(customerId, licenses) {
|
|||||||
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})" title="Status ändern">
|
<button class="btn btn-outline-primary" onclick="toggleLicenseStatus(${license.id}, ${license.is_active})" title="Status ändern">
|
||||||
<i class="bi bi-power"></i>
|
<i class="bi bi-power"></i>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-info" onclick="showResourceManagement(${license.id})" title="Ressourcen verwalten">
|
<button class="btn btn-outline-info" onclick="showDeviceManagement(${license.id})" title="Geräte verwalten">
|
||||||
|
<i class="bi bi-laptop"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary" onclick="showResourceManagement(${license.id})" title="Ressourcen verwalten">
|
||||||
<i class="bi bi-gear"></i>
|
<i class="bi bi-gear"></i>
|
||||||
</button>
|
</button>
|
||||||
<a href="/license/edit/${license.id}${window.location.search ? '?ref=customers-licenses&' + window.location.search.substring(1) : ''}" class="btn btn-outline-secondary" title="Bearbeiten">
|
<a href="/license/edit/${license.id}${window.location.search ? '?ref=customers-licenses&' + window.location.search.substring(1) : ''}" class="btn btn-outline-secondary" title="Bearbeiten">
|
||||||
@@ -774,5 +810,124 @@ function quarantineResource(resourceId, resourceValue) {
|
|||||||
function saveResourceChanges() {
|
function saveResourceChanges() {
|
||||||
alert('Diese Funktion wird noch implementiert');
|
alert('Diese Funktion wird noch implementiert');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Geräte-Verwaltung anzeigen
|
||||||
|
function showDeviceManagement(licenseId) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('deviceManagementModal'));
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Lade Geräte-Daten
|
||||||
|
fetch(`/api/license/${licenseId}/devices`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
let content = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>Gerätelimit:</strong> ${data.active_count} von ${data.device_limit} Geräten aktiv
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
if (data.devices.length === 0) {
|
||||||
|
content += `
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<i class="bi bi-laptop text-muted" style="font-size: 3rem;"></i>
|
||||||
|
<p class="text-muted mt-3">Noch keine Geräte registriert</p>
|
||||||
|
</div>`;
|
||||||
|
} else {
|
||||||
|
content += `
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Gerätename</th>
|
||||||
|
<th>Hardware-ID</th>
|
||||||
|
<th>Betriebssystem</th>
|
||||||
|
<th>Erste Registrierung</th>
|
||||||
|
<th>Letzte Aktivität</th>
|
||||||
|
<th>IP-Adresse</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>`;
|
||||||
|
|
||||||
|
data.devices.forEach(device => {
|
||||||
|
const statusBadge = device.is_active
|
||||||
|
? '<span class="badge bg-success">Aktiv</span>'
|
||||||
|
: '<span class="badge bg-secondary">Deaktiviert</span>';
|
||||||
|
|
||||||
|
const actionButton = device.is_active
|
||||||
|
? `<button class="btn btn-sm btn-danger" onclick="deactivateDevice(${licenseId}, ${device.id}, '${device.device_name}')">
|
||||||
|
<i class="bi bi-x-circle"></i> Deaktivieren
|
||||||
|
</button>`
|
||||||
|
: '<span class="text-muted">-</span>';
|
||||||
|
|
||||||
|
content += `
|
||||||
|
<tr>
|
||||||
|
<td>${device.device_name}</td>
|
||||||
|
<td><small class="text-muted">${device.hardware_id.substring(0, 12)}...</small></td>
|
||||||
|
<td>${device.operating_system}</td>
|
||||||
|
<td>${device.first_seen}</td>
|
||||||
|
<td>${device.last_seen}</td>
|
||||||
|
<td>${device.ip_address}</td>
|
||||||
|
<td>${statusBadge}</td>
|
||||||
|
<td>${actionButton}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
content += `
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('deviceManagementContent').innerHTML = content;
|
||||||
|
} else {
|
||||||
|
document.getElementById('deviceManagementContent').innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> ${data.message || 'Fehler beim Laden der Geräte'}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading devices:', error);
|
||||||
|
document.getElementById('deviceManagementContent').innerHTML = `
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> Fehler beim Laden der Geräte
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gerät deaktivieren
|
||||||
|
function deactivateDevice(licenseId, deviceId, deviceName) {
|
||||||
|
if (!confirm(`Möchten Sie das Gerät "${deviceName}" wirklich deaktivieren?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/api/license/${licenseId}/deactivate-device/${deviceId}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
// Aktualisiere die Geräte-Ansicht
|
||||||
|
showDeviceManagement(licenseId);
|
||||||
|
// Aktualisiere die Lizenz-Anzeige wenn möglich
|
||||||
|
if (typeof loadCustomerLicenses === 'function' && currentCustomerId) {
|
||||||
|
loadCustomerLicenses(currentCustomerId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Fehler beim Deaktivieren des Geräts');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deactivating device:', error);
|
||||||
|
alert('Fehler beim Deaktivieren des Geräts');
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -54,6 +54,15 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="deviceLimit" class="form-label">Gerätelimit</label>
|
||||||
|
<select class="form-select" id="deviceLimit" name="device_limit" required>
|
||||||
|
{% for i in range(1, 11) %}
|
||||||
|
<option value="{{ i }}" {% if license[10] == i %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">Maximale Anzahl gleichzeitig aktiver Geräte</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-check mt-3">
|
<div class="form-check mt-3">
|
||||||
|
|||||||
@@ -126,6 +126,32 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Device Limit -->
|
||||||
|
<div class="card mt-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">
|
||||||
|
<i class="fas fa-laptop"></i> Gerätelimit
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="deviceLimit" class="form-label">
|
||||||
|
Maximale Anzahl Geräte
|
||||||
|
</label>
|
||||||
|
<select class="form-select" id="deviceLimit" name="device_limit" required>
|
||||||
|
{% for i in range(1, 11) %}
|
||||||
|
<option value="{{ i }}" {% if i == 3 %}selected{% endif %}>{{ i }} {% if i == 1 %}Gerät{% else %}Geräte{% endif %}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Anzahl der Geräte, auf denen die Lizenz gleichzeitig aktiviert sein kann.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Test Data Checkbox -->
|
<!-- Test Data Checkbox -->
|
||||||
<div class="form-check mt-3">
|
<div class="form-check mt-3">
|
||||||
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren