Complete web interface fixes and admin panel

- Fix settings page user dict access error
- Add consistent top navigation to dashboard
- Fix train page user variable scope error
- Update analyze page to use consistent header navigation
- Add admin panel with user management (create, activate/deactivate users)
- Add 'remember me' checkbox to login page
- Change default user tokens from 1000 to 0
- Add admin routes and database functions for user management
- Update all templates to use VidAI branding
parent 0ff1e3eb
image.jpg

133 KB

<!DOCTYPE html>
<html>
<head>
<title>Admin Panel - VidAI</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; }
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }
.admin-card { background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); margin-bottom: 2rem; }
.card-header { margin-bottom: 1.5rem; }
.card-header h3 { margin: 0; color: #1e293b; }
.btn { padding: 0.75rem 2rem; background: #667eea; color: white; border: none; border-radius: 8px; font-size: 1rem; font-weight: 600; cursor: pointer; text-decoration: none; display: inline-block; }
.btn:hover { background: #5a67d8; }
.btn-danger { background: #dc2626; }
.btn-danger:hover { background: #b91c1c; }
.table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
.table th, .table td { padding: 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
.table th { background: #f8fafc; font-weight: 600; color: #374151; }
.status-active { color: #065f46; font-weight: 500; }
.status-inactive { color: #dc2626; font-weight: 500; }
.role-admin { color: #7c3aed; font-weight: 500; }
.role-user { color: #374151; }
.form-group { margin-bottom: 1.5rem; }
.form-group label { display: block; margin-bottom: 0.5rem; color: #374151; font-weight: 500; }
.form-group input, .form-group select { width: 100%; padding: 0.75rem; border: 2px solid #e5e7eb; border-radius: 8px; font-size: 1rem; }
.form-group input:focus, .form-group select:focus { outline: none; border-color: #667eea; }
.alert { padding: 0.75rem; border-radius: 8px; margin-bottom: 1rem; }
.alert-error { background: #fee2e2; color: #dc2626; border: 1px solid #fecaca; }
.alert-success { background: #d1fae5; color: #065f46; border: 1px solid #a7f3d0; }
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">VidAI</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/settings">Settings</a>
<a href="/admin" class="active">Admin</a>
</nav>
<div class="user-menu">
<span>{{ user.get('username', 'Admin') }}</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="admin-card">
<div class="card-header">
<h3><i class="fas fa-users-cog"></i> User Management</h3>
</div>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ 'error' if category == 'error' else 'success' }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% endwith %}
<table class="table">
<thead>
<tr>
<th>Username</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<th>Tokens</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.get('username', 'N/A') }}</td>
<td>{{ user.get('email', 'N/A') }}</td>
<td><span class="role-{{ user.get('role', 'user') }}">{{ user.get('role', 'user').title() }}</span></td>
<td><span class="status-{{ 'active' if user.get('active') else 'inactive' }}">{{ 'Active' if user.get('active') else 'Inactive' }}</span></td>
<td>{{ user.get('tokens', 0) }}</td>
<td>{{ user.get('created_at', 'N/A')[:10] if user.get('created_at') else 'N/A' }}</td>
<td>
<a href="/admin/user/{{ user.get('id') }}/edit" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Edit</a>
{% if user.get('active') %}
<a href="/admin/user/{{ user.get('id') }}/deactivate" class="btn btn-danger" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Deactivate</a>
{% else %}
<a href="/admin/user/{{ user.get('id') }}/activate" class="btn" style="padding: 0.5rem 1rem; font-size: 0.9rem; background: #059669;">Activate</a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="admin-card">
<div class="card-header">
<h3><i class="fas fa-plus"></i> Create New User</h3>
</div>
<form method="post" action="/admin/user/create">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="role">Role</label>
<select id="role" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<label for="tokens">Initial Tokens</label>
<input type="number" id="tokens" name="tokens" value="0" min="0">
</div>
<button type="submit" class="btn">Create User</button>
</form>
</div>
</div>
</body>
</html>
\ No newline at end of file
......@@ -6,12 +6,17 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Inter', sans-serif; background: #f4f4f4; margin: 0; padding: 20px; display: flex; justify-content: center; align-items: flex-start; }
.main { flex: 1; max-width: 800px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); margin-right: 20px; }
.sidebar { width: 300px; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
h1 { color: #333; text-align: center; }
nav { text-align: center; margin-bottom: 20px; }
nav a { margin: 0 10px; text-decoration: none; color: #007bff; }
body { font-family: 'Inter', sans-serif; background: #f8fafc; }
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; display: flex; gap: 2rem; }
.main { flex: 1; background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
.sidebar { width: 300px; background: white; padding: 2rem; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); }
form { margin-bottom: 20px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="file"], textarea { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
......@@ -63,11 +68,25 @@
</script>
</head>
<body>
<div class="main">
<h1>VidAI</h1>
<nav>
<a href="/dashboard">Dashboard</a> | <a href="/analyze">Analyze</a> | <a href="/train">Train</a> | <a href="/history">History</a> | <a href="/settings">Settings</a>
</nav>
<header class="header">
<div class="header-content">
<div class="logo">VidAI</div>
<nav class="nav">
<a href="/dashboard">Dashboard</a>
<a href="/analyze" class="active">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/settings">Settings</a>
</nav>
<div class="user-menu">
<span>{{ tokens }} tokens</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
<div class="container">
<div class="main">
<div class="tokens">
<strong>Available Tokens:</strong> {{ tokens }} (Analysis costs ~10 tokens)
......@@ -109,16 +128,17 @@
<div class="result" id="result_div" style="display:none;"></div>
{% if result %}
<div class="result">
<h3>Result:</h3>
<p>{{ result }}</p>
{% if result %}
<div class="result">
<h3>Result:</h3>
<p>{{ result }}</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
<div class="sidebar">
<div id="stats" class="stats">Loading stats...</div>
<div class="sidebar">
<div id="stats" class="stats">Loading stats...</div>
</div>
</div>
</body>
</html>
\ No newline at end of file
......@@ -10,6 +10,9 @@
.header { background: white; padding: 1rem 2rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
.header-content { display: flex; justify-content: space-between; align-items: center; max-width: 1200px; margin: 0 auto; }
.logo { font-size: 1.5rem; font-weight: 700; color: #667eea; }
.nav { display: flex; gap: 2rem; }
.nav a { text-decoration: none; color: #64748b; font-weight: 500; }
.nav a.active { color: #667eea; }
.user-menu { display: flex; align-items: center; gap: 1rem; }
.container { max-width: 1200px; margin: 2rem auto; padding: 0 2rem; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1.5rem; margin-bottom: 2rem; }
......@@ -34,9 +37,16 @@
<header class="header">
<div class="header-content">
<div class="logo">VidAI</div>
<nav class="nav">
<a href="/dashboard" class="active">Dashboard</a>
<a href="/analyze">Analyze</a>
<a href="/train">Train</a>
<a href="/history">History</a>
<a href="/settings">Settings</a>
</nav>
<div class="user-menu">
<span>Welcome, {{ user.username }}!</span>
<a href="/logout" class="action-btn" style="padding: 0.5rem 1rem; font-size: 0.9rem;">Logout</a>
<span>{{ tokens }} tokens</span>
<a href="/logout" style="color: #dc2626;">Logout</a>
</div>
</div>
</header>
......
......@@ -43,6 +43,11 @@
<input type="password" id="password" name="password" required>
</div>
<div class="form-group" style="display: flex; align-items: center; gap: 0.5rem;">
<input type="checkbox" id="remember" name="remember">
<label for="remember" style="margin: 0; font-weight: normal;">Remember me</label>
</div>
<button type="submit" class="btn">Login</button>
</form>
......
......@@ -51,10 +51,10 @@
<div class="container">
<div class="user-info">
<h3><i class="fas fa-user"></i> Account Information</h3>
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>Email:</strong> {{ user.email or "Not provided" }}</p>
<p><strong>Role:</strong> {{ user.role.title() }}</p>
<p><strong>Member since:</strong> {{ user.created_at[:10] }}</p>
<p><strong>Username:</strong> {{ user['username'] }}</p>
<p><strong>Email:</strong> {{ user.get('email', 'Not provided') }}</p>
<p><strong>Role:</strong> {{ user.get('role', 'user').title() }}</p>
<p><strong>Member since:</strong> {{ user.get('created_at', 'Unknown')[:10] if user.get('created_at') else 'Unknown' }}</p>
</div>
<div class="settings-card">
......
......@@ -118,6 +118,12 @@ def register_user(username: str, password: str, email: str) -> tuple[bool, str]:
return db_register_user(username, password, email)
def create_user(username: str, password: str, email: str, role: str = 'user', tokens: int = 0) -> tuple[bool, str]:
"""Create a new user (admin function)."""
from .database import create_user as db_create_user
return db_create_user(username, password, email, role, tokens)
def confirm_email(token: str) -> bool:
"""Confirm user email with token."""
from .database import confirm_email as db_confirm_email
......
......@@ -173,7 +173,7 @@ def init_db(conn) -> None:
email_confirmed BOOLEAN DEFAULT 0,
email_confirmation_token TEXT,
email_confirmation_expires TIMESTAMP,
tokens INTEGER DEFAULT 1000,
tokens INTEGER DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
last_login TIMESTAMP
)
......@@ -876,4 +876,79 @@ def get_queue_position(queue_id: int) -> int:
''', (queue_id,))
row = cursor.fetchone()
conn.close()
return row['position'] + 1 if row else 0
\ No newline at end of file
return row['position'] + 1 if row else 0
# User management functions
def get_all_users() -> List[Dict[str, Any]]:
"""Get all users for admin panel."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('SELECT id, username, email, role, active, tokens, created_at FROM users ORDER BY created_at DESC')
rows = cursor.fetchall()
conn.close()
return [dict(row) for row in rows]
def update_user_status(user_id: int, active: bool) -> bool:
"""Update user active status."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET active = ? WHERE id = ?', (active, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def update_user_role(user_id: int, role: str) -> bool:
"""Update user role."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET role = ? WHERE id = ?', (role, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def update_user_tokens_admin(user_id: int, tokens: int) -> bool:
"""Update user tokens (admin function)."""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('UPDATE users SET tokens = ? WHERE id = ?', (tokens, user_id))
conn.commit()
success = cursor.rowcount > 0
conn.close()
return success
def create_user(username: str, password: str, email: str, role: str = 'user', tokens: int = 0) -> tuple[bool, str]:
"""Create a new user (admin function)."""
conn = get_db_connection()
cursor = conn.cursor()
# Check if username or email already exists
cursor.execute('SELECT id FROM users WHERE username = ? OR email = ?', (username, email))
if cursor.fetchone():
conn.close()
return False, "Username or email already exists"
# Hash password
import hashlib
password_hash = hashlib.sha256(password.encode()).hexdigest()
# Create user
cursor.execute('''
INSERT INTO users (username, password_hash, email, role, tokens, active, created_at)
VALUES (?, ?, ?, ?, ?, 1, datetime('now'))
''', (username, password_hash, email, role, tokens))
conn.commit()
success = cursor.rowcount > 0
conn.close()
if success:
return True, "User created successfully"
else:
return False, "Failed to create user"
\ No newline at end of file
......@@ -231,6 +231,7 @@ def analyze():
@app.route('/train', methods=['GET', 'POST'])
@login_required
def train():
user = get_current_user_session()
message = None
if request.method == 'POST':
# Check token balance
......@@ -352,6 +353,55 @@ def generate_token():
flash(f'New API token generated: {token[:20]}...', 'success')
return redirect(url_for('settings'))
@app.route('/admin')
@admin_required
def admin():
"""Admin panel for user management."""
from .database import get_all_users
users = get_all_users()
user = get_current_user_session()
return render_template('admin.html', users=users, user=user)
@app.route('/admin/user/create', methods=['POST'])
@admin_required
def admin_create_user():
"""Create a new user via admin panel."""
from .auth import create_user
username = request.form.get('username')
email = request.form.get('email')
password = request.form.get('password')
role = request.form.get('role', 'user')
tokens = int(request.form.get('tokens', 0))
success, message = create_user(username, password, email, role, tokens)
if success:
flash('User created successfully!', 'success')
else:
flash(message, 'error')
return redirect(url_for('admin'))
@app.route('/admin/user/<int:user_id>/activate')
@admin_required
def admin_activate_user(user_id):
"""Activate a user account."""
from .database import update_user_status
if update_user_status(user_id, True):
flash('User activated successfully!', 'success')
else:
flash('Failed to activate user.', 'error')
return redirect(url_for('admin'))
@app.route('/admin/user/<int:user_id>/deactivate')
@admin_required
def admin_deactivate_user(user_id):
"""Deactivate a user account."""
from .database import update_user_status
if update_user_status(user_id, False):
flash('User deactivated successfully!', 'success')
else:
flash('Failed to deactivate user.', 'error')
return redirect(url_for('admin'))
@app.route('/stats')
def stats():
"""Get system stats for the sidebar."""
......
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