Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -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()