import os from flask import Flask, render_template, request, redirect, url_for, session, flash from pymongo import MongoClient from werkzeug.security import check_password_hash, generate_password_hash from flask_wtf.csrf import CSRFProtect from datetime import timedelta, datetime from functools import wraps from bson.objectid import ObjectId import paramiko from flask import jsonify import logging from flask_socketio import SocketIO, emit, join_room, leave_room import subprocess import threading from werkzeug.utils import secure_filename import requests import ipaddress import socket import ssl from dotenv import load_dotenv load_dotenv() async_mode = "eventlet" app = Flask(__name__, template_folder='Frontend/HTML', static_folder='Frontend') socketio = SocketIO(app, async_mode=async_mode) BOT_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Bots') if not os.path.exists(BOT_UPLOAD_FOLDER): os.makedirs(BOT_UPLOAD_FOLDER) app.config['BOT_UPLOAD_FOLDER'] = BOT_UPLOAD_FOLDER running_bots = {} class SuppressServerLoadFilter(logging.Filter): def filter(self, record): msg = record.getMessage() return ( "GET /api/server_load" not in msg and "GET /api/system_status" not in msg ) log = logging.getLogger('werkzeug') log.addFilter(SuppressServerLoadFilter()) app.secret_key = os.environ.get('SECRET_KEY', os.urandom(32)) app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=8) app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' csrf = CSRFProtect(app) @app.context_processor def inject_utility_processor(): maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None is_maintenance_active = maintenance_status and maintenance_status.get('enabled', False) return dict( timedelta=timedelta, maintenance_active=is_maintenance_active ) try: client = MongoClient(os.environ.get("MONGO_URI")) db = client["astra_os"] accounts_collection = db["accounts"] logs_collection = db["logs"] roles_collection = db["roles"] dashboard_collection = db["dashboard_stats"] discord_bots_collection = db["discord_bots_data"] ip_analyzer_results_collection = db["ip_analyzer_results"] print("INFO: MongoDB-Verbindung erfolgreich hergestellt.") AVAILABLE_PERMISSIONS = [ "view_dashboard", "view_user_management", "manage_users", "view_role_management", "manage_roles", "view_logs", "maintenance_management", "bypass_maintenance", "manage_discord_bots", "view_ip_analyzer" ] if roles_collection.count_documents({}) == 0: print("WARNUNG: Keine Rollen in der Datenbank gefunden. Erstelle Standard-Rollen...") roles_collection.insert_many([ { "name": "Owner", "permissions": AVAILABLE_PERMISSIONS, "is_deletable": False }, { "name": "Benutzer", "permissions": ["view_dashboard"], "is_deletable": True } ]) print("INFO: Standard-Rollen ('Owner', 'Benutzer') wurden erstellt.") if accounts_collection.count_documents({}) == 0: print("WARNUNG: Keine Benutzer in der Datenbank gefunden. Erstelle Standard-Admin-Benutzer...") owner_role = roles_collection.find_one({"name": "Owner"}) hashed_password = generate_password_hash("1234") accounts_collection.insert_one({ 'username': 'admin', 'password': hashed_password, 'role_id': owner_role['_id'], 'creation_date': datetime.utcnow(), 'discord_id': None, 'discord_username': None, 'discord_avatar': None }) print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.") if dashboard_collection.count_documents({}) == 0: print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.") dashboard_collection.insert_many([ {"key": "active_users", "value": 1, "percent": 100}, {"key": "server_load", "percent": 0, "status": "Nicht Erreichbar"}, {"key": "open_tickets", "count": 0}, {"key": "system_status", "status": "Online"}, {"key": "scheduled_maintenance", "enabled": False, "time": None, "status": "Keine", "duration_minutes": None}, {"key": "last_user_created", "time": None, "by": None} ]) print("INFO: Standard Dashboard-Statistiken wurden erstellt.") except Exception as e: print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}") db = None accounts_collection = None logs_collection = None roles_collection = None dashboard_collection = None discord_bots_collection = None ip_analyzer_results_collection = None print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}") db = None accounts_collection = None logs_collection = None def start_bots_on_startup(): if discord_bots_collection is None: print("WARNUNG: Bot-Autostart übersprungen, da keine DB-Verbindung besteht.") return bots_to_start = list(discord_bots_collection.find({'status': 'running'})) if not bots_to_start: print("INFO: Keine Bots für den Autostart markiert.") return print(f"INFO: Starte {len(bots_to_start)} Bot(s) automatisch...") for bot_info in bots_to_start: bot_id = str(bot_info['_id']) filepath = bot_info.get('filepath') if not os.path.exists(filepath): print(f"FEHLER: Bot-Datei für '{bot_info['name']}' nicht gefunden: {filepath}") continue try: process = subprocess.Popen( ['python', '-u', filepath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace' ) running_bots[bot_id] = {'process': process, 'history': []} thread = threading.Thread(target=stream_bot_output, args=(bot_id, process)) thread.daemon = True thread.start() print(f"INFO: Bot '{bot_info['name']}' (PID: {process.pid}) wurde gestartet.") except Exception as e: print(f"FEHLER: Bot '{bot_info['name']}' konnte nicht gestartet werden: {e}") try: tickets_client = MongoClient(os.environ.get("MONGO_URI")) tickets_db = tickets_client["chainsaw_dc_data"] tickets_collection = tickets_db["tickets"] except Exception as e: tickets_collection = None print(f"FATAL: Verbindung zu Tickets-DB fehlgeschlagen: {e}") def get_user_permissions(user_id): user = accounts_collection.find_one({'_id': ObjectId(user_id)}) if not user or 'role_id' not in user: return [] role = roles_collection.find_one({'_id': user['role_id']}) if not role: return [] return role.get('permissions', []) def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: flash("Bitte melden Sie sich an, um diese Seite zu sehen.", "warning") return redirect(url_for('login')) user = accounts_collection.find_one({'_id': ObjectId(session['user_id'])}) if not user: session.clear() flash("Benutzer nicht gefunden.", "danger") return redirect(url_for('login')) # Discord-Verknüpfung prüfen if not user.get('discord_id') and request.endpoint not in ['link_discord', 'login_discord', 'callback_discord', 'logout', 'static']: return redirect(url_for('link_discord')) session['permissions'] = get_user_permissions(session['user_id']) maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if maintenance_status and maintenance_status.get('enabled', False): if 'bypass_maintenance' not in session['permissions']: return redirect(url_for('maintenance')) return f(*args, **kwargs) return decorated_function def permission_required(permission): def decorator(f): @wraps(f) def decorated_function(*args, **kwargs): if 'user_id' not in session: return redirect(url_for('login')) user_perms = get_user_permissions(session['user_id']) if permission not in user_perms: flash("Sie haben keine Berechtigung, diese Seite anzuzeigen.", "danger") return redirect(url_for('dashboard')) return f(*args, **kwargs) return decorated_function return decorator @app.route('/') def home(): if 'user_id' in session: return redirect(url_for('dashboard')) return redirect(url_for('login')) @app.route('/login', methods=['GET', 'POST']) def login(): if 'user_id' in session: return redirect(url_for('dashboard')) maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None if maintenance_status and maintenance_status.get('enabled', False): return redirect(url_for('maintenance')) if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if not username or not password: flash("Benutzername und Passwort sind erforderlich.", "danger") return render_template('login.html') if accounts_collection is None: flash("Datenbankverbindung nicht verfügbar. Bitte versuchen Sie es später erneut.", "danger") return render_template('login.html'), 500 user = accounts_collection.find_one({'username': username}) if user and check_password_hash(user['password'], password): session.permanent = True session['user_id'] = str(user['_id']) session['username'] = user['username'] session['permissions'] = get_user_permissions(str(user['_id'])) # Prüfen, ob Discord verknüpft ist if not user.get('discord_id'): flash("Bitte verknüpfe deinen Discord-Account, um fortzufahren.", "info") return redirect(url_for('link_discord')) flash("Erfolgreich angemeldet!", "success") return redirect(url_for('dashboard')) else: flash("Ungültiger Benutzername oder Passwort.", "danger") return render_template('login.html') return render_template('login.html') @app.route('/api/server_load') @login_required def api_server_load(): try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh.connect(os.environ.get('SSH_HOST'), username=os.environ.get('SSH_USER'), password=os.environ.get('SSH_PASS'), timeout=10) stdin, stdout, stderr = ssh.exec_command("top -bn1 | grep 'Cpu(s)'") output = stdout.read().decode() ssh.close() import re match = re.search(r"(\d+\.\d+)\s*id", output) if match: idle = float(match.group(1)) usage = 100 - idle return jsonify({"percent": round(usage, 1), "status": "Online"}) else: return jsonify({"percent": 0, "status": "Fehler"}) except paramiko.AuthenticationException: print("SSH-Fehler: Authentifizierung fehlgeschlagen.") return jsonify({"percent": 0, "status": "Auth Fehler"}) except paramiko.SSHException as e: print(f"SSH-Fehler: {e}") return jsonify({"percent": 0, "status": "SSH Fehler"}) except Exception as e: print(f"Allgemeiner Fehler bei Server-Auslastung: {e}") return jsonify({"percent": 0, "status": "Nicht erreichbar"}) @app.route('/api/system_status') @login_required def api_system_status(): maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None maintenance_active = maintenance_status and maintenance_status.get('enabled', False) discord_bots_maintenance = maintenance_status.get('discord_bots_maintenance', False) if maintenance_status else False scpsl_status = "offline" try: udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_sock.settimeout(2) udp_sock.sendto(b"\x00", ("89.144.42.175", 7101)) scpsl_status = "online" udp_sock.close() except Exception as e: print(f"[DEBUG] SCP:SL UDP-Ping failed: {e}") scpsl_status = "offline" if maintenance_active: scpsl_status = "wartung" gamepanel_status = "offline" try: resp = requests.get("https://gamepanel.mrsniff.de", timeout=3) if resp.status_code == 200: gamepanel_status = "online" except Exception: gamepanel_status = "offline" if maintenance_active: gamepanel_status = "wartung" discord_bots_status = "wartung" if discord_bots_maintenance or maintenance_active else "online" db_status = "online" if db is not None else "offline" if maintenance_active: db_status = "wartung" return jsonify({ "scpsl": scpsl_status, "gamepanel": gamepanel_status, "discord_bots": discord_bots_status, "database": db_status, "maintenance": maintenance_active }) @app.route('/status') @login_required def status_page(): maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None maintenance_active = maintenance_status and maintenance_status.get('enabled', False) return render_template('status_page.html', active_page='status_page', maintenance_active=maintenance_active) @app.route('/dashboard') @login_required def dashboard(): user_count = accounts_collection.count_documents({}) if accounts_collection is not None else 1 last_user = accounts_collection.find_one(sort=[("creation_date", -1)]) if accounts_collection is not None else None last_user_time = last_user["creation_date"] if last_user and "creation_date" in last_user else None last_user_by = last_user["username"] if last_user and "username" in last_user else None open_tickets = 0 if tickets_collection is not None: open_tickets = tickets_collection.count_documents({"status": "open"}) server_load = {"percent": 0, "status": "Lädt..."} system_status = {"status": "Online"} try: resp = requests.get(request.url_root.rstrip('/') + '/api/system_status', cookies=request.cookies, timeout=2) if resp.status_code == 200: data = resp.json() if data.get("maintenance"): system_status = {"status": "Online (Wartungsarbeiten)"} elif any(data.get(k) == "wartung" for k in ["scpsl", "gamepanel", "discord_bots", "database"]): system_status = {"status": "Systeme im Wartungsmodus"} elif all(data.get(k) == "online" for k in ["scpsl", "gamepanel", "discord_bots", "database"]): system_status = {"status": "Online"} else: system_status = {"status": "Offline"} else: system_status = {"status": "Offline"} except Exception: system_status = {"status": "Offline"} scheduled_maintenance = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None def _dt_to_str(v): if v is None: return None try: return v.strftime("%d. %b %Y, %H:%M") except Exception: return str(v) maint = scheduled_maintenance or {"time": None, "status": "Keine", "duration_minutes": None} maint["time_str"] = _dt_to_str(maint.get("time")) stats = { "active_users": {"value": user_count, "percent": 0}, "server_load": server_load, "open_tickets": {"count": open_tickets}, "system_status": system_status, "scheduled_maintenance": maint, "last_user_created": { "time": last_user_time, "by": last_user_by, "time_str": _dt_to_str(last_user_time) } } return render_template('dashboard.html', stats=stats, active_page='dashboard') @app.route('/logout') @login_required def logout(): session.clear() flash("Sie wurden erfolgreich abgemeldet.", "info") return redirect(url_for('login')) @app.route('/user_management') @login_required @permission_required('view_user_management') def user_management(): if accounts_collection is None or roles_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('dashboard')) users_list = list(accounts_collection.find()) roles_list = list(roles_collection.find()) for role in roles_list: role['_id'] = str(role['_id']) role_map = {role['_id']: role['name'] for role in roles_list} users_for_modal = {} for user in users_list: user_id_str = str(user['_id']) users_for_modal[user_id_str] = { '_id': user_id_str, 'username': user['username'], 'role_id': str(user.get('role_id')) if user.get('role_id') else None } users_for_table = list(accounts_collection.find()) for user in users_for_table: user['role_name'] = role_map.get(str(user.get('role_id')), 'Unbekannt') user['discord_linked'] = bool(user.get('discord_id')) return render_template('user_management.html', users_for_modal=users_for_modal, users_for_table=users_for_table, roles=roles_list, active_page='user_management') @app.route('/add_user', methods=['POST']) @login_required @permission_required('manage_users') def add_user(): if accounts_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('user_management')) username = request.form.get('username') password = request.form.get('password') role_id = request.form.get('role') if not username or not password or not role_id: flash("Alle Felder sind erforderlich.", "danger") return redirect(url_for('user_management')) if accounts_collection.find_one({'username': username}): flash("Ein Benutzer mit diesem Namen existiert bereits.", "danger") return redirect(url_for('user_management')) hashed_password = generate_password_hash(password) accounts_collection.insert_one({ 'username': username, 'password': hashed_password, 'role_id': ObjectId(role_id), 'creation_date': datetime.utcnow(), 'discord_id': None, 'discord_username': None, 'discord_avatar': None }) if dashboard_collection is not None: dashboard_collection.update_one( {"key": "last_user_created"}, {"$set": { "time": datetime.utcnow(), "by": username }}, upsert=True ) user_count = accounts_collection.count_documents({}) dashboard_collection.update_one( {"key": "active_users"}, {"$set": {"value": user_count}}, upsert=True ) flash(f"Benutzer '{username}' wurde erfolgreich erstellt.", "success") return redirect(url_for('user_management')) @app.route('/edit_user/', methods=['POST']) @login_required @permission_required('manage_users') def edit_user(user_id): if accounts_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('user_management')) username = request.form.get('username') password = request.form.get('password') role_id = request.form.get('role') if not username or not role_id: flash("Benutzername und Rolle sind erforderlich.", "danger") return redirect(url_for('user_management')) existing_user = accounts_collection.find_one({'username': username}) if existing_user and str(existing_user['_id']) != user_id: flash("Ein anderer Benutzer mit diesem Namen existiert bereits.", "danger") return redirect(url_for('user_management')) update_data = { 'username': username, 'role_id': ObjectId(role_id) } if password: update_data['password'] = generate_password_hash(password) accounts_collection.update_one( {'_id': ObjectId(user_id)}, {'$set': update_data} ) flash(f"Benutzer '{username}' wurde erfolgreich aktualisiert.", "success") return redirect(url_for('user_management')) @app.route('/delete_user/', methods=['POST']) @login_required @permission_required('manage_users') def delete_user(user_id): if accounts_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('user_management')) if session.get('user_id') == user_id: flash("Sie können sich nicht selbst löschen.", "danger") return redirect(url_for('user_management')) accounts_collection.delete_one({'_id': ObjectId(user_id)}) flash("Benutzer wurde erfolgreich gelöscht.", "success") return redirect(url_for('user_management')) @app.route('/settings', methods=['GET']) @login_required def settings(): user = accounts_collection.find_one({'_id': ObjectId(session['user_id'])}) return render_template('settings.html', user=user, active_page='settings') @app.route('/update_password', methods=['POST']) @login_required def update_password(): user_id = session['user_id'] current_password = request.form.get('current_password') new_password = request.form.get('new_password') confirm_password = request.form.get('confirm_password') if not all([current_password, new_password, confirm_password]): flash("Alle Passwortfelder sind erforderlich.", "danger") return redirect(url_for('settings')) if new_password != confirm_password: flash("Die neuen Passwörter stimmen nicht überein.", "danger") return redirect(url_for('settings')) user = accounts_collection.find_one({'_id': ObjectId(user_id)}) if not user or not check_password_hash(user['password'], current_password): flash("Das aktuelle Passwort ist nicht korrekt.", "danger") return redirect(url_for('settings')) hashed_password = generate_password_hash(new_password) accounts_collection.update_one({'_id': ObjectId(user_id)}, {'$set': {'password': hashed_password}}) flash("Dein Passwort wurde erfolgreich geändert.", "success") return redirect(url_for('settings')) @app.route('/unlink_discord', methods=['POST']) @login_required def unlink_discord(): user_id = session['user_id'] accounts_collection.update_one( {'_id': ObjectId(user_id)}, {'$set': {'discord_id': None, 'discord_username': None, 'discord_avatar': None}} ) flash("Deine Discord-Verknüpfung wurde entfernt. Bitte verknüpfe dein Konto erneut.", "info") return redirect(url_for('link_discord')) @app.route('/link_discord') @login_required def link_discord(): return render_template('link_discord.html') @app.route('/login/discord') def login_discord(): client_id = os.environ.get('DISCORD_CLIENT_ID') redirect_uri = os.environ.get('DISCORD_REDIRECT_URI') scope = 'identify' discord_auth_url = ( f'https://discord.com/api/oauth2/authorize?client_id={client_id}&' f'redirect_uri={redirect_uri}&response_type=code&scope={scope}' ) return redirect(discord_auth_url) @app.route('/callback/discord') def callback_discord(): code = request.args.get('code') if not code: flash("Discord-Authentifizierung fehlgeschlagen.", "danger") return redirect(url_for('login')) client_id = os.environ.get('DISCORD_CLIENT_ID') client_secret = os.environ.get('DISCORD_CLIENT_SECRET') redirect_uri = os.environ.get('DISCORD_REDIRECT_URI') token_url = 'https://discord.com/api/oauth2/token' token_data = { 'client_id': client_id, 'client_secret': client_secret, 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': redirect_uri, 'scope': 'identify' } headers = {'Content-Type': 'application/x-www-form-urlencoded'} token_r = requests.post(token_url, data=token_data, headers=headers) if token_r.status_code != 200: flash("Fehler beim Abrufen des Tokens von Discord.", "danger") return redirect(url_for('login')) access_token = token_r.json()['access_token'] user_info_url = 'https://discord.com/api/users/@me' user_info_r = requests.get(user_info_url, headers={'Authorization': f'Bearer {access_token}'}) if user_info_r.status_code != 200: flash("Fehler beim Abrufen der Benutzerinformationen von Discord.", "danger") return redirect(url_for('login')) discord_user = user_info_r.json() discord_id = discord_user['id'] discord_username = f"{discord_user['username']}" discord_avatar = discord_user.get('avatar') # Fall 1: Benutzer ist bereits eingeloggt und möchte verknüpfen if 'user_id' in session: user_id = session['user_id'] # Prüfen, ob dieser Discord-Account bereits von einem anderen Benutzer verwendet wird existing_link = accounts_collection.find_one({'discord_id': discord_id}) if existing_link and str(existing_link['_id']) != user_id: flash("Dieser Discord-Account ist bereits mit einem anderen Benutzer verknüpft.", "danger") return redirect(url_for('settings')) accounts_collection.update_one( {'_id': ObjectId(user_id)}, {'$set': { 'discord_id': discord_id, 'discord_username': discord_username, 'discord_avatar': discord_avatar }} ) flash("Discord-Account erfolgreich verknüpft!", "success") return redirect(url_for('dashboard')) # Fall 2: Benutzer meldet sich direkt über Discord an else: user = accounts_collection.find_one({'discord_id': discord_id}) if user: session.permanent = True session['user_id'] = str(user['_id']) session['username'] = user['username'] session['permissions'] = get_user_permissions(str(user['_id'])) flash("Erfolgreich mit Discord angemeldet!", "success") return redirect(url_for('dashboard')) else: flash("Kein AstraOS-Benutzer mit diesem Discord-Konto gefunden. Bitte melde dich zuerst normal an und verknüpfe deinen Account.", "warning") return redirect(url_for('login')) @app.route('/role_management') @login_required @permission_required('view_role_management') def role_management(): if roles_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('dashboard')) roles = list(roles_collection.find()) for role in roles: role['_id'] = str(role['_id']) return render_template('role_management.html', roles=roles, available_permissions=AVAILABLE_PERMISSIONS, active_page='role_management') @app.route('/add_role', methods=['POST']) @login_required @permission_required('manage_roles') def add_role(): role_name = request.form.get('role_name') permissions = request.form.getlist('permissions') if not role_name: flash("Rollenname ist erforderlich.", "danger") return redirect(url_for('role_management')) if roles_collection.find_one({'name': role_name}): flash("Eine Rolle mit diesem Namen existiert bereits.", "danger") return redirect(url_for('role_management')) roles_collection.insert_one({ "name": role_name, "permissions": permissions, "is_deletable": True }) flash(f"Rolle '{role_name}' wurde erstellt.", "success") return redirect(url_for('role_management')) @app.route('/edit_role/', methods=['POST']) @login_required @permission_required('manage_roles') def edit_role(role_id): role_name = request.form.get('role_name') permissions = request.form.getlist('permissions') if not role_name: flash("Rollenname ist erforderlich.", "danger") return redirect(url_for('role_management')) roles_collection.update_one( {'_id': ObjectId(role_id)}, {'$set': {'name': role_name, 'permissions': permissions}} ) flash(f"Rolle '{role_name}' wurde aktualisiert.", "success") return redirect(url_for('role_management')) @app.route('/delete_role/', methods=['POST']) @login_required @permission_required('manage_roles') def delete_role(role_id): role = roles_collection.find_one({'_id': ObjectId(role_id)}) if role and not role.get('is_deletable', True): flash("Diese Rolle kann nicht gelöscht werden.", "danger") return redirect(url_for('role_management')) roles_collection.delete_one({'_id': ObjectId(role_id)}) flash("Rolle wurde gelöscht.", "success") return redirect(url_for('role_management')) @app.route('/ip_analyzer', methods=['GET', 'POST']) @login_required @permission_required('view_ip_analyzer') def ip_analyzer(): ip_data = None error = None similar_ips = [] if request.method == 'POST': ip_address = request.form.get("ip_address") is_cheater = request.form.get("is_cheater") == "on" steam_id = request.form.get("steam_id") if is_cheater else None if not ip_address: error = "Bitte eine IP-Adresse eingeben." elif is_cheater and not steam_id: error = "SteamID ist erforderlich, wenn die IP als Cheater markiert wird." else: try: api_key = os.environ.get('VPNAPI_KEY') response = requests.get(f"https://vpnapi.io/api/{ip_address}?key={api_key}", timeout=5) if response.status_code == 200: ip_data = response.json() ip_data.setdefault('security', {}) ip_data.setdefault('location', {}) ip_data.setdefault('network', {}) similar_ips = list(ip_analyzer_results_collection.find({ "$or": [ {"ip": ip_address}, {"data.location.city": ip_data.get('location', {}).get('city')}, {"data.network.autonomous_system_number": ip_data.get('network', {}).get('autonomous_system_number')} ] })) ip_analyzer_results_collection.insert_one({ "ip": ip_address, "is_cheater": is_cheater, "steam_id": steam_id, "data": ip_data, "added_by": session["username"], "timestamp": datetime.utcnow() }) flash("IP-Adresse erfolgreich analysiert und gespeichert.", "success") else: error = f"Fehler bei der API-Anfrage: Status {response.status_code}" except requests.exceptions.RequestException as e: error = f"Fehler bei der Verbindung zur API: {e}" except Exception as e: error = f"Ein unerwarteter Fehler ist aufgetreten: {e}" return render_template('ip_analyzer.html', active_page='ip_analyzer', ip_data=ip_data, error=error, similar_ips=similar_ips) def stream_bot_output(bot_id, process): def stream_to_socket(pipe, log_type): try: for line in iter(pipe.readline, ''): log_entry = {'type': log_type, 'data': line} if bot_id in running_bots: running_bots[bot_id]['history'].append(log_entry) max_history_lines = 1000 if len(running_bots[bot_id]['history']) > max_history_lines: running_bots[bot_id]['history'].pop(0) socketio.emit('console_output', log_entry, room=bot_id) except Exception as e: print(f"Stream-Fehler für Bot {bot_id}: {e}") finally: if pipe: pipe.close() stdout_thread = threading.Thread(target=stream_to_socket, args=(process.stdout, 'stdout')) stderr_thread = threading.Thread(target=stream_to_socket, args=(process.stderr, 'stderr')) stdout_thread.start() stderr_thread.start() process.wait() if bot_id in running_bots: del running_bots[bot_id] if discord_bots_collection: discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'stopped'}}) socketio.emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id) socketio.emit('console_output', {'bot_id': bot_id, 'type': 'system', 'data': '--- Bot-Prozess beendet ---'}, room=bot_id) @app.route('/discord_bots') @login_required @permission_required('manage_discord_bots') def discord_bots(): bots = list(discord_bots_collection.find()) for bot in bots: bot['status'] = 'running' if str(bot['_id']) in running_bots else 'stopped' return render_template('discord_bots.html', active_page='discord_bots', bots=bots) @app.route('/upload_bot', methods=['POST']) @login_required @permission_required('manage_discord_bots') def upload_bot(): bot_name = request.form.get('bot_name') bot_file = request.files.get('bot_file') if not bot_name or not bot_file or bot_file.filename == '': flash("Bot-Name und Datei sind erforderlich.", "danger") return redirect(url_for('discord_bots')) if discord_bots_collection.find_one({'name': bot_name}): flash("Ein Bot mit diesem Namen existiert bereits.", "danger") return redirect(url_for('discord_bots')) filename = secure_filename(bot_file.filename) filepath = os.path.join(app.config['BOT_UPLOAD_FOLDER'], filename) bot_file.save(filepath) discord_bots_collection.insert_one({ 'name': bot_name, 'filename': filename, 'filepath': filepath, 'created_at': datetime.utcnow(), 'status': 'stopped' }) flash(f"Bot '{bot_name}' wurde erfolgreich hochgeladen.", "success") return redirect(url_for('discord_bots')) @app.route('/delete_bot/', methods=['POST']) @login_required @permission_required('manage_discord_bots') def delete_bot(bot_id): bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)}) if not bot_info: flash("Bot nicht gefunden.", "danger") return redirect(url_for('discord_bots')) if bot_id in running_bots: process = running_bots[bot_id]['process'] try: process.terminate() process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() del running_bots[bot_id] socketio.emit('status_update', {'bot_id': bot_id, 'status': 'deleted'}, room=bot_id) try: filepath = bot_info.get('filepath') if filepath and os.path.exists(filepath): os.remove(filepath) except Exception as e: flash(f"Fehler beim Löschen der Bot-Datei: {e}", "danger") discord_bots_collection.delete_one({'_id': ObjectId(bot_id)}) flash(f"Bot '{bot_info['name']}' wurde erfolgreich gelöscht.", "success") return redirect(url_for('discord_bots')) @app.route('/bot_console/') @login_required @permission_required('manage_discord_bots') def bot_console(bot_id): bot = discord_bots_collection.find_one({'_id': ObjectId(bot_id)}) if not bot: flash("Bot nicht gefunden.", "danger") return redirect(url_for('discord_bots')) bot['status'] = 'running' if bot_id in running_bots else 'stopped' return render_template('bot_console.html', bot=bot, active_page='discord_bots') @socketio.on('connect') def handle_connect(): print('Client verbunden') @socketio.on('join_console') def handle_join_console(data): bot_id = data['bot_id'] join_room(bot_id) emit('console_output', {'type': 'system', 'data': f'Verbunden mit Konsole für Bot {bot_id}.'}) if bot_id in running_bots and 'history' in running_bots[bot_id]: for log in running_bots[bot_id]['history']: emit('console_output', log) @socketio.on('leave_console') def handle_leave_console(data): bot_id = data['bot_id'] leave_room(bot_id) print(f'Client hat Konsole für Bot {bot_id} verlassen.') @socketio.on('toggle_bot') def handle_toggle_bot(data): bot_id = data['bot_id'] action = data['action'] bot_info = discord_bots_collection.find_one({'_id': ObjectId(bot_id)}) if not bot_info: emit('console_output', {'type': 'error', 'data': 'Bot nicht in der Datenbank gefunden.'}, room=bot_id) return if action == 'start' and bot_id not in running_bots: filepath = bot_info.get('filepath') if not os.path.exists(filepath): emit('console_output', {'type': 'error', 'data': f'Bot-Datei nicht gefunden: {filepath}'}, room=bot_id) return try: process = subprocess.Popen( ['python', '-u', filepath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, encoding='utf-8', errors='replace' ) running_bots[bot_id] = {'process': process, 'history': []} thread = threading.Thread(target=stream_bot_output, args=(bot_id, process)) thread.daemon = True thread.start() discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'running'}}) emit('status_update', {'bot_id': bot_id, 'status': 'running'}, room=bot_id) emit('console_output', {'type': 'system', 'data': f'--- Bot-Prozess gestartet (PID: {process.pid}) ---'}, room=bot_id) except Exception as e: emit('console_output', {'type': 'error', 'data': f'Fehler beim Starten des Bots: {e}'}, room=bot_id) elif action == 'stop' and bot_id in running_bots: process = running_bots[bot_id]['process'] try: process.terminate() process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() if bot_id in running_bots: del running_bots[bot_id] discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'stopped'}}) emit('status_update', {'bot_id': bot_id, 'status': 'stopped'}, room=bot_id) emit('console_output', {'type': 'system', 'data': '--- Bot-Prozess wird gestoppt... ---'}, room=bot_id) @app.route('/maintenance') def maintenance(): maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {} if not maint_info or not maint_info.get('enabled', False): return redirect(url_for('login')) return render_template('maintenance.html', maint_info=maint_info) @app.route('/maintenance_login', methods=['GET', 'POST']) def maintenance_login(): if 'user_id' in session: return redirect(url_for('dashboard')) maintenance_status = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else None if not maintenance_status or not maintenance_status.get('enabled', False): return redirect(url_for('login')) if request.method == 'POST': username = request.form.get('username') password = request.form.get('password') if not username or not password: flash("Benutzername und Passwort sind erforderlich.", "danger") return render_template('maintenance_login.html') if accounts_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return render_template('maintenance_login.html'), 500 user = accounts_collection.find_one({'username': username}) if user and check_password_hash(user['password'], password): user_perms = get_user_permissions(str(user['_id'])) if 'bypass_maintenance' in user_perms: session.permanent = True session['user_id'] = str(user['_id']) session['username'] = user['username'] session['permissions'] = user_perms flash("Erfolgreich angemeldet!", "success") return redirect(url_for('dashboard')) else: flash("Dieser Account hat keine Berechtigung für den Wartungs-Login.", "danger") return render_template('maintenance_login.html') else: flash("Ungültiger Benutzername oder Passwort.", "danger") return render_template('maintenance_login.html') return render_template('maintenance_login.html') @app.route('/maintenance_management') @login_required @permission_required('maintenance_management') def maintenance_management(): maint_info = dashboard_collection.find_one({"key": "scheduled_maintenance"}) if dashboard_collection is not None else {} if maint_info and maint_info.get('time'): maint_info['time_local_str'] = maint_info['time'].strftime('%Y-%m-%dT%H:%M') if 'discord_bots_maintenance' not in maint_info: maint_info['discord_bots_maintenance'] = False return render_template('maintenance_management.html', active_page='maintenance_management', maint_info=maint_info) @app.route('/update_maintenance', methods=['POST']) @login_required @permission_required('maintenance_management') def update_maintenance(): if dashboard_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('maintenance_management')) is_enabled = request.form.get('maintenance_enabled') == 'on' status_message = request.form.get('status_message', 'Keine') duration = request.form.get('duration_minutes') time_str = request.form.get('maintenance_time') discord_bots_maintenance = request.form.get('discord_bots_maintenance') == 'on' update_data = { "enabled": is_enabled, "status": status_message, "duration_minutes": int(duration) if duration and duration.isdigit() else None, "time": datetime.fromisoformat(time_str) if time_str else None, "discord_bots_maintenance": discord_bots_maintenance } dashboard_collection.update_one( {"key": "scheduled_maintenance"}, {"$set": update_data}, upsert=True ) flash("Wartungseinstellungen wurden aktualisiert.", "success") return redirect(url_for('maintenance_management')) if __name__ == '__main__': app.config['SESSION_COOKIE_SECURE'] = False print("INFO: Starte den Server im Entwicklungsmodus ohne HTTPS.") with app.app_context(): start_bots_on_startup() socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)