mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-02-18 16:36:27 +00:00
Backend SSL config manager and API endpoints
This commit is contained in:
@@ -5,7 +5,7 @@ import { Button } from "./ui/button"
|
||||
import { Input } from "./ui/input"
|
||||
import { Label } from "./ui/label"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler, Trash2, RefreshCw, Clock } from 'lucide-react'
|
||||
import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut, Wrench, Package, Key, Copy, Eye, EyeOff, Ruler, Trash2, RefreshCw, Clock, ShieldCheck, Globe, FileKey, AlertTriangle } from 'lucide-react'
|
||||
import { APP_VERSION } from "./release-notes-modal"
|
||||
import { getApiUrl, fetchApi } from "../lib/api-config"
|
||||
import { TwoFactorSetup } from "./two-factor-setup"
|
||||
@@ -71,6 +71,19 @@ export function Settings() {
|
||||
const [revokingTokenId, setRevokingTokenId] = useState<string | null>(null)
|
||||
const [tokenName, setTokenName] = useState("API Token")
|
||||
|
||||
// SSL/HTTPS state
|
||||
const [sslEnabled, setSslEnabled] = useState(false)
|
||||
const [sslSource, setSslSource] = useState<"none" | "proxmox" | "custom">("none")
|
||||
const [sslCertPath, setSslCertPath] = useState("")
|
||||
const [sslKeyPath, setSslKeyPath] = useState("")
|
||||
const [proxmoxCertAvailable, setProxmoxCertAvailable] = useState(false)
|
||||
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 [showCustomCertForm, setShowCustomCertForm] = useState(false)
|
||||
const [customCertPath, setCustomCertPath] = useState("")
|
||||
const [customKeyPath, setCustomKeyPath] = useState("")
|
||||
|
||||
const [networkUnitSettings, setNetworkUnitSettings] = useState<"Bytes" | "Bits">("Bytes")
|
||||
const [loadingUnitSettings, setLoadingUnitSettings] = useState(true)
|
||||
|
||||
@@ -78,6 +91,7 @@ export function Settings() {
|
||||
checkAuthStatus()
|
||||
loadProxmenuxTools()
|
||||
loadApiTokens()
|
||||
loadSslStatus(setSslEnabled, setSslSource, setSslCertPath, setSslKeyPath, setProxmoxCertAvailable, setProxmoxCertInfo, setLoadingSsl)
|
||||
getUnitsSettings()
|
||||
}, [])
|
||||
|
||||
@@ -461,6 +475,90 @@ export function Settings() {
|
||||
setLoadingUnitSettings(false)
|
||||
}
|
||||
|
||||
const loadSslStatus = async () => {
|
||||
try {
|
||||
setLoadingSsl(true)
|
||||
const data = await fetchApi("/api/ssl/status")
|
||||
if (data.success) {
|
||||
setSslEnabled(data.ssl_enabled || false)
|
||||
setSslSource(data.source || "none")
|
||||
setSslCertPath(data.cert_path || "")
|
||||
setSslKeyPath(data.key_path || "")
|
||||
setProxmoxCertAvailable(data.proxmox_available || false)
|
||||
setProxmoxCertInfo(data.cert_info || null)
|
||||
}
|
||||
} catch {
|
||||
// Silently fail
|
||||
} finally {
|
||||
setLoadingSsl(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEnableSsl = async (source: "proxmox" | "custom", certPath?: string, keyPath?: string) => {
|
||||
setConfiguringSsl(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const body: Record<string, string> = { source }
|
||||
if (source === "custom" && certPath && keyPath) {
|
||||
body.cert_path = certPath
|
||||
body.key_path = keyPath
|
||||
}
|
||||
|
||||
const data = await fetchApi("/api/ssl/configure", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(data.message || "SSL configured successfully. Restart the monitor service to apply.")
|
||||
setSslEnabled(true)
|
||||
setSslSource(source)
|
||||
setShowCustomCertForm(false)
|
||||
setCustomCertPath("")
|
||||
setCustomKeyPath("")
|
||||
loadSslStatus()
|
||||
} else {
|
||||
setError(data.message || "Failed to configure SSL")
|
||||
}
|
||||
} 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.")) {
|
||||
return
|
||||
}
|
||||
|
||||
setConfiguringSsl(true)
|
||||
setError("")
|
||||
setSuccess("")
|
||||
|
||||
try {
|
||||
const data = await fetchApi("/api/ssl/disable", { method: "POST" })
|
||||
|
||||
if (data.success) {
|
||||
setSuccess(data.message || "SSL disabled. Restart the monitor service to apply.")
|
||||
setSslEnabled(false)
|
||||
setSslSource("none")
|
||||
setSslCertPath("")
|
||||
setSslKeyPath("")
|
||||
loadSslStatus()
|
||||
} else {
|
||||
setError(data.message || "Failed to disable SSL")
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : "Failed to disable SSL")
|
||||
} finally {
|
||||
setConfiguringSsl(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
@@ -1075,6 +1173,208 @@ export function Settings() {
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* SSL/HTTPS Configuration */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-5 w-5 text-green-500" />
|
||||
<CardTitle>SSL / HTTPS</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Serve ProxMenux Monitor over HTTPS using your Proxmox host certificate or a custom certificate
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{loadingSsl ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin h-8 w-8 border-4 border-green-500 border-t-transparent rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Current Status */}
|
||||
<div className="flex items-center justify-between p-4 bg-muted/50 rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${sslEnabled ? "bg-green-500/10" : "bg-gray-500/10"}`}>
|
||||
<Globe className={`h-5 w-5 ${sslEnabled ? "text-green-500" : "text-gray-500"}`} />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">
|
||||
{sslEnabled ? "HTTPS Enabled" : "HTTP (No SSL)"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{sslEnabled
|
||||
? `Using ${sslSource === "proxmox" ? "Proxmox host" : "custom"} certificate`
|
||||
: "Monitor is served over unencrypted HTTP"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full text-sm font-medium ${sslEnabled ? "bg-green-500/10 text-green-500" : "bg-gray-500/10 text-gray-500"}`}>
|
||||
{sslEnabled ? "HTTPS" : "HTTP"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active certificate info */}
|
||||
{sslEnabled && (
|
||||
<div className="space-y-2 p-3 bg-green-500/5 border border-green-500/20 rounded-lg">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-green-500">
|
||||
<FileKey className="h-4 w-4" />
|
||||
Active Certificate
|
||||
</div>
|
||||
<div className="grid gap-1 text-sm text-muted-foreground">
|
||||
<p><span className="font-medium text-foreground">Cert:</span> <code className="text-xs">{sslCertPath}</code></p>
|
||||
<p><span className="font-medium text-foreground">Key:</span> <code className="text-xs">{sslKeyPath}</code></p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={handleDisableSsl}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={configuringSsl}
|
||||
className="mt-2 text-red-500 border-red-500/30 hover:bg-red-500/10 bg-transparent"
|
||||
>
|
||||
{configuringSsl ? "Disabling..." : "Disable HTTPS"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Proxmox certificate detection */}
|
||||
{!sslEnabled && proxmoxCertAvailable && (
|
||||
<div className="space-y-3 p-4 border border-border rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
<ShieldCheck className="h-4 w-4 text-green-500" />
|
||||
<h3 className="font-semibold text-sm">Proxmox Host Certificate Detected</h3>
|
||||
</div>
|
||||
|
||||
{proxmoxCertInfo && (
|
||||
<div className="grid gap-1 text-sm text-muted-foreground bg-muted/50 p-3 rounded">
|
||||
{proxmoxCertInfo.subject && (
|
||||
<p><span className="font-medium text-foreground">Subject:</span> {proxmoxCertInfo.subject}</p>
|
||||
)}
|
||||
{proxmoxCertInfo.issuer && (
|
||||
<p><span className="font-medium text-foreground">Issuer:</span> {proxmoxCertInfo.issuer}</p>
|
||||
)}
|
||||
{proxmoxCertInfo.expires && (
|
||||
<p><span className="font-medium text-foreground">Expires:</span> {proxmoxCertInfo.expires}</p>
|
||||
)}
|
||||
{proxmoxCertInfo.is_self_signed && (
|
||||
<div className="flex items-center gap-1.5 mt-1 text-yellow-500">
|
||||
<AlertTriangle className="h-3.5 w-3.5" />
|
||||
<span className="text-xs">Self-signed certificate (browsers will show a security warning)</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => handleEnableSsl("proxmox")}
|
||||
className="w-full bg-green-600 hover:bg-green-700 text-white"
|
||||
disabled={configuringSsl}
|
||||
>
|
||||
{configuringSsl ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full" />
|
||||
Configuring...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<ShieldCheck className="h-4 w-4 mr-2" />
|
||||
Use Proxmox Certificate
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!sslEnabled && !proxmoxCertAvailable && (
|
||||
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-lg p-3 flex items-start gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
|
||||
<p className="text-sm text-yellow-500">
|
||||
No Proxmox host certificate detected. You can configure a custom certificate below.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom certificate option */}
|
||||
{!sslEnabled && (
|
||||
<div className="space-y-3">
|
||||
{!showCustomCertForm ? (
|
||||
<Button
|
||||
onClick={() => setShowCustomCertForm(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<FileKey className="h-4 w-4 mr-2" />
|
||||
Use Custom Certificate
|
||||
</Button>
|
||||
) : (
|
||||
<div className="space-y-4 border border-border rounded-lg p-4">
|
||||
<h3 className="font-semibold text-sm">Custom Certificate Paths</h3>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter the absolute paths to your SSL certificate and private key files on the Proxmox server.
|
||||
</p>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ssl-cert-path">Certificate Path (.pem / .crt)</Label>
|
||||
<Input
|
||||
id="ssl-cert-path"
|
||||
type="text"
|
||||
placeholder="/etc/ssl/certs/mydomain.pem"
|
||||
value={customCertPath}
|
||||
onChange={(e) => setCustomCertPath(e.target.value)}
|
||||
disabled={configuringSsl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ssl-key-path">Private Key Path (.key / .pem)</Label>
|
||||
<Input
|
||||
id="ssl-key-path"
|
||||
type="text"
|
||||
placeholder="/etc/ssl/private/mydomain.key"
|
||||
value={customKeyPath}
|
||||
onChange={(e) => setCustomKeyPath(e.target.value)}
|
||||
disabled={configuringSsl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={() => handleEnableSsl("custom", customCertPath, customKeyPath)}
|
||||
className="flex-1 bg-green-600 hover:bg-green-700 text-white"
|
||||
disabled={configuringSsl || !customCertPath || !customKeyPath}
|
||||
>
|
||||
{configuringSsl ? "Configuring..." : "Enable HTTPS"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowCustomCertForm(false)
|
||||
setCustomCertPath("")
|
||||
setCustomKeyPath("")
|
||||
}}
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
disabled={configuringSsl}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info note about restart */}
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* ProxMenux Optimizations */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -571,6 +571,203 @@ def disable_totp(username, password):
|
||||
return False, "Failed to disable 2FA"
|
||||
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# SSL/HTTPS Certificate Management
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
SSL_CONFIG_FILE = Path(os.environ.get("PROXMENUX_SSL_CONFIG", "/etc/proxmenux/ssl_config.json"))
|
||||
|
||||
# Default Proxmox certificate paths
|
||||
PROXMOX_CERT_PATH = "/etc/pve/local/pve-ssl.pem"
|
||||
PROXMOX_KEY_PATH = "/etc/pve/local/pve-ssl.key"
|
||||
|
||||
|
||||
def load_ssl_config():
|
||||
"""Load SSL configuration from file"""
|
||||
if not SSL_CONFIG_FILE.exists():
|
||||
return {
|
||||
"enabled": False,
|
||||
"cert_path": "",
|
||||
"key_path": "",
|
||||
"source": "none" # "none", "proxmox", "custom"
|
||||
}
|
||||
|
||||
try:
|
||||
with open(SSL_CONFIG_FILE, 'r') as f:
|
||||
config = json.load(f)
|
||||
config.setdefault("enabled", False)
|
||||
config.setdefault("cert_path", "")
|
||||
config.setdefault("key_path", "")
|
||||
config.setdefault("source", "none")
|
||||
return config
|
||||
except Exception:
|
||||
return {
|
||||
"enabled": False,
|
||||
"cert_path": "",
|
||||
"key_path": "",
|
||||
"source": "none"
|
||||
}
|
||||
|
||||
|
||||
def save_ssl_config(config):
|
||||
"""Save SSL configuration to file"""
|
||||
try:
|
||||
SSL_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(SSL_CONFIG_FILE, 'w') as f:
|
||||
json.dump(config, f, indent=2)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error saving SSL config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def detect_proxmox_certificates():
|
||||
"""
|
||||
Detect available Proxmox certificates.
|
||||
Returns dict with detection results.
|
||||
"""
|
||||
result = {
|
||||
"proxmox_available": False,
|
||||
"proxmox_cert": PROXMOX_CERT_PATH,
|
||||
"proxmox_key": PROXMOX_KEY_PATH,
|
||||
"cert_info": None
|
||||
}
|
||||
|
||||
if os.path.isfile(PROXMOX_CERT_PATH) and os.path.isfile(PROXMOX_KEY_PATH):
|
||||
result["proxmox_available"] = True
|
||||
|
||||
# Try to get certificate info
|
||||
try:
|
||||
import subprocess
|
||||
cert_output = subprocess.run(
|
||||
["openssl", "x509", "-in", PROXMOX_CERT_PATH, "-noout", "-subject", "-enddate", "-issuer"],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if cert_output.returncode == 0:
|
||||
lines = cert_output.stdout.strip().split('\n')
|
||||
info = {}
|
||||
for line in lines:
|
||||
if line.startswith("subject="):
|
||||
info["subject"] = line.replace("subject=", "").strip()
|
||||
elif line.startswith("notAfter="):
|
||||
info["expires"] = line.replace("notAfter=", "").strip()
|
||||
elif line.startswith("issuer="):
|
||||
issuer = line.replace("issuer=", "").strip()
|
||||
info["issuer"] = issuer
|
||||
info["is_self_signed"] = info.get("subject", "") == issuer
|
||||
result["cert_info"] = info
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def validate_certificate_files(cert_path, key_path):
|
||||
"""
|
||||
Validate that cert and key files exist and are readable.
|
||||
Returns (valid: bool, message: str)
|
||||
"""
|
||||
if not cert_path or not key_path:
|
||||
return False, "Certificate and key paths are required"
|
||||
|
||||
if not os.path.isfile(cert_path):
|
||||
return False, f"Certificate file not found: {cert_path}"
|
||||
|
||||
if not os.path.isfile(key_path):
|
||||
return False, f"Key file not found: {key_path}"
|
||||
|
||||
# Verify files are readable
|
||||
try:
|
||||
with open(cert_path, 'r') as f:
|
||||
content = f.read(100)
|
||||
if "BEGIN CERTIFICATE" not in content and "BEGIN TRUSTED CERTIFICATE" not in content:
|
||||
return False, "Certificate file does not appear to be a valid PEM certificate"
|
||||
|
||||
with open(key_path, 'r') as f:
|
||||
content = f.read(100)
|
||||
if "BEGIN" not in content or "KEY" not in content:
|
||||
return False, "Key file does not appear to be a valid PEM key"
|
||||
except PermissionError:
|
||||
return False, "Cannot read certificate files. Check file permissions."
|
||||
except Exception as e:
|
||||
return False, f"Error reading certificate files: {str(e)}"
|
||||
|
||||
# Verify cert and key match
|
||||
try:
|
||||
import subprocess
|
||||
cert_mod = subprocess.run(
|
||||
["openssl", "x509", "-noout", "-modulus", "-in", cert_path],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
key_mod = subprocess.run(
|
||||
["openssl", "rsa", "-noout", "-modulus", "-in", key_path],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if cert_mod.returncode == 0 and key_mod.returncode == 0:
|
||||
if cert_mod.stdout.strip() != key_mod.stdout.strip():
|
||||
return False, "Certificate and key do not match"
|
||||
except Exception:
|
||||
pass # Non-critical, proceed anyway
|
||||
|
||||
return True, "Certificate files are valid"
|
||||
|
||||
|
||||
def configure_ssl(cert_path, key_path, source="custom"):
|
||||
"""
|
||||
Configure SSL with given certificate and key paths.
|
||||
Returns (success: bool, message: str)
|
||||
"""
|
||||
valid, message = validate_certificate_files(cert_path, key_path)
|
||||
if not valid:
|
||||
return False, message
|
||||
|
||||
config = {
|
||||
"enabled": True,
|
||||
"cert_path": cert_path,
|
||||
"key_path": key_path,
|
||||
"source": source
|
||||
}
|
||||
|
||||
if save_ssl_config(config):
|
||||
return True, "SSL configured successfully. Restart the monitor service to apply changes."
|
||||
else:
|
||||
return False, "Failed to save SSL configuration"
|
||||
|
||||
|
||||
def disable_ssl():
|
||||
"""Disable SSL and return to HTTP"""
|
||||
config = {
|
||||
"enabled": False,
|
||||
"cert_path": "",
|
||||
"key_path": "",
|
||||
"source": "none"
|
||||
}
|
||||
|
||||
if save_ssl_config(config):
|
||||
return True, "SSL disabled. Restart the monitor service to apply changes."
|
||||
else:
|
||||
return False, "Failed to save SSL configuration"
|
||||
|
||||
|
||||
def get_ssl_context():
|
||||
"""
|
||||
Get SSL context for Flask if SSL is configured and enabled.
|
||||
Returns tuple (cert_path, key_path) or None
|
||||
"""
|
||||
config = load_ssl_config()
|
||||
|
||||
if not config.get("enabled"):
|
||||
return None
|
||||
|
||||
cert_path = config.get("cert_path", "")
|
||||
key_path = config.get("key_path", "")
|
||||
|
||||
if cert_path and key_path and os.path.isfile(cert_path) and os.path.isfile(key_path):
|
||||
return (cert_path, key_path)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def authenticate(username, password, totp_token=None):
|
||||
"""
|
||||
Authenticate a user with username, password, and optional TOTP
|
||||
|
||||
@@ -24,27 +24,91 @@ def auth_status():
|
||||
|
||||
return jsonify(status)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@auth_bp.route('/api/auth/setup', methods=['POST'])
|
||||
def auth_setup():
|
||||
"""Set up authentication with username and password"""
|
||||
# -------------------------------------------------------------------
|
||||
# SSL/HTTPS Certificate Management
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@auth_bp.route('/api/ssl/status', methods=['GET'])
|
||||
def ssl_status():
|
||||
"""Get current SSL configuration status and detect available certificates"""
|
||||
try:
|
||||
data = request.json
|
||||
username = data.get('username')
|
||||
password = data.get('password')
|
||||
config = auth_manager.load_ssl_config()
|
||||
detection = auth_manager.detect_proxmox_certificates()
|
||||
|
||||
success, message = auth_manager.setup_auth(username, password)
|
||||
return jsonify({
|
||||
"success": True,
|
||||
"ssl_enabled": config.get("enabled", False),
|
||||
"source": config.get("source", "none"),
|
||||
"cert_path": config.get("cert_path", ""),
|
||||
"key_path": config.get("key_path", ""),
|
||||
"proxmox_available": detection.get("proxmox_available", False),
|
||||
"proxmox_cert": detection.get("proxmox_cert", ""),
|
||||
"proxmox_key": detection.get("proxmox_key", ""),
|
||||
"cert_info": detection.get("cert_info")
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
if source == "proxmox":
|
||||
cert_path = auth_manager.PROXMOX_CERT_PATH
|
||||
key_path = auth_manager.PROXMOX_KEY_PATH
|
||||
elif source == "custom":
|
||||
cert_path = data.get("cert_path", "")
|
||||
key_path = data.get("key_path", "")
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Invalid source. Use 'proxmox' or 'custom'."}), 400
|
||||
|
||||
success, message = auth_manager.configure_ssl(cert_path, key_path, source)
|
||||
|
||||
if success:
|
||||
return jsonify({"success": True, "message": message})
|
||||
return jsonify({"success": True, "message": message, "requires_restart": True})
|
||||
else:
|
||||
return jsonify({"success": False, "message": message}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@auth_bp.route('/api/ssl/disable', methods=['POST'])
|
||||
def ssl_disable():
|
||||
"""Disable SSL and return to HTTP"""
|
||||
try:
|
||||
success, message = auth_manager.disable_ssl()
|
||||
|
||||
if success:
|
||||
return jsonify({"success": True, "message": message, "requires_restart": True})
|
||||
else:
|
||||
return jsonify({"success": False, "message": message}), 400
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@auth_bp.route('/api/ssl/validate', methods=['POST'])
|
||||
def ssl_validate():
|
||||
"""Validate custom certificate and key file paths"""
|
||||
try:
|
||||
data = request.json or {}
|
||||
cert_path = data.get("cert_path", "")
|
||||
key_path = data.get("key_path", "")
|
||||
|
||||
valid, message = auth_manager.validate_certificate_files(cert_path, key_path)
|
||||
|
||||
return jsonify({"success": valid, "message": message})
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
|
||||
@auth_bp.route('/api/auth/decline', methods=['POST'])
|
||||
def auth_decline():
|
||||
"""Decline authentication setup"""
|
||||
|
||||
@@ -6694,8 +6694,6 @@ def stream_script_logs(session_id):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics
|
||||
|
||||
import sys
|
||||
import logging
|
||||
|
||||
@@ -6703,11 +6701,25 @@ if __name__ == '__main__':
|
||||
log = logging.getLogger('werkzeug')
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
# Silence Flask CLI banner (removes "Serving Flask app", "Debug mode", "WARNING" messages)
|
||||
# Silence Flask CLI banner
|
||||
cli = sys.modules['flask.cli']
|
||||
cli.show_server_banner = lambda *x: None
|
||||
|
||||
# Print only essential information
|
||||
# print("API endpoints available at: /api/system, /api/system-info, /api/storage, /api/proxmox-storage, /api/network, /api/vms, /api/logs, /api/health, /api/hardware, /api/prometheus, /api/node/metrics")
|
||||
# Check for SSL configuration
|
||||
ssl_ctx = None
|
||||
try:
|
||||
ssl_ctx = auth_manager.get_ssl_context()
|
||||
if ssl_ctx:
|
||||
print(f"[ProxMenux] Starting with HTTPS (cert: {ssl_ctx[0]})")
|
||||
else:
|
||||
print("[ProxMenux] Starting with HTTP (no SSL configured)")
|
||||
except Exception as e:
|
||||
print(f"[ProxMenux] SSL config error, falling back to HTTP: {e}")
|
||||
ssl_ctx = None
|
||||
|
||||
app.run(host='0.0.0.0', port=8008, debug=False)
|
||||
try:
|
||||
app.run(host='0.0.0.0', port=8008, debug=False, ssl_context=ssl_ctx)
|
||||
except Exception as e:
|
||||
if ssl_ctx:
|
||||
print(f"[ProxMenux] SSL startup failed ({e}), falling back to HTTP")
|
||||
app.run(host='0.0.0.0', port=8008, debug=False)
|
||||
|
||||
Reference in New Issue
Block a user