diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 4e9e305..207df2c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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": [] } diff --git a/JOURNAL.md b/JOURNAL.md index 3d61865..fc95379 100644 --- a/JOURNAL.md +++ b/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//devices` - Listet alle Geräte einer Lizenz +- `POST /api/license//register-device` - Registriert ein neues Gerät +- `POST /api/license//deactivate-device/` - 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. diff --git a/v2_adminpanel/app.py b/v2_adminpanel/app.py index 5e7ee60..3b092ba 100644 --- a/v2_adminpanel/app.py +++ b/v2_adminpanel/app.py @@ -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//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//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//deactivate-device/", 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(): diff --git a/v2_adminpanel/init.sql b/v2_adminpanel/init.sql index 6104129..fc91a66 100644 --- a/v2_adminpanel/init.sql +++ b/v2_adminpanel/init.sql @@ -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); diff --git a/v2_adminpanel/migrate_device_limit.sql b/v2_adminpanel/migrate_device_limit.sql new file mode 100644 index 0000000..d62291f --- /dev/null +++ b/v2_adminpanel/migrate_device_limit.sql @@ -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; \ No newline at end of file diff --git a/v2_adminpanel/templates/batch_form.html b/v2_adminpanel/templates/batch_form.html index c2cb82b..0a5901e 100644 --- a/v2_adminpanel/templates/batch_form.html +++ b/v2_adminpanel/templates/batch_form.html @@ -147,6 +147,32 @@ + +
+
+
+ Gerätelimit pro Lizenz +
+
+
+
+
+ + + + Jede generierte Lizenz kann auf maximal dieser Anzahl von Geräten gleichzeitig aktiviert werden. + +
+
+
+
+
diff --git a/v2_adminpanel/templates/customers_licenses.html b/v2_adminpanel/templates/customers_licenses.html index 4dcb79c..af28003 100644 --- a/v2_adminpanel/templates/customers_licenses.html +++ b/v2_adminpanel/templates/customers_licenses.html @@ -168,18 +168,24 @@
{% endif %} {% if license[9] > 0 %} -
+
📱 {{ license[9] }}
{% endif %} +
+ 💻 {{ license[11] }}/{{ license[10] }} +
@@ -253,6 +259,28 @@ + + +