2025-06-04 10:03:22 +02:00

1035 lines
44 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
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
def can_approve_jobs(user_id):
"""Prüft, ob ein Benutzer Anfragen genehmigen darf."""
with get_cached_session() as db_session:
permission = db_session.query(UserPermission).filter_by(user_id=user_id).first()
if not permission:
return False
return permission.can_approve_jobs
def approver_required(f):
"""Decorator zur Prüfung der Genehmigungsberechtigung."""
@wraps(f)
@login_required
def decorated_function(*args, **kwargs):
if not can_approve_jobs(current_user.id):
abort(403, "Keine Berechtigung zum Genehmigen von Anfragen")
return f(*args, **kwargs)
return decorated_function
# 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:
# Aktive Drucker für SelectField laden
printers = db_session.query(Printer).filter_by(active=True).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,
"created_at": guest_request.created_at.isoformat(),
"status": guest_request.status
}
)
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-job', methods=['GET'])
def guest_start_job_form():
"""Code-Eingabe-Formular für Gäste anzeigen."""
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'])
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,
"created_at": guest_request.created_at.isoformat(),
"status": guest_request.status
}
)
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'])
def api_start_job_with_code():
"""Job mit OTP-Code starten."""
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:
# Alle genehmigten Gastanfragen mit OTP-Codes finden
guest_requests = db_session.query(GuestRequest).filter(
GuestRequest.status == "approved",
GuestRequest.otp_code.isnot(None),
GuestRequest.otp_used_at.is_(None) # Noch nicht verwendet
).all()
matching_request = None
for req in guest_requests:
# Code validieren
if req.verify_otp(code):
matching_request = req
break
if not matching_request:
return jsonify({
"success": False,
"error": "Ungültiger oder bereits verwendeter Code"
}), 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
# Job starten
now = datetime.now()
job.status = "running"
job.start_at = now
job.end_at = now + timedelta(minutes=matching_request.duration_min)
job.actual_start_time = now
# OTP als verwendet markieren
matching_request.otp_used_at = now
# Drucker einschalten (falls implementiert)
if job.printer and job.printer.plug_ip:
try:
from utils.job_scheduler import toggle_plug
toggle_plug(job.printer_id, True)
except Exception as e:
logger.warning(f"Fehler beim Einschalten des Druckers: {str(e)}")
db_session.commit()
logger.info(f"Job {job.id} mit OTP-Code gestartet für Gastanfrage {matching_request.id}")
return jsonify({
"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,
"printer_name": job.printer.name if job.printer else "Unbekannt",
"message": f"Job '{job.name}' erfolgreich gestartet"
})
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,
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.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
available_printers = db_session.query(Printer).filter_by(active=True).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
# 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
# Drucker validieren, falls angegeben
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
guest_request.printer_id = printer_id
# Sicherstellen, dass ein Drucker zugewiesen ist
if not guest_request.printer_id:
return jsonify({"error": "Kein Drucker zugewiesen. Bitte wählen Sie einen Drucker aus."}), 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})")
return jsonify({
"success": True,
"status": "approved",
"job_id": job.id,
"otp": otp_plain, # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben
"approved_by": current_user.username,
"approved_at": guest_request.processed_at.isoformat(),
"notes": approval_notes,
"message": f"Anfrage genehmigt. Zugangscode: {otp_plain}"
})
except Exception as e:
logger.error(f"Fehler beim Genehmigen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/requests/<int:request_id>/deny', methods=['POST'])
@approver_required
def api_deny_request(request_id):
"""Gastanfrage ablehnen."""
try:
data = request.get_json() or {}
rejection_reason = data.get('reason', '')
if not rejection_reason.strip():
return jsonify({"error": "Ablehnungsgrund ist erforderlich"}), 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
if guest_request.status != "pending":
return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400
# Anfrage ablehnen
guest_request.status = "denied"
guest_request.processed_by = current_user.id
guest_request.processed_at = datetime.now()
guest_request.rejection_reason = rejection_reason
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",
"rejected_by": current_user.username,
"rejected_at": guest_request.processed_at.isoformat(),
"reason": rejection_reason,
"message": "Anfrage wurde abgelehnt"
})
except Exception as e:
logger.error(f"Fehler beim Ablehnen der Gastanfrage: {str(e)}")
return jsonify({"error": "Fehler beim Verarbeiten der Anfrage"}), 500
@guest_blueprint.route('/api/guest/status', methods=['POST'])
def api_guest_status_by_otp():
"""
Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen.
Keine Authentifizierung erforderlich.
"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'message': 'Keine Daten empfangen'
}), 400
otp_code = data.get('otp_code', '').strip()
email = data.get('email', '').strip() # Optional für zusätzliche Verifikation
if not otp_code:
return jsonify({
'success': False,
'message': 'OTP-Code ist erforderlich'
}), 400
with get_cached_session() as db_session:
# Alle Gastaufträge mit OTP-Codes finden
guest_requests = db_session.query(GuestRequest).filter(
GuestRequest.otp_code.isnot(None)
).all()
found_request = None
for request_obj in guest_requests:
if request_obj.verify_otp(otp_code):
# Zusätzliche E-Mail-Verifikation falls angegeben
if email and request_obj.email and request_obj.email.lower() != email.lower():
continue
found_request = request_obj
break
if not found_request:
logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****")
return jsonify({
'success': False,
'message': 'Ungültiger Code oder E-Mail-Adresse'
}), 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():
"""
Öffentliche Seite für Gäste um ihren Auftragsstatus zu prüfen.
"""
return render_template('guest_status_check.html')