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, send_file 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 socket import zipfile 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 = '/home/astraos/Astra-OS-Panel/bots' try: os.makedirs(BOT_UPLOAD_FOLDER, exist_ok=True) try: os.chmod(BOT_UPLOAD_FOLDER, 0o755) except Exception: pass except Exception as e: BOT_UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Bots') os.makedirs(BOT_UPLOAD_FOLDER, exist_ok=True) print(f"WARNUNG: Konnte '/opt/astraos/Discord_Bots' nicht erstellen ({e}), benutze lokalen Ordner: {BOT_UPLOAD_FOLDER}") app.config['BOT_UPLOAD_FOLDER'] = BOT_UPLOAD_FOLDER ETS2_DLC_ARCHIVE_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ets2_dlc_archives') os.makedirs(ETS2_DLC_ARCHIVE_FOLDER, exist_ok=True) running_bots = {} ets2_dlc_build_locks = {} ets2_dlc_build_locks_guard = threading.Lock() 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"] ets2_dlcs_collection = db["ets2_dlcs_packages"] ip_analyzer_results_collection = db["ip_analyzer_results"] age_verification_collection = db["age_verification"] 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_ets2_dlcs", "manage_ets2_dlcs", "events", "instranet", "view_ip_analyzer", "view_age_verification", "manage_age_verification" ] 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.") else: # Bestehende Owner-Rollen auf neue Permissions anheben. roles_collection.update_many( {"name": "Owner"}, {"$addToSet": {"permissions": {"$each": AVAILABLE_PERMISSIONS}}} ) 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 ets2_dlcs_collection = None ip_analyzer_results_collection = None print(f"FATAL: Verbindung zur MongoDB fehlgeschlagen: {e}") db = None accounts_collection = None logs_collection = None ets2_dlcs_collection = None def _venv_paths(): venv_bin = "/home/astraos/Astra-OS-Panel/venv/bin" return { "python": os.path.join(venv_bin, "python3"), # pip NICHT direkt verwenden (Shebang kann auf falschen Interpreter zeigen) "pip": os.path.join(venv_bin, "pip"), } def _build_bot_command(filepath: str): venv = _venv_paths() vpy = venv["python"] # Keine Auto-Installation bei jedem Start; nur Bot ausführen. return f"{vpy} -u {filepath}" def _spawn_bot_process(bot_id: str, filepath: str, bot_name: str = "N/A", emit_to_socket: bool = False): command = _build_bot_command(filepath) if emit_to_socket: socketio.emit('console_output', {'type': 'system', 'data': f'[startup] Command: {command}\n'}, room=bot_id) process = subprocess.Popen( command, shell=True, executable='/bin/bash', 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() if discord_bots_collection is not None: discord_bots_collection.update_one({'_id': ObjectId(bot_id)}, {'$set': {'status': 'running'}}) if emit_to_socket: socketio.emit('status_update', {'bot_id': bot_id, 'status': 'running'}, room=bot_id) socketio.emit('console_output', {'type': 'system', 'data': f'--- Bot-Prozess gestartet (PID: {process.pid}) ---'}, room=bot_id) print(f"INFO: Bot '{bot_name}' (PID: {process.pid}) wurde gestartet.") return process 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: _spawn_bot_process(bot_id, filepath, bot_name=bot_info.get('name', 'N/A'), emit_to_socket=False) except Exception as e: print(f"FEHLER: Bot '{bot_info.get('name','N/A')}' konnte nicht gestartet werden: {e}") 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 and running_bots.get(bot_id) is not None: del running_bots[bot_id] if discord_bots_collection is not None: 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) if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': with app.app_context(): start_bots_on_startup() 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 add_log(action, details=None): if 'username' in session and logs_collection is not None: log_entry = { 'username': session['username'], 'action': action, 'details': details, 'timestamp': datetime.utcnow() } logs_collection.insert_one(log_entry) def _ets2_archive_filename(package_id: str, package_name: str): safe_name = secure_filename(package_name or 'ets2_dlc_package') or 'ets2_dlc_package' return f'{package_id}_{safe_name}.zip' def _ets2_archive_path(package_id: str, package_name: str): return os.path.join(ETS2_DLC_ARCHIVE_FOLDER, _ets2_archive_filename(package_id, package_name)) def _get_ets2_build_lock(package_id: str): with ets2_dlc_build_locks_guard: lock = ets2_dlc_build_locks.get(package_id) if lock is None: lock = threading.Lock() ets2_dlc_build_locks[package_id] = lock return lock def _set_ets2_archive_state(package_id: str, **fields): if ets2_dlcs_collection is None: return ets2_dlcs_collection.update_one({'_id': ObjectId(package_id)}, {'$set': fields}) def _build_ets2_archive(package_id: str): if ets2_dlcs_collection is None: return package = ets2_dlcs_collection.find_one({'_id': ObjectId(package_id)}) if not package: return folder_path = package.get('folder_path') if not folder_path or not os.path.isdir(folder_path): _set_ets2_archive_state( package_id, archive_status='error', archive_error='Ordnerpfad ist nicht mehr verfügbar.', archive_generated_at=None, archive_path=None ) return archive_path = _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) temp_archive_path = f'{archive_path}.tmp' try: if os.path.exists(temp_archive_path): try: os.remove(temp_archive_path) except Exception: pass _set_ets2_archive_state( package_id, archive_status='building', archive_error=None, archive_path=archive_path, archive_generated_at=None ) with zipfile.ZipFile(temp_archive_path, 'w', compression=zipfile.ZIP_STORED, allowZip64=True) as zipf: has_content = False for root, _, files in os.walk(folder_path): for filename in files: file_path = os.path.join(root, filename) arcname = os.path.relpath(file_path, folder_path) zipf.write(file_path, arcname, compress_type=zipfile.ZIP_STORED) has_content = True if not has_content: zipf.writestr('EMPTY_PACKAGE.txt', 'Dieses DLC-Paket enthaelt aktuell keine Dateien.') os.replace(temp_archive_path, archive_path) _set_ets2_archive_state( package_id, archive_status='ready', archive_error=None, archive_path=archive_path, archive_generated_at=datetime.utcnow() ) except Exception as e: try: if os.path.exists(temp_archive_path): os.remove(temp_archive_path) except Exception: pass _set_ets2_archive_state( package_id, archive_status='error', archive_error=str(e), archive_generated_at=None, archive_path=archive_path ) def _schedule_ets2_archive_build(package_id: str): lock = _get_ets2_build_lock(package_id) if not lock.acquire(blocking=False): return False def runner(): try: _build_ets2_archive(package_id) finally: try: lock.release() except Exception: pass with ets2_dlc_build_locks_guard: ets2_dlc_build_locks.pop(package_id, None) thread = threading.Thread(target=runner, daemon=True) thread.start() return True def _format_ets2_dlcs_for_view(packages): for package in packages: dlc_date = package.get('dlc_date') try: package['dlc_date_display'] = datetime.strptime(dlc_date, '%Y-%m-%d').strftime('%d.%m.%Y') if dlc_date else 'N/A' except ValueError: package['dlc_date_display'] = str(dlc_date) if dlc_date else 'N/A' package_id = str(package['_id']) archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) archive_ready = package.get('archive_status') == 'ready' and os.path.isfile(archive_path) folder_path = package.get('folder_path') if not archive_ready and folder_path and os.path.isdir(folder_path): package.setdefault('archive_status', 'pending') package['download_ready'] = False package['archive_status_display'] = 'Wird erstellt' package['archive_path'] = archive_path _schedule_ets2_archive_build(package_id) elif archive_ready: package['download_ready'] = True package['archive_status_display'] = 'Bereit' package['archive_path'] = archive_path else: package['download_ready'] = False package['archive_status_display'] = 'Fehler' package['archive_path'] = archive_path if package.get('archive_status') == 'error' and package.get('archive_error'): package['archive_status_display'] = f"Fehler: {package.get('archive_error')}" return packages 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')) 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'])) add_log("Benutzer-Login") 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(): add_log("Benutzer-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 }) add_log("Benutzer erstellt", f"Benutzername: {username}") if dashboard_collection is not None: dashboard_collection.update_one( {"key": "last_user_created"}, {"$set": { "time": datetime.utcnow(), "by": session.get('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} ) add_log("Benutzer bearbeitet", f"Benutzername: {username}") 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')) user = accounts_collection.find_one({'_id': ObjectId(user_id)}) accounts_collection.delete_one({'_id': ObjectId(user_id)}) add_log("Benutzer gelöscht", f"Benutzername: {user.get('username', 'N/A')}") 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}}) add_log("Passwort geändert") 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}} ) add_log("Discord-Verknüpfung aufgehoben") 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') if 'user_id' in session: user_id = session['user_id'] 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 }} ) add_log("Discord verknüpft") flash("Discord-Account erfolgreich verknüpft!", "success") return redirect(url_for('dashboard')) 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'])) add_log("Benutzer-Login via Discord") 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 }) add_log("Rolle erstellt", f"Rolle: {role_name}") 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}} ) add_log("Rolle bearbeitet", f"Rolle: {role_name}") 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)}) add_log("Rolle gelöscht", f"Rolle: {role.get('name', 'N/A')}") flash("Rolle wurde gelöscht.", "success") return redirect(url_for('role_management')) def calculate_current_age(birthdate_str): try: birthdate_dt = datetime.strptime(birthdate_str, '%d.%m.%Y') except ValueError: birthdate_dt = datetime.strptime(birthdate_str, '%Y-%m-%d') today = datetime.today() return today.year - birthdate_dt.year - ((today.month, today.day) < (birthdate_dt.month, birthdate_dt.day)) @app.route('/age_verification', methods=['GET', 'POST']) @login_required @permission_required('view_age_verification') def age_verification(): result = None search_query = request.args.get('search', '') if request.method == 'POST': birthdate_str = request.form.get('birthdate') steam_id = request.form.get('steam_id') ingame_name = request.form.get('ingame_name') if birthdate_str and steam_id and ingame_name: try: birthdate_dt = datetime.strptime(birthdate_str, '%d.%m.%Y') today = datetime.today() age = today.year - birthdate_dt.year - ((today.month, today.day) < (birthdate_dt.month, birthdate_dt.day)) user_data = { 'birthdate': birthdate_str, 'steam_id': steam_id, 'ingame_name': ingame_name, 'age': age, 'checked_by': session.get('username'), 'timestamp': datetime.utcnow() } age_verification_collection.insert_one(user_data) result = {'age': age, 'steam_id': steam_id} if age < 13: thirteenth_birthday = birthdate_dt.replace(year=birthdate_dt.year + 13) ban_duration_days = (thirteenth_birthday - today).days + 1 result['ban_duration_days'] = ban_duration_days result['ban_command'] = f"oban {steam_id} {ban_duration_days}d" add_log("Altersüberprüfung hinzugefügt", f"Ingame Name: {ingame_name}, SteamID: {steam_id}") flash("Altersüberprüfung erfolgreich durchgeführt und gespeichert.", "success") except ValueError: flash("Ungültiges Datumsformat. Bitte DD.MM.YYYY verwenden.", "danger") if search_query: users = list(age_verification_collection.find({ "$or": [ {"ingame_name": {"$regex": search_query, "$options": "i"}}, {"steam_id": {"$regex": search_query, "$options": "i"}} ] }).sort("timestamp", -1)) else: users = list(age_verification_collection.find().sort("timestamp", -1).limit(10)) for user in users: user['age'] = calculate_current_age(user['birthdate']) return render_template('age_verification.html', active_page='age_verification', result=result, users=users, search_query=search_query) @app.route('/age_verification/') @login_required @permission_required('view_age_verification') def view_user_age_details(user_id): user = age_verification_collection.find_one({'_id': ObjectId(user_id)}) if not user: flash("Benutzer nicht gefunden.", "danger") return redirect(url_for('age_verification')) user['age'] = calculate_current_age(user['birthdate']) result = None if user.get('age') < 13: today = datetime.today() try: birthdate_dt = datetime.strptime(user['birthdate'], '%d.%m.%Y') except ValueError: birthdate_dt = datetime.strptime(user['birthdate'], '%Y-%m-%d') thirteenth_birthday = birthdate_dt.replace(year=birthdate_dt.year + 13) ban_duration_days = (thirteenth_birthday - today).days + 1 if ban_duration_days > 0: result = { 'ban_duration_days': ban_duration_days, 'ban_command': f"oban {user['steam_id']} {ban_duration_days}d" } return render_template('view_user_age_details.html', active_page='age_verification', user=user, result=result) @app.route('/age_verification/delete/', methods=['POST']) @login_required @permission_required('view_age_verification') def delete_age_verification(user_id): entry = age_verification_collection.find_one({'_id': ObjectId(user_id)}) if not entry: flash("Eintrag nicht gefunden.", "danger") return redirect(url_for('age_verification')) can_delete = False if 'manage_age_verification' in session.get('permissions', []): can_delete = True elif entry.get('checked_by') == session.get('username'): can_delete = True if not can_delete: flash("Sie haben keine Berechtigung, diesen Eintrag zu löschen.", "danger") return redirect(url_for('view_user_age_details', user_id=user_id)) age_verification_collection.delete_one({'_id': ObjectId(user_id)}) add_log("Altersüberprüfung gelöscht", f"Ingame Name: {entry.get('ingame_name', 'N/A')}, SteamID: {entry.get('steam_id', 'N/A')}") flash("Eintrag zur Altersüberprüfung wurde gelöscht.", "success") return redirect(url_for('age_verification')) @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() }) add_log("IP analysiert", f"IP: {ip_address}") 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) @app.route('/logs') @login_required @permission_required('view_logs') def logs(): page = request.args.get('page', 1, type=int) per_page = 25 skip = (page - 1) * per_page log_entries = list(logs_collection.find().sort('timestamp', -1).skip(skip).limit(per_page)) total_logs = logs_collection.count_documents({}) total_pages = (total_logs + per_page - 1) // per_page return render_template('logs.html', logs=log_entries, page=page, total_pages=total_pages, active_page='logs') @app.route('/ets2_dlcs') @login_required @permission_required('view_ets2_dlcs') def ets2_dlcs(): if ets2_dlcs_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('dashboard')) packages = _format_ets2_dlcs_for_view(list(ets2_dlcs_collection.find().sort('created_at', -1))) return render_template( 'ets2_dlcs.html', active_page='ets2_dlcs', dlc_packages=packages, can_manage_ets2_dlcs='manage_ets2_dlcs' in session.get('permissions', []) ) @app.route('/ets2_dlcs/create', methods=['POST']) @login_required @permission_required('manage_ets2_dlcs') def create_ets2_dlc_package(): if ets2_dlcs_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('ets2_dlcs')) package_name = (request.form.get('package_name') or '').strip() folder_path_input = (request.form.get('folder_path') or '').strip() dlc_date = (request.form.get('dlc_date') or '').strip() if not package_name or not folder_path_input or not dlc_date: flash("Name, Ordnerpfad und Datum sind erforderlich.", "danger") return redirect(url_for('ets2_dlcs')) try: datetime.strptime(dlc_date, '%Y-%m-%d') except ValueError: flash("Ungültiges Datumsformat. Bitte YYYY-MM-DD verwenden.", "danger") return redirect(url_for('ets2_dlcs')) folder_path = os.path.abspath(folder_path_input) if not os.path.isdir(folder_path): flash("Der angegebene Ordnerpfad existiert nicht oder ist kein Ordner.", "danger") return redirect(url_for('ets2_dlcs')) if ets2_dlcs_collection.find_one({'name': package_name}): flash("Ein DLC-Paket mit diesem Namen existiert bereits.", "danger") return redirect(url_for('ets2_dlcs')) package_id = ets2_dlcs_collection.insert_one({ 'name': package_name, 'folder_path': folder_path, 'dlc_date': dlc_date, 'created_by': session.get('username'), 'created_at': datetime.utcnow(), 'archive_status': 'pending', 'archive_path': None, 'archive_generated_at': None, 'archive_error': None }).inserted_id add_log("ETS2 DLC-Paket erstellt", f"Name: {package_name}, Pfad: {folder_path}") _schedule_ets2_archive_build(str(package_id)) flash("DLC-Paket wurde erstellt. Das Archiv wird im Hintergrund vorbereitet.", "success") return redirect(url_for('ets2_dlcs')) @app.route('/ets2_dlcs/delete/', methods=['POST']) @login_required @permission_required('manage_ets2_dlcs') def delete_ets2_dlc_package(package_id): if ets2_dlcs_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('ets2_dlcs')) try: package_object_id = ObjectId(package_id) except Exception: flash("Ungültige Paket-ID.", "danger") return redirect(url_for('ets2_dlcs')) package = ets2_dlcs_collection.find_one({'_id': package_object_id}) if not package: flash("DLC-Paket nicht gefunden.", "danger") return redirect(url_for('ets2_dlcs')) archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) if package.get('archive_status') == 'ready' and os.path.isfile(archive_path): try: os.remove(archive_path) except Exception: pass ets2_dlcs_collection.delete_one({'_id': package_object_id}) add_log("ETS2 DLC-Paket gelöscht", f"Name: {package.get('name', 'N/A')}") flash("DLC-Paket wurde gelöscht.", "success") return redirect(url_for('ets2_dlcs')) @app.route('/ets2_dlcs/download/') @login_required @permission_required('view_ets2_dlcs') def download_ets2_dlc_package(package_id): if ets2_dlcs_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('ets2_dlcs')) try: package_object_id = ObjectId(package_id) except Exception: flash("Ungültige Paket-ID.", "danger") return redirect(url_for('ets2_dlcs')) package = ets2_dlcs_collection.find_one({'_id': package_object_id}) if not package: flash("DLC-Paket nicht gefunden.", "danger") return redirect(url_for('ets2_dlcs')) archive_path = package.get('archive_path') or _ets2_archive_path(package_id, package.get('name', 'ets2_dlc_package')) if package.get('archive_status') != 'ready' or not os.path.isfile(archive_path): if package.get('folder_path') and os.path.isdir(package.get('folder_path')): _schedule_ets2_archive_build(package_id) flash("Das DLC-Archiv wird noch erstellt. Bitte in ein paar Augenblicken erneut versuchen.", "info") else: flash("Das gespeicherte Archiv ist nicht verfügbar.", "danger") return redirect(url_for('ets2_dlcs')) safe_name = secure_filename(package.get('name', 'ets2_dlc_package')) or 'ets2_dlc_package' add_log("ETS2 DLC-Paket heruntergeladen", f"Name: {package.get('name', 'N/A')}") return send_file(archive_path, as_attachment=True, download_name=f"{safe_name}.zip", conditional=True) 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 and running_bots.get(bot_id) is not None: del running_bots[bot_id] if discord_bots_collection is not None: 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' }) add_log("Bot hochgeladen", f"Bot Name: {bot_name}") 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 and running_bots.get(bot_id) is not None: 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)}) add_log("Bot gelöscht", f"Bot Name: {bot_info.get('name', 'N/A')}") 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 running_bots.get(bot_id) is not None 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: _spawn_bot_process(bot_id, filepath, bot_name=bot_info.get('name', 'N/A'), emit_to_socket=True) add_log("Bot gestartet", f"Bot Name: {bot_info.get('name', 'N/A')}") 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 and running_bots.get(bot_id) is not None: 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'}}) add_log("Bot gestoppt", f"Bot Name: {bot_info.get('name', 'N/A')}") 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('/impressum') def impressum(): # Impressumsdaten werden aus Umgebungsvariablen geladen, damit keine persönlichen Daten im Code liegen. impressum_data = { 'name': os.environ.get('IMPRESSUM_NAME'), 'address': os.environ.get('IMPRESSUM_ADDRESS'), 'email': os.environ.get('IMPRESSUM_EMAIL'), 'phone': os.environ.get('IMPRESSUM_PHONE'), 'company_registration': os.environ.get('IMPRESSUM_COMPANY_REG') } return render_template('impressum.html', active_page='impressum', impressum=impressum_data) @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 ) add_log("Wartungseinstellungen aktualisiert", f"Aktiviert: {is_enabled}, Nachricht: {status_message}") flash("Wartungseinstellungen wurden aktualisiert.", "success") return redirect(url_for('maintenance_management')) @app.errorhandler(404) def page_not_found(e): return render_template('404.html'), 404 if __name__ == '__main__': app.config['SESSION_COOKIE_SECURE'] = False print("INFO: Starte den Server im Entwicklungsmodus ohne HTTPS.") socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)