test/moxie/api/admin.py
Z User 1f9535d683 Add complete MOXIE web UI with authentication and user management
- New web UI with OpenWebUI-like interface using Tailwind CSS
- SQLite-based authentication with session management
- User registration, login, and profile pages
- Chat interface with conversation history
- Streaming responses with visible thinking phase
- File attachments support
- User document uploads in profile
- Rate limiting (5 requests/day for free users)
- Admin panel user management with promote/demote/delete
- Custom color theme matching balloon logo design
- Compatible with Nuitka build system
2026-03-24 05:15:50 +00:00

344 lines
10 KiB
Python
Executable File

"""
Hidden Admin UI Routes
Configuration, Document Upload, and ComfyUI Workflow Management
"""
import json
import os
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, Request, UploadFile, File, Form, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
from loguru import logger
from config import settings, get_data_dir, get_workflows_dir, save_config_to_db, load_config_from_db
from auth.models import get_auth_manager
router = APIRouter()
# Templates
templates = Jinja2Templates(directory=Path(__file__).parent.parent / "admin" / "templates")
# ============================================================================
# Config Models
# ============================================================================
class EndpointConfig(BaseModel):
"""API endpoint configuration."""
gemini_api_key: Optional[str] = None
gemini_model: str = "gemini-1.5-flash"
openrouter_api_key: Optional[str] = None
openrouter_model: str = "meta-llama/llama-3-8b-instruct:free"
comfyui_host: str = "http://127.0.0.1:8188"
ollama_host: str = "http://127.0.0.1:11434"
ollama_model: str = "qwen2.5:2b"
embedding_model: str = "qwen3-embedding:4b"
# ============================================================================
# Admin UI Routes
# ============================================================================
@router.get("/", response_class=HTMLResponse)
async def admin_dashboard(request: Request):
"""Admin dashboard homepage."""
config = load_config_from_db()
return templates.TemplateResponse(
"dashboard.html",
{
"request": request,
"config": config,
"settings": settings
}
)
@router.get("/endpoints", response_class=HTMLResponse)
async def endpoints_page(request: Request):
"""API endpoint configuration page."""
config = load_config_from_db()
return templates.TemplateResponse(
"endpoints.html",
{
"request": request,
"config": config,
"settings": settings
}
)
@router.post("/endpoints")
async def save_endpoints(config: EndpointConfig):
"""Save endpoint configuration to database."""
config_dict = config.model_dump(exclude_none=True)
for key, value in config_dict.items():
save_config_to_db(key, value)
logger.info("Endpoint configuration saved")
return {"status": "success", "message": "Configuration saved"}
@router.get("/documents", response_class=HTMLResponse)
async def documents_page(request: Request):
"""Document management page."""
rag_store = request.app.state.rag_store
documents = rag_store.list_documents()
return templates.TemplateResponse(
"documents.html",
{
"request": request,
"documents": documents,
"settings": settings
}
)
@router.post("/documents/upload")
async def upload_document(
request: Request,
file: UploadFile = File(...),
chunk_size: int = Form(default=500),
overlap: int = Form(default=50)
):
"""Upload and index a document."""
rag_store = request.app.state.rag_store
# Read file content
content = await file.read()
# Process based on file type
filename = file.filename or "unknown"
file_ext = Path(filename).suffix.lower()
try:
doc_id = await rag_store.add_document(
filename=filename,
content=content,
file_type=file_ext,
chunk_size=chunk_size,
overlap=overlap
)
logger.info(f"Document uploaded: {filename} (ID: {doc_id})")
return {"status": "success", "document_id": doc_id, "filename": filename}
except Exception as e:
logger.error(f"Failed to upload document: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/documents/{doc_id}")
async def delete_document(doc_id: str, request: Request):
"""Delete a document from the store."""
rag_store = request.app.state.rag_store
try:
rag_store.delete_document(doc_id)
logger.info(f"Document deleted: {doc_id}")
return {"status": "success"}
except Exception as e:
logger.error(f"Failed to delete document: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/comfyui", response_class=HTMLResponse)
async def comfyui_page(request: Request):
"""ComfyUI workflow management page."""
config = load_config_from_db()
workflows_dir = get_workflows_dir()
workflows = {
"image": None,
"video": None,
"audio": None
}
for workflow_type in workflows.keys():
workflow_path = workflows_dir / f"{workflow_type}.json"
if workflow_path.exists():
with open(workflow_path, "r") as f:
workflows[workflow_type] = json.load(f)
return templates.TemplateResponse(
"comfyui.html",
{
"request": request,
"config": config,
"workflows": workflows,
"workflows_dir": str(workflows_dir),
"settings": settings
}
)
@router.post("/comfyui/upload")
async def upload_comfyui_workflow(
workflow_type: str = Form(...),
file: UploadFile = File(...)
):
"""Upload a ComfyUI workflow JSON file."""
if workflow_type not in ["image", "video", "audio"]:
raise HTTPException(status_code=400, detail="Invalid workflow type")
workflows_dir = get_workflows_dir()
workflow_path = workflows_dir / f"{workflow_type}.json"
try:
content = await file.read()
# Validate JSON
workflow_data = json.loads(content)
with open(workflow_path, "wb") as f:
f.write(content)
logger.info(f"ComfyUI workflow uploaded: {workflow_type}")
return {"status": "success", "workflow_type": workflow_type}
except json.JSONDecodeError:
raise HTTPException(status_code=400, detail="Invalid JSON file")
except Exception as e:
logger.error(f"Failed to upload workflow: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/comfyui/{workflow_type}")
async def get_comfyui_workflow(workflow_type: str):
"""Get a ComfyUI workflow JSON."""
if workflow_type not in ["image", "video", "audio"]:
raise HTTPException(status_code=400, detail="Invalid workflow type")
workflows_dir = get_workflows_dir()
workflow_path = workflows_dir / f"{workflow_type}.json"
if not workflow_path.exists():
raise HTTPException(status_code=404, detail="Workflow not found")
with open(workflow_path, "r") as f:
return json.load(f)
@router.delete("/comfyui/{workflow_type}")
async def delete_comfyui_workflow(workflow_type: str):
"""Delete a ComfyUI workflow."""
if workflow_type not in ["image", "video", "audio"]:
raise HTTPException(status_code=400, detail="Invalid workflow type")
workflows_dir = get_workflows_dir()
workflow_path = workflows_dir / f"{workflow_type}.json"
if workflow_path.exists():
workflow_path.unlink()
logger.info(f"ComfyUI workflow deleted: {workflow_type}")
return {"status": "success"}
# ============================================================================
# User Management Routes
# ============================================================================
@router.get("/users", response_class=HTMLResponse)
async def users_page(request: Request):
"""User management page."""
auth = get_auth_manager()
users = auth.get_all_users()
return templates.TemplateResponse(
"users.html",
{
"request": request,
"users": users,
"settings": settings
}
)
@router.post("/users/{user_id}/promote")
async def promote_user(user_id: str):
"""Promote a user to admin."""
auth = get_auth_manager()
success = auth.promote_to_admin(user_id)
if success:
logger.info(f"User promoted to admin: {user_id}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Cannot promote this user")
@router.post("/users/{user_id}/demote")
async def demote_user(user_id: str):
"""Demote a user from admin."""
auth = get_auth_manager()
success = auth.demote_from_admin(user_id)
if success:
logger.info(f"User demoted from admin: {user_id}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Cannot demote this user")
@router.post("/users/{user_id}/limit")
async def update_user_limit(user_id: str, limit: int = Form(...)):
"""Update a user's daily request limit."""
auth = get_auth_manager()
success = auth.update_user_limit(user_id, limit)
if success:
logger.info(f"User limit updated: {user_id} -> {limit}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Failed to update user limit")
@router.delete("/users/{user_id}")
async def delete_user(user_id: str):
"""Delete a user."""
auth = get_auth_manager()
success = auth.delete_user(user_id)
if success:
logger.info(f"User deleted: {user_id}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Cannot delete this user")
@router.get("/status")
async def get_status(request: Request):
"""Get system status."""
rag_store = request.app.state.rag_store
config = load_config_from_db()
# Check Ollama connectivity
ollama_status = "unknown"
try:
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(f"{config.get('ollama_host', settings.ollama_host)}/api/tags", timeout=5.0)
ollama_status = "connected" if resp.status_code == 200 else "error"
except Exception:
ollama_status = "disconnected"
# Check ComfyUI connectivity
comfyui_status = "unknown"
try:
import httpx
async with httpx.AsyncClient() as client:
resp = await client.get(f"{config.get('comfyui_host', settings.comfyui_host)}/system_stats", timeout=5.0)
comfyui_status = "connected" if resp.status_code == 200 else "error"
except Exception:
comfyui_status = "disconnected"
return {
"ollama": ollama_status,
"comfyui": comfyui_status,
"documents_count": rag_store.get_document_count(),
"chunks_count": rag_store.get_chunk_count(),
}