Implement User Management UI (Phase 11 P0 #1)
Some checks failed
Test / build (push) Has been cancelled
Test / test (push) Has been cancelled

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

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

View File

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

View 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>