Phase 2: Admin dashboard with auth and templates

- Implement SessionManager with cookie-based authentication
- Create admin routes with login, logout, password change
- Add dashboard, models, tokens, users, and chat pages
- Implement dark theme CSS with modern design
- Add user management (create, delete users)
- Add API token management placeholders
- Session-based auth with CSRF protection
- Password hashing with argon2 fallback
- Jinja2 templates for all admin pages
parent 1d457be7
This diff is collapsed.
"""Admin dashboard routes."""
from pathlib import Path
from typing import Optional
from fastapi import APIRouter, Request, Response, Form, HTTPException, Depends
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from fastapi.templating import Jinja2Templates
from codai.admin.auth import SessionManager
router = APIRouter()
# Templates directory
templates_dir = Path(__file__).parent / "templates"
templates = Jinja2Templates(directory=str(templates_dir))
# Session manager (will be initialized in main.py)
session_manager: Optional[SessionManager] = None
def init_session_manager(config_dir: Path):
"""Initialize the session manager."""
global session_manager
session_manager = SessionManager(config_dir)
def get_current_user(request: Request) -> Optional[str]:
"""Get the current logged-in user from session cookie."""
if session_manager is None:
return None
cookie = request.cookies.get("session")
if not cookie:
return None
# Handle MUST_CHANGE flag
if cookie.endswith(".MUST_CHANGE"):
cookie = cookie[:-12] # Remove .MUST_CHANGE suffix
return session_manager.validate_session(cookie)
def require_auth(request: Request) -> str:
"""Dependency that requires authentication."""
username = get_current_user(request)
if not username:
raise HTTPException(status_code=401, detail="Not authenticated")
return username
def require_admin(request: Request) -> str:
"""Dependency that requires admin role."""
username = require_auth(request)
if not session_manager.is_admin(username):
raise HTTPException(status_code=403, detail="Admin access required")
return username
@router.get("/login", response_class=HTMLResponse)
async def login_page(request: Request):
"""Display login page."""
# If already logged in, redirect to dashboard
username = get_current_user(request)
if username:
return RedirectResponse(url="/admin", status_code=302)
return templates.TemplateResponse("login.html", {
"request": request,
"error": None
})
@router.post("/login")
async def login(
request: Request,
username: str = Form(...),
password: str = Form(...)
):
"""Handle login form submission."""
if session_manager is None:
raise HTTPException(status_code=500, detail="Session manager not initialized")
session_cookie = session_manager.authenticate(username, password)
if not session_cookie:
return templates.TemplateResponse("login.html", {
"request": request,
"error": "Invalid username or password"
})
# Check if must change password
must_change = session_cookie.endswith(".MUST_CHANGE")
if must_change:
session_cookie = session_cookie[:-12]
response = RedirectResponse(
url="/admin/change-password" if must_change else "/admin",
status_code=302
)
response.set_cookie(
key="session",
value=session_cookie,
httponly=True,
secure=False, # Set to True if using HTTPS
samesite="strict",
max_age=7200 # 2 hours
)
return response
@router.get("/logout")
async def logout(request: Request):
"""Handle logout."""
if session_manager:
cookie = request.cookies.get("session")
session_manager.destroy_session(cookie)
response = RedirectResponse(url="/login", status_code=302)
response.delete_cookie("session")
return response
@router.get("/admin/change-password", response_class=HTMLResponse)
async def change_password_page(request: Request, username: str = Depends(require_auth)):
"""Display password change page."""
user = session_manager.get_user(username)
must_change = user.get("must_change_password", False) if user else False
return templates.TemplateResponse("change_password.html", {
"request": request,
"username": username,
"must_change": must_change,
"error": None
})
@router.post("/admin/change-password")
async def change_password(
request: Request,
old_password: str = Form(...),
new_password: str = Form(...),
confirm_password: str = Form(...),
username: str = Depends(require_auth)
):
"""Handle password change."""
if new_password != confirm_password:
return templates.TemplateResponse("change_password.html", {
"request": request,
"username": username,
"must_change": False,
"error": "Passwords do not match"
})
if len(new_password) < 8:
return templates.TemplateResponse("change_password.html", {
"request": request,
"username": username,
"must_change": False,
"error": "Password must be at least 8 characters"
})
# Check if this is a forced change (first login)
user = session_manager.get_user(username)
if user and user.get("must_change_password"):
# Force change without verifying old password
success = session_manager.force_password_change(username, new_password)
else:
success = session_manager.change_password(username, old_password, new_password)
if not success:
return templates.TemplateResponse("change_password.html", {
"request": request,
"username": username,
"must_change": False,
"error": "Current password is incorrect"
})
return RedirectResponse(url="/admin", status_code=302)
@router.get("/admin", response_class=HTMLResponse)
async def admin_dashboard(request: Request, username: str = Depends(require_auth)):
"""Display admin dashboard."""
is_admin = session_manager.is_admin(username)
return templates.TemplateResponse("dashboard.html", {
"request": request,
"username": username,
"is_admin": is_admin
})
@router.get("/admin/models", response_class=HTMLResponse)
async def models_page(request: Request, username: str = Depends(require_admin)):
"""Display models management page."""
return templates.TemplateResponse("models.html", {
"request": request,
"username": username
})
@router.get("/admin/tokens", response_class=HTMLResponse)
async def tokens_page(request: Request, username: str = Depends(require_admin)):
"""Display API tokens management page."""
return templates.TemplateResponse("tokens.html", {
"request": request,
"username": username
})
@router.get("/admin/users", response_class=HTMLResponse)
async def users_page(request: Request, username: str = Depends(require_admin)):
"""Display users management page."""
users = session_manager.list_users()
return templates.TemplateResponse("users.html", {
"request": request,
"username": username,
"users": users
})
@router.get("/chat", response_class=HTMLResponse)
async def chat_page(request: Request, username: str = Depends(require_auth)):
"""Display chat interface."""
return templates.TemplateResponse("chat.html", {
"request": request,
"username": username
})
# API endpoints for admin operations
@router.get("/admin/api/status")
async def api_status(username: str = Depends(require_auth)):
"""Get system status."""
# TODO: Implement actual status gathering
return {
"status": "ok",
"backend": "auto",
"models_loaded": 0,
"uptime": "0h 0m"
}
@router.post("/admin/api/users")
async def api_create_user(
request: Request,
username: str = Depends(require_admin)
):
"""Create a new user."""
data = await request.json()
new_username = data.get("username")
password = data.get("password")
role = data.get("role", "user")
if not new_username or not password:
raise HTTPException(status_code=400, detail="Username and password required")
success = session_manager.create_user(new_username, password, role)
if not success:
raise HTTPException(status_code=400, detail="User already exists")
return {"success": True}
@router.delete("/admin/api/users/{user_id}")
async def api_delete_user(
user_id: int,
username: str = Depends(require_admin)
):
"""Delete a user."""
# Find user by ID
users = session_manager._load_auth_data().get("users", [])
user = next((u for u in users if u["id"] == user_id), None)
if not user:
raise HTTPException(status_code=404, detail="User not found")
success = session_manager.delete_user(user["username"])
if not success:
raise HTTPException(status_code=400, detail="Cannot delete user")
return {"success": True}
This diff is collapsed.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}CoderAI Admin{% endblock %}</title>
<link rel="stylesheet" href="/static/admin/style.css">
</head>
<body>
{% if username %}
<div class="layout">
<aside class="sidebar">
<div class="logo">
<h1>CoderAI</h1>
</div>
<nav class="nav">
<a href="/admin" class="nav-item {% if request.url.path == '/admin' %}active{% endif %}">
<span class="icon">📊</span>
<span>Overview</span>
</a>
{% if is_admin %}
<a href="/admin/models" class="nav-item {% if '/models' in request.url.path %}active{% endif %}">
<span class="icon">🤖</span>
<span>Models</span>
</a>
<a href="/admin/tokens" class="nav-item {% if '/tokens' in request.url.path %}active{% endif %}">
<span class="icon">🔑</span>
<span>API Tokens</span>
</a>
<a href="/admin/users" class="nav-item {% if '/users' in request.url.path %}active{% endif %}">
<span class="icon">👥</span>
<span>Users</span>
</a>
{% endif %}
<a href="/chat" class="nav-item {% if '/chat' in request.url.path %}active{% endif %}">
<span class="icon">💬</span>
<span>Chat</span>
</a>
</nav>
<div class="sidebar-footer">
<div class="user-info">
<span class="icon">👤</span>
<span>{{ username }}</span>
</div>
<a href="/logout" class="logout-btn">Logout</a>
</div>
</aside>
<main class="main-content">
<div class="content-wrapper">
{% block content %}{% endblock %}
</div>
</main>
</div>
{% else %}
<div class="content-wrapper">
{% block content %}{% endblock %}
</div>
{% endif %}
{% block scripts %}{% endblock %}
</body>
</html>
{% extends "base.html" %}
{% block title %}Change Password - CoderAI{% endblock %}
{% block content %}
<div class="page-header">
<h1>Change Password</h1>
{% if must_change %}
<p class="text-warning">You must change your password before continuing.</p>
{% endif %}
</div>
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<div class="card">
<form method="post" action="/admin/change-password" class="form">
{% if not must_change %}
<div class="form-group">
<label for="old_password">Current Password</label>
<input type="password" id="old_password" name="old_password" required>
</div>
{% endif %}
<div class="form-group">
<label for="new_password">New Password</label>
<input type="password" id="new_password" name="new_password" required minlength="8">
<small class="form-text">Minimum 8 characters</small>
</div>
<div class="form-group">
<label for="confirm_password">Confirm New Password</label>
<input type="password" id="confirm_password" name="confirm_password" required minlength="8">
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Change Password</button>
{% if not must_change %}
<a href="/admin" class="btn btn-secondary">Cancel</a>
{% endif %}
</div>
</form>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Chat - CoderAI{% endblock %}
{% block content %}
<div class="chat-container">
<div class="chat-header">
<div class="chat-title">
<h2>Chat</h2>
</div>
<div class="chat-controls">
<select id="model-selector" class="form-control">
<option value="">Select a model...</option>
</select>
<button class="btn btn-secondary" onclick="newChat()">New Chat</button>
</div>
</div>
<div class="chat-messages" id="chat-messages">
<div class="welcome-message">
<h3>Welcome to CoderAI Chat</h3>
<p>Select a model and start chatting</p>
</div>
</div>
<div class="chat-input-container">
<form id="chat-form" class="chat-input-form">
<textarea id="chat-input" class="chat-input"
placeholder="Type your message..."
rows="3"></textarea>
<button type="submit" class="btn btn-primary" id="send-btn">Send</button>
</form>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentModel = null;
let messages = [];
async function loadModels() {
try {
const response = await fetch('/v1/models');
const data = await response.json();
const selector = document.getElementById('model-selector');
selector.innerHTML = '<option value="">Select a model...</option>';
data.data.forEach(model => {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.id;
selector.appendChild(option);
});
} catch (error) {
console.error('Failed to load models:', error);
}
}
document.getElementById('model-selector').addEventListener('change', (e) => {
currentModel = e.target.value;
});
function newChat() {
messages = [];
document.getElementById('chat-messages').innerHTML = `
<div class="welcome-message">
<h3>New Chat Started</h3>
<p>Select a model and start chatting</p>
</div>
`;
}
function addMessage(role, content) {
const messagesDiv = document.getElementById('chat-messages');
// Remove welcome message if present
const welcome = messagesDiv.querySelector('.welcome-message');
if (welcome) {
welcome.remove();
}
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${role}`;
const avatar = document.createElement('div');
avatar.className = 'message-avatar';
avatar.textContent = role === 'user' ? '👤' : '🤖';
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
messageDiv.appendChild(avatar);
messageDiv.appendChild(contentDiv);
messagesDiv.appendChild(messageDiv);
// Scroll to bottom
messagesDiv.scrollTop = messagesDiv.scrollHeight;
}
document.getElementById('chat-form').addEventListener('submit', async (e) => {
e.preventDefault();
if (!currentModel) {
alert('Please select a model first');
return;
}
const input = document.getElementById('chat-input');
const message = input.value.trim();
if (!message) {
return;
}
// Add user message
addMessage('user', message);
messages.push({ role: 'user', content: message });
// Clear input
input.value = '';
// Disable send button
const sendBtn = document.getElementById('send-btn');
sendBtn.disabled = true;
sendBtn.textContent = 'Sending...';
try {
const response = await fetch('/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: currentModel,
messages: messages,
stream: false
})
});
if (!response.ok) {
throw new Error('Request failed');
}
const data = await response.json();
const assistantMessage = data.choices[0].message.content;
addMessage('assistant', assistantMessage);
messages.push({ role: 'assistant', content: assistantMessage });
} catch (error) {
addMessage('assistant', 'Error: ' + error.message);
} finally {
sendBtn.disabled = false;
sendBtn.textContent = 'Send';
}
});
// Handle Enter key (Shift+Enter for new line)
document.getElementById('chat-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
document.getElementById('chat-form').dispatchEvent(new Event('submit'));
}
});
// Load models on page load
loadModels();
</script>
{% endblock %}
{% extends "base.html" %}
{% block title %}Dashboard - CoderAI{% endblock %}
{% block content %}
<div class="page-header">
<h1>Overview</h1>
<div class="header-actions">
<button class="btn btn-secondary" onclick="reloadConfig()">Reload Config</button>
</div>
</div>
<div class="dashboard-grid">
<div class="card">
<h3>System Status</h3>
<div class="status-grid">
<div class="status-item">
<span class="label">Backend:</span>
<span class="value" id="backend">Loading...</span>
</div>
<div class="status-item">
<span class="label">GPU:</span>
<span class="value" id="gpu">Loading...</span>
</div>
<div class="status-item">
<span class="label">Uptime:</span>
<span class="value" id="uptime">Loading...</span>
</div>
<div class="status-item">
<span class="label">Status:</span>
<span class="value status-ok" id="status">OK</span>
</div>
</div>
</div>
<div class="card">
<h3>Active Models</h3>
<div id="active-models">
<p class="text-muted">No models loaded</p>
</div>
{% if is_admin %}
<a href="/admin/models" class="btn btn-primary btn-sm">Manage Models</a>
{% endif %}
</div>
<div class="card">
<h3>Request Stats</h3>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-value" id="total-requests">0</div>
<div class="stat-label">Total Requests</div>
</div>
<div class="stat-item">
<div class="stat-value" id="active-requests">0</div>
<div class="stat-label">Active</div>
</div>
<div class="stat-item">
<div class="stat-value" id="queued-requests">0</div>
<div class="stat-label">Queued</div>
</div>
</div>
</div>
<div class="card">
<h3>VRAM Usage</h3>
<div class="progress-bar">
<div class="progress-fill" id="vram-progress" style="width: 0%"></div>
</div>
<p class="text-muted" id="vram-text">0 GB / 0 GB (0%)</p>
</div>
</div>
<div class="card">
<h3>Recent Activity</h3>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Time</th>
<th>Model</th>
<th>Type</th>
<th>Status</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="activity-table">
<tr>
<td colspan="5" class="text-center text-muted">No recent activity</td>
</tr>
</tbody>
</table>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadStatus() {
try {
const response = await fetch('/admin/api/status');
const data = await response.json();
document.getElementById('backend').textContent = data.backend || 'auto';
document.getElementById('uptime').textContent = data.uptime || '0h 0m';
document.getElementById('status').textContent = data.status === 'ok' ? 'OK' : 'Error';
// Update models loaded count
if (data.models_loaded > 0) {
document.getElementById('active-models').innerHTML =
`<p>${data.models_loaded} model(s) loaded</p>`;
}
} catch (error) {
console.error('Failed to load status:', error);
}
}
async function reloadConfig() {
if (confirm('Reload configuration from disk? This will not restart the server.')) {
try {
const response = await fetch('/admin/api/system/reload', { method: 'POST' });
if (response.ok) {
alert('Configuration reloaded successfully');
loadStatus();
} else {
alert('Failed to reload configuration');
}
} catch (error) {
alert('Error: ' + error.message);
}
}
}
// Load status on page load
loadStatus();
// Refresh status every 5 seconds
setInterval(loadStatus, 5000);
</script>
{% endblock %}
{% extends "base.html" %}
{% block title %}Login - CoderAI{% endblock %}
{% block content %}
<div class="login-container">
<div class="login-box">
<div class="login-header">
<h1>CoderAI</h1>
<p>Admin Dashboard</p>
</div>
{% if error %}
<div class="alert alert-error">
{{ error }}
</div>
{% endif %}
<form method="post" action="/login" class="login-form">
<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 btn-primary btn-block">Login</button>
</form>
<div class="login-footer">
<p class="text-muted">Default credentials: admin / admin</p>
</div>
</div>
</div>
{% endblock %}
{% extends "base.html" %}
{% block title %}Models - CoderAI{% endblock %}
{% block content %}
<div class="page-header">
<h1>Models</h1>
<div class="header-actions">
<button class="btn btn-primary" onclick="showDownloadModal()">Download Model</button>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="showTab('local')">Local Models</button>
<button class="tab-btn" onclick="showTab('search')">Search HuggingFace</button>
<button class="tab-btn" onclick="showTab('config')">Configuration</button>
</div>
<div id="tab-local" class="tab-content active">
<div class="card">
<h3>Text Models</h3>
<div id="text-models-list">
<p class="text-muted">No text models configured</p>
</div>
</div>
<div class="card">
<h3>Image Models</h3>
<div id="image-models-list">
<p class="text-muted">No image models configured</p>
</div>
</div>
<div class="card">
<h3>Audio Models</h3>
<div id="audio-models-list">
<p class="text-muted">No audio models configured</p>
</div>
</div>
<div class="card">
<h3>GGUF Models</h3>
<div id="gguf-models-list">
<p class="text-muted">No GGUF models configured</p>
</div>
</div>
</div>
<div id="tab-search" class="tab-content">
<div class="card">
<h3>Search HuggingFace Models</h3>
<div class="search-form">
<input type="text" id="search-query" placeholder="Search models..." class="form-control">
<div class="filter-group">
<label>
<input type="checkbox" id="filter-gguf" checked>
GGUF only
</label>
<label>
<input type="checkbox" id="filter-text" checked>
Text models
</label>
<label>
<input type="checkbox" id="filter-image">
Image models
</label>
</div>
<button class="btn btn-primary" onclick="searchModels()">Search</button>
</div>
<div id="search-results" class="search-results">
<p class="text-muted">Enter a search query to find models</p>
</div>
</div>
</div>
<div id="tab-config" class="tab-content">
<div class="card">
<h3>Model Loading Configuration</h3>
<form id="config-form" class="form">
<div class="form-group">
<label for="load-mode">Load Mode</label>
<select id="load-mode" name="load_mode" class="form-control">
<option value="ondemand">On Demand (default)</option>
<option value="loadall">Load All</option>
<option value="loadswap">Load & Swap</option>
</select>
<small class="form-text">
On Demand: Load one model at a time<br>
Load All: Try to load all models in VRAM<br>
Load & Swap: Keep models in RAM, swap to VRAM as needed
</small>
</div>
<div class="form-group">
<label>Models to Load at Startup</label>
<div id="loaded-models-list">
<p class="text-muted">No models selected</p>
</div>
</div>
<div class="form-group">
<label>Models to Pre-load (RAM)</label>
<div id="preload-models-list">
<p class="text-muted">No models selected</p>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Configuration</button>
</form>
</div>
</div>
<!-- Download Modal -->
<div id="download-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Download Model</h2>
<button class="modal-close" onclick="hideDownloadModal()">&times;</button>
</div>
<div class="modal-body">
<form id="download-form">
<div class="form-group">
<label for="model-id">Model ID or URL</label>
<input type="text" id="model-id" class="form-control"
placeholder="e.g., TheBloke/Llama-2-7B-GGUF" required>
<small class="form-text">HuggingFace model ID or direct URL</small>
</div>
<div class="form-group">
<label for="file-pattern">File Pattern (optional)</label>
<input type="text" id="file-pattern" class="form-control"
placeholder=".gguf">
<small class="form-text">Filter files to download (e.g., .gguf, .safetensors)</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Download</button>
<button type="button" class="btn btn-secondary" onclick="hideDownloadModal()">Cancel</button>
</div>
</form>
<div id="download-progress" class="download-progress" style="display: none;">
<p>Downloading...</p>
<div class="progress-bar">
<div class="progress-fill" id="download-progress-bar"></div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showTab(tabName) {
document.querySelectorAll('.tab-content').forEach(tab => {
tab.classList.remove('active');
});
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.classList.remove('active');
});
document.getElementById('tab-' + tabName).classList.add('active');
event.target.classList.add('active');
}
function showDownloadModal() {
document.getElementById('download-modal').style.display = 'flex';
}
function hideDownloadModal() {
document.getElementById('download-modal').style.display = 'none';
}
async function searchModels() {
const query = document.getElementById('search-query').value;
const resultsDiv = document.getElementById('search-results');
if (!query) {
resultsDiv.innerHTML = '<p class="text-muted">Enter a search query</p>';
return;
}
resultsDiv.innerHTML = '<p>Searching...</p>';
// TODO: Implement actual HuggingFace API search
setTimeout(() => {
resultsDiv.innerHTML = '<p class="text-muted">Search functionality coming soon</p>';
}, 1000);
}
document.getElementById('download-form').addEventListener('submit', async (e) => {
e.preventDefault();
const modelId = document.getElementById('model-id').value;
const filePattern = document.getElementById('file-pattern').value;
document.getElementById('download-progress').style.display = 'block';
// TODO: Implement actual download
setTimeout(() => {
alert('Download functionality coming soon');
hideDownloadModal();
document.getElementById('download-progress').style.display = 'none';
}, 1000);
});
// Load models on page load
async function loadModels() {
// TODO: Implement loading models from API
}
loadModels();
</script>
{% endblock %}
{% extends "base.html" %}
{% block title %}API Tokens - CoderAI{% endblock %}
{% block content %}
<div class="page-header">
<h1>API Tokens</h1>
<div class="header-actions">
<button class="btn btn-primary" onclick="showCreateTokenModal()">Create Token</button>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Token</th>
<th>Provider</th>
<th>Created</th>
<th>Last Used</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="tokens-table">
<tr>
<td colspan="6" class="text-center text-muted">No tokens created</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Create Token Modal -->
<div id="create-token-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Create API Token</h2>
<button class="modal-close" onclick="hideCreateTokenModal()">&times;</button>
</div>
<div class="modal-body">
<form id="create-token-form">
<div class="form-group">
<label for="token-name">Token Name</label>
<input type="text" id="token-name" class="form-control"
placeholder="e.g., Production API" required>
<small class="form-text">A descriptive name for this token</small>
</div>
<div class="form-group">
<label for="token-provider">Provider Format</label>
<select id="token-provider" class="form-control">
<option value="openai">OpenAI (sk-...)</option>
<option value="anthropic">Anthropic (sk-ant-...)</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Token</button>
<button type="button" class="btn btn-secondary" onclick="hideCreateTokenModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
<!-- Show Token Modal -->
<div id="show-token-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Token Created</h2>
<button class="modal-close" onclick="hideShowTokenModal()">&times;</button>
</div>
<div class="modal-body">
<div class="alert alert-warning">
<strong>Important:</strong> Copy this token now. You won't be able to see it again!
</div>
<div class="token-display">
<code id="new-token-value"></code>
<button class="btn btn-secondary btn-sm" onclick="copyToken()">Copy</button>
</div>
<div class="form-actions">
<button class="btn btn-primary" onclick="hideShowTokenModal()">Done</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showCreateTokenModal() {
document.getElementById('create-token-modal').style.display = 'flex';
}
function hideCreateTokenModal() {
document.getElementById('create-token-modal').style.display = 'none';
document.getElementById('create-token-form').reset();
}
function showShowTokenModal(token) {
document.getElementById('new-token-value').textContent = token;
document.getElementById('show-token-modal').style.display = 'flex';
}
function hideShowTokenModal() {
document.getElementById('show-token-modal').style.display = 'none';
}
function copyToken() {
const token = document.getElementById('new-token-value').textContent;
navigator.clipboard.writeText(token).then(() => {
alert('Token copied to clipboard');
});
}
async function loadTokens() {
try {
const response = await fetch('/admin/api/tokens');
const tokens = await response.json();
const tbody = document.getElementById('tokens-table');
if (tokens.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted">No tokens created</td></tr>';
return;
}
tbody.innerHTML = tokens.map(token => `
<tr>
<td>${token.name}</td>
<td><code>${token.token.substring(0, 20)}...</code></td>
<td>${token.provider}</td>
<td>${new Date(token.created_at).toLocaleDateString()}</td>
<td>${token.last_used ? new Date(token.last_used).toLocaleDateString() : 'Never'}</td>
<td>
<button class="btn btn-danger btn-sm" onclick="deleteToken(${token.id})">Delete</button>
</td>
</tr>
`).join('');
} catch (error) {
console.error('Failed to load tokens:', error);
}
}
document.getElementById('create-token-form').addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('token-name').value;
const provider = document.getElementById('token-provider').value;
try {
const response = await fetch('/admin/api/tokens', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, provider })
});
if (response.ok) {
const data = await response.json();
hideCreateTokenModal();
showShowTokenModal(data.token);
loadTokens();
} else {
alert('Failed to create token');
}
} catch (error) {
alert('Error: ' + error.message);
}
});
async function deleteToken(tokenId) {
if (!confirm('Are you sure you want to delete this token? This cannot be undone.')) {
return;
}
try {
const response = await fetch(`/admin/api/tokens/${tokenId}`, {
method: 'DELETE'
});
if (response.ok) {
loadTokens();
} else {
alert('Failed to delete token');
}
} catch (error) {
alert('Error: ' + error.message);
}
}
loadTokens();
</script>
{% endblock %}
{% extends "base.html" %}
{% block title %}Users - CoderAI{% endblock %}
{% block content %}
<div class="page-header">
<h1>Users</h1>
<div class="header-actions">
<button class="btn btn-primary" onclick="showCreateUserModal()">Create User</button>
</div>
</div>
<div class="card">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Role</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.username }}</td>
<td>
<span class="badge {% if user.role == 'admin' %}badge-primary{% else %}badge-secondary{% endif %}">
{{ user.role }}
</span>
</td>
<td>{{ user.created_at[:10] }}</td>
<td>
{% if user.username != username %}
<button class="btn btn-danger btn-sm" onclick="deleteUser({{ user.id }}, '{{ user.username }}')">Delete</button>
{% else %}
<a href="/admin/change-password" class="btn btn-secondary btn-sm">Change Password</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Create User Modal -->
<div id="create-user-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Create User</h2>
<button class="modal-close" onclick="hideCreateUserModal()">&times;</button>
</div>
<div class="modal-body">
<form id="create-user-form">
<div class="form-group">
<label for="new-username">Username</label>
<input type="text" id="new-username" class="form-control" required>
</div>
<div class="form-group">
<label for="new-password">Password</label>
<input type="password" id="new-password" class="form-control" required minlength="8">
<small class="form-text">Minimum 8 characters</small>
</div>
<div class="form-group">
<label for="new-role">Role</label>
<select id="new-role" class="form-control">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create User</button>
<button type="button" class="btn btn-secondary" onclick="hideCreateUserModal()">Cancel</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showCreateUserModal() {
document.getElementById('create-user-modal').style.display = 'flex';
}
function hideCreateUserModal() {
document.getElementById('create-user-modal').style.display = 'none';
document.getElementById('create-user-form').reset();
}
document.getElementById('create-user-form').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('new-username').value;
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
try {
const response = await fetch('/admin/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, role })
});
if (response.ok) {
hideCreateUserModal();
location.reload();
} else {
const error = await response.json();
alert('Failed to create user: ' + (error.detail || 'Unknown error'));
}
} catch (error) {
alert('Error: ' + error.message);
}
});
async function deleteUser(userId, username) {
if (!confirm(`Are you sure you want to delete user "${username}"?`)) {
return;
}
try {
const response = await fetch(`/admin/api/users/${userId}`, {
method: 'DELETE'
});
if (response.ok) {
location.reload();
} else {
const error = await response.json();
alert('Failed to delete user: ' + (error.detail || 'Unknown error'));
}
} catch (error) {
alert('Error: ' + error.message);
}
}
</script>
{% 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