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 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('/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) # 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/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