| """ |
| 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_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> |
| ''' |
|
|
|
|
| |
| |
| |
|
|
| 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) |
| |
| |
| 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 |
| |
| |
| 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") |