Commit e9a2c8b9 authored by Your Name's avatar Your Name

Add admin user management feature to dashboard

- Added new dashboard template (templates/dashboard/users.html) for managing users
- Added routes in main.py: GET /dashboard/users, POST /dashboard/users/add,
  POST /dashboard/users/{id}/edit, POST /dashboard/users/{id}/toggle,
  POST /dashboard/users/{id}/delete
- Added 'Users' link to navigation menu (visible only for admin users)
- Added update_user method to database.py for editing user details

Features:
- Add new users with username, password, and role (user/admin)
- Edit existing user details
- Toggle user active/inactive status
- Delete users
parent 97ad28ec
......@@ -621,6 +621,46 @@ class DatabaseManager:
cursor.execute('DELETE FROM users WHERE id = ?', (user_id,))
conn.commit()
def update_user(self, user_id: int, username: str, password_hash: str = None, role: str = None, is_active: bool = None):
"""
Update a user.
Args:
user_id: User ID to update
username: New username
password_hash: New password hash (optional)
role: New role (optional)
is_active: New active status (optional)
"""
with self._get_connection() as conn:
cursor = conn.cursor()
placeholder = '?' if self.db_type == 'sqlite' else '%s'
# Build update query dynamically
updates = []
params = []
updates.append(f"username = {placeholder}")
params.append(username)
if password_hash:
updates.append(f"password_hash = {placeholder}")
params.append(password_hash)
if role:
updates.append(f"role = {placeholder}")
params.append(role)
if is_active is not None:
updates.append(f"is_active = {placeholder}")
params.append(1 if is_active else 0)
params.append(user_id)
query = f"UPDATE users SET {', '.join(updates)} WHERE id = {placeholder}"
cursor.execute(query, params)
conn.commit()
# User-specific provider methods
def save_user_provider(self, user_id: int, provider_name: str, config: Dict):
"""
......
......@@ -1843,6 +1843,124 @@ async def dashboard_settings_save(
"success": "Settings saved successfully! Restart server for changes to take effect."
})
return templates.TemplateResponse("dashboard/settings.html", {
"request": request,
"session": request.session,
"config": aisbf_config,
"success": "Settings saved successfully! Restart server for changes to take effect."
})
# Admin user management routes
@app.get("/dashboard/users", response_class=HTMLResponse)
async def dashboard_users(request: Request):
"""Admin user management page"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
# Get all users
users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", {
"request": request,
"session": request.session,
"users": users
})
@app.post("/dashboard/users/add")
async def dashboard_users_add(request: Request, username: str = Form(...), password: str = Form(...), role: str = Form("user")):
"""Add a new user"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
# Hash the password
password_hash = hashlib.sha256(password.encode()).hexdigest()
try:
# Get current admin username
admin_username = request.session.get('username', 'admin')
user_id = db.create_user(username, password_hash, role, admin_username)
return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303)
except Exception as e:
users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", {
"request": request,
"session": request.session,
"users": users,
"error": f"Failed to create user: {str(e)}"
})
@app.post("/dashboard/users/{user_id}/edit")
async def dashboard_users_edit(request: Request, user_id: int, username: str = Form(...), password: str = Form(""), role: str = Form("user"), is_active: bool = Form(True)):
"""Edit an existing user"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
# Update user (only if password is provided)
if password:
password_hash = hashlib.sha256(password.encode()).hexdigest()
db.update_user(user_id, username, password_hash, role, is_active)
else:
db.update_user(user_id, username, None, role, is_active)
return RedirectResponse(url=url_for(request, "/dashboard/users"), status_code=303)
except Exception as e:
users = db.get_users()
return templates.TemplateResponse("dashboard/users.html", {
"request": request,
"session": request.session,
"users": users,
"error": f"Failed to update user: {str(e)}"
})
@app.post("/dashboard/users/{user_id}/toggle")
async def dashboard_users_toggle(request: Request, user_id: int):
"""Toggle user active status"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
users = db.get_users()
for user in users:
if user['id'] == user_id:
new_status = not user['is_active']
db.update_user(user_id, user['username'], None, user['role'], new_status)
return JSONResponse({"success": True})
return JSONResponse({"success": False, "error": "User not found"}, status_code=404)
except Exception as e:
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@app.post("/dashboard/users/{user_id}/delete")
async def dashboard_users_delete(request: Request, user_id: int):
"""Delete a user"""
auth_check = require_admin(request)
if auth_check:
return auth_check
from aisbf.database import get_database
db = get_database()
try:
db.delete_user(user_id)
return JSONResponse({"success": True})
except Exception as e:
return JSONResponse({"success": False, "error": str(e)}, status_code=500)
@app.post("/dashboard/restart")
async def dashboard_restart(request: Request):
"""Restart the server"""
......
......@@ -27,8 +27,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.header { background: #16213e; color: white; padding: 20px 0; margin-bottom: 30px; border-bottom: 2px solid #0f3460; }
.header h1 { font-size: 24px; font-weight: 600; display: inline-block; }
.header-actions { float: right; }
.nav { background: #16213e; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); }
.nav a { color: #a0a0a0; text-decoration: none; margin-right: 20px; padding: 8px 12px; border-radius: 4px; }
.nav { background: #16213e; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); display: flex; flex-wrap: wrap; gap: 10px; }
.nav a { color: #a0a0a0; text-decoration: none; padding: 8px 12px; border-radius: 4px; flex-shrink: 0; }
.nav a:hover { background: #0f3460; color: #e0e0e0; }
.nav a.active { background: #e94560; color: white; }
.content { background: #16213e; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.3); }
......@@ -110,6 +110,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<a href="{{ url_for(request, '/dashboard/autoselect') }}" {% if '/autoselect' in request.path %}class="active"{% endif %}>Autoselect</a>
<a href="{{ url_for(request, '/dashboard/prompts') }}" {% if '/prompts' in request.path %}class="active"{% endif %}>Prompts</a>
<a href="{{ url_for(request, '/dashboard/analytics') }}" {% if '/analytics' in request.path %}class="active"{% endif %}>Analytics</a>
{% if request.session.role == 'admin' %}
<a href="{{ url_for(request, '/dashboard/users') }}" {% if '/users' in request.path %}class="active"{% endif %}>Users</a>
{% endif %}
<a href="{{ url_for(request, '/dashboard/settings') }}" {% if '/settings' in request.path %}class="active"{% endif %}>Settings</a>
<a href="{{ url_for(request, '/dashboard/docs') }}" {% if '/docs' in request.path %}class="active"{% endif %}>Docs</a>
<a href="{{ url_for(request, '/dashboard/about') }}" {% if '/about' in request.path %}class="active"{% endif %}>About</a>
......
<!--
Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
{% extends "base.html" %}
{% block title %}User Management - AISBF Dashboard{% endblock %}
{% block content %}
<h2 style="margin-bottom: 30px;">User Management</h2>
{% if success %}
<div class="alert alert-success">{{ success }}</div>
{% endif %}
{% if error %}
<div class="alert alert-error">{{ error }}</div>
{% endif %}
<!-- Add New User Form -->
<div style="margin-bottom: 30px; padding: 20px; background: #0f3460; border-radius: 8px;">
<h3 style="margin-bottom: 15px;">Add New User</h3>
<form method="POST" action="{{ url_for(request, '/dashboard/users/add') }}" style="display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end;">
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="username">Username</label>
<input type="text" id="username" name="username" required style="width: 100%;">
</div>
<div class="form-group" style="flex: 1; min-width: 200px; margin-bottom: 0;">
<label for="password">Password</label>
<input type="password" id="password" name="password" required style="width: 100%;">
</div>
<div class="form-group" style="flex: 0 0 150px; margin-bottom: 0;">
<label for="role">Role</label>
<select id="role" name="role" style="width: 100%;">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<button type="submit" class="btn">Add User</button>
</form>
</div>
<!-- Users Table -->
<h3 style="margin-bottom: 15px;">All Users</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Role</th>
<th>Created By</th>
<th>Created At</th>
<th>Last Login</th>
<th>Active</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% if users %}
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td>{{ user.username }}</td>
<td>
{% if user.role == 'admin' %}
<span style="color: #e94560; font-weight: bold;">Admin</span>
{% else %}
<span style="color: #a0a0a0;">User</span>
{% endif %}
</td>
<td>{{ user.created_by or '-' }}</td>
<td>{{ user.created_at }}</td>
<td>{{ user.last_login or 'Never' }}</td>
<td>
{% if user.is_active %}
<span style="color: #4ade80;">Yes</span>
{% else %}
<span style="color: #f87171;">No</span>
{% endif %}
</td>
<td>
<div style="display: flex; gap: 5px; flex-wrap: wrap;">
<button onclick="editUser({{ user.id }}, '{{ user.username }}', '{{ user.role }}', {{ user.is_active|lower }})" class="btn btn-secondary" style="padding: 5px 10px; font-size: 12px; margin: 0;">Edit</button>
<button onclick="toggleUserStatus({{ user.id }}, {{ user.is_active|lower }})" class="btn btn-warning" style="padding: 5px 10px; font-size: 12px; margin: 0;">
{% if user.is_active %}Disable{% else %}Enable{% endif %}
</button>
<button onclick="deleteUser({{ user.id }}, '{{ user.username }}')" class="btn btn-danger" style="padding: 5px 10px; font-size: 12px; margin: 0;">Delete</button>
</div>
</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="8" style="text-align: center; color: #666;">No users found</td>
</tr>
{% endif %}
</tbody>
</table>
<!-- Edit User Modal -->
<div id="edit-modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 1000;">
<div style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #16213e; padding: 30px; border-radius: 8px; width: 90%; max-width: 500px;">
<h3 style="margin-bottom: 20px;">Edit User</h3>
<form id="edit-form" method="POST">
<input type="hidden" id="edit-user-id" name="user_id">
<div class="form-group">
<label for="edit-username">Username</label>
<input type="text" id="edit-username" name="username" required>
</div>
<div class="form-group">
<label for="edit-password">New Password (leave blank to keep current)</label>
<input type="password" id="edit-password" name="password" placeholder="Enter new password">
</div>
<div class="form-group">
<label for="edit-role">Role</label>
<select id="edit-role" name="role">
<option value="user">User</option>
<option value="admin">Admin</option>
</select>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="edit-is-active" name="is_active" value="1">
Active
</label>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit" class="btn">Save Changes</button>
<button type="button" onclick="closeEditModal()" class="btn btn-secondary">Cancel</button>
</div>
</form>
</div>
</div>
<script>
function editUser(userId, username, role, isActive) {
document.getElementById('edit-user-id').value = userId;
document.getElementById('edit-username').value = username;
document.getElementById('edit-role').value = role;
document.getElementById('edit-is-active').checked = isActive;
document.getElementById('edit-form').action = '/dashboard/users/' + userId + '/edit';
document.getElementById('edit-modal').style.display = 'block';
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
}
function toggleUserStatus(userId, currentStatus) {
const action = currentStatus ? 'disable' : 'enable';
if (confirm('Are you sure you want to ' + action + ' this user?')) {
fetch('/dashboard/users/' + userId + '/toggle', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Failed to toggle user status');
}
})
.catch(error => {
alert('Error: ' + error);
});
}
}
function deleteUser(userId, username) {
if (confirm('Are you sure you want to delete user "' + username + '"? This action cannot be undone.')) {
fetch('/dashboard/users/' + userId + '/delete', {
method: 'POST',
headers: {'Content-Type': 'application/json'}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert(data.error || 'Failed to delete user');
}
})
.catch(error => {
alert('Error: ' + error);
});
}
}
// Close modal when clicking outside
window.onclick = function(event) {
const modal = document.getElementById('edit-modal');
if (event.target === modal) {
closeEditModal();
}
}
</script>
<style>
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #0f3460;
}
th {
background: #0f3460;
font-weight: 600;
}
</style>
{% endblock %}
\ No newline at end of file
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