File main.py of Package PersistenceOS
#!/usr/bin/env python3
'''
FILE : main.py
PROJECT : PersistenceOS
COPYRIGHT : (c) 2024 PersistenceOS Team
AUTHOR : PersistenceOS Team
PACKAGE : PersistenceOS
LICENSE : MIT
PURPOSE : Main FastAPI application entry point
'''
import os
import sys
import json
import logging
# Add user site-packages to Python path (for pip --user installations)
import site
user_site = site.getusersitepackages()
if user_site and os.path.exists(user_site):
sys.path.insert(0, user_site)
# Try importing required packages with helpful error messages
try:
import uvicorn
from fastapi import FastAPI, Depends, HTTPException, status, Request, Form
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.responses import JSONResponse, FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from typing import Optional, Dict, List, Any
from datetime import datetime, timedelta
import subprocess
import socket
import platform
import psutil
from fastapi.templating import Jinja2Templates
except ImportError as e:
print(f"Error importing required packages: {e}")
print("Please ensure FastAPI, Uvicorn, and psutil are installed:")
print("Try: pip3.11 install --user fastapi uvicorn psutil")
print("Or: pip3 install --user fastapi uvicorn psutil")
sys.exit(1)
# Setup logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger("persistenceos.api")
# Environment variables
API_PORT = int(os.environ.get("API_PORT", 8080))
DEBUG_MODE = os.environ.get("DEBUG_MODE", "false").lower() == "true"
WEB_ROOT = os.environ.get("WEB_ROOT", "/usr/lib/persistence/web-ui")
API_CONFIG_PATH = os.environ.get("API_CONFIG_PATH", "/usr/lib/persistence/web-ui/api-config.json")
# Add this function after the logger initialization
def check_web_files():
"""Check and log all available web UI files at startup."""
logger.info("Checking web UI files...")
paths_to_check = [
WEB_ROOT,
"/var/lib/persistence/web-ui",
"/usr/lib/persistence/web-ui"
]
for path in paths_to_check:
if os.path.exists(path):
logger.info(f"Found web UI directory: {path}")
try:
# Count files in directory
file_count = sum(1 for _ in os.walk(path))
logger.info(f"Directory {path} contains {file_count} files/directories")
# Check for login.html specifically
login_path = os.path.join(path, "login.html")
if os.path.exists(login_path):
logger.info(f"Found login.html at {login_path} ({os.path.getsize(login_path)} bytes)")
else:
logger.warning(f"login.html not found at {login_path}")
# Check static directory
static_path = os.path.join(path, "static")
if os.path.exists(static_path) and os.path.isdir(static_path):
logger.info(f"Found static directory at {static_path}")
static_files = []
for root, dirs, files in os.walk(static_path):
for file in files:
static_files.append(os.path.join(root, file))
logger.info(f"Static directory contains {len(static_files)} files")
else:
logger.warning(f"Static directory not found at {static_path}")
# Log a few root files
try:
root_files = [f for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
logger.info(f"Root files in {path}: {', '.join(root_files[:5])}{'...' if len(root_files) > 5 else ''}")
except Exception as e:
logger.error(f"Error listing files in {path}: {str(e)}")
except Exception as e:
logger.error(f"Error examining {path}: {str(e)}")
else:
logger.warning(f"Web UI directory not found: {path}")
logger.info("Web UI files check complete")
# Create FastAPI app
app = FastAPI(
title="PersistenceOS API",
description="API for PersistenceOS management interface",
version="6.1.0",
docs_url="/api/docs" if DEBUG_MODE else None,
redoc_url="/api/redoc" if DEBUG_MODE else None,
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Check web files at startup
check_web_files()
# OAuth2 security scheme
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/token")
# =============================================
# User authentication and token handling
# =============================================
# In-memory token store (for development; in production use proper storage)
active_tokens = {}
# Mock user database (in production, this would come from PAM or system users)
def get_user(username: str) -> Optional[dict]:
"""Get user by username from system users or mock DB in development mode."""
if DEBUG_MODE:
# Development mode - use mock users
users = {
"root": {
"username": "root",
"display_name": "Administrator",
"roles": ["admin"],
"email": "admin@persistenceos.org",
"password": "linux" # only used in development
}
}
return users.get(username)
else:
# Production mode - validate against system users
try:
# Simple check if user exists on system
result = subprocess.run(
["getent", "passwd", username],
capture_output=True, text=True, check=False
)
if result.returncode == 0:
# User exists, create basic user object
return {
"username": username,
"display_name": username,
"roles": ["admin"] if username == "root" else ["user"]
}
except Exception as e:
logger.error(f"Error checking system user: {e}")
return None
def authenticate_user(username: str, password: str) -> Optional[dict]:
"""Authenticate user against system or mock DB in development mode."""
user = get_user(username)
if not user:
return None
if DEBUG_MODE:
# In development mode, check against mock password
if user.get("password") == password:
return user
else:
# In production, validate against system authentication
try:
# This is a mock implementation - would use PAM in real system
# For security reasons we don't implement actual auth here
# but use mock successful auth for root/linux
if username == "root" and password == "linux":
return user
except Exception as e:
logger.error(f"Authentication error: {e}")
return None
def create_access_token(data: dict, expires_delta: timedelta = None) -> str:
"""Create a new access token."""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=60)
to_encode.update({"exp": expire})
token = f"dev-token-{os.urandom(8).hex()}" # Mock token
# Store token with expiration
active_tokens[token] = {
"data": to_encode,
"expires_at": int(expire.timestamp() * 1000) # milliseconds
}
return token
async def get_current_user(token: str = Depends(oauth2_scheme)) -> dict:
"""Get the current user from a token."""
if token not in active_tokens:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token or token expired",
headers={"WWW-Authenticate": "Bearer"},
)
token_data = active_tokens[token]
# Check if token is expired
now = datetime.utcnow()
if now.timestamp() * 1000 > token_data["expires_at"]:
# Remove expired token
del active_tokens[token]
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"},
)
user_data = token_data["data"].get("sub")
user = get_user(user_data)
if user is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
headers={"WWW-Authenticate": "Bearer"},
)
return user
async def get_current_user_optional(token: str) -> Optional[dict]:
"""Get current user from token without raising exceptions."""
try:
if not token:
return None
if token not in active_tokens:
return None
token_data = active_tokens[token]
# Check if token is expired
now = datetime.utcnow()
if now.timestamp() * 1000 > token_data["expires_at"]:
# Remove expired token
del active_tokens[token]
return None
user_data = token_data["data"].get("sub")
user = get_user(user_data)
return user
except Exception as e:
logger.error(f"Error validating token: {e}")
return None
# =============================================
# API Routes
# =============================================
# Configure templates with the correct directory
templates_dir = os.path.join(WEB_ROOT)
templates = Jinja2Templates(directory=templates_dir)
logger.info(f"Templates directory configured: {templates_dir}")
# Static files will be mounted later in configure_static_files() to avoid conflicts
# with specific route handlers
logger.info("Static file mounting deferred to configure_static_files() function")
@app.get("/")
async def root():
"""Redirect to login page."""
logger.info(f"Accessing root path, redirecting to login")
# Redirect to /login for the login page
return RedirectResponse(url="/login")
@app.get("/login")
async def serve_login(request: Request):
"""Serve the bulletproof login page"""
logger.info("Serving bulletproof login page")
# Try to serve bulletproof-login.js first
bulletproof_login_paths = [
os.path.join(WEB_ROOT, "js/bulletproof-login.js"),
os.path.join(WEB_ROOT, "static/js/bulletproof-login.js"),
"/usr/lib/persistence/web-ui/js/bulletproof-login.js"
]
for login_path in bulletproof_login_paths:
if os.path.exists(login_path):
logger.info(f"Serving bulletproof login from: {login_path}")
return FileResponse(login_path, media_type="application/javascript")
# Fallback to legacy login.html if bulletproof not available
legacy_paths = [
os.path.join(WEB_ROOT, "static/login.html"),
os.path.join(WEB_ROOT, "login.html"),
"/usr/lib/persistence/web-ui/static/login.html",
"/usr/lib/persistence/web-ui/login.html"
]
for login_path in legacy_paths:
if os.path.exists(login_path):
logger.info(f"Serving legacy login page from: {login_path}")
return FileResponse(login_path, media_type="text/html")
# Final fallback to legacy JS login
logger.warning("No login files found, falling back to legacy JS login")
return RedirectResponse(url="/js/login.js", status_code=302)
@app.get("/index", response_class=HTMLResponse)
async def serve_index(request: Request, current_user: dict = Depends(get_current_user)):
"""Protected dashboard route"""
index_path = os.path.join(WEB_ROOT, "index.html")
logger.info(f"Serving index from: {index_path}")
if os.path.exists(index_path):
try:
return templates.TemplateResponse(
"index.html",
{
"request": request,
"user": current_user
}
)
except Exception as e:
logger.error(f"Error rendering index template: {str(e)}")
# Fallback to direct file response if templating fails
return FileResponse(
index_path,
media_type="text/html"
)
else:
logger.error(f"index.html not found at {index_path}")
raise HTTPException(status_code=404, detail=f"index.html not found")
@app.get("/index.html", response_class=HTMLResponse)
async def serve_index_html(request: Request, current_user: dict = Depends(get_current_user)):
"""Alternative route for index.html"""
return await serve_index(request, current_user)
@app.get("/api/debug/files")
async def debug_files():
"""Debug endpoint to list files in the web root."""
result = {
"web_root": WEB_ROOT,
"exists": os.path.exists(WEB_ROOT),
"files": [],
"subdirs": []
}
if os.path.exists(WEB_ROOT):
# List direct files
for item in os.listdir(WEB_ROOT):
item_path = os.path.join(WEB_ROOT, item)
if os.path.isfile(item_path):
result["files"].append({
"name": item,
"size": os.path.getsize(item_path),
"readable": os.access(item_path, os.R_OK),
"modified": datetime.fromtimestamp(os.path.getmtime(item_path)).isoformat()
})
elif os.path.isdir(item_path):
subdir = {"name": item, "files": []}
# List files in subdirectory
try:
for subitem in os.listdir(item_path):
subitem_path = os.path.join(item_path, subitem)
if os.path.isfile(subitem_path):
subdir["files"].append({
"name": subitem,
"size": os.path.getsize(subitem_path),
"readable": os.access(subitem_path, os.R_OK)
})
except Exception as e:
subdir["error"] = str(e)
result["subdirs"].append(subdir)
return result
@app.get("/api/debug/js-files")
async def debug_js_files():
"""Debug endpoint specifically for JavaScript files."""
js_dir = os.path.join(WEB_ROOT, "js")
result = {
"web_root": WEB_ROOT,
"js_directory": js_dir,
"exists": os.path.exists(js_dir),
"files": [],
"current_working_directory": os.getcwd(),
"absolute_js_dir": os.path.abspath(js_dir)
}
if os.path.exists(js_dir):
try:
for item in os.listdir(js_dir):
item_path = os.path.join(js_dir, item)
if os.path.isfile(item_path):
result["files"].append({
"name": item,
"path": item_path,
"absolute_path": os.path.abspath(item_path),
"size": os.path.getsize(item_path),
"readable": os.access(item_path, os.R_OK),
"modified": datetime.fromtimestamp(os.path.getmtime(item_path)).isoformat()
})
except Exception as e:
result["error"] = str(e)
# Also check specific files we're looking for
specific_files = ["app.js", "auth.js", "vue.js"]
result["specific_file_checks"] = {}
for filename in specific_files:
file_path = os.path.join(js_dir, filename)
result["specific_file_checks"][filename] = {
"path": file_path,
"exists": os.path.exists(file_path),
"absolute_path": os.path.abspath(file_path),
"readable": os.access(file_path, os.R_OK) if os.path.exists(file_path) else False
}
return result
@app.get("/api/debug/webui-path")
async def debug_webui_path(request: Request):
"""Debug endpoint to check web UI path and file structure."""
logger.info("Debug webui-path endpoint accessed")
files = []
for root, _, filenames in os.walk(WEB_ROOT):
for filename in filenames:
full_path = os.path.join(root, filename)
rel_path = os.path.relpath(full_path, WEB_ROOT)
files.append({
"path": rel_path,
"size": os.path.getsize(full_path),
"readable": os.access(full_path, os.R_OK),
"executable": os.access(full_path, os.X_OK),
"modified": os.path.getmtime(full_path)
})
# Also include root_path information from FastAPI
return {
"webui_path": WEB_ROOT,
"exists": os.path.exists(WEB_ROOT),
"file_count": len(files),
"files": files,
"root_path": request.scope.get("root_path", ""),
}
@app.get("/api/debug/fallback")
async def debug_fallback():
"""Debug fallback route handler."""
logger.error(f"File not found in fallback route: {{full_path}}")
return JSONResponse(
status_code=404,
content={"detail": f"{{path}} not found at {{full_path}}"}
)
# Protected endpoint example
@app.get("/api/protected")
async def protected_route(current_user: dict = Depends(get_current_user)):
if not current_user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required",
headers={"WWW-Authenticate": "Bearer"},
)
return {
"message": f"Hello, {current_user['username']}! This is a protected endpoint.",
"user": current_user
}
@app.get("/api/health")
async def health_check():
"""Health check endpoint."""
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
@app.get("/api/config")
async def get_config():
"""Get server configuration."""
try:
# Get primary IP address
hostname = socket.gethostname()
primary_ip = socket.gethostbyname(hostname)
# Get all non-loopback IPv4 addresses
available_ips = []
for interface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if addr.family == socket.AF_INET and not addr.address.startswith('127.'):
available_ips.append(addr.address)
# Create config object
config = {
"host": primary_ip,
"port": API_PORT,
"http_port": API_PORT,
"https_port": 8443, # Default HTTPS port
"api_base_url": f"http://{primary_ip}:{API_PORT}/api",
"secure_api_base_url": f"https://{primary_ip}:8443/api",
"available_ips": available_ips,
"features": {
"ssl_enabled": os.path.exists("/etc/ssl/private/persistenceos.key"),
"api_enabled": True,
"debug_enabled": DEBUG_MODE
},
"system": {
"version": "6.1.0",
"hostname": hostname,
"platform": platform.platform(),
"generated_at": datetime.utcnow().isoformat()
}
}
# Save to api-config.json for web UI
with open(API_CONFIG_PATH, 'w') as f:
json.dump(config, f, indent=2)
return config
except Exception as e:
logger.error(f"Error generating config: {e}")
return JSONResponse(
status_code=500,
content={"error": f"Failed to generate configuration: {str(e)}"}
)
@app.get("/api-config.json")
async def api_config_json():
"""Return the API configuration for web UI."""
try:
# Check if file exists, otherwise generate it
if not os.path.exists(API_CONFIG_PATH):
await get_config()
# Return the file
return FileResponse(API_CONFIG_PATH)
except Exception as e:
logger.error(f"Error serving API config: {e}")
return JSONResponse(
status_code=500,
content={"error": f"Failed to serve API configuration: {str(e)}"}
)
@app.post("/api/auth/token")
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
"""
OAuth2 compatible token login, get an access token for future requests.
This endpoint follows the OAuth2 password flow standard.
"""
user = authenticate_user(form_data.username, form_data.password)
if not user:
logger.warning(f"Failed login attempt for user: {form_data.username}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"},
)
logger.info(f"Successful login for user: {form_data.username}")
# Create access token with 1 hour expiry
access_token_expires = timedelta(hours=1)
access_token = create_access_token(
data={"sub": user["username"]},
expires_delta=access_token_expires
)
# Get token expiry timestamp for frontend compatibility
token_data = active_tokens[access_token]
# Return the token in the format expected by OAuth2 and our frontend
return {
"access_token": access_token,
"token_type": "bearer",
"expires_in": access_token_expires.total_seconds(),
"expires_at": token_data["expires_at"], # Add expires_at for frontend
"user": {
"username": user["username"],
"display_name": user.get("display_name", user["username"]),
"roles": user.get("roles", []),
"email": user.get("email", "")
}
}
@app.post("/api/auth/refresh")
async def refresh_token(current_user: dict = Depends(get_current_user)):
"""Refresh access token."""
# Create new token with 1 hour expiry
token_expires = timedelta(hours=1)
token = create_access_token(
data={"sub": current_user["username"]},
expires_delta=token_expires
)
# Get token expiry timestamp
token_data = active_tokens[token]
return {
"access_token": token,
"token_type": "bearer",
"expires_at": token_data["expires_at"]
}
@app.post("/login")
async def handle_login(
request: Request,
username: str = Form(...),
password: str = Form(...)
):
"""Handle form submission"""
# Replace with your actual auth logic
if username == "root" and password == "linux":
token = create_access_token(
data={"sub": username},
expires_delta=timedelta(hours=1)
)
response = RedirectResponse(url="/app.html", status_code=303)
response.set_cookie(
key="access_token",
value=f"Bearer {token}",
httponly=True,
secure=True
)
return response
# Try to find login.html template for error rendering
possible_paths = [
os.path.join(WEB_ROOT, "login.html"),
"/var/lib/persistence/web-ui/login.html",
"/usr/lib/persistence/web-ui/login.html"
]
found_path = None
for path in possible_paths:
if os.path.exists(path):
found_path = path
break
# If template exists, try to render with error
if found_path:
try:
return templates.TemplateResponse(
"login.html",
{
"request": request,
"version": "6.1.0",
"error": "Invalid credentials"
},
status_code=401
)
except Exception as e:
logger.error(f"Error rendering login template with error: {str(e)}")
# Fallback HTML response with error
html_content = f"""<!DOCTYPE html>
<html>
<head>
<title>PersistenceOS Login</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {{ font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }}
.login-container {{ width: 300px; padding: 20px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }}
h1 {{ text-align: center; color: #333; }}
input {{ width: 100%; padding: 10px; margin: 10px 0; box-sizing: border-box; }}
button {{ width: 100%; padding: 10px; background-color: #4CAF50; color: white; border: none; cursor: pointer; }}
.error {{ color: red; margin-top: 10px; text-align: center; }}
</style>
</head>
<body>
<div class="login-container">
<h1>PersistenceOS</h1>
<div class="error">Invalid credentials</div>
<form id="login-form" method="post" action="/api/auth/token">
<input type="text" name="username" placeholder="Username" autocomplete="username" required>
<input type="password" name="password" placeholder="Password" autocomplete="current-password" required>
<button type="submit">Login</button>
</form>
</div>
<script>
document.getElementById('login-form').addEventListener('submit', async (e) => {{
e.preventDefault();
const formData = new FormData(e.target);
try {{
const response = await fetch('/api/auth/token', {{
method: 'POST',
body: new URLSearchParams(formData),
headers: {{ 'Content-Type': 'application/x-www-form-urlencoded' }}
}});
if (response.ok) {{
window.location.href = '/app.html';
}} else {{
document.getElementById('error').textContent = 'Login failed. Check credentials.';
}}
}} catch (err) {{
document.getElementById('error').textContent = 'Connection error. Please try again.';
}}
}});
</script>
</body>
</html>"""
return HTMLResponse(content=html_content, status_code=401)
# =============================================
# System Information Endpoints
# =============================================
@app.get("/api/system/info")
async def system_info(current_user: dict = Depends(get_current_user)):
"""Get system information."""
try:
# Get basic system information
hostname = socket.gethostname()
kernel = platform.release()
arch = platform.machine()
# Get uptime
uptime_seconds = int(time.time() - psutil.boot_time())
days, remainder = divmod(uptime_seconds, 86400)
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)
uptime = f"{days} days, {hours} hours, {minutes} minutes"
# Get CPU info
cpu_count = psutil.cpu_count(logical=True)
cpu_usage = psutil.cpu_percent(interval=0.1)
# Get memory info
memory = psutil.virtual_memory()
memory_total = memory.total
memory_used = memory.used
memory_percent = memory.percent
# Get disk info
disk = psutil.disk_usage('/')
disk_total = disk.total
disk_used = disk.used
disk_percent = disk.percent
return {
"hostname": hostname,
"kernel": kernel,
"architecture": arch,
"uptime": uptime,
"uptime_seconds": uptime_seconds,
"cpu": {
"count": cpu_count,
"usage_percent": cpu_usage
},
"memory": {
"total": memory_total,
"used": memory_used,
"percent": memory_percent
},
"disk": {
"total": disk_total,
"used": disk_used,
"percent": disk_percent
}
}
except Exception as e:
logger.error(f"Error getting system info: {e}")
return JSONResponse(
status_code=500,
content={"error": f"Failed to get system information: {str(e)}"}
)
@app.get("/api/system/network/interfaces")
async def network_interfaces(current_user: dict = Depends(get_current_user)):
"""Get network interface information."""
try:
interfaces = []
# Get network interfaces
for interface, addrs in psutil.net_if_addrs().items():
# Skip loopback interfaces
if interface.startswith('lo'):
continue
# Get interface stats
stats = psutil.net_if_stats().get(interface, None)
is_up = stats.isup if stats else False
# Get IPv4 address
ipv4 = None
for addr in addrs:
if addr.family == socket.AF_INET:
ipv4 = addr.address
break
# Only include interfaces with IPv4 addresses
if ipv4:
interfaces.append({
"name": interface,
"ip": ipv4,
"status": "up" if is_up else "down",
"mac": next((addr.address for addr in addrs if addr.family == psutil.AF_LINK), "")
})
return interfaces
except Exception as e:
logger.error(f"Error getting network interfaces: {e}")
return JSONResponse(
status_code=500,
content={"error": f"Failed to get network interfaces: {str(e)}"}
)
# =============================================
# Static files and startup
# =============================================
def configure_static_files():
"""Configure static file serving."""
if not os.path.exists(WEB_ROOT):
logger.error(f"Web root directory does not exist: {WEB_ROOT}")
return False
try:
# List web root contents for debugging
logger.info(f"Web root directory contents {WEB_ROOT}:")
for item in os.listdir(WEB_ROOT):
item_path = os.path.join(WEB_ROOT, item)
if os.path.isfile(item_path):
logger.info(f" File: {item} ({os.path.getsize(item_path)} bytes)")
else:
logger.info(f" Directory: {item}")
# Check login.html specifically
login_path = os.path.join(WEB_ROOT, "login.html")
if os.path.exists(login_path):
logger.info(f"login.html exists at {login_path} ({os.path.getsize(login_path)} bytes)")
else:
logger.error(f"login.html does not exist at {login_path}")
# Mount static files after explicit routes
static_path = os.path.join(WEB_ROOT, "static")
if os.path.exists(static_path):
app.mount("/static", StaticFiles(directory=static_path), name="static")
logger.info(f"Mounted /static directory from {static_path}")
else:
logger.warning(f"Static directory not found at {static_path}")
# DO NOT mount the entire web root at "/" as it conflicts with our specific routes
# The specific route handlers will serve the files instead
logger.info("Static file configuration complete - using specific route handlers for JS/CSS files")
return True
except Exception as e:
logger.error(f"Failed to configure static files: {e}")
return False
def start():
"""Start the API server."""
# Configure static files
configure_static_files()
# Start uvicorn
logger.info(f"Starting PersistenceOS API on port {API_PORT}")
uvicorn.run(
"main:app",
host="0.0.0.0",
port=API_PORT,
log_level="info",
reload=DEBUG_MODE
)
# Updated handler for direct /login.html requests - redirect to new login system
@app.get("/login.html", response_class=HTMLResponse)
async def serve_login_html(request: Request):
"""Direct handler for /login.html - redirect to new login system"""
logger.info("Legacy /login.html request, redirecting to /login")
return RedirectResponse(url="/login", status_code=301) # Permanent redirect
@app.get("/app.html", response_class=HTMLResponse)
async def serve_app_html(request: Request):
"""Serve Vue.js app HTML wrapper"""
logger.info("Serving app.html - authentication will be handled by client-side JavaScript")
app_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PersistenceOS - Dashboard</title>
<link rel="stylesheet" href="/css/style.css">
<link rel="icon" href="/img/favicon.ico" type="image/x-icon">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- Authentication library -->
<script src="/js/auth.js"></script>
<!-- Vue.js (with CDN fallback) -->
<script>
// Try to load local Vue.js first, fallback to CDN
const vueScript = document.createElement('script');
vueScript.src = '/js/vue.js';
vueScript.onerror = function() {{
console.log('Local Vue.js not found, loading from CDN...');
const cdnScript = document.createElement('script');
cdnScript.src = 'https://unpkg.com/vue@3/dist/vue.global.prod.js';
cdnScript.onload = function() {{
console.log('Vue.js loaded from CDN successfully');
}};
cdnScript.onerror = function() {{
console.error('Failed to load Vue.js from CDN');
document.getElementById('app').innerHTML = '<div style="text-align: center; padding: 50px; color: red;">Error: Could not load Vue.js framework</div>';
}};
document.head.appendChild(cdnScript);
}};
vueScript.onload = function() {{
console.log('Vue.js loaded locally successfully');
}};
document.head.appendChild(vueScript);
</script>
<!-- Main app script -->
<script src="/js/app.js" defer></script>
</head>
<body>
<div id="app">
<!-- Vue.js app will be mounted here -->
<div class="loading-indicator">Loading PersistenceOS Dashboard...</div>
</div>
</body>
</html>
"""
return HTMLResponse(content=app_html, headers={{
"Cache-Control": "no-cache, no-store, must-revalidate",
"Pragma": "no-cache",
"Expires": "0"
}})
@app.get("/js/app.js")
async def serve_app_js(request: Request):
"""Serve the Vue.js application script"""
app_js_path = os.path.join(WEB_ROOT, "js/app.js")
logger.info(f"Attempting to serve app.js from: {app_js_path}")
logger.info(f"WEB_ROOT is: {WEB_ROOT}")
logger.info(f"File exists: {os.path.exists(app_js_path)}")
# Try multiple possible locations
possible_locations = [
app_js_path,
os.path.join("/usr/lib/persistence/web-ui/js", "app.js"),
os.path.join("/usr/lib/persistence/web-ui/static/js", "app.js"),
"app.js" # Current directory
]
for location in possible_locations:
logger.info(f"Checking app.js at: {location} - exists: {os.path.exists(location)}")
if os.path.exists(location):
logger.info(f"Found app.js at: {location}")
return FileResponse(location, media_type="application/javascript")
# Enhanced debugging
logger.error(f"app.js not found in any location: {possible_locations}")
# List directory contents for debugging
for check_dir in [WEB_ROOT, "/usr/lib/persistence/web-ui", "/usr/lib/persistence/web-ui/js"]:
if os.path.exists(check_dir):
try:
contents = os.listdir(check_dir)
logger.info(f"Contents of {check_dir}: {contents}")
except Exception as e:
logger.error(f"Error listing {check_dir}: {e}")
else:
logger.error(f"Directory not found: {check_dir}")
raise HTTPException(status_code=404, detail=f"app.js not found in any expected location")
@app.get("/js/auth.js")
async def serve_auth_js(request: Request):
"""Serve the authentication script"""
auth_js_path = os.path.join(WEB_ROOT, "js/auth.js")
logger.info(f"Attempting to serve auth.js from: {auth_js_path}")
logger.info(f"WEB_ROOT is: {WEB_ROOT}")
logger.info(f"File exists: {os.path.exists(auth_js_path)}")
# Try multiple possible locations
possible_locations = [
auth_js_path,
os.path.join("/usr/lib/persistence/web-ui/js", "auth.js"),
os.path.join("/usr/lib/persistence/web-ui/static/js", "auth.js"),
"auth.js" # Current directory
]
for location in possible_locations:
logger.info(f"Checking auth.js at: {location} - exists: {os.path.exists(location)}")
if os.path.exists(location):
logger.info(f"Found auth.js at: {location}")
return FileResponse(location, media_type="application/javascript")
# Enhanced debugging
logger.error(f"auth.js not found in any location: {possible_locations}")
raise HTTPException(status_code=404, detail=f"auth.js not found in any expected location")
@app.get("/js/vue.js")
async def serve_vue_js(request: Request):
"""Serve the Vue.js library"""
vue_js_path = os.path.join(WEB_ROOT, "js/vue.js")
logger.info(f"Attempting to serve vue.js from: {vue_js_path}")
logger.info(f"File exists: {os.path.exists(vue_js_path)}")
if os.path.exists(vue_js_path):
return FileResponse(vue_js_path, media_type="application/javascript")
else:
logger.error(f"vue.js not found at {vue_js_path}")
# List directory contents for debugging
js_dir = os.path.join(WEB_ROOT, "js")
if os.path.exists(js_dir):
logger.info(f"Contents of {js_dir}: {os.listdir(js_dir)}")
else:
logger.error(f"JS directory not found at {js_dir}")
raise HTTPException(status_code=404, detail=f"vue.js not found at {vue_js_path}")
@app.get("/js/login.js")
async def serve_login_js(request: Request):
"""Serve the login script (legacy fallback)"""
login_js_path = os.path.join(WEB_ROOT, "js/login.js")
logger.info(f"Attempting to serve login.js from: {login_js_path}")
# Try multiple possible locations
possible_locations = [
login_js_path,
os.path.join("/usr/lib/persistence/web-ui/js", "login.js"),
os.path.join("/usr/lib/persistence/web-ui/static/js", "login.js"),
"login.js" # Current directory
]
for location in possible_locations:
if os.path.exists(location):
logger.info(f"Found login.js at: {location}")
return FileResponse(location, media_type="application/javascript")
logger.error(f"login.js not found in any location: {possible_locations}")
raise HTTPException(status_code=404, detail=f"login.js not found in any expected location")
@app.get("/js/bulletproof-login.js")
async def serve_bulletproof_login_js(request: Request):
"""Serve the bulletproof login script"""
bulletproof_login_paths = [
os.path.join(WEB_ROOT, "js/bulletproof-login.js"),
os.path.join(WEB_ROOT, "static/js/bulletproof-login.js"),
"/usr/lib/persistence/web-ui/js/bulletproof-login.js"
]
for location in bulletproof_login_paths:
if os.path.exists(location):
logger.info(f"Found bulletproof-login.js at: {location}")
return FileResponse(location, media_type="application/javascript")
logger.error(f"bulletproof-login.js not found in any location: {bulletproof_login_paths}")
raise HTTPException(status_code=404, detail=f"bulletproof-login.js not found")
@app.get("/js/unified-auth.js")
async def serve_unified_auth_js(request: Request):
"""Serve the unified authentication script"""
unified_auth_paths = [
os.path.join(WEB_ROOT, "js/unified-auth.js"),
os.path.join(WEB_ROOT, "static/js/unified-auth.js"),
"/usr/lib/persistence/web-ui/js/unified-auth.js"
]
for location in unified_auth_paths:
if os.path.exists(location):
logger.info(f"Found unified-auth.js at: {location}")
return FileResponse(location, media_type="application/javascript")
logger.error(f"unified-auth.js not found in any location: {unified_auth_paths}")
raise HTTPException(status_code=404, detail=f"unified-auth.js not found")
@app.get("/js/bulletproof-app.js")
async def serve_bulletproof_app_js(request: Request):
"""Serve the bulletproof app script"""
bulletproof_app_paths = [
os.path.join(WEB_ROOT, "js/bulletproof-app.js"),
os.path.join(WEB_ROOT, "static/js/bulletproof-app.js"),
"/usr/lib/persistence/web-ui/js/bulletproof-app.js"
]
for location in bulletproof_app_paths:
if os.path.exists(location):
logger.info(f"Found bulletproof-app.js at: {location}")
return FileResponse(location, media_type="application/javascript")
logger.error(f"bulletproof-app.js not found in any location: {bulletproof_app_paths}")
raise HTTPException(status_code=404, detail=f"bulletproof-app.js not found")
@app.get("/js/vue.global.prod.js")
async def serve_vue_global_prod_js(request: Request):
"""Serve the Vue.js library (legacy route for compatibility)"""
# Redirect to the new vue.js route
logger.info("Legacy vue.global.prod.js route accessed, redirecting to vue.js")
return await serve_vue_js(request)
@app.get("/css/style.css")
async def serve_style_css(request: Request):
"""Serve the main stylesheet"""
css_path = os.path.join(WEB_ROOT, "css/style.css")
if os.path.exists(css_path):
return FileResponse(css_path, media_type="text/css")
else:
logger.error(f"style.css not found at {css_path}")
raise HTTPException(status_code=404, detail=f"style.css not found")
@app.get("/img/favicon.ico")
async def serve_favicon(request: Request):
"""Serve the favicon"""
favicon_path = os.path.join(WEB_ROOT, "img/favicon.ico")
if os.path.exists(favicon_path):
return FileResponse(favicon_path, media_type="image/x-icon")
else:
logger.error(f"favicon.ico not found at {favicon_path}")
raise HTTPException(status_code=404, detail=f"favicon.ico not found")
@app.get("/static/js/login.js")
async def serve_static_login_js(request: Request):
"""Serve the login.js from static directory"""
login_js_path = os.path.join(WEB_ROOT, "static/js/login.js")
logger.info(f"Attempting to serve static login.js from: {login_js_path}")
if os.path.exists(login_js_path):
return FileResponse(login_js_path, media_type="application/javascript")
else:
logger.error(f"static login.js not found at {login_js_path}")
# Try to find login.js in other locations
alternative_paths = [
os.path.join(WEB_ROOT, "js/login.js"),
"login.js"
]
for alt_path in alternative_paths:
if os.path.exists(alt_path):
logger.info(f"Found login.js at alternative location: {alt_path}")
return FileResponse(alt_path, media_type="application/javascript")
raise HTTPException(status_code=404, detail=f"login.js not found")
# Catch-all route for any other JavaScript files
@app.get("/js/{filename}")
async def serve_js_files(filename: str, request: Request):
"""Serve any JavaScript file from the web UI directory"""
logger.info(f"Generic JS route called for: {filename}")
logger.info(f"WEB_ROOT is: {WEB_ROOT}")
# Try multiple possible locations
possible_locations = [
os.path.join(WEB_ROOT, "js", filename),
os.path.join("/usr/lib/persistence/web-ui/js", filename),
os.path.join("/usr/lib/persistence/web-ui/static/js", filename),
filename # Current directory
]
for location in possible_locations:
logger.info(f"Checking {filename} at: {location} - exists: {os.path.exists(location)}")
if os.path.exists(location):
logger.info(f"Found {filename} at: {location}")
return FileResponse(location, media_type="application/javascript")
# Enhanced debugging
logger.error(f"{filename} not found in any location: {possible_locations}")
# List directory contents for debugging
for check_dir in [WEB_ROOT, "/usr/lib/persistence/web-ui", "/usr/lib/persistence/web-ui/js"]:
if os.path.exists(check_dir):
try:
contents = os.listdir(check_dir)
logger.info(f"Contents of {check_dir}: {contents}")
except Exception as e:
logger.error(f"Error listing {check_dir}: {e}")
else:
logger.error(f"Directory not found: {check_dir}")
raise HTTPException(status_code=404, detail=f"{filename} not found in any expected location")
if __name__ == "__main__":
import time # Import needed for the uptime calculation
import sys
# Handle command line arguments
if len(sys.argv) > 1 and sys.argv[1] == "--debug":
logger.info("Debug mode enabled via command line")
# Override DEBUG_MODE environment variable
import os
os.environ["DEBUG_MODE"] = "true"
# Update the global DEBUG_MODE variable
globals()["DEBUG_MODE"] = True
# Generate initial configuration
try:
logger.info("Generating initial API configuration...")
with app.router.stall_next_request():
get_config()
except Exception as e:
logger.error(f"Error generating initial configuration: {e}")
# Start the application
start()