mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2025-11-17 19:16:25 +00:00
Updae AppImage
This commit is contained in:
@@ -311,11 +311,6 @@ export function ProxmoxDashboard() {
|
||||
|
||||
try {
|
||||
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" }
|
||||
|
||||
if (token) {
|
||||
@@ -325,40 +320,23 @@ export function ProxmoxDashboard() {
|
||||
const apiUrl = getApiUrl("/api/auth/status")
|
||||
console.log("[v0] Auth status API URL:", apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
headers,
|
||||
})
|
||||
|
||||
const response = await fetch(apiUrl, { headers })
|
||||
const data = await response.json()
|
||||
console.log("[v0] Auth status response data:", JSON.stringify(data, null, 2))
|
||||
|
||||
setAuthRequired(data.auth_enabled)
|
||||
setIsAuthenticated(data.authenticated)
|
||||
|
||||
// If auth is not configured and user hasn't declined, show the modal
|
||||
const shouldShowModal = !data.auth_configured && !hasDeclined
|
||||
console.log("[v0] Auth configured:", data.auth_configured)
|
||||
console.log("[v0] Should show modal:", shouldShowModal)
|
||||
|
||||
setAuthDeclined(!shouldShowModal)
|
||||
// auth_configured will be true if user either set up auth OR skipped it
|
||||
const shouldShowModal = !data.auth_configured
|
||||
setAuthDeclined(data.auth_configured) // If configured (either way), don't show modal
|
||||
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) {
|
||||
setupTokenRefresh()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[v0] Failed to check auth status:", error)
|
||||
console.log("[v0] ===== AUTH CHECK FAILED =====")
|
||||
setAuthDeclined(false)
|
||||
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 pathlib import Path
|
||||
|
||||
from flask_auth_routes import auth_bp
|
||||
|
||||
app = Flask(__name__)
|
||||
CORS(app) # Enable CORS for Next.js frontend
|
||||
|
||||
# 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
|
||||
app.register_blueprint(auth_bp)
|
||||
|
||||
# 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__)
|
||||
# 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}")
|
||||
pass
|
||||
return {
|
||||
'error': f'Unable to access VM information: {str(e)}',
|
||||
'error': 'Unable to access VM information: {str(e)}',
|
||||
'vms': []
|
||||
}
|
||||
except Exception as e:
|
||||
@@ -3567,7 +3305,7 @@ def get_detailed_gpu_info(gpu):
|
||||
gfx_clock = clocks['GFX_SCLK']
|
||||
if 'value' in gfx_clock:
|
||||
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
|
||||
data_retrieved = True
|
||||
|
||||
@@ -4380,7 +4118,7 @@ def get_hardware_info():
|
||||
# print(f"[v0] Error getting storage info: {e}")
|
||||
pass
|
||||
|
||||
# Graphics Cards (from lspci - will be duplicated by new PCI device listing, but kept for now)
|
||||
# Graphics Cards
|
||||
try:
|
||||
# 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'],
|
||||
|
||||
Reference in New Issue
Block a user