feat: 多用户管理系统,管理员可分配用户
- 新增 users 数据库表,存储用户名、密码哈希、角色 - 启动时自动从 .env 创建初始管理员账号 - 登录改为数据库校验,不再硬编码比对 - 新增 /api/users 管理接口(仅管理员) - ConfigView 新增用户管理页(创建、改密码、切换角色、删除)
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
<div class="sidebar">
|
||||
<h3>配置管理</h3>
|
||||
<ul class="sidebar-menu">
|
||||
<li :class="{ active: tab === 'users' }" @click="tab = 'users'">用户管理</li>
|
||||
<li :class="{ active: tab === 'servers' }" @click="tab = 'servers'">服务器环境</li>
|
||||
<li :class="{ active: tab === 'apps' }" @click="tab = 'apps'">Apps 配置</li>
|
||||
<li :class="{ active: tab === 'schemes' }" @click="tab = 'schemes'">Schemes 配置</li>
|
||||
@@ -19,6 +20,42 @@
|
||||
</ul>
|
||||
</div>
|
||||
<div class="main-content">
|
||||
<!-- 用户管理 -->
|
||||
<div v-if="tab === 'users'">
|
||||
<h2>用户管理</h2>
|
||||
<p style="color: #666; margin-bottom: 16px; font-size: 14px;">管理员可以创建和管理普通用户账号</p>
|
||||
<button class="btn btn-primary" style="width: auto; margin-bottom: 16px;" @click="openUserModal()">+ 新增用户</button>
|
||||
<table class="config-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>角色</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users" :key="u.id">
|
||||
<td><strong>{{ u.username }}</strong></td>
|
||||
<td>
|
||||
<span :class="['build-type-badge', u.is_admin ? 'badge-appstore' : 'badge-adhoc']">
|
||||
{{ u.is_admin ? '管理员' : '普通用户' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ u.created_at || '-' }}</td>
|
||||
<td class="action-btns">
|
||||
<button class="action-btn" @click="openPasswordModal(u)">改密码</button>
|
||||
<button class="action-btn" @click="toggleUserRole(u)">{{ u.is_admin ? '取消管理员' : '设为管理员' }}</button>
|
||||
<button class="action-btn delete" @click="deleteUser(u)">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="!users.length">
|
||||
<td colspan="4" style="text-align: center; color: #999;">暂无用户</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 服务器环境配置 -->
|
||||
<div v-if="tab === 'servers'">
|
||||
<h2>服务器环境配置</h2>
|
||||
@@ -275,6 +312,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增用户弹窗 -->
|
||||
<div v-if="showUserModal" class="modal-overlay" @click.self="showUserModal = false">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>新增用户</h3>
|
||||
<button class="modal-close" @click="showUserModal = false">×</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>用户名 *</label>
|
||||
<input v-model="userForm.username" type="text" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码 *</label>
|
||||
<input v-model="userForm.password" type="password" placeholder="至少 6 位">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" v-model="userForm.is_admin" style="width: 16px; height: 16px;">
|
||||
管理员权限
|
||||
</label>
|
||||
</div>
|
||||
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
||||
<button class="action-btn" style="padding: 8px 16px;" @click="showUserModal = false">取消</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 8px 24px;" @click="createUser">创建</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码弹窗 -->
|
||||
<div v-if="showPasswordModal" class="modal-overlay" @click.self="showPasswordModal = false">
|
||||
<div class="modal-content" style="width: 400px;">
|
||||
<div class="modal-header">
|
||||
<h3>修改密码 - {{ passwordTarget?.username }}</h3>
|
||||
<button class="modal-close" @click="showPasswordModal = false">×</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<input v-model="newPassword" type="password" placeholder="至少 6 位">
|
||||
</div>
|
||||
<div style="margin-top: 20px; display: flex; gap: 12px; justify-content: flex-end;">
|
||||
<button class="action-btn" style="padding: 8px 16px;" @click="showPasswordModal = false">取消</button>
|
||||
<button class="btn btn-primary" style="width: auto; padding: 8px 24px;" @click="changePassword">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 服务器环境编辑弹窗 -->
|
||||
<div v-if="showServerModal" class="modal-overlay" @click.self="showServerModal = false">
|
||||
<div class="modal-content">
|
||||
@@ -449,7 +532,15 @@ const showLogin = inject('showLogin')
|
||||
const getToken = inject('getToken')
|
||||
const isLoggedIn = inject('isLoggedIn')
|
||||
const isAdmin = inject('isAdmin')
|
||||
const tab = ref('servers')
|
||||
const tab = ref('users')
|
||||
|
||||
// 用户管理
|
||||
const users = ref([])
|
||||
const showUserModal = ref(false)
|
||||
const userForm = ref({ username: '', password: '', is_admin: false })
|
||||
const showPasswordModal = ref(false)
|
||||
const passwordTarget = ref(null)
|
||||
const newPassword = ref('')
|
||||
|
||||
// 带认证的 fetch 封装
|
||||
const authFetch = (url, options = {}) => {
|
||||
@@ -486,7 +577,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
const loadData = async () => {
|
||||
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes] = await Promise.all([
|
||||
const [appsRes, schemesRes, serversRes, branchesRes, buildRes, uploadRes, configRes, versionsRes, usersRes] = await Promise.all([
|
||||
authFetch('/api/config/apps'),
|
||||
authFetch('/api/config/schemes'),
|
||||
authFetch('/api/config/servers'),
|
||||
@@ -495,6 +586,7 @@ const loadData = async () => {
|
||||
authFetch('/api/config/upload'),
|
||||
authFetch('/api/config'),
|
||||
authFetch('/api/config/versions'),
|
||||
authFetch('/api/users'),
|
||||
])
|
||||
apps.value = await appsRes.json()
|
||||
schemes.value = await schemesRes.json()
|
||||
@@ -504,6 +596,88 @@ const loadData = async () => {
|
||||
uploadConfig.value = await uploadRes.json()
|
||||
jsonContent.value = JSON.stringify(await configRes.json(), null, 2)
|
||||
versions.value = await versionsRes.json()
|
||||
if (usersRes.ok) users.value = await usersRes.json()
|
||||
}
|
||||
|
||||
// 用户管理
|
||||
const openUserModal = () => {
|
||||
userForm.value = { username: '', password: '', is_admin: false }
|
||||
showUserModal.value = true
|
||||
}
|
||||
|
||||
const createUser = async () => {
|
||||
if (!userForm.value.username || !userForm.value.password) {
|
||||
alert('请填写用户名和密码')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await authFetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(userForm.value),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '创建失败')
|
||||
}
|
||||
showUserModal.value = false
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
alert('创建失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const openPasswordModal = (u) => {
|
||||
passwordTarget.value = u
|
||||
newPassword.value = ''
|
||||
showPasswordModal.value = true
|
||||
}
|
||||
|
||||
const changePassword = async () => {
|
||||
if (!newPassword.value || newPassword.value.length < 6) {
|
||||
alert('密码至少 6 位')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${passwordTarget.value.id}/password`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ password: newPassword.value }),
|
||||
})
|
||||
if (!res.ok) throw new Error('修改失败')
|
||||
showPasswordModal.value = false
|
||||
alert('密码已更新')
|
||||
} catch (e) {
|
||||
alert('修改失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleUserRole = async (u) => {
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${u.id}/role`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ is_admin: !u.is_admin }),
|
||||
})
|
||||
if (!res.ok) throw new Error('操作失败')
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
alert('操作失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (u) => {
|
||||
if (!confirm(`确定删除用户「${u.username}」?`)) return
|
||||
try {
|
||||
const res = await authFetch(`/api/users/${u.id}`, { method: 'DELETE' })
|
||||
if (!res.ok) {
|
||||
const err = await res.json()
|
||||
throw new Error(err.detail || '删除失败')
|
||||
}
|
||||
await loadData()
|
||||
} catch (e) {
|
||||
alert('删除失败: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器环境管理
|
||||
|
||||
Reference in New Issue
Block a user