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;
|
let _ = username;
|
||||||
Ok(Vec::new())
|
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()),
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -89,6 +89,123 @@ impl DataProvider for SqliteProvider {
|
|||||||
.collect();
|
.collect();
|
||||||
Ok(groups)
|
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)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub mod management;
|
|||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod backup;
|
pub mod backup;
|
||||||
|
pub mod user_management;
|
||||||
|
|
||||||
pub use file_ops::*;
|
pub use file_ops::*;
|
||||||
pub use install::*;
|
pub use install::*;
|
||||||
@@ -14,4 +15,5 @@ pub use diagnostic::*;
|
|||||||
pub use management::*;
|
pub use management::*;
|
||||||
pub use health::*;
|
pub use health::*;
|
||||||
pub use monitor::*;
|
pub use monitor::*;
|
||||||
pub use backup::*;
|
pub use backup::*;
|
||||||
|
pub use user_management::*;
|
||||||
100
markbase-tauri/src-tauri/src/commands/user_management.rs
Normal file
100
markbase-tauri/src-tauri/src/commands/user_management.rs
Normal file
@@ -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<Arc<Mutex<Box<dyn DataProvider>>>> =
|
||||||
|
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<dyn DataProvider>))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn list_auth_users() -> Result<Vec<UserInfo>, 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<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.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(())
|
||||||
|
}
|
||||||
@@ -42,6 +42,11 @@ fn main() {
|
|||||||
get_backup_config,
|
get_backup_config,
|
||||||
set_backup_config,
|
set_backup_config,
|
||||||
run_backup,
|
run_backup,
|
||||||
|
list_auth_users,
|
||||||
|
create_auth_user,
|
||||||
|
update_auth_user,
|
||||||
|
delete_auth_user,
|
||||||
|
reset_auth_password,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Management from '../views/Management.vue'
|
|||||||
import Health from '../views/Health.vue'
|
import Health from '../views/Health.vue'
|
||||||
import Monitor from '../views/Monitor.vue'
|
import Monitor from '../views/Monitor.vue'
|
||||||
import Backup from '../views/Backup.vue'
|
import Backup from '../views/Backup.vue'
|
||||||
|
import Users from '../views/Users.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -48,6 +49,11 @@ const routes = [
|
|||||||
path: '/backup',
|
path: '/backup',
|
||||||
name: 'Backup',
|
name: 'Backup',
|
||||||
component: Backup
|
component: Backup
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
name: 'Users',
|
||||||
|
component: Users
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useRouter } from 'vue-router'
|
|||||||
import { useAppStore } from '../stores/app'
|
import { useAppStore } from '../stores/app'
|
||||||
import { invoke } from '@tauri-apps/api/tauri'
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
import { ElMessage } from 'element-plus'
|
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'
|
import { open } from '@tauri-apps/api/dialog'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -225,6 +225,14 @@ onMounted(async () => {
|
|||||||
<p>Snapshots and scheduler</p>
|
<p>Snapshots and scheduler</p>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="management-card" @click="navigateTo('/users')">
|
||||||
|
<div class="card-content">
|
||||||
|
<el-icon :size="40"><UserFilled /></el-icon>
|
||||||
|
<h3>User Management</h3>
|
||||||
|
<p>Users and permissions</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
264
markbase-tauri/src/src/views/Users.vue
Normal file
264
markbase-tauri/src/src/views/Users.vue
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
UserFilled,
|
||||||
|
Plus,
|
||||||
|
Edit,
|
||||||
|
Delete,
|
||||||
|
Key,
|
||||||
|
FolderOpened,
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
const users = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const showEditDialog = ref(false)
|
||||||
|
const currentUser = ref({
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
home_dir: '',
|
||||||
|
status: 'active',
|
||||||
|
})
|
||||||
|
const editingUser = ref(null)
|
||||||
|
|
||||||
|
const loadUsers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const list = await invoke('list_auth_users')
|
||||||
|
users.value = list
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`Failed to load users: ${error}`)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createUser = async () => {
|
||||||
|
if (!currentUser.value.username) {
|
||||||
|
ElMessage.warning('Please enter username')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!currentUser.value.password) {
|
||||||
|
ElMessage.warning('Please enter password')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await invoke('create_auth_user', {
|
||||||
|
username: currentUser.value.username,
|
||||||
|
password: currentUser.value.password,
|
||||||
|
homeDir: currentUser.value.home_dir || `/data/${currentUser.value.username}`,
|
||||||
|
status: currentUser.value.status,
|
||||||
|
})
|
||||||
|
ElMessage.success(`User '${currentUser.value.username}' created`)
|
||||||
|
showCreateDialog.value = false
|
||||||
|
currentUser.value = { username: '', password: '', home_dir: '', status: 'active' }
|
||||||
|
await loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`Failed to create user: ${error}`)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editUser = (user) => {
|
||||||
|
editingUser.value = { ...user, password: '' }
|
||||||
|
showEditDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateUser = async () => {
|
||||||
|
if (!editingUser.value.username) {
|
||||||
|
ElMessage.warning('Please enter username')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await invoke('update_auth_user', {
|
||||||
|
username: editingUser.value.username,
|
||||||
|
password: editingUser.value.password || null,
|
||||||
|
homeDir: editingUser.value.home_dir,
|
||||||
|
status: editingUser.value.status,
|
||||||
|
})
|
||||||
|
ElMessage.success(`User '${editingUser.value.username}' updated`)
|
||||||
|
showEditDialog.value = false
|
||||||
|
editingUser.value = null
|
||||||
|
await loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error(`Failed to update user: ${error}`)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteUser = async (username) => {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
`Are you sure you want to delete user '${username}'?`,
|
||||||
|
'Delete User',
|
||||||
|
{ type: 'warning' }
|
||||||
|
)
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
await invoke('delete_auth_user', { username })
|
||||||
|
ElMessage.success(`User '${username}' deleted`)
|
||||||
|
await loadUsers()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(`Failed to delete user: ${error}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPassword = async (username) => {
|
||||||
|
try {
|
||||||
|
const { value: newPassword } = await ElMessageBox.prompt(
|
||||||
|
`Enter new password for '${username}'`,
|
||||||
|
'Reset Password',
|
||||||
|
{
|
||||||
|
inputType: 'password',
|
||||||
|
inputPlaceholder: 'New password',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newPassword) {
|
||||||
|
loading.value = true
|
||||||
|
await invoke('reset_auth_password', { username, newPassword })
|
||||||
|
ElMessage.success(`Password reset for '${username}'`)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
ElMessage.error(`Failed to reset password: ${error}`)
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="users-container">
|
||||||
|
<el-card>
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span><el-icon><UserFilled /></el-icon> User Management</span>
|
||||||
|
<el-button type="primary" :icon="Plus" @click="showCreateDialog = true">
|
||||||
|
Create User
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table :data="users" v-loading="loading" style="width: 100%">
|
||||||
|
<el-table-column prop="username" label="Username" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span style="display: flex; align-items: center; gap: 8px;">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
{{ row.username }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="home_dir" label="Home Directory" min-width="200" />
|
||||||
|
<el-table-column prop="status" label="Status" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag :type="row.status === 'active' ? 'success' : 'danger'" size="small">
|
||||||
|
{{ row.status }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="Actions" width="200" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button-group>
|
||||||
|
<el-button size="small" :icon="Edit" @click="editUser(row)">
|
||||||
|
Edit
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" :icon="Key" @click="resetPassword(row.username)">
|
||||||
|
Reset PW
|
||||||
|
</el-button>
|
||||||
|
<el-button size="small" type="danger" :icon="Delete" @click="deleteUser(row.username)">
|
||||||
|
Delete
|
||||||
|
</el-button>
|
||||||
|
</el-button-group>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- Create User Dialog -->
|
||||||
|
<el-dialog v-model="showCreateDialog" title="Create User" width="500px">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="Username">
|
||||||
|
<el-input v-model="currentUser.username" placeholder="Enter username" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Password">
|
||||||
|
<el-input v-model="currentUser.password" type="password" placeholder="Enter password" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Home Directory">
|
||||||
|
<el-input v-model="currentUser.home_dir" placeholder="/data/{username}" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Status">
|
||||||
|
<el-select v-model="currentUser.status" style="width: 100%;">
|
||||||
|
<el-option label="Active" value="active" />
|
||||||
|
<el-option label="Disabled" value="disabled" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showCreateDialog = false">Cancel</el-button>
|
||||||
|
<el-button type="primary" @click="createUser" :loading="loading">Create</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- Edit User Dialog -->
|
||||||
|
<el-dialog v-model="showEditDialog" title="Edit User" width="500px">
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="Username">
|
||||||
|
<el-input v-model="editingUser.username" disabled />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="New Password">
|
||||||
|
<el-input v-model="editingUser.password" type="password" placeholder="Leave empty to keep current" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Home Directory">
|
||||||
|
<el-input v-model="editingUser.home_dir" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Status">
|
||||||
|
<el-select v-model="editingUser.status" style="width: 100%;">
|
||||||
|
<el-option label="Active" value="active" />
|
||||||
|
<el-option label="Disabled" value="disabled" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showEditDialog = false">Cancel</el-button>
|
||||||
|
<el-button type="primary" @click="updateUser" :loading="loading">Update</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.users-container {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user