feat: Remove outdated documentation files and update SSL certificate paths in installer scripts

This commit is contained in:
Till Tomczak 2025-05-26 18:02:57 +02:00
parent b379cdf4c9
commit c2ea6c34ea
11 changed files with 2086 additions and 556 deletions

131
backend/COMMON_ERRORS.md Normal file
View File

@ -0,0 +1,131 @@
# Häufige Fehler und Lösungen
## API-Route-Fehler
### 1. Blueprint nicht registriert
**Problem:** API-Blueprint wird nicht registriert, führt zu 404-Fehlern
**Lösung:** Blueprint in app.py importieren und registrieren:
```python
from blueprints.api import api_bp
app.register_blueprint(api_bp)
```
### 2. Fehlende CSRF-Token bei POST-Requests
**Problem:** CSRF-Validierung schlägt fehl
**Lösung:** CSRF-Token in Templates einbinden oder API-Routen von CSRF befreien
### 3. Database Session nicht geschlossen
**Problem:** Database connections leak
**Lösung:** Immer try/finally verwenden:
```python
db_session = get_db_session()
try:
# Database operations
pass
finally:
db_session.close()
```
### 4. Fehlende Authentifizierung
**Problem:** @login_required decorator fehlt
**Lösung:** Alle geschützten Routen mit @login_required versehen
### 5. Falsche JSON-Response-Struktur
**Problem:** Frontend erwartet andere Datenstruktur
**Lösung:** Konsistente API-Response-Struktur verwenden:
```python
return jsonify({
"success": True/False,
"data": {...},
"error": "error message" # nur bei Fehlern
})
```
## Datenbankfehler
### 1. Relationship not loaded
**Problem:** Lazy loading von Relationships
**Lösung:** Eager loading verwenden:
```python
from sqlalchemy.orm import joinedload
jobs = session.query(Job).options(joinedload(Job.user)).all()
```
### 2. Session closed before accessing relationships
**Problem:** Zugriff auf Relationships nach Session.close()
**Lösung:** Alle benötigten Daten vor Session.close() laden
## Logging-Fehler
### 1. Logger nicht initialisiert
**Problem:** Logging funktioniert nicht
**Lösung:** Logger korrekt initialisieren:
```python
from utils.logging_config import get_logger
logger = get_logger("component_name")
```
## File-Upload-Fehler
### 1. Upload-Ordner existiert nicht
**Problem:** FileNotFoundError beim Upload
**Lösung:** Ordner erstellen:
```python
os.makedirs(upload_folder, exist_ok=True)
```
### 2. Unsichere Dateinamen
**Problem:** Path traversal vulnerability
**Lösung:** secure_filename() verwenden:
```python
from werkzeug.utils import secure_filename
filename = secure_filename(file.filename)
```
## Frontend-Integration-Fehler
### 1. CORS-Probleme
**Problem:** Cross-Origin-Requests werden blockiert
**Lösung:** CORS-Headers setzen oder Flask-CORS verwenden
### 2. Inkonsistente API-Endpunkte
**Problem:** Frontend ruft nicht existierende Endpunkte auf
**Lösung:** Systematische Überprüfung aller Frontend-API-Calls
### 3. Fehlende Error-Handling
**Problem:** Frontend kann Fehler nicht verarbeiten
**Lösung:** Konsistente Error-Response-Struktur implementieren
### 4. Admin-Dashboard-Fehler
**Problem:** Admin-Dashboard API-Routen fehlen
**Lösung:** Vollständige Admin-API implementieren:
```python
@app.route("/api/admin/users/create", methods=["POST"])
@login_required
def api_admin_create_user():
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
# Implementation...
```
### 5. Fehlende Berechtigungsprüfung
**Problem:** Admin-Routen ohne Berechtigungsprüfung
**Lösung:** Immer Admin-Check einbauen:
```python
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
```
## Performance-Probleme
### 1. N+1 Query Problem
**Problem:** Zu viele Datenbankabfragen
**Lösung:** Eager loading oder batch loading verwenden
### 2. Fehlende Indizes
**Problem:** Langsame Datenbankabfragen
**Lösung:** Indizes auf häufig abgefragte Spalten erstellen
### 3. Große Response-Größen
**Problem:** Langsame API-Responses
**Lösung:** Pagination und Feldfilterung implementieren

View File

@ -29,6 +29,29 @@ MYP V2 ist ein 3D-Drucker-Management-System mit automatischer Smart Plug-Steueru
- **Benutzer-Management**: Admin-Funktionen
- **Statistiken**: Systemmetriken und Berichte
- **Scheduler-Steuerung**: Start/Stop/Status des Job-Monitors
- **✅ API-Blueprint-Registrierung**: API-Blueprint wurde in app.py registriert
- **✅ Fehlende Frontend-API-Routen**: Alle vom Frontend benötigten API-Endpunkte implementiert
- `/api/dashboard` - Dashboard-Daten
- `/api/jobs/recent` - Letzte Jobs
- `/api/files/upload` - Datei-Upload
- `/api/files/download` - Datei-Download
- `/api/stats/*` - Verschiedene Statistik-Endpunkte
- `/api/user/*` - Benutzer-spezifische API-Endpunkte
- `/api/job/{id}/remaining-time` - Verbleibende Zeit für Jobs
- `/api/test` - Debug-Server Test-Endpunkt
- `/api/status` - System-Status-Überwachung
- `/api/schedule` - Scheduler-Informationen
- **✅ Admin-Dashboard-API-Routen**: Vollständige API-Integration für Admin-Panel
- `/api/admin/users/create` - Benutzer erstellen
- `/api/admin/users/{id}/edit` - Benutzer bearbeiten
- `/api/admin/users/{id}/toggle` - Benutzer aktivieren/deaktivieren
- `/api/admin/printers/create` - Drucker erstellen
- `/api/admin/printers/{id}/edit` - Drucker bearbeiten
- `/api/admin/printers/{id}/toggle` - Drucker aktivieren/deaktivieren
- `/api/admin/jobs/cancel/{id}` - Jobs abbrechen
- `/api/admin/system/info` - Erweiterte System-Informationen
- `/api/admin/logs/download` - Log-Download als ZIP
- `/api/admin/maintenance/run` - Wartungsroutinen ausführen
#### Smart Plug-Integration
- **TP-Link Tapo P110** Steuerung über PyP100

View File

