diff --git a/AppImage/components/security.tsx b/AppImage/components/security.tsx index 6d692db8..f9adb354 100644 --- a/AppImage/components/security.tsx +++ b/AppImage/components/security.tsx @@ -174,6 +174,7 @@ export function Security() { const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null) const [loadingSsl, setLoadingSsl] = useState(true) const [configuringSsl, setConfiguringSsl] = useState(false) + const [sslRestarting, setSslRestarting] = useState(false) const [showCustomCertForm, setShowCustomCertForm] = useState(false) const [customCertPath, setCustomCertPath] = useState("") const [customKeyPath, setCustomKeyPath] = useState("") @@ -1256,13 +1257,49 @@ ${(report.sections && report.sections.length > 0) ? ` } } + // Wait for the monitor service to come back on the new protocol, then redirect + const waitForServiceAndRedirect = async (newProtocol: "https" | "http") => { + const host = window.location.hostname + const port = window.location.port || "8008" + const newUrl = `${newProtocol}://${host}:${port}${window.location.pathname}` + + // Wait for service to restart (try up to 30 seconds) + const maxAttempts = 15 + for (let i = 0; i < maxAttempts; i++) { + await new Promise(r => setTimeout(r, 2000)) + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 3000) + const resp = await fetch(`${newProtocol}://${host}:${port}/api/ssl/status`, { + signal: controller.signal, + // For self-signed certs, we need to handle rejection + mode: "no-cors" + }).catch(() => null) + clearTimeout(timeout) + + // For HTTPS with self-signed certs, even a failed CORS request means the server is up + if (resp || newProtocol === "https") { + // Give it one more second to fully stabilize + await new Promise(r => setTimeout(r, 1000)) + window.location.href = newUrl + return + } + } catch { + // Server not ready yet, keep waiting + } + } + + // Fallback: redirect anyway after timeout + window.location.href = newUrl + } + const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => { setConfiguringSsl(true) setError("") setSuccess("") try { - const body: Record = { source } + const body: Record = { source, auto_restart: true } if (source === "custom" && certPath && keyPath) { body.cert_path = certPath body.key_path = keyPath @@ -1275,25 +1312,27 @@ ${(report.sections && report.sections.length > 0) ? ` }) if (data.success) { - setSuccess(data.message || "SSL configured successfully. Restart the monitor service to apply.") setSslEnabled(true) setSslSource(source) setShowCustomCertForm(false) setCustomCertPath("") setCustomKeyPath("") - loadSslStatus() + setConfiguringSsl(false) + setSslRestarting(true) + setSuccess("SSL enabled. Restarting service and switching to HTTPS...") + await waitForServiceAndRedirect("https") } else { setError(data.message || "Failed to configure SSL") + setConfiguringSsl(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to configure SSL") - } finally { setConfiguringSsl(false) } } const handleDisableSsl = async () => { - if (!confirm("Are you sure you want to disable HTTPS? The monitor will revert to HTTP after restart.")) { + if (!confirm("Are you sure you want to disable HTTPS? The monitor will switch to HTTP.")) { return } @@ -1302,21 +1341,27 @@ ${(report.sections && report.sections.length > 0) ? ` setSuccess("") try { - const data = await fetchApi("/api/ssl/disable", { method: "POST" }) + const data = await fetchApi("/api/ssl/disable", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ auto_restart: true }), + }) if (data.success) { - setSuccess(data.message || "SSL disabled. Restart the monitor service to apply.") setSslEnabled(false) setSslSource("none") setSslCertPath("") setSslKeyPath("") - loadSslStatus() + setConfiguringSsl(false) + setSslRestarting(true) + setSuccess("SSL disabled. Restarting service and switching to HTTP...") + await waitForServiceAndRedirect("http") } else { setError(data.message || "Failed to disable SSL") + setConfiguringSsl(false) } } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable SSL") - } finally { setConfiguringSsl(false) } } @@ -1683,10 +1728,10 @@ ${(report.sections && report.sections.length > 0) ? ` onClick={handleDisableSsl} variant="outline" size="sm" - disabled={configuringSsl} + disabled={configuringSsl || sslRestarting} className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent" > - {configuringSsl ? "Disabling..." : "Disable HTTPS"} + {configuringSsl ? "Disabling..." : sslRestarting ? "Restarting..." : "Disable HTTPS"} )} @@ -1722,7 +1767,7 @@ ${(report.sections && report.sections.length > 0) ? ` @@ -1817,14 +1862,27 @@ ${(report.sections && report.sections.length > 0) ? ` )} - {/* Info note about restart */} -
- -

- Changes to SSL configuration require a monitor service restart to take effect. - The service will automatically use HTTPS on port 8008 when enabled. -

-
+ {/* Restarting overlay or info note */} + {sslRestarting ? ( +
+
+
+

+ Restarting monitor service... +

+

+ The page will automatically redirect to the new address. +

+
+
+ ) : ( +
+ +

+ SSL changes will automatically restart the monitor service and redirect to the new address. +

+
+ )} )} diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index c7a8c7f3..5a40d6df 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -4,6 +4,11 @@ Provides REST API endpoints for authentication management """ import logging +import os +import signal +import sys +import threading +import time from flask import Blueprint, jsonify, request import auth_manager import jwt @@ -73,12 +78,29 @@ def ssl_status(): return jsonify({"success": False, "message": str(e)}), 500 +def _schedule_service_restart(delay=1.5): + """Schedule a self-restart of the Flask server after a short delay. + This gives time for the HTTP response to reach the client before the process exits. + The process will be restarted by the parent (systemd, AppRun, or manual).""" + def _do_restart(): + time.sleep(delay) + print("[ProxMenux] Restarting monitor service to apply SSL changes...") + # Send SIGTERM to our own process - this triggers a clean shutdown. + # If running under systemd with Restart=always, it will auto-restart. + # If running directly, the process exits (user must restart manually as fallback). + os.kill(os.getpid(), signal.SIGTERM) + + t = threading.Thread(target=_do_restart, daemon=True) + t.start() + + @auth_bp.route('/api/ssl/configure', methods=['POST']) def ssl_configure(): """Configure SSL with Proxmox or custom certificates""" try: data = request.json or {} source = data.get("source", "proxmox") + auto_restart = data.get("auto_restart", True) if source == "proxmox": cert_path = auth_manager.PROXMOX_CERT_PATH @@ -92,7 +114,14 @@ def ssl_configure(): success, message = auth_manager.configure_ssl(cert_path, key_path, source) if success: - return jsonify({"success": True, "message": message, "requires_restart": True}) + if auto_restart: + _schedule_service_restart() + return jsonify({ + "success": True, + "message": "SSL enabled. The service is restarting...", + "restarting": auto_restart, + "new_protocol": "https" + }) else: return jsonify({"success": False, "message": message}), 400 except Exception as e: @@ -103,10 +132,20 @@ def ssl_configure(): def ssl_disable(): """Disable SSL and return to HTTP""" try: + data = request.json or {} + auto_restart = data.get("auto_restart", True) + success, message = auth_manager.disable_ssl() if success: - return jsonify({"success": True, "message": message, "requires_restart": True}) + if auto_restart: + _schedule_service_restart() + return jsonify({ + "success": True, + "message": "SSL disabled. The service is restarting...", + "restarting": auto_restart, + "new_protocol": "http" + }) else: return jsonify({"success": False, "message": message}), 400 except Exception as e: