Files
Projektarbeit-MYP/backend/blueprints/guest.py

1465 lines
65 KiB
Python

import json
import secrets
import bcrypt
from datetime import datetime, timedelta
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, abort, session, flash
from flask_login import current_user, login_required
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, IntegerField, SelectField
from wtforms.validators import DataRequired, Email, Optional, NumberRange
from functools import wraps
from sqlalchemy import desc
from sqlalchemy.orm import joinedload
from models import GuestRequest, Job, Printer, User, UserPermission, Notification, get_cached_session
from utils.logging_config import get_logger
from utils.hardware_integration import get_drucker_steuerung
guest_blueprint = Blueprint('guest', __name__)
logger = get_logger("guest")
# Flask-WTF Formular für Gastanfragen
class GuestRequestForm(FlaskForm):
name = StringField('Vollständiger Name', validators=[DataRequired()])
email = StringField('E-Mail-Adresse', validators=[DataRequired(), Email()])
printer_id = SelectField('Drucker auswählen', coerce=int, validators=[Optional()])
duration_min = IntegerField('Geschätzte Dauer (Minuten)',
validators=[DataRequired(), NumberRange(min=1, max=1440)],
default=60)
reason = TextAreaField('Projektbeschreibung', validators=[Optional()])
file = FileField('3D-Datei hochladen',
validators=[Optional(), FileAllowed(['stl', 'obj', '3mf', 'amf', 'gcode'],
'3D-Dateien sind erlaubt!')])
# Hilfsfunktionen
# Importiere Berechtigungsfunktionen aus utils.permissions
from utils.permissions import can_approve_jobs, approver_required
# Gast-Routen
@guest_blueprint.route('/request', methods=['GET', 'POST'])
def guest_request_form():
"""Formular für Gastanfragen anzeigen und verarbeiten."""
with get_cached_session() as db_session:
# Nur Drucker von TBA Marienfelde für Auswahlfelder anzeigen
printers = db_session.query(Printer).filter(
Printer.location == "TBA Marienfelde"
).all()
# Formular erstellen
form = GuestRequestForm()
# Drucker-Optionen für SelectField setzen
printer_choices = [(0, 'Keinen spezifischen Drucker auswählen')]
printer_choices.extend([(p.id, f"{p.name} ({p.location or 'Kein Standort'})") for p in printers])
form.printer_id.choices = printer_choices
if form.validate_on_submit():
try:
# Daten aus dem Formular extrahieren
name = form.name.data
email = form.email.data
reason = form.reason.data
duration_min = form.duration_min.data
printer_id = form.printer_id.data if form.printer_id.data != 0 else None
# IP-Adresse erfassen
author_ip = request.remote_addr
# Drucker validieren, falls angegeben
if printer_id:
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
if not printer:
flash("Ungültiger Drucker ausgewählt.", "error")
return render_template('guest_request.html', form=form, printers=printers)
# Neue Anfrage erstellen
guest_request = GuestRequest(
name=name,
email=email,
reason=reason,
duration_min=duration_min,
printer_id=printer_id,
author_ip=author_ip
)
db_session.add(guest_request)
db_session.flush() # Um ID zu erhalten
# OTP-Code sofort generieren für Status-Abfrage
otp_code = guest_request.generate_otp()
guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig
db_session.commit()
# Benachrichtigung für Genehmiger erstellen
Notification.create_for_approvers(
notification_type="guest_request",
payload={
"request_id": guest_request.id,
"name": guest_request.name,
"email": guest_request.email,
"reason": guest_request.reason,
"duration_min": guest_request.duration_min,
"printer_name": printer.name if printer_id and printer else "Kein spezifischer Drucker",
"created_at": guest_request.created_at.isoformat(),
"status": guest_request.status,
"author_ip": author_ip
}
)
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}, OTP generiert")
flash("Ihr Antrag wurde erfolgreich eingereicht!", "success")
# Weiterleitung zur Status-Seite mit OTP-Code-Info
return redirect(url_for('guest.guest_request_status', request_id=guest_request.id))
except Exception as e:
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
flash("Fehler beim Verarbeiten Ihres Antrags. Bitte versuchen Sie es erneut.", "error")
# Drucker-Liste von der Session trennen für Template-Verwendung
db_session.expunge_all()
return render_template('guest_request.html', form=form, printers=printers)
@guest_blueprint.route('/start', methods=['GET'])
def guest_start_public():
"""Öffentliche Code-Eingabe-Seite für Gäste (ohne Anmeldung)."""
return render_template('guest_start_job.html')
@guest_blueprint.route('/job/<int:job_id>/status', methods=['GET'])
def guest_job_status(job_id):
"""Job-Status-Seite für Gäste anzeigen."""
with get_cached_session() as db_session:
# Job mit eager loading des printer-Relationships laden
job = db_session.query(Job).options(
joinedload(Job.printer),
joinedload(Job.user)
).filter_by(id=job_id).first()
if not job:
abort(404, "Job nicht gefunden")
# Zugehörige Gastanfrage finden
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
db_session.expunge(job)
if guest_request:
db_session.expunge(guest_request)
return render_template('guest_job_status.html',
job=job,
guest_request=guest_request)
@guest_blueprint.route('/requests/overview', methods=['GET'])
def guest_requests_overview():
"""Öffentliche Übersicht aller Druckanträge mit zensierten persönlichen Daten."""
try:
with get_cached_session() as db_session:
# Alle Gastanfragen mit eager loading des printer-Relationships laden
guest_requests = db_session.query(GuestRequest).options(
joinedload(GuestRequest.printer)
).order_by(desc(GuestRequest.created_at)).all()
# Daten für Gäste aufbereiten (persönliche Daten zensieren)
public_requests = []
for req in guest_requests:
# Name zensieren: Nur ersten Buchstaben und letzten Buchstaben anzeigen
censored_name = "***"
if req.name and len(req.name) > 0:
if len(req.name) == 1:
censored_name = req.name[0] + "***"
elif len(req.name) == 2:
censored_name = req.name[0] + "***" + req.name[-1]
else:
censored_name = req.name[0] + "***" + req.name[-1]
# E-Mail zensieren
censored_email = "***@***.***"
if req.email and "@" in req.email:
email_parts = req.email.split("@")
if len(email_parts[0]) > 2:
censored_email = email_parts[0][:2] + "***@" + email_parts[1]
else:
censored_email = "***@" + email_parts[1]
# Grund zensieren (nur erste 20 Zeichen anzeigen)
censored_reason = "***"
if req.reason and len(req.reason) > 20:
censored_reason = req.reason[:20] + "..."
elif req.reason:
censored_reason = req.reason
public_requests.append({
"id": req.id,
"name": censored_name,
"email": censored_email,
"reason": censored_reason,
"duration_min": req.duration_min,
"created_at": req.created_at,
"status": req.status,
"printer": req.printer.to_dict() if req.printer else None
})
# Objekte explizit von der Session trennen
db_session.expunge_all()
return render_template('guest_requests_overview.html', requests=public_requests)
except Exception as e:
logger.error(f"Fehler beim Laden der öffentlichen Gastanfragen: {str(e)}")
return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Anfragen")
@guest_blueprint.route('/request/<int:request_id>', methods=['GET'])
def guest_request_status(request_id):
"""Status einer Gastanfrage anzeigen."""
with get_cached_session() as db_session:
# Guest Request mit eager loading des printer-Relationships laden
guest_request = db_session.query(GuestRequest).options(
joinedload(GuestRequest.printer)
).filter_by(id=request_id).first()
if not guest_request:
abort(404, "Anfrage nicht gefunden")
# OTP-Code nur anzeigen, wenn Anfrage genehmigt wurde
otp_code = None
show_start_link = False
if guest_request.status == "approved":
if not guest_request.otp_code:
# OTP generieren falls noch nicht vorhanden
otp_code = guest_request.generate_otp()
db_session.commit()
else:
# OTP existiert bereits - prüfen ob noch nicht verwendet
show_start_link = guest_request.otp_used_at is None
# Zugehörigen Job laden, falls vorhanden
job = None
if guest_request.job_id:
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
# Objekte explizit von der Session trennen, um sie außerhalb verwenden zu können
db_session.expunge(guest_request)
if job:
db_session.expunge(job)
return render_template('guest_status.html',
request=guest_request,
job=job,
otp_code=otp_code,
show_start_link=show_start_link)
@guest_blueprint.route('/guest/requests', methods=['GET'])
def guest_requests_by_email():
"""Guest-Requests für eine bestimmte E-Mail-Adresse anzeigen."""
email = request.args.get('email')
if not email:
# Ohne E-Mail-Parameter zur allgemeinen Übersicht weiterleiten
return redirect(url_for('guest.guest_requests_overview'))
try:
with get_cached_session() as db_session:
# Guest-Requests für die angegebene E-Mail-Adresse laden
guest_requests = db_session.query(GuestRequest).options(
joinedload(GuestRequest.printer)
).filter_by(email=email).order_by(desc(GuestRequest.created_at)).all()
# Jobs für die Requests laden falls vorhanden
request_data = []
for req in guest_requests:
job = None
if req.job_id:
job = db_session.query(Job).options(
joinedload(Job.printer)
).filter_by(id=req.job_id).first()
request_data.append({
'request': req,
'job': job
})
# Objekte von der Session trennen
db_session.expunge_all()
return render_template('guest_requests_by_email.html',
requests=request_data,
email=email)
except Exception as e:
logger.error(f"Fehler beim Laden der Guest-Requests für E-Mail {email}: {str(e)}")
return render_template('guest_requests_by_email.html',
requests=[],
email=email,
error="Fehler beim Laden der Anfragen")
# API-Endpunkte
@guest_blueprint.route('/api/guest/requests', methods=['POST'])
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
def api_create_guest_request():
"""Neue Gastanfrage erstellen."""
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
# Pflichtfelder prüfen
name = data.get('name')
if not name:
return jsonify({"error": "Name ist erforderlich"}), 400
# Optionale Felder
email = data.get('email')
reason = data.get('reason')
duration_min = data.get('duration_min', 60) # Standard: 1 Stunde
printer_id = data.get('printer_id')
# IP-Adresse erfassen
author_ip = request.remote_addr
try:
with get_cached_session() as db_session:
# Drucker prüfen
if printer_id:
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
if not printer:
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
# Neue Anfrage erstellen
guest_request = GuestRequest(
name=name,
email=email,
reason=reason,
duration_min=duration_min,
printer_id=printer_id,
author_ip=author_ip
)
db_session.add(guest_request)
db_session.flush() # Um ID zu erhalten
# OTP-Code sofort generieren für Status-Abfrage
otp_code = guest_request.generate_otp()
guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig
db_session.commit()
# Benachrichtigung für Genehmiger erstellen
Notification.create_for_approvers(
notification_type="guest_request",
payload={
"request_id": guest_request.id,
"name": guest_request.name,
"email": guest_request.email,
"reason": guest_request.reason,
"duration_min": guest_request.duration_min,
"printer_name": printer.name if printer_id and printer else "Kein spezifischer Drucker",
"created_at": guest_request.created_at.isoformat(),
"status": guest_request.status,
"author_ip": author_ip
}
)
logger.info(f"Neue Gastanfrage erstellt: ID {guest_request.id}, Name: {name}, OTP generiert")
return jsonify({
"success": True,
"request_id": guest_request.id,
"status": guest_request.status,
"otp_code": otp_code, # Code wird nur bei Erstellung zurückgegeben
"status_check_url": url_for('guest.guest_status_check_page', _external=True),
"redirect_url": url_for('guest.guest_request_status', request_id=guest_request.id)
})
except Exception as e:
logger.error(f"Fehler beim Erstellen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/guest/start-job', methods=['POST'])
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
def api_start_job_with_code():
"""Job mit 6-stelligem OTP-Code starten (vereinfacht - nur Code erforderlich)."""
try:
data = request.get_json()
if not data or 'code' not in data:
return jsonify({"error": "Code ist erforderlich"}), 400
code = data['code'].strip().upper()
if len(code) != 6:
return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400
with get_cached_session() as db_session:
# Gastanfrage nur anhand des OTP-Codes finden (vereinfacht)
matching_request = GuestRequest.find_by_otp(code)
if not matching_request:
return jsonify({
"success": False,
"error": "Ungültiger Code oder Code bereits verwendet"
}), 400
# Prüfen ob zugehöriger Job existiert
if not matching_request.job_id:
return jsonify({
"success": False,
"error": "Kein zugehöriger Job gefunden"
}), 400
job = db_session.query(Job).options(
joinedload(Job.printer)
).filter_by(id=matching_request.job_id).first()
if not job:
return jsonify({
"success": False,
"error": "Job nicht gefunden"
}), 400
# Prüfen ob Job noch startbar ist
if job.status not in ["scheduled", "waiting_for_printer"]:
return jsonify({
"success": False,
"error": f"Job kann im Status '{job.status}' nicht gestartet werden"
}), 400
# ===== NEUE DRUCKER-VERFÜGBARKEITSPRÜFUNG =====
drucker_steuerung = get_drucker_steuerung()
# 1. Prüfen ob ein spezifischer Drucker dem Job zugewiesen ist
if job.printer and job.printer.plug_ip:
# Spezifischen Drucker-Status prüfen
try:
reachable, power_status = drucker_steuerung.check_outlet_status(
job.printer.plug_ip, job.printer.id
)
if not reachable:
return jsonify({
"success": False,
"error": f"Drucker '{job.printer.name}' ist nicht erreichbar und kann nicht gestartet werden",
"error_details": {
"type": "printer_offline",
"printer_name": job.printer.name,
"printer_id": job.printer.id,
"reason": "Drucker ist im Netzwerk nicht erreichbar"
}
}), 400
# Drucker ist erreichbar - kann gestartet werden
logger.info(f"✅ Drucker-Verfügbarkeitsprüfung bestanden: {job.printer.name} (Status: {power_status})")
except Exception as e:
logger.error(f"❌ Fehler bei Drucker-Status-Prüfung für {job.printer.name}: {e}")
return jsonify({
"success": False,
"error": f"Drucker-Status für '{job.printer.name}' konnte nicht geprüft werden",
"error_details": {
"type": "printer_check_failed",
"printer_name": job.printer.name,
"printer_id": job.printer.id,
"reason": "Technischer Fehler bei der Status-Prüfung"
}
}), 500
else:
# 2. Kein spezifischer Drucker zugewiesen - prüfen ob IRGENDEIN Drucker verfügbar ist
logger.info("🔍 Kein spezifischer Drucker zugewiesen - prüfe alle verfügbaren Drucker")
# Alle aktiven Drucker mit Steckdosen laden
available_printers = db_session.query(Printer).filter(
Printer.active == True,
Printer.plug_ip.isnot(None)
).all()
if not available_printers:
return jsonify({
"success": False,
"error": "Keine konfigurierten Drucker verfügbar",
"error_details": {
"type": "no_printers_configured",
"reason": "Es sind keine aktiven Drucker mit Steckdosen-Steuerung konfiguriert"
}
}), 400
# Status aller verfügbaren Drucker prüfen
reachable_printers = []
offline_printers = []
for printer in available_printers:
try:
reachable, power_status = drucker_steuerung.check_outlet_status(
printer.plug_ip, printer.id
)
if reachable:
reachable_printers.append({
'id': printer.id,
'name': printer.name,
'status': power_status
})
else:
offline_printers.append({
'id': printer.id,
'name': printer.name
})
except Exception as e:
logger.warning(f"⚠️ Status-Prüfung für Drucker {printer.name} fehlgeschlagen: {e}")
offline_printers.append({
'id': printer.id,
'name': printer.name,
'error': str(e)
})
# Prüfen ob mindestens ein Drucker erreichbar ist
if not reachable_printers:
total_printers = len(available_printers)
offline_count = len(offline_printers)
offline_names = [p['name'] for p in offline_printers[:3]] # Max 3 Namen
offline_list = ', '.join(offline_names)
if offline_count > 3:
offline_list += f" und {offline_count - 3} weitere"
return jsonify({
"success": False,
"error": f"Alle {total_printers} Drucker sind offline und können nicht gestartet werden",
"error_details": {
"type": "all_printers_offline",
"total_printers": total_printers,
"offline_printers": offline_count,
"offline_list": offline_list,
"reason": "Keine Drucker sind derzeit erreichbar oder einsatzbereit"
}
}), 400
# Mindestens ein Drucker ist verfügbar
reachable_count = len(reachable_printers)
total_count = len(available_printers)
logger.info(f"✅ Drucker-Verfügbarkeitsprüfung bestanden: {reachable_count}/{total_count} Drucker erreichbar")
# Optional: Ersten verfügbaren Drucker automatisch zuweisen wenn keiner zugewiesen
if not job.printer_id and reachable_printers:
best_printer = reachable_printers[0] # Ersten verfügbaren nehmen
job.printer_id = best_printer['id']
logger.info(f"🎯 Drucker automatisch zugewiesen: {best_printer['name']} (ID: {best_printer['id']})")
# ===== JOB STARTEN (bestehender Code) =====
# Job starten
now = datetime.now()
job.status = "running"
job.start_at = now
job.end_at = now + timedelta(minutes=matching_request.duration_min or matching_request.duration_minutes or 60)
job.actual_start_time = now
# OTP als verwendet markieren
matching_request.mark_otp_used()
# Drucker einschalten über Tapo-Steckdose
if job.printer and job.printer.plug_ip:
try:
from utils.job_scheduler import BackgroundTaskScheduler
scheduler = BackgroundTaskScheduler()
plug_success = scheduler.toggle_printer_plug(job.printer_id, True)
if plug_success:
logger.info(f"🔌 Drucker für Gast-Job {job.id} eingeschaltet")
else:
logger.warning(f"⚠️ Steckdose für Gast-Job {job.id} konnte nicht eingeschaltet werden")
except Exception as e:
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
# Response-Daten vor Session-Commit sammeln
response_data = {
"success": True,
"job_id": job.id,
"job_name": job.name,
"start_time": job.start_at.strftime("%H:%M"),
"end_time": job.end_at.strftime("%H:%M"),
"duration_minutes": matching_request.duration_min or matching_request.duration_minutes or 60,
"printer_name": job.printer.name if job.printer else "Unbekannt",
"message": f"Job '{job.name}' erfolgreich gestartet"
}
db_session.commit()
logger.info(f"Job {job.id} mit 6-stelligem OTP-Code gestartet für Gastanfrage {matching_request.id}")
return jsonify(response_data)
except Exception as e:
logger.error(f"Fehler beim Starten des Jobs mit Code: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Starten des Jobs"
}), 500
@guest_blueprint.route('/api/guest/requests/<int:request_id>', methods=['GET'])
def api_get_guest_request(request_id):
"""Status einer Gastanfrage abrufen."""
try:
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
# OTP wird nie über die API zurückgegeben
response_data = guest_request.to_dict()
response_data.pop("otp_code", None)
return jsonify(response_data)
except Exception as e:
logger.error(f"Fehler beim Abrufen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/guest/job/<int:job_id>/status', methods=['GET'])
def api_get_guest_job_status(job_id):
"""Job-Status für Gäste abrufen."""
try:
with get_cached_session() as db_session:
# Job mit Drucker-Information laden
job = db_session.query(Job).options(
joinedload(Job.printer)
).filter_by(id=job_id).first()
if not job:
return jsonify({"error": "Job nicht gefunden"}), 404
# Zugehörige Gastanfrage prüfen
guest_request = db_session.query(GuestRequest).filter_by(job_id=job_id).first()
if not guest_request:
return jsonify({"error": "Kein Gastjob"}), 403
# Aktuelle Zeit für Berechnungen
now = datetime.now()
# Restzeit berechnen
remaining_minutes = 0
if job.status == "running" and job.end_at:
remaining_seconds = (job.end_at - now).total_seconds()
remaining_minutes = max(0, int(remaining_seconds / 60))
# Fortschritt berechnen
progress_percent = 0
if job.status == "running" and job.start_at and job.end_at:
total_duration = (job.end_at - job.start_at).total_seconds()
elapsed_duration = (now - job.start_at).total_seconds()
progress_percent = min(100, max(0, int((elapsed_duration / total_duration) * 100)))
elif job.status in ["completed", "finished"]:
progress_percent = 100
job_data = {
"id": job.id,
"name": job.name,
"status": job.status,
"start_at": job.start_at.isoformat() if job.start_at else None,
"end_at": job.end_at.isoformat() if job.end_at else None,
"duration_minutes": job.duration_minutes,
"remaining_minutes": remaining_minutes,
"progress_percent": progress_percent,
"printer": {
"id": job.printer.id,
"name": job.printer.name,
"location": job.printer.location
} if job.printer else None,
"guest_request": {
"id": guest_request.id,
"name": guest_request.name,
"created_at": guest_request.created_at.isoformat()
},
"is_active": job.status in ["scheduled", "running"],
"is_completed": job.status in ["completed", "finished"],
"is_failed": job.status in ["failed", "cancelled"]
}
return jsonify({
"success": True,
"job": job_data
})
except Exception as e:
logger.error(f"Fehler beim Abrufen des Job-Status: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/notifications', methods=['GET'])
@login_required
def api_get_notifications():
"""Benachrichtigungen für den aktuellen Benutzer abrufen."""
try:
# Zeitstempel für Filter (nur neue Benachrichtigungen)
since = request.args.get('since')
if since:
try:
since_date = datetime.fromisoformat(since)
except ValueError:
return jsonify({"error": "Ungültiges Datumsformat"}), 400
else:
since_date = None
with get_cached_session() as db_session:
query = db_session.query(Notification).filter_by(
user_id=current_user.id,
is_read=False
)
if since_date:
query = query.filter(Notification.created_at > since_date)
notifications = query.order_by(desc(Notification.created_at)).all()
return jsonify({
"count": len(notifications),
"notifications": [n.to_dict() for n in notifications]
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Benachrichtigungen: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/notifications/<int:notification_id>/read', methods=['POST'])
@login_required
def api_mark_notification_read(notification_id):
"""Benachrichtigung als gelesen markieren."""
try:
with get_cached_session() as db_session:
notification = db_session.query(Notification).filter_by(
id=notification_id,
user_id=current_user.id
).first()
if not notification:
return jsonify({"error": "Benachrichtigung nicht gefunden"}), 404
notification.is_read = True
db_session.commit()
return jsonify({"success": True})
except Exception as e:
logger.error(f"Fehler beim Markieren der Benachrichtigung als gelesen: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/admin/requests', methods=['GET'])
@approver_required
def api_get_all_requests():
"""Alle Gastanfragen für Admins abrufen."""
try:
# Filter-Parameter
status_filter = request.args.get('status', 'all') # all, pending, approved, denied
limit = int(request.args.get('limit', 50))
offset = int(request.args.get('offset', 0))
with get_cached_session() as db_session:
# Query mit eager loading
query = db_session.query(GuestRequest).options(
joinedload(GuestRequest.printer),
joinedload(GuestRequest.job),
joinedload(GuestRequest.processed_by_user)
)
# Status-Filter anwenden
if status_filter != 'all':
query = query.filter(GuestRequest.status == status_filter)
# Sortierung: Pending zuerst, dann nach Erstellungsdatum
query = query.order_by(
desc(GuestRequest.status == 'pending'),
desc(GuestRequest.created_at)
)
# Pagination
total_count = query.count()
requests = query.offset(offset).limit(limit).all()
# Daten für Admin aufbereiten
admin_requests = []
for req in requests:
request_data = req.to_dict()
# Zusätzliche Admin-Informationen
request_data.update({
"can_be_processed": req.status == "pending",
"is_overdue": (
req.status == "approved" and
req.job and
req.job.end_at < datetime.now()
) if req.job else False,
"time_since_creation": (datetime.now() - req.created_at).total_seconds() / 3600 if req.created_at else 0 # Stunden
})
admin_requests.append(request_data)
return jsonify({
"success": True,
"requests": admin_requests,
"pagination": {
"total": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count
},
"stats": {
"total": total_count,
"pending": db_session.query(GuestRequest).filter_by(status='pending').count(),
"approved": db_session.query(GuestRequest).filter_by(status='approved').count(),
"denied": db_session.query(GuestRequest).filter_by(status='denied').count()
}
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Admin-Gastanfragen: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['GET'])
@approver_required
def api_get_request_details(request_id):
"""Detaillierte Informationen zu einer Gastanfrage für Admins abrufen."""
try:
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).options(
joinedload(GuestRequest.printer),
joinedload(GuestRequest.job),
joinedload(GuestRequest.processed_by_user)
).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
# Vollständige Admin-Informationen
request_data = guest_request.to_dict()
# Verfügbare Drucker für Zuweisung
# Alle Drucker für Auswahlfelder anzeigen (unabhängig von active-Status)
available_printers = db_session.query(Printer).all()
request_data["available_printers"] = [p.to_dict() for p in available_printers]
# Job-Historie falls vorhanden
if guest_request.job:
job_data = guest_request.job.to_dict()
job_data["is_active"] = guest_request.job.status in ["scheduled", "running"]
job_data["is_overdue"] = guest_request.job.end_at < datetime.now() if guest_request.job.end_at else False
request_data["job_details"] = job_data
return jsonify({
"success": True,
"request": request_data
})
except Exception as e:
logger.error(f"Fehler beim Abrufen der Gastanfrage-Details: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/admin/requests/<int:request_id>/update', methods=['PUT'])
@approver_required
def api_update_request(request_id):
"""Gastanfrage aktualisieren (nur für Admins)."""
try:
data = request.get_json()
if not data:
return jsonify({"error": "Keine Daten erhalten"}), 400
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
# Erlaubte Felder für Updates
allowed_fields = ['printer_id', 'duration_min', 'approval_notes', 'rejection_reason']
changes_made = False
for field in allowed_fields:
if field in data:
if field == 'printer_id' and data[field]:
# Drucker validieren
printer = db_session.query(Printer).filter_by(id=data[field], active=True).first()
if not printer:
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
setattr(guest_request, field, data[field])
changes_made = True
if changes_made:
guest_request.processed_by = current_user.id
guest_request.processed_at = datetime.now()
db_session.commit()
logger.info(f"Gastanfrage {request_id} aktualisiert von Admin {current_user.id}")
return jsonify({
"success": True,
"message": "Anfrage erfolgreich aktualisiert",
"updated_by": current_user.name,
"updated_at": guest_request.processed_at.isoformat()
})
else:
return jsonify({"error": "Keine Änderungen vorgenommen"}), 400
except Exception as e:
logger.error(f"Fehler beim Aktualisieren der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/admin/requests/<int:request_id>', methods=['DELETE'])
@approver_required
def api_delete_request(request_id):
"""Gastanfrage löschen (nur für Admins)."""
try:
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
# Falls ein Job verknüpft ist, diesen auch löschen
if guest_request.job_id:
job = db_session.query(Job).filter_by(id=guest_request.job_id).first()
if job:
db_session.delete(job)
# Gastanfrage löschen
db_session.delete(guest_request)
db_session.commit()
logger.info(f"Gastanfrage {request_id} gelöscht von Admin {current_user.id} ({current_user.username})")
return jsonify({
"success": True,
"message": "Anfrage erfolgreich gelöscht",
"deleted_by": current_user.username,
"deleted_at": datetime.now().isoformat()
})
except Exception as e:
logger.error(f"Fehler beim Löschen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
# Admin-Routen
@guest_blueprint.route('/admin/requests', methods=['GET'])
@approver_required
def admin_requests_management():
"""Admin-Oberfläche für die Verwaltung von Gastanfragen."""
return render_template('admin_guest_requests.html')
@guest_blueprint.route('/api/requests/<int:request_id>/approve', methods=['POST'])
@approver_required
def api_approve_request(request_id):
"""Gastanfrage genehmigen."""
try:
data = request.get_json() or {}
approval_notes = data.get('notes', '')
printer_id = data.get('printer_id') # Optional: Drucker zuweisen/ändern
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
if guest_request.status != "pending":
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
# ===== ERWEITERTE DRUCKER-VALIDIERUNG UND -ZUWEISUNG =====
from utils.hardware_integration import get_drucker_steuerung
drucker_steuerung = get_drucker_steuerung()
# Drucker validieren oder automatisch zuweisen
if printer_id:
printer = db_session.query(Printer).filter_by(id=printer_id, active=True).first()
if not printer:
return jsonify({"error": "Ungültiger Drucker ausgewählt"}), 400
# Status des ausgewählten Druckers prüfen
if printer.plug_ip:
try:
reachable, power_status = drucker_steuerung.check_outlet_status(
printer.plug_ip, printer.id
)
if not reachable:
return jsonify({
"error": f"Drucker '{printer.name}' ist derzeit offline und kann nicht zugewiesen werden",
"warning": "Der Drucker ist im Netzwerk nicht erreichbar. Die Genehmigung kann trotzdem erfolgen, aber der Job kann erst gestartet werden, wenn der Drucker wieder online ist."
}), 400
logger.info(f"✅ Admin-Drucker-Zuweisung validiert: {printer.name} (Status: {power_status})")
except Exception as e:
logger.warning(f"⚠️ Status-Prüfung für zugewiesenen Drucker {printer.name} fehlgeschlagen: {e}")
# Warnung, aber Genehmigung trotzdem fortsetzen
guest_request.printer_id = printer_id
elif not guest_request.printer_id:
# Automatisch besten verfügbaren Drucker zuweisen
available_printers = db_session.query(Printer).filter(
Printer.active == True,
Printer.plug_ip.isnot(None)
).all()
if not available_printers:
return jsonify({
"error": "Kein aktiver Drucker mit Steckdosen-Steuerung verfügbar",
"details": "Bitte aktivieren Sie mindestens einen Drucker mit konfigurierter Tapo-Steckdose."
}), 400
# Status aller verfügbaren Drucker prüfen
online_printers = []
offline_printers = []
for printer in available_printers:
try:
reachable, power_status = drucker_steuerung.check_outlet_status(
printer.plug_ip, printer.id
)
if reachable:
online_printers.append({
'printer': printer,
'status': power_status
})
else:
offline_printers.append(printer)
except Exception as e:
logger.warning(f"⚠️ Status-Prüfung für Drucker {printer.name} fehlgeschlagen: {e}")
offline_printers.append(printer)
# Bevorzuge online Drucker, aber fallback auf offline Drucker
if online_printers:
best_printer = online_printers[0]['printer']
guest_request.printer_id = best_printer.id
logger.info(f"✅ Automatisch ONLINE-Drucker zugewiesen: {best_printer.name} (Status: {online_printers[0]['status']})")
elif available_printers:
# Fallback: Ersten verfügbaren Drucker nehmen, auch wenn offline
fallback_printer = available_printers[0]
guest_request.printer_id = fallback_printer.id
logger.warning(f"⚠️ Automatisch OFFLINE-Drucker zugewiesen: {fallback_printer.name} (alle Drucker sind offline)")
else:
return jsonify({
"error": "Kein Drucker verfügbar",
"details": "Es sind keine aktiven Drucker konfiguriert."
}), 400
# Drucker-Objekt für Job-Erstellung laden
printer = db_session.query(Printer).filter_by(id=guest_request.printer_id).first()
if not printer:
return jsonify({"error": "Zugewiesener Drucker nicht gefunden"}), 400
# Anfrage genehmigen
guest_request.status = "approved"
guest_request.processed_by = current_user.id
guest_request.processed_at = datetime.now()
guest_request.approval_notes = approval_notes
# OTP generieren
otp_plain = guest_request.generate_otp()
# Zugehörigen Job erstellen
start_time = datetime.now() + timedelta(minutes=5) # Start in 5 Minuten
end_time = start_time + timedelta(minutes=guest_request.duration_min)
# Admin-Benutzer als Eigentümer verwenden
admin_user = db_session.query(User).filter_by(is_admin=True).first()
if not admin_user:
admin_user = current_user # Fallback auf aktuellen Benutzer
job = Job(
name=f"Gastauftrag: {guest_request.name}",
description=guest_request.reason or "Gastauftrag",
user_id=admin_user.id,
printer_id=guest_request.printer_id,
start_at=start_time,
end_at=end_time,
status="scheduled",
duration_minutes=guest_request.duration_min,
owner_id=admin_user.id
)
db_session.add(job)
db_session.flush() # ID generieren
# Job-ID in Gastanfrage speichern
guest_request.job_id = job.id
db_session.commit()
logger.info(f"Gastanfrage {request_id} genehmigt von Admin {current_user.id} ({current_user.username}), Drucker: {printer.name}")
return jsonify({
"success": True,
"status": "approved",
"job_id": job.id,
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
"otp_code": otp_plain, # Für Frontend-Kompatibilität
"printer_name": printer.name,
"printer_id": printer.id,
"approved_by": current_user.username,
"approved_at": guest_request.processed_at.isoformat(),
"notes": approval_notes,
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}. Drucker: {printer.name}"
})
except Exception as e:
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
return jsonify({"error": f"Fehler beim Verarbeiten der Anfrage: {str(e)}"}), 500
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
@approver_required
def api_deny_request(request_id):
"""Gastanfrage ablehnen mit robuster Fehlerbehandlung."""
try:
# Content-Type prüfen
if not request.is_json and request.content_type != 'application/json':
return jsonify({"error": "Content-Type muss application/json sein"}), 400
data = request.get_json()
if not data:
return jsonify({"error": "Keine JSON-Daten erhalten"}), 400
# Reason validation verbessert
rejection_reason = data.get('reason', '').strip()
if not rejection_reason or len(rejection_reason) < 3:
return jsonify({
"error": "Ablehnungsgrund ist erforderlich (mindestens 3 Zeichen)",
"field": "reason"
}), 400
with get_cached_session() as db_session:
try:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({
"error": "Anfrage nicht gefunden",
"request_id": request_id
}), 404
# Status-Validation verbessert
if guest_request.status not in ["pending"]:
return jsonify({
"error": f"Anfrage kann im Status '{guest_request.status}' nicht abgelehnt werden",
"current_status": guest_request.status,
"allowed_statuses": ["pending"]
}), 400
# Transaktion mit besserer Fehlerbehandlung
guest_request.status = "denied"
guest_request.processed_by = current_user.id
guest_request.processed_at = datetime.now()
guest_request.rejection_reason = rejection_reason
# Commit mit expliziter Fehlerbehandlung
db_session.commit()
logger.info(f"Gastanfrage {request_id} abgelehnt von Admin {current_user.id} ({current_user.username}): {rejection_reason}")
return jsonify({
"success": True,
"status": "denied",
"request_id": request_id,
"rejected_by": current_user.username,
"rejected_at": guest_request.processed_at.isoformat(),
"reason": rejection_reason,
"message": "Anfrage wurde erfolgreich abgelehnt"
})
except Exception as db_error:
db_session.rollback()
logger.error(f"Datenbankfehler beim Ablehnen der Anfrage {request_id}: {str(db_error)}")
return jsonify({
"error": "Datenbankfehler beim Verarbeiten der Anfrage",
"details": str(db_error) if current_app.debug else None
}), 500
except Exception as e:
logger.error(f"Unerwarteter Fehler beim Ablehnen der Gastanfrage {request_id}: {str(e)}")
return jsonify({
"error": "Unerwarteter Serverfehler",
"details": str(e) if current_app.debug else None
}), 500
@guest_blueprint.route('/api/admin/requests/<int:request_id>/otp', methods=['GET'])
@approver_required
def api_get_request_otp(request_id):
"""OTP-Code für genehmigte Gastanfrage abrufen (nur für Admins)."""
try:
with get_cached_session() as db_session:
guest_request = db_session.query(GuestRequest).filter_by(id=request_id).first()
if not guest_request:
return jsonify({"error": "Anfrage nicht gefunden"}), 404
if guest_request.status != "approved":
return jsonify({"error": "Anfrage ist nicht genehmigt"}), 400
if not guest_request.otp_code:
return jsonify({"error": "Kein OTP-Code verfügbar"}), 400
# Prüfen ob OTP noch gültig ist
if guest_request.otp_expires_at and guest_request.otp_expires_at < datetime.now():
return jsonify({
"error": "OTP-Code ist abgelaufen",
"expired": True,
"expired_at": guest_request.otp_expires_at.isoformat()
}), 400
# Prüfen ob OTP bereits verwendet wurde
otp_used = guest_request.otp_used_at is not None
return jsonify({
"success": True,
"request_id": request_id,
"has_otp": True,
"otp_used": otp_used,
"otp_used_at": guest_request.otp_used_at.isoformat() if guest_request.otp_used_at else None,
"otp_expires_at": guest_request.otp_expires_at.isoformat() if guest_request.otp_expires_at else None,
"job_id": guest_request.job_id,
"message": "OTP-Code wurde bei Genehmigung angezeigt und kann nicht erneut abgerufen werden" if otp_used else "OTP-Code ist bereit zur Verwendung"
})
except Exception as e:
logger.error(f"Fehler beim Abrufen des OTP-Codes: {str(e)}")
return jsonify({"error": "Fehler beim Abrufen des OTP-Codes"}), 500
@guest_blueprint.route('/api/guest/status', methods=['POST'])
# CSRF-Schutz wird in app.py für Guest-APIs deaktiviert
def api_guest_status_by_otp():
"""
Öffentliche Route für Gäste um ihren Auftragsstatus mit Name + OTP-Code zu prüfen.
Keine Authentifizierung erforderlich (Offline-System).
"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': 'Keine Daten empfangen'
}), 400
otp_code = data.get('otp_code', '').strip()
name = data.get('name', '').strip()
if not otp_code:
return jsonify({
'success': False,
'message': 'OTP-Code ist erforderlich'
}), 400
if not name:
return jsonify({
'success': False,
'message': 'Name ist erforderlich'
}), 400
with get_cached_session() as db_session:
# Gastanfrage mit Name + OTP-Code finden (sichere Methode)
found_request = GuestRequest.find_by_otp_and_name(otp_code, name)
if not found_request:
logger.warning(f"Ungültiger OTP-Code oder Name für Gast-Status-Abfrage: {name} / {otp_code[:4]}****")
return jsonify({
'success': False,
'message': 'Ungültiger Code oder Name stimmt nicht überein'
}), 404
# Status-Informationen für den Gast zusammenstellen
status_info = {
'id': found_request.id,
'name': found_request.name,
'file_name': found_request.file_name,
'status': found_request.status,
'created_at': found_request.created_at.isoformat() if found_request.created_at else None,
'updated_at': found_request.updated_at.isoformat() if found_request.updated_at else None,
'duration_min': found_request.duration_min,
'reason': found_request.reason
}
# Status-spezifische Informationen hinzufügen
if found_request.status == 'approved':
status_info.update({
'approved_at': found_request.approved_at.isoformat() if found_request.approved_at else None,
'approval_notes': found_request.approval_notes,
'message': 'Ihr Auftrag wurde genehmigt! Sie können mit dem Drucken beginnen.',
'can_start_job': found_request.otp_used_at is None # Noch nicht verwendet
})
# Job-Informationen hinzufügen falls vorhanden
if found_request.job_id:
job = db_session.query(Job).options(joinedload(Job.printer)).filter_by(id=found_request.job_id).first()
if job:
status_info['job'] = {
'id': job.id,
'name': job.name,
'status': job.status,
'start_at': job.start_at.isoformat() if job.start_at else None,
'end_at': job.end_at.isoformat() if job.end_at else None,
'printer_name': job.printer.name if job.printer else None
}
elif found_request.status == 'rejected':
status_info.update({
'rejected_at': found_request.rejected_at.isoformat() if found_request.rejected_at else None,
'rejection_reason': found_request.rejection_reason,
'message': 'Ihr Auftrag wurde leider abgelehnt.'
})
elif found_request.status == 'pending':
# Berechne wie lange der Auftrag schon wartet
if found_request.created_at:
waiting_time = datetime.now() - found_request.created_at
hours_waiting = int(waiting_time.total_seconds() / 3600)
status_info.update({
'hours_waiting': hours_waiting,
'message': f'Ihr Auftrag wird bearbeitet. Wartezeit: {hours_waiting} Stunden.'
})
else:
status_info['message'] = 'Ihr Auftrag wird bearbeitet.'
# OTP als verwendet markieren (da erfolgreich abgefragt)
db_session.commit()
logger.info(f"Gast-Status-Abfrage erfolgreich für Request {found_request.id}")
return jsonify({
'success': True,
'request': status_info
})
except Exception as e:
logger.error(f"Fehler bei Gast-Status-Abfrage: {str(e)}")
return jsonify({
'success': False,
'message': 'Fehler beim Abrufen des Status'
}), 500
@guest_blueprint.route('/status-check')
def guest_status_check_page():
"""Status-Check-Seite für Gäste."""
return render_template('guest_status_check.html')
@guest_blueprint.route('/api/admin/printer-status', methods=['GET'])
@approver_required
def api_get_printer_status_for_admin():
"""
Drucker-Status für Admin-Oberfläche abrufen.
Zeigt welche Drucker verfügbar sind für die Zuweisung bei Gastanfragen.
"""
try:
from utils.hardware_integration import get_drucker_steuerung
with get_cached_session() as db_session:
# Alle aktiven Drucker laden
printers = db_session.query(Printer).filter(Printer.active == True).all()
if not printers:
return jsonify({
"success": True,
"printers": [],
"summary": {
"total": 0,
"online": 0,
"offline": 0,
"unconfigured": 0
},
"message": "Keine aktiven Drucker konfiguriert"
})
drucker_steuerung = get_drucker_steuerung()
printer_status_list = []
summary = {
"total": len(printers),
"online": 0,
"offline": 0,
"unconfigured": 0
}
for printer in printers:
printer_info = {
"id": printer.id,
"name": printer.name,
"model": printer.model or "Unbekannt",
"location": printer.location or "TBA Marienfelde",
"ip_address": printer.ip_address,
"plug_ip": printer.plug_ip,
"active": printer.active,
"status": "unknown",
"reachable": False,
"power_state": "unknown",
"can_be_assigned": False,
"status_message": "",
"last_checked": datetime.now().isoformat()
}
if printer.plug_ip:
try:
# Echtzeit-Status prüfen
reachable, power_status = drucker_steuerung.check_outlet_status(
printer.plug_ip, printer.id
)
printer_info["reachable"] = reachable
printer_info["power_state"] = power_status
if reachable:
printer_info["status"] = "online"
printer_info["can_be_assigned"] = True
printer_info["status_message"] = f"Online ({power_status.upper()})"
summary["online"] += 1
else:
printer_info["status"] = "offline"
printer_info["can_be_assigned"] = False
printer_info["status_message"] = "Offline - Nicht erreichbar"
summary["offline"] += 1
except Exception as e:
printer_info["status"] = "error"
printer_info["can_be_assigned"] = False
printer_info["status_message"] = f"Fehler: {str(e)}"
printer_info["error"] = str(e)
summary["offline"] += 1
logger.warning(f"⚠️ Admin-Status-Prüfung für {printer.name} fehlgeschlagen: {e}")
else:
printer_info["status"] = "unconfigured"
printer_info["can_be_assigned"] = False
printer_info["status_message"] = "Keine Steckdose konfiguriert"
summary["unconfigured"] += 1
printer_status_list.append(printer_info)
# Nach Status sortieren: Online zuerst, dann offline, dann unkonfiguriert
printer_status_list.sort(key=lambda x: (
0 if x["status"] == "online" else
1 if x["status"] == "offline" else
2 # unconfigured oder error
))
# Empfehlung für Admin generieren
recommendations = []
if summary["online"] == 0:
if summary["unconfigured"] > 0:
recommendations.append("Konfigurieren Sie Tapo-Steckdosen für die Drucker ohne Steuerung")
if summary["offline"] > 0:
recommendations.append("Prüfen Sie die Netzwerkverbindungen der offline Drucker")
recommendations.append("Aktuell können keine Gastanfragen automatisch gestartet werden")
elif summary["online"] < summary["total"]:
recommendations.append(f"Nur {summary['online']} von {summary['total']} Druckern sind verfügbar")
return jsonify({
"success": True,
"printers": printer_status_list,
"summary": summary,
"recommendations": recommendations,
"timestamp": datetime.now().isoformat(),
"message": f"Status von {len(printer_status_list)} Druckern abgerufen"
})
except Exception as e:
logger.error(f"Fehler beim Abrufen des Admin-Drucker-Status: {str(e)}")
return jsonify({
"success": False,
"error": "Fehler beim Abrufen des Drucker-Status",
"details": str(e)
}), 500