henaiv2 / admin_routes.py
joashsam's picture
Create admin_routes.py
8007730 verified
Raw
History Blame Contribute Delete
18 kB
"""
admin_routes.py - Admin Panel Endpoints for HF Space
Accessed via admin proxy with X-Admin-Authenticated header
"""
from flask import jsonify, request, render_template_string
import logging
from datetime import datetime
from auth_middleware import require_auth, admin_required, get_current_user_id, is_admin
from user_storage import (
get_all_user_ids, load_conversations, load_user_profile,
get_user_storage_stats, delete_user_data, get_user_generated_images_dir,
get_user_uploads_dir, get_user_dir
)
import os
logger = logging.getLogger(__name__)
# ===========================================
# ADMIN DASHBOARD HTML (Minimal - served via proxy)
# ===========================================
ADMIN_DASHBOARD_TEMPLATE = '''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HenAi Admin Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f0f12;
color: #d4d4e0;
padding: 20px;
}
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 28px; margin-bottom: 20px; color: #eeeef5; }
h2 { font-size: 20px; margin: 24px 0 16px; color: #eeeef5; }
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stat-card {
background: #141418;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 16px;
padding: 20px;
}
.stat-number {
font-size: 36px;
font-weight: 700;
color: #7c6aff;
}
.stat-label {
font-size: 13px;
color: #6b6b80;
margin-top: 8px;
}
table {
width: 100%;
border-collapse: collapse;
background: #141418;
border-radius: 16px;
overflow: hidden;
}
th, td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
th {
background: #1a1a20;
font-weight: 600;
color: #9191a8;
}
tr:hover { background: rgba(124,106,255,0.05); }
.btn {
padding: 6px 12px;
border-radius: 8px;
border: none;
cursor: pointer;
font-size: 12px;
transition: all 0.2s ease;
}
.btn-danger {
background: rgba(248,113,113,0.2);
color: #f87171;
}
.btn-danger:hover {
background: rgba(248,113,113,0.4);
}
.btn-sm { padding: 4px 10px; font-size: 11px; }
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 500;
}
.badge-active { background: rgba(52,211,153,0.2); color: #34d399; }
.badge-inactive { background: rgba(248,113,113,0.2); color: #f87171; }
.nav-links {
display: flex;
gap: 16px;
margin-bottom: 20px;
border-bottom: 1px solid rgba(255,255,255,0.08);
padding-bottom: 12px;
}
.nav-link {
color: #9191a8;
text-decoration: none;
font-size: 14px;
padding: 4px 0;
}
.nav-link.active {
color: #7c6aff;
border-bottom: 2px solid #7c6aff;
}
.nav-link:hover { color: #d4d4e0; }
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.7);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.active { display: flex; }
.modal-content {
background: #141418;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 24px;
padding: 24px;
max-width: 500px;
width: 90%;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.modal-title { font-size: 18px; font-weight: 600; color: #eeeef5; }
.modal-close {
background: none;
border: none;
color: #6b6b80;
font-size: 20px;
cursor: pointer;
}
.modal-body { margin-bottom: 20px; }
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.btn-primary {
background: #7c6aff;
color: white;
}
.btn-primary:hover { background: #9b8dff; }
</style>
</head>
<body>
<div class="container">
<h1>HenAi Admin Dashboard</h1>
<div class="nav-links">
<a href="#" class="nav-link active" data-tab="overview">Overview</a>
<a href="#" class="nav-link" data-tab="users">Users</a>
<a href="#" class="nav-link" data-tab="storage">Storage</a>
</div>
<div id="overview-tab">
<div class="stats-grid" id="stats-container">
<div class="stat-card">
<div class="stat-number" id="total-users">-</div>
<div class="stat-label">Total Users</div>
</div>
<div class="stat-card">
<div class="stat-number" id="total-conversations">-</div>
<div class="stat-label">Total Conversations</div>
</div>
<div class="stat-card">
<div class="stat-number" id="total-workspace-files">-</div>
<div class="stat-label">Workspace Files</div>
</div>
<div class="stat-card">
<div class="stat-number" id="total-images">-</div>
<div class="stat-label">Generated Images</div>
</div>
</div>
</div>
<div id="users-tab" style="display:none;">
<table id="users-table">
<thead>
<tr>
<th>User ID</th>
<th>Username</th>
<th>Email</th>
<th>Created</th>
<th>Last Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="users-tbody"></tbody>
</table>
</div>
<div id="storage-tab" style="display:none;">
<table id="storage-table">
<thead>
<tr>
<th>User ID</th>
<th>Conversations</th>
<th>Workspace Files</th>
<th>Uploaded Files</th>
<th>Generated Images</th>
<th>Total Size</th>
</tr>
</thead>
<tbody id="storage-tbody"></tbody>
</table>
</div>
</div>
<div id="confirm-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<span class="modal-title">Confirm Delete</span>
<button class="modal-close" onclick="closeModal()">×</button>
</div>
<div class="modal-body">
Are you sure you want to delete this user? This action cannot be undone.
</div>
<div class="modal-footer">
<button class="btn" onclick="closeModal()">Cancel</button>
<button class="btn btn-danger" id="confirm-delete-btn">Delete</button>
</div>
</div>
</div>
<script>
let currentUserToDelete = null;
function showTab(tab) {
document.getElementById('overview-tab').style.display = tab === 'overview' ? 'block' : 'none';
document.getElementById('users-tab').style.display = tab === 'users' ? 'block' : 'none';
document.getElementById('storage-tab').style.display = tab === 'storage' ? 'block' : 'none';
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
if (link.getAttribute('data-tab') === tab) {
link.classList.add('active');
}
});
if (tab === 'overview') loadStats();
if (tab === 'users') loadUsers();
if (tab === 'storage') loadStorage();
}
async function loadStats() {
const res = await fetch('/admin/api/stats');
const data = await res.json();
if (data.success) {
document.getElementById('total-users').textContent = data.stats.total_users;
document.getElementById('total-conversations').textContent = data.stats.total_conversations;
document.getElementById('total-workspace-files').textContent = data.stats.total_workspace_files;
document.getElementById('total-images').textContent = data.stats.total_generated_images;
}
}
async function loadUsers() {
const res = await fetch('/admin/api/users');
const data = await res.json();
if (data.success) {
const tbody = document.getElementById('users-tbody');
tbody.innerHTML = '';
data.users.forEach(user => {
const row = tbody.insertRow();
row.insertCell(0).textContent = user.user_id;
row.insertCell(1).textContent = user.username;
row.insertCell(2).textContent = user.email;
row.insertCell(3).textContent = new Date(user.created_at).toLocaleDateString();
row.insertCell(4).textContent = user.last_active ? new Date(user.last_active).toLocaleDateString() : '-';
const actionsCell = row.insertCell(5);
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.className = 'btn btn-danger btn-sm';
deleteBtn.onclick = () => confirmDelete(user.user_id);
actionsCell.appendChild(deleteBtn);
});
}
}
async function loadStorage() {
const res = await fetch('/admin/api/storage');
const data = await res.json();
if (data.success) {
const tbody = document.getElementById('storage-tbody');
tbody.innerHTML = '';
data.storage.forEach(item => {
const row = tbody.insertRow();
row.insertCell(0).textContent = item.user_id;
row.insertCell(1).textContent = item.conversations;
row.insertCell(2).textContent = item.workspace_files;
row.insertCell(3).textContent = item.uploaded_files;
row.insertCell(4).textContent = item.generated_images;
row.insertCell(5).textContent = item.total_size_mb;
});
}
}
function confirmDelete(userId) {
currentUserToDelete = userId;
document.getElementById('confirm-modal').classList.add('active');
}
function closeModal() {
document.getElementById('confirm-modal').classList.remove('active');
currentUserToDelete = null;
}
async function deleteUser() {
if (!currentUserToDelete) return;
const res = await fetch(`/admin/api/users/${currentUserToDelete}`, {
method: 'DELETE'
});
const data = await res.json();
closeModal();
if (data.success) {
loadUsers();
loadStorage();
loadStats();
alert('User deleted successfully');
} else {
alert('Failed to delete user: ' + (data.error || 'Unknown error'));
}
}
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
showTab(link.getAttribute('data-tab'));
});
});
document.getElementById('confirm-delete-btn').addEventListener('click', deleteUser);
showTab('overview');
</script>
</body>
</html>
'''
# ===========================================
# REGISTER ADMIN ROUTES
# ===========================================
def register_admin_routes(app):
"""Register all admin routes with the Flask app"""
@app.route('/admin')
@require_auth
@admin_required
def admin_dashboard():
"""Admin dashboard HTML page"""
return render_template_string(ADMIN_DASHBOARD_TEMPLATE)
@app.route('/admin/api/stats')
@require_auth
@admin_required
def admin_api_stats():
"""Get system statistics"""
from user_storage import (
get_all_user_ids, load_conversations,
get_user_workspace_stats, list_user_generated_images
)
user_ids = get_all_user_ids()
total_users = len(user_ids)
total_conversations = 0
total_workspace_files = 0
total_generated_images = 0
for user_id in user_ids:
conversations = load_conversations(user_id)
total_conversations += len(conversations)
# Count workspace files (simplified)
try:
from user_storage import load_workspace
workspace = load_workspace(user_id)
def count_files(node):
count = 0
for folder in node.get('folders', []):
count += count_files(folder)
count += len(node.get('files', []))
return count
total_workspace_files += count_files(workspace)
except:
pass
# Count generated images
try:
images_dir = get_user_generated_images_dir(user_id)
if os.path.exists(images_dir):
total_generated_images += len([f for f in os.listdir(images_dir) if f.endswith(('.png', '.jpg', '.jpeg'))])
except:
pass
return jsonify({
'success': True,
'stats': {
'total_users': total_users,
'total_conversations': total_conversations,
'total_workspace_files': total_workspace_files,
'total_generated_images': total_generated_images
}
})
@app.route('/admin/api/users')
@require_auth
@admin_required
def admin_api_users():
"""Get list of all users"""
from user_storage import get_all_user_ids, load_user_profile
user_ids = get_all_user_ids()
users = []
for user_id in user_ids:
profile = load_user_profile(user_id)
users.append({
'user_id': user_id,
'username': profile.get('username', user_id[:8]),
'email': profile.get('email', ''),
'created_at': profile.get('created_at', ''),
'last_active': profile.get('last_active', '')
})
return jsonify({
'success': True,
'users': users
})
@app.route('/admin/api/storage')
@require_auth
@admin_required
def admin_api_storage():
"""Get storage details for all users"""
from user_storage import get_all_user_ids, get_user_storage_stats
user_ids = get_all_user_ids()
storage = []
for user_id in user_ids:
stats = get_user_storage_stats(user_id)
storage.append({
'user_id': user_id,
'conversations': stats['conversations'],
'workspace_files': stats['workspace_files'],
'uploaded_files': stats['uploaded_files'],
'generated_images': stats['generated_images'],
'total_size_mb': f"{stats['total_size_bytes'] / (1024 * 1024):.2f} MB"
})
return jsonify({
'success': True,
'storage': storage
})
@app.route('/admin/api/users/<user_id>', methods=['DELETE'])
@require_auth
@admin_required
def admin_api_delete_user(user_id):
"""Delete a user and all their data"""
from user_storage import delete_user_data
if delete_user_data(user_id):
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': 'Failed to delete user'}), 500
@app.route('/admin/api/health')
def admin_health():
"""Health check for admin endpoints"""
return jsonify({
'status': 'ok',
'service': 'HenAi Admin API',
'timestamp': datetime.now().isoformat()
})
logger.info("Admin routes registered")