modified: app.py

new file:   file_manager.py
	modified:   requirements.txt
	new file:   sessions_db.json
	modified:   templates/base.html
	new file:   templates/file_manager.html
	new file:   templates/file_manager_new.html
	new file:   templates/login.html
	new file:   templates/profile.html
	new file:   templates/users_management.html
	new file:   user_management.py
	new file:   users_db.json
This commit is contained in:
SimolZimol
2025-07-10 00:00:59 +02:00
parent f277508e78
commit 1eac702d7d
12 changed files with 5135 additions and 82 deletions

557
app.py
View File

@@ -1,4 +1,4 @@
from flask import Flask, render_template, request, jsonify, flash, redirect, url_for from flask import Flask, render_template, request, jsonify, flash, redirect, url_for, session
import requests import requests
import os import os
import subprocess import subprocess
@@ -10,11 +10,19 @@ import urllib.request
import urllib.error import urllib.error
import tempfile import tempfile
import socket import socket
import stat
from urllib.parse import urlparse from urllib.parse import urlparse
import docker import docker
import yaml import yaml
from datetime import datetime from datetime import datetime
# User Management System importieren
from user_management import (
user_manager, UserRole, Permission,
login_required, role_required, permission_required,
api_login_required, api_permission_required
)
app = Flask(__name__) app = Flask(__name__)
app.secret_key = 'your-secret-key-change-this' app.secret_key = 'your-secret-key-change-this'
@@ -30,6 +38,212 @@ CONFIG_FILE = 'config.json'
PROJECTS_DIR = 'projects' PROJECTS_DIR = 'projects'
APPS_DIR = 'apps' APPS_DIR = 'apps'
class FileManager:
"""File Manager für Dateioperationen im Web-Interface"""
def __init__(self, base_path=None):
self.base_path = base_path or os.path.abspath('.')
self.allowed_extensions = {
'text': {'.txt', '.md', '.py', '.js', '.html', '.css', '.json', '.xml', '.yml', '.yaml', '.sh', '.bat', '.cfg', '.ini', '.conf'},
'image': {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.svg', '.ico'},
'archive': {'.zip', '.tar', '.gz', '.rar', '.7z'},
'document': {'.pdf', '.doc', '.docx', '.xls', '.xlsx'}
}
def get_safe_path(self, path):
"""Normalisiere und validiere Pfad"""
if not path:
return self.base_path
# Relative Pfade relativ zur base_path auflösen
if not os.path.isabs(path):
full_path = os.path.join(self.base_path, path)
else:
full_path = path
# Normalisiere den Pfad
full_path = os.path.normpath(full_path)
# Sicherheitscheck: Pfad muss innerhalb der base_path liegen
if not full_path.startswith(self.base_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
return full_path
def list_directory(self, path=""):
"""Liste Dateien und Ordner in einem Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Verzeichnis existiert nicht'}
if not os.path.isdir(safe_path):
return {'error': 'Pfad ist kein Verzeichnis'}
items = []
for item in os.listdir(safe_path):
item_path = os.path.join(safe_path, item)
rel_path = os.path.relpath(item_path, self.base_path).replace('\\', '/')
item_info = {
'name': item,
'path': rel_path,
'is_directory': os.path.isdir(item_path),
'size': 0,
'modified': 0,
'type': 'folder' if os.path.isdir(item_path) else self.get_file_type(item)
}
if not os.path.isdir(item_path):
try:
stat = os.stat(item_path)
item_info['size'] = stat.st_size
item_info['modified'] = stat.st_mtime
except:
pass
items.append(item_info)
# Sortiere: Ordner zuerst, dann Dateien
items.sort(key=lambda x: (not x['is_directory'], x['name'].lower()))
# Aktueller Pfad relativ zur base_path
current_path = os.path.relpath(safe_path, self.base_path).replace('\\', '/')
if current_path == '.':
current_path = ''
# Parent-Pfad für Navigation
parent_path = ''
if current_path:
parent_path = os.path.dirname(current_path).replace('\\', '/')
if parent_path == '.':
parent_path = ''
return {
'success': True,
'current_path': current_path,
'parent_path': parent_path,
'items': items
}
except Exception as e:
return {'error': str(e)}
def get_file_type(self, filename):
"""Bestimme Dateityp anhand der Dateiendung"""
ext = os.path.splitext(filename)[1].lower()
for file_type, extensions in self.allowed_extensions.items():
if ext in extensions:
return file_type
return 'unknown'
def read_file(self, path):
"""Lese Dateiinhalt"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Datei existiert nicht'}
if os.path.isdir(safe_path):
return {'error': 'Pfad ist ein Verzeichnis'}
# Prüfe Dateigröße (max 10MB für Text-Dateien)
file_size = os.path.getsize(safe_path)
if file_size > 10 * 1024 * 1024:
return {'error': 'Datei zu groß (max 10MB)'
}
# Versuche als Text zu lesen
try:
with open(safe_path, 'r', encoding='utf-8') as f:
content = f.read()
return {
'success': True,
'content': content,
'size': file_size,
'type': 'text'
}
except UnicodeDecodeError:
return {'error': 'Datei ist nicht als Text lesbar'}
except Exception as e:
return {'error': str(e)}
def write_file(self, path, content):
"""Schreibe Dateiinhalt"""
try:
safe_path = self.get_safe_path(path)
# Erstelle Verzeichnis falls nicht vorhanden
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
with open(safe_path, 'w', encoding='utf-8') as f:
f.write(content)
return {'success': True, 'message': 'Datei erfolgreich gespeichert'}
except Exception as e:
return {'error': str(e)}
def delete_item(self, path):
"""Lösche Datei oder Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if not os.path.exists(safe_path):
return {'error': 'Datei/Verzeichnis existiert nicht'}
if os.path.isdir(safe_path):
shutil.rmtree(safe_path)
return {'success': True, 'message': 'Verzeichnis erfolgreich gelöscht'}
else:
os.remove(safe_path)
return {'success': True, 'message': 'Datei erfolgreich gelöscht'}
except Exception as e:
return {'error': str(e)}
def create_directory(self, path):
"""Erstelle neues Verzeichnis"""
try:
safe_path = self.get_safe_path(path)
if os.path.exists(safe_path):
return {'error': 'Verzeichnis existiert bereits'}
os.makedirs(safe_path, exist_ok=True)
return {'success': True, 'message': 'Verzeichnis erfolgreich erstellt'}
except Exception as e:
return {'error': str(e)}
def rename_item(self, old_path, new_name):
"""Benenne Datei oder Verzeichnis um"""
try:
safe_old_path = self.get_safe_path(old_path)
if not os.path.exists(safe_old_path):
return {'error': 'Datei/Verzeichnis existiert nicht'}
new_path = os.path.join(os.path.dirname(safe_old_path), new_name)
safe_new_path = self.get_safe_path(os.path.relpath(new_path, self.base_path))
if os.path.exists(safe_new_path):
return {'error': 'Ziel existiert bereits'}
os.rename(safe_old_path, safe_new_path)
return {'success': True, 'message': 'Erfolgreich umbenannt'}
except Exception as e:
return {'error': str(e)}
# File Manager Instanz
file_manager = FileManager()
class ProjectManager: class ProjectManager:
def __init__(self): def __init__(self):
self.docker_client = None self.docker_client = None
@@ -1011,7 +1225,7 @@ class ProjectManager:
if alternative_port: if alternative_port:
return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}" return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}"
else: else:
return False, f"Port {blocked_blocked} ist bereits belegt und keine Alternative gefunden." return False, f"Port {blocked_port} ist bereits belegt und keine Alternative gefunden."
else: else:
return False, f"Port-Konflikt: {error_msg}" return False, f"Port-Konflikt: {error_msg}"
else: else:
@@ -1257,7 +1471,174 @@ class ProjectManager:
# Globale Instanz # Globale Instanz
project_manager = ProjectManager() project_manager = ProjectManager()
# ===== USER MANAGEMENT ROUTES =====
@app.route('/login', methods=['GET', 'POST'])
def login():
"""Login-Seite"""
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
if not username or not password:
flash('Benutzername und Passwort sind erforderlich', 'danger')
return render_template('login.html')
success, result = user_manager.authenticate_user(username, password)
if success:
# Erstelle Session
session_token = user_manager.create_session(username)
session['session_token'] = session_token
session['username'] = username
session['user_role'] = result['role']
flash(f'Willkommen, {result.get("first_name", username)}!', 'success')
# Weiterleitung zur ursprünglich angeforderten Seite
next_page = request.args.get('next')
if next_page:
return redirect(next_page)
return redirect(url_for('index'))
else:
flash(result, 'danger')
return render_template('login.html')
@app.route('/logout')
def logout():
"""Logout"""
session_token = session.get('session_token')
if session_token:
user_manager.destroy_session(session_token)
session.clear()
flash('Sie wurden erfolgreich abgemeldet', 'info')
return redirect(url_for('login'))
@app.route('/profile')
@login_required
def profile():
"""Benutzer-Profil"""
user = request.current_user
return render_template('profile.html', user=user)
@app.route('/change_password', methods=['POST'])
@login_required
def change_password():
"""Passwort ändern"""
old_password = request.form.get('old_password', '')
new_password = request.form.get('new_password', '')
confirm_password = request.form.get('confirm_password', '')
if not old_password or not new_password or not confirm_password:
flash('Alle Felder sind erforderlich', 'danger')
return redirect(url_for('profile'))
if new_password != confirm_password:
flash('Neue Passwörter stimmen nicht überein', 'danger')
return redirect(url_for('profile'))
if len(new_password) < 6:
flash('Passwort muss mindestens 6 Zeichen lang sein', 'danger')
return redirect(url_for('profile'))
username = request.current_user['username']
success, message = user_manager.change_password(username, old_password, new_password)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('profile'))
@app.route('/users')
@role_required(UserRole.ADMIN)
def users_management():
"""Benutzerverwaltung"""
users = user_manager.get_all_users()
roles = UserRole.get_all_roles()
return render_template('users_management.html', users=users, roles=roles)
@app.route('/users/create', methods=['POST'])
@role_required(UserRole.ADMIN)
def create_user():
"""Benutzer erstellen"""
username = request.form.get('username', '').strip()
email = request.form.get('email', '').strip()
password = request.form.get('password', '')
role = request.form.get('role', UserRole.USER)
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
if not username or not email or not password:
flash('Benutzername, E-Mail und Passwort sind erforderlich', 'danger')
return redirect(url_for('users_management'))
if len(password) < 6:
flash('Passwort muss mindestens 6 Zeichen lang sein', 'danger')
return redirect(url_for('users_management'))
success, message = user_manager.create_user(
username=username,
email=email,
password=password,
role=role,
first_name=first_name,
last_name=last_name
)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
@app.route('/users/<username>/edit', methods=['POST'])
@role_required(UserRole.ADMIN)
def edit_user(username):
"""Benutzer bearbeiten"""
email = request.form.get('email', '').strip()
role = request.form.get('role', UserRole.USER)
first_name = request.form.get('first_name', '').strip()
last_name = request.form.get('last_name', '').strip()
enabled = request.form.get('enabled') == 'on'
success, message = user_manager.update_user(
username=username,
email=email,
role=role,
first_name=first_name,
last_name=last_name,
enabled=enabled
)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
@app.route('/users/<username>/delete', methods=['POST'])
@role_required(UserRole.ADMIN)
def delete_user(username):
"""Benutzer löschen"""
success, message = user_manager.delete_user(username)
if success:
flash(message, 'success')
else:
flash(message, 'danger')
return redirect(url_for('users_management'))
# ===== PROTECTED ROUTES - Bestehende Routes mit Authentifizierung =====
@app.route('/') @app.route('/')
@login_required
def index(): def index():
"""Hauptseite mit Projektübersicht""" """Hauptseite mit Projektübersicht"""
config = project_manager.load_config() config = project_manager.load_config()
@@ -1275,6 +1656,7 @@ def index():
return render_template('index.html', projects=projects, config=config) return render_template('index.html', projects=projects, config=config)
@app.route('/refresh_projects') @app.route('/refresh_projects')
@permission_required(Permission.PROJECT_VIEW)
def refresh_projects(): def refresh_projects():
"""Aktualisiere Projektliste von URL""" """Aktualisiere Projektliste von URL"""
config = project_manager.load_config() config = project_manager.load_config()
@@ -1291,6 +1673,7 @@ def refresh_projects():
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/available_projects') @app.route('/available_projects')
@permission_required(Permission.PROJECT_VIEW)
def available_projects(): def available_projects():
"""Zeige verfügbare Projekte zum Installieren""" """Zeige verfügbare Projekte zum Installieren"""
config = project_manager.load_config() config = project_manager.load_config()
@@ -1322,6 +1705,7 @@ def available_projects():
return render_template('available_projects.html', projects=projects) return render_template('available_projects.html', projects=projects)
@app.route('/install_project', methods=['POST']) @app.route('/install_project', methods=['POST'])
@permission_required(Permission.PROJECT_CREATE)
def install_project(): def install_project():
"""Installiere Projekt""" """Installiere Projekt"""
project_url = request.form.get('project_url') project_url = request.form.get('project_url')
@@ -1480,6 +1864,7 @@ def install_project():
return jsonify({'success': success, 'message': message, 'method': installation_method}) return jsonify({'success': success, 'message': message, 'method': installation_method})
@app.route('/build_project/<project_name>') @app.route('/build_project/<project_name>')
@permission_required(Permission.PROJECT_UPDATE)
def build_project(project_name): def build_project(project_name):
"""Baue Projekt""" """Baue Projekt"""
success, message = project_manager.build_project(project_name) success, message = project_manager.build_project(project_name)
@@ -1487,6 +1872,7 @@ def build_project(project_name):
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/start_project/<project_name>') @app.route('/start_project/<project_name>')
@permission_required(Permission.PROJECT_START)
def start_project(project_name): def start_project(project_name):
"""Starte Projekt""" """Starte Projekt"""
port = request.args.get('port', None) port = request.args.get('port', None)
@@ -1515,6 +1901,7 @@ def start_project(project_name):
return redirect(url_for('project_details', project_name=project_name)) return redirect(url_for('project_details', project_name=project_name))
@app.route('/stop_project/<project_name>') @app.route('/stop_project/<project_name>')
@permission_required(Permission.PROJECT_STOP)
def stop_project(project_name): def stop_project(project_name):
"""Stoppe Projekt""" """Stoppe Projekt"""
success, message = project_manager.stop_project(project_name) success, message = project_manager.stop_project(project_name)
@@ -1522,6 +1909,7 @@ def stop_project(project_name):
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/remove_project/<project_name>') @app.route('/remove_project/<project_name>')
@permission_required(Permission.PROJECT_DELETE)
def remove_project(project_name): def remove_project(project_name):
"""Entferne Projekt""" """Entferne Projekt"""
success, message = project_manager.remove_project(project_name) success, message = project_manager.remove_project(project_name)
@@ -1529,6 +1917,7 @@ def remove_project(project_name):
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/config', methods=['GET', 'POST']) @app.route('/config', methods=['GET', 'POST'])
@permission_required(Permission.SYSTEM_CONFIG)
def config(): def config():
"""Konfigurationsseite""" """Konfigurationsseite"""
try: try:
@@ -1590,6 +1979,7 @@ def config():
return render_template('config.html', config=fallback_config) return render_template('config.html', config=fallback_config)
@app.route('/project_details/<project_name>') @app.route('/project_details/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def project_details(project_name): def project_details(project_name):
"""Projektdetails und Konfiguration""" """Projektdetails und Konfiguration"""
info = project_manager.get_project_info(project_name) info = project_manager.get_project_info(project_name)
@@ -1611,6 +2001,7 @@ def project_details(project_name):
return render_template('project_details.html', project=info, env_content=env_content) return render_template('project_details.html', project=info, env_content=env_content)
@app.route('/save_env/<project_name>', methods=['POST']) @app.route('/save_env/<project_name>', methods=['POST'])
@permission_required(Permission.PROJECT_FILES)
def save_env(project_name): def save_env(project_name):
"""Speichere .env Konfiguration""" """Speichere .env Konfiguration"""
env_content = request.form.get('env_content', '') env_content = request.form.get('env_content', '')
@@ -1627,6 +2018,7 @@ def save_env(project_name):
return redirect(url_for('project_details', project_name=project_name)) return redirect(url_for('project_details', project_name=project_name))
@app.route('/api/system_status') @app.route('/api/system_status')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_system_status(): def api_system_status():
"""API Endpoint für Systemstatus""" """API Endpoint für Systemstatus"""
status = { status = {
@@ -1692,10 +2084,12 @@ def api_system_status():
return jsonify(status) return jsonify(status)
@app.route('/api/test_connection', methods=['POST']) @app.route('/api/test_connection', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_test_connection(): def api_test_connection():
"""Test Verbindung zu Projektliste URL""" """Test Verbindung zu Projektliste URL"""
data = request.get_json() data = request.get_json()
url = data.get('url', '') url = data.get('url', '')
if not url: if not url:
return jsonify({'success': False, 'error': 'Keine URL angegeben'}) return jsonify({'success': False, 'error': 'Keine URL angegeben'})
@@ -1711,6 +2105,7 @@ def api_test_connection():
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_logs/<project_name>') @app.route('/api/container_logs/<project_name>')
@api_permission_required(Permission.PROJECT_LOGS)
def api_container_logs(project_name): def api_container_logs(project_name):
"""Hole Container-Logs für Debugging""" """Hole Container-Logs für Debugging"""
try: try:
@@ -1733,6 +2128,7 @@ def api_container_logs(project_name):
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_inspect/<project_name>') @app.route('/api/container_inspect/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_inspect(project_name): def api_container_inspect(project_name):
"""Detaillierte Container-Informationen für Debugging""" """Detaillierte Container-Informationen für Debugging"""
try: try:
@@ -1766,6 +2162,7 @@ def api_container_inspect(project_name):
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_stats/<project_name>') @app.route('/api/container_stats/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_stats(project_name): def api_container_stats(project_name):
"""Container Statistiken abrufen""" """Container Statistiken abrufen"""
try: try:
@@ -1801,6 +2198,7 @@ def api_container_stats(project_name):
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/restart_project/<project_name>', methods=['POST']) @app.route('/api/restart_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_RESTART)
def api_restart_project(project_name): def api_restart_project(project_name):
"""Projekt Container neustarten""" """Projekt Container neustarten"""
try: try:
@@ -1814,6 +2212,7 @@ def api_restart_project(project_name):
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/clear_cache', methods=['POST']) @app.route('/api/clear_cache', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_clear_cache(): def api_clear_cache():
"""Cache leeren""" """Cache leeren"""
try: try:
@@ -1823,6 +2222,7 @@ def api_clear_cache():
return jsonify({'success': False, 'message': f'Fehler: {str(e)}'}) return jsonify({'success': False, 'message': f'Fehler: {str(e)}'})
@app.route('/api/export_config') @app.route('/api/export_config')
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_export_config(): def api_export_config():
"""Konfiguration exportieren""" """Konfiguration exportieren"""
try: try:
@@ -1838,6 +2238,7 @@ def api_export_config():
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/import_config', methods=['POST']) @app.route('/api/import_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_import_config(): def api_import_config():
"""Konfiguration importieren""" """Konfiguration importieren"""
try: try:
@@ -1848,6 +2249,7 @@ def api_import_config():
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/api/reset_config', methods=['POST']) @app.route('/api/reset_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_reset_config(): def api_reset_config():
"""Konfiguration zurücksetzen""" """Konfiguration zurücksetzen"""
try: try:
@@ -1863,6 +2265,7 @@ def api_reset_config():
return jsonify({'success': False, 'error': str(e)}) return jsonify({'success': False, 'error': str(e)})
@app.route('/docker_status') @app.route('/docker_status')
@permission_required(Permission.SYSTEM_DOCKER)
def docker_status(): def docker_status():
"""Docker Status und Diagnose Seite""" """Docker Status und Diagnose Seite"""
try: try:
@@ -1881,6 +2284,7 @@ def docker_status():
return render_template('docker_status.html', config=fallback_config) return render_template('docker_status.html', config=fallback_config)
@app.route('/api/docker_diagnose') @app.route('/api/docker_diagnose')
@api_permission_required(Permission.SYSTEM_DOCKER)
def api_docker_diagnose(): def api_docker_diagnose():
"""Docker Diagnose API""" """Docker Diagnose API"""
try: try:
@@ -2018,6 +2422,7 @@ def api_docker_diagnose():
}), 500 }), 500
@app.route('/api/check_port/<int:port>') @app.route('/api/check_port/<int:port>')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_check_port(port): def api_check_port(port):
"""Prüfe ob ein Port verfügbar ist""" """Prüfe ob ein Port verfügbar ist"""
try: try:
@@ -2036,6 +2441,7 @@ def api_check_port(port):
}) })
@app.route('/api/find_available_port') @app.route('/api/find_available_port')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_find_available_port(): def api_find_available_port():
"""Finde einen verfügbaren Port""" """Finde einen verfügbaren Port"""
try: try:
@@ -2060,6 +2466,7 @@ def api_find_available_port():
}) })
@app.route('/api/start_project/<project_name>', methods=['POST']) @app.route('/api/start_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_project(project_name): def api_start_project(project_name):
"""API Endpoint zum Starten eines Projekts""" """API Endpoint zum Starten eines Projekts"""
try: try:
@@ -2112,6 +2519,7 @@ def api_start_project(project_name):
}) })
@app.route('/api/start_native/<project_name>', methods=['POST']) @app.route('/api/start_native/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_native(project_name): def api_start_native(project_name):
"""Starte Projekt nativ (ohne Docker)""" """Starte Projekt nativ (ohne Docker)"""
try: try:
@@ -2177,6 +2585,7 @@ def api_start_native(project_name):
return jsonify({'success': False, 'error': f'API-Fehler: {str(e)}'}) return jsonify({'success': False, 'error': f'API-Fehler: {str(e)}'})
@app.route('/api/open_terminal/<project_name>', methods=['POST']) @app.route('/api/open_terminal/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_CONSOLE)
def api_open_terminal(project_name): def api_open_terminal(project_name):
"""Öffne Terminal im Projektordner""" """Öffne Terminal im Projektordner"""
try: try:
@@ -2197,12 +2606,14 @@ def api_open_terminal(project_name):
# Test Icons Template Route # Test Icons Template Route
@app.route('/templates/test_icons.html') @app.route('/templates/test_icons.html')
@permission_required(Permission.SYSTEM_CONFIG)
def serve_test_icons(): def serve_test_icons():
"""Serve Icon Test Template""" """Serve Icon Test Template"""
with open('test_icons.html', 'r', encoding='utf-8') as f: with open('test_icons.html', 'r', encoding='utf-8') as f:
return f.read(), 200, {'Content-Type': 'text/html; charset=utf-8'} return f.read(), 200, {'Content-Type': 'text/html; charset=utf-8'}
@app.route('/update_project', methods=['POST']) @app.route('/update_project', methods=['POST'])
@permission_required(Permission.PROJECT_UPDATE)
def update_project(): def update_project():
"""Update ein Projekt""" """Update ein Projekt"""
project_name = request.form.get('project_name') project_name = request.form.get('project_name')
@@ -2216,6 +2627,7 @@ def update_project():
return jsonify({'success': success, 'message': message}) return jsonify({'success': success, 'message': message})
@app.route('/update_project_get/<project_name>') @app.route('/update_project_get/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def update_project_get(project_name): def update_project_get(project_name):
"""Update Projekt via GET (Weiterleitung)""" """Update Projekt via GET (Weiterleitung)"""
success, message = project_manager.update_project(project_name) success, message = project_manager.update_project(project_name)
@@ -2223,6 +2635,7 @@ def update_project_get(project_name):
return redirect(url_for('index')) return redirect(url_for('index'))
@app.route('/api/update_project/<project_name>', methods=['POST']) @app.route('/api/update_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_UPDATE)
def api_update_project(project_name): def api_update_project(project_name):
"""API Endpoint zum Updaten eines Projekts""" """API Endpoint zum Updaten eines Projekts"""
try: try:
@@ -2238,6 +2651,7 @@ def api_update_project(project_name):
}), 500 }), 500
@app.route('/api/installed_projects') @app.route('/api/installed_projects')
@api_permission_required(Permission.PROJECT_VIEW)
def api_installed_projects(): def api_installed_projects():
"""API Endpoint für installierte Projekte mit Versionsinfo""" """API Endpoint für installierte Projekte mit Versionsinfo"""
try: try:
@@ -2270,6 +2684,7 @@ def api_installed_projects():
}) })
@app.route('/api/projects_list.json') @app.route('/api/projects_list.json')
@api_permission_required(Permission.PROJECT_VIEW)
def api_projects_list(): def api_projects_list():
"""API Endpoint für die lokale Projektliste (für Tests)""" """API Endpoint für die lokale Projektliste (für Tests)"""
try: try:
@@ -2280,6 +2695,7 @@ def api_projects_list():
return jsonify([]), 500 return jsonify([]), 500
@app.route('/api/refresh_projects', methods=['POST']) @app.route('/api/refresh_projects', methods=['POST'])
@api_permission_required(Permission.PROJECT_VIEW)
def api_refresh_projects(): def api_refresh_projects():
"""API Endpoint zum Aktualisieren der Projektliste via AJAX""" """API Endpoint zum Aktualisieren der Projektliste via AJAX"""
try: try:
@@ -2319,6 +2735,7 @@ def api_refresh_projects():
}) })
@app.route('/custom_install/<project_name>') @app.route('/custom_install/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def custom_install(project_name): def custom_install(project_name):
"""Custom Installation Seite für ein Projekt""" """Custom Installation Seite für ein Projekt"""
config = project_manager.load_config() config = project_manager.load_config()
@@ -2376,6 +2793,142 @@ def check_port_available(port):
except: except:
return False return False
# ===== FILE MANAGER ROUTES =====
@app.route('/file_manager')
@login_required
@permission_required(Permission.FILE_ACCESS)
def file_manager_view():
"""File Manager Hauptseite"""
return render_template('file_manager_new.html')
@app.route('/api/files/list')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_list():
"""API: Liste Dateien und Ordner"""
path = request.args.get('path', '')
result = file_manager.list_directory(path)
return jsonify(result)
@app.route('/api/files/read')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_read():
"""API: Lese Dateiinhalt"""
path = request.args.get('path', '')
if not path:
return jsonify({'error': 'Kein Pfad angegeben'})
result = file_manager.read_file(path)
return jsonify(result)
@app.route('/api/files/write', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_write():
"""API: Schreibe Dateiinhalt"""
data = request.get_json()
if not data or 'path' not in data or 'content' not in data:
return jsonify({'error': 'Pfad und Inhalt erforderlich'})
result = file_manager.write_file(data['path'], data['content'])
return jsonify(result)
@app.route('/api/files/delete', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_delete():
"""API: Lösche Datei oder Verzeichnis"""
data = request.get_json()
if not data or 'path' not in data:
return jsonify({'error': 'Pfad erforderlich'})
result = file_manager.delete_item(data['path'])
return jsonify(result)
@app.route('/api/files/create_directory', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_create_directory():
"""API: Erstelle neues Verzeichnis"""
data = request.get_json()
if not data or 'path' not in data:
return jsonify({'error': 'Pfad erforderlich'})
result = file_manager.create_directory(data['path'])
return jsonify(result)
@app.route('/api/files/rename', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_rename():
"""API: Benenne Datei oder Verzeichnis um"""
data = request.get_json()
if not data or 'path' not in data or 'new_name' not in data:
return jsonify({'error': 'Pfad und neuer Name erforderlich'})
result = file_manager.rename_item(data['path'], data['new_name'])
return jsonify(result)
@app.route('/api/files/upload', methods=['POST'])
@api_permission_required(Permission.FILE_WRITE)
def api_file_upload():
"""API: Datei hochladen"""
try:
if 'file' not in request.files:
return jsonify({'error': 'Keine Datei ausgewählt'})
file = request.files['file']
path = request.form.get('path', '')
if file.filename == '':
return jsonify({'error': 'Keine Datei ausgewählt'})
# Sicherer Dateiname
filename = file.filename
if path:
upload_path = os.path.join(path, filename)
else:
upload_path = filename
# Verwende FileManager für sicheren Pfad
safe_path = file_manager.get_safe_path(upload_path)
# Erstelle Verzeichnis falls nicht vorhanden
os.makedirs(os.path.dirname(safe_path), exist_ok=True)
# Speichere Datei
file.save(safe_path)
return jsonify({
'success': True,
'message': f'Datei {filename} erfolgreich hochgeladen'
})
except Exception as e:
return jsonify({'error': str(e)})
@app.route('/api/files/download')
@api_permission_required(Permission.FILE_ACCESS)
def api_file_download():
"""API: Datei herunterladen"""
try:
path = request.args.get('path', '')
if not path:
return jsonify({'error': 'Kein Pfad angegeben'})
safe_path = file_manager.get_safe_path(path)
if not os.path.exists(safe_path):
return jsonify({'error': 'Datei existiert nicht'})
if os.path.isdir(safe_path):
return jsonify({'error': 'Pfad ist ein Verzeichnis'})
# Sende Datei zum Download
directory = os.path.dirname(safe_path)
filename = os.path.basename(safe_path)
return send_from_directory(directory, filename, as_attachment=True)
except Exception as e:
return jsonify({'error': str(e)})
if __name__ == '__main__': if __name__ == '__main__':
# Finde verfügbaren Port # Finde verfügbaren Port

335
file_manager.py Normal file
View File

@@ -0,0 +1,335 @@
"""
File Management System für App Installer & Manager
Ermöglicht Pterodactyl-ähnliche Dateiverwaltung
"""
import os
import shutil
import mimetypes
import zipfile
import tempfile
from pathlib import Path
from datetime import datetime
from werkzeug.utils import secure_filename
class FileManager:
def __init__(self, base_path):
self.base_path = Path(base_path)
self.allowed_extensions = {
'text': ['.txt', '.log', '.conf', '.cfg', '.ini', '.env', '.md', '.json', '.xml', '.yaml', '.yml'],
'code': ['.py', '.js', '.html', '.css', '.php', '.java', '.cpp', '.h', '.sql'],
'config': ['.dockerfile', '.dockerignore', '.gitignore', '.htaccess'],
'archive': ['.zip', '.tar', '.gz', '.rar'],
'image': ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico']
}
# Maximale Dateigröße (in Bytes)
self.max_file_size = 50 * 1024 * 1024 # 50MB
def get_directory_contents(self, relative_path=""):
"""Hole Verzeichnisinhalt mit Metadaten"""
try:
current_path = self.base_path / relative_path
# Sicherheitscheck - Pfad darf nicht außerhalb der Basis liegen
if not self._is_safe_path(current_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not current_path.exists():
return {'error': 'Verzeichnis nicht gefunden'}
items = []
# Parent Directory Link (außer im Root)
if relative_path and relative_path != "":
parent_path = str(Path(relative_path).parent)
if parent_path == ".":
parent_path = ""
items.append({
'name': '..',
'type': 'directory',
'path': parent_path,
'size': 0,
'modified': '',
'permissions': 'read'
})
# Verzeichnisse und Dateien auflisten
for item in sorted(current_path.iterdir()):
try:
stat_info = item.stat()
relative_item_path = str(item.relative_to(self.base_path))
item_data = {
'name': item.name,
'type': 'directory' if item.is_dir() else 'file',
'path': relative_item_path,
'size': stat_info.st_size if item.is_file() else 0,
'modified': datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
'permissions': self._get_permissions(item),
'extension': item.suffix.lower() if item.is_file() else '',
'mime_type': mimetypes.guess_type(str(item))[0] if item.is_file() else None
}
items.append(item_data)
except (PermissionError, OSError):
# Dateien ohne Berechtigung überspringen
continue
return {
'current_path': relative_path,
'items': items,
'total_items': len(items)
}
except Exception as e:
return {'error': str(e)}
def read_file(self, relative_path):
"""Lese Dateiinhalt (nur Text-Dateien)"""
try:
file_path = self.base_path / relative_path
if not self._is_safe_path(file_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not file_path.exists() or file_path.is_dir():
raise ValueError("Datei nicht gefunden")
# Prüfe ob es eine editierbare Datei ist
if not self._is_editable_file(file_path):
raise ValueError("Dateityp kann nicht bearbeitet werden")
# Größencheck
if file_path.stat().st_size > self.max_file_size:
raise ValueError("Datei zu groß zum Anzeigen")
# Versuche UTF-8, dann Latin-1
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except UnicodeDecodeError:
with open(file_path, 'r', encoding='latin-1') as f:
content = f.read()
return {
'content': content,
'size': file_path.stat().st_size,
'encoding': 'utf-8',
'editable': True
}
except Exception as e:
return {'error': str(e)}
def write_file(self, relative_path, content):
"""Schreibe Dateiinhalt"""
try:
file_path = self.base_path / relative_path
if not self._is_safe_path(file_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not self._is_editable_file(file_path):
raise ValueError("Dateityp kann nicht bearbeitet werden")
# Backup erstellen wenn Datei existiert
if file_path.exists():
backup_path = file_path.with_suffix(file_path.suffix + '.backup')
shutil.copy2(file_path, backup_path)
# Datei schreiben
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
return {'success': True, 'message': 'Datei gespeichert'}
except Exception as e:
return {'error': str(e)}
def create_file(self, relative_path, name):
"""Erstelle neue Datei"""
try:
dir_path = self.base_path / relative_path
file_path = dir_path / secure_filename(name)
if not self._is_safe_path(file_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if file_path.exists():
raise ValueError("Datei existiert bereits")
# Leere Datei erstellen
file_path.touch()
return {'success': True, 'message': 'Datei erstellt'}
except Exception as e:
return {'error': str(e)}
def create_directory(self, relative_path, name):
"""Erstelle neues Verzeichnis"""
try:
dir_path = self.base_path / relative_path
new_dir_path = dir_path / secure_filename(name)
if not self._is_safe_path(new_dir_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if new_dir_path.exists():
raise ValueError("Verzeichnis existiert bereits")
new_dir_path.mkdir(parents=True)
return {'success': True, 'message': 'Verzeichnis erstellt'}
except Exception as e:
return {'error': str(e)}
def delete_item(self, relative_path):
"""Lösche Datei oder Verzeichnis"""
try:
item_path = self.base_path / relative_path
if not self._is_safe_path(item_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not item_path.exists():
raise ValueError("Element nicht gefunden")
if item_path.is_dir():
shutil.rmtree(item_path)
else:
item_path.unlink()
return {'success': True, 'message': 'Element gelöscht'}
except Exception as e:
return {'error': str(e)}
def rename_item(self, relative_path, new_name):
"""Benenne Datei oder Verzeichnis um"""
try:
old_path = self.base_path / relative_path
new_path = old_path.parent / secure_filename(new_name)
if not self._is_safe_path(old_path) or not self._is_safe_path(new_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not old_path.exists():
raise ValueError("Element nicht gefunden")
if new_path.exists():
raise ValueError("Zielname existiert bereits")
old_path.rename(new_path)
return {'success': True, 'message': 'Element umbenannt'}
except Exception as e:
return {'error': str(e)}
def upload_file(self, relative_path, file_obj):
"""Lade Datei hoch"""
try:
if not file_obj or not file_obj.filename:
raise ValueError("Keine Datei ausgewählt")
filename = secure_filename(file_obj.filename)
upload_path = self.base_path / relative_path / filename
if not self._is_safe_path(upload_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
# Größencheck
file_obj.seek(0, 2) # Springe zum Ende
size = file_obj.tell()
file_obj.seek(0) # Zurück zum Anfang
if size > self.max_file_size:
raise ValueError(f"Datei zu groß (max. {self.max_file_size // 1024 // 1024}MB)")
# Datei speichern
file_obj.save(str(upload_path))
return {'success': True, 'message': 'Datei hochgeladen'}
except Exception as e:
return {'error': str(e)}
def download_file(self, relative_path):
"""Bereite Datei für Download vor"""
try:
file_path = self.base_path / relative_path
if not self._is_safe_path(file_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not file_path.exists() or file_path.is_dir():
raise ValueError("Datei nicht gefunden")
return {
'path': str(file_path),
'filename': file_path.name,
'size': file_path.stat().st_size
}
except Exception as e:
return {'error': str(e)}
def compress_directory(self, relative_path):
"""Komprimiere Verzeichnis zu ZIP"""
try:
dir_path = self.base_path / relative_path
if not self._is_safe_path(dir_path):
raise ValueError("Zugriff außerhalb des erlaubten Bereichs")
if not dir_path.exists() or not dir_path.is_dir():
raise ValueError("Verzeichnis nicht gefunden")
# Temporäre ZIP-Datei erstellen
temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
with zipfile.ZipFile(temp_zip.name, 'w', zipfile.ZIP_DEFLATED) as zipf:
for file_path in dir_path.rglob('*'):
if file_path.is_file():
arcname = file_path.relative_to(dir_path)
zipf.write(file_path, arcname)
return {
'path': temp_zip.name,
'filename': f"{dir_path.name}.zip"
}
except Exception as e:
return {'error': str(e)}
def _is_safe_path(self, path):
"""Prüfe ob Pfad sicher ist (innerhalb der Basis)"""
try:
path.resolve().relative_to(self.base_path.resolve())
return True
except ValueError:
return False
def _is_editable_file(self, path):
"""Prüfe ob Datei editierbar ist"""
suffix = path.suffix.lower()
for category in ['text', 'code', 'config']:
if suffix in self.allowed_extensions[category]:
return True
return False
def _get_permissions(self, path):
"""Ermittle Dateiberechtigungen (vereinfacht)"""
try:
if os.access(path, os.W_OK):
return 'write'
elif os.access(path, os.R_OK):
return 'read'
else:
return 'none'
except:
return 'unknown'

View File

@@ -3,3 +3,5 @@ requests==2.31.0
docker==6.1.3 docker==6.1.3
PyYAML==6.0.1 PyYAML==6.0.1
python-dotenv==1.0.0 python-dotenv==1.0.0
secrets
hashlib

19
sessions_db.json Normal file
View File

@@ -0,0 +1,19 @@
{
"sessions": {
"_gePrCgPc7vWx-zI3c_PLWSU0-1ryVMz1Wa4isufgjU": {
"username": "admin",
"created_at": "2025-07-09T20:56:42.783910",
"last_activity": "2025-07-09T23:58:11.364229",
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:140.0) Gecko/20100101 Firefox/140.0"
},
"25V3zUYYjYIoD21HlNwAsw-kLyh5Yu8bJmLtNCvHiBc": {
"username": "admin",
"created_at": "2025-07-09T23:59:01.223498",
"last_activity": "2025-07-09T23:59:01.223498",
"ip_address": "127.0.0.1",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Code/1.101.2 Chrome/134.0.6998.205 Electron/35.5.1 Safari/537.36"
}
},
"created_at": "2025-07-09T20:53:47.361864"
}

View File

@@ -39,6 +39,8 @@
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-radius: 10px; border-radius: 10px;
margin-bottom: 20px; margin-bottom: 20px;
position: relative;
z-index: 10001 !important; /* Höchste Priorität für Navbar */
} }
.card { .card {
@@ -118,98 +120,107 @@
} }
} }
/* Dropdown-Menü Styles */ /* Dropdown-Menü Styles - Maximale Z-Index Priorität */
.dropdown-menu { .dropdown-menu {
border-radius: 10px; border-radius: 10px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
border: none; border: none;
z-index: 999999 !important;
position: absolute !important;
} }
.dropdown-item { .dropdown {
padding: 10px 15px; z-index: 999998 !important;
border-radius: 6px; position: relative !important;
margin: 2px 5px;
transition: background-color 0.2s ease;
} }
/* Navbar Dropdown spezifisch */
.navbar .dropdown-menu {
z-index: 1000000 !important;
position: absolute !important;
}
.navbar .dropdown {
z-index: 999999 !important;
position: relative !important;
}
/* Dropdown-Menu-End spezifische Styles */
.dropdown-menu-end {
right: 0 !important;
left: auto !important;
}
/* Global Dropdown-Fix */
.dropdown-menu.show {
z-index: 1000000 !important;
}
/* Verhindere andere Elemente über Dropdowns */
.card {
z-index: 1;
position: relative;
}
.modal {
z-index: 10050 !important; /* Modals über Dropdowns */
}
.modal-backdrop {
z-index: 10040 !important;
}
/* Spezifische Fixes für überlappende Elemente */
.container-fluid {
z-index: 1;
position: relative;
}
/* Alle anderen Elemente sollen unter Dropdowns bleiben */
.project-card, .header-stats, .btn, .alert {
z-index: 1 !important;
position: relative;
}
/* Sicherstellen, dass Dropdown-Parent korrekt positioniert ist */
.nav-item.dropdown {
position: relative !important;
}
/* Dropdown-Menü-Animation und bessere Unterstützung */
.dropdown-menu {
transition: opacity 0.15s ease-in-out, transform 0.15s ease-in-out;
}
.dropdown-menu:not(.show) {
opacity: 0;
transform: translateY(-10px);
pointer-events: none;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
/* Hover-Effekte für Dropdown-Items */
.dropdown-item:hover { .dropdown-item:hover {
background-color: #f8f9fa; background-color: rgba(0, 123, 255, 0.1);
transition: background-color 0.15s ease-in-out;
} }
.dropdown-item i { /* Mobile-spezifische Dropdown-Styles */
width: 20px; @media (max-width: 768px) {
text-align: center; .navbar .dropdown-menu {
position: static !important;
z-index: 1000000 !important;
box-shadow: none;
border: 1px solid rgba(0,0,0,0.15);
margin-top: 0.5rem;
}
} }
.dropdown-toggle-split {
border-left: 1px solid rgba(255, 255, 255, 0.2);
}
/* Installation Button Group */
.btn-group .btn-success {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group .dropdown-toggle-split {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
padding-left: 8px;
padding-right: 8px;
}
/* Deaktivierte Dropdown-Buttons */
.dropdown-toggle-split.disabled,
.dropdown-toggle-split:disabled {
cursor: not-allowed;
opacity: 0.6;
pointer-events: none;
}
.dropdown-item.disabled {
opacity: 0.5;
pointer-events: none;
cursor: not-allowed;
}
/* Tooltip für deaktivierte Buttons */
.btn[title]:disabled {
cursor: help;
}
/* Neue Kategorie-Statistiken */
.category-stat {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: none;
min-height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
}
.category-stat:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.stat-number-cat {
font-size: 2em;
font-weight: bold;
margin-bottom: 5px;
}
/* Technologie-Statistiken */
.tech-stat {
transition: all 0.2s ease;
border: 1px solid #dee2e6;
}
.tech-stat:hover {
background-color: #f8f9fa !important;
transform: translateX(5px);
}
/* Erweiterte Projekt-Karten */
.project-card { .project-card {
transition: all 0.3s ease; transition: all 0.3s ease;
border-radius: 12px; border-radius: 12px;
@@ -493,16 +504,85 @@
<i class="fas fa-cube"></i> Docker Status <i class="fas fa-cube"></i> Docker Status
</a> </a>
</li> </li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('file_manager_view') }}">
<i class="fas fa-folder-open"></i> File Manager
</a>
</li>
<!-- Admin/Moderator Navigation -->
{% if session.user_role in ['admin', 'moderator'] %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="adminDropdown" role="button" data-bs-toggle="dropdown">
<i class="fas fa-cogs"></i> Verwaltung
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="{{ url_for('config') }}">
<i class="fas fa-cog me-2"></i>System-Konfiguration
</a></li>
{% if session.user_role == 'admin' %}
<li><a class="dropdown-item" href="{{ url_for('users_management') }}">
<i class="fas fa-users me-2"></i>Benutzerverwaltung
</a></li>
{% endif %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-chart-line me-2"></i>System-Monitoring
</a></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-download me-2"></i>Backups
</a></li>
</ul>
</li>
{% else %}
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="{{ url_for('config') }}"> <a class="nav-link" href="{{ url_for('config') }}">
<i class="fas fa-cog"></i> Konfiguration <i class="fas fa-cog"></i> Konfiguration
</a> </a>
</li> </li>
{% endif %}
</ul> </ul>
<!-- User Menu -->
<div class="navbar-nav"> <div class="navbar-nav">
<a class="nav-link btn btn-outline-primary" href="{{ url_for('refresh_projects') }}"> {% if session.username %}
<div class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown">
<div class="user-avatar-small me-2" style="width: 30px; height: 30px; border-radius: 50%; background: #667eea; color: white; display: flex; align-items: center; justify-content: center; font-size: 0.8rem; font-weight: bold;">
{{ session.username[0].upper() }}
</div>
{{ session.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li class="dropdown-header">
<small class="text-muted">
Angemeldet als <strong>{{ session.user_role.title() }}</strong>
</small>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('profile') }}">
<i class="fas fa-user me-2"></i>Mein Profil
</a></li>
<li><a class="dropdown-item" href="#">
<i class="fas fa-cog me-2"></i>Einstellungen
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="{{ url_for('logout') }}">
<i class="fas fa-sign-out-alt me-2"></i>Abmelden
</a></li>
</ul>
</div>
{% else %}
<a class="nav-link btn btn-outline-primary" href="{{ url_for('login') }}">
<i class="fas fa-sign-in-alt"></i> Anmelden
</a>
{% endif %}
<!-- Refresh Button für angemeldete Benutzer -->
{% if session.username %}
<a class="nav-link btn btn-outline-secondary ms-2" href="{{ url_for('refresh_projects') }}">
<i class="fas fa-sync-alt"></i> Aktualisieren <i class="fas fa-sync-alt"></i> Aktualisieren
</a> </a>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@@ -723,7 +803,10 @@
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
checkAndFixIcons(); checkAndFixIcons();
checkSystemHealth(); checkSystemHealth();
// Dropdowns funktionieren automatisch mit Bootstrap - keine zusätzliche Initialisierung nötig
}); });
// Bootstrap Dropdowns funktionieren automatisch ohne zusätzliches JavaScript
</script> </script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}

2145
templates/file_manager.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,565 @@
{% extends "base.html" %}
{% block title %}File Manager - {{ super() }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="fas fa-folder-open me-2"></i>
File Manager
</h4>
<p class="mb-0 mt-1 text-muted">Verwalte deine Projektdateien</p>
</div>
<div class="card-body">
<!-- Navigation -->
<div class="mb-3 p-2 bg-light rounded">
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0" id="breadcrumb">
<li class="breadcrumb-item">
<a href="#" onclick="navigateTo('')">
<i class="fas fa-home"></i> Root
</a>
</li>
</ol>
</nav>
</div>
<!-- Toolbar -->
<div class="mb-3">
<div class="btn-toolbar" role="toolbar">
<div class="btn-group me-2" role="group">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="createFolder()">
<i class="fas fa-folder-plus"></i> Ordner
</button>
<button type="button" class="btn btn-outline-success btn-sm" onclick="createFile()">
<i class="fas fa-file-plus"></i> Datei
</button>
</div>
<div class="btn-group me-2" role="group">
<input type="file" id="fileInput" multiple style="display: none;">
<button type="button" class="btn btn-outline-info btn-sm" onclick="uploadFiles()">
<i class="fas fa-upload"></i> Hochladen
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="refreshFiles()">
<i class="fas fa-sync-alt"></i> Aktualisieren
</button>
</div>
</div>
</div>
<!-- File List -->
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Typ</th>
<th>Größe</th>
<th>Geändert</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody id="fileList">
<tr>
<td colspan="5" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Lade...</span>
</div>
<p class="mt-2 mb-0">Lade Dateien...</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- File Editor Modal -->
<div class="modal fade" id="editorModal" tabindex="-1">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>
<span id="editorFileName">Datei bearbeiten</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea id="fileEditor" class="form-control" rows="20" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
<button type="button" class="btn btn-primary" onclick="saveFile()">
<i class="fas fa-save"></i> Speichern
</button>
</div>
</div>
</div>
</div>
<!-- Create Folder Modal -->
<div class="modal fade" id="createFolderModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neuer Ordner</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="folderName" class="form-label">Ordnername</label>
<input type="text" class="form-control" id="folderName" placeholder="Ordnername eingeben...">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="createFolderConfirm()">Erstellen</button>
</div>
</div>
</div>
</div>
<!-- Create File Modal -->
<div class="modal fade" id="createFileModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Neue Datei</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="fileName" class="form-label">Dateiname</label>
<input type="text" class="form-control" id="fileName" placeholder="Dateiname.txt">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="createFileConfirm()">Erstellen</button>
</div>
</div>
</div>
</div>
<!-- Rename Modal -->
<div class="modal fade" id="renameModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Umbenennen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newName" class="form-label">Neuer Name</label>
<input type="text" class="form-control" id="newName" placeholder="Neuer Name...">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="button" class="btn btn-primary" onclick="renameFileConfirm()">Umbenennen</button>
</div>
</div>
</div>
</div>
<script>
let currentPath = '';
let currentEditFile = '';
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadFiles();
setupFileInput();
});
function setupFileInput() {
document.getElementById('fileInput').addEventListener('change', function(e) {
handleFileUpload(e.target.files);
});
}
function loadFiles() {
fetch(`/api/files/list?path=${encodeURIComponent(currentPath)}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Laden: ' + data.error);
return;
}
renderFiles(data.items || []);
updateBreadcrumb(data.breadcrumbs || []);
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function renderFiles(files) {
const tbody = document.getElementById('fileList');
if (files.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="5" class="text-center py-4 text-muted">
<i class="fas fa-folder-open fa-3x mb-3"></i>
<p>Dieser Ordner ist leer</p>
</td>
</tr>
`;
return;
}
let html = '';
files.forEach(file => {
const icon = getFileIcon(file);
const size = file.type === 'directory' ? '-' : (file.size || '0 B');
const modified = file.modified || '-';
html += `
<tr>
<td>
<i class="${icon}"></i>
<span class="ms-2">${file.name}</span>
${file.is_parent ? '<small class="text-muted">(Zurück)</small>' : ''}
</td>
<td>
<span class="badge ${file.type === 'directory' ? 'bg-warning' : 'bg-info'}">
${file.type === 'directory' ? 'Ordner' : 'Datei'}
</span>
</td>
<td>${size}</td>
<td>${modified}</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-primary" onclick="openFile('${file.path}', '${file.type}')">
<i class="fas fa-eye"></i>
</button>
${file.type !== 'directory' ? `
<button class="btn btn-outline-success" onclick="editFile('${file.path}')">
<i class="fas fa-edit"></i>
</button>
<button class="btn btn-outline-info" onclick="downloadFile('${file.path}')">
<i class="fas fa-download"></i>
</button>
` : ''}
<button class="btn btn-outline-warning" onclick="renameFile('${file.path}', '${file.name}')">
<i class="fas fa-i-cursor"></i>
</button>
<button class="btn btn-outline-danger" onclick="deleteFile('${file.path}', '${file.name}')">
<i class="fas fa-trash"></i>
</button>
</div>
</td>
</tr>
`;
});
tbody.innerHTML = html;
}
function getFileIcon(file) {
if (file.is_parent) {
return 'fas fa-arrow-left text-secondary';
}
if (file.type === 'directory') {
return 'fas fa-folder text-warning';
}
const extension = file.name.split('.').pop().toLowerCase();
const iconMap = {
'txt': 'fas fa-file-alt text-info',
'py': 'fab fa-python text-primary',
'js': 'fab fa-js-square text-warning',
'html': 'fab fa-html5 text-danger',
'css': 'fab fa-css3-alt text-primary',
'json': 'fas fa-file-code text-warning',
'yml': 'fas fa-file-code text-info',
'yaml': 'fas fa-file-code text-info',
'md': 'fas fa-file-alt text-info',
'log': 'fas fa-file-alt text-muted',
'zip': 'fas fa-file-archive text-secondary',
'jpg': 'fas fa-file-image text-success',
'png': 'fas fa-file-image text-success',
'pdf': 'fas fa-file-pdf text-danger'
};
return iconMap[extension] || 'fas fa-file text-muted';
}
function updateBreadcrumb(breadcrumbs) {
const nav = document.getElementById('breadcrumb');
let html = `
<li class="breadcrumb-item">
<a href="#" onclick="navigateTo('')">
<i class="fas fa-home"></i> Root
</a>
</li>
`;
breadcrumbs.forEach((crumb, index) => {
if (crumb.name !== 'Root') {
if (index === breadcrumbs.length - 1) {
html += `<li class="breadcrumb-item active">${crumb.name}</li>`;
} else {
html += `<li class="breadcrumb-item"><a href="#" onclick="navigateTo('${crumb.path}')">${crumb.name}</a></li>`;
}
}
});
nav.innerHTML = html;
}
function navigateTo(path) {
currentPath = path;
loadFiles();
}
function openFile(path, type) {
if (type === 'directory') {
navigateTo(path);
} else {
editFile(path);
}
}
function editFile(path) {
fetch(`/api/files/read?path=${encodeURIComponent(path)}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Laden: ' + data.error);
return;
}
if (data.is_binary) {
showError('Binäre Dateien können nicht bearbeitet werden.');
return;
}
document.getElementById('editorFileName').textContent = path.split('/').pop();
document.getElementById('fileEditor').value = data.content;
currentEditFile = path;
new bootstrap.Modal(document.getElementById('editorModal')).show();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function saveFile() {
if (!currentEditFile) return;
const content = document.getElementById('fileEditor').value;
fetch('/api/files/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: currentEditFile, content: content })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Speichern: ' + data.error);
return;
}
showSuccess('Datei gespeichert!');
bootstrap.Modal.getInstance(document.getElementById('editorModal')).hide();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function downloadFile(path) {
window.open(`/api/files/download?path=${encodeURIComponent(path)}`, '_blank');
}
function deleteFile(path, name) {
if (!confirm(`Möchten Sie "${name}" wirklich löschen?`)) return;
fetch('/api/files/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Löschen: ' + data.error);
return;
}
showSuccess('Element gelöscht!');
loadFiles();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function createFolder() {
document.getElementById('folderName').value = '';
new bootstrap.Modal(document.getElementById('createFolderModal')).show();
}
function createFolderConfirm() {
const name = document.getElementById('folderName').value.trim();
if (!name) return;
const path = currentPath ? `${currentPath}/${name}` : name;
fetch('/api/files/create_directory', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Erstellen: ' + data.error);
return;
}
showSuccess('Ordner erstellt!');
loadFiles();
bootstrap.Modal.getInstance(document.getElementById('createFolderModal')).hide();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function createFile() {
document.getElementById('fileName').value = '';
new bootstrap.Modal(document.getElementById('createFileModal')).show();
}
function createFileConfirm() {
const name = document.getElementById('fileName').value.trim();
if (!name) return;
const path = currentPath ? `${currentPath}/${name}` : name;
fetch('/api/files/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path, content: '' })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Erstellen: ' + data.error);
return;
}
showSuccess('Datei erstellt!');
loadFiles();
bootstrap.Modal.getInstance(document.getElementById('createFileModal')).hide();
// Datei direkt bearbeiten
setTimeout(() => editFile(path), 500);
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function renameFile(path, currentName) {
document.getElementById('newName').value = currentName;
document.getElementById('renameModal').dataset.path = path;
new bootstrap.Modal(document.getElementById('renameModal')).show();
}
function renameFileConfirm() {
const newName = document.getElementById('newName').value.trim();
const path = document.getElementById('renameModal').dataset.path;
if (!newName || !path) return;
fetch('/api/files/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path, new_name: newName })
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Umbenennen: ' + data.error);
return;
}
showSuccess('Element umbenannt!');
loadFiles();
bootstrap.Modal.getInstance(document.getElementById('renameModal')).hide();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function uploadFiles() {
document.getElementById('fileInput').click();
}
function handleFileUpload(files) {
if (!files.length) return;
const formData = new FormData();
for (let file of files) {
formData.append('file', file);
}
formData.append('path', currentPath);
fetch('/api/files/upload', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
showError('Fehler beim Hochladen: ' + data.error);
return;
}
showSuccess('Datei(en) hochgeladen!');
loadFiles();
})
.catch(error => {
showError('Netzwerkfehler: ' + error.message);
});
}
function refreshFiles() {
loadFiles();
}
function showError(message) {
console.error(message);
alert('Fehler: ' + message);
}
function showSuccess(message) {
console.log(message);
// Could be replaced with toast notifications
}
</script>
{% endblock %}

156
templates/login.html Normal file
View File

@@ -0,0 +1,156 @@
{% extends "base.html" %}
{% block title %}Login - App Installer{% endblock %}
{% block head %}
<style>
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
}
.login-card {
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
overflow: hidden;
width: 100%;
max-width: 400px;
}
.login-header {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
padding: 2rem;
text-align: center;
}
.login-body {
padding: 2rem;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-login {
background: linear-gradient(45deg, #667eea, #764ba2);
border: none;
color: white;
padding: 12px;
border-radius: 8px;
width: 100%;
font-weight: 600;
transition: all 0.3s ease;
}
.btn-login:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
color: white;
}
.login-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.9;
}
.input-group {
margin-bottom: 1rem;
}
.input-group-text {
background: #f8f9fa;
border-color: #e9ecef;
}
</style>
{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-card">
<div class="login-header">
<i class="fas fa-rocket login-icon"></i>
<h2 class="mb-0">App Installer</h2>
<p class="mb-0 mt-2 opacity-75">Server Management Panel</p>
</div>
<div class="login-body">
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
<form method="POST">
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-user"></i>
</span>
<input type="text"
class="form-control"
name="username"
placeholder="Benutzername"
required
autofocus>
</div>
<div class="input-group">
<span class="input-group-text">
<i class="fas fa-lock"></i>
</span>
<input type="password"
class="form-control"
name="password"
placeholder="Passwort"
required>
</div>
<button type="submit" class="btn btn-login">
<i class="fas fa-sign-in-alt me-2"></i>
Anmelden
</button>
</form>
<div class="text-center mt-3">
<small class="text-muted">
<i class="fas fa-info-circle me-1"></i>
Pterodactyl-ähnliches Management Panel
</small>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-hide alerts after 5 seconds
document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
if (alert && alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 150);
}
}, 5000);
});
});
</script>
{% endblock %}

262
templates/profile.html Normal file
View File

@@ -0,0 +1,262 @@
{% extends "base.html" %}
{% block title %}Benutzerprofil - App Installer{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-4">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('index') }}"><i class="fas fa-home"></i> Dashboard</a></li>
<li class="breadcrumb-item active">Profil</li>
</ol>
</nav>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
<div class="row">
<!-- Profil-Info -->
<div class="col-lg-4 col-md-6 mb-4">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-user me-2"></i>
Benutzer-Informationen
</h5>
</div>
<div class="card-body">
<div class="text-center mb-3">
<div class="avatar-circle bg-primary text-white mb-3" style="width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto; font-size: 2rem;">
{{ user.first_name[0] if user.first_name else user.username[0] }}{{ user.last_name[0] if user.last_name else '' }}
</div>
<h4 class="mb-1">
{% if user.first_name or user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% else %}
{{ user.username }}
{% endif %}
</h4>
<span class="badge bg-{% if user.role == 'admin' %}danger{% elif user.role == 'moderator' %}warning{% elif user.role == 'user' %}primary{% else %}secondary{% endif %} fs-6">
{{ user.role.title() }}
</span>
</div>
<hr>
<div class="info-item mb-2">
<strong><i class="fas fa-user me-2"></i>Benutzername:</strong>
<span class="text-muted">{{ user.username }}</span>
</div>
<div class="info-item mb-2">
<strong><i class="fas fa-envelope me-2"></i>E-Mail:</strong>
<span class="text-muted">{{ user.email }}</span>
</div>
<div class="info-item mb-2">
<strong><i class="fas fa-calendar-plus me-2"></i>Erstellt:</strong>
<span class="text-muted">{{ user.created_at[:10] }}</span>
</div>
{% if user.last_login %}
<div class="info-item mb-2">
<strong><i class="fas fa-clock me-2"></i>Letzter Login:</strong>
<span class="text-muted">{{ user.last_login[:16] }}</span>
</div>
{% endif %}
<div class="info-item mb-0">
<strong><i class="fas fa-sign-in-alt me-2"></i>Login-Anzahl:</strong>
<span class="text-muted">{{ user.login_count or 0 }}x</span>
</div>
</div>
</div>
</div>
<!-- Passwort ändern -->
<div class="col-lg-8 col-md-6 mb-4">
<div class="card">
<div class="card-header bg-warning text-dark">
<h5 class="card-title mb-0">
<i class="fas fa-key me-2"></i>
Passwort ändern
</h5>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('change_password') }}">
<div class="row">
<div class="col-md-12 mb-3">
<label for="old_password" class="form-label">Aktuelles Passwort</label>
<input type="password"
class="form-control"
id="old_password"
name="old_password"
required>
</div>
<div class="col-md-6 mb-3">
<label for="new_password" class="form-label">Neues Passwort</label>
<input type="password"
class="form-control"
id="new_password"
name="new_password"
minlength="6"
required>
<div class="form-text">Mindestens 6 Zeichen</div>
</div>
<div class="col-md-6 mb-3">
<label for="confirm_password" class="form-label">Passwort bestätigen</label>
<input type="password"
class="form-control"
id="confirm_password"
name="confirm_password"
minlength="6"
required>
</div>
</div>
<button type="submit" class="btn btn-warning">
<i class="fas fa-save me-2"></i>
Passwort ändern
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Berechtigungen anzeigen -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-shield-alt me-2"></i>
Ihre Berechtigungen
</h5>
</div>
<div class="card-body">
<div class="row">
<!-- Projekt-Berechtigungen -->
<div class="col-md-6">
<h6 class="text-primary mb-3"><i class="fas fa-project-diagram me-2"></i>Projekt-Berechtigungen</h6>
<div class="permissions-list">
{% set project_permissions = [
('project.view', 'Projekte anzeigen', 'fas fa-eye'),
('project.create', 'Projekte erstellen', 'fas fa-plus'),
('project.update', 'Projekte aktualisieren', 'fas fa-edit'),
('project.delete', 'Projekte löschen', 'fas fa-trash'),
('project.start', 'Projekte starten', 'fas fa-play'),
('project.stop', 'Projekte stoppen', 'fas fa-stop'),
('project.restart', 'Projekte neustarten', 'fas fa-redo'),
('project.logs', 'Logs anzeigen', 'fas fa-file-alt'),
('project.console', 'Konsole nutzen', 'fas fa-terminal'),
('project.files', 'Dateien verwalten', 'fas fa-folder')
] %}
{% for perm, desc, icon in project_permissions %}
{% set has_perm = user.role in ['admin'] or (user.role == 'moderator' and perm != 'project.delete') or (user.role == 'user' and perm not in ['project.delete', 'project.console']) or (user.role == 'viewer' and perm in ['project.view', 'project.logs']) %}
<div class="permission-item d-flex align-items-center mb-2">
<i class="{{ icon }} me-2 {{ 'text-success' if has_perm else 'text-muted' }}"></i>
<span class="{{ 'text-dark' if has_perm else 'text-muted' }}">{{ desc }}</span>
{% if has_perm %}
<i class="fas fa-check text-success ms-auto"></i>
{% else %}
<i class="fas fa-times text-danger ms-auto"></i>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- System-Berechtigungen -->
<div class="col-md-6">
<h6 class="text-warning mb-3"><i class="fas fa-cogs me-2"></i>System-Berechtigungen</h6>
<div class="permissions-list">
{% set system_permissions = [
('system.config', 'Konfiguration', 'fas fa-cog'),
('system.users', 'Benutzerverwaltung', 'fas fa-users'),
('system.monitoring', 'System-Überwachung', 'fas fa-chart-line'),
('system.backups', 'Backup-Verwaltung', 'fas fa-download'),
('system.docker', 'Docker-Verwaltung', 'fab fa-docker')
] %}
{% for perm, desc, icon in system_permissions %}
{% set has_perm = user.role == 'admin' or (user.role == 'moderator' and perm in ['system.monitoring', 'system.docker']) %}
<div class="permission-item d-flex align-items-center mb-2">
<i class="{{ icon }} me-2 {{ 'text-success' if has_perm else 'text-muted' }}"></i>
<span class="{{ 'text-dark' if has_perm else 'text-muted' }}">{{ desc }}</span>
{% if has_perm %}
<i class="fas fa-check text-success ms-auto"></i>
{% else %}
<i class="fas fa-times text-danger ms-auto"></i>
{% endif %}
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.permission-item {
padding: 0.25rem 0;
border-bottom: 1px solid #f8f9fa;
}
.permission-item:last-child {
border-bottom: none;
}
.permissions-list {
max-height: 300px;
overflow-y: auto;
}
</style>
{% endblock %}
{% block scripts %}
<script>
// Passwort-Bestätigung prüfen
document.getElementById('confirm_password').addEventListener('input', function() {
const newPassword = document.getElementById('new_password').value;
const confirmPassword = this.value;
if (newPassword !== confirmPassword) {
this.setCustomValidity('Passwörter stimmen nicht überein');
} else {
this.setCustomValidity('');
}
});
document.getElementById('new_password').addEventListener('input', function() {
const confirmPassword = document.getElementById('confirm_password');
confirmPassword.dispatchEvent(new Event('input'));
});
</script>
{% endblock %}

View File

@@ -0,0 +1,401 @@
{% extends "base.html" %}
{% block title %}Benutzerverwaltung - App Installer{% endblock %}
{% block head %}
<style>
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
color: white;
margin-right: 10px;
}
.user-avatar.admin { background: #dc3545; }
.user-avatar.moderator { background: #fd7e14; }
.user-avatar.user { background: #0d6efd; }
.user-avatar.viewer { background: #6c757d; }
.role-badge.admin { background: #dc3545 !important; }
.role-badge.moderator { background: #fd7e14 !important; }
.role-badge.user { background: #0d6efd !important; }
.role-badge.viewer { background: #6c757d !important; }
.user-card {
transition: all 0.3s ease;
border: 1px solid #e9ecef;
}
.user-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
}
.stats-card {
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
}
.quick-action-btn {
border: none;
padding: 0.25rem 0.5rem;
border-radius: 0.375rem;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.modal-header.bg-primary {
background: linear-gradient(45deg, #667eea, #764ba2) !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-0">
<i class="fas fa-users me-2 text-primary"></i>
Benutzerverwaltung
</h1>
<p class="text-muted mb-0">Verwalten Sie Benutzer und deren Berechtigungen</p>
</div>
<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#createUserModal">
<i class="fas fa-user-plus me-2"></i>
Neuer Benutzer
</button>
</div>
<!-- Flash Messages -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'danger' if category == 'error' else category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</div>
<!-- Statistiken -->
<div class="row mb-4">
<div class="col-lg-3 col-md-6 mb-3">
<div class="card stats-card">
<div class="card-body text-center">
<i class="fas fa-users fa-2x mb-2"></i>
<h3 class="mb-1">{{ users|length }}</h3>
<small>Benutzer gesamt</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-danger">
<div class="card-body text-center">
<i class="fas fa-user-shield fa-2x mb-2 text-danger"></i>
<h3 class="mb-1 text-danger">{{ users.values()|selectattr('role', 'equalto', 'admin')|list|length }}</h3>
<small>Administratoren</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-warning">
<div class="card-body text-center">
<i class="fas fa-user-cog fa-2x mb-2 text-warning"></i>
<h3 class="mb-1 text-warning">{{ users.values()|selectattr('role', 'equalto', 'moderator')|list|length }}</h3>
<small>Moderatoren</small>
</div>
</div>
</div>
<div class="col-lg-3 col-md-6 mb-3">
<div class="card border-success">
<div class="card-body text-center">
<i class="fas fa-user-check fa-2x mb-2 text-success"></i>
<h3 class="mb-1 text-success">{{ users.values()|selectattr('enabled', 'equalto', true)|list|length }}</h3>
<small>Aktive Benutzer</small>
</div>
</div>
</div>
</div>
<!-- Benutzer-Liste -->
<div class="row">
{% for username, user in users.items() %}
<div class="col-lg-6 col-xl-4 mb-4">
<div class="card user-card h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="user-avatar {{ user.role }}">
{{ user.first_name[0] if user.first_name else user.username[0] }}{{ user.last_name[0] if user.last_name else '' }}
</div>
<div class="flex-grow-1">
<h6 class="mb-1">
{% if user.first_name or user.last_name %}
{{ user.first_name }} {{ user.last_name }}
{% else %}
{{ user.username }}
{% endif %}
</h6>
<small class="text-muted">@{{ user.username }}</small>
</div>
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#" data-bs-toggle="modal" data-bs-target="#editUserModal{{ loop.index }}">
<i class="fas fa-edit me-2"></i>Bearbeiten
</a></li>
{% if user.username != 'admin' or users.values()|selectattr('role', 'equalto', 'admin')|list|length > 1 %}
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item text-danger" href="#" data-bs-toggle="modal" data-bs-target="#deleteUserModal{{ loop.index }}">
<i class="fas fa-trash me-2"></i>Löschen
</a></li>
{% endif %}
</ul>
</div>
</div>
<div class="mb-3">
<span class="badge role-badge {{ user.role }} text-white px-2 py-1">
<i class="fas fa-{{ 'user-shield' if user.role == 'admin' else 'user-cog' if user.role == 'moderator' else 'user' if user.role == 'user' else 'eye' }} me-1"></i>
{{ user.role.title() }}
</span>
{% if not user.get('enabled', true) %}
<span class="badge bg-secondary ms-1">Deaktiviert</span>
{% endif %}
</div>
<div class="user-info">
<div class="info-row mb-2">
<small class="text-muted d-flex align-items-center">
<i class="fas fa-envelope me-2"></i>
{{ user.email }}
</small>
</div>
<div class="info-row mb-2">
<small class="text-muted d-flex align-items-center">
<i class="fas fa-calendar-plus me-2"></i>
Erstellt: {{ user.created_at[:10] }}
</small>
</div>
{% if user.last_login %}
<div class="info-row mb-2">
<small class="text-muted d-flex align-items-center">
<i class="fas fa-clock me-2"></i>
Letzter Login: {{ user.last_login[:16] }}
</small>
</div>
{% endif %}
<div class="info-row">
<small class="text-muted d-flex align-items-center">
<i class="fas fa-sign-in-alt me-2"></i>
Login-Anzahl: {{ user.login_count or 0 }}x
</small>
</div>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal{{ loop.index }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-edit me-2"></i>
Benutzer bearbeiten: {{ user.username }}
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('edit_user', username=user.username) }}">
<div class="modal-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Vorname</label>
<input type="text" class="form-control" name="first_name" value="{{ user.first_name or '' }}">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Nachname</label>
<input type="text" class="form-control" name="last_name" value="{{ user.last_name or '' }}">
</div>
</div>
<div class="mb-3">
<label class="form-label">E-Mail</label>
<input type="email" class="form-control" name="email" value="{{ user.email }}" required>
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<select class="form-select" name="role" required>
{% for role in roles %}
<option value="{{ role }}" {% if role == user.role %}selected{% endif %}>
{{ role.title() }}
</option>
{% endfor %}
</select>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" name="enabled" id="enabled{{ loop.index }}" {% if user.get('enabled', true) %}checked{% endif %}>
<label class="form-check-label" for="enabled{{ loop.index }}">
Benutzer ist aktiviert
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>Speichern
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete User Modal -->
{% if user.username != 'admin' or users.values()|selectattr('role', 'equalto', 'admin')|list|length > 1 %}
<div class="modal fade" id="deleteUserModal{{ loop.index }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">
<i class="fas fa-trash me-2"></i>
Benutzer löschen
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Sind Sie sicher, dass Sie den Benutzer <strong>{{ user.username }}</strong> löschen möchten?</p>
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
Diese Aktion kann nicht rückgängig gemacht werden!
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<form method="POST" action="{{ url_for('delete_user', username=user.username) }}" class="d-inline">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash me-2"></i>Löschen
</button>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
<!-- Create User Modal -->
<div class="modal fade" id="createUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-primary text-white">
<h5 class="modal-title">
<i class="fas fa-user-plus me-2"></i>
Neuen Benutzer erstellen
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<form method="POST" action="{{ url_for('create_user') }}">
<div class="modal-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Vorname</label>
<input type="text" class="form-control" name="first_name">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Nachname</label>
<input type="text" class="form-control" name="last_name">
</div>
</div>
<div class="mb-3">
<label class="form-label">Benutzername *</label>
<input type="text" class="form-control" name="username" required>
</div>
<div class="mb-3">
<label class="form-label">E-Mail *</label>
<input type="email" class="form-control" name="email" required>
</div>
<div class="mb-3">
<label class="form-label">Passwort *</label>
<input type="password" class="form-control" name="password" minlength="6" required>
<div class="form-text">Mindestens 6 Zeichen</div>
</div>
<div class="mb-3">
<label class="form-label">Rolle</label>
<select class="form-select" name="role" required>
{% for role in roles %}
<option value="{{ role }}" {% if role == 'user' %}selected{% endif %}>
{{ role.title() }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">
<i class="fas fa-user-plus me-2"></i>Benutzer erstellen
</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Auto-hide alerts
document.addEventListener('DOMContentLoaded', function() {
const alerts = document.querySelectorAll('.alert');
alerts.forEach(alert => {
setTimeout(() => {
if (alert && alert.parentNode) {
alert.classList.remove('show');
setTimeout(() => {
if (alert.parentNode) {
alert.parentNode.removeChild(alert);
}
}, 150);
}
}, 5000);
});
});
// Form validation
document.querySelectorAll('form').forEach(form => {
form.addEventListener('submit', function(e) {
const requiredFields = form.querySelectorAll('[required]');
let isValid = true;
requiredFields.forEach(field => {
if (!field.value.trim()) {
field.classList.add('is-invalid');
isValid = false;
} else {
field.classList.remove('is-invalid');
}
});
if (!isValid) {
e.preventDefault();
}
});
});
</script>
{% endblock %}

507
user_management.py Normal file
View File

@@ -0,0 +1,507 @@
"""
User Management System für App Installer & Manager
Pterodactyl-ähnliches Benutzer- und Rollensystem
"""
import os
import json
import hashlib
import secrets
import time
from datetime import datetime, timedelta
from functools import wraps
from flask import session, request, redirect, url_for, flash, jsonify
# Benutzer-Datenbank Datei
USERS_DB_FILE = 'users_db.json'
SESSIONS_DB_FILE = 'sessions_db.json'
class UserRole:
"""Benutzerrollen-Definitionen"""
ADMIN = 'admin'
MODERATOR = 'moderator'
USER = 'user'
VIEWER = 'viewer'
# Rollen-Hierarchie (höhere Zahl = mehr Rechte)
HIERARCHY = {
VIEWER: 1,
USER: 2,
MODERATOR: 3,
ADMIN: 4
}
@classmethod
def get_all_roles(cls):
return list(cls.HIERARCHY.keys())
@classmethod
def has_permission(cls, user_role, required_role):
"""Prüfe ob Benutzerrolle ausreichende Berechtigung hat"""
user_level = cls.HIERARCHY.get(user_role, 0)
required_level = cls.HIERARCHY.get(required_role, 0)
return user_level >= required_level
class Permission:
"""Berechtigungs-Definitionen"""
# Projekt-Berechtigungen
PROJECT_VIEW = 'project.view'
PROJECT_CREATE = 'project.create'
PROJECT_UPDATE = 'project.update'
PROJECT_DELETE = 'project.delete'
PROJECT_START = 'project.start'
PROJECT_STOP = 'project.stop'
PROJECT_RESTART = 'project.restart'
PROJECT_LOGS = 'project.logs'
PROJECT_CONSOLE = 'project.console'
PROJECT_FILES = 'project.files'
# File-Berechtigungen
FILE_ACCESS = 'file.access'
FILE_WRITE = 'file.write'
# System-Berechtigungen
SYSTEM_CONFIG = 'system.config'
SYSTEM_USERS = 'system.users'
SYSTEM_MONITORING = 'system.monitoring'
SYSTEM_BACKUPS = 'system.backups'
SYSTEM_DOCKER = 'system.docker'
# Rollen-zu-Berechtigungen Mapping
ROLE_PERMISSIONS = {
UserRole.VIEWER: [
PROJECT_VIEW,
PROJECT_LOGS,
FILE_ACCESS
],
UserRole.USER: [
PROJECT_VIEW,
PROJECT_CREATE,
PROJECT_UPDATE,
PROJECT_START,
PROJECT_STOP,
PROJECT_RESTART,
PROJECT_LOGS,
PROJECT_FILES,
FILE_ACCESS,
FILE_WRITE
],
UserRole.MODERATOR: [
PROJECT_VIEW,
PROJECT_CREATE,
PROJECT_UPDATE,
PROJECT_DELETE,
PROJECT_START,
PROJECT_STOP,
PROJECT_RESTART,
PROJECT_LOGS,
PROJECT_CONSOLE,
PROJECT_FILES,
FILE_ACCESS,
FILE_WRITE,
SYSTEM_MONITORING,
SYSTEM_DOCKER
],
UserRole.ADMIN: [
PROJECT_VIEW,
PROJECT_CREATE,
PROJECT_UPDATE,
PROJECT_DELETE,
PROJECT_START,
PROJECT_STOP,
PROJECT_RESTART,
PROJECT_LOGS,
PROJECT_CONSOLE,
PROJECT_FILES,
FILE_ACCESS,
FILE_WRITE,
SYSTEM_CONFIG,
SYSTEM_USERS,
SYSTEM_MONITORING,
SYSTEM_BACKUPS,
SYSTEM_DOCKER
]
}
@classmethod
def user_has_permission(cls, user_role, permission):
"""Prüfe ob Benutzer eine spezifische Berechtigung hat"""
user_permissions = cls.ROLE_PERMISSIONS.get(user_role, [])
return permission in user_permissions
class UserManager:
"""Benutzer-Management System"""
def __init__(self):
self.users_db = self._load_users_db()
self.sessions_db = self._load_sessions_db()
self._ensure_admin_user()
def _load_users_db(self):
"""Lade Benutzer-Datenbank"""
if os.path.exists(USERS_DB_FILE):
try:
with open(USERS_DB_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Fehler beim Laden der Benutzer-DB: {e}")
# Standard-Struktur
return {
'users': {},
'created_at': datetime.now().isoformat(),
'version': '1.0'
}
def _load_sessions_db(self):
"""Lade Session-Datenbank"""
if os.path.exists(SESSIONS_DB_FILE):
try:
with open(SESSIONS_DB_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
except Exception as e:
print(f"Fehler beim Laden der Session-DB: {e}")
return {
'sessions': {},
'created_at': datetime.now().isoformat()
}
def _save_users_db(self):
"""Speichere Benutzer-Datenbank"""
try:
with open(USERS_DB_FILE, 'w', encoding='utf-8') as f:
json.dump(self.users_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Benutzer-DB: {e}")
def _save_sessions_db(self):
"""Speichere Session-Datenbank"""
try:
with open(SESSIONS_DB_FILE, 'w', encoding='utf-8') as f:
json.dump(self.sessions_db, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Fehler beim Speichern der Session-DB: {e}")
def _ensure_admin_user(self):
"""Stelle sicher, dass ein Admin-Benutzer existiert"""
if not self.users_db['users']:
# Erstelle Standard-Admin
admin_password = self._generate_secure_password()
self.create_user(
username='admin',
email='admin@localhost',
password=admin_password,
role=UserRole.ADMIN,
first_name='System',
last_name='Administrator'
)
print(f"🔐 Standard-Admin erstellt:")
print(f" Benutzername: admin")
print(f" Passwort: {admin_password}")
print(f" ⚠️ BITTE PASSWORT SOFORT ÄNDERN!")
def _generate_secure_password(self, length=12):
"""Generiere sicheres Passwort"""
import string
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
return ''.join(secrets.choice(alphabet) for _ in range(length))
def _hash_password(self, password, salt=None):
"""Hash Passwort mit Salt"""
if salt is None:
salt = secrets.token_hex(32)
# Verwende SHA-256 mit Salt
password_hash = hashlib.sha256((password + salt).encode()).hexdigest()
return password_hash, salt
def _verify_password(self, password, stored_hash, salt):
"""Verifiziere Passwort"""
password_hash, _ = self._hash_password(password, salt)
return password_hash == stored_hash
def create_user(self, username, email, password, role=UserRole.USER,
first_name='', last_name='', enabled=True):
"""Erstelle neuen Benutzer"""
if username in self.users_db['users']:
return False, f"Benutzer '{username}' existiert bereits"
if role not in UserRole.get_all_roles():
return False, f"Ungültige Rolle: {role}"
# Hash Passwort
password_hash, salt = self._hash_password(password)
# Erstelle Benutzer
user_data = {
'username': username,
'email': email,
'password_hash': password_hash,
'password_salt': salt,
'role': role,
'first_name': first_name,
'last_name': last_name,
'enabled': enabled,
'created_at': datetime.now().isoformat(),
'last_login': None,
'login_count': 0,
'projects_access': [], # Spezifische Projekt-Zugriffe
'preferences': {
'theme': 'light',
'language': 'de',
'notifications': True
}
}
self.users_db['users'][username] = user_data
self._save_users_db()
return True, f"Benutzer '{username}' erfolgreich erstellt"
def authenticate_user(self, username, password):
"""Authentifiziere Benutzer"""
if username not in self.users_db['users']:
return False, "Benutzername oder Passwort falsch"
user = self.users_db['users'][username]
# Prüfe ob Benutzer aktiviert ist
if not user.get('enabled', True):
return False, "Benutzer ist deaktiviert"
# Verifiziere Passwort
if not self._verify_password(password, user['password_hash'], user['password_salt']):
return False, "Benutzername oder Passwort falsch"
# Update Login-Statistiken
user['last_login'] = datetime.now().isoformat()
user['login_count'] = user.get('login_count', 0) + 1
self._save_users_db()
return True, user
def create_session(self, username):
"""Erstelle Session für Benutzer"""
session_token = secrets.token_urlsafe(32)
session_data = {
'username': username,
'created_at': datetime.now().isoformat(),
'last_activity': datetime.now().isoformat(),
'ip_address': request.environ.get('REMOTE_ADDR', 'unknown'),
'user_agent': request.environ.get('HTTP_USER_AGENT', 'unknown')
}
self.sessions_db['sessions'][session_token] = session_data
self._save_sessions_db()
# Cleanup alte Sessions (älter als 30 Tage)
self._cleanup_old_sessions()
return session_token
def validate_session(self, session_token):
"""Validiere Session"""
if session_token not in self.sessions_db['sessions']:
return False, None
session_data = self.sessions_db['sessions'][session_token]
# Prüfe Session-Alter (24 Stunden)
created_at = datetime.fromisoformat(session_data['created_at'])
if datetime.now() - created_at > timedelta(hours=24):
self.destroy_session(session_token)
return False, None
# Update letzte Aktivität
session_data['last_activity'] = datetime.now().isoformat()
self._save_sessions_db()
username = session_data['username']
user = self.users_db['users'].get(username)
if not user or not user.get('enabled', True):
self.destroy_session(session_token)
return False, None
return True, user
def destroy_session(self, session_token):
"""Zerstöre Session"""
if session_token in self.sessions_db['sessions']:
del self.sessions_db['sessions'][session_token]
self._save_sessions_db()
def _cleanup_old_sessions(self):
"""Cleanup alte Sessions"""
current_time = datetime.now()
sessions_to_remove = []
for token, session_data in self.sessions_db['sessions'].items():
created_at = datetime.fromisoformat(session_data['created_at'])
if current_time - created_at > timedelta(days=30):
sessions_to_remove.append(token)
for token in sessions_to_remove:
del self.sessions_db['sessions'][token]
if sessions_to_remove:
self._save_sessions_db()
def get_user(self, username):
"""Hole Benutzer-Informationen"""
return self.users_db['users'].get(username)
def get_all_users(self):
"""Hole alle Benutzer"""
return self.users_db['users']
def update_user(self, username, **kwargs):
"""Update Benutzer-Informationen"""
if username not in self.users_db['users']:
return False, f"Benutzer '{username}' nicht gefunden"
user = self.users_db['users'][username]
# Erlaubte Update-Felder
allowed_fields = ['email', 'role', 'first_name', 'last_name', 'enabled', 'projects_access', 'preferences']
for field, value in kwargs.items():
if field in allowed_fields:
user[field] = value
self._save_users_db()
return True, f"Benutzer '{username}' aktualisiert"
def change_password(self, username, old_password, new_password):
"""Ändere Benutzer-Passwort"""
if username not in self.users_db['users']:
return False, "Benutzer nicht gefunden"
user = self.users_db['users'][username]
# Verifiziere altes Passwort
if not self._verify_password(old_password, user['password_hash'], user['password_salt']):
return False, "Altes Passwort ist falsch"
# Setze neues Passwort
password_hash, salt = self._hash_password(new_password)
user['password_hash'] = password_hash
user['password_salt'] = salt
self._save_users_db()
return True, "Passwort erfolgreich geändert"
def delete_user(self, username):
"""Lösche Benutzer"""
if username not in self.users_db['users']:
return False, f"Benutzer '{username}' nicht gefunden"
# Verhindere Löschung des letzten Admins
if self.users_db['users'][username]['role'] == UserRole.ADMIN:
admin_count = sum(1 for user in self.users_db['users'].values()
if user['role'] == UserRole.ADMIN and user.get('enabled', True))
if admin_count <= 1:
return False, "Der letzte Administrator kann nicht gelöscht werden"
del self.users_db['users'][username]
self._save_users_db()
# Entferne alle Sessions des Benutzers
sessions_to_remove = [token for token, session_data in self.sessions_db['sessions'].items()
if session_data['username'] == username]
for token in sessions_to_remove:
del self.sessions_db['sessions'][token]
if sessions_to_remove:
self._save_sessions_db()
return True, f"Benutzer '{username}' gelöscht"
# Globale User Manager Instanz
user_manager = UserManager()
# Decorator für Login-Erfordernis
def login_required(f):
"""Decorator: Erfordert Login"""
@wraps(f)
def decorated_function(*args, **kwargs):
session_token = session.get('session_token')
if not session_token:
flash('Sie müssen sich anmelden.', 'warning')
return redirect(url_for('login'))
valid, user = user_manager.validate_session(session_token)
if not valid:
session.pop('session_token', None)
flash('Ihre Session ist abgelaufen.', 'warning')
return redirect(url_for('login'))
# Füge Benutzer-Info zu Request hinzu
request.current_user = user
return f(*args, **kwargs)
return decorated_function
# Decorator für Rollen-Berechtigung
def role_required(required_role):
"""Decorator: Erfordert spezifische Rolle"""
def decorator(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
user = request.current_user
if not UserRole.has_permission(user['role'], required_role):
flash(f'Keine Berechtigung. Erforderliche Rolle: {required_role}', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
return decorator
# Decorator für spezifische Berechtigung
def permission_required(permission):
"""Decorator: Erfordert spezifische Berechtigung"""
def decorator(f):
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
user = request.current_user
if not Permission.user_has_permission(user['role'], permission):
flash(f'Keine Berechtigung für: {permission}', 'danger')
return redirect(url_for('index'))
return f(*args, **kwargs)
return decorated_function
return decorator
# API Helper für JSON-Antworten
def api_login_required(f):
"""API Decorator: Erfordert Login für API-Calls"""
@wraps(f)
def decorated_function(*args, **kwargs):
session_token = session.get('session_token')
if not session_token:
return jsonify({'success': False, 'error': 'Login erforderlich'}), 401
valid, user = user_manager.validate_session(session_token)
if not valid:
session.pop('session_token', None)
return jsonify({'success': False, 'error': 'Session abgelaufen'}), 401
request.current_user = user
return f(*args, **kwargs)
return decorated_function
def api_permission_required(permission):
"""API Decorator: Erfordert spezifische Berechtigung für API-Calls"""
def decorator(f):
@wraps(f)
@api_login_required
def decorated_function(*args, **kwargs):
user = request.current_user
if not Permission.user_has_permission(user['role'], permission):
return jsonify({'success': False, 'error': f'Keine Berechtigung für: {permission}'}), 403
return f(*args, **kwargs)
return decorated_function
return decorator

25
users_db.json Normal file
View File

@@ -0,0 +1,25 @@
{
"users": {
"admin": {
"username": "admin",
"email": "admin@localhost",
"password_hash": "a0f677d20ac159a9914e92c2a6466fda213a344de3853587e32e3d766992603c",
"password_salt": "4fdbb3d595c5f8e2db69baa4008f196ccc9f8e980da9bc0fd8f3f249caf3efd4",
"role": "admin",
"first_name": "System",
"last_name": "Administrator",
"enabled": true,
"created_at": "2025-07-09T20:53:47.361864",
"last_login": "2025-07-09T23:59:01.222407",
"login_count": 2,
"projects_access": [],
"preferences": {
"theme": "light",
"language": "de",
"notifications": true
}
}
},
"created_at": "2025-07-09T20:53:47.361864",
"version": "1.0"
}