774 lines
32 KiB
Python

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/<int:job_id>/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/<int:request_id>', 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/<int:request_id>', 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/<int:job_id>/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/<int:notification_id>/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/<int:request_id>', 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/<int:request_id>/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/<int:request_id>/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/<int:request_id>/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