diff --git a/AppImage/components/auth-setup.tsx b/AppImage/components/auth-setup.tsx
new file mode 100644
index 0000000..a6ff242
--- /dev/null
+++ b/AppImage/components/auth-setup.tsx
@@ -0,0 +1,221 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { Button } from "./ui/button"
+import { Dialog, DialogContent } from "./ui/dialog"
+import { Input } from "./ui/input"
+import { Label } from "./ui/label"
+import { Shield, Lock, User, AlertCircle } from "lucide-react"
+import { getApiUrl } from "../lib/api-config"
+
+interface AuthSetupProps {
+ onComplete: () => void
+}
+
+export function AuthSetup({ onComplete }: AuthSetupProps) {
+ const [open, setOpen] = useState(false)
+ const [step, setStep] = useState<"choice" | "setup">("choice")
+ const [username, setUsername] = useState("")
+ const [password, setPassword] = useState("")
+ const [confirmPassword, setConfirmPassword] = useState("")
+ const [error, setError] = useState("")
+ const [loading, setLoading] = useState(false)
+
+ useEffect(() => {
+ // Check if onboarding is complete and auth setup is needed
+ const hasSeenOnboarding = localStorage.getItem("proxmenux-onboarding-seen")
+ const authSetupComplete = localStorage.getItem("proxmenux-auth-setup-complete")
+
+ if (hasSeenOnboarding && !authSetupComplete) {
+ // Small delay to show after onboarding closes
+ setTimeout(() => setOpen(true), 500)
+ }
+ }, [])
+
+ const handleSkipAuth = async () => {
+ setLoading(true)
+ setError("")
+
+ try {
+ const response = await fetch(getApiUrl("/api/auth/setup"), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ enable_auth: false }),
+ })
+
+ if (!response.ok) throw new Error("Failed to save preference")
+
+ localStorage.setItem("proxmenux-auth-setup-complete", "true")
+ setOpen(false)
+ onComplete()
+ } catch (err) {
+ setError("Failed to save preference. Please try again.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSetupAuth = async () => {
+ setError("")
+
+ 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 setup authentication")
+ }
+
+ // Save token
+ localStorage.setItem("proxmenux-auth-token", data.token)
+ localStorage.setItem("proxmenux-auth-setup-complete", "true")
+
+ setOpen(false)
+ onComplete()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to setup authentication")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/AppImage/components/login.tsx b/AppImage/components/login.tsx
new file mode 100644
index 0000000..3f8ca87
--- /dev/null
+++ b/AppImage/components/login.tsx
@@ -0,0 +1,141 @@
+"use client"
+
+import type React from "react"
+
+import { useState } from "react"
+import { Button } from "./ui/button"
+import { Input } from "./ui/input"
+import { Label } from "./ui/label"
+import { Lock, User, AlertCircle, Server } from "lucide-react"
+import { getApiUrl } from "../lib/api-config"
+import Image from "next/image"
+
+interface LoginProps {
+ onLogin: () => void
+}
+
+export function Login({ onLogin }: LoginProps) {
+ const [username, setUsername] = useState("")
+ const [password, setPassword] = useState("")
+ const [error, setError] = useState("")
+ const [loading, setLoading] = useState(false)
+
+ const handleLogin = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setError("")
+
+ if (!username || !password) {
+ setError("Please enter username and password")
+ return
+ }
+
+ setLoading(true)
+
+ try {
+ const response = await fetch(getApiUrl("/api/auth/login"), {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ username, password }),
+ })
+
+ const data = await response.json()
+
+ if (!response.ok) {
+ throw new Error(data.error || "Login failed")
+ }
+
+ // Save token
+ localStorage.setItem("proxmenux-auth-token", data.token)
+ onLogin()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Login failed")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ {
+ const target = e.target as HTMLImageElement
+ target.style.display = "none"
+ const fallback = target.parentElement?.querySelector(".fallback-icon")
+ if (fallback) {
+ fallback.classList.remove("hidden")
+ }
+ }}
+ />
+
+
+
+
+
ProxMenux Monitor
+
Sign in to access your dashboard
+
+
+
+
+
+
ProxMenux Monitor v1.0.0
+
+
+ )
+}
diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx
index da744ba..27d2a4b 100644
--- a/AppImage/components/proxmox-dashboard.tsx
+++ b/AppImage/components/proxmox-dashboard.tsx
@@ -11,6 +11,8 @@ import { VirtualMachines } from "./virtual-machines"
import Hardware from "./hardware"
import { SystemLogs } from "./system-logs"
import { OnboardingCarousel } from "./onboarding-carousel"
+import { AuthSetup } from "./auth-setup"
+import { Login } from "./login"
import { getApiUrl } from "../lib/api-config"
import {
RefreshCw,
@@ -63,6 +65,10 @@ export function ProxmoxDashboard() {
const [activeTab, setActiveTab] = useState("overview")
const [showNavigation, setShowNavigation] = useState(true)
const [lastScrollY, setLastScrollY] = useState(0)
+ const [authChecked, setAuthChecked] = useState(false)
+ const [authRequired, setAuthRequired] = useState(false)
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
+ const [authSetupComplete, setAuthSetupComplete] = useState(false)
const fetchSystemData = useCallback(async () => {
console.log("[v0] Fetching system data from Flask server...")
@@ -219,10 +225,119 @@ export function ProxmoxDashboard() {
}
}
+ const setupTokenRefresh = () => {
+ let refreshTimeout: ReturnType
+
+ const refreshToken = async () => {
+ const token = localStorage.getItem("proxmenux-auth-token")
+ if (!token) return
+
+ try {
+ const response = await fetch(getApiUrl("/api/auth/refresh"), {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${token}`,
+ },
+ })
+
+ if (response.ok) {
+ const data = await response.json()
+ localStorage.setItem("proxmenux-auth-token", data.token)
+ console.log("[v0] Token refreshed successfully")
+ }
+ } catch (error) {
+ console.error("[v0] Failed to refresh token:", error)
+ }
+ }
+
+ const resetRefreshTimer = () => {
+ clearTimeout(refreshTimeout)
+ // Refresh token every 25 minutes (before 30 min expiry)
+ refreshTimeout = setTimeout(refreshToken, 25 * 60 * 1000)
+ }
+
+ // Refresh on user activity
+ const events = ["mousedown", "keydown", "scroll", "touchstart"]
+ events.forEach((event) => {
+ window.addEventListener(event, resetRefreshTimer, { passive: true })
+ })
+
+ resetRefreshTimer()
+
+ return () => {
+ clearTimeout(refreshTimeout)
+ events.forEach((event) => {
+ window.removeEventListener(event, resetRefreshTimer)
+ })
+ }
+ }
+
+ const handleAuthSetupComplete = () => {
+ setAuthSetupComplete(true)
+ setIsAuthenticated(true)
+ }
+
+ const handleLoginSuccess = () => {
+ setIsAuthenticated(true)
+ setupTokenRefresh()
+ }
+
+ useEffect(() => {
+ const checkAuth = async () => {
+ try {
+ const token = localStorage.getItem("proxmenux-auth-token")
+ const headers: HeadersInit = { "Content-Type": "application/json" }
+
+ if (token) {
+ headers["Authorization"] = `Bearer ${token}`
+ }
+
+ const response = await fetch(getApiUrl("/api/auth/status"), {
+ headers,
+ })
+
+ const data = await response.json()
+
+ setAuthRequired(data.auth_enabled)
+ setIsAuthenticated(data.authenticated)
+ setAuthSetupComplete(localStorage.getItem("proxmenux-auth-setup-complete") === "true")
+ setAuthChecked(true)
+
+ // Setup token refresh if authenticated
+ if (data.authenticated && token) {
+ setupTokenRefresh()
+ }
+ } catch (error) {
+ console.error("[v0] Failed to check auth status:", error)
+ setAuthChecked(true)
+ }
+ }
+
+ checkAuth()
+ }, [])
+
+ if (!authChecked) {
+ return (
+
+ )
+ }
+
+ if (authRequired && !isAuthenticated) {
+ return
+ }
+
return (
+ {!authSetupComplete &&
}
+
{!isServerConnected && (
diff --git a/AppImage/components/ui/input.tsx b/AppImage/components/ui/input.tsx
index 31bbca4..d32a72e 100644
--- a/AppImage/components/ui/input.tsx
+++ b/AppImage/components/ui/input.tsx
@@ -9,7 +9,7 @@ const Input = React.forwardRef(({ className, type,
,
+ React.ComponentPropsWithoutRef & VariantProps
+>(({ className, ...props }, ref) => (
+
+))
+Label.displayName = LabelPrimitive.Root.displayName
+
+export { Label }
diff --git a/AppImage/scripts/flask_server.py b/AppImage/scripts/flask_server.py
index 783c611..2090fb4 100644
--- a/AppImage/scripts/flask_server.py
+++ b/AppImage/scripts/flask_server.py
@@ -22,6 +22,72 @@ import xml.etree.ElementTree as ET # Added for XML parsing
import math # Imported math for format_bytes function
import urllib.parse # Added for URL encoding
import platform # Added for platform.release()
+import hashlib
+import secrets
+import jwt
+from functools import wraps
+from pathlib import Path
+
+# Authentication configuration
+AUTH_CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
+AUTH_CONFIG_FILE = AUTH_CONFIG_DIR / "auth.json"
+JWT_SECRET = secrets.token_hex(32) # Generate a random secret for JWT
+SESSION_TIMEOUT = 30 * 60 # 30 minutes in seconds
+
+# Ensure config directory exists
+AUTH_CONFIG_DIR.mkdir(parents=True, exist_ok=True)
+
+def hash_password(password: str) -> str:
+ """Hash a password using SHA-256"""
+ return hashlib.sha256(password.encode()).hexdigest()
+
+def load_auth_config():
+ """Load authentication configuration from file"""
+ if not AUTH_CONFIG_FILE.exists():
+ return {"auth_enabled": False}
+
+ try:
+ with open(AUTH_CONFIG_FILE, 'r') as f:
+ return json.load(f)
+ except:
+ return {"auth_enabled": False}
+
+def save_auth_config(config):
+ """Save authentication configuration to file"""
+ with open(AUTH_CONFIG_FILE, 'w') as f:
+ json.dump(config, f, indent=2)
+
+def require_auth(f):
+ """Decorator to require authentication for endpoints"""
+ @wraps(f)
+ def decorated_function(*args, **kwargs):
+ auth_config = load_auth_config()
+
+ # If auth is not enabled, allow access
+ if not auth_config.get("auth_enabled", False):
+ return f(*args, **kwargs)
+
+ # Check for Authorization header
+ auth_header = request.headers.get('Authorization')
+ if not auth_header or not auth_header.startswith('Bearer '):
+ return jsonify({"error": "Authentication required"}), 401
+
+ token = auth_header.split(' ')[1]
+
+ try:
+ # Verify JWT token
+ payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
+
+ # Check if token is expired
+ if time.time() > payload.get('exp', 0):
+ return jsonify({"error": "Token expired"}), 401
+
+ return f(*args, **kwargs)
+ except jwt.InvalidTokenError:
+ return jsonify({"error": "Invalid token"}), 401
+
+ return decorated_function
+
app = Flask(__name__)
CORS(app) # Enable CORS for Next.js frontend