Initial commit
Dieser Commit ist enthalten in:
466
v2_adminpanel/routes/customer_routes.py
Normale Datei
466
v2_adminpanel/routes/customer_routes.py
Normale Datei
@ -0,0 +1,466 @@
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
|
||||
|
||||
import config
|
||||
from auth.decorators import login_required
|
||||
from utils.audit import log_audit
|
||||
from db import get_connection, get_db_connection, get_db_cursor
|
||||
from models import get_customers, get_customer_by_id
|
||||
|
||||
# Create Blueprint
|
||||
customer_bp = Blueprint('customers', __name__)
|
||||
|
||||
# Test route
|
||||
@customer_bp.route("/test-customers")
|
||||
def test_customers():
|
||||
return "Customer blueprint is working!"
|
||||
|
||||
|
||||
@customer_bp.route("/customers")
|
||||
@login_required
|
||||
def customers():
|
||||
show_fake = request.args.get('show_fake', 'false').lower() == 'true'
|
||||
search = request.args.get('search', '').strip()
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = 20
|
||||
sort = request.args.get('sort', 'name')
|
||||
order = request.args.get('order', 'asc')
|
||||
|
||||
customers_list = get_customers(show_fake=show_fake, search=search)
|
||||
|
||||
# Sortierung
|
||||
if sort == 'name':
|
||||
customers_list.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
|
||||
elif sort == 'email':
|
||||
customers_list.sort(key=lambda x: x['email'].lower(), reverse=(order == 'desc'))
|
||||
elif sort == 'created_at':
|
||||
customers_list.sort(key=lambda x: x['created_at'], reverse=(order == 'desc'))
|
||||
|
||||
# Paginierung
|
||||
total_customers = len(customers_list)
|
||||
total_pages = (total_customers + per_page - 1) // per_page
|
||||
start = (page - 1) * per_page
|
||||
end = start + per_page
|
||||
paginated_customers = customers_list[start:end]
|
||||
|
||||
return render_template("customers.html",
|
||||
customers=paginated_customers,
|
||||
show_fake=show_fake,
|
||||
search=search,
|
||||
page=page,
|
||||
per_page=per_page,
|
||||
total_pages=total_pages,
|
||||
total_customers=total_customers,
|
||||
sort=sort,
|
||||
order=order,
|
||||
current_order=order)
|
||||
|
||||
|
||||
@customer_bp.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def edit_customer(customer_id):
|
||||
if request.method == "POST":
|
||||
try:
|
||||
# Get current customer data for comparison
|
||||
current_customer = get_customer_by_id(customer_id)
|
||||
if not current_customer:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
try:
|
||||
# Update customer data
|
||||
new_values = {
|
||||
'name': request.form['name'],
|
||||
'email': request.form['email'],
|
||||
'is_fake': 'is_fake' in request.form
|
||||
}
|
||||
|
||||
cur.execute("""
|
||||
UPDATE customers
|
||||
SET name = %s, email = %s, is_fake = %s
|
||||
WHERE id = %s
|
||||
""", (
|
||||
new_values['name'],
|
||||
new_values['email'],
|
||||
new_values['is_fake'],
|
||||
customer_id
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log changes
|
||||
log_audit('UPDATE', 'customer', customer_id,
|
||||
old_values={
|
||||
'name': current_customer['name'],
|
||||
'email': current_customer['email'],
|
||||
'is_fake': current_customer.get('is_fake', False)
|
||||
},
|
||||
new_values=new_values)
|
||||
|
||||
flash('Kunde erfolgreich aktualisiert!', 'success')
|
||||
|
||||
# Redirect mit show_fake Parameter wenn nötig
|
||||
redirect_url = url_for('customers.customers_licenses')
|
||||
if request.form.get('show_fake') == 'true':
|
||||
redirect_url += '?show_fake=true'
|
||||
return redirect(redirect_url)
|
||||
finally:
|
||||
cur.close()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Aktualisieren des Kunden: {str(e)}")
|
||||
flash('Fehler beim Aktualisieren des Kunden!', 'error')
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
# GET request
|
||||
customer_data = get_customer_by_id(customer_id)
|
||||
if not customer_data:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
return render_template("edit_customer.html", customer=customer_data)
|
||||
|
||||
|
||||
@customer_bp.route("/customer/create", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def create_customer():
|
||||
if request.method == "POST":
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Insert new customer
|
||||
name = request.form['name']
|
||||
email = request.form['email']
|
||||
is_fake = 'is_fake' in request.form # Checkbox ist nur vorhanden wenn angekreuzt
|
||||
|
||||
cur.execute("""
|
||||
INSERT INTO customers (name, email, is_fake, created_at)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
RETURNING id
|
||||
""", (name, email, is_fake, datetime.now()))
|
||||
|
||||
customer_id = cur.fetchone()[0]
|
||||
conn.commit()
|
||||
|
||||
# Log creation
|
||||
log_audit('CREATE', 'customer', customer_id,
|
||||
new_values={
|
||||
'name': name,
|
||||
'email': email,
|
||||
'is_fake': is_fake
|
||||
})
|
||||
|
||||
if is_fake:
|
||||
flash(f'Fake-Kunde {name} erfolgreich erstellt!', 'success')
|
||||
else:
|
||||
flash(f'Kunde {name} erfolgreich erstellt!', 'success')
|
||||
|
||||
# Redirect mit show_fake=true wenn Fake-Kunde erstellt wurde
|
||||
if is_fake:
|
||||
return redirect(url_for('customers.customers_licenses', show_fake='true'))
|
||||
else:
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Erstellen des Kunden: {str(e)}")
|
||||
flash('Fehler beim Erstellen des Kunden!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return render_template("create_customer.html")
|
||||
|
||||
|
||||
@customer_bp.route("/customer/delete/<int:customer_id>", methods=["POST"])
|
||||
@login_required
|
||||
def delete_customer(customer_id):
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Get customer data before deletion
|
||||
customer_data = get_customer_by_id(customer_id)
|
||||
if not customer_data:
|
||||
flash('Kunde nicht gefunden!', 'error')
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
# Check if customer has licenses
|
||||
cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
|
||||
license_count = cur.fetchone()[0]
|
||||
|
||||
if license_count > 0:
|
||||
flash(f'Kunde kann nicht gelöscht werden - hat noch {license_count} Lizenz(en)!', 'error')
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
# Delete the customer
|
||||
cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
|
||||
|
||||
conn.commit()
|
||||
|
||||
# Log deletion
|
||||
log_audit('DELETE', 'customer', customer_id,
|
||||
old_values={
|
||||
'name': customer_data['name'],
|
||||
'email': customer_data['email']
|
||||
})
|
||||
|
||||
flash(f'Kunde {customer_data["name"]} erfolgreich gelöscht!', 'success')
|
||||
|
||||
except Exception as e:
|
||||
conn.rollback()
|
||||
logging.error(f"Fehler beim Löschen des Kunden: {str(e)}")
|
||||
flash('Fehler beim Löschen des Kunden!', 'error')
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
return redirect(url_for('customers.customers_licenses'))
|
||||
|
||||
|
||||
@customer_bp.route("/customers-licenses")
|
||||
@login_required
|
||||
def customers_licenses():
|
||||
"""Zeigt die Übersicht von Kunden und deren Lizenzen"""
|
||||
import logging
|
||||
import psycopg2
|
||||
logging.info("=== CUSTOMERS-LICENSES ROUTE CALLED ===")
|
||||
|
||||
# Get show_fake parameter from URL
|
||||
show_fake = request.args.get('show_fake', 'false').lower() == 'true'
|
||||
logging.info(f"show_fake parameter: {show_fake}")
|
||||
|
||||
try:
|
||||
# Direkte Verbindung ohne Helper-Funktionen
|
||||
conn = psycopg2.connect(
|
||||
host=os.getenv("POSTGRES_HOST", "postgres"),
|
||||
port=os.getenv("POSTGRES_PORT", "5432"),
|
||||
dbname=os.getenv("POSTGRES_DB"),
|
||||
user=os.getenv("POSTGRES_USER"),
|
||||
password=os.getenv("POSTGRES_PASSWORD")
|
||||
)
|
||||
conn.set_client_encoding('UTF8')
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole alle Kunden mit ihren Lizenzen
|
||||
# Wenn show_fake=false, zeige nur Nicht-Test-Kunden
|
||||
query = """
|
||||
SELECT
|
||||
c.id,
|
||||
c.name,
|
||||
c.email,
|
||||
c.created_at,
|
||||
COUNT(l.id),
|
||||
COUNT(CASE WHEN l.is_active = true THEN 1 END),
|
||||
COUNT(CASE WHEN l.is_fake = true THEN 1 END),
|
||||
MAX(l.created_at),
|
||||
c.is_fake
|
||||
FROM customers c
|
||||
LEFT JOIN licenses l ON c.id = l.customer_id
|
||||
WHERE (%s OR c.is_fake = false)
|
||||
GROUP BY c.id, c.name, c.email, c.created_at, c.is_fake
|
||||
ORDER BY c.name
|
||||
"""
|
||||
|
||||
cur.execute(query, (show_fake,))
|
||||
|
||||
customers = []
|
||||
results = cur.fetchall()
|
||||
logging.info(f"=== QUERY RETURNED {len(results)} ROWS ===")
|
||||
|
||||
for idx, row in enumerate(results):
|
||||
logging.info(f"Row {idx}: Type={type(row)}, Length={len(row) if hasattr(row, '__len__') else 'N/A'}")
|
||||
customers.append({
|
||||
'id': row[0],
|
||||
'name': row[1],
|
||||
'email': row[2],
|
||||
'created_at': row[3],
|
||||
'license_count': row[4],
|
||||
'active_licenses': row[5],
|
||||
'test_licenses': row[6],
|
||||
'last_license_created': row[7],
|
||||
'is_fake': row[8]
|
||||
})
|
||||
|
||||
return render_template("customers_licenses.html",
|
||||
customers=customers,
|
||||
show_fake=show_fake)
|
||||
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_details = f"Fehler beim Laden der Kunden-Lizenz-Übersicht: {str(e)}\nType: {type(e)}\nTraceback: {traceback.format_exc()}"
|
||||
logging.error(error_details)
|
||||
flash(f'Datenbankfehler: {str(e)}', 'error')
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
|
||||
|
||||
@customer_bp.route("/api/customer/<int:customer_id>/licenses")
|
||||
@login_required
|
||||
def api_customer_licenses(customer_id):
|
||||
"""API-Endpunkt für die Lizenzen eines Kunden"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# Hole Kundeninformationen
|
||||
customer = get_customer_by_id(customer_id)
|
||||
if not customer:
|
||||
return jsonify({'error': 'Kunde nicht gefunden'}), 404
|
||||
|
||||
# Hole alle Lizenzen des Kunden - vereinfachte Version ohne komplexe Subqueries
|
||||
cur.execute("""
|
||||
SELECT
|
||||
l.id,
|
||||
l.license_key,
|
||||
l.license_type,
|
||||
l.is_active,
|
||||
l.is_fake,
|
||||
l.valid_from,
|
||||
l.valid_until,
|
||||
l.device_limit,
|
||||
l.created_at,
|
||||
CASE
|
||||
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
|
||||
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab'
|
||||
WHEN l.is_active = false THEN 'inaktiv'
|
||||
ELSE 'aktiv'
|
||||
END as status,
|
||||
COALESCE(l.domain_count, 0) as domain_count,
|
||||
COALESCE(l.ipv4_count, 0) as ipv4_count,
|
||||
COALESCE(l.phone_count, 0) as phone_count
|
||||
FROM licenses l
|
||||
WHERE l.customer_id = %s
|
||||
ORDER BY l.created_at DESC, l.id DESC
|
||||
""", (customer_id,))
|
||||
|
||||
licenses = []
|
||||
for row in cur.fetchall():
|
||||
license_id = row[0]
|
||||
|
||||
# Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
|
||||
conn2 = get_connection()
|
||||
cur2 = conn2.cursor()
|
||||
cur2.execute("""
|
||||
SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
|
||||
FROM resource_pools rp
|
||||
JOIN license_resources lr ON rp.id = lr.resource_id
|
||||
WHERE lr.license_id = %s AND lr.is_active = true
|
||||
ORDER BY rp.resource_type, rp.resource_value
|
||||
""", (license_id,))
|
||||
|
||||
resources = {
|
||||
'domains': [],
|
||||
'ipv4s': [],
|
||||
'phones': []
|
||||
}
|
||||
|
||||
for res_row in cur2.fetchall():
|
||||
resource_data = {
|
||||
'id': res_row[0],
|
||||
'value': res_row[2],
|
||||
'assigned_at': res_row[3].strftime('%Y-%m-%d %H:%M:%S') if res_row[3] else None
|
||||
}
|
||||
|
||||
if res_row[1] == 'domain':
|
||||
resources['domains'].append(resource_data)
|
||||
elif res_row[1] == 'ipv4':
|
||||
resources['ipv4s'].append(resource_data)
|
||||
elif res_row[1] == 'phone':
|
||||
resources['phones'].append(resource_data)
|
||||
|
||||
cur2.close()
|
||||
conn2.close()
|
||||
|
||||
licenses.append({
|
||||
'id': row[0],
|
||||
'license_key': row[1],
|
||||
'license_type': row[2],
|
||||
'is_active': row[3],
|
||||
'is_fake': row[4],
|
||||
'valid_from': row[5].strftime('%Y-%m-%d') if row[5] else None,
|
||||
'valid_until': row[6].strftime('%Y-%m-%d') if row[6] else None,
|
||||
'device_limit': row[7],
|
||||
'created_at': row[8].strftime('%Y-%m-%d %H:%M:%S') if row[8] else None,
|
||||
'status': row[9],
|
||||
'domain_count': row[10],
|
||||
'ipv4_count': row[11],
|
||||
'phone_count': row[12],
|
||||
'active_sessions': 0, # Platzhalter
|
||||
'registered_devices': 0, # Platzhalter
|
||||
'active_devices': 0, # Platzhalter
|
||||
'actual_domain_count': len(resources['domains']),
|
||||
'actual_ipv4_count': len(resources['ipv4s']),
|
||||
'actual_phone_count': len(resources['phones']),
|
||||
'resources': resources,
|
||||
# License Server Data (Platzhalter bis Implementation)
|
||||
'recent_heartbeats': 0,
|
||||
'last_heartbeat': None,
|
||||
'active_server_devices': 0,
|
||||
'unresolved_anomalies': 0
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True, # Wichtig: Frontend erwartet dieses Feld
|
||||
'customer': {
|
||||
'id': customer['id'],
|
||||
'name': customer['name'],
|
||||
'email': customer['email'],
|
||||
'is_fake': customer.get('is_fake', False) # Include the is_fake field
|
||||
},
|
||||
'licenses': licenses
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
import traceback
|
||||
error_msg = f"Fehler beim Laden der Kundenlizenzen: {str(e)}\nTraceback: {traceback.format_exc()}"
|
||||
logging.error(error_msg)
|
||||
return jsonify({'error': f'Fehler beim Laden der Daten: {str(e)}', 'details': error_msg}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
@customer_bp.route("/api/customer/<int:customer_id>/quick-stats")
|
||||
@login_required
|
||||
def api_customer_quick_stats(customer_id):
|
||||
"""Schnelle Statistiken für einen Kunden"""
|
||||
conn = get_connection()
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
cur.execute("""
|
||||
SELECT
|
||||
COUNT(l.id) as total_licenses,
|
||||
COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_licenses,
|
||||
COUNT(CASE WHEN l.is_fake = true THEN 1 END) as test_licenses,
|
||||
SUM(l.device_limit) as total_device_limit
|
||||
FROM licenses l
|
||||
WHERE l.customer_id = %s
|
||||
""", (customer_id,))
|
||||
|
||||
row = cur.fetchone()
|
||||
|
||||
return jsonify({
|
||||
'total_licenses': row[0] or 0,
|
||||
'active_licenses': row[1] or 0,
|
||||
'test_licenses': row[2] or 0,
|
||||
'total_device_limit': row[3] or 0
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Fehler beim Laden der Kundenstatistiken: {str(e)}")
|
||||
return jsonify({'error': 'Fehler beim Laden der Daten'}), 500
|
||||
finally:
|
||||
cur.close()
|
||||
conn.close()
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren