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).all() # Drucker-Liste von der Session trennen für Template-Verwendung db_session.expunge_all() 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 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: 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 (jetzt durch eager loading verfügbar) 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: 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(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 Admin {current_user.id} ({current_user.name})") 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.name, "approved_at": guest_request.processed_at.isoformat(), "notes": approval_notes }) 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.name}): {rejection_reason}") return jsonify({ "success": True, "status": "denied", "rejected_by": current_user.name, "rejected_at": guest_request.processed_at.isoformat(), "reason": rejection_reason }) 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 @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')