Update security.tsx

This commit is contained in:
MacRimi
2026-02-14 12:07:51 +01:00
parent 782eaef440
commit 6647a3b083
2 changed files with 122 additions and 25 deletions

View File

@@ -174,6 +174,7 @@ export function Security() {
const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null) const [proxmoxCertInfo, setProxmoxCertInfo] = useState<{subject?: string; expires?: string; issuer?: string; is_self_signed?: boolean} | null>(null)
const [loadingSsl, setLoadingSsl] = useState(true) const [loadingSsl, setLoadingSsl] = useState(true)
const [configuringSsl, setConfiguringSsl] = useState(false) const [configuringSsl, setConfiguringSsl] = useState(false)
const [sslRestarting, setSslRestarting] = useState(false)
const [showCustomCertForm, setShowCustomCertForm] = useState(false) const [showCustomCertForm, setShowCustomCertForm] = useState(false)
const [customCertPath, setCustomCertPath] = useState("") const [customCertPath, setCustomCertPath] = useState("")
const [customKeyPath, setCustomKeyPath] = 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) => { const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => {
setConfiguringSsl(true) setConfiguringSsl(true)
setError("") setError("")
setSuccess("") setSuccess("")
try { try {
const body: Record<string, string> = { source } const body: Record<string, string | boolean> = { source, auto_restart: true }
if (source === "custom" && certPath && keyPath) { if (source === "custom" && certPath && keyPath) {
body.cert_path = certPath body.cert_path = certPath
body.key_path = keyPath body.key_path = keyPath
@@ -1275,25 +1312,27 @@ ${(report.sections && report.sections.length > 0) ? `
}) })
if (data.success) { if (data.success) {
setSuccess(data.message || "SSL configured successfully. Restart the monitor service to apply.")
setSslEnabled(true) setSslEnabled(true)
setSslSource(source) setSslSource(source)
setShowCustomCertForm(false) setShowCustomCertForm(false)
setCustomCertPath("") setCustomCertPath("")
setCustomKeyPath("") setCustomKeyPath("")
loadSslStatus() setConfiguringSsl(false)
setSslRestarting(true)
setSuccess("SSL enabled. Restarting service and switching to HTTPS...")
await waitForServiceAndRedirect("https")
} else { } else {
setError(data.message || "Failed to configure SSL") setError(data.message || "Failed to configure SSL")
setConfiguringSsl(false)
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to configure SSL") setError(err instanceof Error ? err.message : "Failed to configure SSL")
} finally {
setConfiguringSsl(false) setConfiguringSsl(false)
} }
} }
const handleDisableSsl = async () => { 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 return
} }
@@ -1302,21 +1341,27 @@ ${(report.sections && report.sections.length > 0) ? `
setSuccess("") setSuccess("")
try { 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) { if (data.success) {
setSuccess(data.message || "SSL disabled. Restart the monitor service to apply.")
setSslEnabled(false) setSslEnabled(false)
setSslSource("none") setSslSource("none")
setSslCertPath("") setSslCertPath("")
setSslKeyPath("") setSslKeyPath("")
loadSslStatus() setConfiguringSsl(false)
setSslRestarting(true)
setSuccess("SSL disabled. Restarting service and switching to HTTP...")
await waitForServiceAndRedirect("http")
} else { } else {
setError(data.message || "Failed to disable SSL") setError(data.message || "Failed to disable SSL")
setConfiguringSsl(false)
} }
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to disable SSL") setError(err instanceof Error ? err.message : "Failed to disable SSL")
} finally {
setConfiguringSsl(false) setConfiguringSsl(false)
} }
} }
@@ -1683,10 +1728,10 @@ ${(report.sections && report.sections.length > 0) ? `
onClick={handleDisableSsl} onClick={handleDisableSsl}
variant="outline" variant="outline"
size="sm" size="sm"
disabled={configuringSsl} disabled={configuringSsl || sslRestarting}
className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent" 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"}
</Button> </Button>
</div> </div>
)} )}
@@ -1722,7 +1767,7 @@ ${(report.sections && report.sections.length > 0) ? `
<Button <Button
onClick={() => handleEnableSsl("proxmox")} onClick={() => handleEnableSsl("proxmox")}
className="w-full bg-green-600 hover:bg-green-700 text-white" className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={configuringSsl} disabled={configuringSsl || sslRestarting}
> >
{configuringSsl ? ( {configuringSsl ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -1793,9 +1838,9 @@ ${(report.sections && report.sections.length > 0) ? `
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)} onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)}
className="flex-1 bg-green-600 hover:bg-green-700 text-white" className="flex-1 bg-green-600 hover:bg-green-700 text-white"
disabled={configuringSsl || !customCertPath || !customKeyPath} disabled={configuringSsl || sslRestarting || !customCertPath || !customKeyPath}
> >
{configuringSsl ? "Configuring..." : "Enable HTTPS"} {configuringSsl ? "Configuring..." : "Enable HTTPS"}
</Button> </Button>
@@ -1817,14 +1862,27 @@ ${(report.sections && report.sections.length > 0) ? `
</div> </div>
)} )}
{/* Info note about restart */} {/* Restarting overlay or info note */}
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2"> {sslRestarting ? (
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" /> <div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 flex items-center gap-3">
<p className="text-sm text-blue-500"> <div className="h-5 w-5 border-2 border-amber-500 border-t-transparent rounded-full animate-spin flex-shrink-0" />
Changes to SSL configuration require a monitor service restart to take effect. <div>
The service will automatically use HTTPS on port 8008 when enabled. <p className="text-sm font-medium text-amber-500">
</p> Restarting monitor service...
</div> </p>
<p className="text-xs text-amber-400 mt-0.5">
The page will automatically redirect to the new address.
</p>
</div>
</div>
) : (
<div className="bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 flex items-start gap-2">
<Info className="h-5 w-5 text-blue-500 flex-shrink-0 mt-0.5" />
<p className="text-sm text-blue-500">
SSL changes will automatically restart the monitor service and redirect to the new address.
</p>
</div>
)}
</> </>
)} )}
</CardContent> </CardContent>

