diff --git a/markbase-core/src/provider/mod.rs b/markbase-core/src/provider/mod.rs index a1eaf01..5da680d 100644 --- a/markbase-core/src/provider/mod.rs +++ b/markbase-core/src/provider/mod.rs @@ -73,4 +73,19 @@ pub trait DataProvider: Send + Sync { let _ = username; Ok(Vec::new()) } + + /// 列出所有用户 + fn list_users(&self) -> Result, ProviderError>; + + /// 创建用户 + fn create_user(&self, user: &User, password: &str) -> Result<(), ProviderError>; + + /// 更新用户 + fn update_user(&self, user: &User, new_password: Option<&str>) -> Result<(), ProviderError>; + + /// 删除用户 + fn delete_user(&self, username: &str) -> Result<(), ProviderError>; + + /// 重置密码 + fn reset_password(&self, username: &str, new_password: &str) -> Result<(), ProviderError>; } diff --git a/markbase-core/src/provider/pg.rs b/markbase-core/src/provider/pg.rs index cd84b7b..85543b5 100644 --- a/markbase-core/src/provider/pg.rs +++ b/markbase-core/src/provider/pg.rs @@ -115,6 +115,102 @@ impl DataProvider for PgProvider { None => Ok(Vec::new()), } } + + fn list_users(&self) -> Result, ProviderError> { + let mut conn = self.open_conn()?; + + let rows = conn + .query( + "SELECT username, password, home_dir, permissions, uid, gid, status + FROM users ORDER BY username", + &[], + ) + .map_err(|e| ProviderError::Internal(format!("Query error: {}", e)))?; + + let users = rows + .iter() + .map(|row| User { + username: row.get(0), + password_hash: row.get::<_, Option>(1).unwrap_or_default(), + home_dir: PathBuf::from(row.get::<_, String>(2)), + permissions: row + .get::<_, Option>(3) + .unwrap_or_else(|| "*".to_string()), + uid: row.get::<_, i64>(4) as u32, + gid: row.get::<_, i64>(5) as u32, + status: row.get(6), + }) + .collect(); + + Ok(users) + } + + fn create_user(&self, user: &User, password: &str) -> Result<(), ProviderError> { + let mut conn = self.open_conn()?; + + let hash = bcrypt::hash(password, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "INSERT INTO users (username, password, home_dir, permissions, uid, gid, status) + VALUES ($1, $2, $3, $4, $5, $6, $7)", + &[&user.username, &hash, &user.home_dir.to_string_lossy(), &user.permissions, &(user.uid as i64), &(user.gid as i64), &user.status], + ) + .map_err(|e| ProviderError::Internal(format!("Insert error: {}", e)))?; + + Ok(()) + } + + fn update_user(&self, user: &User, new_password: Option<&str>) -> Result<(), ProviderError> { + let mut conn = self.open_conn()?; + + if let Some(pwd) = new_password { + let hash = bcrypt::hash(pwd, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "UPDATE users + SET password = $2, home_dir = $3, permissions = $4, uid = $5, gid = $6, status = $7 + WHERE username = $1", + &[&user.username, &hash, &user.home_dir.to_string_lossy(), &user.permissions, &(user.uid as i64), &(user.gid as i64), &user.status], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + } else { + conn.execute( + "UPDATE users + SET home_dir = $2, permissions = $3, uid = $4, gid = $5, status = $6 + WHERE username = $1", + &[&user.username, &user.home_dir.to_string_lossy(), &user.permissions, &(user.uid as i64), &(user.gid as i64), &user.status], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + } + + Ok(()) + } + + fn delete_user(&self, username: &str) -> Result<(), ProviderError> { + let mut conn = self.open_conn()?; + + conn.execute("DELETE FROM users WHERE username = $1", &[&username]) + .map_err(|e| ProviderError::Internal(format!("Delete error: {}", e)))?; + + Ok(()) + } + + fn reset_password(&self, username: &str, new_password: &str) -> Result<(), ProviderError> { + let mut conn = self.open_conn()?; + + let hash = bcrypt::hash(new_password, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "UPDATE users SET password = $2 WHERE username = $1", + &[&username, &hash], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + + Ok(()) + } } #[cfg(test)] diff --git a/markbase-core/src/provider/sqlite.rs b/markbase-core/src/provider/sqlite.rs index 0149d32..6144eaf 100644 --- a/markbase-core/src/provider/sqlite.rs +++ b/markbase-core/src/provider/sqlite.rs @@ -89,6 +89,123 @@ impl DataProvider for SqliteProvider { .collect(); Ok(groups) } + + fn list_users(&self) -> Result, ProviderError> { + let conn = self.open_conn()?; + + let users = conn + .prepare( + "SELECT username, password_hash, home_dir, permissions, uid, gid, status + FROM sftpgo_users ORDER BY username", + ) + .map_err(|e| ProviderError::Internal(format!("Query prepare error: {}", e)))? + .query_map([], |row| { + Ok(User { + username: row.get(0)?, + password_hash: row.get(1)?, + home_dir: PathBuf::from(row.get::<_, String>(2)?), + permissions: row.get(3)?, + uid: row.get::<_, i64>(4)? as u32, + gid: row.get::<_, i64>(5)? as u32, + status: row.get(6)?, + }) + }) + .map_err(|e| ProviderError::Internal(format!("Query map error: {}", e)))? + .filter_map(|r| r.ok()) + .collect(); + + Ok(users) + } + + fn create_user(&self, user: &User, password: &str) -> Result<(), ProviderError> { + let conn = self.open_conn()?; + + let hash = bcrypt::hash(password, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "INSERT INTO sftpgo_users (username, password_hash, home_dir, permissions, uid, gid, status) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)", + params![ + user.username, + hash, + user.home_dir.to_string_lossy(), + user.permissions, + user.uid as i64, + user.gid as i64, + user.status, + ], + ) + .map_err(|e| ProviderError::Internal(format!("Insert error: {}", e)))?; + + Ok(()) + } + + fn update_user(&self, user: &User, new_password: Option<&str>) -> Result<(), ProviderError> { + let conn = self.open_conn()?; + + if let Some(pwd) = new_password { + let hash = bcrypt::hash(pwd, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "UPDATE sftpgo_users + SET password_hash = ?2, home_dir = ?3, permissions = ?4, uid = ?5, gid = ?6, status = ?7 + WHERE username = ?1", + params![ + user.username, + hash, + user.home_dir.to_string_lossy(), + user.permissions, + user.uid as i64, + user.gid as i64, + user.status, + ], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + } else { + conn.execute( + "UPDATE sftpgo_users + SET home_dir = ?2, permissions = ?3, uid = ?4, gid = ?5, status = ?6 + WHERE username = ?1", + params![ + user.username, + user.home_dir.to_string_lossy(), + user.permissions, + user.uid as i64, + user.gid as i64, + user.status, + ], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + } + + Ok(()) + } + + fn delete_user(&self, username: &str) -> Result<(), ProviderError> { + let conn = self.open_conn()?; + + conn.execute("DELETE FROM sftpgo_users WHERE username = ?1", params![username]) + .map_err(|e| ProviderError::Internal(format!("Delete error: {}", e)))?; + + Ok(()) + } + + fn reset_password(&self, username: &str, new_password: &str) -> Result<(), ProviderError> { + let conn = self.open_conn()?; + + let hash = bcrypt::hash(new_password, bcrypt::DEFAULT_COST) + .map_err(|e| ProviderError::Internal(format!("bcrypt hash error: {}", e)))?; + + conn.execute( + "UPDATE sftpgo_users SET password_hash = ?2 WHERE username = ?1", + params![username, hash], + ) + .map_err(|e| ProviderError::Internal(format!("Update error: {}", e)))?; + + Ok(()) + } } #[cfg(test)] diff --git a/markbase-tauri/src-tauri/src/commands/mod.rs b/markbase-tauri/src-tauri/src/commands/mod.rs index eb9e06c..03752b3 100644 --- a/markbase-tauri/src-tauri/src/commands/mod.rs +++ b/markbase-tauri/src-tauri/src/commands/mod.rs @@ -6,6 +6,7 @@ pub mod management; pub mod health; pub mod monitor; pub mod backup; +pub mod user_management; pub use file_ops::*; pub use install::*; @@ -14,4 +15,5 @@ pub use diagnostic::*; pub use management::*; pub use health::*; pub use monitor::*; -pub use backup::*; \ No newline at end of file +pub use backup::*; +pub use user_management::*; \ No newline at end of file diff --git a/markbase-tauri/src-tauri/src/commands/user_management.rs b/markbase-tauri/src-tauri/src/commands/user_management.rs new file mode 100644 index 0000000..a68a52e --- /dev/null +++ b/markbase-tauri/src-tauri/src/commands/user_management.rs @@ -0,0 +1,100 @@ +use markbase_core::provider::{DataProvider, User, ProviderError, sqlite::SqliteProvider}; +use std::path::PathBuf; +use std::sync::{Arc, LazyLock, Mutex}; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserInfo { + pub username: String, + pub home_dir: String, + pub status: String, +} + +lazy_static::lazy_static! { + static ref DATA_PROVIDER: LazyLock>>> = + LazyLock::new(|| { + Arc::new(Mutex::new(Box::new( + SqliteProvider::new(&PathBuf::from("data/auth.sqlite").to_string_lossy().to_string()) + .expect("Failed to create SqliteProvider") + ) as Box)) + }); +} + +#[tauri::command] +pub async fn list_auth_users() -> Result, String> { + let provider = DATA_PROVIDER.lock().unwrap(); + + let users = provider.list_users().map_err(|e| e.to_string())?; + + Ok(users.into_iter().map(|u| UserInfo { + username: u.username, + home_dir: u.home_dir.to_string_lossy().to_string(), + status: if u.status == 1 { "active".to_string() } else { "disabled".to_string() }, + }).collect()) +} + +#[tauri::command] +pub async fn create_auth_user( + username: String, + password: String, + home_dir: String, + status: String, +) -> Result<(), String> { + let provider = DATA_PROVIDER.lock().unwrap(); + + let user = User { + username: username.clone(), + password_hash: String::new(), + home_dir: PathBuf::from(home_dir), + uid: 1000, + gid: 1000, + permissions: "*".to_string(), + status: if status == "active" { 1 } else { 0 }, + }; + + provider.create_user(&user, &password).map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn update_auth_user( + username: String, + password: Option, + home_dir: String, + status: String, +) -> Result<(), String> { + let provider = DATA_PROVIDER.lock().unwrap(); + + let user = User { + username: username.clone(), + password_hash: String::new(), + home_dir: PathBuf::from(home_dir), + uid: 1000, + gid: 1000, + permissions: "*".to_string(), + status: if status == "active" { 1 } else { 0 }, + }; + + provider.update_user(&user, password.as_deref()).map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn delete_auth_user(username: String) -> Result<(), String> { + let provider = DATA_PROVIDER.lock().unwrap(); + + provider.delete_user(&username).map_err(|e| e.to_string())?; + + Ok(()) +} + +#[tauri::command] +pub async fn reset_auth_password(username: String, new_password: String) -> Result<(), String> { + let provider = DATA_PROVIDER.lock().unwrap(); + + provider.reset_password(&username, &new_password).map_err(|e| e.to_string())?; + + Ok(()) +} \ No newline at end of file diff --git a/markbase-tauri/src-tauri/src/main.rs b/markbase-tauri/src-tauri/src/main.rs index 7895f9b..03ca485 100644 --- a/markbase-tauri/src-tauri/src/main.rs +++ b/markbase-tauri/src-tauri/src/main.rs @@ -42,6 +42,11 @@ fn main() { get_backup_config, set_backup_config, run_backup, + list_auth_users, + create_auth_user, + update_auth_user, + delete_auth_user, + reset_auth_password, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/markbase-tauri/src/src/router/index.js b/markbase-tauri/src/src/router/index.js index b130a2e..c495e54 100644 --- a/markbase-tauri/src/src/router/index.js +++ b/markbase-tauri/src/src/router/index.js @@ -7,6 +7,7 @@ import Management from '../views/Management.vue' import Health from '../views/Health.vue' import Monitor from '../views/Monitor.vue' import Backup from '../views/Backup.vue' +import Users from '../views/Users.vue' const routes = [ { @@ -48,6 +49,11 @@ const routes = [ path: '/backup', name: 'Backup', component: Backup + }, + { + path: '/users', + name: 'Users', + component: Users } ] diff --git a/markbase-tauri/src/src/views/Home.vue b/markbase-tauri/src/src/views/Home.vue index 32a97e4..6a2b054 100644 --- a/markbase-tauri/src/src/views/Home.vue +++ b/markbase-tauri/src/src/views/Home.vue @@ -4,7 +4,7 @@ import { useRouter } from 'vue-router' import { useAppStore } from '../stores/app' import { invoke } from '@tauri-apps/api/tauri' import { ElMessage } from 'element-plus' -import { Folder, Document, Upload, Clock } from '@element-plus/icons-vue' +import { Folder, Document, Upload, Clock, UserFilled } from '@element-plus/icons-vue' import { open } from '@tauri-apps/api/dialog' const router = useRouter() @@ -225,6 +225,14 @@ onMounted(async () => {

Snapshots and scheduler

+ + +
+ +

User Management

+

Users and permissions

+
+
diff --git a/markbase-tauri/src/src/views/Users.vue b/markbase-tauri/src/src/views/Users.vue new file mode 100644 index 0000000..d2e754b --- /dev/null +++ b/markbase-tauri/src/src/views/Users.vue @@ -0,0 +1,264 @@ + + + + + \ No newline at end of file