diff --git a/AstraOS/Frontend/HTML/bot_console.html b/AstraOS/Frontend/HTML/bot_console.html new file mode 100644 index 0000000..8d20ab0 --- /dev/null +++ b/AstraOS/Frontend/HTML/bot_console.html @@ -0,0 +1,103 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Konsole: {{ bot.name }}{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} + +
+
+ ‹ Zurück zur Übersicht +

Konsole: {{ bot.name }}

+
+
+ + {{ bot.status }} + + +
+
+ + +
+
Initialisiere Konsolenverbindung...
+
+ + +{% endblock %} + diff --git a/AstraOS/Frontend/HTML/dashboard.html b/AstraOS/Frontend/HTML/dashboard.html new file mode 100644 index 0000000..1cf8eb1 --- /dev/null +++ b/AstraOS/Frontend/HTML/dashboard.html @@ -0,0 +1,169 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Hybrid Dashboard{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+

Dashboard

+
+ +
+
+

🚀 Willkommen bei AstraOS!

+

+ AstraOS ist da! Wir haben alles von Grund auf neu gebaut, damit du deine Systeme noch intuitiver und effizienter verwalten kannst. Wir hoffen, dir gefallen das neue Design und die vielen praktischen Funktionen. +

+
+
+ +
+ +
+

Aktive Benutzer

+

{{ stats.active_users.value }}

+

+ {% set pct = stats.active_users.percent %} + {% if pct is not none %} + {{ '+' if pct>=0 else '' }}{{ pct }}% seit letzter Woche + {% else %} + - + {% endif %} +

+
+ + +
+

Server-Auslastung

+

{{ stats.server_load.percent }}%

+

{{ stats.server_load.status }}

+
+ + +
+

Offene Tickets

+

{{ stats.open_tickets.count }}

+

Keine offenen Anfragen

+
+ + +
+

Systemstatus

+ {% set status = stats.system_status.status %} +

{{ status }}

+

Alle Systeme funktionsfähig

+
+ + +
+

Geplante Wartung

+ {% set maint = stats.scheduled_maintenance %} + {% if maint and maint.enabled %} +

Aktiv

+

{{ maint.status if maint.status else 'Wartungsarbeiten laufen' }}

+ {% elif maint and maint.time_str %} +

{{ maint.time_str }}

+

{{ maint.status if maint and maint.status else 'Geplant' }} {% if maint and maint.duration_minutes %}— Dauer: {{ maint.duration_minutes }} Min.{% endif %}

+ {% else %} +

Keine geplant

+

Alle Systeme sind betriebsbereit.

+ {% endif %} +
+ + +
+

Letzte Benutzererstellung

+ {% set last = stats.last_user_created %} +

{{ last.time_str if last and last.time_str else 'Keine Daten' }}

+

Durch: {{ last.by if last and last.by else 'N/A' }}

+
+
+ +{% endblock %} diff --git a/AstraOS/Frontend/HTML/discord_bots.html b/AstraOS/Frontend/HTML/discord_bots.html new file mode 100644 index 0000000..5562a3f --- /dev/null +++ b/AstraOS/Frontend/HTML/discord_bots.html @@ -0,0 +1,135 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Discord Bots{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block content %} +
+
+ +
+

Discord Bot Management

+ +
+ + +
+ {% for bot in bots %} +
+
+
+

{{ bot.name }}

+ + {{ bot.status }} + +
+

Filename: {{ bot.filename }}

+
+
+ +
+ + Konsole + + +
+
+
+ {% else %} +

Noch keine Bots hochgeladen.

+ {% endfor %} +
+
+ + + + + + +
+ + +{% endblock %} diff --git a/AstraOS/Frontend/HTML/ip_analyzer.html b/AstraOS/Frontend/HTML/ip_analyzer.html new file mode 100644 index 0000000..6209284 --- /dev/null +++ b/AstraOS/Frontend/HTML/ip_analyzer.html @@ -0,0 +1,112 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — IP Analyzer{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+ +
+

IP Analyzer

+
+ + +
+
+ +
+ + +
+ +
+
+

Als Cheater markieren

+

Markiert diese IP als zu einem Cheater gehörig.

+
+ +
+ +
+ + +
+ +
+ +
+
+
+ + + {% if error %} +
{{ error }}
+ {% endif %} + + {% if ip_data %} +
+

Analyse-Ergebnis

+
+
IP-Adresse: {{ ip_data.ip }}
+
Land: {{ ip_data.location.country }} ({{ ip_data.location.city }})
+
VPN: {{ 'Ja' if ip_data.security.vpn else 'Nein' }}
+
Proxy: {{ 'Ja' if ip_data.security.proxy else 'Nein' }}
+
Tor: {{ 'Ja' if ip_data.security.tor else 'Nein' }}
+
ASN: {{ ip_data.network.autonomous_system_number }}
+
Netzwerk: {{ ip_data.network.network }}
+
Organisation: {{ ip_data.network.autonomous_system_organization }}
+
+
+ {% endif %} + + {% if similar_ips %} +
+

Ähnliche Einträge gefunden

+
+ + + + + + + + + + + + {% for ip in similar_ips %} + + + + + + + + {% endfor %} + +
IPCheaterSteamIDHinzugefügt vonDatum
{{ ip.ip }}{{ 'Ja' if ip.is_cheater else 'Nein' }}{{ ip.steam_id or 'N/A' }}{{ ip.added_by }}{{ ip.timestamp.strftime('%d.%m.%Y %H:%M') }}
+
+
+ {% endif %} +
+{% endblock %} diff --git a/AstraOS/Frontend/HTML/login.html b/AstraOS/Frontend/HTML/login.html index cacb323..65fdbcf 100644 --- a/AstraOS/Frontend/HTML/login.html +++ b/AstraOS/Frontend/HTML/login.html @@ -67,8 +67,18 @@

-
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + +
{ gsap.to("#startup", { opacity: 0, - duration: 0.6, + duration: 0.5, onComplete: () => { document.getElementById("startup").style.display = "none"; const tl = gsap.timeline(); tl.fromTo("#loginBox", - { opacity: 0, scale: 0.1 }, + { opacity: 0, scale: 0.8 }, { opacity: 1, scale: 1, - duration: 0.8, + duration: 0.5, ease: "power2.out" } ); tl.to("#loginTitle", { - duration: 1, + duration: 0.8, text: "AstraOS Login", ease: "none" }, "-=0.2"); tl.to("#loginForm", { opacity: 1, - duration: 0.5 - }, "-=0.5"); + duration: 0.4 + }, "-=0.4"); } }); - }, 1500); + }, 800); + {% else %} + // Wenn Nachrichten vorhanden sind, Animation überspringen + document.getElementById("startup").style.display = "none"; + document.getElementById("loginBox").style.opacity = 1; + document.getElementById("loginForm").style.opacity = 1; + document.getElementById("loginTitle").textContent = "AstraOS Login"; + {% endif %} diff --git a/AstraOS/Frontend/HTML/maintenance.html b/AstraOS/Frontend/HTML/maintenance.html new file mode 100644 index 0000000..3712d79 --- /dev/null +++ b/AstraOS/Frontend/HTML/maintenance.html @@ -0,0 +1,58 @@ + + + + + + AstraOS — Wartung + + + + + + +
+
+
+ + + +
+ +

Wartungsarbeiten

+

+ {{ maint_info.status if maint_info and maint_info.status else 'Wir führen zurzeit planmäßige Wartungsarbeiten durch, um unsere Dienste zu verbessern.' }} +

+

+ Wir sind bald wieder für Sie da. +

+ + {% if maint_info and maint_info.time %} +
+

Voraussichtliches Ende:

+ {% set end_time = maint_info.time + timedelta(minutes=maint_info.duration_minutes or 0) %} +

{{ end_time.strftime('%d. %B %Y, %H:%M Uhr') }}

+
+ {% endif %} + + +
+ + + + + diff --git a/AstraOS/Frontend/HTML/maintenance_login.html b/AstraOS/Frontend/HTML/maintenance_login.html new file mode 100644 index 0000000..7c6d2c8 --- /dev/null +++ b/AstraOS/Frontend/HTML/maintenance_login.html @@ -0,0 +1,148 @@ + + + + + + AstraOS — Maintenance Login + + + + + + + + + + + +
+
+
+ + + +
+

AstraOS Maintenance

+
+ + +
+ +
+ +
+ + + +

+
+ + + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} + {% endwith %} + + + +
+ + +
+ +
+ +
+ + +
+
+ + + + + +
+ +
+ + + + + diff --git a/AstraOS/Frontend/HTML/maintenance_management.html b/AstraOS/Frontend/HTML/maintenance_management.html new file mode 100644 index 0000000..0a0d0c1 --- /dev/null +++ b/AstraOS/Frontend/HTML/maintenance_management.html @@ -0,0 +1,80 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Maintenance Management{% endblock %} + +{% block head %} + + +{% endblock %} + +{% block content %} +
+ +
+

Maintenance Management

+
+ + +
+
+ + + +
+
+

Wartungsmodus

+

Aktiviert den Wartungsmodus und sperrt den Zugang für normale Benutzer.

+
+ +
+ + +
+
+ + +

Diese Nachricht wird auf der Wartungsseite angezeigt.

+
+ +
+
+ + +

Leer lassen für unbestimmte Zeit.

+
+
+ + +
+
+
+ + +
+ +
+
+
+
+{% endblock %} diff --git a/AstraOS/Frontend/HTML/role_management.html b/AstraOS/Frontend/HTML/role_management.html new file mode 100644 index 0000000..aba0995 --- /dev/null +++ b/AstraOS/Frontend/HTML/role_management.html @@ -0,0 +1,113 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Role Management{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+ +
+

Rollen verwalten

+ +
+ +
+ + + + + + + + + + {% for role in roles %} + + + + + + {% endfor %} + +
RollennameBerechtigungen + Aktionen +
{{ role.name }} +
+ {% for perm in role.permissions %} + {{ perm }} + {% endfor %} +
+
+
+ + {% if role.is_deletable %} + + {% endif %} +
+
+
+
+ + + + + +
+{% endblock %} diff --git a/AstraOS/Frontend/HTML/sidebar.html b/AstraOS/Frontend/HTML/sidebar.html new file mode 100644 index 0000000..32793b8 --- /dev/null +++ b/AstraOS/Frontend/HTML/sidebar.html @@ -0,0 +1,325 @@ + + + + + + {% block title %}AstraOS{% endblock %} + + + + {% block head %}{% endblock %} + + + + + + +
+
+
+ + + + +
+

AstraOS

+
+ + +
+ + +
+ + + + +
+ {% block content %}{% endblock %} +
+
+ + + + + + + Logout + + + +
+ © 2025 AstraOS +
+ + + +{% if maintenance_active %} + +{% endif %} + + diff --git a/AstraOS/Frontend/HTML/status_page.html b/AstraOS/Frontend/HTML/status_page.html new file mode 100644 index 0000000..6bffa85 --- /dev/null +++ b/AstraOS/Frontend/HTML/status_page.html @@ -0,0 +1,79 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — Status Page{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} + +
+

System Status

+
+ +
+ +
+ Chainsaw Hood SCP:SL Server +
+ Online +
+
+
+ + +
+ Kettensägen Gamepanel +
+ Online +
+
+
+ + +
+ Discord Bots +
+ Wartungsarbeiten +
+
+
+ + +
+ Datenbank +
+ Offline +
+
+
+
+{% endblock %} diff --git a/AstraOS/Frontend/HTML/user_management.html b/AstraOS/Frontend/HTML/user_management.html new file mode 100644 index 0000000..1b9e4e9 --- /dev/null +++ b/AstraOS/Frontend/HTML/user_management.html @@ -0,0 +1,98 @@ +{% extends "sidebar.html" %} + +{% block title %}AstraOS — User Management{% endblock %} + +{% block head %} + +{% endblock %} + +{% block content %} +
+
+ +
+

User Management

+ +
+ +
+ + + + + + + + + + + {% for user in users %} + + + + + + + {% endfor %} + +
BenutzernameRolleErstellt am + Aktionen +
{{ user.username }}{{ user.role_name }}{{ user.creation_date.strftime('%d.%m.%Y') if user.creation_date else 'N/A' }} + +
+
+
+ + + + + +
+{% endblock %} + +{% block loading %} + +{% endblock %} diff --git a/AstraOS/Frontend/JAVASCRIPT/main.js b/AstraOS/Frontend/JAVASCRIPT/main.js new file mode 100644 index 0000000..3a3016b --- /dev/null +++ b/AstraOS/Frontend/JAVASCRIPT/main.js @@ -0,0 +1,75 @@ +document.addEventListener('DOMContentLoaded', function () { + + // --- Initial Load: Set correct open/closed state --- + document.querySelectorAll('.collapsible').forEach(button => { + const content = button.nextElementSibling; + const chevron = button.querySelector('svg'); + const shouldOpen = button.dataset.open === "true"; + + if (shouldOpen) { + content.classList.remove("hidden"); + content.style.height = "auto"; + content.style.opacity = "1"; + if (chevron) chevron.style.transform = "rotate(180deg)"; + } else { + content.classList.add("hidden"); + content.style.height = "0"; + content.style.opacity = "0"; + if (chevron) chevron.style.transform = "rotate(0deg)"; + } + }); + + + // --- Accordion Click Behavior --- + document.querySelectorAll('.collapsible').forEach(button => { + button.addEventListener('click', () => { + + const content = button.nextElementSibling; + const chevron = button.querySelector('svg'); + + // First close all others + document.querySelectorAll('.collapsible').forEach(other => { + if (other !== button) { + const otherContent = other.nextElementSibling; + const otherChevron = other.querySelector('svg'); + + if (!otherContent.classList.contains('hidden')) { + gsap.to(otherContent, { + height: 0, + opacity: 0, + duration: 0.25, + ease: 'power2.in', + onComplete: () => { + otherContent.classList.add('hidden'); + if (otherChevron) gsap.to(otherChevron, { rotation: 0, duration: 0.25 }); + } + }); + } + } + }); + + // Toggle current + if (content.classList.contains('hidden')) { + content.classList.remove('hidden'); + gsap.fromTo(content, + { height: 0, opacity: 0 }, + { height: 'auto', opacity: 1, duration: 0.25, ease: 'power2.out' } + ); + if (chevron) gsap.to(chevron, { rotation: 180, duration: 0.25 }); + + } else { + gsap.to(content, { + height: 0, + opacity: 0, + duration: 0.25, + ease: 'power2.in', + onComplete: () => { + content.classList.add('hidden'); + if (chevron) gsap.to(chevron, { rotation: 0, duration: 0.25 }); + } + }); + } + }); + }); + +}); diff --git a/AstraOS/astra_os.py b/AstraOS/astra_os.py index e69de29..b8e69c5 100644 --- a/AstraOS/astra_os.py +++ b/AstraOS/astra_os.py @@ -0,0 +1,818 @@ +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 + +# Verwenden Sie eventlet für die Produktion +async_mode = "threading" +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 Anfragen aus den Logs auszublenden +class SuppressServerLoadFilter(logging.Filter): + def filter(self, record): + return "GET /api/server_load" not in record.getMessage() + +# Den Filter zum Werkzeug-Logger hinzufügen +log = logging.getLogger('werkzeug') +log.addFilter(SuppressServerLoadFilter()) + + +app.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("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/") + 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() + }) + 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([ + {"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 + +# MongoDB für Tickets (chainsaw_dc_data) +try: + tickets_client = MongoClient("mongodb+srv://MrSniff:5169Galaxy@chainsaw.d0shx.mongodb.net/") + 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')) + + # 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) + 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')) + + # 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']) + session['username'] = user['username'] + session['permissions'] = get_user_permissions(str(user['_id'])) + 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('/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 (hier statisch, ggf. anpassen) + system_status = {"status": "Online"} + + # 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: + 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}, # Prozent ggf. später berechnen + "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('/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()) + ssh.connect("89.144.42.175", username="root", password="iwkms@1812:(") + 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: + idle = float(match.group(1)) + usage = 100 - idle + return jsonify({"percent": round(usage, 1), "status": "Online"}) + else: + return jsonify({"percent": 0, "status": "Fehler"}) + except Exception as e: + return jsonify({"percent": 0, "status": "Nicht erreichbar"}) + +@app.route('/logout') +@login_required +def logout(): + session.clear() + flash("Sie wurden erfolgreich abgemeldet.", "info") + return redirect(url_for('login')) + +@app.route('/status') +@login_required +def status_page(): + return render_template('status_page.html', active_page='status_page') + +@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()) + + role_map = {str(role['_id']): role['name'] for role in roles_list} + + for user in users_list: + user['role_name'] = role_map.get(str(user.get('role_id')), 'Unbekannt') + + return render_template('user_management.html', users=users_list, 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() + }) + # Dashboard-Statistik aktualisieren + if dashboard_collection is not None: + dashboard_collection.update_one( + {"key": "last_user_created"}, + {"$set": { + "time": datetime.utcnow(), + "by": username + }}, + upsert=True + ) + # Aktive Benutzer aktualisieren + 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('/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('/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()) + # 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, + "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')) + +# --- IP Analyzer --- +@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 = "015ada9472664329affb2f26d7036d47" # API-Schlüssel aus altem Code + 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}, + {"data.location.city": ip_data.get('location', {}).get('city')}, + {"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, + "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) + + +# --- 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 + 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') +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() + }) + + 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')) + + # Bot stoppen, falls er läuft + 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) + + # 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') +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}.'}) + # 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' + + 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, + stderr=subprocess.PIPE, + text=True, + encoding='utf-8', + 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.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() # Force 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 + 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'): + # Konvertiere UTC-Zeit aus DB in ein für datetime-local passendes Format + maint_info['time_local_str'] = maint_info['time'].strftime('%Y-%m-%dT%H:%M') + 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') + + 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 + } + + 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__': + socketio.run(app, host='0.0.0.0', port=5000, debug=True, allow_unsafe_werkzeug=True)