View File

@@ -4,6 +4,11 @@ Provides REST API endpoints for authentication management
""" """
import logging import logging
import os
import signal
import sys
import threading
import time
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
import auth_manager import auth_manager
import jwt import jwt
@@ -73,12 +78,29 @@ def ssl_status():
return jsonify({"success": False, "message": str(e)}), 500 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']) @auth_bp.route('/api/ssl/configure', methods=['POST'])
def ssl_configure(): def ssl_configure():
"""Configure SSL with Proxmox or custom certificates""" """Configure SSL with Proxmox or custom certificates"""
try: try:
data = request.json or {} data = request.json or {}
source = data.get("source", "proxmox") source = data.get("source", "proxmox")
auto_restart = data.get("auto_restart", True)
if source == "proxmox": if source == "proxmox":
cert_path = auth_manager.PROXMOX_CERT_PATH 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) success, message = auth_manager.configure_ssl(cert_path, key_path, source)
if success: 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: else:
return jsonify({"success": False, "message": message}), 400 return jsonify({"success": False, "message": message}), 400
except Exception as e: except Exception as e:
@@ -103,10 +132,20 @@ def ssl_configure():
def ssl_disable(): def ssl_disable():
"""Disable SSL and return to HTTP""" """Disable SSL and return to HTTP"""
try: try:
data = request.json or {}
auto_restart = data.get("auto_restart", True)
success, message = auth_manager.disable_ssl() success, message = auth_manager.disable_ssl()
if success: 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: else:
return jsonify({"success": False, "message": message}), 400 return jsonify({"success": False, "message": message}), 400
except Exception as e: except Exception as e: