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)]
|
||||
|
||||
@@ -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::*;
|
||||
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,
|
||||
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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
<p>Snapshots and scheduler</p>
|
||||
</div>
|
||||
</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>
|
||||
</el-col>
|
||||
</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