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 # 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//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/', 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 # 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/', 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//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//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/', 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//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/', 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//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 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 guest_request.printer_id = printer_id elif not guest_request.printer_id: # Automatisch ersten verfügbaren Drucker zuweisen available_printer = db_session.query(Printer).filter_by(active=True).first() if available_printer: guest_request.printer_id = available_printer.id logger.info(f"Automatisch Drucker {available_printer.id} ({available_printer.name}) für Gastanfrage {request_id} zugewiesen") else: return jsonify({"error": "Kein aktiver Drucker verfügbar. Bitte aktivieren Sie mindestens einen Drucker."}), 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//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//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')