import os import logging from datetime import datetime, timedelta from zoneinfo import ZoneInfo from dateutil.relativedelta import relativedelta 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 utils.network import get_client_ip from utils.license import validate_license_key from db import get_connection, get_db_connection, get_db_cursor from models import get_licenses, get_license_by_id # Create Blueprint license_bp = Blueprint('licenses', __name__) @license_bp.route("/licenses") @login_required def licenses(): from datetime import datetime, timedelta # Get filter parameters search = request.args.get('search', '').strip() data_source = request.args.get('data_source', 'real') # real, fake, all license_type = request.args.get('license_type', '') # '', full, test license_status = request.args.get('license_status', '') # '', active, expiring, expired, inactive sort = request.args.get('sort', 'created_at') order = request.args.get('order', 'desc') page = request.args.get('page', 1, type=int) per_page = 50 # Get licenses based on data source if data_source == 'fake': licenses_list = get_licenses(show_fake=True) licenses_list = [l for l in licenses_list if l.get('is_fake')] elif data_source == 'all': licenses_list = get_licenses(show_fake=True) else: # real licenses_list = get_licenses(show_fake=False) # Type filtering if license_type: if license_type == 'full': licenses_list = [l for l in licenses_list if l.get('license_type') == 'full'] elif license_type == 'test': licenses_list = [l for l in licenses_list if l.get('license_type') == 'test'] # Status filtering if license_status: now = datetime.now().date() filtered_licenses = [] for license in licenses_list: if license_status == 'active' and license.get('is_active'): # Active means is_active=true, regardless of expiration date filtered_licenses.append(license) elif license_status == 'expired' and license.get('valid_until') and license.get('valid_until') <= now: # Expired means past valid_until date, regardless of is_active filtered_licenses.append(license) elif license_status == 'inactive' and not license.get('is_active'): # Inactive means is_active=false, regardless of date filtered_licenses.append(license) licenses_list = filtered_licenses # Search filtering if search: search_lower = search.lower() licenses_list = [l for l in licenses_list if search_lower in str(l.get('license_key', '')).lower() or search_lower in str(l.get('customer_name', '')).lower() or search_lower in str(l.get('customer_email', '')).lower()] # Calculate pagination total = len(licenses_list) total_pages = (total + per_page - 1) // per_page start = (page - 1) * per_page end = start + per_page licenses_list = licenses_list[start:end] return render_template("licenses.html", licenses=licenses_list, search=search, data_source=data_source, license_type=license_type, license_status=license_status, sort=sort, order=order, page=page, total=total, total_pages=total_pages, per_page=per_page, now=datetime.now, timedelta=timedelta) @license_bp.route("/license/edit/", methods=["GET", "POST"]) @login_required def edit_license(license_id): conn = get_connection() cur = conn.cursor() if request.method == "POST": try: # Get current license data for comparison current_license = get_license_by_id(license_id) if not current_license: flash('Lizenz nicht gefunden!', 'error') return redirect(url_for('licenses.licenses')) # Update license data new_values = { 'license_key': request.form['license_key'], 'license_type': request.form['license_type'], 'valid_from': request.form['valid_from'], 'valid_until': request.form['valid_until'], 'is_active': 'is_active' in request.form, 'device_limit': int(request.form.get('device_limit', 3)) } cur.execute(""" UPDATE licenses SET license_key = %s, license_type = %s, valid_from = %s, valid_until = %s, is_active = %s, device_limit = %s WHERE id = %s """, ( new_values['license_key'], new_values['license_type'], new_values['valid_from'], new_values['valid_until'], new_values['is_active'], new_values['device_limit'], license_id )) conn.commit() # Log changes log_audit('UPDATE', 'license', license_id, old_values={ 'license_key': current_license.get('license_key'), 'license_type': current_license.get('license_type'), 'valid_from': str(current_license.get('valid_from', '')), 'valid_until': str(current_license.get('valid_until', '')), 'is_active': current_license.get('is_active'), 'device_limit': current_license.get('device_limit', 3) }, new_values=new_values) flash('Lizenz erfolgreich aktualisiert!', 'success') # Preserve show_test parameter if present show_test = request.args.get('show_test', 'false') return redirect(url_for('licenses.licenses', show_test=show_test)) except Exception as e: conn.rollback() logging.error(f"Fehler beim Aktualisieren der Lizenz: {str(e)}") flash('Fehler beim Aktualisieren der Lizenz!', 'error') finally: cur.close() conn.close() # GET request license_data = get_license_by_id(license_id) if not license_data: flash('Lizenz nicht gefunden!', 'error') return redirect(url_for('licenses.licenses')) return render_template("edit_license.html", license=license_data) @license_bp.route("/license/delete/", methods=["POST"]) @login_required def delete_license(license_id): # Check for force parameter force_delete = request.form.get('force', 'false').lower() == 'true' conn = get_connection() cur = conn.cursor() try: # Get license data before deletion license_data = get_license_by_id(license_id) if not license_data: flash('Lizenz nicht gefunden!', 'error') return redirect(url_for('licenses.licenses')) # Safety check: Don't delete active licenses unless forced if license_data.get('is_active') and not force_delete: flash(f'Lizenz {license_data["license_key"]} ist noch aktiv! Bitte deaktivieren Sie die Lizenz zuerst oder nutzen Sie "Erzwungenes Löschen".', 'warning') return redirect(url_for('licenses.licenses')) # Check for recent activity (heartbeats in last 24 hours) 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 and not force_delete: flash(f'Lizenz {license_data["license_key"]} hatte in den letzten 24 Stunden {recent_heartbeats} Aktivitäten! ' f'Die Lizenz wird möglicherweise noch aktiv genutzt. Bitte prüfen Sie dies vor dem Löschen.', 'danger') return redirect(url_for('licenses.licenses')) except Exception as e: # If heartbeats table doesn't exist, continue logging.warning(f"Could not check heartbeats: {str(e)}") # Check for active devices/activations 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 and not force_delete: flash(f'Lizenz {license_data["license_key"]} hat {active_devices} aktive Geräte! ' f'Bitte deaktivieren Sie alle Geräte vor dem Löschen.', 'danger') return redirect(url_for('licenses.licenses')) except Exception as e: # If activations table doesn't exist, continue logging.warning(f"Could not check activations: {str(e)}") # Delete from sessions first cur.execute("DELETE FROM sessions WHERE license_key = %s", (license_data['license_key'],)) # Delete from license_heartbeats if exists try: cur.execute("DELETE FROM license_heartbeats WHERE license_id = %s", (license_id,)) except: pass # Delete from activations if exists 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,)) conn.commit() # Log deletion with force flag log_audit('DELETE', 'license', license_id, old_values={ 'license_key': license_data['license_key'], 'customer_name': license_data['customer_name'], 'customer_email': license_data['customer_email'], 'was_active': license_data.get('is_active'), 'forced': force_delete }, additional_info=f"{'Forced deletion' if force_delete else 'Normal deletion'}") flash(f'Lizenz {license_data["license_key"]} erfolgreich gelöscht!', 'success') except Exception as e: conn.rollback() logging.error(f"Fehler beim Löschen der Lizenz: {str(e)}") flash('Fehler beim Löschen der Lizenz!', 'error') finally: cur.close() conn.close() # Preserve show_test parameter if present show_test = request.args.get('show_test', 'false') return redirect(url_for('licenses.licenses', show_test=show_test)) @license_bp.route("/create", methods=["GET", "POST"]) @login_required def create_license(): if request.method == "POST": customer_id = request.form.get("customer_id") license_key = request.form["license_key"].upper() # Immer Großbuchstaben license_type = request.form["license_type"] valid_from = request.form["valid_from"] # is_fake wird später vom Kunden geerbt # Berechne valid_until basierend auf Laufzeit duration = int(request.form.get("duration", 1)) duration_type = request.form.get("duration_type", "years") start_date = datetime.strptime(valid_from, "%Y-%m-%d") if duration_type == "days": end_date = start_date + timedelta(days=duration) elif duration_type == "months": end_date = start_date + relativedelta(months=duration) else: # years end_date = start_date + relativedelta(years=duration) # Ein Tag abziehen, da der Starttag mitgezählt wird end_date = end_date - timedelta(days=1) valid_until = end_date.strftime("%Y-%m-%d") # Validiere License Key Format if not validate_license_key(license_key): flash('Ungültiges License Key Format! Erwartet: AF-YYYYMMFT-XXXX-YYYY-ZZZZ', 'error') return redirect(url_for('licenses.create_license')) # Resource counts domain_count = int(request.form.get("domain_count", 1)) ipv4_count = int(request.form.get("ipv4_count", 1)) phone_count = int(request.form.get("phone_count", 1)) device_limit = int(request.form.get("device_limit", 3)) conn = get_connection() cur = conn.cursor() try: # Prüfe ob neuer Kunde oder bestehender if customer_id == "new": # Neuer Kunde name = request.form.get("customer_name") email = request.form.get("email") if not name: flash('Kundenname ist erforderlich!', 'error') return redirect(url_for('licenses.create_license')) # Prüfe ob E-Mail bereits existiert if email: cur.execute("SELECT id, name FROM customers WHERE LOWER(email) = LOWER(%s)", (email,)) existing = cur.fetchone() if existing: flash(f'E-Mail bereits vergeben für Kunde: {existing[1]}', 'error') return redirect(url_for('licenses.create_license')) # Neuer Kunde wird immer als Fake erstellt, da wir in der Testphase sind # TODO: Nach Testphase muss hier die Business-Logik angepasst werden is_fake = True cur.execute(""" INSERT INTO customers (name, email, is_fake, created_at) VALUES (%s, %s, %s, NOW()) RETURNING id """, (name, email, is_fake)) customer_id = cur.fetchone()[0] customer_info = {'name': name, 'email': email, 'is_fake': is_fake} # Audit-Log für neuen Kunden log_audit('CREATE', 'customer', customer_id, new_values={'name': name, 'email': email, 'is_fake': is_fake}) else: # Bestehender Kunde - hole Infos für Audit-Log cur.execute("SELECT name, email, is_fake FROM customers WHERE id = %s", (customer_id,)) customer_data = cur.fetchone() if not customer_data: flash('Kunde nicht gefunden!', 'error') return redirect(url_for('licenses.create_license')) customer_info = {'name': customer_data[0], 'email': customer_data[1]} # Lizenz erbt immer den is_fake Status vom Kunden is_fake = customer_data[2] # Lizenz hinzufügen cur.execute(""" INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active, domain_count, ipv4_count, phone_count, device_limit, is_fake) VALUES (%s, %s, %s, %s, %s, TRUE, %s, %s, %s, %s, %s) RETURNING id """, (license_key, customer_id, license_type, valid_from, valid_until, domain_count, ipv4_count, phone_count, device_limit, is_fake)) license_id = cur.fetchone()[0] # Ressourcen zuweisen try: # Prüfe Verfügbarkeit cur.execute(""" SELECT (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_fake = %s) as domains, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_fake = %s) as ipv4s, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_fake = %s) as phones """, (is_fake, is_fake, is_fake)) available = cur.fetchone() if available[0] < domain_count: raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {available[0]})") if available[1] < ipv4_count: raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {ipv4_count}, verfügbar: {available[1]})") if available[2] < phone_count: raise ValueError(f"Nicht genügend Telefonnummern verfügbar (benötigt: {phone_count}, verfügbar: {available[2]})") # Domains zuweisen if domain_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_fake = %s LIMIT %s FOR UPDATE """, (is_fake, domain_count)) for (resource_id,) in cur.fetchall(): 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)) cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, resource_id, session['username'])) 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())) # IPv4s zuweisen if ipv4_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_fake = %s LIMIT %s FOR UPDATE """, (is_fake, ipv4_count)) for (resource_id,) in cur.fetchall(): 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)) cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, resource_id, session['username'])) 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())) # Telefonnummern zuweisen if phone_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_fake = %s LIMIT %s FOR UPDATE """, (is_fake, phone_count)) for (resource_id,) in cur.fetchall(): 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)) cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, resource_id, session['username'])) 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())) except ValueError as e: conn.rollback() flash(str(e), 'error') return redirect(url_for('licenses.create_license')) conn.commit() # Audit-Log log_audit('CREATE', 'license', license_id, new_values={ 'license_key': license_key, 'customer_name': customer_info['name'], 'customer_email': customer_info['email'], 'license_type': license_type, 'valid_from': valid_from, 'valid_until': valid_until, 'device_limit': device_limit, 'is_fake': is_fake }) flash(f'Lizenz {license_key} erfolgreich erstellt!', 'success') except Exception as e: conn.rollback() logging.error(f"Fehler beim Erstellen der Lizenz: {str(e)}") flash('Fehler beim Erstellen der Lizenz!', 'error') finally: cur.close() conn.close() # Preserve show_test parameter if present redirect_url = url_for('licenses.create_license') if request.args.get('show_test') == 'true': redirect_url += "?show_test=true" return redirect(redirect_url) # Unterstützung für vorausgewählten Kunden preselected_customer_id = request.args.get('customer_id', type=int) return render_template("index.html", username=session.get('username'), preselected_customer_id=preselected_customer_id)