Dieser Commit ist enthalten in:
2025-06-28 18:32:30 +02:00
Ursprung f1050398cf
Commit b28b60e47b
1596 geänderte Dateien mit 0 neuen und 122678 gelöschten Zeilen

Datei anzeigen

@@ -1,2 +0,0 @@
# Routes module initialization
# This module contains all Flask blueprints organized by functionality

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei-Diff unterdrückt, da er zu groß ist Diff laden

Datei anzeigen

@@ -1,377 +0,0 @@
import time
import json
from datetime import datetime
from zoneinfo import ZoneInfo
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
import config
from auth.decorators import login_required
from auth.password import hash_password, verify_password
from auth.two_factor import (
generate_totp_secret, generate_qr_code, verify_totp,
generate_backup_codes, hash_backup_code, verify_backup_code
)
from auth.rate_limiting import (
check_ip_blocked, record_failed_attempt,
reset_login_attempts, get_login_attempts
)
from utils.network import get_client_ip
from utils.audit import log_audit
from models import get_user_by_username
from db import get_db_connection, get_db_cursor
from utils.recaptcha import verify_recaptcha
# Create Blueprint
auth_bp = Blueprint('auth', __name__)
@auth_bp.route("/login", methods=["GET", "POST"])
def login():
# Timing-Attack Schutz - Start Zeit merken
start_time = time.time()
# IP-Adresse ermitteln
ip_address = get_client_ip()
# Prüfen ob IP gesperrt ist
is_blocked, blocked_until = check_ip_blocked(ip_address)
if is_blocked:
time_remaining = (blocked_until - datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None)).total_seconds() / 3600
error_msg = f"IP GESPERRT! Noch {time_remaining:.1f} Stunden warten."
return render_template("login.html", error=error_msg, error_type="blocked")
# Anzahl bisheriger Versuche
attempt_count = get_login_attempts(ip_address)
if request.method == "POST":
username = request.form.get("username")
password = request.form.get("password")
captcha_response = request.form.get("g-recaptcha-response")
# CAPTCHA-Prüfung nur wenn Keys konfiguriert sind
recaptcha_site_key = config.RECAPTCHA_SITE_KEY
if attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and recaptcha_site_key:
if not captcha_response:
# Timing-Attack Schutz
elapsed = time.time() - start_time
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
return render_template("login.html",
error="CAPTCHA ERFORDERLICH!",
show_captcha=True,
error_type="captcha",
attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
recaptcha_site_key=recaptcha_site_key)
# CAPTCHA validieren
if not verify_recaptcha(captcha_response):
# Timing-Attack Schutz
elapsed = time.time() - start_time
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
return render_template("login.html",
error="CAPTCHA UNGÜLTIG! Bitte erneut versuchen.",
show_captcha=True,
error_type="captcha",
attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
recaptcha_site_key=recaptcha_site_key)
# Check user in database first, fallback to env vars
user = get_user_by_username(username)
login_success = False
needs_2fa = False
if user:
# Database user authentication
if verify_password(password, user['password_hash']):
login_success = True
needs_2fa = user['totp_enabled']
else:
# Fallback to environment variables for backward compatibility
if username in config.ADMIN_USERS and password == config.ADMIN_USERS[username]:
login_success = True
# Timing-Attack Schutz - Mindestens 1 Sekunde warten
elapsed = time.time() - start_time
if elapsed < 1.0:
time.sleep(1.0 - elapsed)
if login_success:
# Erfolgreicher Login
if needs_2fa:
# Store temporary session for 2FA verification
session['temp_username'] = username
session['temp_user_id'] = user['id']
session['awaiting_2fa'] = True
return redirect(url_for('auth.verify_2fa'))
else:
# Complete login without 2FA
session.permanent = True # Aktiviert das Timeout
session['logged_in'] = True
session['username'] = username
session['user_id'] = user['id'] if user else None
session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
reset_login_attempts(ip_address)
log_audit('LOGIN_SUCCESS', 'user',
additional_info=f"Erfolgreiche Anmeldung von IP: {ip_address}")
return redirect(url_for('admin.dashboard'))
else:
# Fehlgeschlagener Login
error_message = record_failed_attempt(ip_address, username)
new_attempt_count = get_login_attempts(ip_address)
# Prüfen ob jetzt gesperrt
is_now_blocked, _ = check_ip_blocked(ip_address)
if is_now_blocked:
log_audit('LOGIN_BLOCKED', 'security',
additional_info=f"IP {ip_address} wurde nach {config.MAX_LOGIN_ATTEMPTS} Versuchen gesperrt")
return render_template("login.html",
error=error_message,
show_captcha=(new_attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
error_type="failed",
attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - new_attempt_count),
recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
# GET Request
return render_template("login.html",
show_captcha=(attempt_count >= config.CAPTCHA_AFTER_ATTEMPTS and config.RECAPTCHA_SITE_KEY),
attempts_left=max(0, config.MAX_LOGIN_ATTEMPTS - attempt_count),
recaptcha_site_key=config.RECAPTCHA_SITE_KEY)
@auth_bp.route("/logout")
def logout():
username = session.get('username', 'unknown')
log_audit('LOGOUT', 'user', additional_info=f"Abmeldung")
session.pop('logged_in', None)
session.pop('username', None)
session.pop('user_id', None)
session.pop('temp_username', None)
session.pop('temp_user_id', None)
session.pop('awaiting_2fa', None)
return redirect(url_for('auth.login'))
@auth_bp.route("/verify-2fa", methods=["GET", "POST"])
def verify_2fa():
if not session.get('awaiting_2fa'):
return redirect(url_for('auth.login'))
if request.method == "POST":
token = request.form.get('token', '').replace(' ', '')
username = session.get('temp_username')
user_id = session.get('temp_user_id')
if not username or not user_id:
flash('Session expired. Please login again.', 'error')
return redirect(url_for('auth.login'))
user = get_user_by_username(username)
if not user:
flash('User not found.', 'error')
return redirect(url_for('auth.login'))
# Check if it's a backup code
if len(token) == 8 and token.isupper():
# Try backup code
backup_codes = json.loads(user['backup_codes']) if user['backup_codes'] else []
if verify_backup_code(token, backup_codes):
# Remove used backup code
code_hash = hash_backup_code(token)
backup_codes.remove(code_hash)
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s",
(json.dumps(backup_codes), user_id))
# Complete login
session.permanent = True
session['logged_in'] = True
session['username'] = username
session['user_id'] = user_id
session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
session.pop('temp_username', None)
session.pop('temp_user_id', None)
session.pop('awaiting_2fa', None)
flash('Login successful using backup code. Please generate new backup codes.', 'warning')
log_audit('LOGIN_2FA_BACKUP', 'user', additional_info=f"2FA login with backup code")
return redirect(url_for('admin.dashboard'))
else:
# Try TOTP token
if verify_totp(user['totp_secret'], token):
# Complete login
session.permanent = True
session['logged_in'] = True
session['username'] = username
session['user_id'] = user_id
session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
session.pop('temp_username', None)
session.pop('temp_user_id', None)
session.pop('awaiting_2fa', None)
log_audit('LOGIN_2FA_SUCCESS', 'user', additional_info=f"2FA login successful")
return redirect(url_for('admin.dashboard'))
# Failed verification
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s",
(datetime.now(), user_id))
flash('Invalid authentication code. Please try again.', 'error')
log_audit('LOGIN_2FA_FAILED', 'user', additional_info=f"Failed 2FA attempt")
return render_template('verify_2fa.html')
@auth_bp.route("/profile")
@login_required
def profile():
user = get_user_by_username(session['username'])
if not user:
# For environment-based users, redirect with message
flash('Bitte führen Sie das Migrations-Script aus, um Passwort-Änderung und 2FA zu aktivieren.', 'info')
return redirect(url_for('admin.dashboard'))
return render_template('profile.html', user=user)
@auth_bp.route("/profile/change-password", methods=["POST"])
@login_required
def change_password():
current_password = request.form.get('current_password')
new_password = request.form.get('new_password')
confirm_password = request.form.get('confirm_password')
user = get_user_by_username(session['username'])
# Verify current password
if not verify_password(current_password, user['password_hash']):
flash('Current password is incorrect.', 'error')
return redirect(url_for('auth.profile'))
# Check new password
if new_password != confirm_password:
flash('New passwords do not match.', 'error')
return redirect(url_for('auth.profile'))
if len(new_password) < 8:
flash('Password must be at least 8 characters long.', 'error')
return redirect(url_for('auth.profile'))
# Update password
new_hash = hash_password(new_password)
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s",
(new_hash, datetime.now(), user['id']))
log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'],
additional_info="Password changed successfully")
flash('Password changed successfully.', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route("/profile/setup-2fa")
@login_required
def setup_2fa():
user = get_user_by_username(session['username'])
if user['totp_enabled']:
flash('2FA is already enabled for your account.', 'info')
return redirect(url_for('auth.profile'))
# Generate new TOTP secret
totp_secret = generate_totp_secret()
session['temp_totp_secret'] = totp_secret
# Generate QR code
qr_code = generate_qr_code(user['username'], totp_secret)
return render_template('setup_2fa.html',
totp_secret=totp_secret,
qr_code=qr_code)
@auth_bp.route("/profile/enable-2fa", methods=["POST"])
@login_required
def enable_2fa():
token = request.form.get('token', '').replace(' ', '')
totp_secret = session.get('temp_totp_secret')
if not totp_secret:
flash('2FA setup session expired. Please try again.', 'error')
return redirect(url_for('auth.setup_2fa'))
# Verify the token
if not verify_totp(totp_secret, token):
flash('Invalid authentication code. Please try again.', 'error')
return redirect(url_for('auth.setup_2fa'))
# Generate backup codes
backup_codes = generate_backup_codes()
backup_codes_hashed = [hash_backup_code(code) for code in backup_codes]
# Enable 2FA for user
user = get_user_by_username(session['username'])
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
cur.execute("""
UPDATE users
SET totp_secret = %s, totp_enabled = true, backup_codes = %s
WHERE id = %s
""", (totp_secret, json.dumps(backup_codes_hashed), user['id']))
# Clear temp secret
session.pop('temp_totp_secret', None)
log_audit('2FA_ENABLED', 'user', entity_id=user['id'],
additional_info="2FA successfully enabled")
# Show backup codes
return render_template('backup_codes.html', backup_codes=backup_codes)
@auth_bp.route("/profile/disable-2fa", methods=["POST"])
@login_required
def disable_2fa():
password = request.form.get('password')
user = get_user_by_username(session['username'])
# Verify password
if not verify_password(password, user['password_hash']):
flash('Incorrect password. 2FA was not disabled.', 'error')
return redirect(url_for('auth.profile'))
# Disable 2FA
with get_db_connection() as conn:
with get_db_cursor(conn) as cur:
cur.execute("""
UPDATE users
SET totp_enabled = false, totp_secret = NULL, backup_codes = NULL
WHERE id = %s
""", (user['id'],))
log_audit('2FA_DISABLED', 'user', entity_id=user['id'],
additional_info="2FA disabled by user")
flash('2FA has been disabled for your account.', 'success')
return redirect(url_for('auth.profile'))
@auth_bp.route("/heartbeat", methods=['POST'])
@login_required
def heartbeat():
"""Endpoint für Session Keep-Alive - aktualisiert last_activity"""
# Aktualisiere last_activity nur wenn explizit angefordert
session['last_activity'] = datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat()
# Force session save
session.modified = True
return jsonify({
'status': 'ok',
'last_activity': session['last_activity'],
'username': session.get('username')
})

Datei anzeigen

@@ -1,439 +0,0 @@
import os
import logging
import secrets
import string
from datetime import datetime, timedelta
from pathlib import Path
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, send_file
import config
from auth.decorators import login_required
from utils.audit import log_audit
from utils.network import get_client_ip
from utils.export import create_batch_export
from db import get_connection, get_db_connection, get_db_cursor
from models import get_customers
# Create Blueprint
batch_bp = Blueprint('batch', __name__)
def generate_license_key():
"""Generiert einen zufälligen Lizenzschlüssel"""
chars = string.ascii_uppercase + string.digits
return '-'.join([''.join(secrets.choice(chars) for _ in range(4)) for _ in range(4)])
@batch_bp.route("/batch", methods=["GET", "POST"])
@login_required
def batch_create():
"""Batch-Erstellung von Lizenzen"""
customers = get_customers()
if request.method == "POST":
conn = get_connection()
cur = conn.cursor()
try:
# Form data
customer_id = int(request.form['customer_id'])
license_type = request.form['license_type']
count = int(request.form['quantity']) # Korrigiert von 'count' zu 'quantity'
valid_from = request.form['valid_from']
valid_until = request.form['valid_until']
device_limit = int(request.form['device_limit'])
# Resource allocation parameters
domain_count = int(request.form.get('domain_count', 0))
ipv4_count = int(request.form.get('ipv4_count', 0))
phone_count = int(request.form.get('phone_count', 0))
# Validierung
if count < 1 or count > 100:
flash('Anzahl muss zwischen 1 und 100 liegen!', 'error')
return redirect(url_for('batch.batch_create'))
# Hole Kundendaten inkl. is_fake Status
cur.execute("SELECT name, email, is_fake FROM customers WHERE id = %s", (customer_id,))
customer = cur.fetchone()
if not customer:
flash('Kunde nicht gefunden!', 'error')
return redirect(url_for('batch.batch_create'))
# Lizenz erbt immer den is_fake Status vom Kunden
is_fake = customer[2]
created_licenses = []
# Erstelle Lizenzen
for i in range(count):
license_key = generate_license_key()
# Prüfe ob Schlüssel bereits existiert
while True:
cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,))
if not cur.fetchone():
break
license_key = generate_license_key()
# Erstelle Lizenz
cur.execute("""
INSERT INTO licenses (
license_key, customer_id,
license_type, valid_from, valid_until, device_limit,
is_fake, created_at
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
license_key, customer_id,
license_type, valid_from, valid_until, device_limit,
is_fake, datetime.now()
))
license_id = cur.fetchone()[0]
created_licenses.append({
'id': license_id,
'license_key': license_key
})
# Allocate resources if requested
if domain_count > 0 or ipv4_count > 0 or phone_count > 0:
# Allocate domains
if domain_count > 0:
cur.execute("""
UPDATE resource_pool
SET status = 'allocated',
license_id = %s,
allocated_at = NOW()
WHERE id IN (
SELECT id FROM resource_pool
WHERE type = 'domain'
AND status = 'available'
AND is_fake = %s
ORDER BY id
LIMIT %s
)
""", (license_id, is_fake, domain_count))
# Allocate IPv4s
if ipv4_count > 0:
cur.execute("""
UPDATE resource_pool
SET status = 'allocated',
license_id = %s,
allocated_at = NOW()
WHERE id IN (
SELECT id FROM resource_pool
WHERE type = 'ipv4'
AND status = 'available'
AND is_fake = %s
ORDER BY id
LIMIT %s
)
""", (license_id, is_fake, ipv4_count))
# Allocate phones
if phone_count > 0:
cur.execute("""
UPDATE resource_pool
SET status = 'allocated',
license_id = %s,
allocated_at = NOW()
WHERE id IN (
SELECT id FROM resource_pool
WHERE type = 'phone'
AND status = 'available'
AND is_fake = %s
ORDER BY id
LIMIT %s
)
""", (license_id, is_fake, phone_count))
# Audit-Log
log_audit('CREATE', 'license', license_id,
new_values={
'license_key': license_key,
'customer_name': customer[0],
'batch_creation': True
})
conn.commit()
# Speichere erstellte Lizenzen in Session für Export
session['batch_created_licenses'] = created_licenses
session['batch_customer_name'] = customer[0]
session['batch_customer_email'] = customer[1]
flash(f'{count} Lizenzen erfolgreich erstellt!', 'success')
# Weiterleitung zum Export
return redirect(url_for('batch.batch_export'))
except Exception as e:
conn.rollback()
logging.error(f"Fehler bei Batch-Erstellung: {str(e)}")
flash('Fehler bei der Batch-Erstellung!', 'error')
finally:
cur.close()
conn.close()
return render_template("batch_form.html", customers=customers)
@batch_bp.route("/batch/export")
@login_required
def batch_export():
"""Exportiert die zuletzt erstellten Batch-Lizenzen"""
created_licenses = session.get('batch_created_licenses', [])
if not created_licenses:
flash('Keine Lizenzen zum Exportieren gefunden!', 'error')
return redirect(url_for('batch.batch_create'))
conn = get_connection()
cur = conn.cursor()
try:
# Hole vollständige Lizenzdaten
license_ids = [l['id'] for l in created_licenses]
cur.execute("""
SELECT
l.license_key, c.name, c.email,
l.license_type, l.valid_from, l.valid_until,
l.device_limit, l.is_fake, l.created_at
FROM licenses l
JOIN customers c ON l.customer_id = c.id
WHERE l.id = ANY(%s)
ORDER BY l.id
""", (license_ids,))
licenses = []
for row in cur.fetchall():
licenses.append({
'license_key': row[0],
'customer_name': row[1],
'customer_email': row[2],
'license_type': row[3],
'valid_from': row[4],
'valid_until': row[5],
'device_limit': row[6],
'is_fake': row[7],
'created_at': row[8]
})
# Lösche aus Session
session.pop('batch_created_licenses', None)
session.pop('batch_customer_name', None)
session.pop('batch_customer_email', None)
# Erstelle und sende Excel-Export
return create_batch_export(licenses)
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
flash('Fehler beim Exportieren der Lizenzen!', 'error')
return redirect(url_for('batch.batch_create'))
finally:
cur.close()
conn.close()
@batch_bp.route("/batch/update", methods=["GET", "POST"])
@login_required
def batch_update():
"""Batch-Update von Lizenzen"""
if request.method == "POST":
conn = get_connection()
cur = conn.cursor()
try:
# Form data
license_keys = request.form.get('license_keys', '').strip().split('\n')
license_keys = [key.strip() for key in license_keys if key.strip()]
if not license_keys:
flash('Keine Lizenzschlüssel angegeben!', 'error')
return redirect(url_for('batch.batch_update'))
# Update-Parameter
updates = []
params = []
if 'update_valid_until' in request.form and request.form['valid_until']:
updates.append("valid_until = %s")
params.append(request.form['valid_until'])
if 'update_device_limit' in request.form and request.form['device_limit']:
updates.append("device_limit = %s")
params.append(int(request.form['device_limit']))
if 'update_active' in request.form:
updates.append("is_active = %s")
params.append('is_active' in request.form)
if not updates:
flash('Keine Änderungen angegeben!', 'error')
return redirect(url_for('batch.batch_update'))
# Führe Updates aus
updated_count = 0
not_found = []
for license_key in license_keys:
# Prüfe ob Lizenz existiert
cur.execute("SELECT id FROM licenses WHERE license_key = %s", (license_key,))
result = cur.fetchone()
if not result:
not_found.append(license_key)
continue
license_id = result[0]
# Update ausführen
update_params = params + [license_id]
cur.execute(f"""
UPDATE licenses
SET {', '.join(updates)}
WHERE id = %s
""", update_params)
# Audit-Log
log_audit('BATCH_UPDATE', 'license', license_id,
additional_info=f"Batch-Update: {', '.join(updates)}")
updated_count += 1
conn.commit()
# Feedback
flash(f'{updated_count} Lizenzen erfolgreich aktualisiert!', 'success')
if not_found:
flash(f'{len(not_found)} Lizenzen nicht gefunden: {", ".join(not_found[:5])}{"..." if len(not_found) > 5 else ""}', 'warning')
except Exception as e:
conn.rollback()
logging.error(f"Fehler bei Batch-Update: {str(e)}")
flash('Fehler beim Batch-Update!', 'error')
finally:
cur.close()
conn.close()
return render_template("batch_update.html")
@batch_bp.route("/batch/import", methods=["GET", "POST"])
@login_required
def batch_import():
"""Import von Lizenzen aus CSV/Excel"""
if request.method == "POST":
if 'file' not in request.files:
flash('Keine Datei ausgewählt!', 'error')
return redirect(url_for('batch.batch_import'))
file = request.files['file']
if file.filename == '':
flash('Keine Datei ausgewählt!', 'error')
return redirect(url_for('batch.batch_import'))
# Verarbeite Datei
try:
import pandas as pd
# Lese Datei
if file.filename.endswith('.csv'):
df = pd.read_csv(file)
elif file.filename.endswith(('.xlsx', '.xls')):
df = pd.read_excel(file)
else:
flash('Ungültiges Dateiformat! Nur CSV und Excel erlaubt.', 'error')
return redirect(url_for('batch.batch_import'))
# Validiere Spalten
required_columns = ['customer_email', 'license_type', 'valid_from', 'valid_until', 'device_limit']
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
flash(f'Fehlende Spalten: {", ".join(missing_columns)}', 'error')
return redirect(url_for('batch.batch_import'))
conn = get_connection()
cur = conn.cursor()
imported_count = 0
errors = []
for index, row in df.iterrows():
try:
# Finde oder erstelle Kunde
email = row['customer_email']
cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,))
customer = cur.fetchone()
if not customer:
# Erstelle neuen Kunden
name = row.get('customer_name', email.split('@')[0])
# Neue Kunden werden immer als Fake erstellt in der Testphase
# TODO: Nach Testphase muss hier die Business-Logik angepasst werden
is_fake = True
cur.execute("""
INSERT INTO customers (name, email, is_fake, created_at)
VALUES (%s, %s, %s, %s)
RETURNING id
""", (name, email, is_fake, datetime.now()))
customer_id = cur.fetchone()[0]
customer_name = name
else:
customer_id = customer[0]
customer_name = customer[1]
# Hole is_fake Status vom existierenden Kunden
cur.execute("SELECT is_fake FROM customers WHERE id = %s", (customer_id,))
is_fake = cur.fetchone()[0]
# Generiere Lizenzschlüssel
license_key = row.get('license_key', generate_license_key())
# Erstelle Lizenz - is_fake wird vom Kunden geerbt
cur.execute("""
INSERT INTO licenses (
license_key, customer_id,
license_type, valid_from, valid_until, device_limit,
is_fake, created_at
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""", (
license_key, customer_id,
row['license_type'], row['valid_from'], row['valid_until'],
int(row['device_limit']), is_fake,
datetime.now()
))
license_id = cur.fetchone()[0]
imported_count += 1
# Audit-Log
log_audit('IMPORT', 'license', license_id,
additional_info=f"Importiert aus {file.filename}")
except Exception as e:
errors.append(f"Zeile {index + 2}: {str(e)}")
conn.commit()
# Feedback
flash(f'{imported_count} Lizenzen erfolgreich importiert!', 'success')
if errors:
flash(f'{len(errors)} Fehler aufgetreten. Erste Fehler: {"; ".join(errors[:3])}', 'warning')
except Exception as e:
logging.error(f"Fehler beim Import: {str(e)}")
flash(f'Fehler beim Import: {str(e)}', 'error')
finally:
if 'conn' in locals():
cur.close()
conn.close()
return render_template("batch_import.html")

Datei anzeigen

@@ -1,466 +0,0 @@
import os
import logging
from datetime import datetime
from zoneinfo import ZoneInfo
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify
import config
from auth.decorators import login_required
from utils.audit import log_audit
from db import get_connection, get_db_connection, get_db_cursor
from models import get_customers, get_customer_by_id
# Create Blueprint
customer_bp = Blueprint('customers', __name__)
# Test route
@customer_bp.route("/test-customers")
def test_customers():
return "Customer blueprint is working!"
@customer_bp.route("/customers")
@login_required
def customers():
show_fake = request.args.get('show_fake', 'false').lower() == 'true'
search = request.args.get('search', '').strip()
page = request.args.get('page', 1, type=int)
per_page = 20
sort = request.args.get('sort', 'name')
order = request.args.get('order', 'asc')
customers_list = get_customers(show_fake=show_fake, search=search)
# Sortierung
if sort == 'name':
customers_list.sort(key=lambda x: x['name'].lower(), reverse=(order == 'desc'))
elif sort == 'email':
customers_list.sort(key=lambda x: x['email'].lower(), reverse=(order == 'desc'))
elif sort == 'created_at':
customers_list.sort(key=lambda x: x['created_at'], reverse=(order == 'desc'))
# Paginierung
total_customers = len(customers_list)
total_pages = (total_customers + per_page - 1) // per_page
start = (page - 1) * per_page
end = start + per_page
paginated_customers = customers_list[start:end]
return render_template("customers.html",
customers=paginated_customers,
show_fake=show_fake,
search=search,
page=page,
per_page=per_page,
total_pages=total_pages,
total_customers=total_customers,
sort=sort,
order=order,
current_order=order)
@customer_bp.route("/customer/edit/<int:customer_id>", methods=["GET", "POST"])
@login_required
def edit_customer(customer_id):
if request.method == "POST":
try:
# Get current customer data for comparison
current_customer = get_customer_by_id(customer_id)
if not current_customer:
flash('Kunde nicht gefunden!', 'error')
return redirect(url_for('customers.customers_licenses'))
with get_db_connection() as conn:
cur = conn.cursor()
try:
# Update customer data
new_values = {
'name': request.form['name'],
'email': request.form['email'],
'is_fake': 'is_fake' in request.form
}
cur.execute("""
UPDATE customers
SET name = %s, email = %s, is_fake = %s
WHERE id = %s
""", (
new_values['name'],
new_values['email'],
new_values['is_fake'],
customer_id
))
conn.commit()
# Log changes
log_audit('UPDATE', 'customer', customer_id,
old_values={
'name': current_customer['name'],
'email': current_customer['email'],
'is_fake': current_customer.get('is_fake', False)
},
new_values=new_values)
flash('Kunde erfolgreich aktualisiert!', 'success')
# Redirect mit show_fake Parameter wenn nötig
redirect_url = url_for('customers.customers_licenses')
if request.form.get('show_fake') == 'true':
redirect_url += '?show_fake=true'
return redirect(redirect_url)
finally:
cur.close()
except Exception as e:
logging.error(f"Fehler beim Aktualisieren des Kunden: {str(e)}")
flash('Fehler beim Aktualisieren des Kunden!', 'error')
return redirect(url_for('customers.customers_licenses'))
# GET request
customer_data = get_customer_by_id(customer_id)
if not customer_data:
flash('Kunde nicht gefunden!', 'error')
return redirect(url_for('customers.customers_licenses'))
return render_template("edit_customer.html", customer=customer_data)
@customer_bp.route("/customer/create", methods=["GET", "POST"])
@login_required
def create_customer():
if request.method == "POST":
conn = get_connection()
cur = conn.cursor()
try:
# Insert new customer
name = request.form['name']
email = request.form['email']
is_fake = 'is_fake' in request.form # Checkbox ist nur vorhanden wenn angekreuzt
cur.execute("""
INSERT INTO customers (name, email, is_fake, created_at)
VALUES (%s, %s, %s, %s)
RETURNING id
""", (name, email, is_fake, datetime.now()))
customer_id = cur.fetchone()[0]
conn.commit()
# Log creation
log_audit('CREATE', 'customer', customer_id,
new_values={
'name': name,
'email': email,
'is_fake': is_fake
})
if is_fake:
flash(f'Fake-Kunde {name} erfolgreich erstellt!', 'success')
else:
flash(f'Kunde {name} erfolgreich erstellt!', 'success')
# Redirect mit show_fake=true wenn Fake-Kunde erstellt wurde
if is_fake:
return redirect(url_for('customers.customers_licenses', show_fake='true'))
else:
return redirect(url_for('customers.customers_licenses'))
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Erstellen des Kunden: {str(e)}")
flash('Fehler beim Erstellen des Kunden!', 'error')
finally:
cur.close()
conn.close()
return render_template("create_customer.html")
@customer_bp.route("/customer/delete/<int:customer_id>", methods=["POST"])
@login_required
def delete_customer(customer_id):
conn = get_connection()
cur = conn.cursor()
try:
# Get customer data before deletion
customer_data = get_customer_by_id(customer_id)
if not customer_data:
flash('Kunde nicht gefunden!', 'error')
return redirect(url_for('customers.customers_licenses'))
# Check if customer has licenses
cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,))
license_count = cur.fetchone()[0]
if license_count > 0:
flash(f'Kunde kann nicht gelöscht werden - hat noch {license_count} Lizenz(en)!', 'error')
return redirect(url_for('customers.customers_licenses'))
# Delete the customer
cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,))
conn.commit()
# Log deletion
log_audit('DELETE', 'customer', customer_id,
old_values={
'name': customer_data['name'],
'email': customer_data['email']
})
flash(f'Kunde {customer_data["name"]} erfolgreich gelöscht!', 'success')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Löschen des Kunden: {str(e)}")
flash('Fehler beim Löschen des Kunden!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('customers.customers_licenses'))
@customer_bp.route("/customers-licenses")
@login_required
def customers_licenses():
"""Zeigt die Übersicht von Kunden und deren Lizenzen"""
import logging
import psycopg2
logging.info("=== CUSTOMERS-LICENSES ROUTE CALLED ===")
# Get show_fake parameter from URL
show_fake = request.args.get('show_fake', 'false').lower() == 'true'
logging.info(f"show_fake parameter: {show_fake}")
try:
# Direkte Verbindung ohne Helper-Funktionen
conn = psycopg2.connect(
host=os.getenv("POSTGRES_HOST", "postgres"),
port=os.getenv("POSTGRES_PORT", "5432"),
dbname=os.getenv("POSTGRES_DB"),
user=os.getenv("POSTGRES_USER"),
password=os.getenv("POSTGRES_PASSWORD")
)
conn.set_client_encoding('UTF8')
cur = conn.cursor()
try:
# Hole alle Kunden mit ihren Lizenzen
# Wenn show_fake=false, zeige nur Nicht-Test-Kunden
query = """
SELECT
c.id,
c.name,
c.email,
c.created_at,
COUNT(l.id),
COUNT(CASE WHEN l.is_active = true THEN 1 END),
COUNT(CASE WHEN l.is_fake = true THEN 1 END),
MAX(l.created_at),
c.is_fake
FROM customers c
LEFT JOIN licenses l ON c.id = l.customer_id
WHERE (%s OR c.is_fake = false)
GROUP BY c.id, c.name, c.email, c.created_at, c.is_fake
ORDER BY c.name
"""
cur.execute(query, (show_fake,))
customers = []
results = cur.fetchall()
logging.info(f"=== QUERY RETURNED {len(results)} ROWS ===")
for idx, row in enumerate(results):
logging.info(f"Row {idx}: Type={type(row)}, Length={len(row) if hasattr(row, '__len__') else 'N/A'}")
customers.append({
'id': row[0],
'name': row[1],
'email': row[2],
'created_at': row[3],
'license_count': row[4],
'active_licenses': row[5],
'test_licenses': row[6],
'last_license_created': row[7],
'is_fake': row[8]
})
return render_template("customers_licenses.html",
customers=customers,
show_fake=show_fake)
finally:
cur.close()
conn.close()
except Exception as e:
import traceback
error_details = f"Fehler beim Laden der Kunden-Lizenz-Übersicht: {str(e)}\nType: {type(e)}\nTraceback: {traceback.format_exc()}"
logging.error(error_details)
flash(f'Datenbankfehler: {str(e)}', 'error')
return redirect(url_for('admin.dashboard'))
@customer_bp.route("/api/customer/<int:customer_id>/licenses")
@login_required
def api_customer_licenses(customer_id):
"""API-Endpunkt für die Lizenzen eines Kunden"""
conn = get_connection()
cur = conn.cursor()
try:
# Hole Kundeninformationen
customer = get_customer_by_id(customer_id)
if not customer:
return jsonify({'error': 'Kunde nicht gefunden'}), 404
# Hole alle Lizenzen des Kunden - vereinfachte Version ohne komplexe Subqueries
cur.execute("""
SELECT
l.id,
l.license_key,
l.license_type,
l.is_active,
l.is_fake,
l.valid_from,
l.valid_until,
l.device_limit,
l.created_at,
CASE
WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen'
WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab'
WHEN l.is_active = false THEN 'inaktiv'
ELSE 'aktiv'
END as status,
COALESCE(l.domain_count, 0) as domain_count,
COALESCE(l.ipv4_count, 0) as ipv4_count,
COALESCE(l.phone_count, 0) as phone_count
FROM licenses l
WHERE l.customer_id = %s
ORDER BY l.created_at DESC, l.id DESC
""", (customer_id,))
licenses = []
for row in cur.fetchall():
license_id = row[0]
# Hole die konkreten zugewiesenen Ressourcen für diese Lizenz
conn2 = get_connection()
cur2 = conn2.cursor()
cur2.execute("""
SELECT rp.id, rp.resource_type, rp.resource_value, lr.assigned_at
FROM resource_pools rp
JOIN license_resources lr ON rp.id = lr.resource_id
WHERE lr.license_id = %s AND lr.is_active = true
ORDER BY rp.resource_type, rp.resource_value
""", (license_id,))
resources = {
'domains': [],
'ipv4s': [],
'phones': []
}
for res_row in cur2.fetchall():
resource_data = {
'id': res_row[0],
'value': res_row[2],
'assigned_at': res_row[3].strftime('%Y-%m-%d %H:%M:%S') if res_row[3] else None
}
if res_row[1] == 'domain':
resources['domains'].append(resource_data)
elif res_row[1] == 'ipv4':
resources['ipv4s'].append(resource_data)
elif res_row[1] == 'phone':
resources['phones'].append(resource_data)
cur2.close()
conn2.close()
licenses.append({
'id': row[0],
'license_key': row[1],
'license_type': row[2],
'is_active': row[3],
'is_fake': row[4],
'valid_from': row[5].strftime('%Y-%m-%d') if row[5] else None,
'valid_until': row[6].strftime('%Y-%m-%d') if row[6] else None,
'device_limit': row[7],
'created_at': row[8].strftime('%Y-%m-%d %H:%M:%S') if row[8] else None,
'status': row[9],
'domain_count': row[10],
'ipv4_count': row[11],
'phone_count': row[12],
'active_sessions': 0, # Platzhalter
'registered_devices': 0, # Platzhalter
'active_devices': 0, # Platzhalter
'actual_domain_count': len(resources['domains']),
'actual_ipv4_count': len(resources['ipv4s']),
'actual_phone_count': len(resources['phones']),
'resources': resources,
# License Server Data (Platzhalter bis Implementation)
'recent_heartbeats': 0,
'last_heartbeat': None,
'active_server_devices': 0,
'unresolved_anomalies': 0
})
return jsonify({
'success': True, # Wichtig: Frontend erwartet dieses Feld
'customer': {
'id': customer['id'],
'name': customer['name'],
'email': customer['email'],
'is_fake': customer.get('is_fake', False) # Include the is_fake field
},
'licenses': licenses
})
except Exception as e:
import traceback
error_msg = f"Fehler beim Laden der Kundenlizenzen: {str(e)}\nTraceback: {traceback.format_exc()}"
logging.error(error_msg)
return jsonify({'error': f'Fehler beim Laden der Daten: {str(e)}', 'details': error_msg}), 500
finally:
cur.close()
conn.close()
@customer_bp.route("/api/customer/<int:customer_id>/quick-stats")
@login_required
def api_customer_quick_stats(customer_id):
"""Schnelle Statistiken für einen Kunden"""
conn = get_connection()
cur = conn.cursor()
try:
cur.execute("""
SELECT
COUNT(l.id) as total_licenses,
COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_licenses,
COUNT(CASE WHEN l.is_fake = true THEN 1 END) as test_licenses,
SUM(l.device_limit) as total_device_limit
FROM licenses l
WHERE l.customer_id = %s
""", (customer_id,))
row = cur.fetchone()
return jsonify({
'total_licenses': row[0] or 0,
'active_licenses': row[1] or 0,
'test_licenses': row[2] or 0,
'total_device_limit': row[3] or 0
})
except Exception as e:
logging.error(f"Fehler beim Laden der Kundenstatistiken: {str(e)}")
return jsonify({'error': 'Fehler beim Laden der Daten'}), 500
finally:
cur.close()
conn.close()

Datei anzeigen

@@ -1,495 +0,0 @@
import logging
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from flask import Blueprint, request, send_file
import config
from auth.decorators import login_required
from utils.export import create_excel_export, create_csv_export, prepare_audit_export_data, format_datetime_for_export
from db import get_connection
# Create Blueprint
export_bp = Blueprint('export', __name__, url_prefix='/export')
@export_bp.route("/licenses")
@login_required
def export_licenses():
"""Exportiert Lizenzen als Excel-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Nur reale Daten exportieren - keine Fake-Daten
query = """
SELECT
l.id,
l.license_key,
c.name as customer_name,
c.email as customer_email,
l.license_type,
l.valid_from,
l.valid_until,
l.is_active,
l.device_limit,
l.created_at,
l.is_fake,
CASE
WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen'
WHEN l.is_active = false THEN 'Deaktiviert'
ELSE 'Aktiv'
END as status,
(SELECT COUNT(*) FROM sessions s WHERE s.license_key = l.license_key AND s.is_active = true) as active_sessions,
(SELECT COUNT(DISTINCT hardware_id) FROM sessions s WHERE s.license_key = l.license_key) as registered_devices
FROM licenses l
LEFT JOIN customers c ON l.customer_id = c.id
WHERE l.is_fake = false
ORDER BY l.created_at DESC
"""
cur.execute(query)
# Daten für Export vorbereiten
data = []
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 'Gültig von',
'Gültig bis', 'Aktiv', 'Gerätelimit', 'Erstellt am', 'Fake-Lizenz',
'Status', 'Aktive Sessions', 'Registrierte Geräte']
for row in cur.fetchall():
row_data = list(row)
# Format datetime fields
if row_data[5]: # valid_from
row_data[5] = format_datetime_for_export(row_data[5])
if row_data[6]: # valid_until
row_data[6] = format_datetime_for_export(row_data[6])
if row_data[9]: # created_at
row_data[9] = format_datetime_for_export(row_data[9])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'lizenzen')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'lizenzen')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Lizenzen", 500
finally:
cur.close()
conn.close()
@export_bp.route("/audit")
@login_required
def export_audit():
"""Exportiert Audit-Logs als Excel-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Filter aus Request
days = int(request.args.get('days', 30))
action_filter = request.args.get('action', '')
entity_type_filter = request.args.get('entity_type', '')
# Query aufbauen
query = """
SELECT
id, timestamp, username, action, entity_type, entity_id,
ip_address, user_agent, old_values, new_values, additional_info
FROM audit_log
WHERE timestamp >= CURRENT_TIMESTAMP - INTERVAL '%s days'
"""
params = [days]
if action_filter:
query += " AND action = %s"
params.append(action_filter)
if entity_type_filter:
query += " AND entity_type = %s"
params.append(entity_type_filter)
query += " ORDER BY timestamp DESC"
cur.execute(query, params)
# Daten in Dictionary-Format umwandeln
audit_logs = []
for row in cur.fetchall():
audit_logs.append({
'id': row[0],
'timestamp': row[1],
'username': row[2],
'action': row[3],
'entity_type': row[4],
'entity_id': row[5],
'ip_address': row[6],
'user_agent': row[7],
'old_values': row[8],
'new_values': row[9],
'additional_info': row[10]
})
# Daten für Export vorbereiten
data = prepare_audit_export_data(audit_logs)
# Excel-Datei erstellen
columns = ['ID', 'Zeitstempel', 'Benutzer', 'Aktion', 'Entität', 'Entität ID',
'IP-Adresse', 'User Agent', 'Alte Werte', 'Neue Werte', 'Zusatzinfo']
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'audit_log')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'audit_log')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Audit-Logs", 500
finally:
cur.close()
conn.close()
@export_bp.route("/customers")
@login_required
def export_customers():
"""Exportiert Kunden als Excel-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# SQL Query - nur reale Kunden exportieren
cur.execute("""
SELECT
c.id,
c.name,
c.email,
c.phone,
c.address,
c.created_at,
c.is_fake,
COUNT(l.id) as license_count,
COUNT(CASE WHEN l.is_active = true THEN 1 END) as active_licenses,
COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses
FROM customers c
LEFT JOIN licenses l ON c.id = l.customer_id
WHERE c.is_fake = false
GROUP BY c.id, c.name, c.email, c.phone, c.address, c.created_at, c.is_fake
ORDER BY c.name
""")
# Daten für Export vorbereiten
data = []
columns = ['ID', 'Name', 'E-Mail', 'Telefon', 'Adresse', 'Erstellt am',
'Test-Kunde', 'Anzahl Lizenzen', 'Aktive Lizenzen', 'Abgelaufene Lizenzen']
for row in cur.fetchall():
# Format datetime fields (created_at ist Spalte 5)
row_data = list(row)
if row_data[5]: # created_at
row_data[5] = format_datetime_for_export(row_data[5])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'kunden')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'kunden')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Kunden", 500
finally:
cur.close()
conn.close()
@export_bp.route("/sessions")
@login_required
def export_sessions():
"""Exportiert Sessions als Excel-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Filter aus Request
days = int(request.args.get('days', 7))
active_only = request.args.get('active_only', 'false') == 'true'
# SQL Query
if active_only:
query = """
SELECT
s.id,
s.license_key,
l.customer_name,
s.username,
s.hardware_id,
s.started_at,
s.ended_at,
s.last_heartbeat,
s.is_active,
l.license_type,
l.is_fake
FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE s.is_active = true AND l.is_fake = false
ORDER BY s.started_at DESC
"""
cur.execute(query)
else:
query = """
SELECT
s.id,
s.license_key,
l.customer_name,
s.username,
s.hardware_id,
s.started_at,
s.ended_at,
s.last_heartbeat,
s.is_active,
l.license_type,
l.is_fake
FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE s.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days' AND l.is_fake = false
ORDER BY s.started_at DESC
"""
cur.execute(query, (days,))
# Daten für Export vorbereiten
data = []
columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'Benutzer', 'Geräte-ID',
'Login-Zeit', 'Logout-Zeit', 'Letzte Aktivität', 'Aktiv',
'Lizenztyp', 'Fake-Lizenz']
for row in cur.fetchall():
row_data = list(row)
# Format datetime fields
if row_data[5]: # started_at
row_data[5] = format_datetime_for_export(row_data[5])
if row_data[6]: # ended_at
row_data[6] = format_datetime_for_export(row_data[6])
if row_data[7]: # last_heartbeat
row_data[7] = format_datetime_for_export(row_data[7])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'sessions')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'sessions')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Sessions", 500
finally:
cur.close()
conn.close()
@export_bp.route("/resources")
@login_required
def export_resources():
"""Exportiert Ressourcen als Excel-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Filter aus Request
resource_type = request.args.get('type', 'all')
status_filter = request.args.get('status', 'all')
# SQL Query aufbauen
query = """
SELECT
rp.id,
rp.resource_type,
rp.resource_value,
rp.status,
rp.is_fake,
l.license_key,
c.name as customer_name,
rp.created_at,
rp.created_by,
rp.status_changed_at,
rp.status_changed_by,
rp.quarantine_reason
FROM resource_pools rp
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
LEFT JOIN customers c ON l.customer_id = c.id
WHERE 1=1
"""
params = []
if resource_type != 'all':
query += " AND rp.resource_type = %s"
params.append(resource_type)
if status_filter != 'all':
query += " AND rp.status = %s"
params.append(status_filter)
# Immer nur reale Ressourcen exportieren
query += " AND rp.is_fake = false"
query += " ORDER BY rp.resource_type, rp.resource_value"
cur.execute(query, params)
# Daten für Export vorbereiten
data = []
columns = ['ID', 'Typ', 'Wert', 'Status', 'Test-Ressource', 'Lizenzschlüssel',
'Kunde', 'Erstellt am', 'Erstellt von', 'Status geändert am',
'Status geändert von', 'Quarantäne-Grund']
for row in cur.fetchall():
row_data = list(row)
# Format datetime fields
if row_data[7]: # created_at
row_data[7] = format_datetime_for_export(row_data[7])
if row_data[9]: # status_changed_at
row_data[9] = format_datetime_for_export(row_data[9])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'ressourcen')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'ressourcen')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Ressourcen", 500
finally:
cur.close()
conn.close()
@export_bp.route("/monitoring")
@login_required
def export_monitoring():
"""Exportiert Monitoring-Daten als Excel/CSV-Datei"""
conn = get_connection()
cur = conn.cursor()
try:
# Zeitraum aus Request
hours = int(request.args.get('hours', 24))
# Monitoring-Daten sammeln
data = []
columns = ['Zeitstempel', 'Lizenz-ID', 'Lizenzschlüssel', 'Kunde', 'Hardware-ID',
'IP-Adresse', 'Ereignis-Typ', 'Schweregrad', 'Beschreibung']
# Query für Heartbeats und optionale Anomalien
query = """
WITH monitoring_data AS (
-- Lizenz-Heartbeats
SELECT
lh.timestamp,
lh.license_id,
l.license_key,
c.name as customer_name,
lh.hardware_id,
lh.ip_address,
'Heartbeat' as event_type,
'Normal' as severity,
'License validation' as description
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > CURRENT_TIMESTAMP - INTERVAL '%s hours'
AND l.is_fake = false
"""
# Check if anomaly_detections table exists
cur.execute("""
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_name = 'anomaly_detections'
)
""")
has_anomalies = cur.fetchone()[0]
if has_anomalies:
query += """
UNION ALL
-- Anomalien
SELECT
ad.detected_at as timestamp,
ad.license_id,
l.license_key,
c.name as customer_name,
ad.hardware_id,
ad.ip_address,
ad.anomaly_type as event_type,
ad.severity,
ad.description
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.detected_at > CURRENT_TIMESTAMP - INTERVAL '%s hours'
AND (l.is_fake = false OR l.is_fake IS NULL)
"""
params = [hours, hours]
else:
params = [hours]
query += """
)
SELECT * FROM monitoring_data
ORDER BY timestamp DESC
"""
cur.execute(query, params)
for row in cur.fetchall():
row_data = list(row)
# Format datetime field (timestamp ist Spalte 0)
if row_data[0]: # timestamp
row_data[0] = format_datetime_for_export(row_data[0])
data.append(row_data)
# Format prüfen
format_type = request.args.get('format', 'excel').lower()
if format_type == 'csv':
# CSV-Datei erstellen
return create_csv_export(data, columns, 'monitoring')
else:
# Excel-Datei erstellen
return create_excel_export(data, columns, 'monitoring')
except Exception as e:
logging.error(f"Fehler beim Export: {str(e)}")
return "Fehler beim Exportieren der Monitoring-Daten", 500
finally:
cur.close()
conn.close()

