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:
@@ -85,8 +85,13 @@ pub(super) async fn handle_get_clients(State(state): State<DashboardState>) -> J
|
||||
|
||||
pub(super) async fn handle_create_client(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Json(payload): Json<CreateClientRequest>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
if let Err(e) = super::auth::require_admin(&state, &headers).await {
|
||||
return e;
|
||||
}
|
||||
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let client_id = payload
|
||||
@@ -189,9 +194,14 @@ pub(super) async fn handle_get_client(
|
||||
|
||||
pub(super) async fn handle_update_client(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<UpdateClientPayload>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
if let Err(e) = super::auth::require_admin(&state, &headers).await {
|
||||
return e;
|
||||
}
|
||||
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// Build dynamic UPDATE query from provided fields
|
||||
@@ -281,8 +291,13 @@ pub(super) async fn handle_update_client(
|
||||
|
||||
pub(super) async fn handle_delete_client(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
if let Err(e) = super::auth::require_admin(&state, &headers).await {
|
||||
return e;
|
||||
}
|
||||
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// Don't allow deleting the default client
|
||||
@@ -418,9 +433,14 @@ pub(super) struct CreateTokenRequest {
|
||||
|
||||
pub(super) async fn handle_create_client_token(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path(id): Path<String>,
|
||||
Json(payload): Json<CreateTokenRequest>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
if let Err(e) = super::auth::require_admin(&state, &headers).await {
|
||||
return e;
|
||||
}
|
||||
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
// Verify client exists
|
||||
@@ -462,8 +482,13 @@ pub(super) async fn handle_create_client_token(
|
||||
|
||||
pub(super) async fn handle_delete_client_token(
|
||||
State(state): State<DashboardState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
Path((client_id, token_id)): Path<(String, i64)>,
|
||||
) -> Json<ApiResponse<serde_json::Value>> {
|
||||
if let Err(e) = super::auth::require_admin(&state, &headers).await {
|
||||
return e;
|
||||
}
|
||||
|
||||
let pool = &state.app_state.db_pool;
|
||||
|
||||
let result = sqlx::query("DELETE FROM client_tokens WHERE id = ? AND client_id = ?")
|
||||
|
||||
Reference in New Issue
Block a user