diff --git a/backend/blueprints/admin_unified.py b/backend/blueprints/admin_unified.py index ed3edd403..7010eda84 100644 --- a/backend/blueprints/admin_unified.py +++ b/backend/blueprints/admin_unified.py @@ -26,7 +26,7 @@ from datetime import datetime, timedelta from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, current_app from flask_login import login_required, current_user from functools import wraps -from models import User, Printer, Job, get_cached_session, Stats, SystemLog, PlugStatusLog +from models import User, Printer, Job, get_cached_session, Stats, SystemLog, PlugStatusLog, GuestRequest from utils.logging_config import get_logger # ===== BLUEPRINT-KONFIGURATION ===== @@ -1283,6 +1283,214 @@ def export_logs_api(): admin_logger.error(f"Fehler beim Exportieren der Logs: {str(e)}") return jsonify({"error": "Fehler beim Exportieren der Logs"}), 500 +# ===== GAST-OTP-MANAGEMENT FÜR OFFLINE-BETRIEB ===== + +@admin_api_blueprint.route("/guest-requests", methods=["GET"]) +@admin_required +def get_guest_requests_api(): + """API-Endpunkt zum Abrufen aller Gastanfragen mit OTP-Codes für Admins""" + try: + with get_cached_session() as db_session: + # Alle Gastanfragen laden + guest_requests = db_session.query(GuestRequest).order_by( + GuestRequest.created_at.desc() + ).all() + + # In Dictionary konvertieren mit OTP-Codes für Admins + requests_data = [] + for req in guest_requests: + request_data = { + 'id': req.id, + 'name': req.name, + 'email': req.email, + 'reason': req.reason, + 'status': req.status, + 'duration_min': req.duration_min, + 'created_at': req.created_at.isoformat() if req.created_at else None, + 'processed_at': req.processed_at.isoformat() if req.processed_at else None, + 'processed_by': req.processed_by, + 'approval_notes': req.approval_notes, + 'rejection_reason': req.rejection_reason, + 'author_ip': req.author_ip + } + + # OTP-Code für Admins sichtbar machen (nur wenn aktiv) + if req.status == 'approved' and req.otp_code and req.otp_expires_at: + if req.otp_expires_at > datetime.now() and not req.otp_used_at: + request_data['otp_code'] = req.otp_code # Klartext für Admin + request_data['otp_expires_at'] = req.otp_expires_at.isoformat() + request_data['otp_status'] = 'active' + elif req.otp_used_at: + request_data['otp_status'] = 'used' + request_data['otp_used_at'] = req.otp_used_at.isoformat() + else: + request_data['otp_status'] = 'expired' + else: + request_data['otp_status'] = 'not_generated' + + requests_data.append(request_data) + + admin_logger.info(f"Gastanfragen abgerufen: {len(requests_data)} Einträge für Admin {current_user.name}") + + return jsonify({ + "success": True, + "requests": requests_data, + "count": len(requests_data) + }) + + except Exception as e: + admin_logger.error(f"Fehler beim Abrufen der Gastanfragen: {str(e)}") + return jsonify({"error": "Fehler beim Laden der Gastanfragen"}), 500 + +@admin_api_blueprint.route("/guest-requests//generate-otp", methods=["POST"]) +@admin_required +def generate_guest_otp_api(request_id): + """Generiert einen neuen OTP-Code für eine genehmigte Gastanfrage""" + 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": "Gastanfrage nicht gefunden"}), 404 + + if guest_request.status != 'approved': + return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400 + + # Neuen OTP-Code generieren + otp_code = guest_request.generate_otp() + guest_request.otp_expires_at = datetime.now() + timedelta(hours=72) # 72h gültig + guest_request.otp_used_at = None # Reset falls bereits verwendet + + db_session.commit() + + admin_logger.info(f"Neuer OTP-Code generiert für Gastanfrage {request_id} von Admin {current_user.name}") + + return jsonify({ + "success": True, + "message": "Neuer OTP-Code generiert", + "otp_code": otp_code, + "expires_at": guest_request.otp_expires_at.isoformat(), + "guest_name": guest_request.name + }) + + except Exception as e: + admin_logger.error(f"Fehler beim Generieren des OTP-Codes: {str(e)}") + return jsonify({"error": "Fehler beim Generieren des OTP-Codes"}), 500 + +@admin_api_blueprint.route("/guest-requests//print-credentials", methods=["POST"]) +@admin_required +def print_guest_credentials_api(request_id): + """Erstellt Ausdruck-Template für Gast-Zugangsdaten""" + 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": "Gastanfrage nicht gefunden"}), 404 + + if guest_request.status != 'approved': + return jsonify({"error": "Gastanfrage muss erst genehmigt werden"}), 400 + + if not guest_request.otp_code or not guest_request.otp_expires_at: + return jsonify({"error": "Kein OTP-Code verfügbar"}), 400 + + # Ausdruck-Template erstellen + print_template = { + "type": "guest_credentials", + "title": "MYP GASTZUGANG GENEHMIGT", + "subtitle": "TBA Marienfelde - Offline System", + "guest_info": { + "name": guest_request.name, + "request_id": f"GAS-{guest_request.id:06d}", + "email": guest_request.email, + "approved_at": guest_request.processed_at.strftime("%d.%m.%Y %H:%M") if guest_request.processed_at else None, + "approved_by": guest_request.processed_by + }, + "access_data": { + "otp_code": guest_request.otp_code, + "valid_until": guest_request.otp_expires_at.strftime("%d.%m.%Y %H:%M"), + "login_url": "http://192.168.1.100:5000/auth/guest" + }, + "usage_rules": [ + "Max. Druckzeit pro Job: 4 Stunden", + "Dateiformate: STL, OBJ, 3MF, GCODE", + "Materialien: PLA, PETG", + "Jobs benötigen Admin-Freigabe" + ], + "pickup_info": { + "location": "TBA Marienfelde, Raum B2.1", + "hours": "Mo-Fr 8:00-16:00", + "storage_days": "Max. 7 Tage" + }, + "qr_code_data": f"http://192.168.1.100:5000/auth/guest?name={guest_request.name}&id={guest_request.id}", + "admin_note": "An Gast aushändigen", + "timestamp": datetime.now().isoformat() + } + + admin_logger.info(f"Ausdruck-Template erstellt für Gastanfrage {request_id} von Admin {current_user.name}") + + return jsonify({ + "success": True, + "print_template": print_template + }) + + except Exception as e: + admin_logger.error(f"Fehler beim Erstellen des Ausdruck-Templates: {str(e)}") + return jsonify({"error": "Fehler beim Erstellen des Ausdruck-Templates"}), 500 + +@admin_api_blueprint.route("/guest-requests/pending-otps", methods=["GET"]) +@admin_required +def get_pending_guest_otps_api(): + """Listet alle aktiven OTP-Codes für schnelle Admin-Übersicht""" + try: + with get_cached_session() as db_session: + # Alle genehmigten Anfragen mit aktiven OTP-Codes + active_requests = db_session.query(GuestRequest).filter( + GuestRequest.status == 'approved', + GuestRequest.otp_code.isnot(None), + GuestRequest.otp_expires_at > datetime.now(), + GuestRequest.otp_used_at.is_(None) + ).order_by(GuestRequest.otp_expires_at.asc()).all() + + # Kompakte Liste für Admin-Dashboard + otps_data = [] + for req in active_requests: + time_remaining = req.otp_expires_at - datetime.now() + hours_remaining = int(time_remaining.total_seconds() // 3600) + + otps_data.append({ + 'request_id': req.id, + 'guest_name': req.name, + 'otp_code': req.otp_code, + 'expires_at': req.otp_expires_at.isoformat(), + 'hours_remaining': hours_remaining, + 'urgency': 'critical' if hours_remaining < 2 else 'warning' if hours_remaining < 24 else 'normal' + }) + + admin_logger.info(f"Aktive OTP-Codes abgerufen: {len(otps_data)} Codes") + + return jsonify({ + "success": True, + "active_otps": otps_data, + "count": len(otps_data) + }) + + except Exception as e: + admin_logger.error(f"Fehler beim Abrufen aktiver OTP-Codes: {str(e)}") + return jsonify({"error": "Fehler beim Laden der OTP-Codes"}), 500 + +# ===== ADMIN-UI ROUTES FÜR GAST-OTP-VERWALTUNG ===== + +@admin_blueprint.route("/guest-otps") +@admin_required +def guest_otps_management(): + """Admin-UI für Gast-OTP-Verwaltung (Offline-System)""" + admin_logger.info(f"Gast-OTP-Verwaltung aufgerufen von Admin {current_user.name}") + + return render_template('admin_guest_otps.html', + page_title="Gast-OTP-Verwaltung", + current_user=current_user) + # ===== API-ENDPUNKTE FÜR SYSTEM-INFORMATIONEN ===== @admin_api_blueprint.route("/system/status", methods=["GET"]) diff --git a/backend/blueprints/guest.py b/backend/blueprints/guest.py index efab2ee16..503b25978 100644 --- a/backend/blueprints/guest.py +++ b/backend/blueprints/guest.py @@ -380,24 +380,29 @@ def api_create_guest_request(): @guest_blueprint.route('/api/guest/start-job', methods=['POST']) # CSRF-Schutz wird in app.py für Guest-APIs deaktiviert def api_start_job_with_code(): - """Job mit 6-stelligem OTP-Code starten.""" + """Job mit Name + 6-stelligem OTP-Code starten (Offline-System).""" try: data = request.get_json() - if not data or 'code' not in data: - return jsonify({"error": "Code ist erforderlich"}), 400 + if not data or 'code' not in data or 'name' not in data: + return jsonify({"error": "Name und Code sind erforderlich"}), 400 code = data['code'].strip().upper() + name = data['name'].strip() + if len(code) != 6: return jsonify({"error": "Code muss 6 Zeichen lang sein"}), 400 + if not name: + return jsonify({"error": "Name ist erforderlich"}), 400 + with get_cached_session() as db_session: - # Gastanfrage anhand des OTP-Codes finden - matching_request = GuestRequest.find_by_otp(code) + # Gastanfrage anhand des OTP-Codes UND Names finden + matching_request = GuestRequest.find_by_otp_and_name(code, name) if not matching_request: return jsonify({ "success": False, - "error": "Ungültiger oder bereits verwendeter Code" + "error": "Ungültiger Code oder Name stimmt nicht überein" }), 400 # Prüfen ob zugehöriger Job existiert @@ -997,8 +1002,8 @@ def api_get_request_otp(request_id): # CSRF-Schutz wird in app.py für Guest-APIs deaktiviert def api_guest_status_by_otp(): """ - Öffentliche Route für Gäste um ihren Auftragsstatus mit OTP-Code zu prüfen. - Keine Authentifizierung erforderlich. + Öffentliche Route für Gäste um ihren Auftragsstatus mit Name + OTP-Code zu prüfen. + Keine Authentifizierung erforderlich (Offline-System). """ try: data = request.get_json() @@ -1009,7 +1014,7 @@ def api_guest_status_by_otp(): }), 400 otp_code = data.get('otp_code', '').strip() - email = data.get('email', '').strip() # Optional für zusätzliche Verifikation + name = data.get('name', '').strip() if not otp_code: return jsonify({ @@ -1017,26 +1022,21 @@ def api_guest_status_by_otp(): 'message': 'OTP-Code ist erforderlich' }), 400 + if not name: + return jsonify({ + 'success': False, + 'message': 'Name 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 + # Gastanfrage mit Name + OTP-Code finden (sichere Methode) + found_request = GuestRequest.find_by_otp_and_name(otp_code, name) if not found_request: - logger.warning(f"Ungültiger OTP-Code für Gast-Status-Abfrage: {otp_code[:4]}****") + logger.warning(f"Ungültiger OTP-Code oder Name für Gast-Status-Abfrage: {name} / {otp_code[:4]}****") return jsonify({ 'success': False, - 'message': 'Ungültiger Code oder E-Mail-Adresse' + 'message': 'Ungültiger Code oder Name stimmt nicht überein' }), 404 # Status-Informationen für den Gast zusammenstellen diff --git a/backend/models.py b/backend/models.py index cf83bd891..6e2faa723 100644 --- a/backend/models.py +++ b/backend/models.py @@ -1228,6 +1228,41 @@ class GuestRequest(Base): except Exception as e: logger.error(f"Fehler beim Suchen der Gastanfrage per OTP: {str(e)}") return None + + @classmethod + def find_by_otp_and_name(cls, otp_code: str, name: str) -> Optional['GuestRequest']: + """ + Findet eine Gastanfrage anhand des OTP-Codes UND Names (für Offline-System). + Zusätzliche Sicherheit durch Name-Verifikation. + """ + if not otp_code or len(otp_code) != 6 or not name: + return None + + try: + with get_cached_session() as session: + # Alle genehmigten Gastanfragen mit OTP-Codes und passendem Namen finden + guest_requests = session.query(cls).filter( + cls.status == "approved", + cls.otp_code.isnot(None), + cls.otp_used_at.is_(None), # Noch nicht verwendet + cls.name.ilike(f"%{name.strip()}%") # Name-Matching (case-insensitive) + ).all() + + # Code gegen alle passenden Anfragen prüfen + for request in guest_requests: + if request.verify_otp(otp_code): + # Zusätzliche Name-Verifikation (exakte Übereinstimmung) + if request.name.strip().lower() == name.strip().lower(): + logger.info(f"Gastanfrage {request.id} erfolgreich per Name+OTP authentifiziert") + return request + else: + logger.warning(f"OTP stimmt, aber Name passt nicht exakt: '{request.name}' vs '{name}'") + + return None + + except Exception as e: + logger.error(f"Fehler beim Suchen der Gastanfrage per Name+OTP: {str(e)}") + return None class JobOrder(Base): diff --git a/backend/templates/admin_guest_otps.html b/backend/templates/admin_guest_otps.html new file mode 100644 index 000000000..ead51a972 --- /dev/null +++ b/backend/templates/admin_guest_otps.html @@ -0,0 +1,574 @@ +{% extends "base.html" %} + +{% block title %}Gast-OTP-Verwaltung - Mercedes-Benz TBA Marienfelde{% endblock %} + +{% block head %} +{{ super() }} + + +{% endblock %} + +{% block content %} +
+ + +
+
+
+

+ 🔑 Gast-OTP-Verwaltung +

+

+ Offline-System - OTP-Codes für Gastbenutzer verwalten +

+
+
+ + +
+
+
+ + +
+
+
+
+ 📝 +
+
+

Ausstehend

+

-

+
+
+
+ +
+
+
+ +
+
+

Aktive OTPs

+

-

+
+
+
+ +
+
+
+ ⚠️ +
+
+

Kritisch

+

-

+
+
+
+ +
+
+
+ 📋 +
+
+

Gesamt

+

-

+
+
+
+
+ + + + + +
+
+

+ 👥 Gastanfragen +

+

+ Alle Anfragen mit OTP-Management +

+
+ +
+
+
+ Lade Gastanfragen... +
+
+
+ + + + +
+ + +{% endblock %} \ No newline at end of file