mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Update security.tsx
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user