test/moxie/web/routes.py
Z User 18301108b3 Add OpenWebUI features and integrate admin settings into profile
- Enhanced chat UI with visible action buttons (TTS, delete, regenerate, thumbs up/down)
- Added admin settings section to profile page for admin users
- Added admin API endpoints for user management, config, and RAG documents
- Removed need for separate admin endpoint - admins access settings via profile
2026-03-24 15:02:47 +00:00

1104 lines
32 KiB
Python

"""
Web UI Routes
Landing page, chat interface, and user profile.
"""
import json
import uuid
from datetime import datetime
from pathlib import Path
from typing import Optional, List
from fastapi import APIRouter, Request, Response, Cookie, Form, UploadFile, File, HTTPException
from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel
from loguru import logger
from config import settings
from auth.models import get_auth_manager, User
from core.orchestrator import Orchestrator
router = APIRouter()
# Templates - use Jinja2 directly without Starlette wrapper
TEMPLATES_DIR = Path(__file__).parent / "templates"
jinja_env = Environment(
loader=FileSystemLoader(str(TEMPLATES_DIR)),
autoescape=True,
)
def render_template(name: str, context: dict) -> str:
"""Render a template with the given context."""
template = jinja_env.get_template(name)
return template.render(**context)
def get_template_settings():
"""Get settings as a simple dict for template compatibility."""
return {
"admin_path": settings.admin_path,
"host": settings.host,
"port": settings.port,
}
# ============================================================================
# Helper Functions
# ============================================================================
async def get_current_user(
request: Request,
session_token: Optional[str] = Cookie(None)
) -> Optional[User]:
"""Get the current logged-in user from session cookie."""
if not session_token:
# Try header as fallback
auth_header = request.headers.get("Authorization", "")
if auth_header.startswith("Bearer "):
session_token = auth_header[7:]
if not session_token:
return None
auth = get_auth_manager()
return auth.validate_session(session_token)
async def require_user(
request: Request,
session_token: Optional[str] = Cookie(None)
) -> User:
"""Require a logged-in user, redirect to login if not."""
user = await get_current_user(request, session_token)
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
# ============================================================================
# Page Routes
# ============================================================================
@router.get("/", response_class=HTMLResponse)
async def landing_page(request: Request):
"""Landing page - redirects to chat if logged in, otherwise shows login."""
session_token = request.cookies.get("session_token")
if session_token:
auth = get_auth_manager()
user = auth.validate_session(session_token)
if user:
return HTMLResponse(
status_code=302,
headers={"Location": "/chat"}
)
html = render_template("landing.html", {
"settings": get_template_settings()
})
return HTMLResponse(content=html)
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request, error: Optional[str] = None):
"""Login page."""
html = render_template("login.html", {
"settings": get_template_settings(),
"error": error
})
return HTMLResponse(content=html)
@router.get("/signup", response_class=HTMLResponse)
async def signup_page(request: Request, error: Optional[str] = None):
"""Signup page."""
html = render_template("signup.html", {
"settings": get_template_settings(),
"error": error
})
return HTMLResponse(content=html)
@router.post("/login")
async def login_submit(
request: Request,
username: str = Form(...),
password: str = Form(...)
):
"""Process login form."""
auth = get_auth_manager()
user = auth.authenticate(username, password)
if not user:
html = render_template("login.html", {
"settings": get_template_settings(),
"error": "Invalid username or password"
})
return HTMLResponse(content=html, status_code=401)
# Create session
token = auth.create_session(user.id)
# Set cookie and redirect
response = JSONResponse({"success": True, "redirect": "/chat"})
response.set_cookie(
key="session_token",
value=token,
httponly=True,
max_age=7 * 24 * 60 * 60, # 7 days
samesite="lax"
)
return response
@router.post("/signup")
async def signup_submit(
request: Request,
username: str = Form(...),
email: str = Form(...),
password: str = Form(...),
confirm_password: str = Form(...)
):
"""Process signup form."""
if password != confirm_password:
html = render_template("signup.html", {
"settings": get_template_settings(),
"error": "Passwords do not match"
})
return HTMLResponse(content=html, status_code=400)
if len(password) < 6:
html = render_template("signup.html", {
"settings": get_template_settings(),
"error": "Password must be at least 6 characters"
})
return HTMLResponse(content=html, status_code=400)
auth = get_auth_manager()
user = auth.create_user(username, email, password)
if not user:
html = render_template("signup.html", {
"settings": get_template_settings(),
"error": "Username or email already exists"
})
return HTMLResponse(content=html, status_code=400)
# Create session
token = auth.create_session(user.id)
# Set cookie and redirect
response = JSONResponse({"success": True, "redirect": "/chat"})
response.set_cookie(
key="session_token",
value=token,
httponly=True,
max_age=7 * 24 * 60 * 60,
samesite="lax"
)
return response
@router.get("/logout")
async def logout(response: Response, session_token: Optional[str] = Cookie(None)):
"""Logout user."""
if session_token:
auth = get_auth_manager()
auth.delete_session(session_token)
response = JSONResponse({"success": True})
response.delete_cookie("session_token")
return response
@router.get("/chat", response_class=HTMLResponse)
async def chat_page(request: Request, session_token: Optional[str] = Cookie(None)):
"""Main chat interface."""
auth = get_auth_manager()
user = None
if session_token:
user = auth.validate_session(session_token)
if not user:
return HTMLResponse(
status_code=302,
headers={"Location": "/login"}
)
rate_info = auth.check_rate_limit(user.id)
html = render_template("chat.html", {
"settings": get_template_settings(),
"user": user,
"rate_info": rate_info
})
return HTMLResponse(content=html)
@router.get("/profile", response_class=HTMLResponse)
async def profile_page(request: Request, session_token: Optional[str] = Cookie(None)):
"""User profile page with document management."""
auth = get_auth_manager()
user = None
if session_token:
user = auth.validate_session(session_token)
if not user:
return HTMLResponse(
status_code=302,
headers={"Location": "/login"}
)
documents = auth.get_user_documents(user.id)
rate_info = auth.check_rate_limit(user.id)
html = render_template("profile.html", {
"settings": get_template_settings(),
"user": user,
"documents": documents,
"rate_info": rate_info
})
return HTMLResponse(content=html)
# ============================================================================
# API Routes (for chat functionality)
# ============================================================================
class ChatRequest(BaseModel):
"""Chat request model."""
message: str
conversation_id: Optional[str] = None
attachments: Optional[List[str]] = None
class ConversationUpdate(BaseModel):
"""Conversation update model."""
title: Optional[str] = None
@router.get("/api/user")
async def get_user_info(request: Request, session_token: Optional[str] = Cookie(None)):
"""Get current user info."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
rate_info = auth.check_rate_limit(user.id)
return {
"id": user.id,
"username": user.username,
"email": user.email,
"is_admin": user.is_admin,
"request_count": user.request_count,
"request_limit": user.request_limit,
"remaining_requests": rate_info["remaining"]
}
@router.get("/api/conversations")
async def get_conversations(request: Request, session_token: Optional[str] = Cookie(None)):
"""Get all conversations for the current user."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
# Get conversations from database
from config import get_db_path
import sqlite3
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
cursor.execute("""
SELECT id, title, created_at, updated_at
FROM conversations WHERE user_id = ?
ORDER BY updated_at DESC
""", (user.id,))
conversations = []
for row in cursor.fetchall():
conversations.append({
"id": row[0],
"title": row[1],
"created_at": row[2],
"updated_at": row[3]
})
conn.close()
return conversations
@router.post("/api/conversations")
async def create_conversation(
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Create a new conversation."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
from config import get_db_path
import sqlite3
conv_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
# Ensure conversations table exists
cursor.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
cursor.execute("""
INSERT INTO conversations (id, user_id, title, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (conv_id, user.id, "New Chat", now, now))
conn.commit()
conn.close()
return {"id": conv_id, "title": "New Chat", "created_at": now, "updated_at": now}
@router.get("/api/conversations/{conv_id}/messages")
async def get_conversation_messages(
conv_id: str,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Get all messages for a conversation."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
from config import get_db_path
import sqlite3
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
# Verify ownership
cursor.execute("SELECT user_id FROM conversations WHERE id = ?", (conv_id,))
row = cursor.fetchone()
if not row or row[0] != user.id:
conn.close()
return JSONResponse({"error": "Not found"}, status_code=404)
# Get messages
cursor.execute("""
SELECT id, role, content, attachments, created_at
FROM messages WHERE conversation_id = ?
ORDER BY created_at ASC
""", (conv_id,))
messages = []
for row in cursor.fetchall():
attachments = json.loads(row[3]) if row[3] else []
messages.append({
"id": row[0],
"role": row[1],
"content": row[2],
"attachments": attachments,
"created_at": row[4]
})
conn.close()
return messages
@router.patch("/api/conversations/{conv_id}")
async def update_conversation(
conv_id: str,
update: ConversationUpdate,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Update a conversation (e.g., rename)."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
from config import get_db_path
import sqlite3
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
# Verify ownership
cursor.execute("SELECT user_id FROM conversations WHERE id = ?", (conv_id,))
row = cursor.fetchone()
if not row or row[0] != user.id:
conn.close()
return JSONResponse({"error": "Not found"}, status_code=404)
now = datetime.utcnow().isoformat()
if update.title:
cursor.execute(
"UPDATE conversations SET title = ?, updated_at = ? WHERE id = ?",
(update.title, now, conv_id)
)
conn.commit()
conn.close()
return {"success": True}
@router.delete("/api/conversations/{conv_id}")
async def delete_conversation(
conv_id: str,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Delete a conversation and its messages."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
from config import get_db_path
import sqlite3
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
# Verify ownership
cursor.execute("SELECT user_id FROM conversations WHERE id = ?", (conv_id,))
row = cursor.fetchone()
if not row or row[0] != user.id:
conn.close()
return JSONResponse({"error": "Not found"}, status_code=404)
# Delete messages first
cursor.execute("DELETE FROM messages WHERE conversation_id = ?", (conv_id,))
# Delete conversation
cursor.execute("DELETE FROM conversations WHERE id = ?", (conv_id,))
conn.commit()
conn.close()
return {"success": True}
@router.post("/api/chat")
async def send_chat_message(
chat_request: ChatRequest,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Send a chat message and get a streaming response."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
# Check rate limit
rate_info = auth.check_rate_limit(user.id)
if not rate_info["allowed"]:
return JSONResponse(
{"error": f"Daily limit reached. You have used {user.request_count}/{user.request_limit} requests today."},
status_code=429
)
# Get orchestrator
orchestrator: Orchestrator = request.app.state.orchestrator
from config import get_db_path
import sqlite3
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
# Ensure tables exist
cursor.execute("""
CREATE TABLE IF NOT EXISTS conversations (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS messages (
id TEXT PRIMARY KEY,
conversation_id TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT,
attachments TEXT,
created_at TEXT,
FOREIGN KEY (conversation_id) REFERENCES conversations(id)
)
""")
# Create conversation if needed
conv_id = chat_request.conversation_id
if not conv_id:
conv_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
# Generate title from first message
title = chat_request.message[:50] + ("..." if len(chat_request.message) > 50 else "")
cursor.execute("""
INSERT INTO conversations (id, user_id, title, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
""", (conv_id, user.id, title, now, now))
# Save user message
user_msg_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
cursor.execute("""
INSERT INTO messages (id, conversation_id, role, content, attachments, created_at)
VALUES (?, ?, ?, ?, ?, ?)
""", (user_msg_id, conv_id, "user", chat_request.message,
json.dumps(chat_request.attachments or []), now))
# Update conversation timestamp
cursor.execute("UPDATE conversations SET updated_at = ? WHERE id = ?", (now, conv_id))
# Get previous messages for context
cursor.execute("""
SELECT role, content FROM messages
WHERE conversation_id = ?
ORDER BY created_at ASC
""", (conv_id,))
messages = [{"role": row[0], "content": row[1]} for row in cursor.fetchall()]
conn.commit()
conn.close()
# Increment request count
auth.increment_request_count(user.id)
# Return streaming response
return StreamingResponse(
stream_chat_response(request, orchestrator, messages, conv_id, user),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no",
"X-Conversation-Id": conv_id
}
)
async def stream_chat_response(
request: Request,
orchestrator: Orchestrator,
messages: List[dict],
conv_id: str,
user: User
):
"""Stream the chat response and save to database."""
from config import get_db_path
import sqlite3
full_response = ""
async for chunk in orchestrator.process_stream(messages=messages):
if "content" in chunk and chunk["content"]:
full_response += chunk["content"]
data = {"content": chunk["content"]}
yield f"data: {json.dumps(data)}\n\n"
# Save assistant message to database
conn = sqlite3.connect(str(get_db_path()))
cursor = conn.cursor()
asst_msg_id = str(uuid.uuid4())
now = datetime.utcnow().isoformat()
cursor.execute("""
INSERT INTO messages (id, conversation_id, role, content, created_at)
VALUES (?, ?, ?, ?, ?)
""", (asst_msg_id, conv_id, "assistant", full_response, now))
conn.commit()
conn.close()
# Send done signal
yield "data: [DONE]\n\n"
# ============================================================================
# File Upload Routes
# ============================================================================
@router.post("/api/upload")
async def upload_file(
file: UploadFile = File(...),
session_token: Optional[str] = Cookie(None)
):
"""Upload a file for attachment to messages."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
# Read file
content = await file.read()
# Save to temp location or database
from config import get_data_dir
uploads_dir = get_data_dir() / "uploads" / user.id
uploads_dir.mkdir(parents=True, exist_ok=True)
file_id = str(uuid.uuid4())
file_ext = Path(file.filename or "file").suffix
file_path = uploads_dir / f"{file_id}{file_ext}"
with open(file_path, "wb") as f:
f.write(content)
return {
"id": file_id,
"filename": file.filename,
"size": len(content),
"url": f"/api/files/{file_id}"
}
@router.get("/api/files/{file_id}")
async def get_file(
file_id: str,
session_token: Optional[str] = Cookie(None)
):
"""Get an uploaded file."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
from config import get_data_dir
uploads_dir = get_data_dir() / "uploads" / user.id
# Find file by ID
for file_path in uploads_dir.glob(f"{file_id}.*"):
from fastapi.responses import FileResponse
return FileResponse(file_path)
return JSONResponse({"error": "File not found"}, status_code=404)
# ============================================================================
# User Profile Document Routes
# ============================================================================
@router.post("/api/profile/documents")
async def upload_profile_document(
file: UploadFile = File(...),
session_token: Optional[str] = Cookie(None)
):
"""Upload a document to user's profile."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
content = await file.read()
file_type = Path(file.filename or "file").suffix
doc_id = auth.add_user_document(user.id, file.filename or "document", file_type, content)
return {
"id": doc_id,
"filename": file.filename,
"size": len(content),
"file_type": file_type
}
@router.get("/api/profile/documents")
async def get_profile_documents(
session_token: Optional[str] = Cookie(None)
):
"""Get all documents in user's profile."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
documents = auth.get_user_documents(user.id)
return documents
@router.delete("/api/profile/documents/{doc_id}")
async def delete_profile_document(
doc_id: str,
session_token: Optional[str] = Cookie(None)
):
"""Delete a document from user's profile."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
success = auth.delete_user_document(user.id, doc_id)
if success:
return {"success": True}
return JSONResponse({"error": "Document not found"}, status_code=404)
# ============================================================================
# Password Change
# ============================================================================
class PasswordChange(BaseModel):
current_password: str
new_password: str
@router.post("/api/profile/password")
async def change_password(
data: PasswordChange,
session_token: Optional[str] = Cookie(None)
):
"""Change user's password."""
auth = get_auth_manager()
if not session_token:
return JSONResponse({"error": "Not authenticated"}, status_code=401)
user = auth.validate_session(session_token)
if not user:
return JSONResponse({"error": "Invalid session"}, status_code=401)
# Verify current password
verified = auth.authenticate(user.username, data.current_password)
if not verified:
return JSONResponse({"error": "Current password is incorrect"}, status_code=400)
# Change password
success = auth.change_password(user.id, data.new_password)
if success:
return {"success": True}
return JSONResponse({"error": "Failed to change password"}, status_code=500)
# ============================================================================
# Admin API Routes (for profile page admin section)
# ============================================================================
async def require_admin(
request: Request,
session_token: Optional[str] = Cookie(None)
) -> User:
"""Require an admin user."""
auth = get_auth_manager()
if not session_token:
raise HTTPException(status_code=401, detail="Not authenticated")
user = auth.validate_session(session_token)
if not user:
raise HTTPException(status_code=401, detail="Invalid session")
if not user.is_admin:
raise HTTPException(status_code=403, detail="Admin access required")
return user
@router.get("/api/admin/status")
async def get_admin_status(
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Get system status for admin."""
user = await require_admin(request, session_token)
from config import load_config_from_db
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"
# Get user count
auth = get_auth_manager()
users = auth.get_all_users()
return {
"ollama": ollama_status,
"comfyui": comfyui_status,
"documents_count": rag_store.get_document_count(),
"chunks_count": rag_store.get_chunk_count(),
"users_count": len(users)
}
@router.get("/api/admin/users")
async def get_admin_users(
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Get all users for admin."""
user = await require_admin(request, session_token)
auth = get_auth_manager()
users = auth.get_all_users()
return [
{
"id": u.id,
"username": u.username,
"email": u.email,
"is_admin": u.is_admin,
"request_count": u.request_count,
"request_limit": u.request_limit,
"created_at": u.created_at
}
for u in users
]
@router.get("/api/admin/config")
async def get_admin_config(
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Get configuration for admin."""
user = await require_admin(request, session_token)
from config import load_config_from_db
return load_config_from_db()
class EndpointConfigAPI(BaseModel):
"""API endpoint configuration."""
gemini_api_key: Optional[str] = None
gemini_model: Optional[str] = None
openrouter_api_key: Optional[str] = None
openrouter_model: Optional[str] = None
comfyui_host: Optional[str] = None
ollama_host: Optional[str] = None
@router.post("/api/admin/endpoints")
async def save_admin_endpoints(
config: EndpointConfigAPI,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Save endpoint configuration."""
user = await require_admin(request, session_token)
from config import save_config_to_db
config_dict = config.model_dump(exclude_none=True)
for key, value in config_dict.items():
if value is not None:
save_config_to_db(key, value)
logger.info(f"Endpoint configuration saved by admin: {user.username}")
return {"status": "success", "message": "Configuration saved"}
@router.post("/api/admin/users/{user_id}/promote")
async def admin_promote_user(
user_id: str,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Promote a user to admin."""
admin = await require_admin(request, session_token)
auth = get_auth_manager()
success = auth.promote_to_admin(user_id)
if success:
logger.info(f"User promoted to admin by {admin.username}: {user_id}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Cannot promote this user")
@router.post("/api/admin/users/{user_id}/limit")
async def admin_update_user_limit(
user_id: str,
limit: int = Form(...),
request: Request = None,
session_token: Optional[str] = Cookie(None)
):
"""Update a user's daily request limit."""
admin = await require_admin(request, session_token)
auth = get_auth_manager()
success = auth.update_user_limit(user_id, limit)
if success:
logger.info(f"User limit updated by {admin.username}: {user_id} -> {limit}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Failed to update user limit")
@router.delete("/api/admin/users/{user_id}")
async def admin_delete_user(
user_id: str,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Delete a user."""
admin = await require_admin(request, session_token)
auth = get_auth_manager()
success = auth.delete_user(user_id)
if success:
logger.info(f"User deleted by {admin.username}: {user_id}")
return {"status": "success"}
raise HTTPException(status_code=400, detail="Cannot delete this user")
@router.get("/api/admin/documents")
async def get_admin_documents(
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Get all documents in RAG store."""
user = await require_admin(request, session_token)
rag_store = request.app.state.rag_store
documents = rag_store.list_documents()
return documents
@router.post("/api/admin/documents/upload")
async def admin_upload_document(
request: Request,
file: UploadFile = File(...),
chunk_size: int = Form(default=500),
overlap: int = Form(default=50),
session_token: Optional[str] = Cookie(None)
):
"""Upload and index a document to RAG store."""
admin = await require_admin(request, session_token)
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 by {admin.username}: {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("/api/admin/documents/{doc_id}")
async def admin_delete_document(
doc_id: str,
request: Request,
session_token: Optional[str] = Cookie(None)
):
"""Delete a document from the RAG store."""
admin = await require_admin(request, session_token)
rag_store = request.app.state.rag_store
try:
rag_store.delete_document(doc_id)
logger.info(f"Document deleted by {admin.username}: {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))