466 Zeilen
17 KiB
Python
466 Zeilen
17 KiB
Python
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() |