Implement User Management UI (Phase 11 P0 #1)
User Management Features:
- Users.vue: Complete user CRUD interface
- Tauri commands: 5 auth user endpoints
- REST API: DataProvider trait extensions
UI Components:
- User list table (username, home_dir, status)
- Create user dialog (username, password, home_dir, status)
- Edit user dialog (password optional, home_dir, status)
- Delete user confirmation
- Reset password prompt
Tauri Commands (renamed to avoid conflict):
- list_auth_users: List all users from auth database
- create_auth_user: Create user with bcrypt password
- update_auth_user: Update user (optional password)
- delete_auth_user: Delete user
- reset_auth_password: Reset password
DataProvider Trait Extensions:
- list_users(): List all users
- create_user(): Create user with password
- update_user(): Update user (optional password)
- delete_user(): Delete user
- reset_password(): Reset password
Implementations:
- SqliteProvider: Full implementation (sftpgo_users table)
- PgProvider: Full implementation (users table)
Router:
- Added /users route
Home.vue:
- Added User Management card
Build: ✅ Tauri + markbase-core
Tests: 495 markbase-core + 201 smb-server
This commit is contained in:
@@ -73,4 +73,19 @@ pub trait DataProvider: Send + Sync {
|
||||
let _ = username;
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// 列出所有用户
|
||||
fn list_users(&self) -> Result<Vec<User>, 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>;
|
||||
}
|
||||
|
||||
@@ -115,6 +115,102 @@ impl DataProvider for PgProvider {
|
||||
None => Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_users(&self) -> Result<Vec<User>, 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<String>>(1).unwrap_or_default(),
|
||||
home_dir: PathBuf::from(row.get::<_, String>(2)),
|
||||
permissions: row
|
||||
.get::<_, Option<String>>(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)]
|
||||
|
||||
@@ -89,6 +89,123 @@ impl DataProvider for SqliteProvider {
|
||||
.collect();
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
fn list_users(&self) -> Result<Vec<User>, 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)]
|
||||
|
||||
Reference in New Issue
Block a user