Files
v2-Docker/v2_adminpanel/routes/admin_routes.py
Claude Project Manager 0d7d888502 Initial commit
2025-07-05 17:51:16 +02:00

1442 Zeilen
51 KiB
Python

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/<int:backup_id>", 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/<int:backup_id>")
@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/<int:backup_id>", 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/<anomaly_id>/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/<int:flag_id>", 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/<int:session_id>/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