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 [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<string, string> = { source }
const body: Record<string, string | boolean> = { 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"}
</Button>
</div>
)}
@@ -1722,7 +1767,7 @@ ${(report.sections && report.sections.length > 0) ? `
<Button
onClick={() => handleEnableSsl("proxmox")}
className="w-full bg-green-600 hover:bg-green-700 text-white"
disabled={configuringSsl}
disabled={configuringSsl || sslRestarting}
>
{configuringSsl ? (
<div className="flex items-center gap-2">
@@ -1795,7 +1840,7 @@ ${(report.sections && report.sections.length > 0) ? `
<Button
onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)}
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"}
</Button>
@@ -1817,14 +1862,27 @@ ${(report.sections && report.sections.length > 0) ? `
</div>
)}
{/* Info note about restart */}
{/* Restarting overlay or info note */}
{sslRestarting ? (
<div className="bg-amber-500/10 border border-amber-500/20 rounded-lg p-4 flex items-center gap-3">
<div className="h-5 w-5 border-2 border-amber-500 border-t-transparent rounded-full animate-spin flex-shrink-0" />
<div>
<p className="text-sm font-medium text-amber-500">
Restarting monitor service...
</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">
Changes to SSL configuration require a monitor service restart to take effect.
The service will automatically use HTTPS on port 8008 when enabled.
SSL changes will automatically restart the monitor service and redirect to the new address.
</p>
</div>
)}
</>
)}
</CardContent>

View File

@@ -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: