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 os
import subprocess
@@ -10,11 +10,19 @@ import urllib.request
import urllib.error
import tempfile
import socket
import stat
from urllib.parse import urlparse
import docker
import yaml
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.secret_key = 'your-secret-key-change-this'
@@ -30,6 +38,212 @@ CONFIG_FILE = 'config.json'
PROJECTS_DIR = 'projects'
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:
def __init__(self):
self.docker_client = None
@@ -1011,7 +1225,7 @@ class ProjectManager:
if alternative_port:
return False, f"Port {blocked_port} ist bereits belegt. Alternativer freier Port: {alternative_port}"
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:
return False, f"Port-Konflikt: {error_msg}"
else:
@@ -1257,7 +1471,174 @@ class ProjectManager:
# Globale Instanz
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('/')
@login_required
def index():
"""Hauptseite mit Projektübersicht"""
config = project_manager.load_config()
@@ -1275,6 +1656,7 @@ def index():
return render_template('index.html', projects=projects, config=config)
@app.route('/refresh_projects')
@permission_required(Permission.PROJECT_VIEW)
def refresh_projects():
"""Aktualisiere Projektliste von URL"""
config = project_manager.load_config()
@@ -1291,6 +1673,7 @@ def refresh_projects():
return redirect(url_for('index'))
@app.route('/available_projects')
@permission_required(Permission.PROJECT_VIEW)
def available_projects():
"""Zeige verfügbare Projekte zum Installieren"""
config = project_manager.load_config()
@@ -1322,6 +1705,7 @@ def available_projects():
return render_template('available_projects.html', projects=projects)
@app.route('/install_project', methods=['POST'])
@permission_required(Permission.PROJECT_CREATE)
def install_project():
"""Installiere Projekt"""
project_url = request.form.get('project_url')
@@ -1480,6 +1864,7 @@ def install_project():
return jsonify({'success': success, 'message': message, 'method': installation_method})
@app.route('/build_project/<project_name>')
@permission_required(Permission.PROJECT_UPDATE)
def build_project(project_name):
"""Baue Projekt"""
success, message = project_manager.build_project(project_name)
@@ -1487,6 +1872,7 @@ def build_project(project_name):
return redirect(url_for('index'))
@app.route('/start_project/<project_name>')
@permission_required(Permission.PROJECT_START)
def start_project(project_name):
"""Starte Projekt"""
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))
@app.route('/stop_project/<project_name>')
@permission_required(Permission.PROJECT_STOP)
def stop_project(project_name):
"""Stoppe Projekt"""
success, message = project_manager.stop_project(project_name)
@@ -1522,6 +1909,7 @@ def stop_project(project_name):
return redirect(url_for('index'))
@app.route('/remove_project/<project_name>')
@permission_required(Permission.PROJECT_DELETE)
def remove_project(project_name):
"""Entferne Projekt"""
success, message = project_manager.remove_project(project_name)
@@ -1529,6 +1917,7 @@ def remove_project(project_name):
return redirect(url_for('index'))
@app.route('/config', methods=['GET', 'POST'])
@permission_required(Permission.SYSTEM_CONFIG)
def config():
"""Konfigurationsseite"""
try:
@@ -1590,6 +1979,7 @@ def config():
return render_template('config.html', config=fallback_config)
@app.route('/project_details/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def project_details(project_name):
"""Projektdetails und Konfiguration"""
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)
@app.route('/save_env/<project_name>', methods=['POST'])
@permission_required(Permission.PROJECT_FILES)
def save_env(project_name):
"""Speichere .env Konfiguration"""
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))
@app.route('/api/system_status')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_system_status():
"""API Endpoint für Systemstatus"""
status = {
@@ -1692,10 +2084,12 @@ def api_system_status():
return jsonify(status)
@app.route('/api/test_connection', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_test_connection():
"""Test Verbindung zu Projektliste URL"""
data = request.get_json()
url = data.get('url', '')
if not url:
return jsonify({'success': False, 'error': 'Keine URL angegeben'})
@@ -1711,6 +2105,7 @@ def api_test_connection():
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_logs/<project_name>')
@api_permission_required(Permission.PROJECT_LOGS)
def api_container_logs(project_name):
"""Hole Container-Logs für Debugging"""
try:
@@ -1733,6 +2128,7 @@ def api_container_logs(project_name):
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_inspect/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_inspect(project_name):
"""Detaillierte Container-Informationen für Debugging"""
try:
@@ -1766,6 +2162,7 @@ def api_container_inspect(project_name):
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/container_stats/<project_name>')
@api_permission_required(Permission.PROJECT_VIEW)
def api_container_stats(project_name):
"""Container Statistiken abrufen"""
try:
@@ -1801,6 +2198,7 @@ def api_container_stats(project_name):
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/restart_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_RESTART)
def api_restart_project(project_name):
"""Projekt Container neustarten"""
try:
@@ -1814,6 +2212,7 @@ def api_restart_project(project_name):
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/clear_cache', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_clear_cache():
"""Cache leeren"""
try:
@@ -1823,6 +2222,7 @@ def api_clear_cache():
return jsonify({'success': False, 'message': f'Fehler: {str(e)}'})
@app.route('/api/export_config')
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_export_config():
"""Konfiguration exportieren"""
try:
@@ -1838,6 +2238,7 @@ def api_export_config():
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/import_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_import_config():
"""Konfiguration importieren"""
try:
@@ -1848,6 +2249,7 @@ def api_import_config():
return jsonify({'success': False, 'error': str(e)})
@app.route('/api/reset_config', methods=['POST'])
@api_permission_required(Permission.SYSTEM_CONFIG)
def api_reset_config():
"""Konfiguration zurücksetzen"""
try:
@@ -1863,6 +2265,7 @@ def api_reset_config():
return jsonify({'success': False, 'error': str(e)})
@app.route('/docker_status')
@permission_required(Permission.SYSTEM_DOCKER)
def docker_status():
"""Docker Status und Diagnose Seite"""
try:
@@ -1881,6 +2284,7 @@ def docker_status():
return render_template('docker_status.html', config=fallback_config)
@app.route('/api/docker_diagnose')
@api_permission_required(Permission.SYSTEM_DOCKER)
def api_docker_diagnose():
"""Docker Diagnose API"""
try:
@@ -2018,6 +2422,7 @@ def api_docker_diagnose():
}), 500
@app.route('/api/check_port/<int:port>')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_check_port(port):
"""Prüfe ob ein Port verfügbar ist"""
try:
@@ -2036,6 +2441,7 @@ def api_check_port(port):
})
@app.route('/api/find_available_port')
@api_permission_required(Permission.SYSTEM_MONITORING)
def api_find_available_port():
"""Finde einen verfügbaren Port"""
try:
@@ -2060,6 +2466,7 @@ def api_find_available_port():
})
@app.route('/api/start_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_project(project_name):
"""API Endpoint zum Starten eines Projekts"""
try:
@@ -2112,6 +2519,7 @@ def api_start_project(project_name):
})
@app.route('/api/start_native/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_START)
def api_start_native(project_name):
"""Starte Projekt nativ (ohne Docker)"""
try:
@@ -2177,6 +2585,7 @@ def api_start_native(project_name):
return jsonify({'success': False, 'error': f'API-Fehler: {str(e)}'})
@app.route('/api/open_terminal/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_CONSOLE)
def api_open_terminal(project_name):
"""Öffne Terminal im Projektordner"""
try:
@@ -2197,12 +2606,14 @@ def api_open_terminal(project_name):
# Test Icons Template Route
@app.route('/templates/test_icons.html')
@permission_required(Permission.SYSTEM_CONFIG)
def serve_test_icons():
"""Serve Icon Test Template"""
with open('test_icons.html', 'r', encoding='utf-8') as f:
return f.read(), 200, {'Content-Type': 'text/html; charset=utf-8'}
@app.route('/update_project', methods=['POST'])
@permission_required(Permission.PROJECT_UPDATE)
def update_project():
"""Update ein Projekt"""
project_name = request.form.get('project_name')
@@ -2216,6 +2627,7 @@ def update_project():
return jsonify({'success': success, 'message': message})
@app.route('/update_project_get/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def update_project_get(project_name):
"""Update Projekt via GET (Weiterleitung)"""
success, message = project_manager.update_project(project_name)
@@ -2223,6 +2635,7 @@ def update_project_get(project_name):
return redirect(url_for('index'))
@app.route('/api/update_project/<project_name>', methods=['POST'])
@api_permission_required(Permission.PROJECT_UPDATE)
def api_update_project(project_name):
"""API Endpoint zum Updaten eines Projekts"""
try:
@@ -2238,6 +2651,7 @@ def api_update_project(project_name):
}), 500
@app.route('/api/installed_projects')
@api_permission_required(Permission.PROJECT_VIEW)
def api_installed_projects():
"""API Endpoint für installierte Projekte mit Versionsinfo"""
try:
@@ -2270,6 +2684,7 @@ def api_installed_projects():
})
@app.route('/api/projects_list.json')
@api_permission_required(Permission.PROJECT_VIEW)
def api_projects_list():
"""API Endpoint für die lokale Projektliste (für Tests)"""
try:
@@ -2280,6 +2695,7 @@ def api_projects_list():
return jsonify([]), 500
@app.route('/api/refresh_projects', methods=['POST'])
@api_permission_required(Permission.PROJECT_VIEW)
def api_refresh_projects():
"""API Endpoint zum Aktualisieren der Projektliste via AJAX"""
try:
@@ -2319,6 +2735,7 @@ def api_refresh_projects():
})
@app.route('/custom_install/<project_name>')
@permission_required(Permission.PROJECT_VIEW)
def custom_install(project_name):
"""Custom Installation Seite für ein Projekt"""
config = project_manager.load_config()
@@ -2376,6 +2793,142 @@ def check_port_available(port):
except:
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__':
# 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
PyYAML==6.0.1
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);
border-radius: 10px;
margin-bottom: 20px;
position: relative;
z-index: 10001 !important; /* Höchste Priorität für Navbar */
}
.card {
@@ -118,98 +120,107 @@
}
}
/* Dropdown-Menü Styles */
/* Dropdown-Menü Styles - Maximale Z-Index Priorität */
.dropdown-menu {
border-radius: 10px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15);
border: none;
z-index: 999999 !important;
position: absolute !important;
}
.dropdown-item {
padding: 10px 15px;
border-radius: 6px;
margin: 2px 5px;
transition: background-color 0.2s ease;
.dropdown {
z-index: 999998 !important;
position: relative !important;
}
/* 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 {
background-color: #f8f9fa;
background-color: rgba(0, 123, 255, 0.1);
transition: background-color 0.15s ease-in-out;
}
.dropdown-item i {
width: 20px;
text-align: center;
/* Mobile-spezifische Dropdown-Styles */
@media (max-width: 768px) {
.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 {
transition: all 0.3s ease;
border-radius: 12px;
@@ -493,16 +504,85 @@
<i class="fas fa-cube"></i> Docker Status
</a>
</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">
<a class="nav-link" href="{{ url_for('config') }}">
<i class="fas fa-cog"></i> Konfiguration
</a>
</li>
{% endif %}
</ul>
<!-- User Menu -->
<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
</a>
{% endif %}
</div>
</div>
</div>
@@ -723,7 +803,10 @@
document.addEventListener('DOMContentLoaded', function() {
checkAndFixIcons();
checkSystemHealth();
// Dropdowns funktionieren automatisch mit Bootstrap - keine zusätzliche Initialisierung nötig
});
// Bootstrap Dropdowns funktionieren automatisch ohne zusätzliches JavaScript
</script>
{% 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"
}