import json 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 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") # 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']) def guest_request_form(): """Formular für Gastanfragen anzeigen.""" with get_cached_session() as db_session: printers = db_session.query(Printer).filter_by(active=True) return render_template('guest_request.html', printers=printers) @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 laden, sortiert nach Erstellungsdatum (neueste zuerst) guest_requests = db_session.query(GuestRequest).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: if len(req.reason) > 20: censored_reason = req.reason[:20] + "***" else: censored_reason = req.reason[:10] + "***" if len(req.reason) > 10 else "***" # Drucker-Info laden printer_name = "Unbekannt" if req.printer: printer_name = req.printer.name # Job-Status laden, falls vorhanden job_status = None if req.job_id: job = db_session.query(Job).filter_by(id=req.job_id).first() if job: job_status = job.status 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_name": printer_name, "job_status": job_status }) logger.info(f"Öffentliche Druckanträge-Übersicht aufgerufen - {len(public_requests)} Einträge") return render_template('guest_requests_overview.html', requests=public_requests) except Exception as e: logger.error(f"Fehler beim Laden der öffentlichen Druckanträge-Übersicht: {str(e)}") return render_template('guest_requests_overview.html', requests=[], error="Fehler beim Laden der Daten") @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") # Nur wenn Status "approved" ist, OTP generieren und anzeigen otp_code = None if guest_request.status == "approved" and not guest_request.otp_code: otp_code = guest_request.generate_otp() db_session.commit() # 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) # 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.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}") return jsonify({ "success": True, "request_id": guest_request.id, "status": guest_request.status, "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/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/requests//approve', methods=['POST']) @approver_required def api_approve_request(request_id): """Gastanfrage genehmigen.""" 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 != "pending": return jsonify({"error": "Anfrage wurde bereits bearbeitet"}), 400 # Anfrage genehmigen guest_request.status = "approved" # 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(role="admin").first() 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 Benutzer {current_user.id}") return jsonify({ "success": True, "status": "approved", "job_id": job.id, "otp": otp_plain # Nur in dieser Antwort wird der OTP-Klartext zurückgegeben }) 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 {} reason = data.get('reason', '') 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" db_session.commit() logger.info(f"Gastanfrage {request_id} abgelehnt von Benutzer {current_user.id}") return jsonify({ "success": True, "status": "denied" }) 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/jobs/start/', methods=['POST']) def api_start_job_with_otp(otp): """Job mit OTP starten.""" try: with get_cached_session() as db_session: # Alle genehmigten Anfragen mit OTP durchsuchen guest_requests = db_session.query(GuestRequest).filter_by(status="approved").all() valid_request = None for req in guest_requests: if req.verify_otp(otp): valid_request = req break if not valid_request: return jsonify({"error": "Ungültiger oder abgelaufener Code"}), 400 if not valid_request.job_id: return jsonify({"error": "Kein Job mit diesem Code verknüpft"}), 400 # Job laden job = db_session.query(Job).filter_by(id=valid_request.job_id).first() if not job: return jsonify({"error": "Job nicht gefunden"}), 404 # Job-Status prüfen if job.status != "scheduled": return jsonify({"error": "Job kann nicht gestartet werden"}), 400 # Grace-Period prüfen (5 Minuten vor bis 5 Minuten nach geplantem Start) now = datetime.now() start_time = job.start_at grace_start = start_time - timedelta(minutes=5) grace_end = start_time + timedelta(minutes=5) if now < grace_start: return jsonify({ "error": f"Der Job kann erst ab {grace_start.strftime('%H:%M')} Uhr gestartet werden" }), 400 if now > job.end_at: return jsonify({"error": "Der Job ist bereits abgelaufen"}), 400 # Job starten job.status = "active" job.start_at = now # Aktualisiere Startzeit auf jetzt # OTP als verwendet markieren valid_request.otp_used_at = now db_session.commit() logger.info(f"Job {job.id} mit OTP {otp} gestartet von IP: {request.remote_addr}") return jsonify({ "success": True, "job_id": job.id, "status": job.status, "started_at": job.start_at.isoformat(), "end_at": job.end_at.isoformat() }) except Exception as e: logger.error(f"Fehler beim Starten des Jobs mit OTP: {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