feat: add multi-user RBAC with admin/viewer roles and user management
Some checks failed
CI / Check (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Formatting (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Release Build (push) Has been cancelled

Add complete multi-user support with role-based access control:

Backend:
- Add users CRUD endpoints (GET/POST/PUT/DELETE /api/users) with admin-only guards
- Add display_name column to users table with ALTER TABLE migration
- Fix auth to use session-based user identity (not hardcoded 'admin')
- Add POST /api/auth/logout to revoke server-side sessions
- Add require_admin() and extract_session() helpers for clean RBAC
- Guard all mutating endpoints (clients, providers, models, settings, backup)

Frontend:
- Add Users management page with create/edit/reset-password/delete modals
- Add role gating: hide edit/delete buttons for viewers on clients, providers, models
- Settings page hides auth tokens and admin actions for viewers
- Logout now revokes server session before clearing localStorage
- Sidebar shows real display_name and formatted role (Administrator/Viewer)
- Fix sidebar header: single logo with onerror fallback, renamed to 'LLM Proxy'
- Add badge and btn-action CSS classes for role pills and action buttons
- Bump cache-bust to v=7
This commit is contained in:
2026-03-02 15:58:33 -05:00
parent 5bf41be343
commit e07377adc0
17 changed files with 885 additions and 49 deletions

View File

@@ -15,6 +15,7 @@ class Dashboard {
this.setupSidebar();
this.setupRefresh();
this.updateTime();
this.applyRoleGating();
// Load initial page from hash or default to overview
const initialPage = window.location.hash.substring(1) || 'overview';
@@ -23,6 +24,20 @@ class Dashboard {
setInterval(() => this.updateTime(), 1000);
}
/** Hide admin-only menu items and mutation controls for non-admin users */
applyRoleGating() {
const user = window.authManager && window.authManager.user;
const isAdmin = user && user.role === 'admin';
// Toggle visibility of admin-only sidebar items
document.querySelectorAll('.menu-item.admin-only').forEach(item => {
item.style.display = isAdmin ? '' : 'none';
});
// Store role for use by page scripts
window._userRole = isAdmin ? 'admin' : 'viewer';
}
setupNavigation() {
const menuItems = document.querySelectorAll('.menu-item');
menuItems.forEach(item => {
@@ -103,7 +118,8 @@ class Dashboard {
'monitoring': 'Monitoring',
'settings': 'Settings',
'logs': 'Logs',
'models': 'Models'
'models': 'Models',
'users': 'User Management'
};
if (titleElement) titleElement.textContent = titles[page] || 'Dashboard';
@@ -145,6 +161,7 @@ class Dashboard {
case 'settings': return '<div class="loading-placeholder">Loading settings...</div>';
case 'analytics': return this.getAnalyticsTemplate();
case 'costs': return this.getCostsTemplate();
case 'users': return this.getUsersTemplate();
default: return '<div class="empty-state"><h3>Page not found</h3></div>';
}
}
@@ -219,6 +236,7 @@ class Dashboard {
}
getClientsTemplate() {
const isAdmin = window._userRole === 'admin';
return `
<div class="card">
<div class="card-header">
@@ -226,9 +244,9 @@ class Dashboard {
<h3 class="card-title">API Clients</h3>
<p class="card-subtitle">Manage tokens and access</p>
</div>
<button class="btn btn-primary" id="add-client">
${isAdmin ? `<button class="btn btn-primary" id="add-client">
<i class="fas fa-plus"></i> Create Client
</button>
</button>` : ''}
</div>
<div class="table-container">
<table class="table" id="clients-table">
@@ -474,6 +492,30 @@ class Dashboard {
</div>
`;
}
getUsersTemplate() {
return `
<div class="card">
<div class="card-header">
<div>
<h3 class="card-title">Dashboard Users</h3>
<p class="card-subtitle">Manage accounts and roles</p>
</div>
<button class="btn btn-primary" id="add-user">
<i class="fas fa-user-plus"></i> Create User
</button>
</div>
<div class="table-container">
<table class="table" id="users-table">
<thead>
<tr><th>Username</th><th>Display Name</th><th>Role</th><th>Created</th><th>Status</th><th>Actions</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
`;
}
}
document.addEventListener('DOMContentLoaded', () => {