From 1d6ad149b42c888bbdca6bd3bde56bc0b52b45e4 Mon Sep 17 00:00:00 2001 From: MrWhiff Date: Tue, 9 Dec 2025 17:25:54 +0100 Subject: [PATCH] =?UTF-8?q?Astra=20OS=20Ver.=201.2:=20-=20Vorbereitung=20f?= =?UTF-8?q?=C3=BCr=20Prod=20HTTPS=20Usage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AstraOS/.env | 7 + AstraOS/astra_os.py | 248 +++-------------------------------- AstraOS/requirements.txt | 12 ++ AstraOS/start_prod_server.py | 35 +++++ AstraOS/wsgi.py | 4 + 5 files changed, 73 insertions(+), 233 deletions(-) create mode 100644 AstraOS/.env create mode 100644 AstraOS/requirements.txt create mode 100644 AstraOS/start_prod_server.py create mode 100644 AstraOS/wsgi.py diff --git a/AstraOS/.env b/AstraOS/.env new file mode 100644 index 0000000..c72aa3f --- /dev/null +++ b/AstraOS/.env @@ -0,0 +1,7 @@ +SECRET_KEY='super-secret-key-that-you-should-change' +MONGO_URI='mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/' +SSH_HOST='89.144.42.175' +SSH_USER='root' +SSH_PASS='iwkms@1812:(' +VPNAPI_KEY='015ada9472664329affb2f26d7036d47' + diff --git a/AstraOS/astra_os.py b/AstraOS/astra_os.py index c8af718..e9c46ec 100644 --- a/AstraOS/astra_os.py +++ b/AstraOS/astra_os.py @@ -17,25 +17,18 @@ import requests import ipaddress import socket import ssl - -# Verwenden Sie eventlet für die Produktion -async_mode = "threading" +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) - -# Verzeichnis für hochgeladene Bots 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 - -# Dictionary zum Speichern laufender Bot-Prozesse running_bots = {} - -# Filter, um die GET /api/server_load und GET /api/system_status Anfragen aus den Logs auszublenden class SuppressServerLoadFilter(logging.Filter): def filter(self, record): msg = record.getMessage() @@ -43,20 +36,14 @@ class SuppressServerLoadFilter(logging.Filter): "GET /api/server_load" not in msg and "GET /api/system_status" not in msg ) - -# Den Filter zum Werkzeug-Logger hinzufügen log = logging.getLogger('werkzeug') log.addFilter(SuppressServerLoadFilter()) - - -app.secret_key = os.urandom(32) +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 @@ -65,9 +52,8 @@ def inject_utility_processor(): timedelta=timedelta, maintenance_active=is_maintenance_active ) - try: - client = MongoClient("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/") + client = MongoClient(os.environ.get("MONGO_URI")) db = client["astra_os"] accounts_collection = db["accounts"] logs_collection = db["logs"] @@ -76,7 +62,6 @@ try: 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", @@ -89,7 +74,6 @@ try: "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([ @@ -105,7 +89,6 @@ try: } ]) 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"}) @@ -117,8 +100,6 @@ try: 'creation_date': datetime.utcnow() }) print("INFO: Standard-Admin-Benutzer ('admin', Passwort: '1234') mit Rolle 'Owner' wurde erstellt.") - - # Standard-Dashboard-Einträge anlegen, falls Collection leer if dashboard_collection.count_documents({}) == 0: print("INFO: Leere dashboard_stats Collection - lege Standardstatistiken an.") dashboard_collection.insert_many([ @@ -130,7 +111,6 @@ try: {"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 @@ -144,16 +124,13 @@ except Exception as e: db = None accounts_collection = None logs_collection = None - -# MongoDB für Tickets (chainsaw_dc_data) try: - tickets_client = MongoClient("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/") + 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: @@ -162,26 +139,19 @@ def get_user_permissions(user_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')) - - # Berechtigungen bei jeder Anfrage aktualisieren, um Änderungen sofort zu übernehmen session['permissions'] = get_user_permissions(session['user_id']) - - # Wartungsmodus-Check 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) @@ -195,37 +165,28 @@ def permission_required(permission): 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')) - - # Wartungsmodus-Check vor dem Login-Versuch 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']) @@ -236,22 +197,17 @@ def login(): 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(): - # SSH zu 89.144.42.175, user root, pw iwkms@1812:( try: ssh = paramiko.SSHClient() ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - # Timeout zur Verbindung hinzugefügt, um Hängenbleiben zu verhindern - ssh.connect("89.144.42.175", username="root", password="iwkms@1812:(", timeout=10) + 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() - # Parse CPU usage import re match = re.search(r"(\d+\.\d+)\s*id", output) if match: @@ -269,22 +225,17 @@ def api_server_load(): 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(): - # Wartungsstatus prüfen 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 - - # SCP:SL Server Status (UDP-Ping statt TCP) 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)) - # Wenn keine Exception, dann ist der Port erreichbar (Server antwortet nicht, aber Port offen) scpsl_status = "online" udp_sock.close() except Exception as e: @@ -292,8 +243,6 @@ def api_system_status(): scpsl_status = "offline" if maintenance_active: scpsl_status = "wartung" - - # Gamepanel Status gamepanel_status = "offline" try: resp = requests.get("https://gamepanel.mrsniff.de", timeout=3) @@ -303,15 +252,10 @@ def api_system_status(): gamepanel_status = "offline" if maintenance_active: gamepanel_status = "wartung" - - # Discord Bots Status (online/wartung) discord_bots_status = "wartung" if discord_bots_maintenance or maintenance_active else "online" - - # Datenbank Status db_status = "online" if db is not None else "offline" if maintenance_active: db_status = "wartung" - return jsonify({ "scpsl": scpsl_status, "gamepanel": gamepanel_status, @@ -319,41 +263,28 @@ def api_system_status(): "database": db_status, "maintenance": maintenance_active }) - @app.route('/status') @login_required def status_page(): - # Wartungsstatus prüfen 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(): - # Aktive Benutzer user_count = accounts_collection.count_documents({}) if accounts_collection is not None else 1 - - # Letzte Benutzererstellung 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 - - # Offene Tickets open_tickets = 0 if tickets_collection is not None: open_tickets = tickets_collection.count_documents({"status": "open"}) - - # Server-Auslastung (wird per AJAX geladen, hier nur Platzhalter) server_load = {"percent": 0, "status": "Lädt..."} - - # Systemstatus per API holen (wie Status-Seite) 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() - # Priorität: Wartung > Wartungsmodus > Offline > Online if data.get("maintenance"): system_status = {"status": "Online (Wartungsarbeiten)"} elif any(data.get(k) == "wartung" for k in ["scpsl", "gamepanel", "discord_bots", "database"]): @@ -366,8 +297,6 @@ def dashboard(): system_status = {"status": "Offline"} except Exception: system_status = {"status": "Offline"} - - # Geplante Wartung (aus dashboard_stats falls vorhanden) 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: @@ -378,7 +307,6 @@ def dashboard(): 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, @@ -391,18 +319,13 @@ def dashboard(): "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')) - -# --- User Management --- - @app.route('/user_management') @login_required @permission_required('view_user_management') @@ -410,17 +333,11 @@ 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()) - - # Konvertiere ObjectId in String für die JSON-Serialisierung in roles_list for role in roles_list: role['_id'] = str(role['_id']) - role_map = {role['_id']: role['name'] for role in roles_list} - - # Daten für das Alpine.js Modal vorbereiten (ohne Datum) users_for_modal = {} for user in users_list: user_id_str = str(user['_id']) @@ -429,18 +346,14 @@ def user_management(): 'username': user['username'], 'role_id': str(user.get('role_id')) if user.get('role_id') else None } - - # Erstelle eine separate Liste für die Anzeige in der Tabelle, die das Datum enthält 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') - 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') @@ -448,19 +361,15 @@ 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, @@ -468,7 +377,6 @@ def add_user(): 'role_id': ObjectId(role_id), 'creation_date': datetime.utcnow() }) - # Dashboard-Statistik aktualisieren if dashboard_collection is not None: dashboard_collection.update_one( {"key": "last_user_created"}, @@ -478,7 +386,6 @@ def add_user(): }}, upsert=True ) - # Aktive Benutzer aktualisieren user_count = accounts_collection.count_documents({}) dashboard_collection.update_one( {"key": "active_users"}, @@ -487,7 +394,6 @@ def add_user(): ) 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') @@ -495,38 +401,28 @@ 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')) - - # Überprüfen, ob der neue Benutzername bereits von einem anderen Benutzer verwendet wird 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) } - - # Passwort nur aktualisieren, wenn ein neues eingegeben wurde 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') @@ -534,17 +430,12 @@ 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')) - -# --- Role Management --- - @app.route('/role_management') @login_required @permission_required('view_role_management') @@ -552,29 +443,22 @@ def role_management(): if roles_collection is None: flash("Datenbankverbindung nicht verfügbar.", "danger") return redirect(url_for('dashboard')) - roles = list(roles_collection.find()) - # Konvertiere ObjectId in String für die JSON-Serialisierung im Template 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, @@ -582,25 +466,21 @@ def add_role(): }) 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') @@ -609,12 +489,9 @@ def delete_role(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')) - -# --- IP Analyzer --- @app.route('/ip_analyzer', methods=['GET', 'POST']) @login_required @permission_required('view_ip_analyzer') @@ -622,29 +499,23 @@ 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 = "015ada9472664329affb2f26d7036d47" # API-Schlüssel aus altem Code + 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() - - # Sicherstellen, dass verschachtelte Objekte existieren ip_data.setdefault('security', {}) ip_data.setdefault('location', {}) ip_data.setdefault('network', {}) - - # Ähnliche IPs finden similar_ips = list(ip_analyzer_results_collection.find({ "$or": [ {"ip": ip_address}, @@ -652,8 +523,6 @@ def ip_analyzer(): {"data.network.autonomous_system_number": ip_data.get('network', {}).get('autonomous_system_number')} ] })) - - # Neuen Eintrag speichern ip_analyzer_results_collection.insert_one({ "ip": ip_address, "is_cheater": is_cheater, @@ -669,51 +538,36 @@ def ip_analyzer(): 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) - - -# --- Discord Bot Management --- - def stream_bot_output(bot_id, process): - """Liest stdout/stderr eines Prozesses, speichert den Verlauf und sendet es via SocketIO.""" def stream_to_socket(pipe, log_type): try: for line in iter(pipe.readline, ''): log_entry = {'type': log_type, 'data': line} - # Verlauf speichern, wenn der Bot noch läuft if bot_id in running_bots: running_bots[bot_id]['history'].append(log_entry) - # Optional: Begrenzen Sie die Größe des Verlaufs, um Speicher zu sparen 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() # Warten, bis der Prozess endet - - # Prozess beendet, Status aktualisieren + process.wait() if bot_id in running_bots: del running_bots[bot_id] 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') @@ -722,36 +576,29 @@ def discord_bots(): 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() }) - 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') @@ -760,8 +607,6 @@ def delete_bot(bot_id): if not bot_info: flash("Bot nicht gefunden.", "danger") return redirect(url_for('discord_bots')) - - # Bot stoppen, falls er läuft if bot_id in running_bots: process = running_bots[bot_id]['process'] try: @@ -771,22 +616,15 @@ def delete_bot(bot_id): process.kill() del running_bots[bot_id] socketio.emit('status_update', {'bot_id': bot_id, 'status': 'deleted'}, room=bot_id) - - # Bot-Datei löschen 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") - # Fortfahren, um den DB-Eintrag trotzdem zu löschen - - # Eintrag aus der Datenbank löschen 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') @@ -795,48 +633,38 @@ def bot_console(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}.'}) - # Gespeicherten Verlauf an den neuen Client senden 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'] # 'start' or 'stop' - + 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: - # Start process process = subprocess.Popen( ['python', '-u', filepath], stdout=subprocess.PIPE, @@ -846,65 +674,48 @@ def handle_toggle_bot(data): errors='replace' ) running_bots[bot_id] = {'process': process, 'history': []} - - # Start thread to stream output thread = threading.Thread(target=stream_bot_output, args=(bot_id, process)) thread.daemon = True thread.start() - 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() # Graceful shutdown + process.terminate() process.wait(timeout=5) except subprocess.TimeoutExpired: - process.kill() # Force kill - + process.kill() if bot_id in running_bots: del running_bots[bot_id] - 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): - # Überprüfen, ob der Benutzer die Bypass-Berechtigung hat user_perms = get_user_permissions(str(user['_id'])) if 'bypass_maintenance' in user_perms: session.permanent = True @@ -919,9 +730,7 @@ def maintenance_login(): 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') @@ -929,11 +738,9 @@ 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') - # Discord Bots Wartungsstatus für das Template bereitstellen 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') @@ -941,13 +748,11 @@ 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, @@ -955,37 +760,14 @@ def update_maintenance(): "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__': - # Optionale SSL-Unterstützung: - # Legen Sie entweder die Umgebungsvariablen SSL_CERT_FILE und SSL_KEY_FILE fest - # oder legen Sie die Dateien certs/fullchain.pem und certs/privkey.pem im Projektordner ab. - cert_file = os.environ.get('SSL_CERT_FILE', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs', 'fullchain.pem')) - key_file = os.environ.get('SSL_KEY_FILE', os.path.join(os.path.dirname(os.path.abspath(__file__)), 'certs', 'privkey.pem')) - - use_ssl = os.path.exists(cert_file) and os.path.exists(key_file) - - # Sitzungscookies nur als Secure markieren, wenn tatsächlich HTTPS verwendet wird - app.config['SESSION_COOKIE_SECURE'] = bool(use_ssl) - - if use_ssl: - print(f"INFO: Starte mit SSL, cert={cert_file}, key={key_file}") - # Für bessere Kontrolle können Sie auch ein SSLContext erstellen: - context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) - context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 - context.load_cert_chain(certfile=cert_file, keyfile=key_file) - # In Produktion debug=False setzen und Port 443 verwenden (oder Reverse Proxy) - socketio.run(app, host='0.0.0.0', port=443, debug=False, ssl_context=context) - else: - print("WARNUNG: SSL-Zertifikat nicht gefunden. Starte ohne HTTPS (nur für Entwicklung).") - # allow_unsafe_werkzeug bleibt hier, da dies Entwicklungsmodus ist - socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True) + 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) diff --git a/AstraOS/requirements.txt b/AstraOS/requirements.txt new file mode 100644 index 0000000..2fdf5bb --- /dev/null +++ b/AstraOS/requirements.txt @@ -0,0 +1,12 @@ +Flask +pymongo +werkzeug +Flask-WTF +paramiko +requests +Flask-SocketIO +gunicorn +eventlet +ipaddress +python-dotenv + diff --git a/AstraOS/start_prod_server.py b/AstraOS/start_prod_server.py new file mode 100644 index 0000000..f49ac54 --- /dev/null +++ b/AstraOS/start_prod_server.py @@ -0,0 +1,35 @@ +import subprocess +import os +import sys + +def start_server(): + project_dir = os.path.dirname(os.path.abspath(__file__)) + command = [ + "gunicorn", + "--worker-class", "eventlet", + "-w", "1", + "--bind", "0.0.0.0:5000", + "wsgi:app" + ] + print("=============================================") + print("=== AstraOS Produktionsserver wird gestartet ===") + print(f"=== Arbeitsverzeichnis: {project_dir}") + print(f"=== Befehl: {' '.join(command)}") + print("=============================================") + print("Drücken Sie STRG+C, um den Server zu beenden.") + try: + subprocess.run(command, cwd=project_dir, check=True) + except FileNotFoundError: + print("\n[FEHLER] 'gunicorn' wurde nicht gefunden.") + print("Stellen Sie sicher, dass Gunicorn installiert ist ('pip install gunicorn').") + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"\n[FEHLER] Der Server wurde unerwartet beendet: {e}") + sys.exit(1) + except KeyboardInterrupt: + print("\nServer wurde vom Benutzer gestoppt.") + sys.exit(0) + +if __name__ == "__main__": + start_server() + diff --git a/AstraOS/wsgi.py b/AstraOS/wsgi.py new file mode 100644 index 0000000..3d99339 --- /dev/null +++ b/AstraOS/wsgi.py @@ -0,0 +1,4 @@ +from astra_os import app, socketio + +if __name__ == "__main__": + socketio.run(app)