import os from datetime import datetime, timedelta from zoneinfo import ZoneInfo from pathlib import Path from flask import Blueprint, render_template, request, redirect, session, url_for, flash, send_file, jsonify, current_app import requests import config from config import DATABASE_CONFIG from auth.decorators import login_required from utils.audit import log_audit from utils.backup import create_backup, restore_backup from utils.network import get_client_ip from db import get_connection, get_db_connection, get_db_cursor, execute_query from utils.export import create_excel_export, prepare_audit_export_data # Create Blueprint admin_bp = Blueprint('admin', __name__) def check_service_health(): """Check health status of critical services""" services = [] # License Server Health Check license_server = { 'name': 'License Server', 'status': 'unknown', 'response_time': None, 'icon': '🔐', 'details': None } try: start_time = datetime.now() response = requests.get('http://license-server:8443/health', timeout=2) response_time = (datetime.now() - start_time).total_seconds() * 1000 if response.status_code == 200: license_server['status'] = 'healthy' license_server['response_time'] = round(response_time, 1) license_server['details'] = 'Betriebsbereit' else: license_server['status'] = 'unhealthy' license_server['details'] = f'HTTP {response.status_code}' except requests.exceptions.Timeout: license_server['status'] = 'down' license_server['details'] = 'Timeout - Server antwortet nicht' except requests.exceptions.ConnectionError: license_server['status'] = 'down' license_server['details'] = 'Verbindung fehlgeschlagen' except Exception as e: license_server['status'] = 'down' license_server['details'] = f'Fehler: {str(e)}' services.append(license_server) # PostgreSQL Health Check postgresql = { 'name': 'PostgreSQL', 'status': 'unknown', 'response_time': None, 'icon': 'đŸ—„ïž', 'details': None } try: start_time = datetime.now() with get_db_connection() as conn: cur = conn.cursor() cur.execute('SELECT 1') cur.close() response_time = (datetime.now() - start_time).total_seconds() * 1000 postgresql['status'] = 'healthy' postgresql['response_time'] = round(response_time, 1) postgresql['details'] = 'Datenbankverbindung aktiv' except Exception as e: postgresql['status'] = 'down' postgresql['details'] = f'Verbindungsfehler: {str(e)}' services.append(postgresql) # Calculate overall health healthy_count = sum(1 for s in services if s['status'] == 'healthy') total_count = len(services) return { 'services': services, 'healthy_count': healthy_count, 'total_count': total_count, 'overall_status': 'healthy' if healthy_count == total_count else ('partial' if healthy_count > 0 else 'down') } @admin_bp.route("/") @login_required def dashboard(): try: conn = get_connection() cur = conn.cursor() try: # Hole Statistiken mit sicheren Defaults # Anzahl aktiver Lizenzen (nur echte Daten, keine Testdaten) cur.execute("SELECT COUNT(*) FROM licenses WHERE is_active = true AND is_fake = false") active_licenses = cur.fetchone()[0] if cur.rowcount > 0 else 0 # Anzahl Kunden (nur echte Kunden, keine Fake-Kunden) cur.execute("SELECT COUNT(*) FROM customers WHERE is_fake = false") total_customers = cur.fetchone()[0] if cur.rowcount > 0 else 0 # Testdaten separat zĂ€hlen fĂŒr optionale Anzeige cur.execute("SELECT COUNT(*) FROM customers WHERE is_fake = true") fake_customers_count = cur.fetchone()[0] if cur.rowcount > 0 else 0 cur.execute("SELECT COUNT(*) FROM licenses WHERE is_fake = true") test_licenses_count = cur.fetchone()[0] if cur.rowcount > 0 else 0 # Anzahl aktiver Sessions (Admin-Panel) cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = true") active_sessions = cur.fetchone()[0] if cur.rowcount > 0 else 0 # Aktive Nutzung (Kunden-Software) - Lizenzen mit Heartbeats in den letzten 15 Minuten active_usage = 0 try: # PrĂŒfe ob Tabelle existiert cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'license_heartbeats' ) """) table_exists = cur.fetchone()[0] if table_exists: cur.execute(""" SELECT COUNT(DISTINCT lh.license_id) FROM license_heartbeats lh JOIN licenses l ON l.id = lh.license_id WHERE lh.timestamp > NOW() - INTERVAL '15 minutes' AND l.is_fake = false """) active_usage = cur.fetchone()[0] if cur.rowcount > 0 else 0 except Exception as e: # Bei Fehler einfach 0 verwenden current_app.logger.warning(f"Could not get active usage: {str(e)}") # Rollback der fehlgeschlagenen Transaktion conn.rollback() # Neue Transaktion starten conn = get_connection() cur = conn.cursor() # Top 10 Lizenzen - nur echte Lizenzen cur.execute(""" SELECT l.license_key, c.name as customer_name, COUNT(s.id) as session_count FROM licenses l LEFT JOIN customers c ON l.customer_id = c.id LEFT JOIN sessions s ON l.id = s.license_id WHERE l.is_fake = false AND c.is_fake = false GROUP BY l.license_key, c.name ORDER BY session_count DESC LIMIT 10 """) top_licenses = cur.fetchall() if cur.rowcount > 0 else [] # Letzte AktivitĂ€ten - vereinfacht cur.execute(""" SELECT id, timestamp, username, action, additional_info FROM audit_log ORDER BY timestamp DESC LIMIT 10 """) recent_activities = cur.fetchall() if cur.rowcount > 0 else [] # Lizenztypen zĂ€hlen (nur echte Lizenzen) cur.execute(""" SELECT COUNT(CASE WHEN license_type = 'full' THEN 1 END) as full_licenses, COUNT(CASE WHEN license_type = 'test' THEN 1 END) as test_licenses FROM licenses WHERE is_fake = false """) license_types = cur.fetchone() full_licenses = license_types[0] if license_types and license_types[0] is not None else 0 test_version_licenses = license_types[1] if license_types and license_types[1] is not None else 0 # Lizenzstatus zĂ€hlen (nur echte Lizenzen) cur.execute(""" SELECT COUNT(CASE WHEN is_active = true AND (valid_until IS NULL OR valid_until > NOW()) THEN 1 END) as active, COUNT(CASE WHEN valid_until < NOW() THEN 1 END) as expired, COUNT(CASE WHEN is_active = false THEN 1 END) as inactive FROM licenses WHERE is_fake = false """) license_status = cur.fetchone() active_licenses_count = license_status[0] if license_status and license_status[0] else 0 expired_licenses = license_status[1] if license_status and license_status[1] else 0 inactive_licenses = license_status[2] if license_status and license_status[2] else 0 # Bald ablaufende Lizenzen (nur echte Lizenzen) cur.execute(""" SELECT l.id, l.license_key, c.name as customer_name, l.valid_until, EXTRACT(DAY FROM (l.valid_until - NOW())) as days_remaining FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.is_fake = false AND c.is_fake = false AND l.is_active = true AND l.valid_until IS NOT NULL AND l.valid_until > NOW() AND l.valid_until < NOW() + INTERVAL '30 days' ORDER BY l.valid_until ASC LIMIT 10 """) expiring_licenses = cur.fetchall() if cur.rowcount > 0 else [] # Zuletzt erstellte Lizenzen (nur echte Lizenzen) cur.execute(""" SELECT l.id, l.license_key, c.name as customer_name, l.created_at, CASE WHEN l.is_active = false THEN 'deaktiviert' WHEN l.valid_until < NOW() THEN 'abgelaufen' WHEN l.valid_until < NOW() + INTERVAL '30 days' THEN 'lĂ€uft bald ab' ELSE 'aktiv' END as status FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.is_fake = false AND c.is_fake = false ORDER BY l.created_at DESC LIMIT 5 """) recent_licenses = cur.fetchall() if cur.rowcount > 0 else [] # Stats Objekt fĂŒr Template erstellen stats = { 'total_customers': total_customers, 'total_licenses': active_licenses, # This was already filtered for is_fake = false 'active_sessions': active_sessions, # Admin-Panel Sessions 'active_usage': active_usage, # Aktive Kunden-Nutzung 'active_licenses': active_licenses_count, 'full_licenses': full_licenses or 0, 'fake_licenses': test_version_licenses or 0, # Test versions (license_type='test'), not fake data 'fake_data_count': test_licenses_count, # Actual test data count (is_fake=true) 'fake_customers_count': fake_customers_count, 'fake_resources_count': 0, 'expired_licenses': expired_licenses, 'inactive_licenses': inactive_licenses, 'last_backup': None, 'security_level': 'success', 'security_level_text': 'Sicher', 'blocked_ips_count': 0, 'failed_attempts_today': 0, 'recent_security_events': [], 'expiring_licenses': expiring_licenses, 'recent_licenses': recent_licenses } # Resource Pool Statistics (nur echte Ressourcen, keine Testdaten) resource_stats = {} resource_types = ['domain', 'ipv4', 'phone'] for resource_type in resource_types: try: cur.execute(""" SELECT COUNT(CASE WHEN status = 'available' THEN 1 END) as available, COUNT(CASE WHEN status = 'allocated' THEN 1 END) as allocated, COUNT(CASE WHEN status = 'quarantine' THEN 1 END) as quarantine, COUNT(*) as total FROM resource_pools WHERE resource_type = %s AND is_fake = false """, (resource_type,)) result = cur.fetchone() if result: available = result[0] or 0 allocated = result[1] or 0 quarantine = result[2] or 0 total = result[3] or 0 available_percent = int((available / total * 100)) if total > 0 else 0 resource_stats[resource_type] = { 'available': available, 'allocated': allocated, 'quarantine': quarantine, 'total': total, 'available_percent': available_percent } else: resource_stats[resource_type] = { 'available': 0, 'allocated': 0, 'quarantine': 0, 'total': 0, 'available_percent': 0 } except Exception as e: # Falls die Tabelle nicht existiert current_app.logger.warning(f"Could not get resource stats for {resource_type}: {str(e)}") resource_stats[resource_type] = { 'available': 0, 'allocated': 0, 'quarantine': 0, 'total': 0, 'available_percent': 0 } # Reset the connection after error conn.rollback() # Count test resources separately try: cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_fake = true") fake_resources_count = cur.fetchone()[0] if cur.rowcount > 0 else 0 stats['fake_resources_count'] = fake_resources_count except: pass license_distribution = [] hourly_sessions = [] # Get service health status service_health = check_service_health() return render_template('dashboard.html', stats=stats, top_licenses=top_licenses, recent_activities=recent_activities, license_distribution=license_distribution, hourly_sessions=hourly_sessions, resource_stats=resource_stats, service_health=service_health, username=session.get('username')) finally: cur.close() conn.close() except Exception as e: current_app.logger.error(f"Dashboard error: {str(e)}") current_app.logger.error(f"Error type: {type(e).__name__}") import traceback current_app.logger.error(f"Traceback: {traceback.format_exc()}") flash(f'Dashboard-Fehler: {str(e)}', 'error') return redirect(url_for('auth.login')) @admin_bp.route("/audit") @login_required def audit_log(): page = request.args.get('page', 1, type=int) per_page = 50 search = request.args.get('search', '') filter_user = request.args.get('user', '') action_filter = request.args.get('action', '') entity_filter = request.args.get('entity', '') conn = get_connection() cur = conn.cursor() try: # Base query query = """ SELECT id, timestamp AT TIME ZONE 'Europe/Berlin' as timestamp, username, action, entity_type, entity_id, old_values::text, new_values::text, ip_address, user_agent, additional_info FROM audit_log WHERE 1=1 """ params = [] # Suchfilter if search: query += """ AND ( username ILIKE %s OR action ILIKE %s OR entity_type ILIKE %s OR additional_info ILIKE %s OR ip_address ILIKE %s )""" search_param = f"%{search}%" params.extend([search_param] * 5) # Action Filter if action_filter: query += " AND action = %s" params.append(action_filter) # Entity Filter if entity_filter: query += " AND entity_type = %s" params.append(entity_filter) # User Filter if filter_user: query += " AND username = %s" params.append(filter_user) # Count total count_query = f"SELECT COUNT(*) FROM ({query}) as filtered" cur.execute(count_query, params) total_count = cur.fetchone()[0] # Add pagination query += " ORDER BY timestamp DESC LIMIT %s OFFSET %s" params.extend([per_page, (page - 1) * per_page]) cur.execute(query, params) logs = cur.fetchall() # Get unique actions and entities for filters cur.execute("SELECT DISTINCT action FROM audit_log ORDER BY action") actions = [row[0] for row in cur.fetchall()] cur.execute("SELECT DISTINCT entity_type FROM audit_log ORDER BY entity_type") entities = [row[0] for row in cur.fetchall()] # Pagination info total_pages = (total_count + per_page - 1) // per_page # Convert to dictionaries for easier template access audit_logs = [] for log in logs: # Parse JSON strings for old_values and new_values old_values = None new_values = None try: if log[6]: import json old_values = json.loads(log[6]) except: old_values = log[6] try: if log[7]: import json new_values = json.loads(log[7]) except: new_values = log[7] audit_logs.append({ 'id': log[0], 'timestamp': log[1], 'username': log[2], 'action': log[3], 'entity_type': log[4], 'entity_id': log[5], 'old_values': old_values, 'new_values': new_values, 'ip_address': log[8], 'user_agent': log[9], 'additional_info': log[10] }) return render_template('audit_log.html', logs=audit_logs, page=page, total_pages=total_pages, total=total_count, search=search, filter_user=filter_user, action_filter=action_filter, entity_filter=entity_filter, actions=actions, entities=entities, username=session.get('username')) finally: cur.close() conn.close() @admin_bp.route("/backups") @login_required def backups(): conn = get_connection() cur = conn.cursor() try: # Hole alle Backups cur.execute(""" SELECT id, filename, created_at AT TIME ZONE 'Europe/Berlin' as created_at, filesize, backup_type, status, created_by, duration_seconds, tables_count, records_count, error_message, is_encrypted FROM backup_history ORDER BY created_at DESC """) backups = cur.fetchall() # PrĂŒfe ob Dateien noch existieren backups_with_status = [] for backup in backups: backup_dict = { 'id': backup[0], 'filename': backup[1], 'created_at': backup[2], 'filesize': backup[3], 'backup_type': backup[4], 'status': backup[5], 'created_by': backup[6], 'duration_seconds': backup[7], 'tables_count': backup[8], 'records_count': backup[9], 'error_message': backup[10], 'is_encrypted': backup[11], 'file_exists': False } # PrĂŒfe ob Datei existiert if backup[1]: # filename filepath = config.BACKUP_DIR / backup[1] backup_dict['file_exists'] = filepath.exists() backups_with_status.append(backup_dict) return render_template('backups.html', backups=backups_with_status, username=session.get('username')) finally: cur.close() conn.close() @admin_bp.route("/backup/create", methods=["POST"]) @login_required def create_backup_route(): """Manuelles Backup erstellen""" from flask import jsonify success, result = create_backup(backup_type="manual", created_by=session.get('username')) if success: return jsonify({ 'success': True, 'message': f'Backup erfolgreich erstellt: {result}' }) else: return jsonify({ 'success': False, 'message': f'Backup fehlgeschlagen: {result}' }), 500 @admin_bp.route("/backup/restore/", methods=["POST"]) @login_required def restore_backup_route(backup_id): """Backup wiederherstellen""" from flask import jsonify encryption_key = request.form.get('encryption_key') success, message = restore_backup(backup_id, encryption_key) if success: return jsonify({ 'success': True, 'message': message }) else: return jsonify({ 'success': False, 'message': f'Wiederherstellung fehlgeschlagen: {message}' }), 500 @admin_bp.route("/backup/download/") @login_required def download_backup(backup_id): """Backup herunterladen""" conn = get_connection() cur = conn.cursor() try: # Hole Backup-Info cur.execute("SELECT filename, filepath FROM backup_history WHERE id = %s", (backup_id,)) result = cur.fetchone() if not result: flash('Backup nicht gefunden', 'error') return redirect(url_for('admin.backups')) filename, filepath = result filepath = Path(filepath) if not filepath.exists(): flash('Backup-Datei nicht gefunden', 'error') return redirect(url_for('admin.backups')) # Audit-Log log_audit('BACKUP_DOWNLOAD', 'backup', backup_id, additional_info=f"Backup heruntergeladen: {filename}") return send_file(filepath, as_attachment=True, download_name=filename) finally: cur.close() conn.close() @admin_bp.route("/backup/delete/", methods=["DELETE"]) @login_required def delete_backup(backup_id): """Backup löschen""" conn = get_connection() cur = conn.cursor() try: # Hole Backup-Info cur.execute("SELECT filename, filepath FROM backup_history WHERE id = %s", (backup_id,)) result = cur.fetchone() if not result: return jsonify({'success': False, 'message': 'Backup nicht gefunden'}), 404 filename, filepath = result filepath = Path(filepath) # Lösche Datei wenn vorhanden if filepath.exists(): try: filepath.unlink() except Exception as e: return jsonify({'success': False, 'message': f'Fehler beim Löschen der Datei: {str(e)}'}), 500 # Lösche Datenbank-Eintrag cur.execute("DELETE FROM backup_history WHERE id = %s", (backup_id,)) conn.commit() # Audit-Log log_audit('BACKUP_DELETE', 'backup', backup_id, additional_info=f"Backup gelöscht: {filename}") return jsonify({'success': True, 'message': 'Backup erfolgreich gelöscht'}) except Exception as e: conn.rollback() return jsonify({'success': False, 'message': str(e)}), 500 finally: cur.close() conn.close() @admin_bp.route("/security/blocked-ips") @login_required def blocked_ips(): """Zeigt gesperrte IP-Adressen""" conn = get_connection() cur = conn.cursor() try: cur.execute(""" SELECT ip_address, attempt_count, last_attempt AT TIME ZONE 'Europe/Berlin' as last_attempt, blocked_until AT TIME ZONE 'Europe/Berlin' as blocked_until, last_username_tried, last_error_message FROM login_attempts WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP ORDER BY blocked_until DESC """) blocked = cur.fetchall() # Alle Login-Versuche (auch nicht gesperrte) cur.execute(""" SELECT ip_address, attempt_count, last_attempt AT TIME ZONE 'Europe/Berlin' as last_attempt, blocked_until AT TIME ZONE 'Europe/Berlin' as blocked_until, last_username_tried, last_error_message FROM login_attempts ORDER BY last_attempt DESC LIMIT 100 """) all_attempts = cur.fetchall() return render_template('blocked_ips.html', blocked_ips=blocked, all_attempts=all_attempts, username=session.get('username')) finally: cur.close() conn.close() @admin_bp.route("/security/unblock-ip", methods=["POST"]) @login_required def unblock_ip(): """Entsperrt eine IP-Adresse""" ip_address = request.form.get('ip_address') if not ip_address: flash('Keine IP-Adresse angegeben', 'error') return redirect(url_for('admin.blocked_ips')) conn = get_connection() cur = conn.cursor() try: cur.execute(""" UPDATE login_attempts SET blocked_until = NULL WHERE ip_address = %s """, (ip_address,)) if cur.rowcount > 0: conn.commit() flash(f'IP-Adresse {ip_address} wurde entsperrt', 'success') log_audit('UNBLOCK_IP', 'security', additional_info=f"IP-Adresse entsperrt: {ip_address}") else: flash(f'IP-Adresse {ip_address} nicht gefunden', 'warning') except Exception as e: conn.rollback() flash(f'Fehler beim Entsperren: {str(e)}', 'error') finally: cur.close() conn.close() return redirect(url_for('admin.blocked_ips')) @admin_bp.route("/security/clear-attempts", methods=["POST"]) @login_required def clear_attempts(): """Löscht alle Login-Versuche""" conn = get_connection() cur = conn.cursor() try: cur.execute("DELETE FROM login_attempts") count = cur.rowcount conn.commit() flash(f'{count} Login-Versuche wurden gelöscht', 'success') log_audit('CLEAR_LOGIN_ATTEMPTS', 'security', additional_info=f"{count} Login-Versuche gelöscht") except Exception as e: conn.rollback() flash(f'Fehler beim Löschen: {str(e)}', 'error') finally: cur.close() conn.close() return redirect(url_for('admin.blocked_ips')) # ===================== LICENSE SERVER MONITORING ROUTES ===================== @admin_bp.route("/lizenzserver/monitor") @login_required def license_monitor(): """Redirect to new analytics page""" return redirect(url_for('monitoring.analytics')) @admin_bp.route("/lizenzserver/analytics") @login_required def license_analytics(): """License usage analytics""" try: conn = get_connection() cur = conn.cursor() # Time range from query params days = int(request.args.get('days', 30)) # Usage trends over time cur.execute(""" SELECT DATE(timestamp) as date, COUNT(DISTINCT license_id) as unique_licenses, COUNT(DISTINCT hardware_id) as unique_devices, COUNT(*) as total_validations FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '%s days' GROUP BY date ORDER BY date """, (days,)) usage_trends = cur.fetchall() # License performance metrics cur.execute(""" SELECT l.id, l.license_key, c.name as customer_name, COUNT(DISTINCT lh.hardware_id) as device_count, l.max_devices, COUNT(*) as total_validations, COUNT(DISTINCT DATE(lh.timestamp)) as active_days, MIN(lh.timestamp) as first_seen, MAX(lh.timestamp) as last_seen FROM licenses l JOIN customers c ON l.customer_id = c.id LEFT JOIN license_heartbeats lh ON l.id = lh.license_id WHERE lh.timestamp > NOW() - INTERVAL '%s days' GROUP BY l.id, l.license_key, c.name, l.max_devices ORDER BY total_validations DESC """, (days,)) license_metrics = cur.fetchall() # Device distribution cur.execute(""" SELECT l.max_devices as limit, COUNT(*) as license_count, AVG(device_count) as avg_usage FROM licenses l LEFT JOIN ( SELECT license_id, COUNT(DISTINCT hardware_id) as device_count FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '30 days' GROUP BY license_id ) usage ON l.id = usage.license_id WHERE l.is_active = true GROUP BY l.max_devices ORDER BY l.max_devices """) device_distribution = cur.fetchall() # Revenue analysis cur.execute(""" SELECT l.license_type, COUNT(DISTINCT l.id) as license_count, COUNT(DISTINCT CASE WHEN lh.license_id IS NOT NULL THEN l.id END) as active_licenses, COUNT(DISTINCT lh.hardware_id) as total_devices FROM licenses l LEFT JOIN license_heartbeats lh ON l.id = lh.license_id AND lh.timestamp > NOW() - INTERVAL '%s days' GROUP BY l.license_type """, (days,)) revenue_analysis = cur.fetchall() return render_template('license_analytics.html', days=days, usage_trends=usage_trends, license_metrics=license_metrics, device_distribution=device_distribution, revenue_analysis=revenue_analysis ) except Exception as e: flash(f'Fehler beim Laden der Analytics-Daten: {str(e)}', 'error') return render_template('license_analytics.html', days=30) finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/lizenzserver/anomalies") @login_required def license_anomalies(): """Redirect to unified monitoring page""" return redirect(url_for('monitoring.unified_monitoring')) @admin_bp.route("/lizenzserver/anomaly//resolve", methods=["POST"]) @login_required def resolve_anomaly(anomaly_id): """Resolve an anomaly""" try: conn = get_connection() cur = conn.cursor() action_taken = request.form.get('action_taken', '') cur.execute(""" UPDATE anomaly_detections SET resolved = true, resolved_at = NOW(), resolved_by = %s, action_taken = %s WHERE id = %s """, (session.get('username'), action_taken, str(anomaly_id))) conn.commit() flash('Anomalie wurde als behoben markiert', 'success') log_audit('RESOLVE_ANOMALY', 'license_server', entity_id=str(anomaly_id), additional_info=f"Action: {action_taken}") except Exception as e: if 'conn' in locals(): conn.rollback() flash(f'Fehler beim Beheben der Anomalie: {str(e)}', 'error') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() return redirect(url_for('admin.license_anomalies')) @admin_bp.route("/lizenzserver/config") @login_required def license_config(): """License server configuration""" try: conn = get_connection() cur = conn.cursor() # Get client configuration cur.execute(""" SELECT id, client_name, heartbeat_interval, session_timeout, current_version, minimum_version, created_at, updated_at FROM client_configs WHERE client_name = 'Account Forger' """) client_config = cur.fetchone() # Get active sessions - table doesn't exist, use empty list active_sessions = [] # Get feature flags - table doesn't exist, use empty list feature_flags = [] # Get rate limits - table doesn't exist, use empty list rate_limits = [] # Get system API key cur.execute(""" SELECT api_key, created_at, regenerated_at, last_used_at, usage_count, created_by, regenerated_by FROM system_api_key WHERE id = 1 """) api_key_data = cur.fetchone() if api_key_data: system_api_key = { 'api_key': api_key_data[0], 'created_at': api_key_data[1], 'regenerated_at': api_key_data[2], 'last_used_at': api_key_data[3], 'usage_count': api_key_data[4], 'created_by': api_key_data[5], 'regenerated_by': api_key_data[6] } else: system_api_key = None return render_template('license_config.html', client_config=client_config, active_sessions=active_sessions, feature_flags=feature_flags, rate_limits=rate_limits, system_api_key=system_api_key ) except Exception as e: import traceback current_app.logger.error(f"Error in license_config: {str(e)}") current_app.logger.error(traceback.format_exc()) flash(f'Fehler beim Laden der Konfiguration: {str(e)}', 'error') return render_template('license_config.html') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/lizenzserver/config/feature-flag/", methods=["POST"]) @login_required def update_feature_flag(flag_id): """Update feature flag settings""" try: conn = get_connection() cur = conn.cursor() is_enabled = request.form.get('is_enabled') == 'on' rollout_percentage = int(request.form.get('rollout_percentage', 0)) cur.execute(""" UPDATE feature_flags SET is_enabled = %s, rollout_percentage = %s, updated_at = NOW() WHERE id = %s """, (is_enabled, rollout_percentage, flag_id)) conn.commit() flash('Feature Flag wurde aktualisiert', 'success') log_audit('UPDATE_FEATURE_FLAG', 'license_server', entity_id=flag_id) except Exception as e: if 'conn' in locals(): conn.rollback() flash(f'Fehler beim Aktualisieren: {str(e)}', 'error') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() return redirect(url_for('admin.license_config')) @admin_bp.route("/lizenzserver/config/update", methods=["POST"]) @login_required def update_client_config(): """Update client configuration""" if session.get('username') not in ['rac00n', 'w@rh@mm3r']: flash('Zugriff verweigert', 'error') return redirect(url_for('admin.dashboard')) try: conn = get_connection() cur = conn.cursor() # Update configuration cur.execute(""" UPDATE client_configs SET current_version = %s, minimum_version = %s, heartbeat_interval = %s, session_timeout = %s, updated_at = CURRENT_TIMESTAMP WHERE client_name = 'Account Forger' """, ( request.form.get('current_version'), request.form.get('minimum_version'), 30, # heartbeat_interval - fixed 60 # session_timeout - fixed )) conn.commit() flash('Client-Konfiguration wurde aktualisiert', 'success') # Log action log_action( username=session.get('username'), action='UPDATE', entity_type='client_config', entity_id=1, new_values={ 'current_version': request.form.get('current_version'), 'minimum_version': request.form.get('minimum_version') } ) except Exception as e: flash(f'Fehler beim Aktualisieren: {str(e)}', 'error') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() return redirect(url_for('admin.license_config')) @admin_bp.route("/lizenzserver/sessions") @login_required def license_sessions(): """Show active license sessions""" try: conn = get_connection() cur = conn.cursor() # Get active sessions cur.execute(""" SELECT ls.id, ls.session_token, l.license_key, c.name as customer_name, ls.hardware_id, ls.ip_address, ls.client_version, ls.started_at AT TIME ZONE 'Europe/Berlin' as started_at, ls.last_heartbeat AT TIME ZONE 'Europe/Berlin' as last_heartbeat, EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - ls.last_heartbeat)) as seconds_since_heartbeat FROM license_sessions ls JOIN licenses l ON ls.license_id = l.id LEFT JOIN customers c ON l.customer_id = c.id ORDER BY ls.last_heartbeat DESC """) sessions = cur.fetchall() # Get session history (last 24h) cur.execute(""" SELECT sh.id, l.license_key, c.name as customer_name, sh.hardware_id, sh.ip_address, sh.client_version, sh.started_at AT TIME ZONE 'Europe/Berlin' as started_at, sh.ended_at AT TIME ZONE 'Europe/Berlin' as ended_at, sh.end_reason, EXTRACT(EPOCH FROM (sh.ended_at - sh.started_at)) as duration_seconds FROM session_history sh JOIN licenses l ON sh.license_id = l.id LEFT JOIN customers c ON l.customer_id = c.id WHERE sh.ended_at > CURRENT_TIMESTAMP - INTERVAL '24 hours' ORDER BY sh.ended_at DESC LIMIT 100 """) history = cur.fetchall() return render_template('license_sessions.html', active_sessions=sessions, session_history=history) except Exception as e: flash(f'Fehler beim Laden der Sessions: {str(e)}', 'error') return render_template('license_sessions.html') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/lizenzserver/sessions//terminate", methods=["POST"]) @login_required def terminate_session(session_id): """Force terminate a session""" if session.get('username') not in ['rac00n', 'w@rh@mm3r']: flash('Zugriff verweigert', 'error') return redirect(url_for('admin.license_sessions')) try: conn = get_connection() cur = conn.cursor() # Get session info cur.execute(""" SELECT license_id, hardware_id, ip_address, client_version, started_at FROM license_sessions WHERE id = %s """, (session_id,)) session_info = cur.fetchone() if session_info: # Log to history cur.execute(""" INSERT INTO session_history (license_id, hardware_id, ip_address, client_version, started_at, ended_at, end_reason) VALUES (%s, %s, %s, %s, %s, CURRENT_TIMESTAMP, 'forced') """, session_info) # Delete session cur.execute("DELETE FROM license_sessions WHERE id = %s", (session_id,)) conn.commit() flash('Session wurde beendet', 'success') # Log action log_action( username=session.get('username'), action='TERMINATE_SESSION', entity_type='license_session', entity_id=session_id, additional_info={'hardware_id': session_info[1]} ) else: flash('Session nicht gefunden', 'error') except Exception as e: flash(f'Fehler beim Beenden der Session: {str(e)}', 'error') finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() return redirect(url_for('admin.license_sessions')) @admin_bp.route("/api/admin/lizenzserver/live-stats") @login_required def license_live_stats(): """API endpoint for live statistics (for AJAX updates)""" try: conn = get_connection() cur = conn.cursor() # Get real-time stats cur.execute(""" SELECT COUNT(DISTINCT license_id) as active_licenses, COUNT(*) as validations_per_minute, COUNT(DISTINCT hardware_id) as active_devices FROM license_heartbeats WHERE timestamp > NOW() - INTERVAL '1 minute' """) stats = cur.fetchone() # Get active sessions count cur.execute(""" SELECT COUNT(*) FROM license_sessions """) active_count = cur.fetchone()[0] # Get latest sessions cur.execute(""" SELECT ls.id, l.license_key, c.name as customer_name, ls.client_version, ls.last_heartbeat AT TIME ZONE 'Europe/Berlin' as last_heartbeat, EXTRACT(EPOCH FROM (CURRENT_TIMESTAMP - ls.last_heartbeat)) as seconds_since FROM license_sessions ls JOIN licenses l ON ls.license_id = l.id LEFT JOIN customers c ON l.customer_id = c.id ORDER BY ls.last_heartbeat DESC LIMIT 5 """) latest_sessions = cur.fetchall() return jsonify({ 'active_licenses': active_count, 'validations_per_minute': stats[1] or 0, 'active_devices': stats[2] or 0, 'latest_sessions': [ { 'customer_name': s[2], 'version': s[3], 'last_heartbeat': s[4].strftime('%H:%M:%S'), 'seconds_since': int(s[5]) } for s in latest_sessions ] }) except Exception as e: return jsonify({'error': str(e)}), 500 finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/api/admin/license/auth-token") @login_required def get_analytics_token(): """Get JWT token for accessing Analytics Service""" import jwt from datetime import datetime, timedelta # Generate a short-lived token for the analytics service payload = { 'sub': session.get('user_id', 'admin'), 'type': 'analytics_access', 'exp': datetime.utcnow() + timedelta(hours=1), 'iat': datetime.utcnow() } # Use the same secret as configured in the analytics service jwt_secret = os.environ.get('JWT_SECRET', 'your-secret-key') token = jwt.encode(payload, jwt_secret, algorithm='HS256') return jsonify({'token': token}) # ===================== API KEY MANAGEMENT ===================== @admin_bp.route("/api-key/regenerate", methods=["POST"]) @login_required def regenerate_api_key(): """Regenerate the system API key""" import string import random conn = get_connection() cur = conn.cursor() try: # Generate new API key year_part = datetime.now().strftime('%Y') random_part = ''.join(random.choices(string.ascii_uppercase + string.digits, k=32)) new_api_key = f"AF-{year_part}-{random_part}" # Update the API key cur.execute(""" UPDATE system_api_key SET api_key = %s, regenerated_at = CURRENT_TIMESTAMP, regenerated_by = %s WHERE id = 1 """, (new_api_key, session.get('username'))) conn.commit() flash('API Key wurde erfolgreich regeneriert', 'success') # Log action log_audit('API_KEY_REGENERATED', 'system_api_key', 1, additional_info="API Key regenerated") except Exception as e: conn.rollback() flash(f'Fehler beim Regenerieren des API Keys: {str(e)}', 'error') finally: cur.close() conn.close() return redirect(url_for('admin.license_config')) @admin_bp.route("/test-api-key") @login_required def test_api_key(): """Test route to check API key in database""" try: conn = get_connection() cur = conn.cursor() # Test if table exists cur.execute(""" SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name = 'system_api_key' ); """) table_exists = cur.fetchone()[0] # Get API key if table exists api_key = None if table_exists: cur.execute("SELECT api_key FROM system_api_key WHERE id = 1;") result = cur.fetchone() if result: api_key = result[0] return jsonify({ 'table_exists': table_exists, 'api_key': api_key, 'database': DATABASE_CONFIG['dbname'] }) except Exception as e: return jsonify({ 'error': str(e), 'database': DATABASE_CONFIG.get('dbname', 'unknown') }) finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/test-license-types") @login_required def test_license_types(): """Test route to check license type counts""" try: conn = get_connection() cur = conn.cursor() # Count license types cur.execute(""" SELECT COUNT(CASE WHEN license_type = 'full' THEN 1 END) as full_licenses, COUNT(CASE WHEN license_type = 'test' THEN 1 END) as test_licenses, COUNT(*) as total_licenses FROM licenses WHERE is_fake = false """) result = cur.fetchone() # Count all licenses by type cur.execute(""" SELECT license_type, COUNT(*) as count FROM licenses GROUP BY license_type ORDER BY license_type """) all_types = cur.fetchall() return jsonify({ 'full_licenses': result[0] if result and result[0] is not None else 0, 'test_licenses': result[1] if result and result[1] is not None else 0, 'total_non_fake': result[2] if result and result[2] is not None else 0, 'all_license_types': [{'type': row[0], 'count': row[1]} for row in all_types] if all_types else [] }) except Exception as e: return jsonify({ 'error': str(e) }) finally: if 'cur' in locals(): cur.close() if 'conn' in locals(): conn.close() @admin_bp.route("/admin/licenses/check-expiration", methods=["POST"]) @login_required def check_license_expiration(): """Manually trigger license expiration check""" if session.get('username') not in ['rac00n', 'w@rh@mm3r']: return jsonify({'error': 'Zugriff verweigert'}), 403 try: from scheduler import deactivate_expired_licenses deactivate_expired_licenses() flash('License expiration check completed successfully', 'success') log_audit('MANUAL_LICENSE_EXPIRATION_CHECK', 'system', additional_info="Manual license expiration check triggered") return jsonify({'success': True, 'message': 'License expiration check completed'}) except Exception as e: current_app.logger.error(f"Error in manual license expiration check: {str(e)}") return jsonify({'error': str(e)}), 500