Add comprehensive database integration, authentication, and admin dashboard
This commit introduces major enhancements to Talk2Me: ## Database Integration - PostgreSQL support with SQLAlchemy ORM - Redis integration for caching and real-time analytics - Automated database initialization scripts - Migration support infrastructure ## User Authentication System - JWT-based API authentication - Session-based web authentication - API key authentication for programmatic access - User roles and permissions (admin/user) - Login history and session tracking - Rate limiting per user with customizable limits ## Admin Dashboard - Real-time analytics and monitoring - User management interface (create, edit, delete users) - System health monitoring - Request/error tracking - Language pair usage statistics - Performance metrics visualization ## Key Features - Dual authentication support (token + user accounts) - Graceful fallback for missing services - Non-blocking analytics middleware - Comprehensive error handling - Session management with security features ## Bug Fixes - Fixed rate limiting bypass for admin routes - Added missing email validation method - Improved error handling for missing database tables - Fixed session-based authentication for API endpoints 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
		
							
								
								
									
										693
									
								
								templates/admin_users.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										693
									
								
								templates/admin_users.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,693 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>User Management - Talk2Me Admin</title> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"> | ||||
|     <style> | ||||
|         .user-avatar { | ||||
|             width: 40px; | ||||
|             height: 40px; | ||||
|             border-radius: 50%; | ||||
|             object-fit: cover; | ||||
|         } | ||||
|         .status-badge { | ||||
|             font-size: 0.875rem; | ||||
|         } | ||||
|         .action-buttons .btn { | ||||
|             padding: 0.25rem 0.5rem; | ||||
|             font-size: 0.875rem; | ||||
|         } | ||||
|         .stats-card { | ||||
|             border: none; | ||||
|             border-radius: 10px; | ||||
|             box-shadow: 0 2px 4px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         .stats-card .card-body { | ||||
|             padding: 1.5rem; | ||||
|         } | ||||
|         .stats-number { | ||||
|             font-size: 2rem; | ||||
|             font-weight: bold; | ||||
|             color: #4a5568; | ||||
|         } | ||||
|         .table-responsive { | ||||
|             border-radius: 8px; | ||||
|             box-shadow: 0 1px 3px rgba(0,0,0,0.1); | ||||
|         } | ||||
|         .modal-header { | ||||
|             background-color: #f8f9fa; | ||||
|         } | ||||
|         .search-filters { | ||||
|             background-color: #f8f9fa; | ||||
|             padding: 1rem; | ||||
|             border-radius: 8px; | ||||
|             margin-bottom: 1rem; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||||
|         <div class="container-fluid"> | ||||
|             <a class="navbar-brand" href="/admin">Talk2Me Admin</a> | ||||
|             <div class="navbar-nav ms-auto"> | ||||
|                 <a class="nav-link" href="/" target="_blank"> | ||||
|                     <i class="bi bi-box-arrow-up-right"></i> Main App | ||||
|                 </a> | ||||
|                 <a class="nav-link" href="#" onclick="logout()"> | ||||
|                     <i class="bi bi-box-arrow-right"></i> Logout | ||||
|                 </a> | ||||
|             </div> | ||||
|         </div> | ||||
|     </nav> | ||||
|  | ||||
|     <div class="container-fluid mt-4"> | ||||
|         <div class="row"> | ||||
|             <div class="col-12"> | ||||
|                 <h1 class="mb-4">User Management</h1> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Statistics Cards --> | ||||
|         <div class="row mb-4"> | ||||
|             <div class="col-md-3"> | ||||
|                 <div class="card stats-card"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="text-muted mb-2">Total Users</h6> | ||||
|                         <div class="stats-number" id="stat-total">0</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3"> | ||||
|                 <div class="card stats-card"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="text-muted mb-2">Active Users</h6> | ||||
|                         <div class="stats-number text-success" id="stat-active">0</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3"> | ||||
|                 <div class="card stats-card"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="text-muted mb-2">Suspended Users</h6> | ||||
|                         <div class="stats-number text-warning" id="stat-suspended">0</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="col-md-3"> | ||||
|                 <div class="card stats-card"> | ||||
|                     <div class="card-body"> | ||||
|                         <h6 class="text-muted mb-2">Admin Users</h6> | ||||
|                         <div class="stats-number text-primary" id="stat-admins">0</div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Search and Filters --> | ||||
|         <div class="search-filters"> | ||||
|             <div class="row"> | ||||
|                 <div class="col-md-4"> | ||||
|                     <input type="text" class="form-control" id="searchInput" placeholder="Search by email, username, or name..."> | ||||
|                 </div> | ||||
|                 <div class="col-md-2"> | ||||
|                     <select class="form-select" id="roleFilter"> | ||||
|                         <option value="">All Roles</option> | ||||
|                         <option value="admin">Admin</option> | ||||
|                         <option value="user">User</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div class="col-md-2"> | ||||
|                     <select class="form-select" id="statusFilter"> | ||||
|                         <option value="">All Status</option> | ||||
|                         <option value="active">Active</option> | ||||
|                         <option value="suspended">Suspended</option> | ||||
|                         <option value="inactive">Inactive</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div class="col-md-2"> | ||||
|                     <select class="form-select" id="sortBy"> | ||||
|                         <option value="created_at">Created Date</option> | ||||
|                         <option value="last_login_at">Last Login</option> | ||||
|                         <option value="total_requests">Total Requests</option> | ||||
|                         <option value="username">Username</option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div class="col-md-2"> | ||||
|                     <button class="btn btn-primary w-100" onclick="createUser()"> | ||||
|                         <i class="bi bi-plus-circle"></i> Create User | ||||
|                     </button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Users Table --> | ||||
|         <div class="table-responsive"> | ||||
|             <table class="table table-hover"> | ||||
|                 <thead> | ||||
|                     <tr> | ||||
|                         <th>User</th> | ||||
|                         <th>Role</th> | ||||
|                         <th>Status</th> | ||||
|                         <th>Usage</th> | ||||
|                         <th>Last Login</th> | ||||
|                         <th>Created</th> | ||||
|                         <th>Actions</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody id="usersTableBody"> | ||||
|                     <!-- Users will be loaded here --> | ||||
|                 </tbody> | ||||
|             </table> | ||||
|         </div> | ||||
|  | ||||
|         <!-- Pagination --> | ||||
|         <nav aria-label="Page navigation"> | ||||
|             <ul class="pagination justify-content-center" id="pagination"> | ||||
|                 <!-- Pagination will be loaded here --> | ||||
|             </ul> | ||||
|         </nav> | ||||
|     </div> | ||||
|  | ||||
|     <!-- User Details Modal --> | ||||
|     <div class="modal fade" id="userModal" tabindex="-1"> | ||||
|         <div class="modal-dialog modal-lg"> | ||||
|             <div class="modal-content"> | ||||
|                 <div class="modal-header"> | ||||
|                     <h5 class="modal-title">User Details</h5> | ||||
|                     <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | ||||
|                 </div> | ||||
|                 <div class="modal-body" id="userModalBody"> | ||||
|                     <!-- User details will be loaded here --> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <!-- Create/Edit User Modal --> | ||||
|     <div class="modal fade" id="createUserModal" tabindex="-1"> | ||||
|         <div class="modal-dialog"> | ||||
|             <div class="modal-content"> | ||||
|                 <div class="modal-header"> | ||||
|                     <h5 class="modal-title" id="createUserModalTitle">Create User</h5> | ||||
|                     <button type="button" class="btn-close" data-bs-dismiss="modal"></button> | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <form id="userForm"> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userEmail" class="form-label">Email</label> | ||||
|                             <input type="email" class="form-control" id="userEmail" required> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userUsername" class="form-label">Username</label> | ||||
|                             <input type="text" class="form-control" id="userUsername" required> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userPassword" class="form-label">Password</label> | ||||
|                             <input type="password" class="form-control" id="userPassword" minlength="8"> | ||||
|                             <small class="text-muted">Leave blank to keep existing password (edit mode)</small> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userFullName" class="form-label">Full Name</label> | ||||
|                             <input type="text" class="form-control" id="userFullName"> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userRole" class="form-label">Role</label> | ||||
|                             <select class="form-select" id="userRole"> | ||||
|                                 <option value="user">User</option> | ||||
|                                 <option value="admin">Admin</option> | ||||
|                             </select> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <div class="form-check"> | ||||
|                                 <input class="form-check-input" type="checkbox" id="userVerified"> | ||||
|                                 <label class="form-check-label" for="userVerified"> | ||||
|                                     Email Verified | ||||
|                                 </label> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="mb-3"> | ||||
|                             <label for="userRateLimit" class="form-label">Rate Limit (per minute)</label> | ||||
|                             <input type="number" class="form-control" id="userRateLimit" value="30"> | ||||
|                         </div> | ||||
|                     </form> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button> | ||||
|                     <button type="button" class="btn btn-primary" onclick="saveUser()">Save User</button> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | ||||
|     <script> | ||||
|         let currentPage = 1; | ||||
|         let editingUserId = null; | ||||
|  | ||||
|         // Configure axios defaults for session-based auth | ||||
|         axios.defaults.withCredentials = true; | ||||
|          | ||||
|         // Also check if there's a JWT token (for API users) | ||||
|         let authToken = localStorage.getItem('auth_token'); | ||||
|         if (authToken) { | ||||
|             axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}`; | ||||
|         } | ||||
|          | ||||
|         // For admin token auth, add the admin token header | ||||
|         const adminToken = '{{ session.get("admin_token", "") }}'; | ||||
|         if (adminToken) { | ||||
|             axios.defaults.headers.common['X-Admin-Token'] = adminToken; | ||||
|         } | ||||
|  | ||||
|         // Load users on page load | ||||
|         document.addEventListener('DOMContentLoaded', function() { | ||||
|             loadStats(); | ||||
|             loadUsers(); | ||||
|              | ||||
|             // Add event listeners | ||||
|             document.getElementById('searchInput').addEventListener('input', debounce(loadUsers, 300)); | ||||
|             document.getElementById('roleFilter').addEventListener('change', loadUsers); | ||||
|             document.getElementById('statusFilter').addEventListener('change', loadUsers); | ||||
|             document.getElementById('sortBy').addEventListener('change', loadUsers); | ||||
|         }); | ||||
|  | ||||
|         function debounce(func, wait) { | ||||
|             let timeout; | ||||
|             return function executedFunction(...args) { | ||||
|                 const later = () => { | ||||
|                     clearTimeout(timeout); | ||||
|                     func(...args); | ||||
|                 }; | ||||
|                 clearTimeout(timeout); | ||||
|                 timeout = setTimeout(later, wait); | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         async function loadStats() { | ||||
|             try { | ||||
|                 const response = await axios.get('/api/auth/admin/stats/users'); | ||||
|                 const stats = response.data.stats; | ||||
|                  | ||||
|                 document.getElementById('stat-total').textContent = stats.total_users; | ||||
|                 document.getElementById('stat-active').textContent = stats.active_users; | ||||
|                 document.getElementById('stat-suspended').textContent = stats.suspended_users; | ||||
|                 document.getElementById('stat-admins').textContent = stats.admin_users; | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to load stats:', error); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         async function loadUsers(page = 1) { | ||||
|             try { | ||||
|                 currentPage = page; | ||||
|                 const params = { | ||||
|                     page: page, | ||||
|                     per_page: 20, | ||||
|                     search: document.getElementById('searchInput').value, | ||||
|                     role: document.getElementById('roleFilter').value, | ||||
|                     status: document.getElementById('statusFilter').value, | ||||
|                     sort_by: document.getElementById('sortBy').value, | ||||
|                     sort_order: 'desc' | ||||
|                 }; | ||||
|  | ||||
|                 const response = await axios.get('/api/auth/admin/users', { params }); | ||||
|                 const data = response.data; | ||||
|                  | ||||
|                 displayUsers(data.users); | ||||
|                 displayPagination(data.pagination); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to load users:', error); | ||||
|                 showAlert('Failed to load users', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function displayUsers(users) { | ||||
|             const tbody = document.getElementById('usersTableBody'); | ||||
|             tbody.innerHTML = ''; | ||||
|  | ||||
|             users.forEach(user => { | ||||
|                 const tr = document.createElement('tr'); | ||||
|                 tr.innerHTML = ` | ||||
|                     <td> | ||||
|                         <div class="d-flex align-items-center"> | ||||
|                             ${user.avatar_url ?  | ||||
|                                 `<img src="${user.avatar_url}" class="user-avatar me-2" alt="${user.username}">` : | ||||
|                                 `<div class="user-avatar me-2 bg-secondary d-flex align-items-center justify-content-center text-white"> | ||||
|                                     ${user.username.charAt(0).toUpperCase()} | ||||
|                                 </div>` | ||||
|                             } | ||||
|                             <div> | ||||
|                                 <div class="fw-bold">${user.username}</div> | ||||
|                                 <small class="text-muted">${user.email}</small> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <span class="badge ${user.role === 'admin' ? 'bg-primary' : 'bg-secondary'}"> | ||||
|                             ${user.role} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         ${getStatusBadge(user)} | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <small> | ||||
|                             <i class="bi bi-translate"></i> ${user.total_translations}<br> | ||||
|                             <i class="bi bi-mic"></i> ${user.total_transcriptions}<br> | ||||
|                             <i class="bi bi-volume-up"></i> ${user.total_tts_requests} | ||||
|                         </small> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <small>${user.last_login_at ? new Date(user.last_login_at).toLocaleString() : 'Never'}</small> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <small>${new Date(user.created_at).toLocaleDateString()}</small> | ||||
|                     </td> | ||||
|                     <td class="action-buttons"> | ||||
|                         <button class="btn btn-sm btn-info" onclick="viewUser('${user.id}')" title="View Details"> | ||||
|                             <i class="bi bi-eye"></i> | ||||
|                         </button> | ||||
|                         <button class="btn btn-sm btn-warning" onclick="editUser('${user.id}')" title="Edit"> | ||||
|                             <i class="bi bi-pencil"></i> | ||||
|                         </button> | ||||
|                         ${user.is_suspended ?  | ||||
|                             `<button class="btn btn-sm btn-success" onclick="unsuspendUser('${user.id}')" title="Unsuspend"> | ||||
|                                 <i class="bi bi-play-circle"></i> | ||||
|                             </button>` : | ||||
|                             `<button class="btn btn-sm btn-warning" onclick="suspendUser('${user.id}')" title="Suspend"> | ||||
|                                 <i class="bi bi-pause-circle"></i> | ||||
|                             </button>` | ||||
|                         } | ||||
|                         ${user.role !== 'admin' ? | ||||
|                             `<button class="btn btn-sm btn-danger" onclick="deleteUser('${user.id}')" title="Delete"> | ||||
|                                 <i class="bi bi-trash"></i> | ||||
|                             </button>` : '' | ||||
|                         } | ||||
|                     </td> | ||||
|                 `; | ||||
|                 tbody.appendChild(tr); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function getStatusBadge(user) { | ||||
|             if (user.is_suspended) { | ||||
|                 return '<span class="badge bg-warning status-badge">Suspended</span>'; | ||||
|             } else if (!user.is_active) { | ||||
|                 return '<span class="badge bg-secondary status-badge">Inactive</span>'; | ||||
|             } else if (!user.is_verified) { | ||||
|                 return '<span class="badge bg-info status-badge">Unverified</span>'; | ||||
|             } else { | ||||
|                 return '<span class="badge bg-success status-badge">Active</span>'; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function displayPagination(pagination) { | ||||
|             const nav = document.getElementById('pagination'); | ||||
|             nav.innerHTML = ''; | ||||
|  | ||||
|             const totalPages = pagination.pages; | ||||
|             const currentPage = pagination.page; | ||||
|  | ||||
|             // Previous button | ||||
|             const prevLi = document.createElement('li'); | ||||
|             prevLi.className = `page-item ${currentPage === 1 ? 'disabled' : ''}`; | ||||
|             prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadUsers(${currentPage - 1})">Previous</a>`; | ||||
|             nav.appendChild(prevLi); | ||||
|  | ||||
|             // Page numbers | ||||
|             for (let i = 1; i <= totalPages; i++) { | ||||
|                 if (i === 1 || i === totalPages || (i >= currentPage - 2 && i <= currentPage + 2)) { | ||||
|                     const li = document.createElement('li'); | ||||
|                     li.className = `page-item ${i === currentPage ? 'active' : ''}`; | ||||
|                     li.innerHTML = `<a class="page-link" href="#" onclick="loadUsers(${i})">${i}</a>`; | ||||
|                     nav.appendChild(li); | ||||
|                 } else if (i === currentPage - 3 || i === currentPage + 3) { | ||||
|                     const li = document.createElement('li'); | ||||
|                     li.className = 'page-item disabled'; | ||||
|                     li.innerHTML = '<span class="page-link">...</span>'; | ||||
|                     nav.appendChild(li); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // Next button | ||||
|             const nextLi = document.createElement('li'); | ||||
|             nextLi.className = `page-item ${currentPage === totalPages ? 'disabled' : ''}`; | ||||
|             nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadUsers(${currentPage + 1})">Next</a>`; | ||||
|             nav.appendChild(nextLi); | ||||
|         } | ||||
|  | ||||
|         async function viewUser(userId) { | ||||
|             try { | ||||
|                 const response = await axios.get(`/api/auth/admin/users/${userId}`); | ||||
|                 const data = response.data; | ||||
|                  | ||||
|                 const modalBody = document.getElementById('userModalBody'); | ||||
|                 modalBody.innerHTML = ` | ||||
|                     <div class="row"> | ||||
|                         <div class="col-md-6"> | ||||
|                             <h6>User Information</h6> | ||||
|                             <dl class="row"> | ||||
|                                 <dt class="col-sm-4">Username:</dt> | ||||
|                                 <dd class="col-sm-8">${data.user.username}</dd> | ||||
|                                 <dt class="col-sm-4">Email:</dt> | ||||
|                                 <dd class="col-sm-8">${data.user.email}</dd> | ||||
|                                 <dt class="col-sm-4">Full Name:</dt> | ||||
|                                 <dd class="col-sm-8">${data.user.full_name || 'N/A'}</dd> | ||||
|                                 <dt class="col-sm-4">Role:</dt> | ||||
|                                 <dd class="col-sm-8">${data.user.role}</dd> | ||||
|                                 <dt class="col-sm-4">Status:</dt> | ||||
|                                 <dd class="col-sm-8">${getStatusBadge(data.user)}</dd> | ||||
|                                 <dt class="col-sm-4">API Key:</dt> | ||||
|                                 <dd class="col-sm-8"> | ||||
|                                     <code>${data.user.api_key}</code> | ||||
|                                     <button class="btn btn-sm btn-secondary ms-2" onclick="copyToClipboard('${data.user.api_key}')"> | ||||
|                                         <i class="bi bi-clipboard"></i> | ||||
|                                     </button> | ||||
|                                 </dd> | ||||
|                             </dl> | ||||
|                         </div> | ||||
|                         <div class="col-md-6"> | ||||
|                             <h6>Usage Statistics</h6> | ||||
|                             <dl class="row"> | ||||
|                                 <dt class="col-sm-6">Total Requests:</dt> | ||||
|                                 <dd class="col-sm-6">${data.user.total_requests}</dd> | ||||
|                                 <dt class="col-sm-6">Translations:</dt> | ||||
|                                 <dd class="col-sm-6">${data.user.total_translations}</dd> | ||||
|                                 <dt class="col-sm-6">Transcriptions:</dt> | ||||
|                                 <dd class="col-sm-6">${data.user.total_transcriptions}</dd> | ||||
|                                 <dt class="col-sm-6">TTS Requests:</dt> | ||||
|                                 <dd class="col-sm-6">${data.user.total_tts_requests}</dd> | ||||
|                                 <dt class="col-sm-6">Rate Limits:</dt> | ||||
|                                 <dd class="col-sm-6"> | ||||
|                                     ${data.user.rate_limit_per_minute}/min<br> | ||||
|                                     ${data.user.rate_limit_per_hour}/hour<br> | ||||
|                                     ${data.user.rate_limit_per_day}/day | ||||
|                                 </dd> | ||||
|                             </dl> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <hr> | ||||
|                     <h6>Login History</h6> | ||||
|                     <div class="table-responsive"> | ||||
|                         <table class="table table-sm"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th>Date</th> | ||||
|                                     <th>IP Address</th> | ||||
|                                     <th>Method</th> | ||||
|                                     <th>Status</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 ${data.login_history.map(login => ` | ||||
|                                     <tr> | ||||
|                                         <td>${new Date(login.login_at).toLocaleString()}</td> | ||||
|                                         <td>${login.ip_address}</td> | ||||
|                                         <td>${login.login_method}</td> | ||||
|                                         <td>${login.success ?  | ||||
|                                             '<span class="badge bg-success">Success</span>' :  | ||||
|                                             `<span class="badge bg-danger">Failed</span>` | ||||
|                                         }</td> | ||||
|                                     </tr> | ||||
|                                 `).join('')} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                     <hr> | ||||
|                     <h6>Active Sessions (${data.active_sessions.length})</h6> | ||||
|                     <div class="table-responsive"> | ||||
|                         <table class="table table-sm"> | ||||
|                             <thead> | ||||
|                                 <tr> | ||||
|                                     <th>Session ID</th> | ||||
|                                     <th>Created</th> | ||||
|                                     <th>Last Active</th> | ||||
|                                     <th>IP Address</th> | ||||
|                                 </tr> | ||||
|                             </thead> | ||||
|                             <tbody> | ||||
|                                 ${data.active_sessions.map(session => ` | ||||
|                                     <tr> | ||||
|                                         <td><code>${session.session_id.substr(0, 8)}...</code></td> | ||||
|                                         <td>${new Date(session.created_at).toLocaleString()}</td> | ||||
|                                         <td>${new Date(session.last_active_at).toLocaleString()}</td> | ||||
|                                         <td>${session.ip_address}</td> | ||||
|                                     </tr> | ||||
|                                 `).join('')} | ||||
|                             </tbody> | ||||
|                         </table> | ||||
|                     </div> | ||||
|                 `; | ||||
|                  | ||||
|                 const modal = new bootstrap.Modal(document.getElementById('userModal')); | ||||
|                 modal.show(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to load user details:', error); | ||||
|                 showAlert('Failed to load user details', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function createUser() { | ||||
|             editingUserId = null; | ||||
|             document.getElementById('createUserModalTitle').textContent = 'Create User'; | ||||
|             document.getElementById('userForm').reset(); | ||||
|             document.getElementById('userPassword').required = true; | ||||
|             const modal = new bootstrap.Modal(document.getElementById('createUserModal')); | ||||
|             modal.show(); | ||||
|         } | ||||
|  | ||||
|         async function editUser(userId) { | ||||
|             try { | ||||
|                 const response = await axios.get(`/api/auth/admin/users/${userId}`); | ||||
|                 const user = response.data.user; | ||||
|                  | ||||
|                 editingUserId = userId; | ||||
|                 document.getElementById('createUserModalTitle').textContent = 'Edit User'; | ||||
|                 document.getElementById('userEmail').value = user.email; | ||||
|                 document.getElementById('userUsername').value = user.username; | ||||
|                 document.getElementById('userPassword').value = ''; | ||||
|                 document.getElementById('userPassword').required = false; | ||||
|                 document.getElementById('userFullName').value = user.full_name || ''; | ||||
|                 document.getElementById('userRole').value = user.role; | ||||
|                 document.getElementById('userVerified').checked = user.is_verified; | ||||
|                 document.getElementById('userRateLimit').value = user.rate_limit_per_minute; | ||||
|                  | ||||
|                 const modal = new bootstrap.Modal(document.getElementById('createUserModal')); | ||||
|                 modal.show(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to load user for editing:', error); | ||||
|                 showAlert('Failed to load user', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         async function saveUser() { | ||||
|             try { | ||||
|                 const data = { | ||||
|                     email: document.getElementById('userEmail').value, | ||||
|                     username: document.getElementById('userUsername').value, | ||||
|                     full_name: document.getElementById('userFullName').value, | ||||
|                     role: document.getElementById('userRole').value, | ||||
|                     is_verified: document.getElementById('userVerified').checked, | ||||
|                     rate_limit_per_minute: parseInt(document.getElementById('userRateLimit').value) | ||||
|                 }; | ||||
|                  | ||||
|                 if (editingUserId) { | ||||
|                     // Update existing user | ||||
|                     if (document.getElementById('userPassword').value) { | ||||
|                         data.password = document.getElementById('userPassword').value; | ||||
|                     } | ||||
|                     await axios.put(`/api/auth/admin/users/${editingUserId}`, data); | ||||
|                     showAlert('User updated successfully', 'success'); | ||||
|                 } else { | ||||
|                     // Create new user | ||||
|                     data.password = document.getElementById('userPassword').value; | ||||
|                     await axios.post('/api/auth/admin/users', data); | ||||
|                     showAlert('User created successfully', 'success'); | ||||
|                 } | ||||
|                  | ||||
|                 bootstrap.Modal.getInstance(document.getElementById('createUserModal')).hide(); | ||||
|                 loadUsers(currentPage); | ||||
|                 loadStats(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to save user:', error); | ||||
|                 showAlert(error.response?.data?.error || 'Failed to save user', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         async function suspendUser(userId) { | ||||
|             if (!confirm('Are you sure you want to suspend this user?')) return; | ||||
|              | ||||
|             try { | ||||
|                 const reason = prompt('Enter suspension reason:'); | ||||
|                 if (!reason) return; | ||||
|                  | ||||
|                 await axios.post(`/api/auth/admin/users/${userId}/suspend`, { reason }); | ||||
|                 showAlert('User suspended successfully', 'success'); | ||||
|                 loadUsers(currentPage); | ||||
|                 loadStats(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to suspend user:', error); | ||||
|                 showAlert('Failed to suspend user', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         async function unsuspendUser(userId) { | ||||
|             if (!confirm('Are you sure you want to unsuspend this user?')) return; | ||||
|              | ||||
|             try { | ||||
|                 await axios.post(`/api/auth/admin/users/${userId}/unsuspend`); | ||||
|                 showAlert('User unsuspended successfully', 'success'); | ||||
|                 loadUsers(currentPage); | ||||
|                 loadStats(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to unsuspend user:', error); | ||||
|                 showAlert('Failed to unsuspend user', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         async function deleteUser(userId) { | ||||
|             if (!confirm('Are you sure you want to delete this user? This action cannot be undone.')) return; | ||||
|              | ||||
|             try { | ||||
|                 await axios.delete(`/api/auth/admin/users/${userId}`); | ||||
|                 showAlert('User deleted successfully', 'success'); | ||||
|                 loadUsers(currentPage); | ||||
|                 loadStats(); | ||||
|             } catch (error) { | ||||
|                 console.error('Failed to delete user:', error); | ||||
|                 showAlert('Failed to delete user', 'danger'); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         function copyToClipboard(text) { | ||||
|             navigator.clipboard.writeText(text).then(() => { | ||||
|                 showAlert('Copied to clipboard', 'success'); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         function showAlert(message, type) { | ||||
|             const alertDiv = document.createElement('div'); | ||||
|             alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed top-0 start-50 translate-middle-x mt-3`; | ||||
|             alertDiv.style.zIndex = '9999'; | ||||
|             alertDiv.innerHTML = ` | ||||
|                 ${message} | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | ||||
|             `; | ||||
|             document.body.appendChild(alertDiv); | ||||
|              | ||||
|             setTimeout(() => { | ||||
|                 alertDiv.remove(); | ||||
|             }, 5000); | ||||
|         } | ||||
|  | ||||
|         function logout() { | ||||
|             localStorage.removeItem('auth_token'); | ||||
|             window.location.href = '/login'; | ||||
|         } | ||||
|     </script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										287
									
								
								templates/login.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								templates/login.html
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,287 @@ | ||||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||
|     <title>Login - Talk2Me</title> | ||||
|     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"> | ||||
|     <style> | ||||
|         body { | ||||
|             background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | ||||
|             height: 100vh; | ||||
|             display: flex; | ||||
|             align-items: center; | ||||
|             justify-content: center; | ||||
|         } | ||||
|         .login-container { | ||||
|             background: white; | ||||
|             padding: 2rem; | ||||
|             border-radius: 10px; | ||||
|             box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); | ||||
|             width: 100%; | ||||
|             max-width: 400px; | ||||
|         } | ||||
|         .login-header { | ||||
|             text-align: center; | ||||
|             margin-bottom: 2rem; | ||||
|         } | ||||
|         .login-header h1 { | ||||
|             color: #333; | ||||
|             font-size: 2rem; | ||||
|             margin-bottom: 0.5rem; | ||||
|         } | ||||
|         .login-header p { | ||||
|             color: #666; | ||||
|             margin: 0; | ||||
|         } | ||||
|         .form-control:focus { | ||||
|             border-color: #667eea; | ||||
|             box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25); | ||||
|         } | ||||
|         .btn-primary { | ||||
|             background-color: #667eea; | ||||
|             border-color: #667eea; | ||||
|             width: 100%; | ||||
|             padding: 0.75rem; | ||||
|             font-weight: 500; | ||||
|             margin-top: 1rem; | ||||
|         } | ||||
|         .btn-primary:hover { | ||||
|             background-color: #5a67d8; | ||||
|             border-color: #5a67d8; | ||||
|         } | ||||
|         .alert { | ||||
|             font-size: 0.875rem; | ||||
|         } | ||||
|         .divider { | ||||
|             text-align: center; | ||||
|             margin: 1.5rem 0; | ||||
|             position: relative; | ||||
|         } | ||||
|         .divider::before { | ||||
|             content: ''; | ||||
|             position: absolute; | ||||
|             top: 50%; | ||||
|             left: 0; | ||||
|             right: 0; | ||||
|             height: 1px; | ||||
|             background: #e0e0e0; | ||||
|         } | ||||
|         .divider span { | ||||
|             background: white; | ||||
|             padding: 0 1rem; | ||||
|             position: relative; | ||||
|             color: #666; | ||||
|             font-size: 0.875rem; | ||||
|         } | ||||
|         .api-key-section { | ||||
|             margin-top: 1.5rem; | ||||
|             padding-top: 1.5rem; | ||||
|             border-top: 1px solid #e0e0e0; | ||||
|         } | ||||
|         .api-key-display { | ||||
|             background: #f8f9fa; | ||||
|             padding: 0.75rem; | ||||
|             border-radius: 5px; | ||||
|             font-family: monospace; | ||||
|             font-size: 0.875rem; | ||||
|             word-break: break-all; | ||||
|             margin-top: 0.5rem; | ||||
|         } | ||||
|         .back-link { | ||||
|             text-align: center; | ||||
|             margin-top: 1rem; | ||||
|         } | ||||
|         .loading { | ||||
|             display: none; | ||||
|         } | ||||
|         .loading.show { | ||||
|             display: inline-block; | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
|     <div class="login-container"> | ||||
|         <div class="login-header"> | ||||
|             <h1><i class="bi bi-translate"></i> Talk2Me</h1> | ||||
|             <p>Voice Translation Made Simple</p> | ||||
|         </div> | ||||
|  | ||||
|         <div id="alertContainer"> | ||||
|             {% if error %} | ||||
|             <div class="alert alert-danger alert-dismissible fade show" role="alert"> | ||||
|                 {{ error }} | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | ||||
|             </div> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|  | ||||
|         <form id="loginForm" method="POST" action="{{ url_for('login', next=request.args.get('next')) }}"> | ||||
|             <div class="mb-3"> | ||||
|                 <label for="username" class="form-label">Email or Username</label> | ||||
|                 <div class="input-group"> | ||||
|                     <span class="input-group-text"><i class="bi bi-person"></i></span> | ||||
|                     <input type="text" class="form-control" id="username" name="username" required autofocus> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <div class="mb-3"> | ||||
|                 <label for="password" class="form-label">Password</label> | ||||
|                 <div class="input-group"> | ||||
|                     <span class="input-group-text"><i class="bi bi-lock"></i></span> | ||||
|                     <input type="password" class="form-control" id="password" name="password" required> | ||||
|                 </div> | ||||
|             </div> | ||||
|             <button type="submit" class="btn btn-primary"> | ||||
|                 <span class="loading spinner-border spinner-border-sm me-2" role="status"></span> | ||||
|                 <span class="btn-text">Sign In</span> | ||||
|             </button> | ||||
|         </form> | ||||
|  | ||||
|         <div class="divider"> | ||||
|             <span>OR</span> | ||||
|         </div> | ||||
|  | ||||
|         <div class="text-center"> | ||||
|             <p class="mb-2">Use the app without signing in</p> | ||||
|             <a href="/" class="btn btn-outline-secondary w-100"> | ||||
|                 <i class="bi bi-arrow-right-circle"></i> Continue as Guest | ||||
|             </a> | ||||
|         </div> | ||||
|  | ||||
|         <div class="api-key-section" id="apiKeySection" style="display: none;"> | ||||
|             <h6 class="mb-2">Your API Key</h6> | ||||
|             <p class="text-muted small">Use this key to authenticate API requests:</p> | ||||
|             <div class="api-key-display" id="apiKeyDisplay"> | ||||
|                 <span id="apiKey"></span> | ||||
|                 <button class="btn btn-sm btn-outline-secondary float-end" onclick="copyApiKey()"> | ||||
|                     <i class="bi bi-clipboard"></i> Copy | ||||
|                 </button> | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="back-link"> | ||||
|             <a href="/" class="text-muted small"> | ||||
|                 <i class="bi bi-arrow-left"></i> Back to App | ||||
|             </a> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script> | ||||
|     <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> | ||||
|     <script> | ||||
|         // Check if already logged in | ||||
|         const authToken = localStorage.getItem('auth_token'); | ||||
|         if (authToken) { | ||||
|             // Verify token is still valid | ||||
|             axios.defaults.headers.common['Authorization'] = `Bearer ${authToken}`; | ||||
|             axios.get('/api/auth/profile').then(response => { | ||||
|                 // Token is valid, redirect to app | ||||
|                 window.location.href = '/'; | ||||
|             }).catch(() => { | ||||
|                 // Token invalid, clear it | ||||
|                 localStorage.removeItem('auth_token'); | ||||
|                 delete axios.defaults.headers.common['Authorization']; | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         document.getElementById('loginForm').addEventListener('submit', async (e) => { | ||||
|             e.preventDefault(); | ||||
|              | ||||
|             const username = document.getElementById('username').value; | ||||
|             const password = document.getElementById('password').value; | ||||
|             const loadingSpinner = document.querySelector('.loading'); | ||||
|             const btnText = document.querySelector('.btn-text'); | ||||
|             const submitBtn = e.target.querySelector('button[type="submit"]'); | ||||
|              | ||||
|             // Show loading state | ||||
|             loadingSpinner.classList.add('show'); | ||||
|             btnText.textContent = 'Signing in...'; | ||||
|             submitBtn.disabled = true; | ||||
|              | ||||
|             try { | ||||
|                 const response = await axios.post('/api/auth/login', { | ||||
|                     username: username, | ||||
|                     password: password | ||||
|                 }); | ||||
|                  | ||||
|                 if (response.data.success) { | ||||
|                     // Store token | ||||
|                     const token = response.data.tokens.access_token; | ||||
|                     localStorage.setItem('auth_token', token); | ||||
|                     localStorage.setItem('refresh_token', response.data.tokens.refresh_token); | ||||
|                     localStorage.setItem('user_id', response.data.user.id); | ||||
|                     localStorage.setItem('username', response.data.user.username); | ||||
|                     localStorage.setItem('user_role', response.data.user.role); | ||||
|                      | ||||
|                     // Show API key | ||||
|                     document.getElementById('apiKey').textContent = response.data.user.api_key; | ||||
|                     document.getElementById('apiKeySection').style.display = 'block'; | ||||
|                      | ||||
|                     showAlert('Login successful! Redirecting...', 'success'); | ||||
|                      | ||||
|                     // Redirect based on role or next parameter | ||||
|                     setTimeout(() => { | ||||
|                         const urlParams = new URLSearchParams(window.location.search); | ||||
|                         const nextUrl = urlParams.get('next'); | ||||
|                          | ||||
|                         if (nextUrl) { | ||||
|                             window.location.href = nextUrl; | ||||
|                         } else if (response.data.user.role === 'admin') { | ||||
|                             window.location.href = '/admin'; | ||||
|                         } else { | ||||
|                             window.location.href = '/'; | ||||
|                         } | ||||
|                     }, 1500); | ||||
|                 } | ||||
|             } catch (error) { | ||||
|                 console.error('Login error:', error); | ||||
|                 const errorMessage = error.response?.data?.error || 'Login failed. Please try again.'; | ||||
|                 showAlert(errorMessage, 'danger'); | ||||
|                  | ||||
|                 // Reset button state | ||||
|                 loadingSpinner.classList.remove('show'); | ||||
|                 btnText.textContent = 'Sign In'; | ||||
|                 submitBtn.disabled = false; | ||||
|                  | ||||
|                 // Clear password field on error | ||||
|                 document.getElementById('password').value = ''; | ||||
|                 document.getElementById('password').focus(); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         function showAlert(message, type) { | ||||
|             const alertContainer = document.getElementById('alertContainer'); | ||||
|             const alert = document.createElement('div'); | ||||
|             alert.className = `alert alert-${type} alert-dismissible fade show`; | ||||
|             alert.innerHTML = ` | ||||
|                 ${message} | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="alert"></button> | ||||
|             `; | ||||
|             alertContainer.innerHTML = ''; | ||||
|             alertContainer.appendChild(alert); | ||||
|              | ||||
|             // Auto-dismiss after 5 seconds | ||||
|             setTimeout(() => { | ||||
|                 alert.remove(); | ||||
|             }, 5000); | ||||
|         } | ||||
|  | ||||
|         function copyApiKey() { | ||||
|             const apiKey = document.getElementById('apiKey').textContent; | ||||
|             navigator.clipboard.writeText(apiKey).then(() => { | ||||
|                 showAlert('API key copied to clipboard!', 'success'); | ||||
|             }).catch(() => { | ||||
|                 showAlert('Failed to copy API key', 'danger'); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         // Handle Enter key in form fields | ||||
|         document.getElementById('username').addEventListener('keypress', (e) => { | ||||
|             if (e.key === 'Enter') { | ||||
|                 document.getElementById('password').focus(); | ||||
|             } | ||||
|         }); | ||||
|     </script> | ||||
| </body> | ||||
| </html> | ||||
		Reference in New Issue
	
	Block a user