- Database schema: users, permissions, client_access tables - Auth: JWT tokens with Argon2 password hashing - API: login, user CRUD, permission management - Dashboard: login required, admin Users tab - Auto-creates initial admin user on first run
593 lines
16 KiB
Rust
593 lines
16 KiB
Rust
//! User management API endpoints (admin only)
|
|
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::StatusCode,
|
|
Json,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use uuid::Uuid;
|
|
|
|
use crate::auth::{hash_password, AdminUser};
|
|
use crate::db;
|
|
use crate::AppState;
|
|
|
|
use super::auth::ErrorResponse;
|
|
|
|
/// User info response
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UserInfo {
|
|
pub id: String,
|
|
pub username: String,
|
|
pub email: Option<String>,
|
|
pub role: String,
|
|
pub enabled: bool,
|
|
pub created_at: String,
|
|
pub last_login: Option<String>,
|
|
pub permissions: Vec<String>,
|
|
}
|
|
|
|
/// Create user request
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateUserRequest {
|
|
pub username: String,
|
|
pub password: String,
|
|
pub email: Option<String>,
|
|
pub role: String,
|
|
pub permissions: Option<Vec<String>>,
|
|
}
|
|
|
|
/// Update user request
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UpdateUserRequest {
|
|
pub email: Option<String>,
|
|
pub role: String,
|
|
pub enabled: bool,
|
|
pub password: Option<String>,
|
|
}
|
|
|
|
/// Set permissions request
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SetPermissionsRequest {
|
|
pub permissions: Vec<String>,
|
|
}
|
|
|
|
/// Set client access request
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SetClientAccessRequest {
|
|
pub client_ids: Vec<String>,
|
|
}
|
|
|
|
/// GET /api/users - List all users
|
|
pub async fn list_users(
|
|
State(state): State<AppState>,
|
|
_admin: AdminUser,
|
|
) -> Result<Json<Vec<UserInfo>>, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let users = db::get_all_users(db.pool())
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to fetch users".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let mut result = Vec::new();
|
|
for user in users {
|
|
let permissions = db::get_user_permissions(db.pool(), user.id)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
result.push(UserInfo {
|
|
id: user.id.to_string(),
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
enabled: user.enabled,
|
|
created_at: user.created_at.to_rfc3339(),
|
|
last_login: user.last_login.map(|t| t.to_rfc3339()),
|
|
permissions,
|
|
});
|
|
}
|
|
|
|
Ok(Json(result))
|
|
}
|
|
|
|
/// POST /api/users - Create new user
|
|
pub async fn create_user(
|
|
State(state): State<AppState>,
|
|
_admin: AdminUser,
|
|
Json(request): Json<CreateUserRequest>,
|
|
) -> Result<Json<UserInfo>, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Validate role
|
|
let valid_roles = ["admin", "operator", "viewer"];
|
|
if !valid_roles.contains(&request.role.as_str()) {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: format!("Invalid role. Must be one of: {:?}", valid_roles),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Validate password
|
|
if request.password.len() < 8 {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Password must be at least 8 characters".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Check if username exists
|
|
if db::get_user_by_username(db.pool(), &request.username)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Database error".to_string(),
|
|
}),
|
|
)
|
|
})?
|
|
.is_some()
|
|
{
|
|
return Err((
|
|
StatusCode::CONFLICT,
|
|
Json(ErrorResponse {
|
|
error: "Username already exists".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Hash password
|
|
let password_hash = hash_password(&request.password).map_err(|e| {
|
|
tracing::error!("Password hashing error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to hash password".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Create user
|
|
let user = db::create_user(
|
|
db.pool(),
|
|
&request.username,
|
|
&password_hash,
|
|
request.email.as_deref(),
|
|
&request.role,
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to create user: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to create user".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Set initial permissions if provided
|
|
let permissions = if let Some(perms) = request.permissions {
|
|
db::set_user_permissions(db.pool(), user.id, &perms)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Failed to set permissions: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to set permissions".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
perms
|
|
} else {
|
|
// Default permissions based on role
|
|
let default_perms = match request.role.as_str() {
|
|
"admin" => vec!["view", "control", "transfer", "manage_users", "manage_clients"],
|
|
"operator" => vec!["view", "control", "transfer"],
|
|
"viewer" => vec!["view"],
|
|
_ => vec!["view"],
|
|
};
|
|
let perms: Vec<String> = default_perms.into_iter().map(String::from).collect();
|
|
db::set_user_permissions(db.pool(), user.id, &perms)
|
|
.await
|
|
.ok();
|
|
perms
|
|
};
|
|
|
|
tracing::info!("Created user: {} ({})", user.username, user.role);
|
|
|
|
Ok(Json(UserInfo {
|
|
id: user.id.to_string(),
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
enabled: user.enabled,
|
|
created_at: user.created_at.to_rfc3339(),
|
|
last_login: None,
|
|
permissions,
|
|
}))
|
|
}
|
|
|
|
/// GET /api/users/:id - Get user details
|
|
pub async fn get_user(
|
|
State(state): State<AppState>,
|
|
_admin: AdminUser,
|
|
Path(id): Path<String>,
|
|
) -> Result<Json<UserInfo>, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user_id = Uuid::parse_str(&id).map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid user ID".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user = db::get_user_by_id(db.pool(), user_id)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Database error".to_string(),
|
|
}),
|
|
)
|
|
})?
|
|
.ok_or_else(|| {
|
|
(
|
|
StatusCode::NOT_FOUND,
|
|
Json(ErrorResponse {
|
|
error: "User not found".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let permissions = db::get_user_permissions(db.pool(), user.id)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
Ok(Json(UserInfo {
|
|
id: user.id.to_string(),
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
enabled: user.enabled,
|
|
created_at: user.created_at.to_rfc3339(),
|
|
last_login: user.last_login.map(|t| t.to_rfc3339()),
|
|
permissions,
|
|
}))
|
|
}
|
|
|
|
/// PUT /api/users/:id - Update user
|
|
pub async fn update_user(
|
|
State(state): State<AppState>,
|
|
admin: AdminUser,
|
|
Path(id): Path<String>,
|
|
Json(request): Json<UpdateUserRequest>,
|
|
) -> Result<Json<UserInfo>, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user_id = Uuid::parse_str(&id).map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid user ID".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Prevent admin from disabling themselves
|
|
if user_id.to_string() == admin.0.user_id && !request.enabled {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Cannot disable your own account".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Validate role
|
|
let valid_roles = ["admin", "operator", "viewer"];
|
|
if !valid_roles.contains(&request.role.as_str()) {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: format!("Invalid role. Must be one of: {:?}", valid_roles),
|
|
}),
|
|
));
|
|
}
|
|
|
|
// Update user
|
|
let user = db::update_user(
|
|
db.pool(),
|
|
user_id,
|
|
request.email.as_deref(),
|
|
&request.role,
|
|
request.enabled,
|
|
)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to update user".to_string(),
|
|
}),
|
|
)
|
|
})?
|
|
.ok_or_else(|| {
|
|
(
|
|
StatusCode::NOT_FOUND,
|
|
Json(ErrorResponse {
|
|
error: "User not found".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Update password if provided
|
|
if let Some(password) = request.password {
|
|
if password.len() < 8 {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Password must be at least 8 characters".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
let password_hash = hash_password(&password).map_err(|e| {
|
|
tracing::error!("Password hashing error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to hash password".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
db::update_user_password(db.pool(), user_id, &password_hash)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to update password".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
}
|
|
|
|
let permissions = db::get_user_permissions(db.pool(), user.id)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
tracing::info!("Updated user: {}", user.username);
|
|
|
|
Ok(Json(UserInfo {
|
|
id: user.id.to_string(),
|
|
username: user.username,
|
|
email: user.email,
|
|
role: user.role,
|
|
enabled: user.enabled,
|
|
created_at: user.created_at.to_rfc3339(),
|
|
last_login: user.last_login.map(|t| t.to_rfc3339()),
|
|
permissions,
|
|
}))
|
|
}
|
|
|
|
/// DELETE /api/users/:id - Delete user
|
|
pub async fn delete_user(
|
|
State(state): State<AppState>,
|
|
admin: AdminUser,
|
|
Path(id): Path<String>,
|
|
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user_id = Uuid::parse_str(&id).map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid user ID".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Prevent admin from deleting themselves
|
|
if user_id.to_string() == admin.0.user_id {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Cannot delete your own account".to_string(),
|
|
}),
|
|
));
|
|
}
|
|
|
|
let deleted = db::delete_user(db.pool(), user_id)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to delete user".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
if deleted {
|
|
tracing::info!("Deleted user: {}", id);
|
|
Ok(StatusCode::NO_CONTENT)
|
|
} else {
|
|
Err((
|
|
StatusCode::NOT_FOUND,
|
|
Json(ErrorResponse {
|
|
error: "User not found".to_string(),
|
|
}),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// PUT /api/users/:id/permissions - Set user permissions
|
|
pub async fn set_permissions(
|
|
State(state): State<AppState>,
|
|
_admin: AdminUser,
|
|
Path(id): Path<String>,
|
|
Json(request): Json<SetPermissionsRequest>,
|
|
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user_id = Uuid::parse_str(&id).map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid user ID".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Validate permissions
|
|
let valid_permissions = ["view", "control", "transfer", "manage_users", "manage_clients"];
|
|
for perm in &request.permissions {
|
|
if !valid_permissions.contains(&perm.as_str()) {
|
|
return Err((
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: format!("Invalid permission: {}. Valid: {:?}", perm, valid_permissions),
|
|
}),
|
|
));
|
|
}
|
|
}
|
|
|
|
db::set_user_permissions(db.pool(), user_id, &request.permissions)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to set permissions".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
tracing::info!("Updated permissions for user: {}", id);
|
|
Ok(StatusCode::OK)
|
|
}
|
|
|
|
/// PUT /api/users/:id/clients - Set user client access
|
|
pub async fn set_client_access(
|
|
State(state): State<AppState>,
|
|
_admin: AdminUser,
|
|
Path(id): Path<String>,
|
|
Json(request): Json<SetClientAccessRequest>,
|
|
) -> Result<StatusCode, (StatusCode, Json<ErrorResponse>)> {
|
|
let db = state.db.as_ref().ok_or_else(|| {
|
|
(
|
|
StatusCode::SERVICE_UNAVAILABLE,
|
|
Json(ErrorResponse {
|
|
error: "Database not available".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
let user_id = Uuid::parse_str(&id).map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid user ID".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
// Parse client IDs
|
|
let client_ids: Result<Vec<Uuid>, _> = request
|
|
.client_ids
|
|
.iter()
|
|
.map(|s| Uuid::parse_str(s))
|
|
.collect();
|
|
|
|
let client_ids = client_ids.map_err(|_| {
|
|
(
|
|
StatusCode::BAD_REQUEST,
|
|
Json(ErrorResponse {
|
|
error: "Invalid client ID format".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
db::set_user_client_access(db.pool(), user_id, &client_ids)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("Database error: {}", e);
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(ErrorResponse {
|
|
error: "Failed to set client access".to_string(),
|
|
}),
|
|
)
|
|
})?;
|
|
|
|
tracing::info!("Updated client access for user: {}", id);
|
|
Ok(StatusCode::OK)
|
|
}
|