diff --git a/AppImage/app/page.tsx b/AppImage/app/page.tsx index b6d2032..c4f3736 100644 --- a/AppImage/app/page.tsx +++ b/AppImage/app/page.tsx @@ -7,7 +7,17 @@ import { AuthSetup } from "../components/auth-setup" import { getApiUrl } from "../lib/api-config" export default function Home() { - const [authState, setAuthState] = useState<"loading" | "setup" | "login" | "authenticated">("loading") + const [authStatus, setAuthStatus] = useState<{ + loading: boolean + authEnabled: boolean + authConfigured: boolean + authenticated: boolean + }>({ + loading: true, + authEnabled: false, + authConfigured: false, + authenticated: false, + }) useEffect(() => { checkAuthStatus() @@ -15,80 +25,61 @@ export default function Home() { const checkAuthStatus = async () => { try { + const token = localStorage.getItem("proxmenux-auth-token") const response = await fetch(getApiUrl("/api/auth/status"), { - method: "GET", - headers: { - "Content-Type": "application/json", - }, + headers: token ? { Authorization: `Bearer ${token}` } : {}, }) - const data = await response.json() - if (!data.auth_enabled) { - // Auth no está habilitada, permitir acceso directo - setAuthState("authenticated") - return - } + console.log("[v0] Auth status:", data) - // Auth está habilitada, verificar si hay token válido - const token = localStorage.getItem("proxmenux-auth-token") + const authenticated = data.auth_enabled ? data.authenticated : true - if (!token) { - setAuthState("login") - return - } - - // Verificar que el token sea válido - const verifyResponse = await fetch(getApiUrl("/api/auth/verify"), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, + setAuthStatus({ + loading: false, + authEnabled: data.auth_enabled, + authConfigured: data.auth_configured, + authenticated, }) - - if (verifyResponse.ok) { - setAuthState("authenticated") - } else { - // Token inválido, limpiar y pedir login - localStorage.removeItem("proxmenux-auth-token") - localStorage.removeItem("proxmenux-saved-username") - localStorage.removeItem("proxmenux-saved-password") - setAuthState("login") - } } catch (error) { - console.error("Error checking auth status:", error) - // En caso de error, mostrar setup - setAuthState("setup") + console.error("[v0] Failed to check auth status:", error) + setAuthStatus({ + loading: false, + authEnabled: false, + authConfigured: false, + authenticated: true, + }) } } - const handleSetupComplete = () => { - setAuthState("login") + const handleAuthComplete = () => { + checkAuthStatus() } const handleLoginSuccess = () => { - setAuthState("authenticated") + checkAuthStatus() } - if (authState === "loading") { + if (authStatus.loading) { return (
-
+
-

Loading...

+

Loading...

) } - if (authState === "setup") { - return - } - - if (authState === "login") { + if (authStatus.authEnabled && !authStatus.authenticated) { return } - return + // Show dashboard in all other cases + return ( + <> + {!authStatus.authConfigured && } + + + ) } diff --git a/AppImage/components/login.tsx b/AppImage/components/login.tsx index 96e3b14..38f3aac 100644 --- a/AppImage/components/login.tsx +++ b/AppImage/components/login.tsx @@ -2,11 +2,10 @@ import type React from "react" -import { useState, useEffect } from "react" +import { useState } from "react" import { Button } from "./ui/button" import { Input } from "./ui/input" import { Label } from "./ui/label" -import { Checkbox } from "./ui/checkbox" import { Lock, User, AlertCircle, Server } from "lucide-react" import { getApiUrl } from "../lib/api-config" import Image from "next/image" @@ -18,49 +17,15 @@ interface LoginProps { export function Login({ onLogin }: LoginProps) { const [username, setUsername] = useState("") const [password, setPassword] = useState("") - const [rememberMe, setRememberMe] = useState(false) const [error, setError] = useState("") const [loading, setLoading] = useState(false) - useEffect(() => { - const savedUsername = localStorage.getItem("proxmenux-saved-username") - const savedPassword = localStorage.getItem("proxmenux-saved-password") - - if (savedUsername && savedPassword) { - setUsername(savedUsername) - setPassword(savedPassword) - setRememberMe(true) - - // Auto-login si hay credenciales guardadas - handleAutoLogin(savedUsername, savedPassword) - } - }, []) - - const handleAutoLogin = async (user: string, pass: string) => { - try { - const response = await fetch(getApiUrl("/api/auth/login"), { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username: user, password: pass, remember_me: true }), - }) - - const data = await response.json() - - if (response.ok && data.token) { - localStorage.setItem("proxmenux-auth-token", data.token) - onLogin() - } - } catch (err) { - console.log("Auto-login failed, showing login form") - } - } - const handleLogin = async (e: React.FormEvent) => { e.preventDefault() setError("") if (!username || !password) { - setError("Por favor, introduce usuario y contraseña") + setError("Please enter username and password") return } @@ -70,30 +35,20 @@ export function Login({ onLogin }: LoginProps) { const response = await fetch(getApiUrl("/api/auth/login"), { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ username, password, remember_me: rememberMe }), + body: JSON.stringify({ username, password }), }) const data = await response.json() if (!response.ok) { - throw new Error(data.error || "Fallo en el login") + throw new Error(data.error || "Login failed") } // Save token localStorage.setItem("proxmenux-auth-token", data.token) - - if (rememberMe) { - localStorage.setItem("proxmenux-saved-username", username) - localStorage.setItem("proxmenux-saved-password", password) - } else { - // Limpiar credenciales guardadas si no se marcó recordar - localStorage.removeItem("proxmenux-saved-username") - localStorage.removeItem("proxmenux-saved-password") - } - onLogin() } catch (err) { - setError(err instanceof Error ? err.message : "Fallo en el login") + setError(err instanceof Error ? err.message : "Login failed") } finally { setLoading(false) } @@ -173,18 +128,6 @@ export function Login({ onLogin }: LoginProps) {
-
- setRememberMe(checked as boolean)} - disabled={loading} - /> - -
- diff --git a/AppImage/components/proxmox-dashboard.tsx b/AppImage/components/proxmox-dashboard.tsx index 8a59db0..9cce96a 100644 --- a/AppImage/components/proxmox-dashboard.tsx +++ b/AppImage/components/proxmox-dashboard.tsx @@ -1,7 +1,5 @@ "use client" -import type React from "react" - import { useState, useEffect, useMemo, useCallback } from "react" import { Badge } from "./ui/badge" import { Button } from "./ui/button" @@ -55,13 +53,9 @@ interface FlaskSystemData { interface FlaskSystemInfo { hostname: string - uptime_seconds: number - uptime_formatted: string - health: { - status: string - summary: string - } - timestamp: string + node_id: string + uptime: string + health_status: "healthy" | "warning" | "critical" } export function ProxmoxDashboard() { @@ -80,7 +74,6 @@ export function ProxmoxDashboard() { const [showNavigation, setShowNavigation] = useState(true) const [lastScrollY, setLastScrollY] = useState(0) const [showHealthModal, setShowHealthModal] = useState(false) - const [isAuthenticated, setIsAuthenticated] = useState(false) const fetchSystemData = useCallback(async () => { const apiUrl = getApiUrl("/api/system-info") @@ -101,26 +94,14 @@ export function ProxmoxDashboard() { const data: FlaskSystemInfo = await response.json() const uptimeValue = - data.uptime_formatted && typeof data.uptime_formatted === "string" && data.uptime_formatted.trim() !== "" - ? data.uptime_formatted - : "N/A" - - const healthStatus = data.health?.status || "healthy" - const mappedStatus = - healthStatus === "OK" - ? "healthy" - : healthStatus === "WARNING" - ? "warning" - : healthStatus === "CRITICAL" - ? "critical" - : "healthy" + data.uptime && typeof data.uptime === "string" && data.uptime.trim() !== "" ? data.uptime : "N/A" setSystemStatus({ - status: mappedStatus as "healthy" | "warning" | "critical", + status: data.health_status || "healthy", uptime: uptimeValue, lastUpdate: new Date().toLocaleTimeString("en-US", { hour12: false }), serverName: data.hostname || "Unknown", - nodeId: data.hostname || "Unknown", + nodeId: data.node_id || "Unknown", }) setIsServerConnected(true) } catch (error) { @@ -203,18 +184,6 @@ export function ProxmoxDashboard() { setIsRefreshing(false) } - const handleLogout = (e: React.MouseEvent) => { - e.stopPropagation() - localStorage.removeItem("proxmenux-auth-token") - localStorage.removeItem("proxmenux-saved-username") - localStorage.removeItem("proxmenux-saved-password") - window.location.href = "/" - } - - useEffect(() => { - setIsAuthenticated(!!localStorage.getItem("proxmenux-auth-token")) - }, []) - const statusIcon = useMemo(() => { switch (systemStatus.status) { case "healthy": @@ -287,16 +256,18 @@ export function ProxmoxDashboard() { className="border-b border-border bg-card sticky top-0 z-50 shadow-sm cursor-pointer hover:bg-accent/5 transition-colors" onClick={() => setShowHealthModal(true)} > -
-
-
-
+
+ {/* Logo and Title */} +
+ {/* Logo and Title */} +
+
ProxMenux Logo { console.log("[v0] Logo failed to load, using fallback icon") @@ -308,65 +279,85 @@ export function ProxmoxDashboard() { } }} /> - +
-
-

ProxMenux Monitor

-

Proxmox System Dashboard

-
- - Node: {systemStatus.serverName} +
+

ProxMenux Monitor

+

Proxmox System Dashboard

+
+ + Node: {systemStatus.serverName}
-
-
- - {statusIcon} - {systemStatus.status} - - - + {/* Desktop Actions */} +
+
+ +
+
Node: {systemStatus.serverName}
+
-
- - {statusIcon} - + + {statusIcon} + {systemStatus.status} + - +
+ Uptime: {systemStatus.uptime || "N/A"}
+ + +
e.stopPropagation()}> + +
+
+ + {/* Mobile Actions */} +
+ + {statusIcon} + {systemStatus.status} + + + +
e.stopPropagation()}>
-
Uptime: {systemStatus.uptime}
+ {/* Mobile Server Info */} +
+ Uptime: {systemStatus.uptime || "N/A"} +
diff --git a/AppImage/components/settings.tsx b/AppImage/components/settings.tsx index b031017..36ea073 100644 --- a/AppImage/components/settings.tsx +++ b/AppImage/components/settings.tsx @@ -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 } from "lucide-react" +import { Shield, Lock, User, AlertCircle, CheckCircle, Info, LogOut } from "lucide-react" import { getApiUrl } from "../lib/api-config" export function Settings() { @@ -98,7 +98,7 @@ export function Settings() { const handleDisableAuth = async () => { if ( !confirm( - "¿Estás seguro de que quieres deshabilitar la autenticación? Esto eliminará la protección por contraseña de tu dashboard y tendrás que configurarla de nuevo si quieres reactivarla.", + "Are you sure you want to disable authentication? This will remove password protection from your dashboard.", ) ) { return @@ -109,35 +109,31 @@ export function Settings() { 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 ${localStorage.getItem("proxmenux-auth-token")}`, + Authorization: `Bearer ${token}`, }, }) const data = await response.json() if (!response.ok) { - throw new Error(data.error || "Failed to disable authentication") + throw new Error(data.message || "Failed to disable authentication") } localStorage.removeItem("proxmenux-auth-token") - localStorage.removeItem("proxmenux-saved-username") - localStorage.removeItem("proxmenux-saved-password") localStorage.removeItem("proxmenux-auth-setup-complete") - setSuccess("¡Autenticación deshabilitada correctamente! La página se recargará...") + setSuccess("Authentication disabled successfully! Reloading...") - // Recargar la página después de 1.5 segundos setTimeout(() => { window.location.reload() - }, 1500) + }, 1000) } catch (err) { - setError( - err instanceof Error ? err.message : "No se pudo deshabilitar la autenticación. Por favor, inténtalo de nuevo.", - ) + setError(err instanceof Error ? err.message : "Failed to disable authentication. Please try again.") } finally { setLoading(false) } @@ -202,6 +198,7 @@ export function Settings() { const handleLogout = () => { localStorage.removeItem("proxmenux-auth-token") + localStorage.removeItem("proxmenux-auth-setup-complete") window.location.reload() } @@ -338,6 +335,7 @@ export function Settings() { {authEnabled && (
diff --git a/AppImage/scripts/auth_manager.py b/AppImage/scripts/auth_manager.py index a36cdb6..9326737 100644 --- a/AppImage/scripts/auth_manager.py +++ b/AppImage/scripts/auth_manager.py @@ -26,7 +26,6 @@ AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json" JWT_SECRET = "proxmenux-monitor-secret-key-change-in-production" JWT_ALGORITHM = "HS256" TOKEN_EXPIRATION_HOURS = 24 -TOKEN_EXPIRATION_DAYS_REMEMBER = 30 # Token más largo para "recordar contraseña" def ensure_config_dir(): @@ -95,18 +94,15 @@ def verify_password(password, password_hash): return hash_password(password) == password_hash -def generate_token(username, remember_me=False): # Añadido parámetro remember_me +def generate_token(username): """Generate a JWT token for the given username""" if not JWT_AVAILABLE: return None - expiration_delta = timedelta(days=TOKEN_EXPIRATION_DAYS_REMEMBER) if remember_me else timedelta(hours=TOKEN_EXPIRATION_HOURS) - payload = { 'username': username, - 'exp': datetime.utcnow() + expiration_delta, - 'iat': datetime.utcnow(), - 'remember_me': remember_me # Guardar preferencia en el token + 'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS), + 'iat': datetime.utcnow() } try: @@ -151,7 +147,7 @@ def get_auth_status(): config = load_auth_config() return { "auth_enabled": config.get("enabled", False), - "auth_configured": config.get("configured", False), + "auth_configured": config.get("configured", False), # Frontend expects this field name "declined": config.get("declined", False), "username": config.get("username") if config.get("enabled") else None, "authenticated": False # Will be set to True by the route handler if token is valid @@ -206,16 +202,15 @@ def disable_auth(): Disable authentication (different from decline - can be re-enabled) Returns (success: bool, message: str) """ - config = { - "enabled": False, - "username": None, - "password_hash": None, - "declined": False, - "configured": False - } + config = load_auth_config() + config["enabled"] = False + config["username"] = None + config["password_hash"] = None + config["declined"] = False + config["configured"] = False if save_auth_config(config): - return True, "Authentication disabled successfully" + return True, "Authentication disabled" else: return False, "Failed to save configuration" @@ -239,7 +234,7 @@ def enable_auth(): return False, "Failed to save configuration" -def change_password(current_password, new_password): # Corregido nombre del parámetro +def change_password(old_password, new_password): """ Change the authentication password Returns (success: bool, message: str) @@ -249,7 +244,7 @@ def change_password(current_password, new_password): # Corregido nombre del par if not config.get("enabled"): return False, "Authentication is not enabled" - if not verify_password(current_password, config.get("password_hash", "")): + if not verify_password(old_password, config.get("password_hash", "")): return False, "Current password is incorrect" if len(new_password) < 6: @@ -263,7 +258,7 @@ def change_password(current_password, new_password): # Corregido nombre del par return False, "Failed to save new password" -def authenticate(username, password, remember_me=False): # Añadido parámetro remember_me +def authenticate(username, password): """ Authenticate a user with username and password Returns (success: bool, token: str or None, message: str) @@ -279,7 +274,7 @@ def authenticate(username, password, remember_me=False): # Añadido parámetro if not verify_password(password, config.get("password_hash", "")): return False, None, "Invalid username or password" - token = generate_token(username, remember_me) + token = generate_token(username) if token: return True, token, "Authentication successful" else: diff --git a/AppImage/scripts/flask_auth_routes.py b/AppImage/scripts/flask_auth_routes.py index 4e4fcf5..d8b3691 100644 --- a/AppImage/scripts/flask_auth_routes.py +++ b/AppImage/scripts/flask_auth_routes.py @@ -33,47 +33,28 @@ def auth_setup(): username = data.get('username') password = data.get('password') - if not username or not password: - return jsonify({"success": False, "error": "Username and password are required"}), 400 - success, message = auth_manager.setup_auth(username, password) - if success: - # Generate token for immediate login - token = auth_manager.generate_token(username) - return jsonify({"success": True, "message": message, "token": token}) - else: - return jsonify({"success": False, "error": message}), 400 - except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 - - -@auth_bp.route('/api/auth/skip', methods=['POST']) -def auth_skip(): - """Skip authentication setup (user declined)""" - try: - success, message = auth_manager.decline_auth() - if success: return jsonify({"success": True, "message": message}) else: - return jsonify({"success": False, "error": message}), 400 + return jsonify({"success": False, "message": message}), 400 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 @auth_bp.route('/api/auth/decline', methods=['POST']) def auth_decline(): - """Decline authentication setup (deprecated, use /api/auth/skip)""" + """Decline authentication setup""" try: success, message = auth_manager.decline_auth() if success: return jsonify({"success": True, "message": message}) else: - return jsonify({"success": False, "error": message}), 400 + return jsonify({"success": False, "message": message}), 400 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 @auth_bp.route('/api/auth/login', methods=['POST']) @@ -83,19 +64,15 @@ def auth_login(): data = request.json username = data.get('username') password = data.get('password') - remember_me = data.get('remember_me', False) # Soporte para "recordar contraseña" - success, token, message = auth_manager.authenticate(username, password, remember_me) + success, token, message = auth_manager.authenticate(username, password) if success: - response_data = {"success": True, "token": token, "message": message} - if remember_me: - response_data["remember_me"] = True # Indicar al frontend que guarde las credenciales - return jsonify(response_data) + return jsonify({"success": True, "token": token, "message": message}) else: - return jsonify({"success": False, "error": message}), 401 + return jsonify({"success": False, "message": message}), 401 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 @auth_bp.route('/api/auth/enable', methods=['POST']) @@ -107,9 +84,9 @@ def auth_enable(): if success: return jsonify({"success": True, "message": message}) else: - return jsonify({"success": False, "error": message}), 400 + return jsonify({"success": False, "message": message}), 400 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 @auth_bp.route('/api/auth/disable', methods=['POST']) @@ -117,21 +94,17 @@ def auth_disable(): """Disable authentication""" try: token = request.headers.get('Authorization', '').replace('Bearer ', '') - if not token: - return jsonify({"success": False, "error": "Authentication required"}), 401 - - username = auth_manager.verify_token(token) - if not username: - return jsonify({"success": False, "error": "Invalid or expired token"}), 401 - + if not token or not auth_manager.verify_token(token): + return jsonify({"success": False, "message": "Unauthorized"}), 401 + success, message = auth_manager.disable_auth() if success: return jsonify({"success": True, "message": message}) else: - return jsonify({"success": False, "error": message}), 400 + return jsonify({"success": False, "message": message}), 400 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 @auth_bp.route('/api/auth/change-password', methods=['POST']) @@ -139,25 +112,28 @@ def auth_change_password(): """Change authentication password""" try: data = request.json - current_password = data.get('current_password') # Corregido el nombre del campo + old_password = data.get('old_password') new_password = data.get('new_password') - # Verify current authentication - token = request.headers.get('Authorization', '').replace('Bearer ', '') - if not token: - return jsonify({"success": False, "error": "Authentication required"}), 401 - - username = auth_manager.verify_token(token) - if not username: - return jsonify({"success": False, "error": "Invalid or expired token"}), 401 - - success, message = auth_manager.change_password(current_password, new_password) + success, message = auth_manager.change_password(old_password, new_password) if success: - # Generate new token - new_token = auth_manager.generate_token(username) - return jsonify({"success": True, "message": message, "token": new_token}) + return jsonify({"success": True, "message": message}) else: - return jsonify({"success": False, "error": message}), 400 + return jsonify({"success": False, "message": message}), 400 except Exception as e: - return jsonify({"success": False, "error": str(e)}), 500 + return jsonify({"success": False, "message": str(e)}), 500 + + +@auth_bp.route('/api/auth/skip', methods=['POST']) +def auth_skip(): + """Skip authentication setup (same as decline)""" + try: + success, message = auth_manager.decline_auth() + + if success: + return jsonify({"success": True, "message": message}) + else: + return jsonify({"success": False, "message": message}), 400 + except Exception as e: + return jsonify({"success": False, "message": str(e)}), 500