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:
Warren
2026-06-24 05:10:27 +08:00
parent 72503f7db9
commit e07d17aee7
9 changed files with 615 additions and 2 deletions

View File

@@ -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>;
}

View File

@@ -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)]

View File

@@ -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)]