feat: add multi-user RBAC with admin/viewer roles and user management
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:
@@ -87,7 +87,20 @@ class AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
logout() {
|
||||
async logout() {
|
||||
// Revoke server-side session first
|
||||
try {
|
||||
if (this.token) {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.token}` }
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
// Best-effort — still clear local state even if server call fails
|
||||
console.warn('Server logout failed:', e);
|
||||
}
|
||||
|
||||
// Clear localStorage
|
||||
localStorage.removeItem('dashboard_token');
|
||||
localStorage.removeItem('dashboard_user');
|
||||
@@ -104,14 +117,6 @@ class AuthManager {
|
||||
this.showToast('Successfully logged out', 'info');
|
||||
}
|
||||
|
||||
generateToken() {
|
||||
// Generate a simple token for demo purposes
|
||||
// In production, this would come from the server
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substring(2);
|
||||
return btoa(`${timestamp}:${random}`).replace(/=/g, '');
|
||||
}
|
||||
|
||||
showLogin() {
|
||||
const loginScreen = document.getElementById('login-screen');
|
||||
const dashboard = document.getElementById('dashboard');
|
||||
@@ -154,13 +159,14 @@ class AuthManager {
|
||||
updateUserInfo() {
|
||||
const userNameElement = document.querySelector('.user-name');
|
||||
const userRoleElement = document.querySelector('.user-role');
|
||||
|
||||
|
||||
if (userNameElement && this.user) {
|
||||
userNameElement.textContent = this.user.name;
|
||||
userNameElement.textContent = this.user.name || this.user.username || 'User';
|
||||
}
|
||||
|
||||
|
||||
if (userRoleElement && this.user) {
|
||||
userRoleElement.textContent = this.user.role;
|
||||
const roleLabels = { admin: 'Administrator', viewer: 'Viewer' };
|
||||
userRoleElement.textContent = roleLabels[this.user.role] || this.user.role || 'User';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user