Commit 3f6647d2 authored by Your Name's avatar Your Name

Implement full web dashboard with authentication

- Added jinja2 and itsdangerous dependencies to requirements.txt
- Created templates directory with base layout and dashboard pages
- Implemented login/logout with session-based authentication
- Added dashboard overview page showing server stats
- Added configuration editors for:
  - Providers (providers.json)
  - Rotations (rotations.json)
  - Autoselect (autoselect.json)
  - Condensation prompts (markdown files)
  - Server settings (aisbf.json)
- Dashboard accessible at /dashboard with configurable username/password
- Session middleware with secure random secret key
- Authentication middleware skips dashboard routes
- All config changes saved to ~/.aisbf/ directory
- Dashboard config loaded from aisbf.json (username, password)
- Default credentials: admin/admin
parent 4ee2fc61
......@@ -22,18 +22,22 @@ Why did the programmer quit his job? Because he didn't get arrays!
Main application for AISBF.
"""
from fastapi import FastAPI, HTTPException, Request, status
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi import FastAPI, HTTPException, Request, status, Form
from fastapi.responses import JSONResponse, StreamingResponse, HTMLResponse, RedirectResponse
from fastapi.middleware.cors import CORSMiddleware
from fastapi.exceptions import RequestValidationError
from fastapi.templating import Jinja2Templates
from aisbf.models import ChatCompletionRequest, ChatCompletionResponse
from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler
from aisbf.database import initialize_database
from starlette.middleware.sessions import SessionMiddleware
from itsdangerous import URLSafeTimedSerializer
import time
import logging
import sys
import os
import argparse
import secrets
from logging.handlers import RotatingFileHandler
from datetime import datetime, timedelta
from collections import defaultdict
......@@ -342,6 +346,13 @@ logger = setup_logging()
# For now, we'll delay the import and initialization
app = FastAPI(title="AI Proxy Server")
# Initialize Jinja2 templates
templates = Jinja2Templates(directory="templates")
# Add session middleware (will be configured with secret key in main())
# Placeholder - will be added in main() after we have a secret key
session_secret_key = None
# These will be initialized in main() after config is loaded
request_handler = None
rotation_handler = None
......@@ -354,8 +365,8 @@ config = None
async def auth_middleware(request: Request, call_next):
"""Check API token authentication if enabled"""
if server_config.get('auth_enabled', False):
# Skip auth for root endpoint
if request.url.path == "/":
# Skip auth for root endpoint and dashboard routes
if request.url.path == "/" or request.url.path.startswith("/dashboard"):
response = await call_next(request)
return response
......@@ -428,6 +439,319 @@ app.add_middleware(
allow_headers=["*"],
)
# Dashboard routes
@app.get("/dashboard/login", response_class=HTMLResponse)
async def dashboard_login_page(request: Request):
"""Show dashboard login page"""
return templates.TemplateResponse("dashboard/login.html", {"request": request})
@app.post("/dashboard/login")
async def dashboard_login(request: Request, username: str = Form(...), password: str = Form(...)):
"""Handle dashboard login"""
dashboard_config = server_config.get('dashboard_config', {})
if username == dashboard_config.get('username', 'admin') and password == dashboard_config.get('password', 'admin'):
request.session['logged_in'] = True
request.session['username'] = username
return RedirectResponse(url="/dashboard", status_code=303)
return templates.TemplateResponse("dashboard/login.html", {"request": request, "error": "Invalid credentials"})
@app.get("/dashboard/logout")
async def dashboard_logout(request: Request):
"""Handle dashboard logout"""
request.session.clear()
return RedirectResponse(url="/dashboard/login", status_code=303)
def require_dashboard_auth(request: Request):
"""Check if user is logged in to dashboard"""
if not request.session.get('logged_in'):
return RedirectResponse(url="/dashboard/login", status_code=303)
return None
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard_index(request: Request):
"""Dashboard overview page"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
return templates.TemplateResponse("dashboard/index.html", {
"request": request,
"session": request.session,
"providers_count": len(config.providers) if config else 0,
"rotations_count": len(config.rotations) if config else 0,
"autoselect_count": len(config.autoselect) if config else 0,
"server_config": server_config or {}
})
@app.get("/dashboard/providers", response_class=HTMLResponse)
async def dashboard_providers(request: Request):
"""Edit providers configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
# Load providers.json
config_path = Path.home() / '.aisbf' / 'providers.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'providers.json'
with open(config_path) as f:
config_content = f.read()
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Providers Configuration",
"config_content": config_content
})
@app.post("/dashboard/providers")
async def dashboard_providers_save(request: Request, config: str = Form(...)):
"""Save providers configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
# Validate JSON
json.loads(config)
# Save to file
config_path = Path.home() / '.aisbf' / 'providers.json'
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
f.write(config)
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Providers Configuration",
"config_content": config,
"success": "Configuration saved successfully! Restart server for changes to take effect."
})
except json.JSONDecodeError as e:
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Providers Configuration",
"config_content": config,
"error": f"Invalid JSON: {str(e)}"
})
@app.get("/dashboard/rotations", response_class=HTMLResponse)
async def dashboard_rotations(request: Request):
"""Edit rotations configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
config_path = Path.home() / '.aisbf' / 'rotations.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'rotations.json'
with open(config_path) as f:
config_content = f.read()
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Rotations Configuration",
"config_content": config_content
})
@app.post("/dashboard/rotations")
async def dashboard_rotations_save(request: Request, config: str = Form(...)):
"""Save rotations configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
json.loads(config)
config_path = Path.home() / '.aisbf' / 'rotations.json'
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
f.write(config)
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Rotations Configuration",
"config_content": config,
"success": "Configuration saved successfully! Restart server for changes to take effect."
})
except json.JSONDecodeError as e:
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Rotations Configuration",
"config_content": config,
"error": f"Invalid JSON: {str(e)}"
})
@app.get("/dashboard/autoselect", response_class=HTMLResponse)
async def dashboard_autoselect(request: Request):
"""Edit autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
config_path = Path.home() / '.aisbf' / 'autoselect.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'autoselect.json'
with open(config_path) as f:
config_content = f.read()
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Autoselect Configuration",
"config_content": config_content
})
@app.post("/dashboard/autoselect")
async def dashboard_autoselect_save(request: Request, config: str = Form(...)):
"""Save autoselect configuration"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
try:
json.loads(config)
config_path = Path.home() / '.aisbf' / 'autoselect.json'
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
f.write(config)
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Autoselect Configuration",
"config_content": config,
"success": "Configuration saved successfully! Restart server for changes to take effect."
})
except json.JSONDecodeError as e:
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Autoselect Configuration",
"config_content": config,
"error": f"Invalid JSON: {str(e)}"
})
@app.get("/dashboard/condensation", response_class=HTMLResponse)
async def dashboard_condensation(request: Request):
"""Edit condensation prompts"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
# Load condensation_conversational.md
config_path = Path.home() / '.aisbf' / 'condensation_conversational.md'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'condensation_conversational.md'
with open(config_path) as f:
config_content = f.read()
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Condensation Prompts (Conversational)",
"config_content": config_content
})
@app.post("/dashboard/condensation")
async def dashboard_condensation_save(request: Request, config: str = Form(...)):
"""Save condensation prompts"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
config_path = Path.home() / '.aisbf' / 'condensation_conversational.md'
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
f.write(config)
return templates.TemplateResponse("dashboard/edit_config.html", {
"request": request,
"session": request.session,
"title": "Condensation Prompts (Conversational)",
"config_content": config,
"success": "Prompts saved successfully!"
})
@app.get("/dashboard/settings", response_class=HTMLResponse)
async def dashboard_settings(request: Request):
"""Edit server settings"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
# Load aisbf.json
config_path = Path.home() / '.aisbf' / 'aisbf.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'aisbf.json'
with open(config_path) as f:
aisbf_config = json.load(f)
return templates.TemplateResponse("dashboard/settings.html", {
"request": request,
"session": request.session,
"config": aisbf_config
})
@app.post("/dashboard/settings")
async def dashboard_settings_save(
request: Request,
host: str = Form(...),
port: int = Form(...),
protocol: str = Form(...),
auth_enabled: bool = Form(False),
auth_tokens: str = Form(""),
dashboard_username: str = Form(...),
dashboard_password: str = Form(""),
internal_model_id: str = Form(...)
):
"""Save server settings"""
auth_check = require_dashboard_auth(request)
if auth_check:
return auth_check
# Load current config
config_path = Path.home() / '.aisbf' / 'aisbf.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'aisbf.json'
with open(config_path) as f:
aisbf_config = json.load(f)
# Update config
aisbf_config['server']['host'] = host
aisbf_config['server']['port'] = port
aisbf_config['server']['protocol'] = protocol
aisbf_config['auth']['enabled'] = auth_enabled
aisbf_config['auth']['tokens'] = [t.strip() for t in auth_tokens.split('\n') if t.strip()]
aisbf_config['dashboard']['username'] = dashboard_username
if dashboard_password: # Only update if provided
aisbf_config['dashboard']['password'] = dashboard_password
aisbf_config['internal_model']['model_id'] = internal_model_id
# Save config
config_path = Path.home() / '.aisbf' / 'aisbf.json'
config_path.parent.mkdir(parents=True, exist_ok=True)
with open(config_path, 'w') as f:
json.dump(aisbf_config, f, indent=2)
return templates.TemplateResponse("dashboard/settings.html", {
"request": request,
"session": request.session,
"config": aisbf_config,
"success": "Settings saved successfully! Restart server for changes to take effect."
})
@app.get("/")
async def root():
return {
......@@ -780,6 +1104,25 @@ Examples:
# Load server configuration
server_config = load_server_config(args.config)
# Add session middleware for dashboard
secret_key = secrets.token_urlsafe(32)
app.add_middleware(SessionMiddleware, secret_key=secret_key)
# Load dashboard config
aisbf_config_path = Path.home() / '.aisbf' / 'aisbf.json'
if not aisbf_config_path.exists():
if args.config:
aisbf_config_path = Path(args.config) / 'aisbf.json'
else:
aisbf_config_path = Path(__file__).parent / 'config' / 'aisbf.json'
if aisbf_config_path.exists():
with open(aisbf_config_path) as f:
aisbf_config = json.load(f)
server_config['dashboard_config'] = aisbf_config.get('dashboard', {})
else:
server_config['dashboard_config'] = {'username': 'admin', 'password': 'admin'}
# CLI arguments take precedence over config file
host = args.host if args.host else server_config['host']
port = args.port if args.port else server_config['port']
......
......@@ -12,4 +12,6 @@ anthropic
langchain-text-splitters
tiktoken
torch
transformers
\ No newline at end of file
transformers
jinja2
itsdangerous
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}AISBF Dashboard{% endblock %}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { background: #2c3e50; color: white; padding: 20px 0; margin-bottom: 30px; }
.header h1 { font-size: 24px; font-weight: 600; }
.nav { background: white; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.nav a { color: #2c3e50; text-decoration: none; margin-right: 20px; padding: 8px 12px; border-radius: 4px; }
.nav a:hover { background: #ecf0f1; }
.nav a.active { background: #3498db; color: white; }
.content { background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; margin-bottom: 5px; font-weight: 500; color: #2c3e50; }
.form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; }
.form-group textarea { min-height: 200px; font-family: 'Courier New', monospace; }
.btn { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; }
.btn:hover { background: #2980b9; }
.btn-secondary { background: #95a5a6; }
.btn-secondary:hover { background: #7f8c8d; }
.btn-danger { background: #e74c3c; }
.btn-danger:hover { background: #c0392b; }
.alert { padding: 15px; border-radius: 4px; margin-bottom: 20px; }
.alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.logout { float: right; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background: #f8f9fa; font-weight: 600; }
.code { background: #f8f9fa; padding: 15px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 13px; overflow-x: auto; }
</style>
</head>
<body>
<div class="header">
<div class="container">
<h1>AISBF Dashboard</h1>
{% if session.logged_in %}
<a href="/dashboard/logout" class="btn btn-secondary logout">Logout</a>
{% endif %}
</div>
</div>
{% if session.logged_in %}
<div class="container">
<div class="nav">
<a href="/dashboard" {% if request.path == '/dashboard' %}class="active"{% endif %}>Overview</a>
<a href="/dashboard/providers" {% if '/providers' in request.path %}class="active"{% endif %}>Providers</a>
<a href="/dashboard/rotations" {% if '/rotations' in request.path %}class="active"{% endif %}>Rotations</a>
<a href="/dashboard/autoselect" {% if '/autoselect' in request.path %}class="active"{% endif %}>Autoselect</a>
<a href="/dashboard/condensation" {% if '/condensation' in request.path %}class="active"{% endif %}>Condensation</a>
<a href="/dashboard/settings" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
</div>
</div>
{% endif %}
<div class="container">
<div class="content">
{% block content %}{% endblock %}
</div>
</div>
</body>
</html>
{% extends "base.html" %}
{% block title %}{{ title }} - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">{{ title }}</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="POST">
<div class="form-group">
<label for="config">Configuration (JSON)</label>
<textarea id="config" name="config" required>{{ config_content }}</textarea>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn">Save Changes</button>
<a href="/dashboard" class="btn btn-secondary">Cancel</a>
</div>
</form>
<div style="margin-top: 30px; padding: 15px; background: #f8f9fa; border-radius: 4px;">
<h4 style="margin-bottom: 10px;">Tips:</h4>
<ul style="margin-left: 20px; line-height: 1.8;">
<li>Ensure valid JSON syntax before saving</li>
<li>Changes take effect after server restart</li>
<li>Backup your configuration before making changes</li>
</ul>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Overview - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">Dashboard Overview</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px;">
<div style="background: #3498db; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Providers</h3>
<p style="font-size: 32px; font-weight: bold;">{{ providers_count }}</p>
</div>
<div style="background: #2ecc71; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Rotations</h3>
<p style="font-size: 32px; font-weight: bold;">{{ rotations_count }}</p>
</div>
<div style="background: #e74c3c; color: white; padding: 20px; border-radius: 8px;">
<h3 style="font-size: 16px; margin-bottom: 10px;">Autoselect</h3>
<p style="font-size: 32px; font-weight: bold;">{{ autoselect_count }}</p>
</div>
</div>
<h3 style="margin-bottom: 15px;">Server Information</h3>
<table>
<tr>
<th>Setting</th>
<th>Value</th>
</tr>
<tr>
<td>Host</td>
<td>{{ server_config.host }}</td>
</tr>
<tr>
<td>Port</td>
<td>{{ server_config.port }}</td>
</tr>
<tr>
<td>Protocol</td>
<td>{{ server_config.protocol }}</td>
</tr>
<tr>
<td>Authentication</td>
<td>{{ 'Enabled' if server_config.auth_enabled else 'Disabled' }}</td>
</tr>
</table>
<h3 style="margin-top: 30px; margin-bottom: 15px;">Quick Actions</h3>
<div style="display: flex; gap: 10px;">
<a href="/dashboard/providers" class="btn">Manage Providers</a>
<a href="/dashboard/rotations" class="btn">Manage Rotations</a>
<a href="/dashboard/settings" class="btn btn-secondary">Server Settings</a>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Login - AISBF Dashboard{% endblock %}
{% block content %}
<div style="max-width: 400px; margin: 50px auto;">
<h2 style="margin-bottom: 30px; text-align: center;">Dashboard Login</h2>
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="POST" action="/dashboard/login">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required autofocus>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<button type="submit" class="btn" style="width: 100%;">Login</button>
</form>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Settings - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">Server Settings</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<form method="POST">
<h3 style="margin-bottom: 20px;">Server Configuration</h3>
<div class="form-group">
<label for="host">Host</label>
<input type="text" id="host" name="host" value="{{ config.server.host }}" required>
</div>
<div class="form-group">
<label for="port">Port</label>
<input type="number" id="port" name="port" value="{{ config.server.port }}" required>
</div>
<div class="form-group">
<label for="protocol">Protocol</label>
<select id="protocol" name="protocol">
<option value="http" {% if config.server.protocol == 'http' %}selected{% endif %}>HTTP</option>
<option value="https" {% if config.server.protocol == 'https' %}selected{% endif %}>HTTPS</option>
</select>
</div>
<h3 style="margin: 30px 0 20px;">Authentication</h3>
<div class="form-group">
<label>
<input type="checkbox" name="auth_enabled" {% if config.auth.enabled %}checked{% endif %}>
Enable API Authentication
</label>
</div>
<div class="form-group">
<label for="auth_tokens">Auth Tokens (one per line)</label>
<textarea id="auth_tokens" name="auth_tokens" style="min-height: 100px;">{{ '\n'.join(config.auth.tokens) }}</textarea>
</div>
<h3 style="margin: 30px 0 20px;">Dashboard</h3>
<div class="form-group">
<label for="dashboard_username">Dashboard Username</label>
<input type="text" id="dashboard_username" name="dashboard_username" value="{{ config.dashboard.username }}" required>
</div>
<div class="form-group">
<label for="dashboard_password">Dashboard Password</label>
<input type="password" id="dashboard_password" name="dashboard_password" placeholder="Leave blank to keep current">
</div>
<h3 style="margin: 30px 0 20px;">Internal Model</h3>
<div class="form-group">
<label for="internal_model_id">Model ID</label>
<input type="text" id="internal_model_id" name="internal_model_id" value="{{ config.internal_model.model_id }}" required>
</div>
<div style="display: flex; gap: 10px; margin-top: 30px;">
<button type="submit" class="btn">Save Settings</button>
<a href="/dashboard" class="btn btn-secondary">Cancel</a>
</div>
</form>
{% endblock %}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment