1022 Zeilen
34 KiB
Python
1022 Zeilen
34 KiB
Python
import logging
|
|
from datetime import datetime
|
|
from zoneinfo import ZoneInfo
|
|
from flask import Blueprint, request, jsonify, session
|
|
|
|
import config
|
|
from auth.decorators import login_required
|
|
from utils.audit import log_audit
|
|
from utils.network import get_client_ip
|
|
from utils.license import generate_license_key
|
|
from db import get_connection, get_db_connection, get_db_cursor
|
|
from models import get_license_by_id, get_customers
|
|
|
|
# Create Blueprint
|
|
api_bp = Blueprint('api', __name__, url_prefix='/api')
|
|
|
|
|
|
@api_bp.route("/customers", methods=["GET"])
|
|
@login_required
|
|
def api_customers():
|
|
"""API endpoint for customer search (used by Select2)"""
|
|
search = request.args.get('q', '').strip()
|
|
page = int(request.args.get('page', 1))
|
|
per_page = 20
|
|
|
|
try:
|
|
# Get all customers (with optional search)
|
|
customers = get_customers(show_fake=True, search=search)
|
|
|
|
# Pagination
|
|
start = (page - 1) * per_page
|
|
end = start + per_page
|
|
paginated_customers = customers[start:end]
|
|
|
|
# Format for Select2
|
|
results = []
|
|
for customer in paginated_customers:
|
|
results.append({
|
|
'id': customer['id'],
|
|
'text': f"{customer['name']} ({customer['email'] or 'keine E-Mail'})",
|
|
'is_fake': customer.get('is_fake', False) # Include the is_fake field
|
|
})
|
|
|
|
return jsonify({
|
|
'results': results,
|
|
'pagination': {
|
|
'more': len(customers) > end
|
|
}
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Error in api_customers: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Laden der Kunden'}), 500
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/toggle", methods=["POST"])
|
|
@login_required
|
|
def toggle_license(license_id):
|
|
"""Toggle license is_active status"""
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Get current status
|
|
license_data = get_license_by_id(license_id)
|
|
if not license_data:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
new_status = not license_data['is_active']
|
|
|
|
# Update status
|
|
cur.execute("UPDATE licenses SET is_active = %s WHERE id = %s", (new_status, license_id))
|
|
conn.commit()
|
|
|
|
# Log change
|
|
log_audit('TOGGLE', 'license', license_id,
|
|
old_values={'is_active': license_data['is_active']},
|
|
new_values={'is_active': new_status})
|
|
|
|
return jsonify({'success': True, 'is_active': new_status})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Umschalten der Lizenz: {str(e)}", exc_info=True)
|
|
return jsonify({'error': 'Fehler beim Umschalten der Lizenz'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/licenses/bulk-activate", methods=["POST"])
|
|
@login_required
|
|
def bulk_activate_licenses():
|
|
"""Aktiviere mehrere Lizenzen gleichzeitig"""
|
|
data = request.get_json()
|
|
license_ids = data.get('license_ids', [])
|
|
|
|
if not license_ids:
|
|
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Update all selected licenses
|
|
cur.execute("""
|
|
UPDATE licenses
|
|
SET is_active = true
|
|
WHERE id = ANY(%s) AND is_active = false
|
|
RETURNING id
|
|
""", (license_ids,))
|
|
|
|
updated_ids = [row[0] for row in cur.fetchall()]
|
|
conn.commit()
|
|
|
|
# Log changes
|
|
for license_id in updated_ids:
|
|
log_audit('BULK_ACTIVATE', 'license', license_id,
|
|
new_values={'is_active': True})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'updated_count': len(updated_ids)
|
|
})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Bulk-Aktivieren: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Aktivieren der Lizenzen'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/licenses/bulk-deactivate", methods=["POST"])
|
|
@login_required
|
|
def bulk_deactivate_licenses():
|
|
"""Deaktiviere mehrere Lizenzen gleichzeitig"""
|
|
data = request.get_json()
|
|
license_ids = data.get('license_ids', [])
|
|
|
|
if not license_ids:
|
|
return jsonify({'error': 'Keine Lizenzen ausgewählt'}), 400
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Update all selected licenses
|
|
cur.execute("""
|
|
UPDATE licenses
|
|
SET is_active = false
|
|
WHERE id = ANY(%s) AND is_active = true
|
|
RETURNING id
|
|
""", (license_ids,))
|
|
|
|
updated_ids = [row[0] for row in cur.fetchall()]
|
|
conn.commit()
|
|
|
|
# Log changes
|
|
for license_id in updated_ids:
|
|
log_audit('BULK_DEACTIVATE', 'license', license_id,
|
|
new_values={'is_active': False})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'updated_count': len(updated_ids)
|
|
})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Bulk-Deaktivieren: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Deaktivieren der Lizenzen'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/devices")
|
|
@login_required
|
|
def get_license_devices(license_id):
|
|
"""Hole alle Geräte einer Lizenz"""
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Hole Lizenz-Info
|
|
license_data = get_license_by_id(license_id)
|
|
if not license_data:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
# Hole registrierte Geräte
|
|
cur.execute("""
|
|
SELECT
|
|
dr.id,
|
|
dr.hardware_id,
|
|
dr.device_name,
|
|
dr.device_type,
|
|
dr.first_seen as registration_date,
|
|
dr.last_seen,
|
|
dr.is_active,
|
|
dr.operating_system,
|
|
dr.ip_address,
|
|
(SELECT COUNT(*) FROM sessions s
|
|
WHERE s.license_key = l.license_key
|
|
AND s.hardware_id = dr.hardware_id
|
|
AND s.is_active = true) as active_sessions
|
|
FROM device_registrations dr
|
|
JOIN licenses l ON dr.license_id = l.id
|
|
WHERE l.license_key = %s
|
|
ORDER BY dr.first_seen DESC
|
|
""", (license_data['license_key'],))
|
|
|
|
devices = []
|
|
for row in cur.fetchall():
|
|
devices.append({
|
|
'id': row[0],
|
|
'hardware_id': row[1],
|
|
'device_name': row[2],
|
|
'device_type': row[3],
|
|
'registration_date': row[4].isoformat() if row[4] else None,
|
|
'last_seen': row[5].isoformat() if row[5] else None,
|
|
'is_active': row[6],
|
|
'operating_system': row[7] or 'Unknown',
|
|
'ip_address': row[8] or 'Unknown',
|
|
'active_sessions': row[9],
|
|
'first_seen': row[4].isoformat() if row[4] else None
|
|
})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'license_key': license_data['license_key'],
|
|
'device_limit': license_data['device_limit'],
|
|
'devices': devices,
|
|
'device_count': len(devices),
|
|
'active_count': len([d 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({'error': 'Fehler beim Abrufen der Geräte'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/register-device", methods=["POST"])
|
|
@login_required
|
|
def register_device(license_id):
|
|
"""Registriere ein neues Gerät für eine Lizenz"""
|
|
data = request.get_json()
|
|
|
|
hardware_id = data.get('hardware_id')
|
|
device_name = data.get('device_name')
|
|
device_type = data.get('device_type', 'unknown')
|
|
|
|
if not hardware_id or not device_name:
|
|
return jsonify({'error': 'Geräte-ID und Name erforderlich'}), 400
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Hole Lizenz-Info
|
|
license_data = get_license_by_id(license_id)
|
|
if not license_data:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
# Prüfe Gerätelimit
|
|
cur.execute("""
|
|
SELECT COUNT(*) FROM device_registrations dr
|
|
JOIN licenses l ON dr.license_id = l.id
|
|
WHERE l.license_key = %s AND dr.is_active = true
|
|
""", (license_data['license_key'],))
|
|
|
|
active_device_count = cur.fetchone()[0]
|
|
|
|
if active_device_count >= license_data['device_limit']:
|
|
return jsonify({'error': 'Gerätelimit erreicht'}), 400
|
|
|
|
# Prüfe ob Gerät bereits registriert
|
|
cur.execute("""
|
|
SELECT dr.id, dr.is_active FROM device_registrations dr
|
|
JOIN licenses l ON dr.license_id = l.id
|
|
WHERE l.license_key = %s AND dr.hardware_id = %s
|
|
""", (license_data['license_key'], hardware_id))
|
|
|
|
existing = cur.fetchone()
|
|
|
|
if existing:
|
|
if existing[1]: # is_active
|
|
return jsonify({'error': 'Gerät bereits registriert'}), 400
|
|
else:
|
|
# Reaktiviere Gerät
|
|
cur.execute("""
|
|
UPDATE device_registrations
|
|
SET is_active = true, last_seen = CURRENT_TIMESTAMP
|
|
WHERE id = %s
|
|
""", (existing[0],))
|
|
else:
|
|
# Registriere neues Gerät
|
|
cur.execute("""
|
|
INSERT INTO device_registrations
|
|
(license_id, hardware_id, device_name, device_type, is_active)
|
|
VALUES (%s, %s, %s, %s, true)
|
|
""", (license_id, hardware_id, device_name, device_type))
|
|
|
|
conn.commit()
|
|
|
|
# Audit-Log
|
|
log_audit('DEVICE_REGISTER', 'license', license_id,
|
|
additional_info=f"Gerät {device_name} ({hardware_id}) registriert")
|
|
|
|
return jsonify({'success': True})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Registrieren des Geräts: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Registrieren des Geräts'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/deactivate-device/<int:device_id>", methods=["POST"])
|
|
@login_required
|
|
def deactivate_device(license_id, device_id):
|
|
"""Deaktiviere ein Gerät einer Lizenz"""
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Prüfe ob Gerät zur Lizenz gehört
|
|
cur.execute("""
|
|
SELECT dr.device_name, dr.hardware_id, l.license_key
|
|
FROM device_registrations dr
|
|
JOIN licenses l ON dr.license_id = l.id
|
|
WHERE dr.id = %s AND l.id = %s
|
|
""", (device_id, license_id))
|
|
|
|
device = cur.fetchone()
|
|
if not device:
|
|
return jsonify({'error': 'Gerät nicht gefunden'}), 404
|
|
|
|
# Deaktiviere Gerät
|
|
cur.execute("""
|
|
UPDATE device_registrations
|
|
SET is_active = false
|
|
WHERE id = %s
|
|
""", (device_id,))
|
|
|
|
# Beende aktive Sessions
|
|
cur.execute("""
|
|
UPDATE sessions
|
|
SET is_active = false, ended_at = CURRENT_TIMESTAMP
|
|
WHERE license_key = %s AND hardware_id = %s AND is_active = true
|
|
""", (device[2], device[1]))
|
|
|
|
conn.commit()
|
|
|
|
# Audit-Log
|
|
log_audit('DEVICE_DEACTIVATE', 'license', license_id,
|
|
additional_info=f"Gerät {device[0]} ({device[1]}) deaktiviert")
|
|
|
|
return jsonify({'success': True})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Deaktivieren des Geräts'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/licenses/bulk-delete", methods=["POST"])
|
|
@login_required
|
|
def bulk_delete_licenses():
|
|
"""Lösche mehrere Lizenzen gleichzeitig mit Sicherheitsprüfungen"""
|
|
data = request.get_json()
|
|
# 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
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
deleted_count = 0
|
|
skipped_licenses = []
|
|
active_licenses = []
|
|
recently_used_licenses = []
|
|
|
|
for license_id in license_ids:
|
|
# Hole vollständige Lizenz-Info
|
|
cur.execute("""
|
|
SELECT l.id, l.license_key, l.is_active, l.is_fake,
|
|
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 not result:
|
|
continue
|
|
|
|
license_id, license_key, is_active, is_fake, 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
|
|
|
|
try:
|
|
cur.execute("DELETE FROM license_heartbeats WHERE license_id = %s", (license_id,))
|
|
except:
|
|
pass
|
|
|
|
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,
|
|
'skipped_count': len(skipped_licenses),
|
|
'message': message,
|
|
'warnings': warnings
|
|
})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Bulk-Löschen: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Löschen der Lizenzen'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/quick-edit", methods=['POST'])
|
|
@login_required
|
|
def quick_edit_license(license_id):
|
|
"""Schnellbearbeitung einer Lizenz"""
|
|
data = request.get_json()
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Hole aktuelle Lizenz für Vergleich
|
|
current_license = get_license_by_id(license_id)
|
|
if not current_license:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
# Update nur die übergebenen Felder
|
|
updates = []
|
|
params = []
|
|
old_values = {}
|
|
new_values = {}
|
|
|
|
if 'device_limit' in data:
|
|
updates.append("device_limit = %s")
|
|
params.append(int(data['device_limit']))
|
|
old_values['device_limit'] = current_license['device_limit']
|
|
new_values['device_limit'] = int(data['device_limit'])
|
|
|
|
if 'valid_until' in data:
|
|
updates.append("valid_until = %s")
|
|
params.append(data['valid_until'])
|
|
old_values['valid_until'] = str(current_license['valid_until'])
|
|
new_values['valid_until'] = data['valid_until']
|
|
|
|
if 'is_active' in data:
|
|
updates.append("is_active = %s")
|
|
params.append(bool(data['is_active']))
|
|
old_values['is_active'] = current_license['is_active']
|
|
new_values['is_active'] = bool(data['is_active'])
|
|
|
|
if not updates:
|
|
return jsonify({'error': 'Keine Änderungen angegeben'}), 400
|
|
|
|
# Führe Update aus
|
|
params.append(license_id)
|
|
cur.execute(f"""
|
|
UPDATE licenses
|
|
SET {', '.join(updates)}
|
|
WHERE id = %s
|
|
""", params)
|
|
|
|
conn.commit()
|
|
|
|
# Audit-Log
|
|
log_audit('QUICK_EDIT', 'license', license_id,
|
|
old_values=old_values,
|
|
new_values=new_values)
|
|
|
|
return jsonify({'success': True})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler bei Schnellbearbeitung: {str(e)}")
|
|
return jsonify({'error': 'Fehler bei der Bearbeitung'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/license/<int:license_id>/resources")
|
|
@login_required
|
|
def get_license_resources(license_id):
|
|
"""Hole alle Ressourcen einer Lizenz"""
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Hole Lizenz-Info
|
|
license_data = get_license_by_id(license_id)
|
|
if not license_data:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
# Hole zugewiesene Ressourcen
|
|
cur.execute("""
|
|
SELECT
|
|
rp.id,
|
|
rp.resource_type,
|
|
rp.resource_value,
|
|
rp.is_fake,
|
|
rp.status_changed_at,
|
|
lr.assigned_at,
|
|
lr.assigned_by
|
|
FROM resource_pools rp
|
|
JOIN license_resources lr ON rp.id = lr.resource_id
|
|
WHERE lr.license_id = %s
|
|
ORDER BY rp.resource_type, rp.resource_value
|
|
""", (license_id,))
|
|
|
|
resources = []
|
|
for row in cur.fetchall():
|
|
resources.append({
|
|
'id': row[0],
|
|
'type': row[1],
|
|
'value': row[2],
|
|
'is_fake': row[3],
|
|
'status_changed_at': row[4].isoformat() if row[4] else None,
|
|
'assigned_at': row[5].isoformat() if row[5] else None,
|
|
'assigned_by': row[6]
|
|
})
|
|
|
|
# Gruppiere nach Typ
|
|
grouped = {}
|
|
for resource in resources:
|
|
res_type = resource['type']
|
|
if res_type not in grouped:
|
|
grouped[res_type] = []
|
|
grouped[res_type].append(resource)
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'license_key': license_data['license_key'],
|
|
'resources': resources,
|
|
'grouped': grouped,
|
|
'total_count': len(resources)
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Fehler beim Abrufen der Ressourcen: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Abrufen der Ressourcen'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/resources/allocate", methods=['POST'])
|
|
@login_required
|
|
def allocate_resources():
|
|
"""Weise Ressourcen einer Lizenz zu"""
|
|
data = request.get_json()
|
|
|
|
license_id = data.get('license_id')
|
|
resource_ids = data.get('resource_ids', [])
|
|
|
|
if not license_id or not resource_ids:
|
|
return jsonify({'error': 'Lizenz-ID und Ressourcen erforderlich'}), 400
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Prüfe Lizenz
|
|
license_data = get_license_by_id(license_id)
|
|
if not license_data:
|
|
return jsonify({'error': 'Lizenz nicht gefunden'}), 404
|
|
|
|
allocated_count = 0
|
|
errors = []
|
|
|
|
for resource_id in resource_ids:
|
|
try:
|
|
# Prüfe ob Ressource verfügbar ist
|
|
cur.execute("""
|
|
SELECT resource_value, status, is_fake
|
|
FROM resource_pools
|
|
WHERE id = %s
|
|
""", (resource_id,))
|
|
|
|
resource = cur.fetchone()
|
|
if not resource:
|
|
errors.append(f"Ressource {resource_id} nicht gefunden")
|
|
continue
|
|
|
|
if resource[1] != 'available':
|
|
errors.append(f"Ressource {resource[0]} ist nicht verfügbar")
|
|
continue
|
|
|
|
# Prüfe Test/Produktion Kompatibilität
|
|
if resource[2] != license_data['is_fake']:
|
|
errors.append(f"Ressource {resource[0]} ist {'Test' if resource[2] else 'Produktion'}, Lizenz ist {'Test' if license_data['is_fake'] else 'Produktion'}")
|
|
continue
|
|
|
|
# Weise Ressource zu
|
|
cur.execute("""
|
|
UPDATE resource_pools
|
|
SET status = 'allocated',
|
|
allocated_to_license = %s,
|
|
status_changed_at = CURRENT_TIMESTAMP,
|
|
status_changed_by = %s
|
|
WHERE id = %s
|
|
""", (license_id, session['username'], resource_id))
|
|
|
|
# Erstelle Verknüpfung
|
|
cur.execute("""
|
|
INSERT INTO license_resources (license_id, resource_id, assigned_by)
|
|
VALUES (%s, %s, %s)
|
|
""", (license_id, resource_id, session['username']))
|
|
|
|
# History-Eintrag
|
|
cur.execute("""
|
|
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
|
|
VALUES (%s, %s, 'allocated', %s, %s)
|
|
""", (resource_id, license_id, session['username'], get_client_ip()))
|
|
|
|
allocated_count += 1
|
|
|
|
except Exception as e:
|
|
errors.append(f"Fehler bei Ressource {resource_id}: {str(e)}")
|
|
|
|
conn.commit()
|
|
|
|
# Audit-Log
|
|
if allocated_count > 0:
|
|
log_audit('RESOURCE_ALLOCATE', 'license', license_id,
|
|
additional_info=f"{allocated_count} Ressourcen zugewiesen")
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'allocated_count': allocated_count,
|
|
'errors': errors
|
|
})
|
|
|
|
except Exception as e:
|
|
conn.rollback()
|
|
logging.error(f"Fehler beim Zuweisen der Ressourcen: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Zuweisen der Ressourcen'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/resources/check-availability", methods=['GET'])
|
|
@login_required
|
|
def check_resource_availability():
|
|
"""Prüfe Verfügbarkeit von Ressourcen"""
|
|
# Einzelne Ressource prüfen (alte API)
|
|
resource_type = request.args.get('type')
|
|
if resource_type:
|
|
count = int(request.args.get('count', 1))
|
|
is_fake = request.args.get('is_fake', 'false') == 'true'
|
|
show_fake = request.args.get('show_fake', 'false') == 'true'
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Hole verfügbare Ressourcen mit Details
|
|
if show_fake:
|
|
# Zeige alle verfügbaren Ressourcen (Test und Produktion)
|
|
cur.execute("""
|
|
SELECT id, resource_value, is_fake
|
|
FROM resource_pools
|
|
WHERE resource_type = %s
|
|
AND status = 'available'
|
|
ORDER BY is_fake, resource_value
|
|
LIMIT %s
|
|
""", (resource_type, count))
|
|
else:
|
|
# Zeige nur Produktions-Ressourcen
|
|
cur.execute("""
|
|
SELECT id, resource_value, is_fake
|
|
FROM resource_pools
|
|
WHERE resource_type = %s
|
|
AND status = 'available'
|
|
AND is_fake = false
|
|
ORDER BY resource_value
|
|
LIMIT %s
|
|
""", (resource_type, count))
|
|
|
|
available_resources = []
|
|
for row in cur.fetchall():
|
|
available_resources.append({
|
|
'id': row[0],
|
|
'value': row[1],
|
|
'is_fake': row[2]
|
|
})
|
|
|
|
return jsonify({
|
|
'resource_type': resource_type,
|
|
'requested': count,
|
|
'available': available_resources,
|
|
'sufficient': len(available_resources) >= count,
|
|
'show_fake': show_fake
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Fehler beim Prüfen der Verfügbarkeit: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
# Mehrere Ressourcen gleichzeitig prüfen (für Batch)
|
|
domain_count = int(request.args.get('domain', 0))
|
|
ipv4_count = int(request.args.get('ipv4', 0))
|
|
phone_count = int(request.args.get('phone', 0))
|
|
is_fake = request.args.get('is_fake', 'false') == 'true'
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
try:
|
|
# Zähle verfügbare Ressourcen für jeden Typ
|
|
result = {}
|
|
|
|
# Domains
|
|
cur.execute("""
|
|
SELECT COUNT(*)
|
|
FROM resource_pools
|
|
WHERE resource_type = 'domain'
|
|
AND status = 'available'
|
|
AND is_fake = %s
|
|
""", (is_fake,))
|
|
domain_available = cur.fetchone()[0]
|
|
|
|
# IPv4
|
|
cur.execute("""
|
|
SELECT COUNT(*)
|
|
FROM resource_pools
|
|
WHERE resource_type = 'ipv4'
|
|
AND status = 'available'
|
|
AND is_fake = %s
|
|
""", (is_fake,))
|
|
ipv4_available = cur.fetchone()[0]
|
|
|
|
# Phones
|
|
cur.execute("""
|
|
SELECT COUNT(*)
|
|
FROM resource_pools
|
|
WHERE resource_type = 'phone'
|
|
AND status = 'available'
|
|
AND is_fake = %s
|
|
""", (is_fake,))
|
|
phone_available = cur.fetchone()[0]
|
|
|
|
return jsonify({
|
|
'domain_requested': domain_count,
|
|
'domain_available': domain_available,
|
|
'domain_sufficient': domain_available >= domain_count,
|
|
'ipv4_requested': ipv4_count,
|
|
'ipv4_available': ipv4_available,
|
|
'ipv4_sufficient': ipv4_available >= ipv4_count,
|
|
'phone_requested': phone_count,
|
|
'phone_available': phone_available,
|
|
'phone_sufficient': phone_available >= phone_count,
|
|
'all_sufficient': (
|
|
domain_available >= domain_count and
|
|
ipv4_available >= ipv4_count and
|
|
phone_available >= phone_count
|
|
),
|
|
'is_fake': is_fake
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Fehler beim Prüfen der Verfügbarkeit: {str(e)}")
|
|
return jsonify({'error': 'Fehler beim Prüfen der Verfügbarkeit'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/global-search", methods=['GET'])
|
|
@login_required
|
|
def global_search():
|
|
"""Globale Suche über alle Entitäten"""
|
|
query = request.args.get('q', '').strip()
|
|
|
|
if not query or len(query) < 3:
|
|
return jsonify({'error': 'Suchbegriff muss mindestens 3 Zeichen haben'}), 400
|
|
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
results = {
|
|
'licenses': [],
|
|
'customers': [],
|
|
'resources': [],
|
|
'sessions': []
|
|
}
|
|
|
|
try:
|
|
# Suche in Lizenzen
|
|
cur.execute("""
|
|
SELECT id, license_key, customer_name, is_active
|
|
FROM licenses
|
|
WHERE license_key ILIKE %s
|
|
OR customer_name ILIKE %s
|
|
OR customer_email ILIKE %s
|
|
LIMIT 10
|
|
""", (f'%{query}%', f'%{query}%', f'%{query}%'))
|
|
|
|
for row in cur.fetchall():
|
|
results['licenses'].append({
|
|
'id': row[0],
|
|
'license_key': row[1],
|
|
'customer_name': row[2],
|
|
'is_active': row[3]
|
|
})
|
|
|
|
# Suche in Kunden
|
|
cur.execute("""
|
|
SELECT id, name, email, is_fake
|
|
FROM customers
|
|
WHERE name ILIKE %s OR email ILIKE %s
|
|
LIMIT 10
|
|
""", (f'%{query}%', f'%{query}%'))
|
|
|
|
for row in cur.fetchall():
|
|
results['customers'].append({
|
|
'id': row[0],
|
|
'name': row[1],
|
|
'email': row[2],
|
|
'is_fake': row[3]
|
|
})
|
|
|
|
# Suche in Ressourcen
|
|
cur.execute("""
|
|
SELECT id, resource_type, resource_value, status
|
|
FROM resource_pools
|
|
WHERE resource_value ILIKE %s
|
|
LIMIT 10
|
|
""", (f'%{query}%',))
|
|
|
|
for row in cur.fetchall():
|
|
results['resources'].append({
|
|
'id': row[0],
|
|
'type': row[1],
|
|
'value': row[2],
|
|
'status': row[3]
|
|
})
|
|
|
|
# Suche in Sessions
|
|
cur.execute("""
|
|
SELECT id, license_key, username, hardware_id, is_active
|
|
FROM sessions
|
|
WHERE username ILIKE %s OR hardware_id ILIKE %s
|
|
ORDER BY started_at DESC
|
|
LIMIT 10
|
|
""", (f'%{query}%', f'%{query}%'))
|
|
|
|
for row in cur.fetchall():
|
|
results['sessions'].append({
|
|
'id': row[0],
|
|
'license_key': row[1],
|
|
'username': row[2],
|
|
'hardware_id': row[3],
|
|
'is_active': row[4]
|
|
})
|
|
|
|
return jsonify(results)
|
|
|
|
except Exception as e:
|
|
logging.error(f"Fehler bei der globalen Suche: {str(e)}")
|
|
return jsonify({'error': 'Fehler bei der Suche'}), 500
|
|
finally:
|
|
cur.close()
|
|
conn.close()
|
|
|
|
|
|
@api_bp.route("/generate-license-key", methods=['POST'])
|
|
@login_required
|
|
def api_generate_key():
|
|
"""API Endpoint zur Generierung eines neuen Lizenzschlüssels"""
|
|
try:
|
|
# Lizenztyp aus Request holen (default: full)
|
|
data = request.get_json() or {}
|
|
license_type = data.get('type', 'full')
|
|
|
|
# Key generieren
|
|
key = generate_license_key(license_type)
|
|
|
|
# Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher)
|
|
conn = get_connection()
|
|
cur = conn.cursor()
|
|
|
|
# Wiederhole bis eindeutiger Key gefunden
|
|
attempts = 0
|
|
while attempts < 10: # Max 10 Versuche
|
|
cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,))
|
|
if not cur.fetchone():
|
|
break # Key ist eindeutig
|
|
key = generate_license_key(license_type)
|
|
attempts += 1
|
|
|
|
cur.close()
|
|
conn.close()
|
|
|
|
# Log für Audit
|
|
log_audit('GENERATE_KEY', 'license',
|
|
additional_info={'type': license_type, 'key': key})
|
|
|
|
return jsonify({
|
|
'success': True,
|
|
'key': key,
|
|
'type': license_type
|
|
})
|
|
|
|
except Exception as e:
|
|
logging.error(f"Fehler bei Key-Generierung: {str(e)}")
|
|
return jsonify({
|
|
'success': False,
|
|
'error': 'Fehler bei der Key-Generierung'
|
|
}), 500
|
|
|
|
|
|
|