diff --git a/v2_adminpanel/routes/api_routes.py b/v2_adminpanel/routes/api_routes.py index 2498f63..5d8de92 100644 --- a/v2_adminpanel/routes/api_routes.py +++ b/v2_adminpanel/routes/api_routes.py @@ -375,9 +375,11 @@ def deactivate_device(license_id, device_id): @api_bp.route("/licenses/bulk-delete", methods=["POST"]) @login_required def bulk_delete_licenses(): - """Lösche mehrere Lizenzen gleichzeitig""" + """Lösche mehrere Lizenzen gleichzeitig mit Sicherheitsprüfungen""" data = request.get_json() - license_ids = data.get('license_ids', []) + # Accept both 'ids' (from frontend) and 'license_ids' for compatibility + license_ids = data.get('ids', data.get('license_ids', [])) + force_delete = data.get('force', False) if not license_ids: return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400 @@ -387,35 +389,120 @@ def bulk_delete_licenses(): try: deleted_count = 0 + skipped_licenses = [] + active_licenses = [] + recently_used_licenses = [] for license_id in license_ids: - # Hole Lizenz-Info für Audit - cur.execute("SELECT license_key FROM licenses WHERE id = %s", (license_id,)) + # Hole vollständige Lizenz-Info + cur.execute(""" + SELECT l.id, l.license_key, l.is_active, l.is_test, + c.name as customer_name + FROM licenses l + LEFT JOIN customers c ON l.customer_id = c.id + WHERE l.id = %s + """, (license_id,)) result = cur.fetchone() - if result: - license_key = result[0] + if not result: + continue - # Lösche Sessions - cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_key,)) - - # Lösche Geräte-Registrierungen + license_id, license_key, is_active, is_test, customer_name = result + + # Safety check: Don't delete active licenses unless forced + if is_active and not force_delete: + active_licenses.append(f"{license_key} ({customer_name})") + skipped_licenses.append(license_id) + continue + + # Check for recent activity (heartbeats in last 24 hours) + if not force_delete: + try: + cur.execute(""" + SELECT COUNT(*) + FROM license_heartbeats + WHERE license_id = %s + AND timestamp > NOW() - INTERVAL '24 hours' + """, (license_id,)) + recent_heartbeats = cur.fetchone()[0] + + if recent_heartbeats > 0: + recently_used_licenses.append(f"{license_key} ({recent_heartbeats} activities)") + skipped_licenses.append(license_id) + continue + except: + # If heartbeats table doesn't exist, continue + pass + + # Check for active devices + if not force_delete: + try: + cur.execute(""" + SELECT COUNT(*) + FROM activations + WHERE license_id = %s + AND is_active = true + """, (license_id,)) + active_devices = cur.fetchone()[0] + + if active_devices > 0: + recently_used_licenses.append(f"{license_key} ({active_devices} active devices)") + skipped_licenses.append(license_id) + continue + except: + # If activations table doesn't exist, continue + pass + + # Delete associated data + cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_key,)) + + try: cur.execute("DELETE FROM device_registrations WHERE license_id = %s", (license_id,)) + except: + pass - # Lösche Lizenz - cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,)) + try: + cur.execute("DELETE FROM license_heartbeats WHERE license_id = %s", (license_id,)) + except: + pass - # Audit-Log - log_audit('BULK_DELETE', 'license', license_id, - old_values={'license_key': license_key}) - - deleted_count += 1 + try: + cur.execute("DELETE FROM activations WHERE license_id = %s", (license_id,)) + except: + pass + + # Delete the license + cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,)) + + # Audit-Log + log_audit('BULK_DELETE', 'license', license_id, + old_values={ + 'license_key': license_key, + 'customer_name': customer_name, + 'was_active': is_active, + 'forced': force_delete + }) + + deleted_count += 1 conn.commit() + # Build response message + message = f"{deleted_count} Lizenz(en) gelöscht." + warnings = [] + + if active_licenses: + warnings.append(f"Aktive Lizenzen übersprungen: {', '.join(active_licenses[:3])}{'...' if len(active_licenses) > 3 else ''}") + + if recently_used_licenses: + warnings.append(f"Kürzlich genutzte Lizenzen übersprungen: {', '.join(recently_used_licenses[:3])}{'...' if len(recently_used_licenses) > 3 else ''}") + return jsonify({ 'success': True, - 'deleted_count': deleted_count + 'deleted_count': deleted_count, + 'skipped_count': len(skipped_licenses), + 'message': message, + 'warnings': warnings }) except Exception as e: diff --git a/v2_adminpanel/routes/license_routes.py b/v2_adminpanel/routes/license_routes.py index 21f8811..ce7afb8 100644 --- a/v2_adminpanel/routes/license_routes.py +++ b/v2_adminpanel/routes/license_routes.py @@ -175,6 +175,9 @@ def edit_license(license_id): @license_bp.route("/license/delete/", methods=["POST"]) @login_required def delete_license(license_id): + # Check for force parameter + force_delete = request.form.get('force', 'false').lower() == 'true' + conn = get_connection() cur = conn.cursor() @@ -185,21 +188,77 @@ def delete_license(license_id): flash('Lizenz nicht gefunden!', 'error') return redirect(url_for('licenses.licenses')) + # Safety check: Don't delete active licenses unless forced + if license_data.get('is_active') and not force_delete: + flash(f'Lizenz {license_data["license_key"]} ist noch aktiv! Bitte deaktivieren Sie die Lizenz zuerst oder nutzen Sie "Erzwungenes Löschen".', 'warning') + return redirect(url_for('licenses.licenses')) + + # Check for recent activity (heartbeats in last 24 hours) + try: + cur.execute(""" + SELECT COUNT(*) + FROM license_heartbeats + WHERE license_id = %s + AND timestamp > NOW() - INTERVAL '24 hours' + """, (license_id,)) + recent_heartbeats = cur.fetchone()[0] + + if recent_heartbeats > 0 and not force_delete: + flash(f'Lizenz {license_data["license_key"]} hatte in den letzten 24 Stunden {recent_heartbeats} Aktivitäten! ' + f'Die Lizenz wird möglicherweise noch aktiv genutzt. Bitte prüfen Sie dies vor dem Löschen.', 'danger') + return redirect(url_for('licenses.licenses')) + except Exception as e: + # If heartbeats table doesn't exist, continue + logging.warning(f"Could not check heartbeats: {str(e)}") + + # Check for active devices/activations + try: + cur.execute(""" + SELECT COUNT(*) + FROM activations + WHERE license_id = %s + AND is_active = true + """, (license_id,)) + active_devices = cur.fetchone()[0] + + if active_devices > 0 and not force_delete: + flash(f'Lizenz {license_data["license_key"]} hat {active_devices} aktive Geräte! ' + f'Bitte deaktivieren Sie alle Geräte vor dem Löschen.', 'danger') + return redirect(url_for('licenses.licenses')) + except Exception as e: + # If activations table doesn't exist, continue + logging.warning(f"Could not check activations: {str(e)}") + # Delete from sessions first cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_data['license_key'],)) + # Delete from license_heartbeats if exists + try: + cur.execute("DELETE FROM license_heartbeats WHERE license_id = %s", (license_id,)) + except: + pass + + # Delete from activations if exists + try: + cur.execute("DELETE FROM activations WHERE license_id = %s", (license_id,)) + except: + pass + # Delete the license cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,)) conn.commit() - # Log deletion + # Log deletion with force flag log_audit('DELETE', 'license', license_id, old_values={ 'license_key': license_data['license_key'], 'customer_name': license_data['customer_name'], - 'customer_email': license_data['customer_email'] - }) + 'customer_email': license_data['customer_email'], + 'was_active': license_data.get('is_active'), + 'forced': force_delete + }, + additional_info=f"{'Forced deletion' if force_delete else 'Normal deletion'}") flash(f'Lizenz {license_data["license_key"]} erfolgreich gelöscht!', 'success') diff --git a/v2_adminpanel/templates/licenses.html b/v2_adminpanel/templates/licenses.html index 40ce445..b6a1c1e 100644 --- a/v2_adminpanel/templates/licenses.html +++ b/v2_adminpanel/templates/licenses.html @@ -367,9 +367,18 @@ function performBulkAction(url, ids) { .then(response => response.json()) .then(data => { if (data.success) { + // Show warnings if any licenses were skipped + if (data.warnings && data.warnings.length > 0) { + let warningMessage = data.message + '\n\n'; + warningMessage += 'Warnungen:\n'; + data.warnings.forEach(warning => { + warningMessage += '⚠️ ' + warning + '\n'; + }); + alert(warningMessage); + } location.reload(); } else { - alert('Fehler bei der Bulk-Aktion: ' + data.message); + alert('Fehler bei der Bulk-Aktion: ' + (data.error || data.message)); } }); }