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 \"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 \"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": []
|
||||
}
|
||||
|
||||
44
JOURNAL.md
44
JOURNAL.md
@@ -1,5 +1,49 @@
|
||||
# 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
|
||||
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))
|
||||
ipv4_count = int(request.form.get("ipv4_count", 1))
|
||||
phone_count = int(request.form.get("phone_count", 1))
|
||||
device_limit = int(request.form.get("device_limit", 3))
|
||||
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
@@ -1536,11 +1537,11 @@ def create_license():
|
||||
# Lizenz hinzufügen
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active,
|
||||
domain_count, ipv4_count, phone_count, is_test)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s)
|
||||
domain_count, ipv4_count, phone_count, device_limit, is_test)
|
||||
VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (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]
|
||||
|
||||
# Ressourcen zuweisen
|
||||
@@ -1652,6 +1653,7 @@ def create_license():
|
||||
'license_type': license_type,
|
||||
'valid_from': valid_from,
|
||||
'valid_until': valid_until,
|
||||
'device_limit': device_limit,
|
||||
'is_test': is_test
|
||||
})
|
||||
|
||||
@@ -1711,6 +1713,7 @@ def batch_licenses():
|
||||
domain_count = int(request.form.get("domain_count", 1))
|
||||
ipv4_count = int(request.form.get("ipv4_count", 1))
|
||||
phone_count = int(request.form.get("phone_count", 1))
|
||||
device_limit = int(request.form.get("device_limit", 3))
|
||||
|
||||
# Sicherheitslimit
|
||||
if quantity < 1 or quantity > 100:
|
||||
@@ -1803,11 +1806,11 @@ def batch_licenses():
|
||||
cur.execute("""
|
||||
INSERT INTO licenses (license_key, customer_id, license_type, is_test,
|
||||
valid_from, valid_until, is_active,
|
||||
domain_count, ipv4_count, phone_count)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s)
|
||||
domain_count, ipv4_count, phone_count, device_limit)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (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]
|
||||
|
||||
# Ressourcen für diese Lizenz zuweisen
|
||||
@@ -1983,7 +1986,7 @@ def edit_license(license_id):
|
||||
if request.method == "POST":
|
||||
# Alte Werte für Audit-Log abrufen
|
||||
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
|
||||
""", (license_id,))
|
||||
old_license = cur.fetchone()
|
||||
@@ -1995,13 +1998,14 @@ def edit_license(license_id):
|
||||
valid_until = request.form["valid_until"]
|
||||
is_active = request.form.get("is_active") == "on"
|
||||
is_test = request.form.get("is_test") == "on"
|
||||
device_limit = int(request.form.get("device_limit", 3))
|
||||
|
||||
cur.execute("""
|
||||
UPDATE licenses
|
||||
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
|
||||
""", (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()
|
||||
|
||||
@@ -2013,7 +2017,8 @@ def edit_license(license_id):
|
||||
'valid_from': str(old_license[2]),
|
||||
'valid_until': str(old_license[3]),
|
||||
'is_active': old_license[4],
|
||||
'is_test': old_license[5]
|
||||
'is_test': old_license[5],
|
||||
'device_limit': old_license[6]
|
||||
},
|
||||
new_values={
|
||||
'license_key': license_key,
|
||||
@@ -2021,7 +2026,8 @@ def edit_license(license_id):
|
||||
'valid_from': valid_from,
|
||||
'valid_until': valid_until,
|
||||
'is_active': is_active,
|
||||
'is_test': is_test
|
||||
'is_test': is_test,
|
||||
'device_limit': device_limit
|
||||
})
|
||||
|
||||
cur.close()
|
||||
@@ -2048,7 +2054,7 @@ def edit_license(license_id):
|
||||
# Get license data
|
||||
cur.execute("""
|
||||
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
|
||||
JOIN customers c ON l.customer_id = c.id
|
||||
WHERE l.id = %s
|
||||
@@ -2343,7 +2349,9 @@ def customers_licenses():
|
||||
END as status,
|
||||
l.domain_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
|
||||
WHERE l.customer_id = %s
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
@@ -2384,7 +2392,9 @@ def api_customer_licenses(customer_id):
|
||||
END as status,
|
||||
l.domain_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
|
||||
WHERE l.customer_id = %s
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
@@ -2434,6 +2444,8 @@ def api_customer_licenses(customer_id):
|
||||
'domain_count': row[7],
|
||||
'ipv4_count': row[8],
|
||||
'phone_count': row[9],
|
||||
'device_limit': row[10],
|
||||
'active_devices': row[11],
|
||||
'resources': resources
|
||||
})
|
||||
|
||||
@@ -3711,6 +3723,218 @@ def bulk_deactivate_licenses():
|
||||
except Exception as e:
|
||||
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"])
|
||||
@login_required
|
||||
def bulk_delete_licenses():
|
||||
|
||||
@@ -173,6 +173,38 @@ BEGIN
|
||||
END IF;
|
||||
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
|
||||
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);
|
||||
|
||||
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>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||
|
||||
@@ -168,18 +168,24 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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] }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="d-inline-block" data-bs-toggle="tooltip" title="Geräte">
|
||||
💻 {{ license[11] }}/{{ license[10] }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</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>
|
||||
</a>
|
||||
</div>
|
||||
@@ -253,6 +259,28 @@
|
||||
</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>
|
||||
.customer-item {
|
||||
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 += `
|
||||
<tr data-license-id="${license.id}">
|
||||
<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">
|
||||
<i class="bi bi-power"></i>
|
||||
</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>
|
||||
</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">
|
||||
@@ -774,5 +810,124 @@ function quarantineResource(resourceId, resourceValue) {
|
||||
function saveResourceChanges() {
|
||||
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>
|
||||
{% endblock %}
|
||||
@@ -54,6 +54,15 @@
|
||||
</label>
|
||||
</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 class="form-check mt-3">
|
||||
|
||||
@@ -126,6 +126,32 @@
|
||||
</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 -->
|
||||
<div class="form-check mt-3">
|
||||
<input class="form-check-input" type="checkbox" id="isTest" name="is_test">
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren