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:
557
app.py
557
app.py
@@ -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
335
file_manager.py
Normal 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'
|
||||
@@ -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
19
sessions_db.json
Normal 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"
|
||||
}
|
||||
@@ -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
2145
templates/file_manager.html
Normal file
File diff suppressed because it is too large
Load Diff
565
templates/file_manager_new.html
Normal file
565
templates/file_manager_new.html
Normal 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
156
templates/login.html
Normal 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
262
templates/profile.html
Normal 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 %}
|
||||
401
templates/users_management.html
Normal file
401
templates/users_management.html
Normal 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
507
user_management.py
Normal 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
25
users_db.json
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user