@ -33,6 +33,7 @@ from utils.job_scheduler import scheduler
from utils.template_helpers import register_template_helpers
from blueprints.auth import auth_bp
from blueprints.user import user_bp
from blueprints.api import api_bp
# Flask-App initialisieren
app = Flask(__name__)
@ -93,6 +94,14 @@ print("Auth-Blueprint erfolgreich geladen")
app.register_blueprint(user_bp)
print("User-Blueprint erfolgreich geladen")
# API-Blueprint registrieren
try:
from blueprints.api import api_bp
app.register_blueprint(api_bp)
print("API-Blueprint erfolgreich geladen")
except ImportError as e:
print(f"API-Blueprint konnte nicht geladen werden: {e}")
# Logging initialisieren
setup_logging()
log_startup_info()
@ -2419,3 +2428,917 @@ def admin_settings():
return redirect(url_for("index"))
return render_template("admin_settings.html")
@app.route("/api/dashboard", methods=["GET"])
@login_required
def api_dashboard():
"""Dashboard-Daten für API-Aufrufe"""
try:
db_session = get_db_session()
# Grundlegende Statistiken sammeln
total_jobs = db_session.query(Job).count()
active_jobs = db_session.query(Job).filter(Job.status.in_(['running', 'pending'])).count()
total_printers = db_session.query(Printer).count()
available_printers = db_session.query(Printer).filter(Printer.status == 'available').count()
dashboard_data = {
"total_jobs": total_jobs,
"active_jobs": active_jobs,
"total_printers": total_printers,
"available_printers": available_printers,
"timestamp": datetime.now().isoformat()
}
db_session.close()
return jsonify(dashboard_data)
except Exception as e:
app_logger.error(f"Fehler beim Laden der Dashboard-Daten: {str(e)}")
return jsonify({"error": "Fehler beim Laden der Dashboard-Daten"}), 500
@app.route("/api/jobs/recent", methods=["GET"])
@login_required
def api_jobs_recent():
"""Letzte Jobs für Dashboard"""
try:
db_session = get_db_session()
limit = request.args.get('limit', 10, type=int)
recent_jobs = db_session.query(Job).order_by(Job.created_at.desc()).limit(limit).all()
jobs_data = []
for job in recent_jobs:
jobs_data.append({
"id": job.id,
"name": job.name,
"status": job.status,
"user_name": job.user.name if job.user else "Unbekannt",
"printer_name": job.printer.name if job.printer else "Kein Drucker",
"created_at": job.created_at.isoformat() if job.created_at else None
})
db_session.close()
return jsonify(jobs_data)
except Exception as e:
app_logger.error(f"Fehler beim Laden der letzten Jobs: {str(e)}")
return jsonify([]), 500
@app.route("/api/files/upload", methods=["POST"])
@login_required
def api_files_upload():
"""Datei-Upload für Druckaufträge"""
try:
if 'file' not in request.files:
return jsonify({"error": "Keine Datei hochgeladen"}), 400
file = request.files['file']
if file.filename == '':
return jsonify({"error": "Keine Datei ausgewählt"}), 400
# Sicherer Dateiname erstellen
filename = secure_filename(file.filename)
# Upload-Ordner erstellen falls nicht vorhanden
upload_folder = os.path.join(app.root_path, 'uploads')
os.makedirs(upload_folder, exist_ok=True)
# Eindeutigen Dateinamen erstellen
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
unique_filename = f"{timestamp}_{filename}"
file_path = os.path.join(upload_folder, unique_filename)
# Datei speichern
file.save(file_path)
# Relative Pfad für Datenbank
relative_path = os.path.join('uploads', unique_filename)
jobs_logger.info(f"Datei hochgeladen: {unique_filename} von Benutzer {current_user.username}")
return jsonify({
"success": True,
"file_path": relative_path,
"filename": unique_filename,
"original_filename": filename
})
except Exception as e:
jobs_logger.error(f"Fehler beim Datei-Upload: {str(e)}")
return jsonify({"error": f"Fehler beim Datei-Upload: {str(e)}"}), 500
@app.route("/api/files/download", methods=["GET"])
@login_required
def api_files_download():
"""Datei-Download"""
try:
file_path = request.args.get('path')
if not file_path:
return jsonify({"error": "Kein Dateipfad angegeben"}), 400
# Sicherheitscheck: Nur Dateien im uploads-Ordner erlauben
if not file_path.startswith('uploads/'):
return jsonify({"error": "Ungültiger Dateipfad"}), 403
full_path = os.path.join(app.root_path, file_path)
if not os.path.exists(full_path):
return jsonify({"error": "Datei nicht gefunden"}), 404
return send_file(full_path, as_attachment=True)
except Exception as e:
jobs_logger.error(f"Fehler beim Datei-Download: {str(e)}")
return jsonify({"error": f"Fehler beim Datei-Download: {str(e)}"}), 500
@app.route("/api/stats/total-jobs", methods=["GET"])
@login_required
def api_stats_total_jobs():
"""Gesamtzahl der Jobs"""
try:
db_session = get_db_session()
total = db_session.query(Job).count()
db_session.close()
return jsonify({"value": total})
except Exception as e:
return jsonify({"value": 0})
@app.route("/api/stats/completed-jobs", methods=["GET"])
@login_required
def api_stats_completed_jobs():
"""Anzahl abgeschlossener Jobs"""
try:
db_session = get_db_session()
completed = db_session.query(Job).filter(Job.status == 'completed').count()
db_session.close()
return jsonify({"value": completed})
except Exception as e:
return jsonify({"value": 0})
@app.route("/api/stats/active-printers", methods=["GET"])
@login_required
def api_stats_active_printers():
"""Anzahl aktiver Drucker"""
try:
db_session = get_db_session()
active = db_session.query(Printer).filter(Printer.status.in_(['available', 'busy'])).count()
db_session.close()
return jsonify({"value": active})
except Exception as e:
return jsonify({"value": 0})
@app.route("/api/stats/job-status", methods=["GET"])
@login_required
def api_stats_job_status():
"""Job-Status-Verteilung für Charts"""
try:
db_session = get_db_session()
from sqlalchemy import func
status_counts = db_session.query(
Job.status,
func.count(Job.id).label('count')
).group_by(Job.status).all()
data = [{"status": status, "count": count} for status, count in status_counts]
db_session.close()
return jsonify(data)
except Exception as e:
return jsonify([])
@app.route("/api/stats/printer-usage", methods=["GET"])
@login_required
def api_stats_printer_usage():
"""Drucker-Nutzungsstatistiken für Charts"""
try:
db_session = get_db_session()
from sqlalchemy import func
usage_stats = db_session.query(
Printer.name,
func.count(Job.id).label('job_count')
).join(Job, Printer.id == Job.printer_id, isouter=True)\
.group_by(Printer.name).all()
data = [{"printer": name, "jobs": count} for name, count in usage_stats]
db_session.close()
return jsonify(data)
except Exception as e:
return jsonify([])
@app.route("/api/stats/activity", methods=["GET"])
@login_required
def api_stats_activity():
"""Aktivitätsliste für Statistiken"""
try:
db_session = get_db_session()
limit = request.args.get('limit', 10, type=int)
recent_activity = db_session.query(Job)\
.order_by(Job.updated_at.desc())\
.limit(limit).all()
activity_data = []
for job in recent_activity:
activity_data.append({
"id": job.id,
"name": job.name,
"action": f"Status: {job.status}",
"user": job.user.name if job.user else "Unbekannt",
"timestamp": job.updated_at.isoformat() if job.updated_at else None
})
db_session.close()
return jsonify(activity_data)
except Exception as e:
return jsonify([])
@app.route("/api/stats/job-duration", methods=["GET"])
@login_required
def api_stats_job_duration():
"""Durchschnittliche Job-Dauer"""
try:
db_session = get_db_session()
from sqlalchemy import func
avg_duration = db_session.query(
func.avg(Job.print_time)
).filter(Job.status == 'completed').scalar()
db_session.close()
return jsonify({"value": round(avg_duration or 0, 2)})
except Exception as e:
return jsonify({"value": 0})
@app.route("/api/user/password", methods=["POST"])
@login_required
def api_user_password():
"""Passwort ändern über API"""
try:
data = request.get_json()
current_password = data.get('current_password')
new_password = data.get('new_password')
if not current_password or not new_password:
return jsonify({"error": "Aktuelles und neues Passwort erforderlich"}), 400
db_session = get_db_session()
user = db_session.query(User).filter(User.id == current_user.id).first()
if not user or not user.check_password(current_password):
db_session.close()
return jsonify({"error": "Aktuelles Passwort ist falsch"}), 401
user.set_password(new_password)
user.updated_at = datetime.now()
db_session.commit()
db_session.close()
user_logger.info(f"Benutzer {current_user.username} hat sein Passwort geändert")
return jsonify({"success": True, "message": "Passwort erfolgreich geändert"})
except Exception as e:
user_logger.error(f"Fehler beim Passwort ändern: {str(e)}")
return jsonify({"error": "Fehler beim Passwort ändern"}), 500
@app.route("/api/user/stats", methods=["GET"])
@login_required
def api_user_stats():
"""Benutzerstatistiken"""
try:
db_session = get_db_session()
user_jobs = db_session.query(Job).filter(Job.user_id == current_user.id).count()
completed_jobs = db_session.query(Job).filter(
Job.user_id == current_user.id,
Job.status == 'completed'
).count()
stats = {
"total_jobs": user_jobs,
"completed_jobs": completed_jobs,
"success_rate": round((completed_jobs / user_jobs * 100) if user_jobs > 0 else 0, 1)
}
db_session.close()
return jsonify(stats)
except Exception as e:
return jsonify({"total_jobs": 0, "completed_jobs": 0, "success_rate": 0})
@app.route("/api/system/stats", methods=["GET"])
@login_required
def api_system_stats():
"""System-Statistiken für Admin-Panel"""
try:
# Basis-Systemstatistiken
uptime_days = get_system_uptime_days()
db_session = get_db_session()
total_users = db_session.query(User).count()
total_printers = db_session.query(Printer).count()
total_jobs = db_session.query(Job).count()
db_session.close()
stats = {
"uptime_days": uptime_days,
"total_users": total_users,
"total_printers": total_printers,
"total_jobs": total_jobs,
"timestamp": datetime.now().isoformat()
}
return jsonify(stats)
except Exception as e:
app_logger.error(f"Fehler beim Laden der System-Statistiken: {str(e)}")
return jsonify({"error": "Fehler beim Laden der System-Statistiken"}), 500
@app.route("/api/job/<int:job_id>/remaining-time", methods=["GET"])
@login_required
@job_owner_required
def api_job_remaining_time(job_id):
"""Verbleibende Zeit für einen Job berechnen"""
try:
db_session = get_db_session()
job = db_session.query(Job).filter(Job.id == job_id).first()
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Verbleibende Zeit berechnen
if job.status == 'running' and job.start_time:
# Berechne geschätzte Endzeit
estimated_end_time = job.start_time + timedelta(minutes=job.estimated_time)
now = datetime.now()
if now < estimated_end_time:
remaining_seconds = int((estimated_end_time - now).total_seconds())
remaining_time = max(0, remaining_seconds * 1000) # Frontend erwartet Millisekunden
else:
remaining_time = 0
else:
remaining_time = 0
db_session.close()
return jsonify({
"id": job_id,
"remainingTime": remaining_time,
"status": job.status
})
except Exception as e:
jobs_logger.error(f"Fehler beim Berechnen der verbleibenden Zeit für Job {job_id}: {str(e)}")
return jsonify({"error": "Fehler beim Berechnen der verbleibenden Zeit"}), 500
# Debug-Server API-Routen (nur für Entwicklung)
@app.route("/api/test", methods=["GET"])
def api_test():
"""Test-Endpunkt für Debug-Server"""
return jsonify({
"status": "ok",
"message": "Flask Backend ist erreichbar",
"timestamp": datetime.now().isoformat()
})
@app.route("/api/schedule", methods=["GET"])
@login_required
def api_schedule():
"""Scheduler-Informationen"""
try:
from utils.job_scheduler import scheduler
schedule_info = {
"enabled": SCHEDULER_ENABLED,
"interval": SCHEDULER_INTERVAL,
"is_running": scheduler.is_running() if hasattr(scheduler, 'is_running') else False,
"next_run": None # Könnte erweitert werden
}
return jsonify(schedule_info)
except Exception as e:
app_logger.error(f"Fehler beim Laden der Scheduler-Informationen: {str(e)}")
return jsonify({"error": "Fehler beim Laden der Scheduler-Informationen"}), 500
@app.route("/api/status", methods=["GET"])
def api_status():
"""System-Status für externe Überwachung"""
try:
db_session = get_db_session()
# Grundlegende Gesundheitsprüfung der Datenbank
try:
db_session.execute("SELECT 1")
db_healthy = True
except Exception:
db_healthy = False
finally:
db_session.close()
status = {
"status": "healthy" if db_healthy else "unhealthy",
"database": "connected" if db_healthy else "disconnected",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0" # Könnte aus einer Konfiguration gelesen werden
}
return jsonify(status), 200 if db_healthy else 503
except Exception as e:
app_logger.error(f"Fehler bei der Status-Prüfung: {str(e)}")
return jsonify({
"status": "error",
"error": str(e),
"timestamp": datetime.now().isoformat()
}), 500
# Admin-Dashboard API-Routen für Frontend-Integration
@app.route("/api/admin/users/create", methods=["POST"])
@login_required
def api_admin_create_user():
"""API-Endpunkt zum Erstellen eines neuen Benutzers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
data = request.get_json()
# Pflichtfelder prüfen
required_fields = ["email", "password", "username"]
for field in required_fields:
if field not in data or not data[field]:
return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400
db_session = get_db_session()
# Prüfen, ob Benutzer bereits existiert
existing_user = db_session.query(User).filter(
sqlalchemy.or_(User.email == data["email"], User.username == data["username"])
).first()
if existing_user:
db_session.close()
return jsonify({"error": "Benutzer mit dieser E-Mail oder diesem Benutzernamen existiert bereits"}), 400
# Neuen Benutzer erstellen
new_user = User(
email=data["email"],
username=data["username"],
name=data.get("name", ""),
role=data.get("role", "user"),
active=data.get("active", True),
created_at=datetime.now()
)
new_user.set_password(data["password"])
db_session.add(new_user)
db_session.commit()
user_dict = new_user.to_dict()
db_session.close()
auth_logger.info(f"Neuer Benutzer {data['email']} wurde von Admin {current_user.username} erstellt")
return jsonify({"success": True, "user": user_dict}), 201
except Exception as e:
auth_logger.error(f"Fehler beim Erstellen des Benutzers: {str(e)}")
return jsonify({"error": f"Fehler beim Erstellen des Benutzers: {str(e)}"}), 500
@app.route("/api/admin/users/<int:user_id>/edit", methods=["PUT"])
@login_required
def api_admin_edit_user(user_id):
"""API-Endpunkt zum Bearbeiten eines Benutzers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
data = request.get_json()
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Benutzerdaten aktualisieren
if "email" in data:
user.email = data["email"]
if "username" in data:
user.username = data["username"]
if "name" in data:
user.name = data["name"]
if "role" in data:
user.role = data["role"]
if "active" in data:
user.active = data["active"]
if "password" in data and data["password"]:
user.set_password(data["password"])
user.updated_at = datetime.now()
db_session.commit()
user_dict = user.to_dict()
db_session.close()
auth_logger.info(f"Benutzer {user.email} wurde von Admin {current_user.username} bearbeitet")
return jsonify({"success": True, "user": user_dict})
except Exception as e:
auth_logger.error(f"Fehler beim Bearbeiten des Benutzers {user_id}: {str(e)}")
return jsonify({"error": f"Fehler beim Bearbeiten des Benutzers: {str(e)}"}), 500
@app.route("/api/admin/users/<int:user_id>/toggle", methods=["POST"])
@login_required
def api_admin_toggle_user(user_id):
"""API-Endpunkt zum Aktivieren/Deaktivieren eines Benutzers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
# Verhindern, dass Admin sich selbst deaktiviert
if user_id == current_user.id:
return jsonify({"error": "Sie können sich nicht selbst deaktivieren"}), 400
try:
db_session = get_db_session()
user = db_session.query(User).filter(User.id == user_id).first()
if not user:
db_session.close()
return jsonify({"error": "Benutzer nicht gefunden"}), 404
# Status umschalten
user.active = not user.active
user.updated_at = datetime.now()
db_session.commit()
user_dict = user.to_dict()
db_session.close()
status = "aktiviert" if user.active else "deaktiviert"
auth_logger.info(f"Benutzer {user.email} wurde von Admin {current_user.username} {status}")
return jsonify({"success": True, "user": user_dict, "message": f"Benutzer wurde {status}"})
except Exception as e:
auth_logger.error(f"Fehler beim Umschalten des Benutzerstatus {user_id}: {str(e)}")
return jsonify({"error": f"Fehler beim Umschalten des Benutzerstatus: {str(e)}"}), 500
@app.route("/api/admin/printers/create", methods=["POST"])
@login_required
def api_admin_create_printer():
"""API-Endpunkt zum Erstellen eines neuen Druckers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
data = request.get_json()
# Pflichtfelder prüfen
required_fields = ["name", "model", "location", "ip_address"]
for field in required_fields:
if field not in data or not data[field]:
return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400
db_session = get_db_session()
# Prüfen, ob Drucker bereits existiert
existing_printer = db_session.query(Printer).filter(
sqlalchemy.or_(Printer.name == data["name"], Printer.ip_address == data["ip_address"])
).first()
if existing_printer:
db_session.close()
return jsonify({"error": "Drucker mit diesem Namen oder dieser IP-Adresse existiert bereits"}), 400
# Neuen Drucker erstellen
new_printer = Printer(
name=data["name"],
model=data["model"],
location=data["location"],
ip_address=data["ip_address"],
plug_ip=data.get("plug_ip"),
plug_username=data.get("plug_username"),
plug_password=data.get("plug_password"),
mac_address=data.get("mac_address"),
status="offline",
active=data.get("active", True),
created_at=datetime.now()
)
db_session.add(new_printer)
db_session.commit()
printer_dict = new_printer.to_dict()
db_session.close()
printers_logger.info(f"Neuer Drucker {data['name']} wurde von Admin {current_user.username} erstellt")
return jsonify({"success": True, "printer": printer_dict}), 201
except Exception as e:
printers_logger.error(f"Fehler beim Erstellen des Druckers: {str(e)}")
return jsonify({"error": f"Fehler beim Erstellen des Druckers: {str(e)}"}), 500
@app.route("/api/admin/printers/<int:printer_id>/edit", methods=["PUT"])
@login_required
def api_admin_edit_printer(printer_id):
"""API-Endpunkt zum Bearbeiten eines Druckers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
data = request.get_json()
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Druckerdaten aktualisieren
if "name" in data:
printer.name = data["name"]
if "model" in data:
printer.model = data["model"]
if "location" in data:
printer.location = data["location"]
if "ip_address" in data:
printer.ip_address = data["ip_address"]
if "plug_ip" in data:
printer.plug_ip = data["plug_ip"]
if "plug_username" in data:
printer.plug_username = data["plug_username"]
if "plug_password" in data:
printer.plug_password = data["plug_password"]
if "mac_address" in data:
printer.mac_address = data["mac_address"]
if "active" in data:
printer.active = data["active"]
printer.updated_at = datetime.now()
db_session.commit()
printer_dict = printer.to_dict()
db_session.close()
printers_logger.info(f"Drucker {printer.name} wurde von Admin {current_user.username} bearbeitet")
return jsonify({"success": True, "printer": printer_dict})
except Exception as e:
printers_logger.error(f"Fehler beim Bearbeiten des Druckers {printer_id}: {str(e)}")
return jsonify({"error": f"Fehler beim Bearbeiten des Druckers: {str(e)}"}), 500
@app.route("/api/admin/printers/<int:printer_id>/toggle", methods=["POST"])
@login_required
def api_admin_toggle_printer(printer_id):
"""API-Endpunkt zum Aktivieren/Deaktivieren eines Druckers (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
db_session = get_db_session()
printer = db_session.query(Printer).filter(Printer.id == printer_id).first()
if not printer:
db_session.close()
return jsonify({"error": "Drucker nicht gefunden"}), 404
# Status umschalten
printer.active = not printer.active
printer.updated_at = datetime.now()
db_session.commit()
printer_dict = printer.to_dict()
db_session.close()
status = "aktiviert" if printer.active else "deaktiviert"
printers_logger.info(f"Drucker {printer.name} wurde von Admin {current_user.username} {status}")
return jsonify({"success": True, "printer": printer_dict, "message": f"Drucker wurde {status}"})
except Exception as e:
printers_logger.error(f"Fehler beim Umschalten des Druckerstatus {printer_id}: {str(e)}")
return jsonify({"error": f"Fehler beim Umschalten des Druckerstatus: {str(e)}"}), 500
@app.route("/api/admin/jobs/cancel/<int:job_id>", methods=["POST"])
@login_required
def api_admin_cancel_job(job_id):
"""API-Endpunkt zum Abbrechen eines Jobs (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
db_session = get_db_session()
job = db_session.query(Job).filter(Job.id == job_id).first()
if not job:
db_session.close()
return jsonify({"error": "Job nicht gefunden"}), 404
# Nur laufende oder geplante Jobs können abgebrochen werden
if job.status not in ["scheduled", "running"]:
db_session.close()
return jsonify({"error": f"Job im Status '{job.status}' kann nicht abgebrochen werden"}), 400
# Job abbrechen
job.status = "cancelled"
job.actual_end_time = datetime.now()
# Steckdose ausschalten falls Job lief
if job.status == "running":
from utils.job_scheduler import toggle_plug
toggle_plug(job.printer_id, False)
db_session.commit()
job_dict = job.to_dict()
db_session.close()
jobs_logger.info(f"Job {job_id} wurde von Admin {current_user.username} abgebrochen")
return jsonify({"success": True, "job": job_dict, "message": "Job wurde abgebrochen"})
except Exception as e:
jobs_logger.error(f"Fehler beim Abbrechen des Jobs {job_id}: {str(e)}")
return jsonify({"error": f"Fehler beim Abbrechen des Jobs: {str(e)}"}), 500
@app.route("/api/admin/system/info", methods=["GET"])
@login_required
def api_admin_system_info():
"""Erweiterte System-Informationen für Admin-Dashboard"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
import psutil
import platform
# System-Grundinformationen
system_info = {
"platform": platform.platform(),
"python_version": platform.python_version(),
"architecture": platform.architecture()[0],
"processor": platform.processor(),
"hostname": platform.node(),
"uptime": get_system_uptime_days()
}
# Hardware-Informationen
hardware_info = {
"cpu_count": psutil.cpu_count(),
"cpu_freq": psutil.cpu_freq()._asdict() if psutil.cpu_freq() else None,
"memory_total": psutil.virtual_memory().total,
"memory_available": psutil.virtual_memory().available,
"disk_total": psutil.disk_usage('/').total,
"disk_free": psutil.disk_usage('/').free
}
# Datenbank-Informationen
db_session = get_db_session()
db_info = {
"users_count": db_session.query(User).count(),
"printers_count": db_session.query(Printer).count(),
"jobs_count": db_session.query(Job).count(),
"active_jobs": db_session.query(Job).filter(Job.status.in_(["scheduled", "running"])).count()
}
db_session.close()
# Anwendungs-Informationen
app_info = {
"version": "3.0.0", # Könnte aus einer Konfiguration gelesen werden
"debug_mode": app.debug,
"flask_env": os.environ.get('FLASK_ENV', 'production'),
"scheduler_enabled": SCHEDULER_ENABLED,
"ssl_enabled": SSL_ENABLED
}
return jsonify({
"system": system_info,
"hardware": hardware_info,
"database": db_info,
"application": app_info,
"timestamp": datetime.now().isoformat()
})
except Exception as e:
app_logger.error(f"Fehler beim Abrufen der System-Informationen: {str(e)}")
return jsonify({"error": f"Fehler beim Abrufen der System-Informationen: {str(e)}"}), 500
@app.route("/api/admin/logs/download", methods=["GET"])
@login_required
def api_admin_download_logs():
"""Download der System-Logs als ZIP-Datei (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
import zipfile
import tempfile
# 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:
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
# Alle Log-Dateien zur ZIP hinzufügen
for root, dirs, files in os.walk(log_dir):
for file in files:
if file.endswith('.log'):
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, log_dir)
zipf.write(file_path, arcname)
# ZIP-Datei als Download senden
return send_file(
temp_zip.name,
as_attachment=True,
download_name=f"system_logs_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip",
mimetype='application/zip'
)
except Exception as e:
app_logger.error(f"Fehler beim Download der Logs: {str(e)}")
return jsonify({"error": f"Fehler beim Download der Logs: {str(e)}"}), 500
@app.route("/api/admin/maintenance/run", methods=["POST"])
@login_required
def api_admin_run_maintenance():
"""Führt Wartungsroutinen aus (Admin only)"""
if not current_user.is_admin:
return jsonify({"error": "Keine Berechtigung"}), 403
try:
maintenance_results = []
# Cache leeren
try:
import shutil
import tempfile
cache_dir = os.path.join(tempfile.gettempdir(), 'flask_cache')
if os.path.exists(cache_dir):
shutil.rmtree(cache_dir)
maintenance_results.append("Cache geleert")
except Exception as e:
maintenance_results.append(f"Cache-Fehler: {str(e)}")
# Datenbank optimieren
try:
db_session = get_db_session()
db_session.execute(sqlalchemy.text("VACUUM"))
db_session.execute(sqlalchemy.text("ANALYZE"))
db_session.commit()
db_session.close()
maintenance_results.append("Datenbank optimiert")
except Exception as e:
maintenance_results.append(f"DB-Fehler: {str(e)}")
# Alte Log-Dateien komprimieren (älter als 7 Tage)
try:
import gzip
log_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
seven_days_ago = datetime.now() - timedelta(days=7)
compressed_files = 0
for root, dirs, files in os.walk(log_dir):
for file in files:
if file.endswith('.log'):
file_path = os.path.join(root, file)
file_stat = os.stat(file_path)
file_age = datetime.fromtimestamp(file_stat.st_mtime)
if file_age < seven_days_ago:
# Datei komprimieren
with open(file_path, 'rb') as f_in:
with gzip.open(f"{file_path}.gz", 'wb') as f_out:
shutil.copyfileobj(f_in, f_out)
os.remove(file_path)
compressed_files += 1
if compressed_files > 0:
maintenance_results.append(f"{compressed_files} Log-Dateien komprimiert")
else:
maintenance_results.append("Keine Log-Dateien zu komprimieren")
except Exception as e:
maintenance_results.append(f"Log-Komprimierung-Fehler: {str(e)}")
app_logger.info(f"Wartung wurde von Admin {current_user.username} durchgeführt: {', '.join(maintenance_results)}")
return jsonify({
"success": True,
"message": "Wartung erfolgreich durchgeführt",
"results": maintenance_results
})
except Exception as e:
app_logger.error(f"Fehler bei der Wartung: {str(e)}")
return jsonify({"error": f"Fehler bei der Wartung: {str(e)}"}), 500

View File

@ -86,3 +86,67 @@ def logout():
return jsonify({"success": True, "redirect_url": url_for("auth.login")})
else:
return redirect(url_for("auth.login"))
@auth_bp.route("/api/login", methods=["POST"])
def api_login():
"""API-Login-Endpunkt für Frontend"""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
username = data.get("username")
password = data.get("password")
remember_me = data.get("remember_me", False)
if not username or not password:
return jsonify({"error": "Benutzername und Passwort müssen angegeben werden"}), 400
db_session = get_db_session()
user = db_session.query(User).filter(
(User.username == username) | (User.email == username)
).first()
if user and user.check_password(password):
login_user(user, remember=remember_me)
auth_logger.info(f"API-Login erfolgreich für Benutzer {username}")
user_data = {
"id": user.id,
"username": user.username,
"name": user.name,
"email": user.email,
"is_admin": user.is_admin
}
db_session.close()
return jsonify({
"success": True,
"user": user_data,
"redirect_url": url_for("index")
})
else:
auth_logger.warning(f"Fehlgeschlagener API-Login für Benutzer {username}")
db_session.close()
return jsonify({"error": "Ungültiger Benutzername oder Passwort"}), 401
except Exception as e:
auth_logger.error(f"Fehler beim API-Login: {str(e)}")
return jsonify({"error": "Anmeldefehler. Bitte versuchen Sie es später erneut"}), 500
@auth_bp.route("/api/callback", methods=["GET", "POST"])
def api_callback():
"""OAuth-Callback-Endpunkt für externe Authentifizierung"""
try:
# Dieser Endpunkt würde für OAuth-Integration verwendet werden
# Hier könnte GitHub/OAuth-Code verarbeitet werden
# Placeholder für OAuth-Integration
return jsonify({
"message": "OAuth-Callback noch nicht implementiert",
"redirect_url": url_for("auth.login")
})
except Exception as e:
auth_logger.error(f"Fehler im OAuth-Callback: {str(e)}")
return jsonify({"error": "OAuth-Callback-Fehler"}), 500

View File

@ -111,7 +111,7 @@ mkdir -p "$APP_DIR/logs/jobs"
mkdir -p "$APP_DIR/logs/printers"
mkdir -p "$APP_DIR/logs/scheduler"
mkdir -p "$APP_DIR/logs/errors"
mkdir -p "$BACKEND_DIR/certs"
mkdir -p "$APP_DIR/certs"
mkdir -p "$PROJECT_DIR/frontend/ssl"
# Berechtigungen setzen
@ -119,7 +119,7 @@ log "8. Berechtigungen setzen..."
chown -R $USER:$USER "$PROJECT_DIR"
chmod -R 755 "$PROJECT_DIR"
chmod -R 700 "$APP_DIR/logs"
chmod -R 700 "$BACKEND_DIR/certs"
chmod -R 700 "$APP_DIR/certs"
# Datenbank initialisieren
log "9. Datenbank initialisieren..."
@ -196,8 +196,8 @@ server {
server_name raspberrypi localhost;
# SSL-Konfiguration
ssl_certificate $BACKEND_DIR/certs/myp.crt;
ssl_certificate_key $BACKEND_DIR/certs/myp.key;
ssl_certificate $APP_DIR/certs/myp.crt;
ssl_certificate_key $APP_DIR/certs/myp.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
@ -313,7 +313,7 @@ log "📁 Wichtige Verzeichnisse:"
log " • Anwendung: $APP_DIR"
log " • Logs: $APP_DIR/logs"
log " • Datenbank: $APP_DIR/database/myp.db"
log " • SSL-Zertifikate: $BACKEND_DIR/certs"
log " • SSL-Zertifikate: $APP_DIR/certs"
log ""
log "⚠️ Hinweise:"
log " • Das SSL-Zertifikat ist selbstsigniert"

View File

@ -23,7 +23,7 @@ done
# Execute docker compose
echo "Running docker compose..."
docker compose -f "docker/compose.yml" up -d
docker compose -f "compose.yml" up -d
# Check if the operation was successful
if [ $? -eq 0 ]; then

View File

@ -476,8 +476,8 @@ function Show-SSLStatus {
Show-Header "SSL-Zertifikat-Status"
$certPaths = @(
"backend\instance\ssl\myp.crt",
"backend\instance\ssl\myp.key",
"backend\app\certs\myp.crt",
"backend\app\certs\myp.key",
"frontend\ssl\myp.crt",
"frontend\ssl\myp.key"
)
@ -610,7 +610,7 @@ function Create-SSLCertificates {
Show-Header "SSL-Zertifikat-Generator"
# Parameter definieren
$certDir = "./backend/instance/ssl"
$certDir = "./backend/app/certs"
$backendCertFile = "$certDir/myp.crt"
$backendKeyFile = "$certDir/myp.key"
$frontendCertFile = "$certDir/frontend.crt"

File diff suppressed because it is too large Load Diff