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//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']) 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/', 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, 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.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 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//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//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//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')