mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-18 03:26:17 +00:00
Updae AppImage
This commit is contained in:
@@ -311,11 +311,6 @@ export function ProxmoxDashboard() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("proxmenux-auth-token")
|
const token = localStorage.getItem("proxmenux-auth-token")
|
||||||
const hasDeclined = localStorage.getItem("proxmenux-auth-declined") === "true"
|
|
||||||
|
|
||||||
console.log("[v0] Token in localStorage:", token ? "EXISTS" : "NOT FOUND")
|
|
||||||
console.log("[v0] Has declined flag:", hasDeclined)
|
|
||||||
|
|
||||||
const headers: HeadersInit = { "Content-Type": "application/json" }
|
const headers: HeadersInit = { "Content-Type": "application/json" }
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
@@ -325,40 +320,23 @@ export function ProxmoxDashboard() {
|
|||||||
const apiUrl = getApiUrl("/api/auth/status")
|
const apiUrl = getApiUrl("/api/auth/status")
|
||||||
console.log("[v0] Auth status API URL:", apiUrl)
|
console.log("[v0] Auth status API URL:", apiUrl)
|
||||||
|
|
||||||
const response = await fetch(apiUrl, {
|
const response = await fetch(apiUrl, { headers })
|
||||||
headers,
|
|
||||||
})
|
|
||||||
|
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
console.log("[v0] Auth status response data:", JSON.stringify(data, null, 2))
|
console.log("[v0] Auth status response data:", JSON.stringify(data, null, 2))
|
||||||
|
|
||||||
setAuthRequired(data.auth_enabled)
|
setAuthRequired(data.auth_enabled)
|
||||||
setIsAuthenticated(data.authenticated)
|
setIsAuthenticated(data.authenticated)
|
||||||
|
|
||||||
// If auth is not configured and user hasn't declined, show the modal
|
// auth_configured will be true if user either set up auth OR skipped it
|
||||||
const shouldShowModal = !data.auth_configured && !hasDeclined
|
const shouldShowModal = !data.auth_configured
|
||||||
console.log("[v0] Auth configured:", data.auth_configured)
|
setAuthDeclined(data.auth_configured) // If configured (either way), don't show modal
|
||||||
console.log("[v0] Should show modal:", shouldShowModal)
|
|
||||||
|
|
||||||
setAuthDeclined(!shouldShowModal)
|
|
||||||
setAuthChecked(true)
|
setAuthChecked(true)
|
||||||
|
|
||||||
console.log("[v0] Final auth state:", {
|
|
||||||
authRequired: data.auth_enabled,
|
|
||||||
isAuthenticated: data.authenticated,
|
|
||||||
authConfigured: data.auth_configured,
|
|
||||||
hasDeclined: hasDeclined,
|
|
||||||
shouldShowModal: shouldShowModal,
|
|
||||||
authDeclined: !shouldShowModal,
|
|
||||||
})
|
|
||||||
console.log("[v0] ===== AUTH CHECK END =====")
|
|
||||||
|
|
||||||
if (data.authenticated && token) {
|
if (data.authenticated && token) {
|
||||||
setupTokenRefresh()
|
setupTokenRefresh()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[v0] Failed to check auth status:", error)
|
console.error("[v0] Failed to check auth status:", error)
|
||||||
console.log("[v0] ===== AUTH CHECK FAILED =====")
|
|
||||||
setAuthDeclined(false)
|
setAuthDeclined(false)
|
||||||
setAuthChecked(true)
|
setAuthChecked(true)
|
||||||
}
|
}
|
||||||
|
|||||||
277
AppImage/scripts/auth_manager.py
Normal file
277
AppImage/scripts/auth_manager.py
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
"""
|
||||||
|
Authentication Manager Module
|
||||||
|
Handles all authentication-related operations including:
|
||||||
|
- Loading/saving auth configuration
|
||||||
|
- Password hashing and verification
|
||||||
|
- JWT token generation and validation
|
||||||
|
- Auth status checking
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jwt
|
||||||
|
JWT_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
JWT_AVAILABLE = False
|
||||||
|
print("Warning: PyJWT not available. Authentication features will be limited.")
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONFIG_DIR = Path.home() / ".config" / "proxmenux-monitor"
|
||||||
|
AUTH_CONFIG_FILE = CONFIG_DIR / "auth.json"
|
||||||
|
JWT_SECRET = "proxmenux-monitor-secret-key-change-in-production"
|
||||||
|
JWT_ALGORITHM = "HS256"
|
||||||
|
TOKEN_EXPIRATION_HOURS = 24
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_config_dir():
|
||||||
|
"""Ensure the configuration directory exists"""
|
||||||
|
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def load_auth_config():
|
||||||
|
"""
|
||||||
|
Load authentication configuration from file
|
||||||
|
Returns dict with structure:
|
||||||
|
{
|
||||||
|
"enabled": bool,
|
||||||
|
"username": str,
|
||||||
|
"password_hash": str,
|
||||||
|
"declined": bool, # True if user explicitly declined auth
|
||||||
|
"configured": bool # True if auth has been set up (enabled or declined)
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if not AUTH_CONFIG_FILE.exists():
|
||||||
|
return {
|
||||||
|
"enabled": False,
|
||||||
|
"username": None,
|
||||||
|
"password_hash": None,
|
||||||
|
"declined": False,
|
||||||
|
"configured": False
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(AUTH_CONFIG_FILE, 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
# Ensure all required fields exist
|
||||||
|
config.setdefault("declined", False)
|
||||||
|
config.setdefault("configured", config.get("enabled", False) or config.get("declined", False))
|
||||||
|
return config
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading auth config: {e}")
|
||||||
|
return {
|
||||||
|
"enabled": False,
|
||||||
|
"username": None,
|
||||||
|
"password_hash": None,
|
||||||
|
"declined": False,
|
||||||
|
"configured": False
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def save_auth_config(config):
|
||||||
|
"""Save authentication configuration to file"""
|
||||||
|
ensure_config_dir()
|
||||||
|
try:
|
||||||
|
with open(AUTH_CONFIG_FILE, 'w') as f:
|
||||||
|
json.dump(config, f, indent=2)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving auth config: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def hash_password(password):
|
||||||
|
"""Hash a password using SHA-256"""
|
||||||
|
return hashlib.sha256(password.encode()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def verify_password(password, password_hash):
|
||||||
|
"""Verify a password against its hash"""
|
||||||
|
return hash_password(password) == password_hash
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token(username):
|
||||||
|
"""Generate a JWT token for the given username"""
|
||||||
|
if not JWT_AVAILABLE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
'username': username,
|
||||||
|
'exp': datetime.utcnow() + timedelta(hours=TOKEN_EXPIRATION_HOURS),
|
||||||
|
'iat': datetime.utcnow()
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
token = jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
|
return token
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error generating token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def verify_token(token):
|
||||||
|
"""
|
||||||
|
Verify a JWT token
|
||||||
|
Returns username if valid, None otherwise
|
||||||
|
"""
|
||||||
|
if not JWT_AVAILABLE or not token:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
|
return payload.get('username')
|
||||||
|
except jwt.ExpiredSignatureError:
|
||||||
|
print("Token has expired")
|
||||||
|
return None
|
||||||
|
except jwt.InvalidTokenError as e:
|
||||||
|
print(f"Invalid token: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_auth_status():
|
||||||
|
"""
|
||||||
|
Get current authentication status
|
||||||
|
Returns dict with:
|
||||||
|
{
|
||||||
|
"enabled": bool,
|
||||||
|
"configured": bool,
|
||||||
|
"declined": bool,
|
||||||
|
"username": str or None
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
return {
|
||||||
|
"enabled": config.get("enabled", False),
|
||||||
|
"configured": config.get("configured", False),
|
||||||
|
"declined": config.get("declined", False),
|
||||||
|
"username": config.get("username") if config.get("enabled") else None
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def setup_auth(username, password):
|
||||||
|
"""
|
||||||
|
Set up authentication with username and password
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
if not username or not password:
|
||||||
|
return False, "Username and password are required"
|
||||||
|
|
||||||
|
if len(password) < 6:
|
||||||
|
return False, "Password must be at least 6 characters"
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"enabled": True,
|
||||||
|
"username": username,
|
||||||
|
"password_hash": hash_password(password),
|
||||||
|
"declined": False,
|
||||||
|
"configured": True
|
||||||
|
}
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication configured successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save authentication configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def decline_auth():
|
||||||
|
"""
|
||||||
|
Mark authentication as declined by user
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
config["enabled"] = False
|
||||||
|
config["declined"] = True
|
||||||
|
config["configured"] = True
|
||||||
|
config["username"] = None
|
||||||
|
config["password_hash"] = None
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication declined"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def disable_auth():
|
||||||
|
"""
|
||||||
|
Disable authentication (different from decline - can be re-enabled)
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
config["enabled"] = False
|
||||||
|
# Keep configured=True and don't set declined=True
|
||||||
|
# This allows re-enabling without showing the setup modal again
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication disabled"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def enable_auth():
|
||||||
|
"""
|
||||||
|
Enable authentication (must already be configured)
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("username") or not config.get("password_hash"):
|
||||||
|
return False, "Authentication not configured. Please set up username and password first."
|
||||||
|
|
||||||
|
config["enabled"] = True
|
||||||
|
config["declined"] = False
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Authentication enabled"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save configuration"
|
||||||
|
|
||||||
|
|
||||||
|
def change_password(old_password, new_password):
|
||||||
|
"""
|
||||||
|
Change the authentication password
|
||||||
|
Returns (success: bool, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("enabled"):
|
||||||
|
return False, "Authentication is not enabled"
|
||||||
|
|
||||||
|
if not verify_password(old_password, config.get("password_hash", "")):
|
||||||
|
return False, "Current password is incorrect"
|
||||||
|
|
||||||
|
if len(new_password) < 6:
|
||||||
|
return False, "New password must be at least 6 characters"
|
||||||
|
|
||||||
|
config["password_hash"] = hash_password(new_password)
|
||||||
|
|
||||||
|
if save_auth_config(config):
|
||||||
|
return True, "Password changed successfully"
|
||||||
|
else:
|
||||||
|
return False, "Failed to save new password"
|
||||||
|
|
||||||
|
|
||||||
|
def authenticate(username, password):
|
||||||
|
"""
|
||||||
|
Authenticate a user with username and password
|
||||||
|
Returns (success: bool, token: str or None, message: str)
|
||||||
|
"""
|
||||||
|
config = load_auth_config()
|
||||||
|
|
||||||
|
if not config.get("enabled"):
|
||||||
|
return False, None, "Authentication is not enabled"
|
||||||
|
|
||||||
|
if username != config.get("username"):
|
||||||
|
return False, None, "Invalid username or password"
|
||||||
|
|
||||||
|
if not verify_password(password, config.get("password_hash", "")):
|
||||||
|
return False, None, "Invalid username or password"
|
||||||
|
|
||||||
|
token = generate_token(username)
|
||||||
|
if token:
|
||||||
|
return True, token, "Authentication successful"
|
||||||
|
else:
|
||||||
|
return False, None, "Failed to generate authentication token"
|
||||||
116
AppImage/scripts/flask_auth_routes.py
Normal file
116
AppImage/scripts/flask_auth_routes.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
Flask Authentication Routes
|
||||||
|
Provides REST API endpoints for authentication management
|
||||||
|
"""
|
||||||
|
|
||||||
|
from flask import jsonify, request
|
||||||
|
import auth_manager
|
||||||
|
|
||||||
|
|
||||||
|
def register_auth_routes(app):
|
||||||
|
"""Register authentication routes with the Flask app"""
|
||||||
|
|
||||||
|
@app.route('/api/auth/status', methods=['GET'])
|
||||||
|
def auth_status():
|
||||||
|
"""Get current authentication status"""
|
||||||
|
try:
|
||||||
|
status = auth_manager.get_auth_status()
|
||||||
|
return jsonify(status)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/setup', methods=['POST'])
|
||||||
|
def auth_setup():
|
||||||
|
"""Set up authentication with username and password"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
success, message = auth_manager.setup_auth(username, password)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/decline', methods=['POST'])
|
||||||
|
def auth_decline():
|
||||||
|
"""Decline authentication setup"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/login', methods=['POST'])
|
||||||
|
def auth_login():
|
||||||
|
"""Authenticate user and return JWT token"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
username = data.get('username')
|
||||||
|
password = data.get('password')
|
||||||
|
|
||||||
|
success, token, message = auth_manager.authenticate(username, password)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
return jsonify({"success": True, "token": token, "message": message})
|
||||||
|
else:
|
||||||
|
return jsonify({"success": False, "message": message}), 401
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"success": False, "message": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/enable', methods=['POST'])
|
||||||
|
def auth_enable():
|
||||||
|
"""Enable authentication"""
|
||||||
|
try:
|
||||||
|
success, message = auth_manager.enable_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
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/disable', methods=['POST'])
|
||||||
|
def auth_disable():
|
||||||
|
"""Disable authentication"""
|
||||||
|
try:
|
||||||
|
success, message = auth_manager.disable_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
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/auth/change-password', methods=['POST'])
|
||||||
|
def auth_change_password():
|
||||||
|
"""Change authentication password"""
|
||||||
|
try:
|
||||||
|
data = request.json
|
||||||
|
old_password = data.get('old_password')
|
||||||
|
new_password = data.get('new_password')
|
||||||
|
|
||||||
|
success, message = auth_manager.change_password(old_password, new_password)
|
||||||
|
|
||||||
|
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
|
||||||
@@ -28,275 +28,13 @@ import jwt
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask_auth_routes import auth_bp
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app) # Enable CORS for Next.js frontend
|
CORS(app) # Enable CORS for Next.js frontend
|
||||||
|
|
||||||
# Authentication configuration
|
app.register_blueprint(auth_bp)
|
||||||
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 {} # Return empty dict if file doesn't exist
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(AUTH_CONFIG_FILE, 'r') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (json.JSONDecodeError, FileNotFoundError): # Handle potential errors
|
|
||||||
return {} # Return empty dict on error
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Authentication endpoints
|
|
||||||
@app.route('/api/auth/status', methods=['GET'])
|
|
||||||
def auth_status():
|
|
||||||
"""Check if authentication is enabled and if current session is valid"""
|
|
||||||
try:
|
|
||||||
auth_config = load_auth_config()
|
|
||||||
is_configured = auth_config is not None and len(auth_config) > 0
|
|
||||||
is_enabled = auth_config.get("auth_enabled", False) if is_configured else False
|
|
||||||
|
|
||||||
# Check if user has valid token
|
|
||||||
is_authenticated = False
|
|
||||||
if is_enabled:
|
|
||||||
auth_header = request.headers.get('Authorization')
|
|
||||||
if auth_header and auth_header.startswith('Bearer '):
|
|
||||||
token = auth_header.split(' ')[1]
|
|
||||||
try:
|
|
||||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
|
||||||
if time.time() <= payload.get('exp', 0):
|
|
||||||
is_authenticated = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"auth_enabled": is_enabled,
|
|
||||||
"auth_configured": is_configured, # New field to indicate if auth has been set up
|
|
||||||
"authenticated": is_authenticated or not is_enabled
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/setup', methods=['POST'])
|
|
||||||
def auth_setup():
|
|
||||||
"""Setup authentication for the first time"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get('username', '').strip()
|
|
||||||
password = data.get('password', '').strip()
|
|
||||||
|
|
||||||
if not username or not password:
|
|
||||||
return jsonify({"error": "Username and password are required"}), 400
|
|
||||||
|
|
||||||
if len(password) < 6:
|
|
||||||
return jsonify({"error": "Password must be at least 6 characters"}), 400
|
|
||||||
|
|
||||||
# Hash password and save config
|
|
||||||
password_hash = hash_password(password)
|
|
||||||
|
|
||||||
auth_config = {
|
|
||||||
"auth_enabled": True,
|
|
||||||
"username": username,
|
|
||||||
"password_hash": password_hash,
|
|
||||||
"created_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
save_auth_config(auth_config)
|
|
||||||
|
|
||||||
# Generate JWT token
|
|
||||||
token = jwt.encode({
|
|
||||||
'username': username,
|
|
||||||
'exp': time.time() + SESSION_TIMEOUT
|
|
||||||
}, JWT_SECRET, algorithm='HS256')
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"token": token
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/skip', methods=['POST'])
|
|
||||||
def auth_skip():
|
|
||||||
"""Skip authentication setup"""
|
|
||||||
try:
|
|
||||||
auth_config = {
|
|
||||||
"auth_enabled": False,
|
|
||||||
"skipped": True,
|
|
||||||
"skipped_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
save_auth_config(auth_config)
|
|
||||||
|
|
||||||
return jsonify({"success": True})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/login', methods=['POST'])
|
|
||||||
def auth_login():
|
|
||||||
"""Login with username and password"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
username = data.get('username', '').strip()
|
|
||||||
password = data.get('password', '').strip()
|
|
||||||
|
|
||||||
if not username or not password:
|
|
||||||
return jsonify({"error": "Username and password are required"}), 400
|
|
||||||
|
|
||||||
# Load auth config
|
|
||||||
auth_config = load_auth_config()
|
|
||||||
|
|
||||||
if not auth_config or not auth_config.get("auth_enabled", False):
|
|
||||||
return jsonify({"error": "Authentication is not enabled"}), 400
|
|
||||||
|
|
||||||
# Verify credentials
|
|
||||||
stored_username = auth_config.get("username", "")
|
|
||||||
stored_password_hash = auth_config.get("password_hash", "")
|
|
||||||
|
|
||||||
if username != stored_username or hash_password(password) != stored_password_hash:
|
|
||||||
return jsonify({"error": "Invalid username or password"}), 401
|
|
||||||
|
|
||||||
# Generate JWT token
|
|
||||||
token = jwt.encode({
|
|
||||||
'username': username,
|
|
||||||
'exp': time.time() + SESSION_TIMEOUT
|
|
||||||
}, JWT_SECRET, algorithm='HS256')
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"token": token
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/refresh', methods=['POST'])
|
|
||||||
def auth_refresh():
|
|
||||||
"""Refresh JWT token"""
|
|
||||||
try:
|
|
||||||
auth_header = request.headers.get('Authorization')
|
|
||||||
if not auth_header or not auth_header.startswith('Bearer '):
|
|
||||||
return jsonify({"error": "No token provided"}), 401
|
|
||||||
|
|
||||||
token = auth_header.split(' ')[1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Verify current token
|
|
||||||
payload = jwt.decode(token, JWT_SECRET, algorithms=['HS256'])
|
|
||||||
username = payload.get('username')
|
|
||||||
|
|
||||||
# Generate new token
|
|
||||||
new_token = jwt.encode({
|
|
||||||
'username': username,
|
|
||||||
'exp': time.time() + SESSION_TIMEOUT
|
|
||||||
}, JWT_SECRET, algorithm='HS256')
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"success": True,
|
|
||||||
"token": new_token
|
|
||||||
})
|
|
||||||
except jwt.InvalidTokenError:
|
|
||||||
return jsonify({"error": "Invalid token"}), 401
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/logout', methods=['POST'])
|
|
||||||
@require_auth
|
|
||||||
def auth_logout():
|
|
||||||
"""Logout (client should delete token)"""
|
|
||||||
return jsonify({"success": True})
|
|
||||||
|
|
||||||
@app.route('/api/auth/disable', methods=['POST'])
|
|
||||||
@require_auth
|
|
||||||
def auth_disable():
|
|
||||||
"""Disable authentication"""
|
|
||||||
try:
|
|
||||||
auth_config = {
|
|
||||||
"auth_enabled": False,
|
|
||||||
"disabled_at": datetime.now().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
save_auth_config(auth_config)
|
|
||||||
|
|
||||||
return jsonify({"success": True})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
@app.route('/api/auth/change-password', methods=['POST'])
|
|
||||||
@require_auth
|
|
||||||
def auth_change_password():
|
|
||||||
"""Change password"""
|
|
||||||
try:
|
|
||||||
data = request.get_json()
|
|
||||||
current_password = data.get('current_password', '').strip()
|
|
||||||
new_password = data.get('new_password', '').strip()
|
|
||||||
|
|
||||||
if not current_password or not new_password:
|
|
||||||
return jsonify({"error": "Current and new password are required"}), 400
|
|
||||||
|
|
||||||
if len(new_password) < 6:
|
|
||||||
return jsonify({"error": "New password must be at least 6 characters"}), 400
|
|
||||||
|
|
||||||
# Load auth config
|
|
||||||
auth_config = load_auth_config()
|
|
||||||
|
|
||||||
# Verify current password
|
|
||||||
stored_password_hash = auth_config.get("password_hash", "")
|
|
||||||
if hash_password(current_password) != stored_password_hash:
|
|
||||||
return jsonify({"error": "Current password is incorrect"}), 401
|
|
||||||
|
|
||||||
# Update password
|
|
||||||
auth_config["password_hash"] = hash_password(new_password)
|
|
||||||
auth_config["updated_at"] = datetime.now().isoformat()
|
|
||||||
|
|
||||||
save_auth_config(auth_config)
|
|
||||||
|
|
||||||
return jsonify({"success": True})
|
|
||||||
except Exception as e:
|
|
||||||
return jsonify({"error": str(e)}), 500
|
|
||||||
|
|
||||||
# app = Flask(__name__)
|
# app = Flask(__name__)
|
||||||
# CORS(app) # Enable CORS for Next.js frontend
|
# CORS(app) # Enable CORS for Next.js frontend
|
||||||
@@ -2465,7 +2203,7 @@ def get_proxmox_vms():
|
|||||||
# print(f"[v0] Error getting VM/LXC info: {e}")
|
# print(f"[v0] Error getting VM/LXC info: {e}")
|
||||||
pass
|
pass
|
||||||
return {
|
return {
|
||||||
'error': f'Unable to access VM information: {str(e)}',
|
'error': 'Unable to access VM information: {str(e)}',
|
||||||
'vms': []
|
'vms': []
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -3567,7 +3305,7 @@ def get_detailed_gpu_info(gpu):
|
|||||||
gfx_clock = clocks['GFX_SCLK']
|
gfx_clock = clocks['GFX_SCLK']
|
||||||
if 'value' in gfx_clock:
|
if 'value' in gfx_clock:
|
||||||
detailed_info['clock_graphics'] = f"{gfx_clock['value']} MHz"
|
detailed_info['clock_graphics'] = f"{gfx_clock['value']} MHz"
|
||||||
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']}", flush=True)
|
# print(f"[v0] Graphics Clock: {detailed_info['clock_graphics']} MHz", flush=True)
|
||||||
pass
|
pass
|
||||||
data_retrieved = True
|
data_retrieved = True
|
||||||
|
|
||||||
@@ -4380,7 +4118,7 @@ def get_hardware_info():
|
|||||||
# print(f"[v0] Error getting storage info: {e}")
|
# print(f"[v0] Error getting storage info: {e}")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Graphics Cards (from lspci - will be duplicated by new PCI device listing, but kept for now)
|
# Graphics Cards
|
||||||
try:
|
try:
|
||||||
# Try nvidia-smi first
|
# Try nvidia-smi first
|
||||||
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw,utilization.gpu,utilization.memory,clocks.graphics,clocks.memory', '--format=csv,noheader,nounits'],
|
result = subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total,memory.used,temperature.gpu,power.draw,utilization.gpu,utilization.memory,clocks.graphics,clocks.memory', '--format=csv,noheader,nounits'],
|
||||||
|
|||||||
Reference in New Issue
Block a user