//! User database operations use anyhow::Result; use chrono::{DateTime, Utc}; use sqlx::PgPool; use uuid::Uuid; /// User record from database #[derive(Debug, Clone, sqlx::FromRow)] pub struct User { pub id: Uuid, pub username: String, pub password_hash: String, pub email: Option, pub role: String, pub enabled: bool, pub created_at: DateTime, pub updated_at: DateTime, pub last_login: Option>, } /// User without password hash (for API responses) #[derive(Debug, Clone, serde::Serialize)] pub struct UserInfo { pub id: Uuid, pub username: String, pub email: Option, pub role: String, pub enabled: bool, pub created_at: DateTime, pub last_login: Option>, pub permissions: Vec, } impl From for UserInfo { fn from(u: User) -> Self { Self { id: u.id, username: u.username, email: u.email, role: u.role, enabled: u.enabled, created_at: u.created_at, last_login: u.last_login, permissions: Vec::new(), // Filled in by caller } } } /// Get user by username pub async fn get_user_by_username(pool: &PgPool, username: &str) -> Result> { let user = sqlx::query_as::<_, User>( "SELECT * FROM users WHERE username = $1" ) .bind(username) .fetch_optional(pool) .await?; Ok(user) } /// Get user by ID pub async fn get_user_by_id(pool: &PgPool, id: Uuid) -> Result> { let user = sqlx::query_as::<_, User>( "SELECT * FROM users WHERE id = $1" ) .bind(id) .fetch_optional(pool) .await?; Ok(user) } /// Get all users pub async fn get_all_users(pool: &PgPool) -> Result> { let users = sqlx::query_as::<_, User>( "SELECT * FROM users ORDER BY username" ) .fetch_all(pool) .await?; Ok(users) } /// Create a new user pub async fn create_user( pool: &PgPool, username: &str, password_hash: &str, email: Option<&str>, role: &str, ) -> Result { let user = sqlx::query_as::<_, User>( r#" INSERT INTO users (username, password_hash, email, role) VALUES ($1, $2, $3, $4) RETURNING * "# ) .bind(username) .bind(password_hash) .bind(email) .bind(role) .fetch_one(pool) .await?; Ok(user) } /// Update user pub async fn update_user( pool: &PgPool, id: Uuid, email: Option<&str>, role: &str, enabled: bool, ) -> Result> { let user = sqlx::query_as::<_, User>( r#" UPDATE users SET email = $2, role = $3, enabled = $4, updated_at = NOW() WHERE id = $1 RETURNING * "# ) .bind(id) .bind(email) .bind(role) .bind(enabled) .fetch_optional(pool) .await?; Ok(user) } /// Update user password pub async fn update_user_password( pool: &PgPool, id: Uuid, password_hash: &str, ) -> Result { let result = sqlx::query( "UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1" ) .bind(id) .bind(password_hash) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// Update last login timestamp pub async fn update_last_login(pool: &PgPool, id: Uuid) -> Result<()> { sqlx::query("UPDATE users SET last_login = NOW() WHERE id = $1") .bind(id) .execute(pool) .await?; Ok(()) } /// Delete user pub async fn delete_user(pool: &PgPool, id: Uuid) -> Result { let result = sqlx::query("DELETE FROM users WHERE id = $1") .bind(id) .execute(pool) .await?; Ok(result.rows_affected() > 0) } /// Count users (for initial admin check) pub async fn count_users(pool: &PgPool) -> Result { let count: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM users") .fetch_one(pool) .await?; Ok(count.0) } /// Get user permissions pub async fn get_user_permissions(pool: &PgPool, user_id: Uuid) -> Result> { let perms: Vec<(String,)> = sqlx::query_as( "SELECT permission FROM user_permissions WHERE user_id = $1" ) .bind(user_id) .fetch_all(pool) .await?; Ok(perms.into_iter().map(|p| p.0).collect()) } /// Set user permissions (replaces all) pub async fn set_user_permissions( pool: &PgPool, user_id: Uuid, permissions: &[String], ) -> Result<()> { // Delete existing sqlx::query("DELETE FROM user_permissions WHERE user_id = $1") .bind(user_id) .execute(pool) .await?; // Insert new for perm in permissions { sqlx::query( "INSERT INTO user_permissions (user_id, permission) VALUES ($1, $2)" ) .bind(user_id) .bind(perm) .execute(pool) .await?; } Ok(()) } /// Get user's accessible client IDs (empty = all access) pub async fn get_user_client_access(pool: &PgPool, user_id: Uuid) -> Result> { let clients: Vec<(Uuid,)> = sqlx::query_as( "SELECT client_id FROM user_client_access WHERE user_id = $1" ) .bind(user_id) .fetch_all(pool) .await?; Ok(clients.into_iter().map(|c| c.0).collect()) } /// Set user's client access (replaces all) pub async fn set_user_client_access( pool: &PgPool, user_id: Uuid, client_ids: &[Uuid], ) -> Result<()> { // Delete existing sqlx::query("DELETE FROM user_client_access WHERE user_id = $1") .bind(user_id) .execute(pool) .await?; // Insert new for client_id in client_ids { sqlx::query( "INSERT INTO user_client_access (user_id, client_id) VALUES ($1, $2)" ) .bind(user_id) .bind(client_id) .execute(pool) .await?; } Ok(()) } /// Check if user has access to a specific client pub async fn user_has_client_access( pool: &PgPool, user_id: Uuid, client_id: Uuid, ) -> Result { // Admins have access to all let user = get_user_by_id(pool, user_id).await?; if let Some(u) = user { if u.role == "admin" { return Ok(true); } } // Check explicit access let access: Option<(Uuid,)> = sqlx::query_as( "SELECT client_id FROM user_client_access WHERE user_id = $1 AND client_id = $2" ) .bind(user_id) .bind(client_id) .fetch_optional(pool) .await?; // If no explicit access entries exist, user has access to all (legacy behavior) if access.is_some() { return Ok(true); } // Check if user has ANY access restrictions let count: (i64,) = sqlx::query_as( "SELECT COUNT(*) FROM user_client_access WHERE user_id = $1" ) .bind(user_id) .fetch_one(pool) .await?; // No restrictions means access to all Ok(count.0 == 0) }