1442 Zeilen
51 KiB
Python
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
|