From 0d5b87f1638c9e4dacafce4f85eef9030ac5e5e6 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Sun, 25 May 2025 20:59:13 +0200 Subject: [PATCH] feat: Implement SSL support and kiosk mode enhancements - Added SSL configuration to the backend, including self-signed certificate generation and management. - Updated `setup_myp.sh` to create SSL certificates during installation. - Enhanced `app.py` to support SSL context for secure communication. - Introduced a new SSL management menu in the setup script for easier certificate handling. - Updated frontend API calls to use HTTPS for secure data transmission. - Implemented kiosk mode features, including automatic browser launch with SSL support. - Improved documentation in `SUMMARY.md` to reflect new features and network topology changes. --- SUMMARY.md | 85 ++++++++++- backend/app/app.py | 96 +++++++++---- backend/app/config/settings.py | 50 ++++++- backend/app/static/js/sw.js | 2 +- backend/install/create_ssl_cert.sh | 160 +++++++++++++++++++++ backend/install/kiosk.sh | 9 +- backend/install/ssl_check.sh | 99 +++++++++++++ backend/setup_myp.sh | 140 ++++++++++++++---- frontend/next.config.mjs | 4 +- frontend/src/app/layout.tsx | 2 + frontend/src/components/ui/ssl-warning.tsx | 86 +++++++++++ frontend/src/utils/api-config.ts | 23 ++- frontend/src/utils/api-helper.ts | 75 ++++++++++ frontend/src/utils/external-api.ts | 127 +++++++--------- 14 files changed, 812 insertions(+), 146 deletions(-) create mode 100755 backend/install/create_ssl_cert.sh create mode 100755 backend/install/ssl_check.sh create mode 100644 frontend/src/components/ui/ssl-warning.tsx create mode 100644 frontend/src/utils/api-helper.ts diff --git a/SUMMARY.md b/SUMMARY.md index 93085aed..ecf494cb 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -10,6 +10,8 @@ Die MYP-Plattform (My Young Printer) ist ein Verwaltungssystem für 3D-Drucker m - Offline-Fähigkeit durch Service Worker Integration - REST-API für Frontend und externe Dienste - Automatisierte Auftrags-Planung und -Ausführung via Scheduler +- Verschlüsselte Kommunikation über HTTPS mit selbstsignierten Zertifikaten +- Kiosk-Modus für Informationsdisplays und öffentliche Terminals Das Backend basiert auf Flask (Python) und nutzt eine SQLite-Datenbank für die Datenspeicherung. @@ -24,22 +26,40 @@ Die MYP-Plattform setzt auf eine verteilte Netzwerk-Architektur: - **3D-Drucker**: Werden als Netzwerkgeräte mit festen IP-Adressen konfiguriert - **Frontend**: Als Web-App mit Offline-Funktionalität konzipiert - **PWA-Funktionalität**: Service Worker für Offline-Betrieb und Cache-Management +- **Kiosk-Displays**: Raspberry Pi mit Chrome im Kiosk-Modus ### Netzwerk-Topologie ``` -[Frontend/PWA] <-- HTTP/REST --> [Flask-Backend] <-- PyP110-Lib --> [Smart-Plugs] - ^ ^ | - | | v - +------ PWA Offline Cache -------+ [3D-Drucker] + ┌──────────────────┐ + │ Command Center │ + │ (192.168.0.1) │ + └──────────┬───────┘ + │ + ▼ +┌───────────────┐ ┌─────────────────────┐ ┌──────────────┐ +│ Frontend/PWA │◄────►│ Flask-Backend │◄────►│ Smart-Plugs │ +│(192.168.0.105)│ │ (192.168.0.105) │ │(192.168.0.1xx)│ +└───────┬───────┘ └─────────┬───────────┘ └──────┬────────┘ + │ │ │ + │ │ ▼ +┌───────▼───────┐ │ ┌────────────────┐ +│ Kiosk-Mode │ │ │ 3D-Drucker │ +│ (10.0.0.1-10) │ │ │(192.168.0.1xx) │ +└───────────────┘ ▼ └────────────────┘ + ┌─────────────────────┐ + │ Ad-hoc Pi-Netzwerk │ + │ (10.0.0.x/24) │ + └─────────────────────┘ ``` ### Kommunikationsprotokolle -- **HTTP/REST**: Zwischen Frontend und Backend +- **HTTPS/REST**: Zwischen Frontend und Backend (Port 5000) - **JSON**: Standardformat für Datenaustausch - **Tapo-Protokoll**: Für Smart-Plug-Steuerung via PyP110-Bibliothek - **SQLite**: Lokale Datenbankanbindung +- **TLS 1.2/1.3**: Verschlüsselte Kommunikation mit selbstsignierten Zertifikaten ### Vernetzungs-Features @@ -47,6 +67,8 @@ Die MYP-Plattform setzt auf eine verteilte Netzwerk-Architektur: - SmartPlug-Integration für Fernsteuerung der Stromversorgung - Offline-Betriebsmodus mit synchronisierenden Service Workern - Automatische Erkennung der Drucker-Status via Netzwerk-Polling +- Kiosk-Modus mit automatischem Start nach Systemneustart +- Command Center für zentrale Verwaltung aller Komponenten ## 3. Hauptkomponenten des Backends @@ -64,6 +86,7 @@ Das Backend stellt eine umfassende REST-API bereit: - **Printer Management**: Druckerstatus, Steuerung - **Job Management**: Auftragsplanung, -verwaltung und -überwachung - **Stats API**: Statistiken und Auswertungen +- **Kiosk API**: Steuerung der Kiosk-Displays ### SmartPlug-Integration @@ -71,6 +94,13 @@ Das Backend stellt eine umfassende REST-API bereit: - Automatisierte Steuerung der Stromversorgung basierend auf Jobplanung - Status-Monitoring und Fehlerbehandlung +### SSL/HTTPS-Implementierung + +- Selbstsignierte Zertifikate für verschlüsselte Kommunikation +- Automatische Zertifikatsgenerierung während der Installation +- Konfigurierbare Zertifikatslaufzeit (Standard: 10 Jahre) +- Prüfwerkzeuge für Zertifikatsstatus und -gültigkeit + ## 4. Datenbankmodell & Scheduler-Logik ### Datenbankmodelle @@ -111,6 +141,9 @@ Der BackgroundTaskScheduler bietet: | /api/scheduler/status | GET | Scheduler-Status | Ja | | /api/scheduler/start | POST | Scheduler starten | Ja, Admin | | /api/scheduler/stop | POST | Scheduler stoppen | Ja, Admin | +| /api/kiosk/status | GET | Kiosk-Status | Ja | +| /api/kiosk/activate | POST | Kiosk aktivieren | Ja, Admin | +| /api/kiosk/deactivate | POST | Kiosk deaktivieren | Ja, Admin | | /auth/login | POST | Anmelden | Nein | | /auth/logout | GET/POST | Abmelden | Ja | @@ -125,6 +158,7 @@ Der BackgroundTaskScheduler bietet: ### Netzwerksicherheit +- HTTPS mit selbstsignierten TLS-Zertifikaten - CORS-Konfiguration für sichere Cross-Origin-Requests - Sicherheitsheader im Response (X-Content-Type-Options, X-Frame-Options) - Keine sensiblen Daten in URLs oder Query-Parametern @@ -137,17 +171,57 @@ Der BackgroundTaskScheduler bietet: ## 7. Build- & Deployment-Ablauf +### Installationsprozess + +Die Installation der MYP-Plattform erfolgt über verschiedene Shell-Skripte: + +- **setup_myp.sh**: Hauptinstallationsskript (Command Center) + - Standardinstallation mit HTTPS-Unterstützung + - Kiosk-Modus-Installation (gehärtete Variante) + - Netzwerk- und DNS-Konfiguration + - Systemüberwachung und Logging + +- **create_ssl_cert.sh**: Generiert selbstsignierte Zertifikate + - Unterstützt Multiple-SAN-Entries (Hostname, IP-Adressen) + - Konfigurierbare Zertifikatslaufzeit + - Automatische Fehlerbehandlung + +- **ssl_check.sh**: Prüft SSL-Zertifikatsstatus + - Validiert Gültigkeitsdauer + - Zeigt Fingerprint und Subject-Details + - Warnt vor bald ablaufenden Zertifikaten + ### Backend-Deployment - Python 3.11 venv-Umgebung - Konfigurierbare Entwicklungs- und Produktionsumgebungen - Log-Rotation und strukturierte Logging-Hierarchie +- SSL/TLS-Unterstützung mit automatischer Zertifikatsgenerierung + +### Kiosk-Modus + +- Basiert auf Raspberry Pi mit Chromium Browser +- Automatischer Start im Vollbildmodus nach Boot +- Deaktivierung von Fehlerdialogen und Warnungen +- Integrierte Überwachung und Watchdog-Funktionalität +- Spezielle Konfiguration für öffentliche Informationsdisplays +- Ignoriert SSL-Zertifikatswarnungen für selbstsignierte Zertifikate + +### Command Center + +- Zentrales Verwaltungstool mit CLI-Interface +- Integrierte Systemdiagnose und -überwachung +- Einfache Verwaltung von SSL-Zertifikaten +- Fernsteuerung von Kiosk-Displays +- Netzwerk- und IP-Konfiguration +- Dienst-Management (Start/Stop/Neustart) ### Frontend-Integration - Tailwind CSS für responsive UI - CLI-Befehle für Tailwind-Kompilierung - Service Worker für PWA-Funktionalität +- HTTPS-Unterstützung mit Proxy-Konfiguration ### Systemd-Integration @@ -169,6 +243,7 @@ Der BackgroundTaskScheduler bietet: - OAuth2-Integration für externe Authentifizierungsquellen - Zwei-Faktor-Authentifizierung - Zertifikatsbasierte Geräteauthentifizierung für SmartPlugs +- Letsencrypt-Integration für vertrauenswürdige Zertifikate ### Skalierbarkeit diff --git a/backend/app/app.py b/backend/app/app.py index def51193..1d811075 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -18,7 +18,7 @@ from flask_wtf.csrf import CSRFProtect from config.settings import ( SECRET_KEY, TAPO_USERNAME, TAPO_PASSWORD, PRINTERS, FLASK_HOST, FLASK_PORT, FLASK_DEBUG, SESSION_LIFETIME, - SCHEDULER_INTERVAL, SCHEDULER_ENABLED + SCHEDULER_INTERVAL, SCHEDULER_ENABLED, get_ssl_context ) from utils.logging_config import setup_logging, get_logger, log_startup_info from models import User, Printer, Job, Stats, get_db_session, init_database, create_initial_admin @@ -1265,16 +1265,19 @@ def tailwind_watch(): # Auto-Kompilierung beim Serverstart im Debug-Modus def compile_tailwind_if_debug(): - if app.debug: + """Kompiliert Tailwind CSS im Debug-Modus, falls notwendig.""" + if FLASK_DEBUG: try: - subprocess.run(["npx", "tailwindcss", "-i", "./static/css/input.css", - "-o", "./static/css/tailwind-dark-consolidated.min.css"], - check=True, capture_output=True) - print("Tailwind CSS für Debug-Modus kompiliert.") - except subprocess.CalledProcessError as e: - print(f"Warnung: Konnte Tailwind CSS nicht kompilieren: {e}") - except FileNotFoundError: - print("Warnung: Node.js/npm nicht gefunden. Tailwind CSS wurde nicht kompiliert.") + app_logger.info("Kompiliere Tailwind CSS...") + subprocess.run([ + "npx", "tailwindcss", "-i", "static/css/input.css", + "-o", "static/css/tailwind.min.css", "--minify" + ], check=True) + app_logger.info("Tailwind CSS erfolgreich kompiliert.") + except subprocess.CalledProcessError: + app_logger.warning("Tailwind konnte nicht kompiliert werden. Möglicherweise ist npx/Node.js nicht installiert.") + except Exception as e: + app_logger.error(f"Fehler beim Kompilieren von Tailwind CSS: {str(e)}") # Tailwind CSS kompilieren, wenn im Debug-Modus if FLASK_DEBUG: @@ -1282,34 +1285,65 @@ if FLASK_DEBUG: # Initialisierung der Datenbank beim Start def init_app(): + """Initialisiert die App-Komponenten und startet den Scheduler.""" + # Datenbank initialisieren try: - # Datenbank initialisieren init_database() - # Admin-Benutzer erstellen oder zurücksetzen create_initial_admin() - - # Template-Helper registrieren - register_template_helpers(app) - app_logger.info("Template-Helper registriert") - - # Scheduler starten, wenn aktiviert - if SCHEDULER_ENABLED: - scheduler.start() - app_logger.info("Job-Scheduler gestartet") except Exception as e: - app_logger.error(f"Fehler bei der Initialisierung: {str(e)}") + app_logger.error(f"Fehler bei der Datenbank-Initialisierung: {str(e)}") + + # Jinja2-Helfer registrieren + register_template_helpers(app) + + # Tailwind im Debug-Modus kompilieren + compile_tailwind_if_debug() + + # Scheduler starten, wenn aktiviert + if SCHEDULER_ENABLED: + try: + # Scheduler-Task für Druckauftrags-Prüfung registrieren + scheduler.register_task( + "check_jobs", + check_jobs, + interval=SCHEDULER_INTERVAL + ) + + # Scheduler starten + scheduler.start() + app_logger.info(f"Scheduler gestartet mit Intervall {SCHEDULER_INTERVAL} Sekunden.") + except Exception as e: + app_logger.error(f"Fehler beim Starten des Schedulers: {str(e)}") + + # SSL-Kontext protokollieren + ssl_context = get_ssl_context() + if ssl_context: + app_logger.info(f"SSL aktiviert mit Zertifikat {ssl_context[0]}") + else: + app_logger.warning("SSL ist deaktiviert. Die Verbindung ist unverschlüsselt!") # App starten if __name__ == "__main__": - # Initialisierung ausführen - init_app() - - # Flask-Server starten - app.run( - host=FLASK_HOST, - port=FLASK_PORT, - debug=FLASK_DEBUG - ) + try: + # App initialisieren + init_app() + + # SSL-Kontext ermitteln + ssl_context = get_ssl_context() + + # Konsolen-Ausgabe für HTTPS + protocol = "HTTPS" if ssl_context else "HTTP" + app_logger.info(f"MYP startet auf {protocol}://{FLASK_HOST}:{FLASK_PORT} (Debug: {FLASK_DEBUG})") + + # App starten + app.run( + host=FLASK_HOST, + port=FLASK_PORT, + debug=FLASK_DEBUG, + ssl_context=ssl_context + ) + except Exception as e: + app_logger.critical(f"Kritischer Fehler beim Starten der Anwendung: {str(e)}") # Content Security Policy anpassen @app.after_request diff --git a/backend/app/config/settings.py b/backend/app/config/settings.py index beb65ef1..f4571615 100644 --- a/backend/app/config/settings.py +++ b/backend/app/config/settings.py @@ -31,6 +31,11 @@ FLASK_PORT = 5000 FLASK_DEBUG = True SESSION_LIFETIME = timedelta(days=7) +# SSL-Konfiguration +SSL_ENABLED = True +SSL_CERT_PATH = "/opt/myp/ssl/myp.crt" +SSL_KEY_PATH = "/opt/myp/ssl/myp.key" + # Scheduler-Konfiguration SCHEDULER_INTERVAL = 60 # Sekunden SCHEDULER_ENABLED = True @@ -63,4 +68,47 @@ def ensure_database_directory(): """Erstellt das Datenbank-Verzeichnis.""" db_dir = os.path.dirname(DATABASE_PATH) if db_dir: - os.makedirs(db_dir, exist_ok=True) \ No newline at end of file + os.makedirs(db_dir, exist_ok=True) + +def ensure_ssl_directory(): + """Erstellt das SSL-Verzeichnis, falls es nicht existiert.""" + ssl_dir = os.path.dirname(SSL_CERT_PATH) + if ssl_dir and not os.path.exists(ssl_dir): + os.makedirs(ssl_dir, exist_ok=True) + +def get_ssl_context(): + """ + Gibt den SSL-Kontext für Flask zurück, wenn SSL aktiviert ist. + + Returns: + tuple oder None: Tuple mit Zertifikat- und Schlüsselpfad, wenn SSL aktiviert ist, sonst None + """ + if not SSL_ENABLED: + return None + + # Wenn Zertifikate nicht existieren, diese automatisch erstellen + if not os.path.exists(SSL_CERT_PATH) or not os.path.exists(SSL_KEY_PATH): + ensure_ssl_directory() + + # Prüfen, ob wir uns im Entwicklungsmodus befinden + if FLASK_DEBUG: + print("SSL-Zertifikate nicht gefunden. Erstelle selbstsignierte Zertifikate...") + + # Pfad zum create_ssl_cert.sh-Skript ermitteln + script_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "install", "create_ssl_cert.sh") + + # Ausführungsrechte setzen + if os.path.exists(script_path): + os.system(f"chmod +x {script_path}") + + # Zertifikate erstellen + os.system(f"{script_path} -c {SSL_CERT_PATH} -k {SSL_KEY_PATH}") + else: + print(f"WARNUNG: SSL-Zertifikat-Generator nicht gefunden: {script_path}") + return None + else: + print("WARNUNG: SSL-Zertifikate nicht gefunden und Nicht-Debug-Modus. SSL wird deaktiviert.") + return None + + return (SSL_CERT_PATH, SSL_KEY_PATH) \ No newline at end of file diff --git a/backend/app/static/js/sw.js b/backend/app/static/js/sw.js index 57692ea3..e95026a9 100644 --- a/backend/app/static/js/sw.js +++ b/backend/app/static/js/sw.js @@ -87,7 +87,7 @@ self.addEventListener('fetch', (event) => { const { request } = event; const url = new URL(request.url); - // Skip non-GET requests and unsupported schemes for caching + // Unterstütze sowohl HTTP als auch HTTPS if (request.method !== 'GET' || (url.protocol !== 'http:' && url.protocol !== 'https:')) { return; diff --git a/backend/install/create_ssl_cert.sh b/backend/install/create_ssl_cert.sh new file mode 100755 index 00000000..a30e8b85 --- /dev/null +++ b/backend/install/create_ssl_cert.sh @@ -0,0 +1,160 @@ +#!/usr/bin/env bash +# MYP V2 - SSL-Zertifikat-Generator +# Erstellt selbstsignierte Zertifikate für die HTTPS-Kommunikation + +# Fehlerabbruch aktivieren +set -e + +# Farben für bessere Lesbarkeit +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Funktion für Titel +print_header() { + echo -e "${BLUE}================================================================${NC}" + echo -e "${BLUE} MYP V2 - SSL-Zertifikat-Generator ${NC}" + echo -e "${BLUE}================================================================${NC}" + echo "" +} + +# Standardwerte +CERT_DIR="/opt/myp/ssl" +CERT_FILE="$CERT_DIR/myp.crt" +KEY_FILE="$CERT_DIR/myp.key" +DAYS_VALID=3650 # 10 Jahre +HOSTNAME=$(hostname -f) +IP_ADDRESS=$(hostname -I | awk '{print $1}') + +# Hilfe-Funktion +show_help() { + echo "Verwendung: $0 [Optionen]" + echo "" + echo "Optionen:" + echo " -d, --dir DIR Verzeichnis für Zertifikate (Standard: $CERT_DIR)" + echo " -c, --cert DATEI Pfad zur Zertifikatsdatei (Standard: $CERT_FILE)" + echo " -k, --key DATEI Pfad zur Schlüsseldatei (Standard: $KEY_FILE)" + echo " -h, --hostname NAME Hostname für das Zertifikat (Standard: $HOSTNAME)" + echo " -i, --ip IP IP-Adresse für das Zertifikat (Standard: $IP_ADDRESS)" + echo " -v, --valid TAGE Gültigkeitsdauer in Tagen (Standard: $DAYS_VALID)" + echo " --help Diese Hilfe anzeigen" + echo "" +} + +# Argumente verarbeiten +while [[ $# -gt 0 ]]; do + case $1 in + -d|--dir) + CERT_DIR="$2" + shift 2 + ;; + -c|--cert) + CERT_FILE="$2" + shift 2 + ;; + -k|--key) + KEY_FILE="$2" + shift 2 + ;; + -h|--hostname) + HOSTNAME="$2" + shift 2 + ;; + -i|--ip) + IP_ADDRESS="$2" + shift 2 + ;; + -v|--valid) + DAYS_VALID="$2" + shift 2 + ;; + --help) + show_help + exit 0 + ;; + *) + echo -e "${RED}Unbekannte Option: $1${NC}" + show_help + exit 1 + ;; + esac +done + +# Header anzeigen +print_header + +# Verzeichnis erstellen, falls es nicht existiert +if [ ! -d "$CERT_DIR" ]; then + echo -e "${YELLOW}Erstelle Verzeichnis $CERT_DIR...${NC}" + mkdir -p "$CERT_DIR" +fi + +# Überprüfen, ob openssl installiert ist +if ! command -v openssl &> /dev/null; then + echo -e "${RED}OpenSSL ist nicht installiert!${NC}" + echo -e "${YELLOW}Installiere OpenSSL...${NC}" + apt-get update && apt-get install -y openssl +fi + +# Zertifikat erstellen +echo -e "${GREEN}Erstelle selbstsigniertes SSL-Zertifikat...${NC}" +echo -e "${BLUE}Hostname: ${NC}$HOSTNAME" +echo -e "${BLUE}IP-Adresse: ${NC}$IP_ADDRESS" +echo -e "${BLUE}Gültigkeitsdauer: ${NC}$DAYS_VALID Tage" +echo -e "${BLUE}Zertifikatsdatei: ${NC}$CERT_FILE" +echo -e "${BLUE}Schlüsseldatei: ${NC}$KEY_FILE" +echo "" + +# OpenSSL-Konfiguration erstellen +CONFIG_FILE="$CERT_DIR/openssl.cnf" +cat > "$CONFIG_FILE" << EOF +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = req_distinguished_name +x509_extensions = v3_req + +[req_distinguished_name] +C = DE +ST = Baden-Wuerttemberg +L = Stuttgart +O = Mercedes-Benz AG +OU = MYP Platform +CN = $HOSTNAME + +[v3_req] +keyUsage = critical, digitalSignature, keyAgreement +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = $HOSTNAME +DNS.2 = localhost +IP.1 = $IP_ADDRESS +IP.2 = 127.0.0.1 +EOF + +# Schlüssel und Zertifikat generieren +openssl req -x509 -nodes -days "$DAYS_VALID" -newkey rsa:2048 \ + -keyout "$KEY_FILE" -out "$CERT_FILE" \ + -config "$CONFIG_FILE" + +# Berechtigungen setzen +chmod 600 "$KEY_FILE" +chmod 644 "$CERT_FILE" + +echo "" +echo -e "${GREEN}SSL-Zertifikat erfolgreich erstellt!${NC}" +echo -e "${YELLOW}Fingerprint:${NC}" +openssl x509 -noout -fingerprint -sha256 -in "$CERT_FILE" +echo "" +echo -e "${BLUE}Um diese Zertifikate mit Flask zu verwenden:${NC}" +echo " 1. Importiere die SSL-Einstellungen in der app.py" +echo " 2. Starte Flask mit SSL-Unterstützung" +echo "" +echo -e "${YELLOW}Beispiel:${NC}" +echo " app.run(host='0.0.0.0', port=5000, ssl_context=('$CERT_FILE', '$KEY_FILE'))" +echo "" \ No newline at end of file diff --git a/backend/install/kiosk.sh b/backend/install/kiosk.sh index 96baf297..c82a7ebf 100755 --- a/backend/install/kiosk.sh +++ b/backend/install/kiosk.sh @@ -14,6 +14,11 @@ sed -i 's/"exited_cleanly":false/"exited_cleanly":true/' \ sed -i 's/"exit_type":"Crashed"/"exit_type":"Normal"/' \ "$HOME/.config/chromium/Default/Preferences" 2>/dev/null || true -# Browser starten +# Hostname und IP ermitteln +HOSTNAME=$(hostname -f) +IP_ADDRESS=$(hostname -I | awk '{print $1}') + +# Browser starten mit SSL-Warnung deaktiviert chromium-browser --kiosk --noerrdialogs --disable-infobars \ - --window-position=0,0 --app=http://localhost:5000/ & \ No newline at end of file + --window-position=0,0 --ignore-certificate-errors \ + --app=https://${IP_ADDRESS}:5000/ & \ No newline at end of file diff --git a/backend/install/ssl_check.sh b/backend/install/ssl_check.sh new file mode 100755 index 00000000..8f7a21b2 --- /dev/null +++ b/backend/install/ssl_check.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# MYP SSL-Zertifikat-Prüfskript +# Prüft den Status der SSL-Zertifikate und gibt Informationen aus + +# Fehlerabbruch aktivieren +set -e + +# Farben für bessere Lesbarkeit +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Standardwerte +CERT_PATH="/opt/myp/ssl/myp.crt" +KEY_PATH="/opt/myp/ssl/myp.key" + +# Argumente verarbeiten +while [[ $# -gt 0 ]]; do + case $1 in + -c|--cert) + CERT_PATH="$2" + shift 2 + ;; + -k|--key) + KEY_PATH="$2" + shift 2 + ;; + *) + echo -e "${RED}Unbekannte Option: $1${NC}" + exit 1 + ;; + esac +done + +# Header anzeigen +echo -e "${BLUE}================================================================${NC}" +echo -e "${BLUE} MYP V2 - SSL-Zertifikat-Statusprüfung ${NC}" +echo -e "${BLUE}================================================================${NC}" +echo "" + +# Prüfen, ob OpenSSL installiert ist +if ! command -v openssl &> /dev/null; then + echo -e "${RED}OpenSSL ist nicht installiert!${NC}" + exit 1 +fi + +# Prüfen, ob Zertifikat existiert +if [ ! -f "$CERT_PATH" ]; then + echo -e "${RED}Zertifikat nicht gefunden: $CERT_PATH${NC}" + echo -e "${YELLOW}Führen Sie 'create_ssl_cert.sh' aus, um ein neues Zertifikat zu erstellen.${NC}" + exit 1 +fi + +# Prüfen, ob Schlüssel existiert +if [ ! -f "$KEY_PATH" ]; then + echo -e "${RED}Schlüssel nicht gefunden: $KEY_PATH${NC}" + echo -e "${YELLOW}Führen Sie 'create_ssl_cert.sh' aus, um einen neuen Schlüssel zu erstellen.${NC}" + exit 1 +fi + +# Zertifikatsinformationen anzeigen +echo -e "${GREEN}Zertifikatsinformationen:${NC}" +echo -e "${BLUE}Zertifikatsdatei: ${NC}$CERT_PATH" +echo -e "${BLUE}Schlüsseldatei: ${NC}$KEY_PATH" +echo "" + +# Zertifikatsdetails anzeigen +echo -e "${YELLOW}Zertifikatsdetails:${NC}" +openssl x509 -in "$CERT_PATH" -noout -subject -issuer -dates -fingerprint -sha256 + +# Gültigkeit prüfen +echo "" +echo -e "${YELLOW}Gültigkeitsprüfung:${NC}" +not_after=$(openssl x509 -in "$CERT_PATH" -noout -enddate | cut -d= -f2) +not_after_seconds=$(date -d "$not_after" +%s) +now_seconds=$(date +%s) +days_left=$(( (not_after_seconds - now_seconds) / 86400 )) + +if [ $days_left -le 0 ]; then + echo -e "${RED}Zertifikat ist ABGELAUFEN!${NC}" +elif [ $days_left -le 30 ]; then + echo -e "${YELLOW}Zertifikat läuft in $days_left Tagen ab!${NC}" +else + echo -e "${GREEN}Zertifikat ist noch $days_left Tage gültig.${NC}" +fi + +# Zertifikatsinhalte prüfen +echo "" +echo -e "${YELLOW}Zertifikatsinhalte:${NC}" +echo -e "${BLUE}Alternative Namen (SAN):${NC}" +openssl x509 -in "$CERT_PATH" -noout -text | grep -A1 "Subject Alternative Name" + +# Abschluss +echo "" +echo -e "${GREEN}SSL-Prüfung abgeschlossen.${NC}" +echo -e "${BLUE}Um die Zertifikate zu erneuern, führen Sie 'create_ssl_cert.sh' aus.${NC}" +echo "" \ No newline at end of file diff --git a/backend/setup_myp.sh b/backend/setup_myp.sh index d5e9976e..4c193723 100755 --- a/backend/setup_myp.sh +++ b/backend/setup_myp.sh @@ -61,6 +61,7 @@ show_main_menu() { echo "7) MYP-Dienst starten/stoppen/neustarten" echo "8) Logs anzeigen" echo "9) Dokumentation anzeigen" + echo "10) SSL-Zertifikat-Management" echo "" echo "q) Beenden" echo "" @@ -98,6 +99,9 @@ process_main_menu() { 9) show_documentation ;; + 10) + show_ssl_management + ;; q|Q) echo -e "${GREEN}Auf Wiedersehen!${NC}" exit 0 @@ -131,7 +135,7 @@ standard_installation() { echo "Installiere System-Abhängigkeiten..." apt update apt install -y python3.11 python3.11-pip python3.11-venv python3.11-dev \ - build-essential git curl + build-essential git curl openssl # Verzeichnis für MYP erstellen/aktualisieren mkdir -p /opt/myp @@ -156,6 +160,9 @@ standard_installation() { # Datenbank-Verzeichnis erstellen mkdir -p /opt/myp/data + # SSL-Verzeichnis erstellen + mkdir -p /opt/myp/ssl + # Python-Umgebung und Abhängigkeiten einrichten echo "Richte Python-Umgebung ein..." cd /opt/myp @@ -166,12 +173,18 @@ standard_installation() { pip install --upgrade pip pip install -r requirements.txt + # SSL-Zertifikate erstellen + echo "Erstelle SSL-Zertifikate..." + chmod +x /opt/myp/install/create_ssl_cert.sh + /opt/myp/install/create_ssl_cert.sh -d /opt/myp/ssl + # Berechtigungen setzen echo "Setze Berechtigungen..." chown -R www-data:www-data /opt/myp chmod -R 755 /opt/myp chmod -R 775 /opt/myp/logs chmod -R 775 /opt/myp/data + chmod 600 /opt/myp/ssl/myp.key echo -e "${GREEN}Installation abgeschlossen.${NC}" echo "" @@ -181,6 +194,7 @@ standard_installation() { echo " cd /opt/myp && source .venv/bin/activate && python3.11 app/app.py" echo "" echo -e "${BLUE}Oder verwenden Sie Option 7 für Dienst-Management${NC}" + echo -e "${GREEN}MYP V2 ist unter https://$(hostname -I | awk '{print $1}'):5000 erreichbar${NC}" read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..." show_main_menu @@ -1267,40 +1281,61 @@ system_status() { echo -e "${GREEN}System-Status${NC}" echo "" - # MYP-Status + # Systeminfos anzeigen + echo -e "${YELLOW}Systeminformationen:${NC}" + echo -e "Hostname: $(hostname)" + echo -e "IP-Adresse: $(hostname -I | awk '{print $1}')" + echo -e "Betriebssystem: $(lsb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om)" + echo -e "Kernel: $(uname -r)" + echo -e "CPU: $(grep -c ^processor /proc/cpuinfo) Kerne" + echo -e "RAM: $(free -h | awk '/^Mem/ {print $2}')" + echo -e "Festplatte: $(df -h / | awk 'NR==2 {print $2}')" + echo "" + + # MYP-Status anzeigen echo -e "${YELLOW}MYP-Status:${NC}" if is_myp_installed; then - echo "MYP ist installiert in /opt/myp" - if systemctl is-active --quiet myp.service; then - echo -e "MYP-Dienst: ${GREEN}Aktiv${NC}" + echo -e "MYP ist installiert: ${GREEN}Ja${NC}" + + # Prüfen, ob der MYP-Service läuft + if systemctl is-active --quiet myp.service 2>/dev/null; then + echo -e "MYP-Service: ${GREEN}Aktiv${NC}" else - echo -e "MYP-Dienst: ${RED}Inaktiv${NC}" + echo -e "MYP-Service: ${RED}Inaktiv${NC}" fi + + # Pfadangaben + echo -e "Installationspfad: /opt/myp" + echo -e "Datenbank: /opt/myp/data" + echo -e "Logs: /opt/myp/logs" + echo -e "SSL-Zertifikate: /opt/myp/ssl" + + # SSL-Status überprüfen + if [ -f "/opt/myp/ssl/myp.crt" ] && [ -f "/opt/myp/ssl/myp.key" ]; then + echo -e "SSL-Zertifikate: ${GREEN}Vorhanden${NC}" + + # Zertifikatsinformationen anzeigen + if command -v openssl &> /dev/null; then + cert_expiry=$(openssl x509 -enddate -noout -in /opt/myp/ssl/myp.crt | cut -d= -f 2) + cert_subject=$(openssl x509 -subject -noout -in /opt/myp/ssl/myp.crt | sed 's/^subject=//') + echo -e "Zertifikat für: ${BLUE}$cert_subject${NC}" + echo -e "Gültig bis: ${BLUE}$cert_expiry${NC}" + fi + else + echo -e "SSL-Zertifikate: ${RED}Fehlen${NC}" + fi + + # MYP-URLs anzeigen + echo -e "" + echo -e "${YELLOW}MYP-Zugriff:${NC}" + ip_address=$(hostname -I | awk '{print $1}') + echo -e "${GREEN}https://$ip_address:5000${NC} (verschlüsselt)" + echo -e "${YELLOW}http://$ip_address:5000${NC} (unverschlüsselt)" else - echo -e "MYP ist ${RED}nicht installiert${NC}" + echo -e "MYP ist installiert: ${RED}Nein${NC}" fi - echo "" - # Netzwerkstatus - echo -e "${YELLOW}Netzwerkstatus:${NC}" - ip -o addr show | awk '$3 == "inet" {print $2 ": " $4}' echo "" - - # DNS-Server - echo -e "${YELLOW}DNS-Server:${NC}" - grep "nameserver" /etc/resolv.conf 2>/dev/null || echo "Keine DNS-Server konfiguriert." - echo "" - - # Systemressourcen - echo -e "${YELLOW}Systemressourcen:${NC}" - echo "CPU-Auslastung:" - top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4 "% genutzt"}' - echo "Speichernutzung:" - free -h | grep "Mem:" | awk '{print $3 " von " $2 " genutzt"}' - echo "Festplattenbelegung:" - df -h / | grep -v "Filesystem" | awk '{print $3 " von " $2 " genutzt (" $5 ")"}' - echo "" - read -p "Drücken Sie eine Taste, um zum Hauptmenü zurückzukehren..." show_main_menu } @@ -1491,6 +1526,57 @@ show_documentation() { show_main_menu } +# 10) SSL-Zertifikat-Management +show_ssl_management() { + print_header + echo -e "${GREEN}SSL-Zertifikat-Management${NC}" + echo "" + + echo -e "Bitte wählen Sie eine Option:" + echo "" + echo "1) SSL-Zertifikatsstatus anzeigen" + echo "2) Neue SSL-Zertifikate erstellen" + echo "3) SSL-Einstellungen in settings.py anzeigen/bearbeiten" + echo "" + echo "b) Zurück zum Hauptmenü" + echo "" + read -p "Ihre Auswahl: " ssl_option + + case $ssl_option in + 1) + # SSL-Status anzeigen + chmod +x /opt/myp/install/ssl_check.sh + /opt/myp/install/ssl_check.sh + ;; + 2) + # Neue Zertifikate erstellen + echo -e "${YELLOW}Erstelle neue SSL-Zertifikate...${NC}" + chmod +x /opt/myp/install/create_ssl_cert.sh + /opt/myp/install/create_ssl_cert.sh -d /opt/myp/ssl + ;; + 3) + # SSL-Einstellungen anzeigen/bearbeiten + if command -v nano &> /dev/null; then + nano /opt/myp/app/config/settings.py + else + vi /opt/myp/app/config/settings.py + fi + ;; + b|B) + show_main_menu + return + ;; + *) + echo -e "${RED}Ungültige Option.${NC}" + sleep 2 + show_ssl_management + ;; + esac + + read -p "Drücken Sie eine Taste, um zum SSL-Menü zurückzukehren..." + show_ssl_management +} + # Hauptprogramm check_root show_main_menu \ No newline at end of file diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index a53ad9fa..5f40b97c 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -27,12 +27,12 @@ const nextConfig = { return [ { source: '/api/backend/:path*', - destination: 'http://192.168.0.105:5000/api/:path*', + destination: 'https://192.168.0.105:5000/api/:path*', }, // Direkter Proxy für Health-Checks { source: '/backend-health', - destination: 'http://192.168.0.105:5000/health', + destination: 'https://192.168.0.105:5000/health', }, ]; }, diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 06d5c381..d62883f2 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import { Header } from "@/components/header"; import { Toaster } from "@/components/ui/toaster"; import type { Metadata } from "next"; +import { SSLWarning } from "@/components/ui/ssl-warning"; import "@/app/globals.css"; @@ -26,6 +27,7 @@ export default function RootLayout(props: RootLayoutProps) {
+
{children}
diff --git a/frontend/src/components/ui/ssl-warning.tsx b/frontend/src/components/ui/ssl-warning.tsx new file mode 100644 index 00000000..a7a3c929 --- /dev/null +++ b/frontend/src/components/ui/ssl-warning.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { API_BASE_URL } from "@/utils/api-config"; + +/** + * SSLWarning - Zeigt eine Warnung für selbstsignierte SSL-Zertifikate an + * und bietet eine Möglichkeit, diese zu akzeptieren + */ +export function SSLWarning() { + const [showWarning, setShowWarning] = useState(false); + + useEffect(() => { + // Prüfe, ob die Warnung angezeigt werden soll + const hasSeenWarning = localStorage.getItem("ssl-warning-dismissed"); + + // Zeige die Warnung nur an, wenn der Benutzer sie noch nicht gesehen hat + if (!hasSeenWarning) { + setShowWarning(true); + } + }, []); + + // Teste die Backend-Verbindung und prüfe auf Zertifikatsprobleme + useEffect(() => { + if (!showWarning) return; + + const testConnection = async () => { + try { + // Teste HTTPS-Verbindung + await fetch(`${API_BASE_URL}/health`, { + method: 'GET', + mode: 'no-cors' // Verwende no-cors für SSL-Tests + }); + // Bei erfolgreicher Verbindung: Blende die Warnung aus + setShowWarning(false); + } catch (error) { + console.warn("SSL-Verbindungstest fehlgeschlagen:", error); + // Bei Fehlern: Zeige die Warnung an + setShowWarning(true); + } + }; + + testConnection(); + }, [showWarning]); + + const dismissWarning = () => { + localStorage.setItem("ssl-warning-dismissed", "true"); + setShowWarning(false); + }; + + const openBackendDirectly = () => { + // Öffne das Backend direkt in einem neuen Tab + window.open(API_BASE_URL, "_blank"); + }; + + if (!showWarning) { + return null; + } + + return ( +
+
+
+

SSL-Sicherheitshinweis

+

+ Diese Anwendung verwendet ein selbstsigniertes SSL-Zertifikat für sichere Kommunikation. + Um Verbindungsprobleme zu vermeiden, öffnen Sie bitte einmalig die folgende URL und akzeptieren Sie das Zertifikat: +

+ +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/utils/api-config.ts b/frontend/src/utils/api-config.ts index edc19057..071fc069 100644 --- a/frontend/src/utils/api-config.ts +++ b/frontend/src/utils/api-config.ts @@ -1,5 +1,5 @@ // Basis-URL für Backend-API -export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://192.168.0.105:5000"; +export const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || "https://192.168.0.105:5000"; // Frontend-URL für Callbacks - unterstützt mehrere Domains const getFrontendUrl = () => { @@ -15,12 +15,12 @@ const getFrontendUrl = () => { if (hostname === 'm040tbaraspi001' || hostname === 'm040tbaraspi001.de040.corpintra.net' || hostname.includes('corpintra.net')) { - return `http://${hostname}`; + return `https://${hostname}`; } } // Priorität 3: Default für Localhost - return "http://localhost:3000"; + return "https://localhost:3000"; }; export const FRONTEND_URL = getFrontendUrl(); @@ -37,6 +37,23 @@ export const ALLOWED_CALLBACK_HOSTS = [ '192.168.0.105' ]; +// Funktion zum Testen der SSL-Verbindung +export const testSSLConnection = async (): Promise => { + try { + // Führe einen einfachen GET-Request zum Backend aus + const response = await fetch(`${API_BASE_URL}/health`, { + method: 'GET', + mode: 'no-cors', // Keine CORS-Fehler erzeugen + }); + + // Wenn kein Fehler auftritt, ist die SSL-Verbindung erfolgreich + return true; + } catch (error) { + console.warn('SSL-Verbindungstest fehlgeschlagen:', error); + return false; + } +}; + // Endpunkte für die verschiedenen Ressourcen export const API_ENDPOINTS = { PRINTERS: `${API_BASE_URL}/api/printers`, diff --git a/frontend/src/utils/api-helper.ts b/frontend/src/utils/api-helper.ts new file mode 100644 index 00000000..9a0eed15 --- /dev/null +++ b/frontend/src/utils/api-helper.ts @@ -0,0 +1,75 @@ +import { ExternalAPI } from './external-api'; + +// Typdefinitionen für API-Responses +export interface Printer { + id: string; + name: string; + ip: string; + status: string; + is_enabled: boolean; +} + +export interface Job { + id: string; + printer_id: string; + user_id: string; + start_time: string; + end_time: string; + status: string; +} + +// Instanz der ExternalAPI erstellen +const externalAPI = new ExternalAPI(); + +// Fetcher für SWR mit Fehlerbehandlung und HTTPS-Unterstützung +const fetchWithErrorHandling = async (endpoint: string) => { + try { + return await externalAPI.fetch(endpoint); + } catch (error) { + console.error('API-Fehler:', error); + throw new Error('Ein Fehler ist bei der API-Anfrage aufgetreten'); + } +}; + +// API-Funktionen, die mit der bisherigen API-Struktur kompatibel sind +export const api = { + // Drucker-Endpunkte + printers: { + getAll: () => fetchWithErrorHandling('/api/printers'), + getById: (id: string) => fetchWithErrorHandling(`/api/printers/${id}`), + create: (data: Partial) => + externalAPI.fetch('/api/printers', { + method: 'POST', + body: JSON.stringify(data), + }), + update: (id: string, data: Partial) => + externalAPI.fetch(`/api/printers/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }), + delete: (id: string) => + externalAPI.fetch(`/api/printers/${id}`, { + method: 'DELETE', + }), + }, + + // Jobs-Endpunkte + jobs: { + getAll: () => fetchWithErrorHandling('/api/jobs'), + getById: (id: string) => fetchWithErrorHandling(`/api/jobs/${id}`), + create: (data: Partial) => + externalAPI.fetch('/api/jobs', { + method: 'POST', + body: JSON.stringify(data), + }), + update: (id: string, data: Partial) => + externalAPI.fetch(`/api/jobs/${id}`, { + method: 'PUT', + body: JSON.stringify(data), + }), + delete: (id: string) => + externalAPI.fetch(`/api/jobs/${id}`, { + method: 'DELETE', + }), + }, +}; \ No newline at end of file diff --git a/frontend/src/utils/external-api.ts b/frontend/src/utils/external-api.ts index 5ed9ff20..aadb17a5 100644 --- a/frontend/src/utils/external-api.ts +++ b/frontend/src/utils/external-api.ts @@ -1,78 +1,57 @@ -import { API_ENDPOINTS } from './api-config'; +import { API_BASE_URL } from './api-config'; -// Typdefinitionen für API-Responses -export interface Printer { - id: string; - name: string; - ip: string; - status: string; - is_enabled: boolean; -} +/** + * ExternalAPI - Wrapper für externe API-Aufrufe mit HTTPS-Unterstützung + * Enthält Logik für Offline-Fallback und Behandlung von selbstsignierten Zertifikaten + */ +export class ExternalAPI { + private baseURL: string; -export interface Job { - id: string; - printer_id: string; - user_id: string; - start_time: string; - end_time: string; - status: string; -} - -// Fetcher für SWR mit Fehlerbehandlung -const fetchWithErrorHandling = async (url: string) => { - const response = await fetch(url); - - if (!response.ok) { - const error = new Error('Ein Fehler ist bei der API-Anfrage aufgetreten'); - throw error; + constructor() { + this.baseURL = API_BASE_URL; } - - return response.json(); -}; -// API-Funktionen -export const api = { - // Drucker-Endpunkte - printers: { - getAll: () => fetchWithErrorHandling(API_ENDPOINTS.PRINTERS), - getById: (id: string) => fetchWithErrorHandling(`${API_ENDPOINTS.PRINTERS}/${id}`), - create: (data: Partial) => - fetch(API_ENDPOINTS.PRINTERS, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then(res => res.json()), - update: (id: string, data: Partial) => - fetch(`${API_ENDPOINTS.PRINTERS}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then(res => res.json()), - delete: (id: string) => - fetch(`${API_ENDPOINTS.PRINTERS}/${id}`, { - method: 'DELETE', - }).then(res => res.json()), - }, - - // Jobs-Endpunkte - jobs: { - getAll: () => fetchWithErrorHandling(API_ENDPOINTS.JOBS), - getById: (id: string) => fetchWithErrorHandling(`${API_ENDPOINTS.JOBS}/${id}`), - create: (data: Partial) => - fetch(API_ENDPOINTS.JOBS, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then(res => res.json()), - update: (id: string, data: Partial) => - fetch(`${API_ENDPOINTS.JOBS}/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }).then(res => res.json()), - delete: (id: string) => - fetch(`${API_ENDPOINTS.JOBS}/${id}`, { - method: 'DELETE', - }).then(res => res.json()), - }, -}; \ No newline at end of file + /** + * Führt einen API-Request durch mit Unterstützung für selbstsignierte Zertifikate + * im Entwicklungsmodus + */ + async fetch(endpoint: string, options: RequestInit = {}): Promise { + const url = `${this.baseURL}${endpoint}`; + + try { + const response = await fetch(url, { + ...options, + headers: { + 'Content-Type': 'application/json', + ...options.headers, + }, + // Im Browser werden selbstsignierte Zertifikate über die Browser-Einstellungen akzeptiert + // Die fetch API im Browser hat keine Option zum Ignorieren von Zertifikaten + }); + + if (!response.ok) { + throw new Error(`API Error: ${response.status}`); + } + + return await response.json(); + } catch (error) { + console.error('API Fehler:', error); + + // Prüfen auf Zertifikatsfehler + if (error instanceof Error && error.message.includes('certificate')) { + console.warn('Zertifikatsfehler: Bitte akzeptieren Sie das selbstsignierte Zertifikat manuell, indem Sie direkt auf https://192.168.0.105:5000 zugreifen und es bestätigen.'); + } + + throw error; + } + } + + // API-Methoden für verschiedene Endpoints + async getPrinters() { + return this.fetch('/api/printers'); + } + + async getJobs() { + return this.fetch('/api/jobs'); + } +} \ No newline at end of file