import os import sys import time import json import logging import requests import re import random import base64 from io import BytesIO from datetime import datetime, timedelta from zoneinfo import ZoneInfo from pathlib import Path # Add current directory to Python path to ensure modules can be imported sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from flask import Flask, render_template, request, redirect, session, url_for, send_file, jsonify, flash from flask_session import Session from werkzeug.middleware.proxy_fix import ProxyFix from apscheduler.schedulers.background import BackgroundScheduler import pandas as pd from psycopg2.extras import Json # Import our new modules import config from db import get_connection, get_db_connection, get_db_cursor, execute_query 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 utils.license import generate_license_key, validate_license_key from utils.backup import create_backup, restore_backup, get_or_create_encryption_key from utils.export import ( create_excel_export, format_datetime_for_export, prepare_license_export_data, prepare_customer_export_data, prepare_session_export_data, prepare_audit_export_data ) from models import get_user_by_username app = Flask(__name__) # Load configuration from config module app.config['SECRET_KEY'] = config.SECRET_KEY app.config['SESSION_TYPE'] = config.SESSION_TYPE app.config['JSON_AS_ASCII'] = config.JSON_AS_ASCII app.config['JSONIFY_MIMETYPE'] = config.JSONIFY_MIMETYPE app.config['PERMANENT_SESSION_LIFETIME'] = config.PERMANENT_SESSION_LIFETIME app.config['SESSION_COOKIE_HTTPONLY'] = config.SESSION_COOKIE_HTTPONLY app.config['SESSION_COOKIE_SECURE'] = config.SESSION_COOKIE_SECURE app.config['SESSION_COOKIE_SAMESITE'] = config.SESSION_COOKIE_SAMESITE app.config['SESSION_COOKIE_NAME'] = config.SESSION_COOKIE_NAME app.config['SESSION_REFRESH_EACH_REQUEST'] = config.SESSION_REFRESH_EACH_REQUEST Session(app) # ProxyFix für korrekte IP-Adressen hinter Nginx app.wsgi_app = ProxyFix( app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1 ) # Configuration is now loaded from config module # Scheduler für automatische Backups scheduler = BackgroundScheduler() scheduler.start() # Logging konfigurieren logging.basicConfig(level=logging.INFO) # Import and register blueprints from routes.auth_routes import auth_bp from routes.admin_routes import admin_bp from routes.license_routes import license_bp from routes.customer_routes import customer_bp from routes.resource_routes import resource_bp from routes.session_routes import session_bp from routes.batch_routes import batch_bp from routes.api_routes import api_bp from routes.export_routes import export_bp # Register blueprints app.register_blueprint(auth_bp) app.register_blueprint(admin_bp) app.register_blueprint(license_bp) app.register_blueprint(customer_bp) app.register_blueprint(resource_bp) app.register_blueprint(session_bp) app.register_blueprint(batch_bp) app.register_blueprint(api_bp) app.register_blueprint(export_bp) # Scheduled Backup Job def scheduled_backup(): """Führt ein geplantes Backup aus""" logging.info("Starte geplantes Backup...") create_backup(backup_type="scheduled", created_by="scheduler") # Scheduler konfigurieren - täglich um 3:00 Uhr scheduler.add_job( scheduled_backup, 'cron', hour=config.SCHEDULER_CONFIG['backup_hour'], minute=config.SCHEDULER_CONFIG['backup_minute'], id='daily_backup', replace_existing=True ) def verify_recaptcha(response): """Verifiziert die reCAPTCHA v2 Response mit Google""" secret_key = config.RECAPTCHA_SECRET_KEY # Wenn kein Secret Key konfiguriert ist, CAPTCHA als bestanden werten (für PoC) if not secret_key: logging.warning("RECAPTCHA_SECRET_KEY nicht konfiguriert - CAPTCHA wird übersprungen") return True # Verifizierung bei Google try: verify_url = 'https://www.google.com/recaptcha/api/siteverify' data = { 'secret': secret_key, 'response': response } # Timeout für Request setzen r = requests.post(verify_url, data=data, timeout=5) result = r.json() # Log für Debugging if not result.get('success'): logging.warning(f"reCAPTCHA Validierung fehlgeschlagen: {result.get('error-codes', [])}") return result.get('success', False) except requests.exceptions.RequestException as e: logging.error(f"reCAPTCHA Verifizierung fehlgeschlagen: {str(e)}") # Bei Netzwerkfehlern CAPTCHA als bestanden werten return True except Exception as e: logging.error(f"Unerwarteter Fehler bei reCAPTCHA: {str(e)}") return False # @app.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('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('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) # @app.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('login')) # @app.route("/verify-2fa", methods=["GET", "POST"]) # def verify_2fa(): # if not session.get('awaiting_2fa'): # return redirect(url_for('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('login')) # user = get_user_by_username(username) # if not user: # flash('User not found.', 'error') # return redirect(url_for('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) # conn = get_connection() # cur = conn.cursor() # cur.execute("UPDATE users SET backup_codes = %s WHERE id = %s", # (json.dumps(backup_codes), user_id)) # conn.commit() # cur.close() # conn.close() # 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('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('dashboard')) # Failed verification # conn = get_connection() # cur = conn.cursor() # cur.execute("UPDATE users SET failed_2fa_attempts = failed_2fa_attempts + 1, last_failed_2fa = %s WHERE id = %s", # (datetime.now(), user_id)) # conn.commit() # cur.close() # conn.close() # 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') # @app.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('dashboard')) return render_template('profile.html', user=user) # @app.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('profile')) # Check new password if new_password != confirm_password: flash('New passwords do not match.', 'error') return redirect(url_for('profile')) if len(new_password) < 8: flash('Password must be at least 8 characters long.', 'error') return redirect(url_for('profile')) # Update password new_hash = hash_password(new_password) conn = get_connection() cur = conn.cursor() cur.execute("UPDATE users SET password_hash = %s, last_password_change = %s WHERE id = %s", (new_hash, datetime.now(), user['id'])) conn.commit() cur.close() conn.close() log_audit('PASSWORD_CHANGE', 'user', entity_id=user['id'], additional_info="Password changed successfully") flash('Password changed successfully.', 'success') return redirect(url_for('profile')) # @app.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('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) # @app.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('setup_2fa')) # Verify the token if not verify_totp(totp_secret, token): flash('Invalid authentication code. Please try again.', 'error') return redirect(url_for('setup_2fa')) # Generate backup codes backup_codes = generate_backup_codes() hashed_codes = [hash_backup_code(code) for code in backup_codes] # Enable 2FA conn = get_connection() cur = conn.cursor() cur.execute(""" UPDATE users SET totp_secret = %s, totp_enabled = TRUE, backup_codes = %s WHERE username = %s """, (totp_secret, json.dumps(hashed_codes), session['username'])) conn.commit() cur.close() conn.close() session.pop('temp_totp_secret', None) log_audit('2FA_ENABLED', 'user', additional_info="2FA enabled successfully") # Show backup codes return render_template('backup_codes.html', backup_codes=backup_codes) # @app.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.', 'error') return redirect(url_for('profile')) # Disable 2FA conn = get_connection() cur = conn.cursor() cur.execute(""" UPDATE users SET totp_enabled = FALSE, totp_secret = NULL, backup_codes = NULL WHERE username = %s """, (session['username'],)) conn.commit() cur.close() conn.close() log_audit('2FA_DISABLED', 'user', additional_info="2FA disabled") flash('2FA has been disabled for your account.', 'success') return redirect(url_for('profile')) # @app.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') }) # @app.route("/api/generate-license-key", methods=['POST']) # @login_required def api_generate_key(): """API Endpoint zur Generierung eines neuen Lizenzschlüssels""" try: # Lizenztyp aus Request holen (default: full) data = request.get_json() or {} license_type = data.get('type', 'full') # Key generieren key = generate_license_key(license_type) # Prüfen ob Key bereits existiert (sehr unwahrscheinlich aber sicher ist sicher) conn = get_connection() cur = conn.cursor() # Wiederhole bis eindeutiger Key gefunden attempts = 0 while attempts < 10: # Max 10 Versuche cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (key,)) if not cur.fetchone(): break # Key ist eindeutig key = generate_license_key(license_type) attempts += 1 cur.close() conn.close() # Log für Audit log_audit('GENERATE_KEY', 'license', additional_info={'type': license_type, 'key': key}) return jsonify({ 'success': True, 'key': key, 'type': license_type }) except Exception as e: logging.error(f"Fehler bei Key-Generierung: {str(e)}") return jsonify({ 'success': False, 'error': 'Fehler bei der Key-Generierung' }), 500 # @app.route("/api/customers", methods=['GET']) # @login_required def api_customers(): """API Endpoint für die Kundensuche mit Select2""" try: # Suchparameter search = request.args.get('q', '').strip() page = request.args.get('page', 1, type=int) per_page = 20 customer_id = request.args.get('id', type=int) conn = get_connection() cur = conn.cursor() # Einzelnen Kunden per ID abrufen if customer_id: cur.execute(""" SELECT c.id, c.name, c.email, COUNT(l.id) as license_count FROM customers c LEFT JOIN licenses l ON c.id = l.customer_id WHERE c.id = %s GROUP BY c.id, c.name, c.email """, (customer_id,)) customer = cur.fetchone() results = [] if customer: results.append({ 'id': customer[0], 'text': f"{customer[1]} ({customer[2]})", 'name': customer[1], 'email': customer[2], 'license_count': customer[3] }) cur.close() conn.close() return jsonify({ 'results': results, 'pagination': {'more': False} }) # SQL Query mit optionaler Suche elif search: cur.execute(""" SELECT c.id, c.name, c.email, COUNT(l.id) as license_count FROM customers c LEFT JOIN licenses l ON c.id = l.customer_id WHERE LOWER(c.name) LIKE LOWER(%s) OR LOWER(c.email) LIKE LOWER(%s) GROUP BY c.id, c.name, c.email ORDER BY c.name LIMIT %s OFFSET %s """, (f'%{search}%', f'%{search}%', per_page, (page - 1) * per_page)) else: cur.execute(""" SELECT c.id, c.name, c.email, COUNT(l.id) as license_count FROM customers c LEFT JOIN licenses l ON c.id = l.customer_id GROUP BY c.id, c.name, c.email ORDER BY c.name LIMIT %s OFFSET %s """, (per_page, (page - 1) * per_page)) customers = cur.fetchall() # Format für Select2 results = [] for customer in customers: results.append({ 'id': customer[0], 'text': f"{customer[1]} - {customer[2]} ({customer[3]} Lizenzen)", 'name': customer[1], 'email': customer[2], 'license_count': customer[3] }) # Gesamtanzahl für Pagination if search: cur.execute(""" SELECT COUNT(*) FROM customers WHERE LOWER(name) LIKE LOWER(%s) OR LOWER(email) LIKE LOWER(%s) """, (f'%{search}%', f'%{search}%')) else: cur.execute("SELECT COUNT(*) FROM customers") total_count = cur.fetchone()[0] cur.close() conn.close() # Select2 Response Format return jsonify({ 'results': results, 'pagination': { 'more': (page * per_page) < total_count } }) except Exception as e: logging.error(f"Fehler bei Kundensuche: {str(e)}") return jsonify({ 'results': [], 'error': 'Fehler bei der Kundensuche' }), 500 # @app.route("/") # @login_required def dashboard(): conn = get_connection() cur = conn.cursor() # Statistiken abrufen # Gesamtanzahl Kunden (ohne Testdaten) cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = FALSE") total_customers = cur.fetchone()[0] # Gesamtanzahl Lizenzen (ohne Testdaten) cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = FALSE") total_licenses = cur.fetchone()[0] # Aktive Lizenzen (nicht abgelaufen und is_active = true, ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses WHERE valid_until >= CURRENT_DATE AND is_active = TRUE AND is_test = FALSE """) active_licenses = cur.fetchone()[0] # Aktive Sessions cur.execute("SELECT COUNT(*) FROM sessions WHERE is_active = TRUE") active_sessions_count = cur.fetchone()[0] # Abgelaufene Lizenzen (ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses WHERE valid_until < CURRENT_DATE AND is_test = FALSE """) expired_licenses = cur.fetchone()[0] # Deaktivierte Lizenzen (ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses WHERE is_active = FALSE AND is_test = FALSE """) inactive_licenses = cur.fetchone()[0] # Lizenzen die in den nächsten 30 Tagen ablaufen (ohne Testdaten) cur.execute(""" SELECT COUNT(*) FROM licenses WHERE valid_until >= CURRENT_DATE AND valid_until < CURRENT_DATE + INTERVAL '30 days' AND is_active = TRUE AND is_test = FALSE """) expiring_soon = cur.fetchone()[0] # Testlizenzen vs Vollversionen (ohne Testdaten) cur.execute(""" SELECT license_type, COUNT(*) FROM licenses WHERE is_test = FALSE GROUP BY license_type """) license_types = dict(cur.fetchall()) # Anzahl Testdaten cur.execute("SELECT COUNT(*) FROM licenses WHERE is_test = TRUE") test_data_count = cur.fetchone()[0] # Anzahl Test-Kunden cur.execute("SELECT COUNT(*) FROM customers WHERE is_test = TRUE") test_customers_count = cur.fetchone()[0] # Anzahl Test-Ressourcen cur.execute("SELECT COUNT(*) FROM resource_pools WHERE is_test = TRUE") test_resources_count = cur.fetchone()[0] # Letzte 5 erstellten Lizenzen (ohne Testdaten) cur.execute(""" SELECT l.id, l.license_key, c.name, l.valid_until, CASE WHEN l.is_active = FALSE THEN 'deaktiviert' WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen' WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab' ELSE 'aktiv' END as status FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.is_test = FALSE ORDER BY l.id DESC LIMIT 5 """) recent_licenses = cur.fetchall() # Bald ablaufende Lizenzen (nächste 30 Tage, ohne Testdaten) cur.execute(""" SELECT l.id, l.license_key, c.name, l.valid_until, l.valid_until - CURRENT_DATE as days_left FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.valid_until >= CURRENT_DATE AND l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.is_active = TRUE AND l.is_test = FALSE ORDER BY l.valid_until LIMIT 10 """) expiring_licenses = cur.fetchall() # Letztes Backup cur.execute(""" SELECT created_at, filesize, duration_seconds, backup_type, status FROM backup_history ORDER BY created_at DESC LIMIT 1 """) last_backup_info = cur.fetchone() # Sicherheitsstatistiken # Gesperrte IPs cur.execute(""" SELECT COUNT(*) FROM login_attempts WHERE blocked_until IS NOT NULL AND blocked_until > CURRENT_TIMESTAMP """) blocked_ips_count = cur.fetchone()[0] # Fehlversuche heute cur.execute(""" SELECT COALESCE(SUM(attempt_count), 0) FROM login_attempts WHERE last_attempt::date = CURRENT_DATE """) failed_attempts_today = cur.fetchone()[0] # Letzte 5 Sicherheitsereignisse cur.execute(""" SELECT la.ip_address, la.attempt_count, la.last_attempt, la.blocked_until, la.last_username_tried, la.last_error_message FROM login_attempts la ORDER BY la.last_attempt DESC LIMIT 5 """) recent_security_events = [] for event in cur.fetchall(): recent_security_events.append({ 'ip_address': event[0], 'attempt_count': event[1], 'last_attempt': event[2].strftime('%d.%m %H:%M'), 'blocked_until': event[3].strftime('%d.%m %H:%M') if event[3] and event[3] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None) else None, 'username_tried': event[4], 'error_message': event[5] }) # Sicherheitslevel berechnen if blocked_ips_count > 5 or failed_attempts_today > 50: security_level = 'danger' security_level_text = 'KRITISCH' elif blocked_ips_count > 2 or failed_attempts_today > 20: security_level = 'warning' security_level_text = 'ERHÖHT' else: security_level = 'success' security_level_text = 'NORMAL' # Resource Pool Statistiken (nur Live-Daten, keine Testdaten) cur.execute(""" SELECT resource_type, COUNT(*) FILTER (WHERE status = 'available') as available, COUNT(*) FILTER (WHERE status = 'allocated') as allocated, COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine, COUNT(*) as total FROM resource_pools WHERE is_test = FALSE GROUP BY resource_type """) resource_stats = {} resource_warning = None for row in cur.fetchall(): available_percent = round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1) resource_stats[row[0]] = { 'available': row[1], 'allocated': row[2], 'quarantine': row[3], 'total': row[4], 'available_percent': available_percent, 'warning_level': 'danger' if row[1] < 50 else 'warning' if row[1] < 100 else 'success' } # Warnung bei niedrigem Bestand if row[1] < 50: if not resource_warning: resource_warning = f"Niedriger Bestand bei {row[0].upper()}: nur noch {row[1]} verfügbar!" else: resource_warning += f" | {row[0].upper()}: {row[1]}" cur.close() conn.close() stats = { 'total_customers': total_customers, 'total_licenses': total_licenses, 'active_licenses': active_licenses, 'expired_licenses': expired_licenses, 'inactive_licenses': inactive_licenses, 'expiring_soon': expiring_soon, 'full_licenses': license_types.get('full', 0), 'test_licenses': license_types.get('test', 0), 'test_data_count': test_data_count, 'test_customers_count': test_customers_count, 'test_resources_count': test_resources_count, 'recent_licenses': recent_licenses, 'expiring_licenses': expiring_licenses, 'active_sessions': active_sessions_count, 'last_backup': last_backup_info, # Sicherheitsstatistiken 'blocked_ips_count': blocked_ips_count, 'failed_attempts_today': failed_attempts_today, 'recent_security_events': recent_security_events, 'security_level': security_level, 'security_level_text': security_level_text, 'resource_stats': resource_stats } return render_template("dashboard.html", stats=stats, resource_stats=resource_stats, resource_warning=resource_warning, username=session.get('username')) # @app.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_test = request.form.get("is_test") == "on" # Checkbox value # Berechne valid_until basierend auf Laufzeit duration = int(request.form.get("duration", 1)) duration_type = request.form.get("duration_type", "years") from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta 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('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('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('create_license')) # Kunde einfügen (erbt Test-Status von Lizenz) cur.execute(""" INSERT INTO customers (name, email, is_test, created_at) VALUES (%s, %s, %s, NOW()) RETURNING id """, (name, email, is_test)) customer_id = cur.fetchone()[0] customer_info = {'name': name, 'email': email, 'is_test': is_test} # Audit-Log für neuen Kunden log_audit('CREATE', 'customer', customer_id, new_values={'name': name, 'email': email, 'is_test': is_test}) else: # Bestehender Kunde - hole Infos für Audit-Log cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,)) customer_data = cur.fetchone() if not customer_data: flash('Kunde nicht gefunden!', 'error') return redirect(url_for('create_license')) customer_info = {'name': customer_data[0], 'email': customer_data[1]} # Wenn Kunde Test-Kunde ist, Lizenz auch als Test markieren if customer_data[2]: # is_test des Kunden is_test = True # 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_test) 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_test)) 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_test = %s) as domains, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones """, (is_test, is_test, is_test)) 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_test = %s LIMIT %s FOR UPDATE """, (is_test, 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_test = %s LIMIT %s FOR UPDATE """, (is_test, 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_test = %s LIMIT %s FOR UPDATE """, (is_test, 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('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_test': is_test }) 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 = "/create" 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) # @app.route("/batch", methods=["GET", "POST"]) # @login_required def batch_licenses(): """Batch-Generierung mehrerer Lizenzen für einen Kunden""" if request.method == "POST": # Formulardaten customer_id = request.form.get("customer_id") license_type = request.form["license_type"] quantity = int(request.form["quantity"]) valid_from = request.form["valid_from"] is_test = request.form.get("is_test") == "on" # Checkbox value # Berechne valid_until basierend auf Laufzeit duration = int(request.form.get("duration", 1)) duration_type = request.form.get("duration_type", "years") from datetime import datetime, timedelta from dateutil.relativedelta import relativedelta 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") # 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)) # Sicherheitslimit if quantity < 1 or quantity > 100: flash('Anzahl muss zwischen 1 und 100 liegen!', 'error') return redirect(url_for('batch_licenses')) 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('batch_licenses')) # 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('batch_licenses')) # Kunde einfügen (erbt Test-Status von Lizenz) cur.execute(""" INSERT INTO customers (name, email, is_test, created_at) VALUES (%s, %s, %s, NOW()) RETURNING id """, (name, email, is_test)) customer_id = cur.fetchone()[0] # Audit-Log für neuen Kunden log_audit('CREATE', 'customer', customer_id, new_values={'name': name, 'email': email, 'is_test': is_test}) else: # Bestehender Kunde - hole Infos cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,)) customer_data = cur.fetchone() if not customer_data: flash('Kunde nicht gefunden!', 'error') return redirect(url_for('batch_licenses')) name = customer_data[0] email = customer_data[1] # Wenn Kunde Test-Kunde ist, Lizenzen auch als Test markieren if customer_data[2]: # is_test des Kunden is_test = True # Prüfe Ressourcen-Verfügbarkeit für gesamten Batch total_domains_needed = domain_count * quantity total_ipv4s_needed = ipv4_count * quantity total_phones_needed = phone_count * quantity cur.execute(""" SELECT (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s) as domains, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s) as ipv4s, (SELECT COUNT(*) FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s) as phones """, (is_test, is_test, is_test)) available = cur.fetchone() if available[0] < total_domains_needed: flash(f"Nicht genügend Domains verfügbar (benötigt: {total_domains_needed}, verfügbar: {available[0]})", 'error') return redirect(url_for('batch_licenses')) if available[1] < total_ipv4s_needed: flash(f"Nicht genügend IPv4-Adressen verfügbar (benötigt: {total_ipv4s_needed}, verfügbar: {available[1]})", 'error') return redirect(url_for('batch_licenses')) if available[2] < total_phones_needed: flash(f"Nicht genügend Telefonnummern verfügbar (benötigt: {total_phones_needed}, verfügbar: {available[2]})", 'error') return redirect(url_for('batch_licenses')) # Lizenzen generieren und speichern generated_licenses = [] for i in range(quantity): # Eindeutigen Key generieren attempts = 0 while attempts < 10: license_key = generate_license_key(license_type) cur.execute("SELECT 1 FROM licenses WHERE license_key = %s", (license_key,)) if not cur.fetchone(): break attempts += 1 # Lizenz einfügen cur.execute(""" INSERT INTO licenses (license_key, customer_id, license_type, is_test, valid_from, valid_until, is_active, domain_count, ipv4_count, phone_count, device_limit) VALUES (%s, %s, %s, %s, %s, %s, true, %s, %s, %s, %s) RETURNING id """, (license_key, customer_id, license_type, is_test, valid_from, valid_until, domain_count, ipv4_count, phone_count, device_limit)) license_id = cur.fetchone()[0] # Ressourcen für diese Lizenz zuweisen # Domains if domain_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE """, (is_test, 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 if ipv4_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE """, (is_test, 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 if phone_count > 0: cur.execute(""" SELECT id FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' AND is_test = %s LIMIT %s FOR UPDATE """, (is_test, 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())) generated_licenses.append({ 'id': license_id, 'key': license_key, 'type': license_type }) conn.commit() # Audit-Log log_audit('CREATE_BATCH', 'license', new_values={'customer': name, 'quantity': quantity, 'type': license_type}, additional_info=f"Batch-Generierung von {quantity} Lizenzen") # Session für Export speichern session['batch_export'] = { 'customer': name, 'email': email, 'licenses': generated_licenses, 'valid_from': valid_from, 'valid_until': valid_until, 'timestamp': datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None).isoformat() } flash(f'{quantity} Lizenzen erfolgreich generiert!', 'success') return render_template("batch_result.html", customer=name, email=email, licenses=generated_licenses, valid_from=valid_from, valid_until=valid_until) except Exception as e: conn.rollback() logging.error(f"Fehler bei Batch-Generierung: {str(e)}") flash('Fehler bei der Batch-Generierung!', 'error') return redirect(url_for('batch_licenses')) finally: cur.close() conn.close() # GET Request return render_template("batch_form.html") # @app.route("/batch/export") # @login_required def export_batch(): """Exportiert die zuletzt generierten Batch-Lizenzen""" batch_data = session.get('batch_export') if not batch_data: flash('Keine Batch-Daten zum Exportieren vorhanden!', 'error') return redirect(url_for('batch_licenses')) # CSV generieren output = io.StringIO() output.write('\ufeff') # UTF-8 BOM für Excel # Header output.write(f"Kunde: {batch_data['customer']}\n") output.write(f"E-Mail: {batch_data['email']}\n") output.write(f"Generiert am: {datetime.fromisoformat(batch_data['timestamp']).strftime('%d.%m.%Y %H:%M')}\n") output.write(f"Gültig von: {batch_data['valid_from']} bis {batch_data['valid_until']}\n") output.write("\n") output.write("Nr;Lizenzschlüssel;Typ\n") # Lizenzen for i, license in enumerate(batch_data['licenses'], 1): typ_text = "Vollversion" if license['type'] == 'full' else "Testversion" output.write(f"{i};{license['key']};{typ_text}\n") output.seek(0) # Audit-Log log_audit('EXPORT', 'batch_licenses', additional_info=f"Export von {len(batch_data['licenses'])} Batch-Lizenzen") return send_file( io.BytesIO(output.getvalue().encode('utf-8-sig')), mimetype='text/csv', as_attachment=True, download_name=f"batch_licenses_{batch_data['customer'].replace(' ', '_')}_{datetime.now(ZoneInfo('Europe/Berlin')).strftime('%Y%m%d_%H%M%S')}.csv" ) # @app.route("/licenses") # @login_required def licenses(): # Redirect zur kombinierten Ansicht return redirect("/customers-licenses") # @app.route("/license/edit/", methods=["GET", "POST"]) # @login_required def edit_license(license_id): conn = get_connection() cur = conn.cursor() if request.method == "POST": # Alte Werte für Audit-Log abrufen cur.execute(""" SELECT license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit FROM licenses WHERE id = %s """, (license_id,)) old_license = cur.fetchone() # Update license 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 = request.form.get("is_active") == "on" is_test = request.form.get("is_test") == "on" 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, is_test = %s, device_limit = %s WHERE id = %s """, (license_key, license_type, valid_from, valid_until, is_active, is_test, device_limit, license_id)) conn.commit() # Audit-Log log_audit('UPDATE', 'license', license_id, old_values={ 'license_key': old_license[0], 'license_type': old_license[1], 'valid_from': str(old_license[2]), 'valid_until': str(old_license[3]), 'is_active': old_license[4], 'is_test': old_license[5], 'device_limit': old_license[6] }, new_values={ 'license_key': license_key, 'license_type': license_type, 'valid_from': valid_from, 'valid_until': valid_until, 'is_active': is_active, 'is_test': is_test, 'device_limit': device_limit }) cur.close() conn.close() # Redirect zurück zu customers-licenses mit beibehaltenen Parametern redirect_url = "/customers-licenses" # Behalte show_test Parameter bei (aus Form oder GET-Parameter) show_test = request.form.get('show_test') or request.args.get('show_test') if show_test == 'true': redirect_url += "?show_test=true" # Behalte customer_id bei wenn vorhanden if request.referrer and 'customer_id=' in request.referrer: import re match = re.search(r'customer_id=(\d+)', request.referrer) if match: connector = "&" if "?" in redirect_url else "?" redirect_url += f"{connector}customer_id={match.group(1)}" return redirect(redirect_url) # Get license data cur.execute(""" SELECT l.id, l.license_key, c.name, c.email, l.license_type, l.valid_from, l.valid_until, l.is_active, c.id, l.is_test, l.device_limit FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.id = %s """, (license_id,)) license = cur.fetchone() cur.close() conn.close() if not license: return redirect("/licenses") return render_template("edit_license.html", license=license, username=session.get('username')) # @app.route("/license/delete/", methods=["POST"]) # @login_required def delete_license(license_id): conn = get_connection() cur = conn.cursor() # Lizenzdetails für Audit-Log abrufen cur.execute(""" SELECT l.license_key, c.name, l.license_type FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE l.id = %s """, (license_id,)) license_info = cur.fetchone() cur.execute("DELETE FROM licenses WHERE id = %s", (license_id,)) conn.commit() # Audit-Log if license_info: log_audit('DELETE', 'license', license_id, old_values={ 'license_key': license_info[0], 'customer_name': license_info[1], 'license_type': license_info[2] }) cur.close() conn.close() return redirect("/licenses") # @app.route("/customers") # @login_required def customers(): # Redirect zur kombinierten Ansicht return redirect("/customers-licenses") # @app.route("/customer/edit/", methods=["GET", "POST"]) # @login_required def edit_customer(customer_id): conn = get_connection() cur = conn.cursor() if request.method == "POST": # Alte Werte für Audit-Log abrufen cur.execute("SELECT name, email, is_test FROM customers WHERE id = %s", (customer_id,)) old_customer = cur.fetchone() # Update customer name = request.form["name"] email = request.form["email"] is_test = request.form.get("is_test") == "on" cur.execute(""" UPDATE customers SET name = %s, email = %s, is_test = %s WHERE id = %s """, (name, email, is_test, customer_id)) conn.commit() # Audit-Log log_audit('UPDATE', 'customer', customer_id, old_values={ 'name': old_customer[0], 'email': old_customer[1], 'is_test': old_customer[2] }, new_values={ 'name': name, 'email': email, 'is_test': is_test }) cur.close() conn.close() # Redirect zurück zu customers-licenses mit beibehaltenen Parametern redirect_url = "/customers-licenses" # Behalte show_test Parameter bei (aus Form oder GET-Parameter) show_test = request.form.get('show_test') or request.args.get('show_test') if show_test == 'true': redirect_url += "?show_test=true" # Behalte customer_id bei (immer der aktuelle Kunde) connector = "&" if "?" in redirect_url else "?" redirect_url += f"{connector}customer_id={customer_id}" return redirect(redirect_url) # Get customer data with licenses cur.execute(""" SELECT id, name, email, created_at, is_test FROM customers WHERE id = %s """, (customer_id,)) customer = cur.fetchone() if not customer: cur.close() conn.close() return "Kunde nicht gefunden", 404 # Get customer's licenses cur.execute(""" SELECT id, license_key, license_type, valid_from, valid_until, is_active FROM licenses WHERE customer_id = %s ORDER BY valid_until DESC """, (customer_id,)) licenses = cur.fetchall() cur.close() conn.close() if not customer: return redirect("/customers-licenses") return render_template("edit_customer.html", customer=customer, licenses=licenses, username=session.get('username')) # @app.route("/customer/create", methods=["GET", "POST"]) # @login_required def create_customer(): """Erstellt einen neuen Kunden ohne Lizenz""" if request.method == "POST": name = request.form.get('name') email = request.form.get('email') is_test = request.form.get('is_test') == 'on' if not name or not email: flash("Name und E-Mail sind Pflichtfelder!", "error") return render_template("create_customer.html", username=session.get('username')) conn = get_connection() cur = conn.cursor() try: # Prüfen ob E-Mail bereits existiert cur.execute("SELECT id, name FROM customers WHERE email = %s", (email,)) existing = cur.fetchone() if existing: flash(f"Ein Kunde mit der E-Mail '{email}' existiert bereits: {existing[1]}", "error") return render_template("create_customer.html", username=session.get('username')) # Kunde erstellen cur.execute(""" INSERT INTO customers (name, email, created_at, is_test) VALUES (%s, %s, %s, %s) RETURNING id """, (name, email, datetime.now(), is_test)) customer_id = cur.fetchone()[0] conn.commit() # Audit-Log log_audit('CREATE', 'customer', customer_id, new_values={ 'name': name, 'email': email, 'is_test': is_test }) flash(f"Kunde '{name}' wurde erfolgreich angelegt!", "success") return redirect(f"/customer/edit/{customer_id}") except Exception as e: conn.rollback() flash(f"Fehler beim Anlegen des Kunden: {str(e)}", "error") return render_template("create_customer.html", username=session.get('username')) finally: cur.close() conn.close() # GET Request - Formular anzeigen return render_template("create_customer.html", username=session.get('username')) # @app.route("/customer/delete/", methods=["POST"]) # @login_required def delete_customer(customer_id): conn = get_connection() cur = conn.cursor() # Prüfen ob Kunde Lizenzen hat cur.execute("SELECT COUNT(*) FROM licenses WHERE customer_id = %s", (customer_id,)) license_count = cur.fetchone()[0] if license_count > 0: # Kunde hat Lizenzen - nicht löschen cur.close() conn.close() return redirect("/customers") # Kundendetails für Audit-Log abrufen cur.execute("SELECT name, email FROM customers WHERE id = %s", (customer_id,)) customer_info = cur.fetchone() # Kunde löschen wenn keine Lizenzen vorhanden cur.execute("DELETE FROM customers WHERE id = %s", (customer_id,)) conn.commit() # Audit-Log if customer_info: log_audit('DELETE', 'customer', customer_id, old_values={ 'name': customer_info[0], 'email': customer_info[1] }) cur.close() conn.close() return redirect("/customers") # @app.route("/customers-licenses") # @login_required def customers_licenses(): """Kombinierte Ansicht für Kunden und deren Lizenzen""" conn = get_connection() cur = conn.cursor() # Hole alle Kunden mit Lizenzstatistiken (inkl. Testkunden wenn gewünscht) show_test = request.args.get('show_test', 'false').lower() == 'true' query = """ SELECT c.id, c.name, c.email, c.created_at, COUNT(l.id) as total_licenses, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE 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 """ if not show_test: query += " WHERE c.is_test = FALSE" query += """ GROUP BY c.id, c.name, c.email, c.created_at ORDER BY c.name """ cur.execute(query) customers = cur.fetchall() # Hole ausgewählten Kunden nur wenn explizit in URL angegeben selected_customer_id = request.args.get('customer_id', type=int) licenses = [] selected_customer = None if customers and selected_customer_id: # Hole Daten des ausgewählten Kunden for customer in customers: if customer[0] == selected_customer_id: selected_customer = customer break # Hole Lizenzen des ausgewählten Kunden if selected_customer: cur.execute(""" SELECT l.id, l.license_key, l.license_type, l.valid_from, l.valid_until, l.is_active, CASE WHEN l.is_active = FALSE THEN 'deaktiviert' WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen' WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab' ELSE 'aktiv' END as status, l.domain_count, l.ipv4_count, l.phone_count, l.device_limit, (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices, -- Actual resource counts (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count, (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count, (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_phone_count FROM licenses l WHERE l.customer_id = %s ORDER BY l.created_at DESC, l.id DESC """, (selected_customer_id,)) licenses = cur.fetchall() cur.close() conn.close() return render_template("customers_licenses.html", customers=customers, selected_customer=selected_customer, selected_customer_id=selected_customer_id, licenses=licenses, show_test=show_test) # @app.route("/api/customer//licenses") # @login_required def api_customer_licenses(customer_id): """API-Endpoint für AJAX-Abruf der Lizenzen eines Kunden""" conn = get_connection() cur = conn.cursor() # Hole Lizenzen des Kunden cur.execute(""" SELECT l.id, l.license_key, l.license_type, l.valid_from, l.valid_until, l.is_active, CASE WHEN l.is_active = FALSE THEN 'deaktiviert' WHEN l.valid_until < CURRENT_DATE THEN 'abgelaufen' WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'läuft bald ab' ELSE 'aktiv' END as status, l.domain_count, l.ipv4_count, l.phone_count, l.device_limit, (SELECT COUNT(*) FROM device_registrations WHERE license_id = l.id AND is_active = TRUE) as active_devices, -- Actual resource counts (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'domain') as actual_domain_count, (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'ipv4') as actual_ipv4_count, (SELECT COUNT(*) FROM license_resources lr JOIN resource_pools rp ON lr.resource_id = rp.id WHERE lr.license_id = l.id AND lr.is_active = true AND rp.resource_type = 'phone') as actual_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 cur.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 cur.fetchall(): resource_info = { 'id': res_row[0], 'value': res_row[2], 'assigned_at': res_row[3].strftime('%d.%m.%Y') if res_row[3] else '' } if res_row[1] == 'domain': resources['domains'].append(resource_info) elif res_row[1] == 'ipv4': resources['ipv4s'].append(resource_info) elif res_row[1] == 'phone': resources['phones'].append(resource_info) licenses.append({ 'id': row[0], 'license_key': row[1], 'license_type': row[2], 'valid_from': row[3].strftime('%d.%m.%Y') if row[3] else '', 'valid_until': row[4].strftime('%d.%m.%Y') if row[4] else '', 'is_active': row[5], 'status': row[6], 'domain_count': row[7], # limit 'ipv4_count': row[8], # limit 'phone_count': row[9], # limit 'device_limit': row[10], 'active_devices': row[11], 'actual_domain_count': row[12], # actual count 'actual_ipv4_count': row[13], # actual count 'actual_phone_count': row[14], # actual count 'resources': resources }) cur.close() conn.close() return jsonify({ 'success': True, 'licenses': licenses, 'count': len(licenses) }) # @app.route("/api/customer//quick-stats") # @login_required def api_customer_quick_stats(customer_id): """API-Endpoint für Schnellstatistiken eines Kunden""" conn = get_connection() cur = conn.cursor() # Hole Kundenstatistiken cur.execute(""" SELECT COUNT(l.id) as total_licenses, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE THEN 1 END) as active_licenses, COUNT(CASE WHEN l.valid_until < CURRENT_DATE THEN 1 END) as expired_licenses, COUNT(CASE WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' AND l.valid_until >= CURRENT_DATE THEN 1 END) as expiring_soon FROM licenses l WHERE l.customer_id = %s """, (customer_id,)) stats = cur.fetchone() cur.close() conn.close() return jsonify({ 'success': True, 'stats': { 'total': stats[0], 'active': stats[1], 'expired': stats[2], 'expiring_soon': stats[3] } }) # @app.route("/api/license//quick-edit", methods=['POST']) # @login_required def api_license_quick_edit(license_id): """API-Endpoint für schnelle Lizenz-Bearbeitung""" conn = get_connection() cur = conn.cursor() try: data = request.get_json() # Hole alte Werte für Audit-Log cur.execute(""" SELECT is_active, valid_until, license_type FROM licenses WHERE id = %s """, (license_id,)) old_values = cur.fetchone() if not old_values: return jsonify({'success': False, 'error': 'Lizenz nicht gefunden'}), 404 # Update-Felder vorbereiten updates = [] params = [] new_values = {} if 'is_active' in data: updates.append("is_active = %s") params.append(data['is_active']) new_values['is_active'] = data['is_active'] if 'valid_until' in data: updates.append("valid_until = %s") params.append(data['valid_until']) new_values['valid_until'] = data['valid_until'] if 'license_type' in data: updates.append("license_type = %s") params.append(data['license_type']) new_values['license_type'] = data['license_type'] if updates: params.append(license_id) cur.execute(f""" UPDATE licenses SET {', '.join(updates)} WHERE id = %s """, params) conn.commit() # Audit-Log log_audit('UPDATE', 'license', license_id, old_values={ 'is_active': old_values[0], 'valid_until': old_values[1].isoformat() if old_values[1] else None, 'license_type': old_values[2] }, new_values=new_values) cur.close() conn.close() return jsonify({'success': True}) except Exception as e: conn.rollback() cur.close() conn.close() return jsonify({'success': False, 'error': str(e)}), 500 # @app.route("/api/license//resources") # @login_required def api_license_resources(license_id): """API-Endpoint für detaillierte Ressourcen-Informationen einer Lizenz""" conn = get_connection() cur = conn.cursor() try: # Hole die konkreten zugewiesenen Ressourcen für diese Lizenz cur.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 row in cur.fetchall(): resource_info = { 'id': row[0], 'value': row[2], 'assigned_at': row[3].strftime('%d.%m.%Y') if row[3] else '' } if row[1] == 'domain': resources['domains'].append(resource_info) elif row[1] == 'ipv4': resources['ipv4s'].append(resource_info) elif row[1] == 'phone': resources['phones'].append(resource_info) cur.close() conn.close() return jsonify({ 'success': True, 'resources': resources }) except Exception as e: cur.close() conn.close() return jsonify({'success': False, 'error': str(e)}), 500 # @app.route("/sessions") # @login_required def sessions(): conn = get_connection() cur = conn.cursor() # Sortierparameter active_sort = request.args.get('active_sort', 'last_heartbeat') active_order = request.args.get('active_order', 'desc') ended_sort = request.args.get('ended_sort', 'ended_at') ended_order = request.args.get('ended_order', 'desc') # Whitelist für erlaubte Sortierfelder - Aktive Sessions active_sort_fields = { 'customer': 'c.name', 'license': 'l.license_key', 'ip': 's.ip_address', 'started': 's.started_at', 'last_heartbeat': 's.last_heartbeat', 'inactive': 'minutes_inactive' } # Whitelist für erlaubte Sortierfelder - Beendete Sessions ended_sort_fields = { 'customer': 'c.name', 'license': 'l.license_key', 'ip': 's.ip_address', 'started': 's.started_at', 'ended_at': 's.ended_at', 'duration': 'duration_minutes' } # Validierung if active_sort not in active_sort_fields: active_sort = 'last_heartbeat' if ended_sort not in ended_sort_fields: ended_sort = 'ended_at' if active_order not in ['asc', 'desc']: active_order = 'desc' if ended_order not in ['asc', 'desc']: ended_order = 'desc' # Aktive Sessions abrufen cur.execute(f""" 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 {active_sort_fields[active_sort]} {active_order.upper()} """) active_sessions = cur.fetchall() # Inaktive Sessions der letzten 24 Stunden cur.execute(f""" 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 {ended_sort_fields[ended_sort]} {ended_order.upper()} LIMIT 50 """) recent_sessions = cur.fetchall() cur.close() conn.close() return render_template("sessions.html", active_sessions=active_sessions, recent_sessions=recent_sessions, active_sort=active_sort, active_order=active_order, ended_sort=ended_sort, ended_order=ended_order, username=session.get('username')) # @app.route("/session/end/", methods=["POST"]) # @login_required def end_session(session_id): conn = get_connection() cur = conn.cursor() # Session beenden cur.execute(""" UPDATE sessions SET is_active = FALSE, ended_at = NOW() WHERE id = %s AND is_active = TRUE """, (session_id,)) conn.commit() cur.close() conn.close() return redirect("/sessions") # @app.route("/export/licenses") # @login_required def export_licenses(): conn = get_connection() cur = conn.cursor() # Alle Lizenzen mit Kundeninformationen abrufen (ohne Testdaten, außer explizit gewünscht) include_test = request.args.get('include_test', 'false').lower() == 'true' customer_id = request.args.get('customer_id', type=int) 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.is_test, CASE WHEN l.is_active = FALSE THEN 'Deaktiviert' WHEN l.valid_until < CURRENT_DATE THEN 'Abgelaufen' WHEN l.valid_until < CURRENT_DATE + INTERVAL '30 days' THEN 'Läuft bald ab' ELSE 'Aktiv' END as status FROM licenses l JOIN customers c ON l.customer_id = c.id """ # Build WHERE clause where_conditions = [] params = [] if not include_test: where_conditions.append("l.is_test = FALSE") if customer_id: where_conditions.append("l.customer_id = %s") params.append(customer_id) if where_conditions: query += " WHERE " + " AND ".join(where_conditions) query += " ORDER BY l.id" cur.execute(query, params) # Spaltennamen columns = ['ID', 'Lizenzschlüssel', 'Kunde', 'E-Mail', 'Typ', 'Gültig von', 'Gültig bis', 'Aktiv', 'Testdaten', 'Status'] # Daten in DataFrame data = cur.fetchall() df = pd.DataFrame(data, columns=columns) # Datumsformatierung df['Gültig von'] = pd.to_datetime(df['Gültig von']).dt.strftime('%d.%m.%Y') df['Gültig bis'] = pd.to_datetime(df['Gültig bis']).dt.strftime('%d.%m.%Y') # Typ und Aktiv Status anpassen df['Typ'] = df['Typ'].replace({'full': 'Vollversion', 'test': 'Testversion'}) df['Aktiv'] = df['Aktiv'].replace({True: 'Ja', False: 'Nein'}) df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'}) cur.close() conn.close() # Export Format export_format = request.args.get('format', 'excel') # Audit-Log log_audit('EXPORT', 'license', additional_info=f"Export aller Lizenzen als {export_format.upper()}") filename = f'lizenzen_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}' if export_format == 'csv': # CSV Export output = io.StringIO() df.to_csv(output, index=False, encoding='utf-8-sig', sep=';') output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8-sig')), mimetype='text/csv', as_attachment=True, download_name=f'{filename}.csv' ) else: # Excel Export output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='Lizenzen', index=False) # Formatierung worksheet = writer.sheets['Lizenzen'] for column in worksheet.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) worksheet.column_dimensions[column_letter].width = adjusted_width output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # @app.route("/export/audit") # @login_required def export_audit(): conn = get_connection() cur = conn.cursor() # Holen der Filter-Parameter filter_user = request.args.get('user', '') filter_action = request.args.get('action', '') filter_entity = request.args.get('entity', '') export_format = request.args.get('format', 'excel') # SQL Query mit Filtern query = """ SELECT id, timestamp, username, action, entity_type, entity_id, old_values, new_values, ip_address, user_agent, additional_info FROM audit_log WHERE 1=1 """ params = [] if filter_user: query += " AND username ILIKE %s" params.append(f'%{filter_user}%') if filter_action: query += " AND action = %s" params.append(filter_action) if filter_entity: query += " AND entity_type = %s" params.append(filter_entity) query += " ORDER BY timestamp DESC" cur.execute(query, params) audit_logs = cur.fetchall() cur.close() conn.close() # Daten für Export vorbereiten data = [] for log in audit_logs: action_text = { 'CREATE': 'Erstellt', 'UPDATE': 'Bearbeitet', 'DELETE': 'Gelöscht', 'LOGIN': 'Anmeldung', 'LOGOUT': 'Abmeldung', 'AUTO_LOGOUT': 'Auto-Logout', 'EXPORT': 'Export', 'GENERATE_KEY': 'Key generiert', 'CREATE_BATCH': 'Batch erstellt', 'BACKUP': 'Backup erstellt', 'LOGIN_2FA_SUCCESS': '2FA-Anmeldung', 'LOGIN_2FA_BACKUP': '2FA-Backup-Code', 'LOGIN_2FA_FAILED': '2FA-Fehlgeschlagen', 'LOGIN_BLOCKED': 'Login-Blockiert', 'RESTORE': 'Wiederhergestellt', 'PASSWORD_CHANGE': 'Passwort geändert', '2FA_ENABLED': '2FA aktiviert', '2FA_DISABLED': '2FA deaktiviert' }.get(log[3], log[3]) data.append({ 'ID': log[0], 'Zeitstempel': log[1].strftime('%d.%m.%Y %H:%M:%S'), 'Benutzer': log[2], 'Aktion': action_text, 'Entität': log[4], 'Entität-ID': log[5] or '', 'IP-Adresse': log[8] or '', 'Zusatzinfo': log[10] or '' }) # DataFrame erstellen df = pd.DataFrame(data) # Timestamp für Dateiname timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S') filename = f'audit_log_export_{timestamp}' # Audit Log für Export log_audit('EXPORT', 'audit_log', additional_info=f"{export_format.upper()} Export mit {len(data)} Einträgen") if export_format == 'csv': # CSV Export output = io.StringIO() # UTF-8 BOM für Excel output.write('\ufeff') df.to_csv(output, index=False, sep=';', encoding='utf-8') output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8')), mimetype='text/csv;charset=utf-8', as_attachment=True, download_name=f'{filename}.csv' ) else: # Excel Export output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='Audit Log') # Spaltenbreiten anpassen worksheet = writer.sheets['Audit Log'] for idx, col in enumerate(df.columns): max_length = max( df[col].astype(str).map(len).max(), len(col) ) + 2 worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50) output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # @app.route("/export/customers") # @login_required def export_customers(): conn = get_connection() cur = conn.cursor() # Check if test data should be included include_test = request.args.get('include_test', 'false').lower() == 'true' # Build query based on test data filter if include_test: # Include all customers query = """ SELECT c.id, c.name, c.email, c.created_at, c.is_test, COUNT(l.id) as total_licenses, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE 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 GROUP BY c.id, c.name, c.email, c.created_at, c.is_test ORDER BY c.id """ else: # Exclude test customers and test licenses query = """ SELECT c.id, c.name, c.email, c.created_at, c.is_test, COUNT(CASE WHEN l.is_test = FALSE THEN 1 END) as total_licenses, COUNT(CASE WHEN l.is_active = TRUE AND l.valid_until >= CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as active_licenses, COUNT(CASE WHEN l.valid_until < CURRENT_DATE AND l.is_test = FALSE THEN 1 END) as expired_licenses FROM customers c LEFT JOIN licenses l ON c.id = l.customer_id WHERE c.is_test = FALSE GROUP BY c.id, c.name, c.email, c.created_at, c.is_test ORDER BY c.id """ cur.execute(query) # Spaltennamen columns = ['ID', 'Name', 'E-Mail', 'Erstellt am', 'Testdaten', 'Lizenzen gesamt', 'Aktive Lizenzen', 'Abgelaufene Lizenzen'] # Daten in DataFrame data = cur.fetchall() df = pd.DataFrame(data, columns=columns) # Datumsformatierung df['Erstellt am'] = pd.to_datetime(df['Erstellt am']).dt.strftime('%d.%m.%Y %H:%M') # Testdaten formatting df['Testdaten'] = df['Testdaten'].replace({True: 'Ja', False: 'Nein'}) cur.close() conn.close() # Export Format export_format = request.args.get('format', 'excel') # Audit-Log log_audit('EXPORT', 'customer', additional_info=f"Export aller Kunden als {export_format.upper()}") filename = f'kunden_export_{datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y%m%d_%H%M%S")}' if export_format == 'csv': # CSV Export output = io.StringIO() df.to_csv(output, index=False, encoding='utf-8-sig', sep=';') output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8-sig')), mimetype='text/csv', as_attachment=True, download_name=f'{filename}.csv' ) else: # Excel Export output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='Kunden', index=False) # Formatierung worksheet = writer.sheets['Kunden'] for column in worksheet.columns: max_length = 0 column_letter = column[0].column_letter for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) worksheet.column_dimensions[column_letter].width = adjusted_width output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # @app.route("/export/sessions") # @login_required def export_sessions(): conn = get_connection() cur = conn.cursor() # Holen des Session-Typs (active oder ended) session_type = request.args.get('type', 'active') export_format = request.args.get('format', 'excel') # Daten je nach Typ abrufen if session_type == 'active': # Aktive Lizenz-Sessions cur.execute(""" SELECT s.id, l.license_key, c.name as customer_name, s.session_id, s.started_at, s.last_heartbeat, EXTRACT(EPOCH FROM (NOW() - s.started_at))::INT as duration_seconds, s.ip_address, s.user_agent 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 """) sessions = cur.fetchall() # Daten für Export vorbereiten data = [] for sess in sessions: duration = sess[6] hours = duration // 3600 minutes = (duration % 3600) // 60 seconds = duration % 60 data.append({ 'Session-ID': sess[0], 'Lizenzschlüssel': sess[1], 'Kunde': sess[2], 'Session-ID (Tech)': sess[3], 'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'), 'Letzte Aktivität': sess[5].strftime('%d.%m.%Y %H:%M:%S'), 'Dauer': f"{hours}h {minutes}m {seconds}s", 'IP-Adresse': sess[7], 'Browser': sess[8] }) sheet_name = 'Aktive Sessions' filename_prefix = 'aktive_sessions' else: # Beendete Lizenz-Sessions cur.execute(""" SELECT s.id, l.license_key, c.name as customer_name, s.session_id, s.started_at, s.ended_at, EXTRACT(EPOCH FROM (s.ended_at - s.started_at))::INT as duration_seconds, s.ip_address, s.user_agent 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 IS NOT NULL ORDER BY s.ended_at DESC LIMIT 1000 """) sessions = cur.fetchall() # Daten für Export vorbereiten data = [] for sess in sessions: duration = sess[6] if sess[6] else 0 hours = duration // 3600 minutes = (duration % 3600) // 60 seconds = duration % 60 data.append({ 'Session-ID': sess[0], 'Lizenzschlüssel': sess[1], 'Kunde': sess[2], 'Session-ID (Tech)': sess[3], 'Startzeit': sess[4].strftime('%d.%m.%Y %H:%M:%S'), 'Endzeit': sess[5].strftime('%d.%m.%Y %H:%M:%S') if sess[5] else '', 'Dauer': f"{hours}h {minutes}m {seconds}s", 'IP-Adresse': sess[7], 'Browser': sess[8] }) sheet_name = 'Beendete Sessions' filename_prefix = 'beendete_sessions' cur.close() conn.close() # DataFrame erstellen df = pd.DataFrame(data) # Timestamp für Dateiname timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S') filename = f'{filename_prefix}_export_{timestamp}' # Audit Log für Export log_audit('EXPORT', 'sessions', additional_info=f"{export_format.upper()} Export von {session_type} Sessions mit {len(data)} Einträgen") if export_format == 'csv': # CSV Export output = io.StringIO() # UTF-8 BOM für Excel output.write('\ufeff') df.to_csv(output, index=False, sep=';', encoding='utf-8') output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8')), mimetype='text/csv;charset=utf-8', as_attachment=True, download_name=f'{filename}.csv' ) else: # Excel Export output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name=sheet_name) # Spaltenbreiten anpassen worksheet = writer.sheets[sheet_name] for idx, col in enumerate(df.columns): max_length = max( df[col].astype(str).map(len).max(), len(col) ) + 2 worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50) output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # @app.route("/export/resources") # @login_required def export_resources(): conn = get_connection() cur = conn.cursor() # Holen der Filter-Parameter filter_type = request.args.get('type', '') filter_status = request.args.get('status', '') search_query = request.args.get('search', '') show_test = request.args.get('show_test', 'false').lower() == 'true' export_format = request.args.get('format', 'excel') # SQL Query mit Filtern query = """ SELECT r.id, r.resource_type, r.resource_value, r.status, r.allocated_to_license, r.created_at, r.status_changed_at, l.license_key, c.name as customer_name, c.email as customer_email, l.license_type FROM resource_pools r LEFT JOIN licenses l ON r.allocated_to_license = l.id LEFT JOIN customers c ON l.customer_id = c.id WHERE 1=1 """ params = [] # Filter für Testdaten if not show_test: query += " AND (r.is_test = false OR r.is_test IS NULL)" # Filter für Ressourcentyp if filter_type: query += " AND r.resource_type = %s" params.append(filter_type) # Filter für Status if filter_status: query += " AND r.status = %s" params.append(filter_status) # Suchfilter if search_query: query += " AND (r.resource_value ILIKE %s OR l.license_key ILIKE %s OR c.name ILIKE %s)" params.extend([f'%{search_query}%', f'%{search_query}%', f'%{search_query}%']) query += " ORDER BY r.id DESC" cur.execute(query, params) resources = cur.fetchall() cur.close() conn.close() # Daten für Export vorbereiten data = [] for res in resources: status_text = { 'available': 'Verfügbar', 'allocated': 'Zugewiesen', 'quarantine': 'Quarantäne' }.get(res[3], res[3]) type_text = { 'domain': 'Domain', 'ipv4': 'IPv4', 'phone': 'Telefon' }.get(res[1], res[1]) data.append({ 'ID': res[0], 'Typ': type_text, 'Ressource': res[2], 'Status': status_text, 'Lizenzschlüssel': res[7] or '', 'Kunde': res[8] or '', 'Kunden-Email': res[9] or '', 'Lizenztyp': res[10] or '', 'Erstellt am': res[5].strftime('%d.%m.%Y %H:%M:%S') if res[5] else '', 'Zugewiesen am': res[6].strftime('%d.%m.%Y %H:%M:%S') if res[6] else '' }) # DataFrame erstellen df = pd.DataFrame(data) # Timestamp für Dateiname timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S') filename = f'resources_export_{timestamp}' # Audit Log für Export log_audit('EXPORT', 'resources', additional_info=f"{export_format.upper()} Export mit {len(data)} Ressourcen") if export_format == 'csv': # CSV Export output = io.StringIO() # UTF-8 BOM für Excel output.write('\ufeff') df.to_csv(output, index=False, sep=';', encoding='utf-8') output.seek(0) return send_file( io.BytesIO(output.getvalue().encode('utf-8')), mimetype='text/csv;charset=utf-8', as_attachment=True, download_name=f'{filename}.csv' ) else: # Excel Export output = BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, index=False, sheet_name='Resources') # Spaltenbreiten anpassen worksheet = writer.sheets['Resources'] for idx, col in enumerate(df.columns): max_length = max( df[col].astype(str).map(len).max(), len(col) ) + 2 worksheet.column_dimensions[get_column_letter(idx + 1)].width = min(max_length, 50) output.seek(0) return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx' ) # @app.route("/audit") # @login_required def audit_log(): conn = get_connection() cur = conn.cursor() # Parameter filter_user = request.args.get('user', '').strip() filter_action = request.args.get('action', '').strip() filter_entity = request.args.get('entity', '').strip() page = request.args.get('page', 1, type=int) sort = request.args.get('sort', 'timestamp') order = request.args.get('order', 'desc') per_page = 50 # Whitelist für erlaubte Sortierfelder allowed_sort_fields = { 'timestamp': 'timestamp', 'username': 'username', 'action': 'action', 'entity': 'entity_type', 'ip': 'ip_address' } # Validierung if sort not in allowed_sort_fields: sort = 'timestamp' if order not in ['asc', 'desc']: order = 'desc' sort_field = allowed_sort_fields[sort] # SQL Query mit optionalen Filtern query = """ SELECT id, timestamp, username, action, entity_type, entity_id, old_values, new_values, ip_address, user_agent, additional_info FROM audit_log WHERE 1=1 """ params = [] # Filter if filter_user: query += " AND LOWER(username) LIKE LOWER(%s)" params.append(f'%{filter_user}%') if filter_action: query += " AND action = %s" params.append(filter_action) if filter_entity: query += " AND entity_type = %s" params.append(filter_entity) # Gesamtanzahl für Pagination count_query = "SELECT COUNT(*) FROM (" + query + ") as count_table" cur.execute(count_query, params) total = cur.fetchone()[0] # Pagination offset = (page - 1) * per_page query += f" ORDER BY {sort_field} {order.upper()} LIMIT %s OFFSET %s" params.extend([per_page, offset]) cur.execute(query, params) logs = cur.fetchall() # JSON-Werte parsen parsed_logs = [] for log in logs: parsed_log = list(log) # old_values und new_values sind bereits Dictionaries (JSONB) # Keine Konvertierung nötig parsed_logs.append(parsed_log) # Pagination Info total_pages = (total + per_page - 1) // per_page cur.close() conn.close() return render_template("audit_log.html", logs=parsed_logs, filter_user=filter_user, filter_action=filter_action, filter_entity=filter_entity, page=page, total_pages=total_pages, total=total, sort=sort, order=order, username=session.get('username')) # @app.route("/backups") # @login_required def backups(): """Zeigt die Backup-Historie an""" conn = get_connection() cur = conn.cursor() # Letztes erfolgreiches Backup für Dashboard cur.execute(""" SELECT created_at, filesize, duration_seconds FROM backup_history WHERE status = 'success' ORDER BY created_at DESC LIMIT 1 """) last_backup = cur.fetchone() # Alle Backups abrufen cur.execute(""" SELECT id, filename, filesize, backup_type, status, error_message, created_at, created_by, tables_count, records_count, duration_seconds, is_encrypted FROM backup_history ORDER BY created_at DESC """) backups = cur.fetchall() cur.close() conn.close() return render_template("backups.html", backups=backups, last_backup=last_backup, username=session.get('username')) # @app.route("/backup/create", methods=["POST"]) # @login_required def create_backup_route(): """Erstellt ein manuelles Backup""" username = session.get('username') success, result = create_backup(backup_type="manual", created_by=username) if success: return jsonify({ 'success': True, 'message': f'Backup erfolgreich erstellt: {result}' }) else: return jsonify({ 'success': False, 'message': f'Backup fehlgeschlagen: {result}' }), 500 # @app.route("/backup/restore/", methods=["POST"]) # @login_required def restore_backup_route(backup_id): """Stellt ein Backup wieder her""" encryption_key = request.form.get('encryption_key') success, message = restore_backup(backup_id, encryption_key) if success: return jsonify({ 'success': True, 'message': message }) else: return jsonify({ 'success': False, 'message': message }), 500 # @app.route("/backup/download/") # @login_required def download_backup(backup_id): """Lädt eine Backup-Datei herunter""" conn = get_connection() cur = conn.cursor() cur.execute(""" SELECT filename, filepath FROM backup_history WHERE id = %s """, (backup_id,)) backup_info = cur.fetchone() cur.close() conn.close() if not backup_info: return "Backup nicht gefunden", 404 filename, filepath = backup_info filepath = Path(filepath) if not filepath.exists(): return "Backup-Datei nicht gefunden", 404 # Audit-Log log_audit('DOWNLOAD', 'backup', backup_id, additional_info=f"Backup heruntergeladen: {filename}") return send_file(filepath, as_attachment=True, download_name=filename) # @app.route("/backup/delete/", methods=["DELETE"]) # @login_required def delete_backup(backup_id): """Löscht ein Backup""" conn = get_connection() cur = conn.cursor() try: # Backup-Informationen abrufen cur.execute(""" SELECT filename, filepath FROM backup_history WHERE id = %s """, (backup_id,)) backup_info = cur.fetchone() if not backup_info: return jsonify({ 'success': False, 'message': 'Backup nicht gefunden' }), 404 filename, filepath = backup_info filepath = Path(filepath) # Datei löschen, wenn sie existiert if filepath.exists(): filepath.unlink() # Aus Datenbank löschen cur.execute(""" DELETE FROM backup_history WHERE id = %s """, (backup_id,)) conn.commit() # Audit-Log log_audit('DELETE', 'backup', backup_id, additional_info=f"Backup gelöscht: {filename}") return jsonify({ 'success': True, 'message': f'Backup "{filename}" wurde erfolgreich gelöscht' }) except Exception as e: conn.rollback() return jsonify({ 'success': False, 'message': f'Fehler beim Löschen des Backups: {str(e)}' }), 500 finally: cur.close() conn.close() # @app.route("/security/blocked-ips") # @login_required def blocked_ips(): """Zeigt alle gesperrten IPs an""" conn = get_connection() cur = conn.cursor() cur.execute(""" SELECT ip_address, attempt_count, first_attempt, last_attempt, blocked_until, last_username_tried, last_error_message FROM login_attempts WHERE blocked_until IS NOT NULL ORDER BY blocked_until DESC """) blocked_ips_list = [] for ip in cur.fetchall(): blocked_ips_list.append({ 'ip_address': ip[0], 'attempt_count': ip[1], 'first_attempt': ip[2].strftime('%d.%m.%Y %H:%M'), 'last_attempt': ip[3].strftime('%d.%m.%Y %H:%M'), 'blocked_until': ip[4].strftime('%d.%m.%Y %H:%M'), 'is_active': ip[4] > datetime.now(ZoneInfo("Europe/Berlin")).replace(tzinfo=None), 'last_username': ip[5], 'last_error': ip[6] }) cur.close() conn.close() return render_template("blocked_ips.html", blocked_ips=blocked_ips_list, username=session.get('username')) # @app.route("/security/unblock-ip", methods=["POST"]) # @login_required def unblock_ip(): """Entsperrt eine IP-Adresse""" ip_address = request.form.get('ip_address') if ip_address: conn = get_connection() cur = conn.cursor() cur.execute(""" UPDATE login_attempts SET blocked_until = NULL WHERE ip_address = %s """, (ip_address,)) conn.commit() cur.close() conn.close() # Audit-Log log_audit('UNBLOCK_IP', 'security', additional_info=f"IP {ip_address} manuell entsperrt") return redirect(url_for('blocked_ips')) # @app.route("/security/clear-attempts", methods=["POST"]) # @login_required def clear_attempts(): """Löscht alle Login-Versuche für eine IP""" ip_address = request.form.get('ip_address') if ip_address: reset_login_attempts(ip_address) # Audit-Log log_audit('CLEAR_ATTEMPTS', 'security', additional_info=f"Login-Versuche für IP {ip_address} zurückgesetzt") return redirect(url_for('blocked_ips')) # API Endpoints for License Management # @app.route("/api/license//toggle", methods=["POST"]) # @login_required def toggle_license_api(license_id): """Toggle license active status via API""" try: data = request.get_json() is_active = data.get('is_active', False) conn = get_connection() cur = conn.cursor() # Update license status cur.execute(""" UPDATE licenses SET is_active = %s WHERE id = %s """, (is_active, license_id)) conn.commit() # Log the action log_audit('UPDATE', 'license', license_id, new_values={'is_active': is_active}, additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle") cur.close() conn.close() return jsonify({'success': True, 'message': 'Status erfolgreich geändert'}) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 # @app.route("/api/licenses/bulk-activate", methods=["POST"]) # @login_required def bulk_activate_licenses(): """Activate multiple licenses at once""" try: data = request.get_json() license_ids = data.get('ids', []) if not license_ids: return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400 conn = get_connection() cur = conn.cursor() # Update all selected licenses (nur Live-Daten) cur.execute(""" UPDATE licenses SET is_active = TRUE WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount conn.commit() # Log the bulk action log_audit('BULK_UPDATE', 'licenses', None, new_values={'is_active': True, 'count': affected_rows}, additional_info=f"{affected_rows} Lizenzen aktiviert") cur.close() conn.close() return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'}) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 # @app.route("/api/licenses/bulk-deactivate", methods=["POST"]) # @login_required def bulk_deactivate_licenses(): """Deactivate multiple licenses at once""" try: data = request.get_json() license_ids = data.get('ids', []) if not license_ids: return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400 conn = get_connection() cur = conn.cursor() # Update all selected licenses (nur Live-Daten) cur.execute(""" UPDATE licenses SET is_active = FALSE WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount conn.commit() # Log the bulk action log_audit('BULK_UPDATE', 'licenses', None, new_values={'is_active': False, 'count': affected_rows}, additional_info=f"{affected_rows} Lizenzen deaktiviert") cur.close() conn.close() return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'}) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 # @app.route("/api/license//devices") # @login_required def get_license_devices(license_id): """Hole alle registrierten Geräte einer Lizenz""" try: conn = get_connection() cur = conn.cursor() # Prüfe ob Lizenz existiert und hole device_limit cur.execute(""" SELECT device_limit FROM licenses WHERE id = %s """, (license_id,)) license_data = cur.fetchone() if not license_data: return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404 device_limit = license_data[0] # Hole alle Geräte für diese Lizenz cur.execute(""" SELECT id, hardware_id, device_name, operating_system, first_seen, last_seen, is_active, ip_address FROM device_registrations WHERE license_id = %s ORDER BY is_active DESC, last_seen DESC """, (license_id,)) devices = [] for row in cur.fetchall(): devices.append({ 'id': row[0], 'hardware_id': row[1], 'device_name': row[2] or 'Unbekanntes Gerät', 'operating_system': row[3] or 'Unbekannt', 'first_seen': row[4].strftime('%d.%m.%Y %H:%M') if row[4] else '', 'last_seen': row[5].strftime('%d.%m.%Y %H:%M') if row[5] else '', 'is_active': row[6], 'ip_address': row[7] or '-' }) cur.close() conn.close() return jsonify({ 'success': True, 'devices': devices, 'device_limit': device_limit, 'active_count': sum(1 for d in devices if d['is_active']) }) except Exception as e: logging.error(f"Fehler beim Abrufen der Geräte: {str(e)}") return jsonify({'success': False, 'message': 'Fehler beim Abrufen der Geräte'}), 500 # @app.route("/api/license//register-device", methods=["POST"]) # def register_device(license_id): # """Registriere ein neues Gerät für eine Lizenz""" # try: # data = request.get_json() # hardware_id = data.get('hardware_id') # device_name = data.get('device_name', '') # operating_system = data.get('operating_system', '') # if not hardware_id: # return jsonify({'success': False, 'message': 'Hardware-ID fehlt'}), 400 # conn = get_connection() # cur = conn.cursor() # Prüfe ob Lizenz existiert und aktiv ist # cur.execute(""" # SELECT device_limit, is_active, valid_until # FROM licenses # WHERE id = %s # """, (license_id,)) # license_data = cur.fetchone() # if not license_data: # return jsonify({'success': False, 'message': 'Lizenz nicht gefunden'}), 404 # device_limit, is_active, valid_until = license_data # Prüfe ob Lizenz aktiv und gültig ist # if not is_active: # return jsonify({'success': False, 'message': 'Lizenz ist deaktiviert'}), 403 # if valid_until < datetime.now(ZoneInfo("Europe/Berlin")).date(): # return jsonify({'success': False, 'message': 'Lizenz ist abgelaufen'}), 403 # Prüfe ob Gerät bereits registriert ist # cur.execute(""" # SELECT id, is_active FROM device_registrations # WHERE license_id = %s AND hardware_id = %s # """, (license_id, hardware_id)) # existing_device = cur.fetchone() # if existing_device: # device_id, is_device_active = existing_device # if is_device_active: # Gerät ist bereits aktiv, update last_seen # cur.execute(""" # UPDATE device_registrations # SET last_seen = CURRENT_TIMESTAMP, # ip_address = %s, # user_agent = %s # WHERE id = %s # """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id)) # conn.commit() # return jsonify({'success': True, 'message': 'Gerät bereits registriert', 'device_id': device_id}) # else: # Gerät war deaktiviert, prüfe ob wir es reaktivieren können # cur.execute(""" # SELECT COUNT(*) FROM device_registrations # WHERE license_id = %s AND is_active = TRUE # """, (license_id,)) # active_count = cur.fetchone()[0] # if active_count >= device_limit: # return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403 # Reaktiviere das Gerät # cur.execute(""" # UPDATE device_registrations # SET is_active = TRUE, # last_seen = CURRENT_TIMESTAMP, # deactivated_at = NULL, # deactivated_by = NULL, # ip_address = %s, # user_agent = %s # WHERE id = %s # """, (get_client_ip(), request.headers.get('User-Agent', ''), device_id)) # conn.commit() # return jsonify({'success': True, 'message': 'Gerät reaktiviert', 'device_id': device_id}) # Neues Gerät - prüfe Gerätelimit # cur.execute(""" # SELECT COUNT(*) FROM device_registrations # WHERE license_id = %s AND is_active = TRUE # """, (license_id,)) # active_count = cur.fetchone()[0] # if active_count >= device_limit: # return jsonify({'success': False, 'message': f'Gerätelimit erreicht ({device_limit} Geräte)'}), 403 # Registriere neues Gerät # cur.execute(""" # INSERT INTO device_registrations # (license_id, hardware_id, device_name, operating_system, ip_address, user_agent) # VALUES (%s, %s, %s, %s, %s, %s) # RETURNING id # """, (license_id, hardware_id, device_name, operating_system, # get_client_ip(), request.headers.get('User-Agent', ''))) # device_id = cur.fetchone()[0] # conn.commit() # Audit Log # log_audit('DEVICE_REGISTER', 'device', device_id, # new_values={'license_id': license_id, 'hardware_id': hardware_id}) # cur.close() # conn.close() # return jsonify({'success': True, 'message': 'Gerät erfolgreich registriert', 'device_id': device_id}) # except Exception as e: # logging.error(f"Fehler bei Geräte-Registrierung: {str(e)}") # return jsonify({'success': False, 'message': 'Fehler bei der Registrierung'}), 500 # @app.route("/api/license//deactivate-device/", methods=["POST"]) # @login_required def deactivate_device(license_id, device_id): """Deaktiviere ein registriertes Gerät""" try: conn = get_connection() cur = conn.cursor() # Prüfe ob das Gerät zu dieser Lizenz gehört cur.execute(""" SELECT id FROM device_registrations WHERE id = %s AND license_id = %s AND is_active = TRUE """, (device_id, license_id)) if not cur.fetchone(): return jsonify({'success': False, 'message': 'Gerät nicht gefunden oder bereits deaktiviert'}), 404 # Deaktiviere das Gerät cur.execute(""" UPDATE device_registrations SET is_active = FALSE, deactivated_at = CURRENT_TIMESTAMP, deactivated_by = %s WHERE id = %s """, (session['username'], device_id)) conn.commit() # Audit Log log_audit('DEVICE_DEACTIVATE', 'device', device_id, old_values={'is_active': True}, new_values={'is_active': False}) cur.close() conn.close() return jsonify({'success': True, 'message': 'Gerät erfolgreich deaktiviert'}) except Exception as e: logging.error(f"Fehler beim Deaktivieren des Geräts: {str(e)}") return jsonify({'success': False, 'message': 'Fehler beim Deaktivieren'}), 500 # @app.route("/api/licenses/bulk-delete", methods=["POST"]) # @login_required def bulk_delete_licenses(): """Delete multiple licenses at once""" try: data = request.get_json() license_ids = data.get('ids', []) if not license_ids: return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400 conn = get_connection() cur = conn.cursor() # Get license info for audit log (nur Live-Daten) cur.execute(""" SELECT license_key FROM licenses WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) license_keys = [row[0] for row in cur.fetchall()] # Delete all selected licenses (nur Live-Daten) cur.execute(""" DELETE FROM licenses WHERE id = ANY(%s) AND is_test = FALSE """, (license_ids,)) affected_rows = cur.rowcount conn.commit() # Log the bulk action log_audit('BULK_DELETE', 'licenses', None, old_values={'license_keys': license_keys, 'count': affected_rows}, additional_info=f"{affected_rows} Lizenzen gelöscht") cur.close() conn.close() return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'}) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 # ===================== RESOURCE POOL MANAGEMENT ===================== # @app.route('/resources') # @login_required def resources(): """Resource Pool Hauptübersicht""" conn = get_connection() cur = conn.cursor() # Prüfe ob Testdaten angezeigt werden sollen (gleiche Logik wie bei Kunden) show_test = request.args.get('show_test', 'false').lower() == 'true' # Statistiken abrufen cur.execute(""" SELECT resource_type, COUNT(*) FILTER (WHERE status = 'available') as available, COUNT(*) FILTER (WHERE status = 'allocated') as allocated, COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine, COUNT(*) as total FROM resource_pools WHERE is_test = %s GROUP BY resource_type """, (show_test,)) stats = {} for row in cur.fetchall(): stats[row[0]] = { 'available': row[1], 'allocated': row[2], 'quarantine': row[3], 'total': row[4], 'available_percent': round((row[1] / row[4] * 100) if row[4] > 0 else 0, 1) } # Letzte Aktivitäten (gefiltert nach Test/Live) cur.execute(""" SELECT rh.action, rh.action_by, rh.action_at, rp.resource_type, rp.resource_value, rh.details FROM resource_history rh JOIN resource_pools rp ON rh.resource_id = rp.id WHERE rp.is_test = %s ORDER BY rh.action_at DESC LIMIT 10 """, (show_test,)) recent_activities = cur.fetchall() # Ressourcen-Liste mit Pagination page = request.args.get('page', 1, type=int) per_page = 50 offset = (page - 1) * per_page resource_type = request.args.get('type', '') status_filter = request.args.get('status', '') search = request.args.get('search', '') # Sortierung sort_by = request.args.get('sort', 'id') sort_order = request.args.get('order', 'desc') # Base Query query = """ SELECT rp.id, rp.resource_type, rp.resource_value, rp.status, rp.allocated_to_license, l.license_key, c.name as customer_name, rp.status_changed_at, rp.quarantine_reason, rp.quarantine_until, c.id as customer_id 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 rp.is_test = %s """ params = [show_test] if resource_type: query += " AND rp.resource_type = %s" params.append(resource_type) if status_filter: query += " AND rp.status = %s" params.append(status_filter) if search: query += " AND rp.resource_value ILIKE %s" params.append(f'%{search}%') # Count total count_query = f"SELECT COUNT(*) FROM ({query}) as cnt" cur.execute(count_query, params) total = cur.fetchone()[0] total_pages = (total + per_page - 1) // per_page # Get paginated results with dynamic sorting sort_column_map = { 'id': 'rp.id', 'type': 'rp.resource_type', 'resource': 'rp.resource_value', 'status': 'rp.status', 'assigned': 'c.name', 'changed': 'rp.status_changed_at' } sort_column = sort_column_map.get(sort_by, 'rp.id') sort_direction = 'ASC' if sort_order == 'asc' else 'DESC' query += f" ORDER BY {sort_column} {sort_direction} LIMIT %s OFFSET %s" params.extend([per_page, offset]) cur.execute(query, params) resources = cur.fetchall() cur.close() conn.close() return render_template('resources.html', stats=stats, resources=resources, recent_activities=recent_activities, page=page, total_pages=total_pages, total=total, resource_type=resource_type, status_filter=status_filter, search=search, show_test=show_test, sort_by=sort_by, sort_order=sort_order, datetime=datetime, timedelta=timedelta) # @app.route('/resources/add', methods=['GET', 'POST']) # @login_required def add_resources(): """Ressourcen zum Pool hinzufügen""" # Hole show_test Parameter für die Anzeige show_test = request.args.get('show_test', 'false').lower() == 'true' if request.method == 'POST': resource_type = request.form.get('resource_type') resources_text = request.form.get('resources_text', '') is_test = request.form.get('is_test') == 'on' # Checkbox für Testdaten # Parse resources (one per line) resources = [r.strip() for r in resources_text.split('\n') if r.strip()] if not resources: flash('Keine Ressourcen angegeben', 'error') return redirect(url_for('add_resources', show_test=show_test)) conn = get_connection() cur = conn.cursor() added = 0 duplicates = 0 for resource_value in resources: try: cur.execute(""" INSERT INTO resource_pools (resource_type, resource_value, status_changed_by, is_test) VALUES (%s, %s, %s, %s) ON CONFLICT (resource_type, resource_value) DO NOTHING """, (resource_type, resource_value, session['username'], is_test)) if cur.rowcount > 0: added += 1 # Get the inserted ID cur.execute("SELECT id FROM resource_pools WHERE resource_type = %s AND resource_value = %s", (resource_type, resource_value)) resource_id = cur.fetchone()[0] # Log in history cur.execute(""" INSERT INTO resource_history (resource_id, action, action_by, ip_address) VALUES (%s, 'created', %s, %s) """, (resource_id, session['username'], get_client_ip())) else: duplicates += 1 except Exception as e: app.logger.error(f"Error adding resource {resource_value}: {e}") conn.commit() cur.close() conn.close() log_audit('CREATE', 'resource_pool', None, new_values={'type': resource_type, 'added': added, 'duplicates': duplicates, 'is_test': is_test}, additional_info=f"{added} {'Test-' if is_test else ''}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen") flash(f'{added} {"Test-" if is_test else ""}Ressourcen hinzugefügt, {duplicates} Duplikate übersprungen', 'success') return redirect(url_for('resources', show_test=show_test)) return render_template('add_resources.html', show_test=show_test) # @app.route('/resources/quarantine/', methods=['POST']) # @login_required def quarantine_resource(resource_id): """Ressource in Quarantäne setzen""" reason = request.form.get('reason', 'review') until_date = request.form.get('until_date') notes = request.form.get('notes', '') conn = get_connection() cur = conn.cursor() # Get current resource info cur.execute("SELECT resource_type, resource_value, status FROM resource_pools WHERE id = %s", (resource_id,)) resource = cur.fetchone() if not resource: flash('Ressource nicht gefunden', 'error') return redirect(url_for('resources')) old_status = resource[2] # Update resource cur.execute(""" UPDATE resource_pools SET status = 'quarantine', quarantine_reason = %s, quarantine_until = %s, notes = %s, status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s WHERE id = %s """, (reason, until_date if until_date else None, notes, session['username'], resource_id)) # Log in history cur.execute(""" INSERT INTO resource_history (resource_id, action, action_by, ip_address, details) VALUES (%s, 'quarantined', %s, %s, %s) """, (resource_id, session['username'], get_client_ip(), Json({'reason': reason, 'until': until_date, 'notes': notes, 'old_status': old_status}))) conn.commit() cur.close() conn.close() log_audit('UPDATE', 'resource', resource_id, old_values={'status': old_status}, new_values={'status': 'quarantine', 'reason': reason}, additional_info=f"Ressource {resource[0]}: {resource[1]} in Quarantäne") flash('Ressource in Quarantäne gesetzt', 'success') # Redirect mit allen aktuellen Filtern return redirect(url_for('resources', show_test=request.args.get('show_test', request.form.get('show_test', 'false')), type=request.args.get('type', request.form.get('type', '')), status=request.args.get('status', request.form.get('status', '')), search=request.args.get('search', request.form.get('search', '')))) # @app.route('/resources/release', methods=['POST']) # @login_required def release_resources(): """Ressourcen aus Quarantäne freigeben""" resource_ids = request.form.getlist('resource_ids') if not resource_ids: flash('Keine Ressourcen ausgewählt', 'error') return redirect(url_for('resources')) conn = get_connection() cur = conn.cursor() released = 0 for resource_id in resource_ids: cur.execute(""" UPDATE resource_pools SET status = 'available', quarantine_reason = NULL, quarantine_until = NULL, allocated_to_license = NULL, status_changed_at = CURRENT_TIMESTAMP, status_changed_by = %s WHERE id = %s AND status = 'quarantine' """, (session['username'], resource_id)) if cur.rowcount > 0: released += 1 # Log in history cur.execute(""" INSERT INTO resource_history (resource_id, action, action_by, ip_address) VALUES (%s, 'released', %s, %s) """, (resource_id, session['username'], get_client_ip())) conn.commit() cur.close() conn.close() log_audit('UPDATE', 'resource_pool', None, new_values={'released': released}, additional_info=f"{released} Ressourcen aus Quarantäne freigegeben") flash(f'{released} Ressourcen freigegeben', 'success') # Redirect mit allen aktuellen Filtern return redirect(url_for('resources', show_test=request.args.get('show_test', request.form.get('show_test', 'false')), type=request.args.get('type', request.form.get('type', '')), status=request.args.get('status', request.form.get('status', '')), search=request.args.get('search', request.form.get('search', '')))) # @app.route('/api/resources/allocate', methods=['POST']) # @login_required def allocate_resources_api(): """API für Ressourcen-Zuweisung bei Lizenzerstellung""" data = request.json license_id = data.get('license_id') domain_count = data.get('domain_count', 1) ipv4_count = data.get('ipv4_count', 1) phone_count = data.get('phone_count', 1) conn = get_connection() cur = conn.cursor() try: allocated = {'domains': [], 'ipv4s': [], 'phones': []} # Allocate domains if domain_count > 0: cur.execute(""" SELECT id, resource_value FROM resource_pools WHERE resource_type = 'domain' AND status = 'available' LIMIT %s FOR UPDATE """, (domain_count,)) domains = cur.fetchall() if len(domains) < domain_count: raise ValueError(f"Nicht genügend Domains verfügbar (benötigt: {domain_count}, verfügbar: {len(domains)})") for domain_id, domain_value in domains: # Update resource status 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'], domain_id)) # Create assignment cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, domain_id, session['username'])) # Log history cur.execute(""" INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address) VALUES (%s, %s, 'allocated', %s, %s) """, (domain_id, license_id, session['username'], get_client_ip())) allocated['domains'].append(domain_value) # Allocate IPv4s (similar logic) if ipv4_count > 0: cur.execute(""" SELECT id, resource_value FROM resource_pools WHERE resource_type = 'ipv4' AND status = 'available' LIMIT %s FOR UPDATE """, (ipv4_count,)) ipv4s = cur.fetchall() if len(ipv4s) < ipv4_count: raise ValueError(f"Nicht genügend IPv4-Adressen verfügbar") for ipv4_id, ipv4_value in ipv4s: 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'], ipv4_id)) cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, ipv4_id, session['username'])) cur.execute(""" INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address) VALUES (%s, %s, 'allocated', %s, %s) """, (ipv4_id, license_id, session['username'], get_client_ip())) allocated['ipv4s'].append(ipv4_value) # Allocate phones (similar logic) if phone_count > 0: cur.execute(""" SELECT id, resource_value FROM resource_pools WHERE resource_type = 'phone' AND status = 'available' LIMIT %s FOR UPDATE """, (phone_count,)) phones = cur.fetchall() if len(phones) < phone_count: raise ValueError(f"Nicht genügend Telefonnummern verfügbar") for phone_id, phone_value in phones: 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'], phone_id)) cur.execute(""" INSERT INTO license_resources (license_id, resource_id, assigned_by) VALUES (%s, %s, %s) """, (license_id, phone_id, session['username'])) cur.execute(""" INSERT INTO resource_history (resource_id, license_id, action, action_by, ip_address) VALUES (%s, %s, 'allocated', %s, %s) """, (phone_id, license_id, session['username'], get_client_ip())) allocated['phones'].append(phone_value) # Update license resource counts cur.execute(""" UPDATE licenses SET domain_count = %s, ipv4_count = %s, phone_count = %s WHERE id = %s """, (domain_count, ipv4_count, phone_count, license_id)) conn.commit() cur.close() conn.close() return jsonify({ 'success': True, 'allocated': allocated }) except Exception as e: conn.rollback() cur.close() conn.close() return jsonify({ 'success': False, 'error': str(e) }), 400 # @app.route('/api/resources/check-availability', methods=['GET']) # @login_required def check_resource_availability(): """Prüft verfügbare Ressourcen""" resource_type = request.args.get('type', '') count = request.args.get('count', 10, type=int) show_test = request.args.get('show_test', 'false').lower() == 'true' conn = get_connection() cur = conn.cursor() if resource_type: # Spezifische Ressourcen für einen Typ cur.execute(""" SELECT id, resource_value FROM resource_pools WHERE status = 'available' AND resource_type = %s AND is_test = %s ORDER BY resource_value LIMIT %s """, (resource_type, show_test, count)) resources = [] for row in cur.fetchall(): resources.append({ 'id': row[0], 'value': row[1] }) cur.close() conn.close() return jsonify({ 'available': resources, 'type': resource_type, 'count': len(resources) }) else: # Zusammenfassung aller Typen cur.execute(""" SELECT resource_type, COUNT(*) as available FROM resource_pools WHERE status = 'available' AND is_test = %s GROUP BY resource_type """, (show_test,)) availability = {} for row in cur.fetchall(): availability[row[0]] = row[1] cur.close() conn.close() return jsonify(availability) # @app.route('/api/global-search', methods=['GET']) # @login_required def global_search(): """Global search API endpoint for searching customers and licenses""" query = request.args.get('q', '').strip() if not query or len(query) < 2: return jsonify({'customers': [], 'licenses': []}) conn = get_connection() cur = conn.cursor() # Search pattern with wildcards search_pattern = f'%{query}%' # Search customers cur.execute(""" SELECT id, name, email, company_name FROM customers WHERE (LOWER(name) LIKE LOWER(%s) OR LOWER(email) LIKE LOWER(%s) OR LOWER(company_name) LIKE LOWER(%s)) AND is_test = FALSE ORDER BY name LIMIT 5 """, (search_pattern, search_pattern, search_pattern)) customers = [] for row in cur.fetchall(): customers.append({ 'id': row[0], 'name': row[1], 'email': row[2], 'company_name': row[3] }) # Search licenses cur.execute(""" SELECT l.id, l.license_key, c.name as customer_name FROM licenses l JOIN customers c ON l.customer_id = c.id WHERE LOWER(l.license_key) LIKE LOWER(%s) AND l.is_test = FALSE ORDER BY l.created_at DESC LIMIT 5 """, (search_pattern,)) licenses = [] for row in cur.fetchall(): licenses.append({ 'id': row[0], 'license_key': row[1], 'customer_name': row[2] }) cur.close() conn.close() return jsonify({ 'customers': customers, 'licenses': licenses }) # @app.route('/resources/history/') # @login_required def resource_history(resource_id): """Zeigt die komplette Historie einer Ressource""" conn = get_connection() cur = conn.cursor() # Get complete resource info using named columns cur.execute(""" SELECT id, resource_type, resource_value, status, allocated_to_license, status_changed_at, status_changed_by, quarantine_reason, quarantine_until, created_at, notes FROM resource_pools WHERE id = %s """, (resource_id,)) row = cur.fetchone() if not row: flash('Ressource nicht gefunden', 'error') return redirect(url_for('resources')) # Create resource object with named attributes resource = { 'id': row[0], 'resource_type': row[1], 'resource_value': row[2], 'status': row[3], 'allocated_to_license': row[4], 'status_changed_at': row[5], 'status_changed_by': row[6], 'quarantine_reason': row[7], 'quarantine_until': row[8], 'created_at': row[9], 'notes': row[10] } # Get license info if allocated license_info = None if resource['allocated_to_license']: cur.execute("SELECT license_key FROM licenses WHERE id = %s", (resource['allocated_to_license'],)) lic = cur.fetchone() if lic: license_info = {'license_key': lic[0]} # Get history with named columns cur.execute(""" SELECT rh.action, rh.action_by, rh.action_at, rh.details, rh.license_id, rh.ip_address FROM resource_history rh WHERE rh.resource_id = %s ORDER BY rh.action_at DESC """, (resource_id,)) history = [] for row in cur.fetchall(): history.append({ 'action': row[0], 'action_by': row[1], 'action_at': row[2], 'details': row[3], 'license_id': row[4], 'ip_address': row[5] }) cur.close() conn.close() # Convert to object-like for template class ResourceObj: def __init__(self, data): for key, value in data.items(): setattr(self, key, value) resource_obj = ResourceObj(resource) history_objs = [ResourceObj(h) for h in history] return render_template('resource_history.html', resource=resource_obj, license_info=license_info, history=history_objs) # @app.route('/resources/metrics') # @login_required def resources_metrics(): """Dashboard für Resource Metrics und Reports""" conn = get_connection() cur = conn.cursor() # Overall stats with fallback values cur.execute(""" SELECT COUNT(DISTINCT resource_id) as total_resources, COALESCE(AVG(performance_score), 0) as avg_performance, COALESCE(SUM(cost), 0) as total_cost, COALESCE(SUM(revenue), 0) as total_revenue, COALESCE(SUM(issues_count), 0) as total_issues FROM resource_metrics WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days' """) row = cur.fetchone() # Calculate ROI roi = 0 if row[2] > 0: # if total_cost > 0 roi = row[3] / row[2] # revenue / cost stats = { 'total_resources': row[0] or 0, 'avg_performance': row[1] or 0, 'total_cost': row[2] or 0, 'total_revenue': row[3] or 0, 'total_issues': row[4] or 0, 'roi': roi } # Performance by type cur.execute(""" SELECT rp.resource_type, COALESCE(AVG(rm.performance_score), 0) as avg_score, COUNT(DISTINCT rp.id) as resource_count FROM resource_pools rp LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY rp.resource_type ORDER BY rp.resource_type """) performance_by_type = cur.fetchall() # Utilization data cur.execute(""" SELECT resource_type, COUNT(*) FILTER (WHERE status = 'allocated') as allocated, COUNT(*) as total, ROUND(COUNT(*) FILTER (WHERE status = 'allocated') * 100.0 / COUNT(*), 1) as allocated_percent FROM resource_pools GROUP BY resource_type """) utilization_rows = cur.fetchall() utilization_data = [ { 'type': row[0].upper(), 'allocated': row[1], 'total': row[2], 'allocated_percent': row[3] } for row in utilization_rows ] # Top performing resources cur.execute(""" SELECT rp.id, rp.resource_type, rp.resource_value, COALESCE(AVG(rm.performance_score), 0) as avg_score, COALESCE(SUM(rm.revenue), 0) as total_revenue, COALESCE(SUM(rm.cost), 1) as total_cost, CASE WHEN COALESCE(SUM(rm.cost), 0) = 0 THEN 0 ELSE COALESCE(SUM(rm.revenue), 0) / COALESCE(SUM(rm.cost), 1) END as roi FROM resource_pools rp LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days' WHERE rp.status != 'quarantine' GROUP BY rp.id, rp.resource_type, rp.resource_value HAVING AVG(rm.performance_score) IS NOT NULL ORDER BY avg_score DESC LIMIT 10 """) top_rows = cur.fetchall() top_performers = [ { 'id': row[0], 'resource_type': row[1], 'resource_value': row[2], 'avg_score': row[3], 'roi': row[6] } for row in top_rows ] # Resources with issues cur.execute(""" SELECT rp.id, rp.resource_type, rp.resource_value, rp.status, COALESCE(SUM(rm.issues_count), 0) as total_issues FROM resource_pools rp LEFT JOIN resource_metrics rm ON rp.id = rm.resource_id AND rm.metric_date >= CURRENT_DATE - INTERVAL '30 days' WHERE rm.issues_count > 0 OR rp.status = 'quarantine' GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status HAVING SUM(rm.issues_count) > 0 ORDER BY total_issues DESC LIMIT 10 """) problem_rows = cur.fetchall() problem_resources = [ { 'id': row[0], 'resource_type': row[1], 'resource_value': row[2], 'status': row[3], 'total_issues': row[4] } for row in problem_rows ] # Daily metrics for trend chart (last 30 days) cur.execute(""" SELECT metric_date, COALESCE(AVG(performance_score), 0) as avg_performance, COALESCE(SUM(issues_count), 0) as total_issues FROM resource_metrics WHERE metric_date >= CURRENT_DATE - INTERVAL '30 days' GROUP BY metric_date ORDER BY metric_date """) daily_rows = cur.fetchall() daily_metrics = [ { 'date': row[0].strftime('%d.%m'), 'performance': float(row[1]), 'issues': int(row[2]) } for row in daily_rows ] cur.close() conn.close() return render_template('resource_metrics.html', stats=stats, performance_by_type=performance_by_type, utilization_data=utilization_data, top_performers=top_performers, problem_resources=problem_resources, daily_metrics=daily_metrics) # @app.route('/resources/report', methods=['GET']) # @login_required def resources_report(): """Generiert Ressourcen-Reports oder zeigt Report-Formular""" # Prüfe ob Download angefordert wurde if request.args.get('download') == 'true': report_type = request.args.get('type', 'usage') format_type = request.args.get('format', 'excel') date_from = request.args.get('from', (datetime.now(ZoneInfo("Europe/Berlin")) - timedelta(days=30)).strftime('%Y-%m-%d')) date_to = request.args.get('to', datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y-%m-%d')) conn = get_connection() cur = conn.cursor() if report_type == 'usage': # Auslastungsreport query = """ SELECT rp.resource_type, rp.resource_value, rp.status, COUNT(DISTINCT rh.license_id) as unique_licenses, COUNT(rh.id) as total_allocations, MIN(rh.action_at) as first_used, MAX(rh.action_at) as last_used FROM resource_pools rp LEFT JOIN resource_history rh ON rp.id = rh.resource_id AND rh.action = 'allocated' AND rh.action_at BETWEEN %s AND %s GROUP BY rp.id, rp.resource_type, rp.resource_value, rp.status ORDER BY rp.resource_type, total_allocations DESC """ cur.execute(query, (date_from, date_to)) columns = ['Typ', 'Ressource', 'Status', 'Unique Lizenzen', 'Gesamt Zuweisungen', 'Erste Nutzung', 'Letzte Nutzung'] elif report_type == 'performance': # Performance-Report query = """ SELECT rp.resource_type, rp.resource_value, AVG(rm.performance_score) as avg_performance, SUM(rm.usage_count) as total_usage, SUM(rm.revenue) as total_revenue, SUM(rm.cost) as total_cost, SUM(rm.revenue - rm.cost) as profit, SUM(rm.issues_count) as total_issues FROM resource_pools rp JOIN resource_metrics rm ON rp.id = rm.resource_id WHERE rm.metric_date BETWEEN %s AND %s GROUP BY rp.id, rp.resource_type, rp.resource_value ORDER BY profit DESC """ cur.execute(query, (date_from, date_to)) columns = ['Typ', 'Ressource', 'Durchschn. Performance', 'Gesamt Nutzung', 'Umsatz', 'Kosten', 'Gewinn', 'Issues'] elif report_type == 'compliance': # Compliance-Report query = """ SELECT rh.action_at, rh.action, rh.action_by, rp.resource_type, rp.resource_value, l.license_key, c.name as customer_name, rh.ip_address FROM resource_history rh JOIN resource_pools rp ON rh.resource_id = rp.id LEFT JOIN licenses l ON rh.license_id = l.id LEFT JOIN customers c ON l.customer_id = c.id WHERE rh.action_at BETWEEN %s AND %s ORDER BY rh.action_at DESC """ cur.execute(query, (date_from, date_to)) columns = ['Zeit', 'Aktion', 'Von', 'Typ', 'Ressource', 'Lizenz', 'Kunde', 'IP-Adresse'] else: # inventory report # Inventar-Report query = """ SELECT resource_type, COUNT(*) FILTER (WHERE status = 'available') as available, COUNT(*) FILTER (WHERE status = 'allocated') as allocated, COUNT(*) FILTER (WHERE status = 'quarantine') as quarantine, COUNT(*) as total FROM resource_pools GROUP BY resource_type ORDER BY resource_type """ cur.execute(query) columns = ['Typ', 'Verfügbar', 'Zugeteilt', 'Quarantäne', 'Gesamt'] # Convert to DataFrame data = cur.fetchall() df = pd.DataFrame(data, columns=columns) cur.close() conn.close() # Generate file timestamp = datetime.now(ZoneInfo("Europe/Berlin")).strftime('%Y%m%d_%H%M%S') filename = f"resource_report_{report_type}_{timestamp}" if format_type == 'excel': output = io.BytesIO() with pd.ExcelWriter(output, engine='openpyxl') as writer: df.to_excel(writer, sheet_name='Report', index=False) # Auto-adjust columns width worksheet = writer.sheets['Report'] for column in worksheet.columns: max_length = 0 column = [cell for cell in column] for cell in column: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = (max_length + 2) worksheet.column_dimensions[column[0].column_letter].width = adjusted_width output.seek(0) log_audit('EXPORT', 'resource_report', None, new_values={'type': report_type, 'format': 'excel', 'rows': len(df)}, additional_info=f"Resource Report {report_type} exportiert") return send_file(output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=f'{filename}.xlsx') else: # CSV output = io.StringIO() df.to_csv(output, index=False, sep=';', encoding='utf-8-sig') output.seek(0) log_audit('EXPORT', 'resource_report', None, new_values={'type': report_type, 'format': 'csv', 'rows': len(df)}, additional_info=f"Resource Report {report_type} exportiert") return send_file(io.BytesIO(output.getvalue().encode('utf-8-sig')), mimetype='text/csv', as_attachment=True, download_name=f'{filename}.csv') # Wenn kein Download, zeige Report-Formular return render_template('resource_report.html', datetime=datetime, timedelta=timedelta, username=session.get('username')) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)