"use client" import { useState, useEffect } from "react" 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, 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" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select" import { getNetworkUnit } from "../lib/format-network" interface ProxMenuxTool { key: string name: string enabled: boolean } interface ApiTokenEntry { id: string name: string token_prefix: string created_at: string expires_at: string revoked: boolean } export function Settings() { const [authEnabled, setAuthEnabled] = useState(false) const [totpEnabled, setTotpEnabled] = useState(false) const [loading, setLoading] = useState(false) const [error, setError] = useState("") const [success, setSuccess] = useState("") // Setup form state const [showSetupForm, setShowSetupForm] = useState(false) const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") // Change password form state const [showChangePassword, setShowChangePassword] = useState(false) const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [confirmNewPassword, setConfirmNewPassword] = useState("") const [show2FASetup, setShow2FASetup] = useState(false) const [show2FADisable, setShow2FADisable] = useState(false) const [disable2FAPassword, setDisable2FAPassword] = useState("") const [proxmenuxTools, setProxmenuxTools] = useState([]) const [loadingTools, setLoadingTools] = useState(true) const [expandedVersions, setExpandedVersions] = useState>({ [APP_VERSION]: true, // Current version expanded by default }) // API Token state management const [showApiTokenSection, setShowApiTokenSection] = useState(false) const [apiToken, setApiToken] = useState("") const [apiTokenVisible, setApiTokenVisible] = useState(false) const [tokenPassword, setTokenPassword] = useState("") const [tokenTotpCode, setTokenTotpCode] = useState("") const [generatingToken, setGeneratingToken] = useState(false) const [tokenCopied, setTokenCopied] = useState(false) // Token list state const [existingTokens, setExistingTokens] = useState([]) const [loadingTokens, setLoadingTokens] = useState(false) const [revokingTokenId, setRevokingTokenId] = useState(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) useEffect(() => { checkAuthStatus() loadProxmenuxTools() loadApiTokens() loadSslStatus(setSslEnabled, setSslSource, setSslCertPath, setSslKeyPath, setProxmoxCertAvailable, setProxmoxCertInfo, setLoadingSsl) getUnitsSettings() }, []) const checkAuthStatus = async () => { try { const response = await fetch(getApiUrl("/api/auth/status")) const data = await response.json() setAuthEnabled(data.auth_enabled || false) setTotpEnabled(data.totp_enabled || false) // Get 2FA status } catch (err) { console.error("Failed to check auth status:", err) } } const loadProxmenuxTools = async () => { try { const response = await fetch(getApiUrl("/api/proxmenux/installed-tools")) const data = await response.json() if (data.success) { setProxmenuxTools(data.installed_tools || []) } } catch (err) { console.error("Failed to load ProxMenux tools:", err) } finally { setLoadingTools(false) } } const handleEnableAuth = async () => { setError("") setSuccess("") if (!username || !password) { setError("Please fill in all fields") return } if (password !== confirmPassword) { setError("Passwords do not match") return } if (password.length < 6) { setError("Password must be at least 6 characters") return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/setup"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password, enable_auth: true, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to enable authentication") } // Save token localStorage.setItem("proxmenux-auth-token", data.token) localStorage.setItem("proxmenux-auth-setup-complete", "true") setSuccess("Authentication enabled successfully!") setAuthEnabled(true) setShowSetupForm(false) setUsername("") setPassword("") setConfirmPassword("") } catch (err) { setError(err instanceof Error ? err.message : "Failed to enable authentication") } finally { setLoading(false) } } const handleDisableAuth = async () => { if ( !confirm( "Are you sure you want to disable authentication? This will remove password protection from your dashboard.", ) ) { return } setLoading(true) setError("") setSuccess("") try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/disable"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, }) const data = await response.json() if (!response.ok) { throw new Error(data.message || "Failed to disable authentication") } localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-setup-complete") setSuccess("Authentication disabled successfully! Reloading...") setTimeout(() => { window.location.reload() }, 1000) } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.") } finally { setLoading(false) } } const handleChangePassword = async () => { setError("") setSuccess("") if (!currentPassword || !newPassword) { setError("Please fill in all fields") return } if (newPassword !== confirmNewPassword) { setError("New passwords do not match") return } if (newPassword.length < 6) { setError("Password must be at least 6 characters") return } setLoading(true) try { const response = await fetch(getApiUrl("/api/auth/change-password"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${localStorage.getItem("proxmenux-auth-token")}`, }, body: JSON.stringify({ current_password: currentPassword, new_password: newPassword, }), }) const data = await response.json() if (!response.ok) { throw new Error(data.error || "Failed to change password") } // Update token if provided if (data.token) { localStorage.setItem("proxmenux-auth-token", data.token) } setSuccess("Password changed successfully!") setShowChangePassword(false) setCurrentPassword("") setNewPassword("") setConfirmNewPassword("") } catch (err) { setError(err instanceof Error ? err.message : "Failed to change password") } finally { setLoading(false) } } const handleDisable2FA = async () => { setError("") setSuccess("") if (!disable2FAPassword) { setError("Please enter your password") return } setLoading(true) try { const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/totp/disable"), { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}`, }, body: JSON.stringify({ password: disable2FAPassword }), }) const data = await response.json() if (!response.ok) { throw new Error(data.message || "Failed to disable 2FA") } setSuccess("2FA disabled successfully!") setTotpEnabled(false) setShow2FADisable(false) setDisable2FAPassword("") checkAuthStatus() } catch (err) { setError(err instanceof Error ? err.message : "Failed to disable 2FA") } finally { setLoading(false) } } const handleLogout = () => { localStorage.removeItem("proxmenux-auth-token") localStorage.removeItem("proxmenux-auth-setup-complete") window.location.reload() } const loadApiTokens = async () => { try { setLoadingTokens(true) const data = await fetchApi("/api/auth/api-tokens") if (data.success) { setExistingTokens(data.tokens || []) } } catch { // Silently fail - tokens section is optional } finally { setLoadingTokens(false) } } const handleRevokeToken = async (tokenId: string) => { if (!confirm("Are you sure you want to revoke this token? Any integration using it will stop working immediately.")) { return } setRevokingTokenId(tokenId) setError("") setSuccess("") try { const data = await fetchApi(`/api/auth/api-tokens/${tokenId}`, { method: "DELETE", }) if (data.success) { setSuccess("Token revoked successfully") setExistingTokens((prev) => prev.filter((t) => t.id !== tokenId)) } else { setError(data.message || "Failed to revoke token") } } catch (err) { setError(err instanceof Error ? err.message : "Failed to revoke token") } finally { setRevokingTokenId(null) } } const handleGenerateApiToken = async () => { setError("") setSuccess("") if (!tokenPassword) { setError("Please enter your password") return } if (totpEnabled && !tokenTotpCode) { setError("Please enter your 2FA code") return } setGeneratingToken(true) try { const data = await fetchApi("/api/auth/generate-api-token", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password: tokenPassword, totp_token: totpEnabled ? tokenTotpCode : undefined, token_name: tokenName || "API Token", }), }) if (!data.success) { setError(data.message || data.error || "Failed to generate API token") return } if (!data.token) { setError("No token received from server") return } setApiToken(data.token) setSuccess("API token generated successfully! Make sure to copy it now as you won't be able to see it again.") setTokenPassword("") setTokenTotpCode("") setTokenName("API Token") loadApiTokens() } catch (err) { setError(err instanceof Error ? err.message : "Failed to generate API token. Please try again.") } finally { setGeneratingToken(false) } } const copyToClipboard = async (text: string) => { try { if (navigator.clipboard && typeof navigator.clipboard.writeText === "function") { await navigator.clipboard.writeText(text) } else { // Fallback for non-secure contexts (HTTP on local network) const textarea = document.createElement("textarea") textarea.value = text textarea.style.position = "fixed" textarea.style.left = "-9999px" textarea.style.top = "-9999px" textarea.style.opacity = "0" document.body.appendChild(textarea) textarea.focus() textarea.select() document.execCommand("copy") document.body.removeChild(textarea) } return true } catch { return false } } const copyApiToken = async () => { const ok = await copyToClipboard(apiToken) if (ok) { setTokenCopied(true) setTimeout(() => setTokenCopied(false), 2000) } } const toggleVersion = (version: string) => { setExpandedVersions((prev) => ({ ...prev, [version]: !prev[version], })) } const changeNetworkUnit = (unit: string) => { const networkUnit = unit as "Bytes" | "Bits" localStorage.setItem("proxmenux-network-unit", networkUnit) setNetworkUnitSettings(networkUnit) // Dispatch custom event to notify other components window.dispatchEvent(new CustomEvent("networkUnitChanged", { detail: networkUnit })) // Also dispatch storage event for backward compatibility window.dispatchEvent(new StorageEvent("storage", { key: "proxmenux-network-unit", newValue: networkUnit, url: window.location.href })) } const getUnitsSettings = () => { const networkUnit = getNetworkUnit() setNetworkUnitSettings(networkUnit) 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 = { 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 (

Settings

Manage your dashboard security and preferences

{/* Authentication Settings */}
Authentication
Protect your dashboard with username and password authentication
{error && (

{error}

)} {success && (

{success}

)}

Authentication Status

{authEnabled ? "Password protection is enabled" : "No password protection"}

{authEnabled ? "Enabled" : "Disabled"}
{!authEnabled && !showSetupForm && (

Enable authentication to protect your dashboard when accessing from non-private networks.

)} {!authEnabled && showSetupForm && (

Setup Authentication

setUsername(e.target.value)} className="pl-10" disabled={loading} />
setPassword(e.target.value)} className="pl-10" disabled={loading} />
setConfirmPassword(e.target.value)} className="pl-10" disabled={loading} />
)} {authEnabled && (
{!showChangePassword && ( )} {showChangePassword && (

Change Password

setCurrentPassword(e.target.value)} className="pl-10" disabled={loading} />
setNewPassword(e.target.value)} className="pl-10" disabled={loading} />
setConfirmNewPassword(e.target.value)} className="pl-10" disabled={loading} />
)} {!totpEnabled && (

Two-Factor Authentication (2FA)

Add an extra layer of security by requiring a code from your authenticator app in addition to your password.

)} {totpEnabled && (

2FA is enabled

{!show2FADisable && ( )} {show2FADisable && (

Disable Two-Factor Authentication

Enter your password to confirm

setDisable2FAPassword(e.target.value)} className="pl-10" disabled={loading} />
)}
)}
)}
{/* Network Units Settings */}
Network Units
Change how network traffic is displayed
{loadingUnitSettings ? (
) : (
Network Unit Display
)} {/* API Access Tokens */} {authEnabled && (
API Access Tokens
Generate long-lived API tokens for external integrations like Homepage and Home Assistant
{error && (

{error}

)} {success && (

{success}

)}

About API Tokens

  • Tokens are valid for 1 year
  • Use them to access APIs from external services
  • Include in Authorization header: Bearer YOUR_TOKEN
  • See README.md for complete integration examples
{!showApiTokenSection && !apiToken && ( )} {showApiTokenSection && !apiToken && (

Generate API Token

Enter your credentials to generate a new long-lived API token

setTokenName(e.target.value)} className="pl-10" disabled={generatingToken} />
setTokenPassword(e.target.value)} className="pl-10" disabled={generatingToken} />
{totpEnabled && (
setTokenTotpCode(e.target.value)} className="pl-10" maxLength={6} disabled={generatingToken} />
)}
)} {apiToken && (

Your API Token

⚠️ Important: Save this token now!

You won't be able to see it again. Store it securely.

{tokenCopied && (

Copied to clipboard!

)}

How to use this token:

# Add to request headers:

Authorization: Bearer YOUR_TOKEN_HERE

See the README documentation for complete integration examples with Homepage and Home Assistant.

)} {/* Existing Tokens List */} {!loadingTokens && existingTokens.length > 0 && (

Active Tokens

{existingTokens.map((token) => (

{token.name}

{token.token_prefix} {token.created_at ? new Date(token.created_at).toLocaleDateString() : "Unknown"}
))}
)} {loadingTokens && (
Loading tokens...
)} {!loadingTokens && existingTokens.length === 0 && !showApiTokenSection && !apiToken && (
No API tokens created yet
)} )} {/* SSL/HTTPS Configuration */}
SSL / HTTPS
Serve ProxMenux Monitor over HTTPS using your Proxmox host certificate or a custom certificate
{loadingSsl ? (
) : ( <> {/* Current Status */}

{sslEnabled ? "HTTPS Enabled" : "HTTP (No SSL)"}

{sslEnabled ? `Using ${sslSource === "proxmox" ? "Proxmox host" : "custom"} certificate` : "Monitor is served over unencrypted HTTP"}

{sslEnabled ? "HTTPS" : "HTTP"}
{/* Active certificate info */} {sslEnabled && (
Active Certificate

Cert: {sslCertPath}

Key: {sslKeyPath}

)} {/* Proxmox certificate detection */} {!sslEnabled && proxmoxCertAvailable && (

Proxmox Host Certificate Detected

{proxmoxCertInfo && (
{proxmoxCertInfo.subject && (

Subject: {proxmoxCertInfo.subject}

)} {proxmoxCertInfo.issuer && (

Issuer: {proxmoxCertInfo.issuer}

)} {proxmoxCertInfo.expires && (

Expires: {proxmoxCertInfo.expires}

)} {proxmoxCertInfo.is_self_signed && (
Self-signed certificate (browsers will show a security warning)
)}
)}
)} {!sslEnabled && !proxmoxCertAvailable && (

No Proxmox host certificate detected. You can configure a custom certificate below.

)} {/* Custom certificate option */} {!sslEnabled && (
{!showCustomCertForm ? ( ) : (

Custom Certificate Paths

Enter the absolute paths to your SSL certificate and private key files on the Proxmox server.

setCustomCertPath(e.target.value)} disabled={configuringSsl} />
setCustomKeyPath(e.target.value)} disabled={configuringSsl} />
)}
)} {/* 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.

)} {/* ProxMenux Optimizations */}
ProxMenux Optimizations
System optimizations and utilities installed via ProxMenux
{loadingTools ? (
) : proxmenuxTools.length === 0 ? (

No ProxMenux optimizations installed yet

Run ProxMenux to configure system optimizations

) : (
Installed Tools {proxmenuxTools.length} active
{proxmenuxTools.map((tool) => (
{tool.name}
))}
)} setShow2FASetup(false)} onSuccess={() => { setSuccess("2FA enabled successfully!") checkAuthStatus() }} />
) }