Datei anzeigen

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

Datei anzeigen

@@ -1,428 +0,0 @@
from flask import Blueprint, render_template, jsonify, request, session, redirect, url_for
from functools import wraps
import psycopg2
from psycopg2.extras import RealDictCursor
import os
import requests
from datetime import datetime, timedelta
import logging
from utils.partition_helper import ensure_partition_exists, check_table_exists
# Configure logging
logger = logging.getLogger(__name__)
# Create a function to get database connection
def get_db_connection():
return psycopg2.connect(
host=os.environ.get('POSTGRES_HOST', 'postgres'),
database=os.environ.get('POSTGRES_DB', 'v2_adminpanel'),
user=os.environ.get('POSTGRES_USER', 'postgres'),
password=os.environ.get('POSTGRES_PASSWORD', 'postgres')
)
def login_required(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if 'user_id' not in session:
return redirect(url_for('auth.login'))
return f(*args, **kwargs)
return decorated_function
# Create Blueprint
monitoring_bp = Blueprint('monitoring', __name__)
@monitoring_bp.route('/monitoring')
@login_required
def unified_monitoring():
"""Unified monitoring dashboard combining live activity and anomaly detection"""
try:
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
# Initialize default values
system_status = 'normal'
status_color = 'success'
active_alerts = 0
live_metrics = {
'active_licenses': 0,
'total_validations': 0,
'unique_devices': 0,
'unique_ips': 0,
'avg_response_time': 0
}
trend_data = []
activity_stream = []
geo_data = []
top_licenses = []
anomaly_distribution = []
performance_data = []
# Check if tables exist before querying
has_heartbeats = check_table_exists(conn, 'license_heartbeats')
has_anomalies = check_table_exists(conn, 'anomaly_detections')
if has_anomalies:
# Get active alerts count
cur.execute("""
SELECT COUNT(*) as count
FROM anomaly_detections
WHERE resolved = false
AND detected_at > NOW() - INTERVAL '24 hours'
""")
active_alerts = cur.fetchone()['count'] or 0
# Determine system status based on alerts
if active_alerts == 0:
system_status = 'normal'
status_color = 'success'
elif active_alerts < 5:
system_status = 'warning'
status_color = 'warning'
else:
system_status = 'critical'
status_color = 'danger'
if has_heartbeats:
# Ensure current month partition exists
ensure_partition_exists(conn, 'license_heartbeats', datetime.now())
# Executive summary metrics
cur.execute("""
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips,
0 as avg_response_time
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
result = cur.fetchone()
if result:
live_metrics = result
# Get 24h trend data for metrics
cur.execute("""
SELECT
DATE_TRUNC('hour', timestamp) as hour,
COUNT(DISTINCT license_id) as licenses,
COUNT(*) as validations
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY hour
ORDER BY hour
""")
trend_data = cur.fetchall()
# Activity stream - just validations if no anomalies table
if has_anomalies:
cur.execute("""
WITH combined_events AS (
-- Normal validations
SELECT
lh.timestamp,
'validation' as event_type,
'normal' as severity,
l.license_key,
c.name as customer_name,
lh.ip_address,
lh.hardware_id,
NULL as anomaly_type,
NULL as description
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > NOW() - INTERVAL '1 hour'
UNION ALL
-- Anomalies
SELECT
ad.detected_at as timestamp,
'anomaly' as event_type,
ad.severity,
l.license_key,
c.name as customer_name,
ad.ip_address,
ad.hardware_id,
ad.anomaly_type,
ad.description
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.detected_at > NOW() - INTERVAL '1 hour'
)
SELECT * FROM combined_events
ORDER BY timestamp DESC
LIMIT 100
""")
else:
# Just show validations
cur.execute("""
SELECT
lh.timestamp,
'validation' as event_type,
'normal' as severity,
l.license_key,
c.name as customer_name,
lh.ip_address,
lh.hardware_id,
NULL as anomaly_type,
NULL as description
FROM license_heartbeats lh
JOIN licenses l ON l.id = lh.license_id
JOIN customers c ON c.id = l.customer_id
WHERE lh.timestamp > NOW() - INTERVAL '1 hour'
ORDER BY lh.timestamp DESC
LIMIT 100
""")
activity_stream = cur.fetchall()
# Geographic distribution
cur.execute("""
SELECT
ip_address,
COUNT(*) as request_count,
COUNT(DISTINCT license_id) as license_count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '1 hour'
GROUP BY ip_address
ORDER BY request_count DESC
LIMIT 20
""")
geo_data = cur.fetchall()
# Top active licenses
if has_anomalies:
cur.execute("""
SELECT
l.id,
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(lh.*) as validation_count,
MAX(lh.timestamp) as last_seen,
COUNT(DISTINCT ad.id) as anomaly_count
FROM licenses l
JOIN customers c ON c.id = l.customer_id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '1 hour'
LEFT JOIN anomaly_detections ad ON l.id = ad.license_id
AND ad.detected_at > NOW() - INTERVAL '24 hours'
WHERE lh.license_id IS NOT NULL
GROUP BY l.id, l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
else:
cur.execute("""
SELECT
l.id,
l.license_key,
c.name as customer_name,
COUNT(DISTINCT lh.hardware_id) as device_count,
COUNT(lh.*) as validation_count,
MAX(lh.timestamp) as last_seen,
0 as anomaly_count
FROM licenses l
JOIN customers c ON c.id = l.customer_id
LEFT JOIN license_heartbeats lh ON l.id = lh.license_id
AND lh.timestamp > NOW() - INTERVAL '1 hour'
WHERE lh.license_id IS NOT NULL
GROUP BY l.id, l.license_key, c.name
ORDER BY validation_count DESC
LIMIT 10
""")
top_licenses = cur.fetchall()
# Performance metrics
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
0 as avg_response_time,
0 as max_response_time,
COUNT(*) as request_count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes'
GROUP BY minute
ORDER BY minute DESC
""")
performance_data = cur.fetchall()
if has_anomalies:
# Anomaly distribution
cur.execute("""
SELECT
anomaly_type,
COUNT(*) as count,
MAX(severity) as max_severity
FROM anomaly_detections
WHERE detected_at > NOW() - INTERVAL '24 hours'
GROUP BY anomaly_type
ORDER BY count DESC
""")
anomaly_distribution = cur.fetchall()
cur.close()
conn.close()
return render_template('monitoring/unified_monitoring.html',
system_status=system_status,
status_color=status_color,
active_alerts=active_alerts,
live_metrics=live_metrics,
trend_data=trend_data,
activity_stream=activity_stream,
geo_data=geo_data,
top_licenses=top_licenses,
anomaly_distribution=anomaly_distribution,
performance_data=performance_data)
except Exception as e:
logger.error(f"Error in unified monitoring: {str(e)}")
return render_template('error.html',
error='Fehler beim Laden des Monitorings',
details=str(e))
@monitoring_bp.route('/live-dashboard')
@login_required
def live_dashboard():
"""Redirect to unified monitoring dashboard"""
return redirect(url_for('monitoring.unified_monitoring'))
@monitoring_bp.route('/alerts')
@login_required
def alerts():
"""Show active alerts from Alertmanager"""
alerts = []
try:
# Get alerts from Alertmanager
response = requests.get('http://alertmanager:9093/api/v1/alerts', timeout=2)
if response.status_code == 200:
alerts = response.json()
except:
# Fallback to database anomalies if table exists
conn = get_db_connection()
if check_table_exists(conn, 'anomaly_detections'):
cur = conn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT
ad.*,
l.license_key,
c.name as company_name
FROM anomaly_detections ad
LEFT JOIN licenses l ON l.id = ad.license_id
LEFT JOIN customers c ON c.id = l.customer_id
WHERE ad.resolved = false
ORDER BY ad.detected_at DESC
LIMIT 50
""")
alerts = cur.fetchall()
cur.close()
conn.close()
return render_template('monitoring/alerts.html', alerts=alerts)
@monitoring_bp.route('/analytics')
@login_required
def analytics():
"""Combined analytics and license server status page"""
try:
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
# Initialize default values
live_stats = [0, 0, 0, 0]
validation_rates = []
if check_table_exists(conn, 'license_heartbeats'):
# Get live statistics for the top cards
cur.execute("""
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
live_stats_data = cur.fetchone()
live_stats = [
live_stats_data['active_licenses'] or 0,
live_stats_data['total_validations'] or 0,
live_stats_data['unique_devices'] or 0,
live_stats_data['unique_ips'] or 0
]
# Get validation rates for the chart (last 30 minutes, aggregated by minute)
cur.execute("""
SELECT
DATE_TRUNC('minute', timestamp) as minute,
COUNT(*) as count
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '30 minutes'
GROUP BY minute
ORDER BY minute DESC
LIMIT 30
""")
validation_rates = [(row['minute'].isoformat(), row['count']) for row in cur.fetchall()]
cur.close()
conn.close()
return render_template('monitoring/analytics.html',
live_stats=live_stats,
validation_rates=validation_rates)
except Exception as e:
logger.error(f"Error in analytics: {str(e)}")
return render_template('error.html',
error='Fehler beim Laden der Analytics',
details=str(e))
@monitoring_bp.route('/analytics/stream')
@login_required
def analytics_stream():
"""Server-sent event stream for live analytics updates"""
def generate():
while True:
try:
conn = get_db_connection()
cur = conn.cursor(cursor_factory=RealDictCursor)
data = {'active_licenses': 0, 'total_validations': 0,
'unique_devices': 0, 'unique_ips': 0}
if check_table_exists(conn, 'license_heartbeats'):
cur.execute("""
SELECT
COUNT(DISTINCT license_id) as active_licenses,
COUNT(*) as total_validations,
COUNT(DISTINCT hardware_id) as unique_devices,
COUNT(DISTINCT ip_address) as unique_ips
FROM license_heartbeats
WHERE timestamp > NOW() - INTERVAL '5 minutes'
""")
result = cur.fetchone()
if result:
data = dict(result)
cur.close()
conn.close()
yield f"data: {jsonify(data).get_data(as_text=True)}\n\n"
except Exception as e:
logger.error(f"Error in analytics stream: {str(e)}")
yield f"data: {jsonify({'error': str(e)}).get_data(as_text=True)}\n\n"
import time
time.sleep(5) # Update every 5 seconds
from flask import Response
return Response(generate(), mimetype="text/event-stream")

Datei anzeigen

@@ -1,721 +0,0 @@
import logging
from datetime import datetime
from zoneinfo import ZoneInfo
from flask import Blueprint, render_template, request, redirect, session, url_for, flash, jsonify, send_file
import config
from auth.decorators import login_required
from utils.audit import log_audit
from utils.network import get_client_ip
from db import get_connection, get_db_connection, get_db_cursor
# Create Blueprint
resource_bp = Blueprint('resources', __name__)
@resource_bp.route('/resources')
@login_required
def resources():
"""Zeigt die Ressourcenpool-Übersicht"""
import logging
logging.info("=== RESOURCES ROUTE CALLED ===")
conn = get_connection()
cur = conn.cursor()
try:
# Filter aus Query-Parametern
resource_type = request.args.get('type', 'all')
status_filter = request.args.get('status', 'all')
search_query = request.args.get('search', '')
show_fake = request.args.get('show_fake', 'false') == 'true'
logging.info(f"Filters: type={resource_type}, status={status_filter}, search={search_query}, show_fake={show_fake}")
# Basis-Query
query = """
SELECT
rp.id,
rp.resource_type,
rp.resource_value,
rp.status,
rp.is_fake,
rp.allocated_to_license,
rp.created_at,
rp.status_changed_at,
rp.status_changed_by,
c.name as customer_name,
l.license_type
FROM resource_pools rp
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
LEFT JOIN customers c ON l.customer_id = c.id
WHERE 1=1
"""
params = []
# Filter anwenden
if resource_type != 'all':
query += " AND rp.resource_type = %s"
params.append(resource_type)
if status_filter != 'all':
query += " AND rp.status = %s"
params.append(status_filter)
if search_query:
query += " AND (rp.resource_value ILIKE %s OR c.name ILIKE %s)"
params.extend([f'%{search_query}%', f'%{search_query}%'])
if not show_fake:
query += " AND rp.is_fake = false"
query += " ORDER BY rp.resource_type, rp.resource_value"
cur.execute(query, params)
resources_list = []
rows = cur.fetchall()
logging.info(f"Query returned {len(rows)} rows")
for row in rows:
resources_list.append({
'id': row[0],
'resource_type': row[1],
'resource_value': row[2],
'status': row[3],
'is_fake': row[4],
'allocated_to_license': row[5],
'created_at': row[6],
'status_changed_at': row[7],
'status_changed_by': row[8],
'customer_name': row[9],
'license_type': row[10]
})
# Statistiken
stats_query = """
SELECT
resource_type,
status,
is_fake,
COUNT(*) as count
FROM resource_pools
"""
# Apply test filter to statistics as well
if not show_fake:
stats_query += " WHERE is_fake = false"
stats_query += " GROUP BY resource_type, status, is_fake"
cur.execute(stats_query)
stats = {}
for row in cur.fetchall():
res_type = row[0]
status = row[1]
is_fake = row[2]
count = row[3]
if res_type not in stats:
stats[res_type] = {
'total': 0,
'available': 0,
'allocated': 0,
'quarantined': 0,
'test': 0,
'prod': 0,
'available_percent': 0
}
stats[res_type]['total'] += count
stats[res_type][status] = stats[res_type].get(status, 0) + count
if is_fake:
stats[res_type]['test'] += count
else:
stats[res_type]['prod'] += count
# Calculate percentages
for res_type in stats:
if stats[res_type]['total'] > 0:
stats[res_type]['available_percent'] = int((stats[res_type]['available'] / stats[res_type]['total']) * 100)
# Pagination parameters (simple defaults for now)
try:
page = int(request.args.get('page', '1') or '1')
except (ValueError, TypeError):
page = 1
per_page = 50
total = len(resources_list)
total_pages = (total + per_page - 1) // per_page if total > 0 else 1
# Sort parameters
sort_by = request.args.get('sort', 'id')
sort_order = request.args.get('order', 'asc')
return render_template('resources.html',
resources=resources_list,
stats=stats,
resource_type=resource_type,
status_filter=status_filter,
search=search_query, # Changed from search_query to search
show_fake=show_fake,
total=total,
page=page,
total_pages=total_pages,
sort_by=sort_by,
sort_order=sort_order,
recent_activities=[], # Empty for now
datetime=datetime) # For template datetime usage
except Exception as e:
import traceback
logging.error(f"Fehler beim Laden der Ressourcen: {str(e)}")
logging.error(f"Traceback: {traceback.format_exc()}")
flash('Fehler beim Laden der Ressourcen!', 'error')
return redirect(url_for('admin.dashboard'))
finally:
cur.close()
conn.close()
# Old add_resource function removed - using add_resources instead
@resource_bp.route('/resources/quarantine/<int:resource_id>', methods=['POST'])
@login_required
def quarantine(resource_id):
"""Ressource in Quarantäne versetzen"""
conn = get_connection()
cur = conn.cursor()
try:
reason = request.form.get('reason', '')
# Hole aktuelle Ressourcen-Info
cur.execute("""
SELECT resource_value, status, allocated_to_license
FROM resource_pools WHERE id = %s
""", (resource_id,))
resource = cur.fetchone()
if not resource:
flash('Ressource nicht gefunden!', 'error')
return redirect(url_for('resources.resources'))
# Setze Status auf quarantined
cur.execute("""
UPDATE resource_pools
SET status = 'quarantined',
allocated_to_license = NULL,
status_changed_at = CURRENT_TIMESTAMP,
status_changed_by = %s,
quarantine_reason = %s
WHERE id = %s
""", (session['username'], reason, resource_id))
# Wenn die Ressource zugewiesen war, entferne die Zuweisung
if resource[2]: # allocated_to_license
cur.execute("""
DELETE FROM license_resources
WHERE license_id = %s AND resource_id = %s
""", (resource[2], resource_id))
# History-Eintrag
cur.execute("""
INSERT INTO resource_history (resource_id, license_id, action, action_by, notes, ip_address)
VALUES (%s, %s, 'quarantined', %s, %s, %s)
""", (resource_id, resource[2], session['username'], reason, get_client_ip()))
conn.commit()
# Audit-Log
log_audit('QUARANTINE', 'resource', resource_id,
old_values={'status': resource[1]},
new_values={'status': 'quarantined', 'reason': reason})
flash(f'Ressource {resource[0]} in Quarantäne versetzt!', 'warning')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Quarantänisieren der Ressource: {str(e)}")
flash('Fehler beim Quarantänisieren der Ressource!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('resources.resources'))
@resource_bp.route('/resources/release', methods=['POST'])
@login_required
def release():
"""Ressourcen aus Quarantäne freigeben oder von Lizenz entfernen"""
conn = get_connection()
cur = conn.cursor()
try:
resource_ids = request.form.getlist('resource_ids[]')
action = request.form.get('action', 'release')
if not resource_ids:
flash('Keine Ressourcen ausgewählt!', 'error')
return redirect(url_for('resources.resources'))
for resource_id in resource_ids:
# Hole aktuelle Ressourcen-Info
cur.execute("""
SELECT resource_value, status, allocated_to_license
FROM resource_pools WHERE id = %s
""", (resource_id,))
resource = cur.fetchone()
if resource:
# Setze Status auf available
cur.execute("""
UPDATE resource_pools
SET status = 'available',
allocated_to_license = NULL,
status_changed_at = CURRENT_TIMESTAMP,
status_changed_by = %s,
quarantine_reason = NULL
WHERE id = %s
""", (session['username'], resource_id))
# Entferne Lizenz-Zuweisung wenn vorhanden
if resource[2]: # allocated_to_license
cur.execute("""
DELETE FROM license_resources
WHERE license_id = %s AND resource_id = %s
""", (resource[2], resource_id))
# History-Eintrag
cur.execute("""
INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address)
VALUES (%s, %s, 'released', %s, %s)
""", (resource_id, resource[2], session['username'], get_client_ip()))
# Audit-Log
log_audit('RELEASE', 'resource', resource_id,
old_values={'status': resource[1]},
new_values={'status': 'available'})
conn.commit()
flash(f'{len(resource_ids)} Ressource(n) freigegeben!', 'success')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Freigeben der Ressourcen: {str(e)}")
flash('Fehler beim Freigeben der Ressourcen!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('resources.resources'))
@resource_bp.route('/resources/history/<int:resource_id>')
@login_required
def resource_history(resource_id):
"""Zeigt die Historie einer Ressource"""
conn = get_connection()
cur = conn.cursor()
try:
# Hole Ressourcen-Info
cur.execute("""
SELECT resource_type, resource_value, status, is_fake
FROM resource_pools WHERE id = %s
""", (resource_id,))
resource = cur.fetchone()
if not resource:
flash('Ressource nicht gefunden!', 'error')
return redirect(url_for('resources.resources'))
# Hole Historie
cur.execute("""
SELECT
rh.action,
rh.action_timestamp,
rh.action_by,
rh.notes,
rh.ip_address,
l.license_key,
c.name as customer_name
FROM resource_history rh
LEFT JOIN licenses l ON rh.license_id = l.id
LEFT JOIN customers c ON l.customer_id = c.id
WHERE rh.resource_id = %s
ORDER BY rh.action_timestamp DESC
""", (resource_id,))
history = []
for row in cur.fetchall():
history.append({
'action': row[0],
'timestamp': row[1],
'by': row[2],
'notes': row[3],
'ip_address': row[4],
'license_key': row[5],
'customer_name': row[6]
})
return render_template('resource_history.html',
resource={
'id': resource_id,
'type': resource[0],
'value': resource[1],
'status': resource[2],
'is_fake': resource[3]
},
history=history)
except Exception as e:
logging.error(f"Fehler beim Laden der Ressourcen-Historie: {str(e)}")
flash('Fehler beim Laden der Historie!', 'error')
return redirect(url_for('resources.resources'))
finally:
cur.close()
conn.close()
@resource_bp.route('/resources/metrics')
@login_required
def resource_metrics():
"""Zeigt Metriken und Statistiken zu Ressourcen"""
conn = get_connection()
cur = conn.cursor()
try:
# Allgemeine Statistiken
cur.execute("""
SELECT
resource_type,
status,
is_fake,
COUNT(*) as count
FROM resource_pools
GROUP BY resource_type, status, is_fake
ORDER BY resource_type, status
""")
general_stats = {}
for row in cur.fetchall():
res_type = row[0]
if res_type not in general_stats:
general_stats[res_type] = {
'total': 0,
'available': 0,
'allocated': 0,
'quarantined': 0,
'test': 0,
'production': 0
}
general_stats[res_type]['total'] += row[3]
general_stats[res_type][row[1]] += row[3]
if row[2]:
general_stats[res_type]['test'] += row[3]
else:
general_stats[res_type]['production'] += row[3]
# Zuweisungs-Statistiken
cur.execute("""
SELECT
rp.resource_type,
COUNT(DISTINCT l.customer_id) as unique_customers,
COUNT(DISTINCT rp.allocated_to_license) as unique_licenses
FROM resource_pools rp
JOIN licenses l ON rp.allocated_to_license = l.id
WHERE rp.status = 'allocated'
GROUP BY rp.resource_type
""")
allocation_stats = {}
for row in cur.fetchall():
allocation_stats[row[0]] = {
'unique_customers': row[1],
'unique_licenses': row[2]
}
# Historische Daten (letzte 30 Tage)
cur.execute("""
SELECT
DATE(action_timestamp) as date,
action,
COUNT(*) as count
FROM resource_history
WHERE action_timestamp >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(action_timestamp), action
ORDER BY date, action
""")
historical_data = {}
for row in cur.fetchall():
date_str = row[0].strftime('%Y-%m-%d')
if date_str not in historical_data:
historical_data[date_str] = {}
historical_data[date_str][row[1]] = row[2]
# Top-Kunden nach Ressourcennutzung
cur.execute("""
SELECT
c.name,
rp.resource_type,
COUNT(*) as count
FROM resource_pools rp
JOIN licenses l ON rp.allocated_to_license = l.id
JOIN customers c ON l.customer_id = c.id
WHERE rp.status = 'allocated'
GROUP BY c.name, rp.resource_type
ORDER BY count DESC
LIMIT 20
""")
top_customers = []
for row in cur.fetchall():
top_customers.append({
'customer': row[0],
'resource_type': row[1],
'count': row[2]
})
return render_template('resource_metrics.html',
general_stats=general_stats,
allocation_stats=allocation_stats,
historical_data=historical_data,
top_customers=top_customers)
except Exception as e:
logging.error(f"Fehler beim Laden der Ressourcen-Metriken: {str(e)}")
flash('Fehler beim Laden der Metriken!', 'error')
return redirect(url_for('resources.resources'))
finally:
cur.close()
conn.close()
@resource_bp.route('/resources/report', methods=['GET'])
@login_required
def resources_report():
"""Generiert einen Ressourcen-Report"""
from io import BytesIO
import xlsxwriter
conn = get_connection()
cur = conn.cursor()
try:
# Erstelle Excel-Datei im Speicher
output = BytesIO()
workbook = xlsxwriter.Workbook(output)
# Formatierungen
header_format = workbook.add_format({
'bold': True,
'bg_color': '#4CAF50',
'font_color': 'white',
'border': 1
})
date_format = workbook.add_format({'num_format': 'dd.mm.yyyy hh:mm'})
# Sheet 1: Übersicht
overview_sheet = workbook.add_worksheet('Übersicht')
# Header
headers = ['Ressourcen-Typ', 'Gesamt', 'Verfügbar', 'Zugewiesen', 'Quarantäne', 'Test', 'Produktion']
for col, header in enumerate(headers):
overview_sheet.write(0, col, header, header_format)
# Daten
cur.execute("""
SELECT
resource_type,
COUNT(*) as total,
COUNT(CASE WHEN status = 'available' THEN 1 END) as available,
COUNT(CASE WHEN status = 'allocated' THEN 1 END) as allocated,
COUNT(CASE WHEN status = 'quarantined' THEN 1 END) as quarantined,
COUNT(CASE WHEN is_fake = true THEN 1 END) as test,
COUNT(CASE WHEN is_fake = false THEN 1 END) as production
FROM resource_pools
GROUP BY resource_type
ORDER BY resource_type
""")
row = 1
for data in cur.fetchall():
for col, value in enumerate(data):
overview_sheet.write(row, col, value)
row += 1
# Sheet 2: Detailliste
detail_sheet = workbook.add_worksheet('Detailliste')
# Header
headers = ['Typ', 'Wert', 'Status', 'Test', 'Kunde', 'Lizenz', 'Zugewiesen am', 'Zugewiesen von']
for col, header in enumerate(headers):
detail_sheet.write(0, col, header, header_format)
# Daten
cur.execute("""
SELECT
rp.resource_type,
rp.resource_value,
rp.status,
rp.is_fake,
c.name as customer_name,
l.license_key,
rp.status_changed_at,
rp.status_changed_by
FROM resource_pools rp
LEFT JOIN licenses l ON rp.allocated_to_license = l.id
LEFT JOIN customers c ON l.customer_id = c.id
ORDER BY rp.resource_type, rp.resource_value
""")
row = 1
for data in cur.fetchall():
for col, value in enumerate(data):
if col == 6 and value: # Datum
detail_sheet.write_datetime(row, col, value, date_format)
else:
detail_sheet.write(row, col, value if value is not None else '')
row += 1
# Spaltenbreiten anpassen
overview_sheet.set_column('A:A', 20)
overview_sheet.set_column('B:G', 12)
detail_sheet.set_column('A:A', 15)
detail_sheet.set_column('B:B', 30)
detail_sheet.set_column('C:D', 12)
detail_sheet.set_column('E:F', 25)
detail_sheet.set_column('G:H', 20)
workbook.close()
output.seek(0)
# Sende Datei
filename = f"ressourcen_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return send_file(
output,
mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
as_attachment=True,
download_name=filename
)
except Exception as e:
logging.error(f"Fehler beim Generieren des Reports: {str(e)}")
flash('Fehler beim Generieren des Reports!', 'error')
return redirect(url_for('resources.resources'))
finally:
cur.close()
conn.close()
@resource_bp.route('/resources/add', methods=['GET', 'POST'])
@login_required
def add_resources():
"""Fügt neue Ressourcen zum Pool hinzu"""
if request.method == 'POST':
conn = get_connection()
cur = conn.cursor()
try:
resource_type = request.form.get('resource_type')
resources_text = request.form.get('resources_text', '')
is_fake = request.form.get('is_fake', 'false') == 'true'
if not resource_type or not resources_text.strip():
flash('Bitte Ressourcentyp und Ressourcen angeben!', 'error')
return redirect(url_for('resources.add_resources'))
# Parse resources (one per line)
resources = [r.strip() for r in resources_text.strip().split('\n') if r.strip()]
# Validate resources based on type
valid_resources = []
invalid_resources = []
for resource in resources:
if resource_type == 'domain':
# Basic domain validation
import re
if re.match(r'^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]?\.[a-zA-Z]{2,}$', resource):
valid_resources.append(resource)
else:
invalid_resources.append(resource)
elif resource_type == 'ipv4':
# IPv4 validation
parts = resource.split('.')
if len(parts) == 4 and all(p.isdigit() and 0 <= int(p) <= 255 for p in parts):
valid_resources.append(resource)
else:
invalid_resources.append(resource)
elif resource_type == 'phone':
# Phone number validation (basic)
import re
if re.match(r'^\+?[0-9]{7,15}$', resource.replace(' ', '').replace('-', '')):
valid_resources.append(resource)
else:
invalid_resources.append(resource)
else:
invalid_resources.append(resource)
# Check for duplicates
existing_resources = []
if valid_resources:
placeholders = ','.join(['%s'] * len(valid_resources))
cur.execute(f"""
SELECT resource_value
FROM resource_pools
WHERE resource_type = %s
AND resource_value IN ({placeholders})
""", [resource_type] + valid_resources)
existing_resources = [row[0] for row in cur.fetchall()]
# Filter out existing resources
new_resources = [r for r in valid_resources if r not in existing_resources]
# Insert new resources
added_count = 0
for resource in new_resources:
cur.execute("""
INSERT INTO resource_pools
(resource_type, resource_value, status, is_fake, created_by)
VALUES (%s, %s, 'available', %s, %s)
""", (resource_type, resource, is_fake, session['username']))
added_count += 1
conn.commit()
# Log audit
if added_count > 0:
log_audit('BULK_CREATE', 'resource',
additional_info=f"Added {added_count} {resource_type} resources")
# Flash messages
if added_count > 0:
flash(f'{added_count} neue Ressourcen erfolgreich hinzugefügt!', 'success')
if existing_resources:
flash(f'⚠️ {len(existing_resources)} Ressourcen existierten bereits und wurden übersprungen.', 'warning')
if invalid_resources:
flash(f'{len(invalid_resources)} ungültige Ressourcen wurden ignoriert.', 'error')
return redirect(url_for('resources.resources', show_fake=request.form.get('show_fake', 'false')))
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Hinzufügen von Ressourcen: {str(e)}")
flash('Fehler beim Hinzufügen der Ressourcen!', 'error')
finally:
cur.close()
conn.close()
# GET request - show form
show_fake = request.args.get('show_fake', 'false') == 'true'
return render_template('add_resources.html', show_fake=show_fake)

Datei anzeigen

@@ -1,429 +0,0 @@
import logging
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from flask import Blueprint, render_template, request, redirect, session, url_for, flash
import config
from auth.decorators import login_required
from utils.audit import log_audit
from utils.network import get_client_ip
from db import get_connection, get_db_connection, get_db_cursor
from models import get_active_sessions
# Create Blueprint
session_bp = Blueprint('sessions', __name__)
@session_bp.route("/sessions")
@login_required
def sessions():
conn = get_connection()
cur = conn.cursor()
try:
# Get is_active sessions with calculated inactive time
cur.execute("""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.user_agent, s.started_at, s.last_heartbeat,
EXTRACT(EPOCH FROM (NOW() - s.last_heartbeat))/60 as minutes_inactive
FROM sessions s
JOIN licenses l ON s.license_id = l.id
JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = TRUE
ORDER BY s.last_heartbeat DESC
""")
active_sessions = cur.fetchall()
# Get recent ended sessions
cur.execute("""
SELECT s.id, s.session_id, l.license_key, c.name, s.ip_address,
s.started_at, s.ended_at,
EXTRACT(EPOCH FROM (s.ended_at - s.started_at))/60 as duration_minutes
FROM sessions s
JOIN licenses l ON s.license_id = l.id
JOIN customers c ON l.customer_id = c.id
WHERE s.is_active = FALSE
AND s.ended_at > NOW() - INTERVAL '24 hours'
ORDER BY s.ended_at DESC
LIMIT 50
""")
recent_sessions = cur.fetchall()
return render_template("sessions.html",
active_sessions=active_sessions,
recent_sessions=recent_sessions)
except Exception as e:
logging.error(f"Error loading sessions: {str(e)}")
flash('Fehler beim Laden der Sessions!', 'error')
return redirect(url_for('admin.dashboard'))
finally:
cur.close()
conn.close()
@session_bp.route("/sessions/history")
@login_required
def session_history():
"""Zeigt die Session-Historie"""
conn = get_connection()
cur = conn.cursor()
try:
# Query parameters
license_key = request.args.get('license_key', '')
username = request.args.get('username', '')
days = int(request.args.get('days', 7))
# Base query
query = """
SELECT
s.id,
s.license_key,
s.username,
s.hardware_id,
s.started_at,
s.ended_at,
s.last_heartbeat,
s.is_active,
l.customer_name,
l.license_type,
l.is_test
FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE 1=1
"""
params = []
# Apply filters
if license_key:
query += " AND s.license_key = %s"
params.append(license_key)
if username:
query += " AND s.username ILIKE %s"
params.append(f'%{username}%')
# Time filter
query += " AND s.started_at >= CURRENT_TIMESTAMP - INTERVAL '%s days'"
params.append(days)
query += " ORDER BY s.started_at DESC LIMIT 1000"
cur.execute(query, params)
sessions_list = []
for row in cur.fetchall():
session_duration = None
if row[4] and row[5]: # started_at and ended_at
duration = row[5] - row[4]
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
session_duration = f"{hours}h {minutes}m"
elif row[4] and row[7]: # started_at and is_active
duration = datetime.now(ZoneInfo("UTC")) - row[4]
hours = int(duration.total_seconds() // 3600)
minutes = int((duration.total_seconds() % 3600) // 60)
session_duration = f"{hours}h {minutes}m (aktiv)"
sessions_list.append({
'id': row[0],
'license_key': row[1],
'username': row[2],
'hardware_id': row[3],
'started_at': row[4],
'ended_at': row[5],
'last_heartbeat': row[6],
'is_active': row[7],
'customer_name': row[8],
'license_type': row[9],
'is_test': row[10],
'duration': session_duration
})
# Get unique license keys for filter dropdown
cur.execute("""
SELECT DISTINCT s.license_key, l.customer_name
FROM sessions s
LEFT JOIN licenses l ON s.license_key = l.license_key
WHERE s.started_at >= CURRENT_TIMESTAMP - INTERVAL '30 days'
ORDER BY l.customer_name, s.license_key
""")
available_licenses = []
for row in cur.fetchall():
available_licenses.append({
'license_key': row[0],
'customer_name': row[1] or 'Unbekannt'
})
return render_template("session_history.html",
sessions=sessions_list,
available_licenses=available_licenses,
filters={
'license_key': license_key,
'username': username,
'days': days
})
except Exception as e:
logging.error(f"Fehler beim Laden der Session-Historie: {str(e)}")
flash('Fehler beim Laden der Session-Historie!', 'error')
return redirect(url_for('sessions.sessions'))
finally:
cur.close()
conn.close()
@session_bp.route("/session/end/<int:session_id>", methods=["POST"])
@login_required
def terminate_session(session_id):
"""Beendet eine aktive Session"""
conn = get_connection()
cur = conn.cursor()
try:
# Get session info
cur.execute("""
SELECT license_key, username, hardware_id
FROM sessions
WHERE id = %s AND is_active = true
""", (session_id,))
session_info = cur.fetchone()
if not session_info:
flash('Session nicht gefunden oder bereits beendet!', 'error')
return redirect(url_for('sessions.sessions'))
# Terminate session
cur.execute("""
UPDATE sessions
SET is_active = false, ended_at = CURRENT_TIMESTAMP
WHERE id = %s
""", (session_id,))
conn.commit()
# Audit log
log_audit('SESSION_TERMINATE', 'session', session_id,
additional_info=f"Session beendet für {session_info[1]} auf Lizenz {session_info[0]}")
flash('Session erfolgreich beendet!', 'success')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Beenden der Session: {str(e)}")
flash('Fehler beim Beenden der Session!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('sessions.sessions'))
@session_bp.route("/sessions/terminate-all/<license_key>", methods=["POST"])
@login_required
def terminate_all_sessions(license_key):
"""Beendet alle aktiven Sessions einer Lizenz"""
conn = get_connection()
cur = conn.cursor()
try:
# Count is_active sessions
cur.execute("""
SELECT COUNT(*) FROM sessions
WHERE license_key = %s AND is_active = true
""", (license_key,))
active_count = cur.fetchone()[0]
if active_count == 0:
flash('Keine aktiven Sessions gefunden!', 'info')
return redirect(url_for('sessions.sessions'))
# Terminate all sessions
cur.execute("""
UPDATE sessions
SET is_active = false, ended_at = CURRENT_TIMESTAMP
WHERE license_key = %s AND is_active = true
""", (license_key,))
conn.commit()
# Audit log
log_audit('SESSION_TERMINATE_ALL', 'license', None,
additional_info=f"{active_count} Sessions beendet für Lizenz {license_key}")
flash(f'{active_count} Sessions erfolgreich beendet!', 'success')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Beenden der Sessions: {str(e)}")
flash('Fehler beim Beenden der Sessions!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('sessions.sessions'))
@session_bp.route("/sessions/cleanup", methods=["POST"])
@login_required
def cleanup_sessions():
"""Bereinigt alte inaktive Sessions"""
conn = get_connection()
cur = conn.cursor()
try:
days = int(request.form.get('days', 30))
# Delete old inactive sessions
cur.execute("""
DELETE FROM sessions
WHERE is_active = false
AND ended_at < CURRENT_TIMESTAMP - INTERVAL '%s days'
RETURNING id
""", (days,))
deleted_ids = [row[0] for row in cur.fetchall()]
deleted_count = len(deleted_ids)
conn.commit()
# Audit log
if deleted_count > 0:
log_audit('SESSION_CLEANUP', 'system', None,
additional_info=f"{deleted_count} Sessions älter als {days} Tage gelöscht")
flash(f'{deleted_count} alte Sessions bereinigt!', 'success')
except Exception as e:
conn.rollback()
logging.error(f"Fehler beim Bereinigen der Sessions: {str(e)}")
flash('Fehler beim Bereinigen der Sessions!', 'error')
finally:
cur.close()
conn.close()
return redirect(url_for('sessions.session_history'))
@session_bp.route("/sessions/statistics")
@login_required
def session_statistics():
"""Zeigt Session-Statistiken"""
conn = get_connection()
cur = conn.cursor()
try:
# Aktuelle Statistiken
cur.execute("""
SELECT
COUNT(DISTINCT s.license_key) as active_licenses,
COUNT(DISTINCT s.username) as unique_users,
COUNT(DISTINCT s.hardware_id) as unique_devices,
COUNT(*) as total_active_sessions
FROM sessions s
WHERE s.is_active = true
""")
current_stats = cur.fetchone()
# Sessions nach Lizenztyp
cur.execute("""
SELECT
l.license_type,
COUNT(*) as session_count
FROM sessions s
JOIN licenses l ON s.license_key = l.license_key
WHERE s.is_active = true
GROUP BY l.license_type
ORDER BY session_count DESC
""")
sessions_by_type = []
for row in cur.fetchall():
sessions_by_type.append({
'license_type': row[0],
'count': row[1]
})
# Top 10 Lizenzen nach aktiven Sessions
cur.execute("""
SELECT
s.license_key,
l.customer_name,
COUNT(*) as session_count,
l.device_limit
FROM sessions s
JOIN licenses l ON s.license_key = l.license_key
WHERE s.is_active = true
GROUP BY s.license_key, l.customer_name, l.device_limit
ORDER BY session_count DESC
LIMIT 10
""")
top_licenses = []
for row in cur.fetchall():
top_licenses.append({
'license_key': row[0],
'customer_name': row[1],
'session_count': row[2],
'device_limit': row[3]
})
# Session-Verlauf (letzte 7 Tage)
cur.execute("""
SELECT
DATE(started_at) as date,
COUNT(*) as login_count,
COUNT(DISTINCT license_key) as unique_licenses,
COUNT(DISTINCT username) as unique_users
FROM sessions
WHERE started_at >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE(started_at)
ORDER BY date
""")
session_history = []
for row in cur.fetchall():
session_history.append({
'date': row[0].strftime('%Y-%m-%d'),
'login_count': row[1],
'unique_licenses': row[2],
'unique_users': row[3]
})
# Durchschnittliche Session-Dauer
cur.execute("""
SELECT
AVG(EXTRACT(EPOCH FROM (ended_at - started_at))/3600) as avg_duration_hours
FROM sessions
WHERE is_active = false
AND ended_at IS NOT NULL
AND ended_at - started_at < INTERVAL '24 hours'
AND started_at >= CURRENT_DATE - INTERVAL '30 days'
""")
avg_duration = cur.fetchone()[0] or 0
return render_template("session_statistics.html",
current_stats={
'active_licenses': current_stats[0],
'unique_users': current_stats[1],
'unique_devices': current_stats[2],
'total_sessions': current_stats[3]
},
sessions_by_type=sessions_by_type,
top_licenses=top_licenses,
session_history=session_history,
avg_duration=round(avg_duration, 1))
except Exception as e:
logging.error(f"Fehler beim Laden der Session-Statistiken: {str(e)}")
flash('Fehler beim Laden der Statistiken!', 'error')
return redirect(url_for('sessions.sessions'))
finally:
cur.close()
conn.close()