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/", 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/", 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//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//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()