feat(服务器用户管理): 完成了服务器用户管理的fastapi封装的修改和更新
This commit is contained in:
parent
5ec6df4af8
commit
f9cd176743
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -43,6 +43,8 @@ declare module 'vue' {
|
||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||
ElTable: typeof import('element-plus/es')['ElTable']
|
||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
|
||||
@ -19,6 +19,30 @@ export const serversApi = {
|
||||
syncUserPermissions(id: number, users: Array<Record<string, unknown>>, partial = false) {
|
||||
return request.put(`/servers/${id}/user-permissions`, { users, partial })
|
||||
},
|
||||
systemUsersMeta(id: number) {
|
||||
return request.get(`/servers/${id}/system-users/meta`)
|
||||
},
|
||||
createSystemUser(id: number, data: Record<string, unknown>) {
|
||||
return request.post(`/servers/${id}/system-users`, data)
|
||||
},
|
||||
removeSystemUser(id: number, username: string) {
|
||||
return request.delete(`/servers/${id}/system-users/${encodeURIComponent(username)}`)
|
||||
},
|
||||
updateSystemUserPassword(id: number, username: string, data: Record<string, unknown>) {
|
||||
return request.patch(`/servers/${id}/system-users/${encodeURIComponent(username)}/password`, data)
|
||||
},
|
||||
updateBoundSystemUserPassword(id: number, data: Record<string, unknown>) {
|
||||
return request.patch(`/servers/${id}/bound-system-user/password`, data)
|
||||
},
|
||||
createSystemGroup(id: number, data: Record<string, unknown>) {
|
||||
return request.post(`/servers/${id}/system-groups`, data)
|
||||
},
|
||||
removeSystemGroup(id: number, groupname: string) {
|
||||
return request.delete(`/servers/${id}/system-groups/${encodeURIComponent(groupname)}`)
|
||||
},
|
||||
syncSystemUserGroups(id: number, username: string, groups: string[], mode = 'replace') {
|
||||
return request.put(`/servers/${id}/system-users/${encodeURIComponent(username)}/groups`, { groups, mode })
|
||||
},
|
||||
useResource(id: number, data: Record<string, unknown>) {
|
||||
return request.post(`/servers/${id}/use`, data)
|
||||
},
|
||||
|
||||
@ -22,6 +22,9 @@ export const usersApi = {
|
||||
syncBatchAssignments(payload: { user_ids: number[]; role_ids: number[]; permission_ids: number[] }) {
|
||||
return request.put('/users/batch-assignments', payload)
|
||||
},
|
||||
syncServerBindings(id: number, serverBindings: Array<Record<string, unknown>>) {
|
||||
return request.put(`/users/${id}/server-bindings`, { server_bindings: serverBindings })
|
||||
},
|
||||
importUsers(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
@ -21,6 +21,8 @@
|
||||
<span class='server-name'>{{ server.display_name || server.name }}</span>
|
||||
<span class='server-code'>{{ server.name }}</span>
|
||||
<span class='server-ip'>{{ server.internal_ip }}</span>
|
||||
<el-tag v-if='server.server_username' size='small' type='success'>{{ server.server_username }}</el-tag>
|
||||
<el-button v-if='server.server_username' size='small' plain :loading='updatingBoundPassword' @click.stop='openBoundPasswordDialog(server)'>改密</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -28,6 +30,7 @@
|
||||
<el-button size='small' @click='openEdit(server)'>编辑服务器</el-button>
|
||||
<el-button size='small' type='primary' plain @click='openCreateResource(server.id)'>添加资源</el-button>
|
||||
<el-button size='small' type='success' plain @click='openServerPermissionDialog(server)'>分配用户权限</el-button>
|
||||
<el-button size='small' type='warning' plain @click='openSystemUsersDialog(server)'>管理用户及用户组</el-button>
|
||||
<el-button size='small' type='danger' @click='removeRow(server)'>删除服务器</el-button>
|
||||
</div>
|
||||
|
||||
@ -42,6 +45,9 @@
|
||||
<el-table-column label='协议' width='120' sortable :sort-method='sortByProtocol'>
|
||||
<template #default='{ row }'>{{ row.protocols?.[0] || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='绑定用户' min-width='120'>
|
||||
<template #default='{ row }'>{{ row.server_username || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='状态' width='90' prop='is_active' sortable>
|
||||
<template #default='{ row }'><el-tag :type='row.is_active ? "success" : "info"'>{{ row.is_active ? '启用' :
|
||||
'停用' }}</el-tag></template>
|
||||
@ -86,6 +92,12 @@
|
||||
<el-form-item label='内网IP' v-if='form.type === "server"'><el-input v-model='form.internal_ip' /></el-form-item>
|
||||
<el-form-item label='asset_id' v-if='form.type === "server"'><el-input-number v-model='form.asset_id' :min='1'
|
||||
class='w-full' /></el-form-item>
|
||||
<el-form-item label='用户API地址' v-if='form.type === "server"'>
|
||||
<el-input v-model='form.user_api_base_url' placeholder='http://10.0.0.28:8000' clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label='用户API密钥' v-if='form.type === "server"'>
|
||||
<el-input v-model='form.user_api_token' type='password' show-password placeholder='留空则不修改' clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label='account_id' v-if='form.type === "resource"'><el-input-number v-model='form.account_id'
|
||||
:min='1' class='w-full' /></el-form-item>
|
||||
<el-form-item label='协议' v-if='form.type === "resource"'>
|
||||
@ -106,6 +118,92 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='systemUsersDialogVisible' :title='`管理用户及用户组 - ${systemUsersServer?.display_name || systemUsersServer?.name || ""}`' width='96vw' top='3vh' class='system-users-dialog'>
|
||||
<div class='system-user-toolbar btn-gap-8'>
|
||||
<el-button type='primary' @click='openCreateSystemUser'>新增服务器用户</el-button>
|
||||
<el-button type='success' @click='openCreateSystemGroup'>新增用户组</el-button>
|
||||
<el-button :loading='systemUsersLoading' @click='loadSystemUsersMeta'>刷新</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model='systemUsersTab'>
|
||||
<el-tab-pane label='用户' name='users'>
|
||||
<el-table :data='systemUsers' v-loading='systemUsersLoading' border max-height='62vh'>
|
||||
<el-table-column prop='username' label='用户名' min-width='140' fixed='left' />
|
||||
<el-table-column prop='uid' label='UID' width='90' />
|
||||
<el-table-column prop='gid' label='GID' width='90' />
|
||||
<el-table-column prop='home_dir' label='Home' min-width='220' />
|
||||
<el-table-column prop='shell' label='Shell' min-width='130' />
|
||||
<el-table-column label='用户组' min-width='260'>
|
||||
<template #default='{ row }'>
|
||||
<el-select v-model='systemUserGroupSelections[row.username]' multiple collapse-tags clearable class='w-full'>
|
||||
<el-option v-for='group in systemGroups' :key='group.groupname' :label='group.groupname' :value='group.groupname' />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='绑定SSO用户' min-width='180'>
|
||||
<template #default='{ row }'>{{ bindingLabel(row.username) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='操作' width='260' fixed='right'>
|
||||
<template #default='{ row }'>
|
||||
<div class='btn-gap-8 btn-gap-8--nowrap'>
|
||||
<el-button size='small' type='primary' plain :loading='savingSystemUserGroups[row.username]' @click='saveSystemUserGroups(row)'>保存组</el-button>
|
||||
<el-button size='small' @click='openPasswordDialog(row)'>改密码</el-button>
|
||||
<el-button size='small' type='danger' @click='removeSystemUser(row)'>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label='用户组' name='groups'>
|
||||
<el-table :data='systemGroups' v-loading='systemUsersLoading' border max-height='62vh'>
|
||||
<el-table-column prop='groupname' label='用户组' min-width='180' />
|
||||
<el-table-column prop='gid' label='GID' width='100' />
|
||||
<el-table-column label='成员' min-width='360'>
|
||||
<template #default='{ row }'>{{ (row.members || []).join(', ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='操作' width='120'>
|
||||
<template #default='{ row }'>
|
||||
<el-button size='small' type='danger' @click='removeSystemGroup(row)'>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='systemUserFormVisible' :title='systemUserFormMode === "group" ? "新增用户组" : "新增服务器用户"' width='520px'>
|
||||
<el-form :model='systemUserForm' label-width='95px'>
|
||||
<template v-if='systemUserFormMode === "user"'>
|
||||
<el-form-item label='用户名'><el-input v-model='systemUserForm.username' /></el-form-item>
|
||||
<el-form-item label='密码'><el-input v-model='systemUserForm.password' type='password' show-password /></el-form-item>
|
||||
<el-form-item label='用户组'>
|
||||
<el-select v-model='systemUserForm.groups' multiple collapse-tags clearable class='w-full'>
|
||||
<el-option v-for='group in systemGroups' :key='group.groupname' :label='group.groupname' :value='group.groupname' />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label='Shell'><el-input v-model='systemUserForm.shell' /></el-form-item>
|
||||
<el-form-item label='Home'><el-input v-model='systemUserForm.home_dir' placeholder='可为空' clearable /></el-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-form-item label='用户组'><el-input v-model='systemUserForm.groupname' /></el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='systemUserFormVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='systemUsersSaving' @click='submitSystemUserForm'>提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='passwordDialogVisible' :title='`修改密码 - ${passwordTarget?.username || ""}`' width='460px'>
|
||||
<el-form :model='passwordForm' label-width='80px'>
|
||||
<el-form-item label='新密码'><el-input v-model='passwordForm.password' type='password' show-password /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='passwordDialogVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='systemUsersSaving' @click='submitPassword'>提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='permissionDialogVisible' :title='permissionDialogTitle' width='1100px'>
|
||||
<el-table :data='permissionMatrixRows' max-height='520' border>
|
||||
<el-table-column prop='nickname' label='用户名' min-width='150' fixed='left' />
|
||||
@ -149,6 +247,21 @@
|
||||
<el-button type='primary' :loading='usingResource' @click='submitUseResource'>连接并访问</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='boundPasswordDialogVisible' :title='`修改绑定账号密码 - ${boundPasswordTarget?.server_username || ""}`' width='460px'>
|
||||
<el-form :model='boundPasswordForm' label-width='90px'>
|
||||
<el-form-item label='服务器账号'>
|
||||
<div>{{ boundPasswordTarget?.server_username || '-' }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item label='新密码'>
|
||||
<el-input v-model='boundPasswordForm.password' type='password' show-password />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='boundPasswordDialogVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='updatingBoundPassword' @click='submitBoundPassword'>提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
|
||||
<el-card v-loading='opsLoading' style="margin-top: 1rem;">
|
||||
@ -271,11 +384,19 @@ const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const permissionDialogVisible = ref(false)
|
||||
const useDialogVisible = ref(false)
|
||||
const systemUsersDialogVisible = ref(false)
|
||||
const systemUserFormVisible = ref(false)
|
||||
const passwordDialogVisible = ref(false)
|
||||
const boundPasswordDialogVisible = ref(false)
|
||||
const protocolDialogVisible = ref(false)
|
||||
const softwareDialogVisible = ref(false)
|
||||
const savingPermissions = ref(false)
|
||||
const usingResource = ref(false)
|
||||
const copyingTempPassword = ref(false)
|
||||
const updatingBoundPassword = ref(false)
|
||||
const systemUsersLoading = ref(false)
|
||||
const systemUsersSaving = ref(false)
|
||||
const savingSystemUserGroups = reactive<Record<string, boolean>>({})
|
||||
const opsLoading = ref(false)
|
||||
const opsSaving = ref(false)
|
||||
const forceSaveOpsReady = ref(false)
|
||||
@ -290,17 +411,29 @@ const permissionMatrixRows = ref<any[]>([])
|
||||
const permissionResourceOptions = ref<any[]>([])
|
||||
const activeServers = ref<string[]>([])
|
||||
const useTargetRow = ref<any | null>(null)
|
||||
const systemUsersServer = ref<any | null>(null)
|
||||
const passwordTarget = ref<any | null>(null)
|
||||
const boundPasswordTarget = ref<any | null>(null)
|
||||
const opsProtocols = ref<any[]>([])
|
||||
const softwareProtocol = ref<any | null>(null)
|
||||
const opsLinkLoading = ref<Record<number, boolean>>({})
|
||||
const opsSavedSelections = reactive<Record<string, number | null>>({})
|
||||
|
||||
const typeOptions = [{ label: '服务器', value: 'server' }, { label: '资源', value: 'resource' }]
|
||||
const form = reactive<any>({ type: 'server', name: '', display_name: '', parent_id: null, internal_ip: '', asset_id: null, account_id: null, protocol: '', allow_copy_temp_password: false, description: '', is_active: true })
|
||||
const form = reactive<any>({ type: 'server', name: '', display_name: '', parent_id: null, internal_ip: '', user_api_base_url: '', user_api_token: '', asset_id: null, account_id: null, protocol: '', allow_copy_temp_password: false, description: '', is_active: true })
|
||||
const systemUserForm = reactive<any>({ username: '', password: '', groups: [], shell: '/bin/bash', home_dir: '', groupname: '' })
|
||||
const useForm = reactive<any>({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' })
|
||||
const passwordForm = reactive<any>({ password: '' })
|
||||
const boundPasswordForm = reactive<any>({ password: '' })
|
||||
const protocolForm = reactive<any>({ name: '', bastion_protocol_id: 2, description: '', sort: 0, is_active: true })
|
||||
const softwareForm = reactive<any>({ name: '', client_path: '', sort: 0, is_active: true })
|
||||
const opsSelections = reactive<Record<string, number | null>>({})
|
||||
const systemUserGroupSelections = reactive<Record<string, string[]>>({})
|
||||
const systemUsers = ref<any[]>([])
|
||||
const systemGroups = ref<any[]>([])
|
||||
const systemUserBindings = ref<any[]>([])
|
||||
const systemUsersTab = ref<'users' | 'groups'>('users')
|
||||
const systemUserFormMode = ref<'user' | 'group'>('user')
|
||||
const permissionDialogTitle = computed(() => (permissionMode.value === 'server_assign' ? '服务器资源用户权限分配' : '资源已有用户权限修改'))
|
||||
const opsSaveButtonLabel = computed(() => (forceSaveOpsReady.value ? '强制保存' : '保存我的软件选择'))
|
||||
const resourceProtocolOptions = computed<string[]>(() => {
|
||||
@ -340,6 +473,8 @@ function resetForm(): void {
|
||||
display_name: '',
|
||||
parent_id: null,
|
||||
internal_ip: '',
|
||||
user_api_base_url: '',
|
||||
user_api_token: '',
|
||||
asset_id: null,
|
||||
account_id: null,
|
||||
protocol: resourceProtocolOptions.value[0] || '',
|
||||
@ -384,6 +519,8 @@ function openEdit(row: any): void {
|
||||
display_name: row.display_name || '',
|
||||
parent_id: row.parent_id,
|
||||
internal_ip: row.internal_ip,
|
||||
user_api_base_url: row.user_api_base_url || '',
|
||||
user_api_token: '',
|
||||
asset_id: row.asset_id,
|
||||
account_id: row.account_id,
|
||||
protocol: row.protocols?.[0] || 'SSH',
|
||||
@ -411,6 +548,8 @@ async function submit(): Promise<void> {
|
||||
name: form.name,
|
||||
display_name: form.display_name || form.name,
|
||||
internal_ip: form.type === 'server' ? form.internal_ip : null,
|
||||
user_api_base_url: form.type === 'server' ? form.user_api_base_url || null : null,
|
||||
user_api_token: form.type === 'server' ? form.user_api_token || null : null,
|
||||
asset_id: form.type === 'server' ? form.asset_id : null,
|
||||
account_id: form.type === 'resource' ? form.account_id : null,
|
||||
protocol: form.type === 'resource' ? form.protocol : null,
|
||||
@ -556,13 +695,185 @@ function openUseResourceDialog(row: any): void {
|
||||
const serverId = Number(row.parent_id || row.id || 0)
|
||||
const saved = getServerCredential(currentUserId.value, serverId)
|
||||
useForm.protocol = row.protocols?.[0] || 'SSH'
|
||||
useForm.account_name = saved?.account_name || ''
|
||||
useForm.account_name = row.server_username || saved?.account_name || ''
|
||||
useForm.password = saved?.password || ''
|
||||
useForm.remember = Boolean(saved)
|
||||
useForm.last_temp_password = ''
|
||||
useDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openBoundPasswordDialog(row: any): void {
|
||||
boundPasswordTarget.value = row
|
||||
boundPasswordForm.password = ''
|
||||
boundPasswordDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitBoundPassword(): Promise<void> {
|
||||
if (!boundPasswordTarget.value?.id) {
|
||||
ElMessage.warning('未选择服务器')
|
||||
return
|
||||
}
|
||||
if (!boundPasswordForm.password?.trim()) {
|
||||
ElMessage.warning('请输入新密码')
|
||||
return
|
||||
}
|
||||
|
||||
updatingBoundPassword.value = true
|
||||
try {
|
||||
await serversApi.updateBoundSystemUserPassword(boundPasswordTarget.value.id, {
|
||||
password: boundPasswordForm.password,
|
||||
})
|
||||
boundPasswordDialogVisible.value = false
|
||||
if (useTargetRow.value?.id === boundPasswordTarget.value.id || useTargetRow.value?.parent_id === boundPasswordTarget.value.id) {
|
||||
useForm.password = boundPasswordForm.password
|
||||
}
|
||||
ElMessage.success('绑定账号密码已更新')
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '修改绑定账号密码失败')
|
||||
} finally {
|
||||
updatingBoundPassword.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openSystemUsersDialog(server: any): Promise<void> {
|
||||
systemUsersServer.value = server
|
||||
systemUsersTab.value = 'users'
|
||||
systemUsersDialogVisible.value = true
|
||||
await loadSystemUsersMeta()
|
||||
}
|
||||
|
||||
async function loadSystemUsersMeta(): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
systemUsersLoading.value = true
|
||||
try {
|
||||
const response: any = await serversApi.systemUsersMeta(systemUsersServer.value.id)
|
||||
systemUsers.value = response.data.users || []
|
||||
systemGroups.value = response.data.groups || []
|
||||
systemUserBindings.value = response.data.bindings || []
|
||||
Object.keys(systemUserGroupSelections).forEach((key) => delete systemUserGroupSelections[key])
|
||||
Object.keys(savingSystemUserGroups).forEach((key) => delete savingSystemUserGroups[key])
|
||||
const userGroups = response.data.user_groups || {}
|
||||
for (const user of systemUsers.value) {
|
||||
systemUserGroupSelections[user.username] = [...(userGroups[user.username] || [])]
|
||||
}
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '加载服务器用户失败')
|
||||
} finally {
|
||||
systemUsersLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function bindingLabel(username: string): string {
|
||||
const binding = systemUserBindings.value.find((item: any) => item.username === username)
|
||||
const user = binding?.user
|
||||
return user ? `${user.nickname || user.email || user.id}` : '-'
|
||||
}
|
||||
|
||||
function openCreateSystemUser(): void {
|
||||
systemUserFormMode.value = 'user'
|
||||
Object.assign(systemUserForm, { username: '', password: '', groups: [], shell: '/bin/bash', home_dir: '', groupname: '' })
|
||||
systemUserFormVisible.value = true
|
||||
}
|
||||
|
||||
function openCreateSystemGroup(): void {
|
||||
systemUserFormMode.value = 'group'
|
||||
Object.assign(systemUserForm, { username: '', password: '', groups: [], shell: '/bin/bash', home_dir: '', groupname: '' })
|
||||
systemUserFormVisible.value = true
|
||||
}
|
||||
|
||||
async function submitSystemUserForm(): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
systemUsersSaving.value = true
|
||||
try {
|
||||
if (systemUserFormMode.value === 'group') {
|
||||
await serversApi.createSystemGroup(systemUsersServer.value.id, { groupname: systemUserForm.groupname })
|
||||
ElMessage.success('用户组创建成功')
|
||||
} else {
|
||||
await serversApi.createSystemUser(systemUsersServer.value.id, {
|
||||
username: systemUserForm.username,
|
||||
password: systemUserForm.password,
|
||||
groups: systemUserForm.groups,
|
||||
shell: systemUserForm.shell || '/bin/bash',
|
||||
home_dir: systemUserForm.home_dir || null,
|
||||
})
|
||||
ElMessage.success('服务器用户创建成功')
|
||||
}
|
||||
systemUserFormVisible.value = false
|
||||
await loadSystemUsersMeta()
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '提交失败')
|
||||
} finally {
|
||||
systemUsersSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSystemUserGroups(row: any): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
savingSystemUserGroups[row.username] = true
|
||||
try {
|
||||
await serversApi.syncSystemUserGroups(systemUsersServer.value.id, row.username, systemUserGroupSelections[row.username] || [], 'replace')
|
||||
ElMessage.success('用户组已保存')
|
||||
await loadSystemUsersMeta()
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '保存用户组失败')
|
||||
} finally {
|
||||
savingSystemUserGroups[row.username] = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPasswordDialog(row: any): void {
|
||||
passwordTarget.value = row
|
||||
passwordForm.password = ''
|
||||
passwordDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function submitPassword(): Promise<void> {
|
||||
if (!systemUsersServer.value?.id || !passwordTarget.value?.username) {
|
||||
return
|
||||
}
|
||||
systemUsersSaving.value = true
|
||||
try {
|
||||
await serversApi.updateSystemUserPassword(systemUsersServer.value.id, passwordTarget.value.username, { password: passwordForm.password })
|
||||
ElMessage.success('密码已更新')
|
||||
passwordDialogVisible.value = false
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '修改密码失败')
|
||||
} finally {
|
||||
systemUsersSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function removeSystemUser(row: any): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除服务器用户 ${row.username} 吗?`, '提示', { type: 'warning' })
|
||||
await serversApi.removeSystemUser(systemUsersServer.value.id, row.username)
|
||||
ElMessage.success('服务器用户已删除')
|
||||
await loadSystemUsersMeta()
|
||||
}
|
||||
|
||||
async function removeSystemGroup(row: any): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
await ElMessageBox.confirm(`确认删除用户组 ${row.groupname} 吗?`, '提示', { type: 'warning' })
|
||||
await serversApi.removeSystemGroup(systemUsersServer.value.id, row.groupname)
|
||||
ElMessage.success('用户组已删除')
|
||||
await loadSystemUsersMeta()
|
||||
}
|
||||
|
||||
async function submitUseResource(): Promise<void> {
|
||||
const result = await requestUseResource('connect')
|
||||
if (!result) {
|
||||
@ -916,6 +1227,15 @@ onBeforeUnmount(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.system-user-toolbar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
:deep(.system-users-dialog .el-dialog__body) {
|
||||
max-height: 78vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.server-collapse) {
|
||||
border-top: 0;
|
||||
border-bottom: 0;
|
||||
|
||||
@ -20,6 +20,9 @@
|
||||
<el-table-column label='角色' min-width='160'>
|
||||
<template #default='{ row }'>{{ (row.roles || []).map((r:any) => r.name).join(', ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='服务器账号' min-width='260'>
|
||||
<template #default='{ row }'>{{ serverBindingsLabel(row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='直授权限' min-width='220'>
|
||||
<template #default='{ row }'>{{ (row.permissions || []).map((p:any) => p.name).join(', ') || '-' }}</template>
|
||||
</el-table-column>
|
||||
@ -45,7 +48,7 @@
|
||||
@size-change='handleSizeChange'
|
||||
/>
|
||||
|
||||
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑用户" : "新增用户"' width='560px'>
|
||||
<el-dialog v-model='dialogVisible' :title='editingId ? "编辑用户" : "新增用户"' width='780px'>
|
||||
<el-form :model='form' label-width='95px'>
|
||||
<el-form-item label='昵称'><el-input v-model='form.nickname' /></el-form-item>
|
||||
<el-form-item label='邮箱'><el-input v-model='form.email' /></el-form-item>
|
||||
@ -57,6 +60,26 @@
|
||||
<el-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-divider>服务器账号绑定</el-divider>
|
||||
<el-form-item label='服务器'>
|
||||
<el-select v-model='form.server_ids' multiple collapse-tags clearable class='w-full' @change='syncServerBindingRows'>
|
||||
<el-option v-for='item in serverOptions' :key='item.id' :label='item.display_name || item.name' :value='item.id' />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-table v-if='form.server_bindings.length' :data='form.server_bindings' border size='small' class='mb-3'>
|
||||
<el-table-column label='服务器' min-width='160'>
|
||||
<template #default='{ row }'>{{ serverLabel(row.server_resource_id) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='服务器用户名' min-width='160'>
|
||||
<template #default='{ row }'><el-input v-model='row.username' /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label='需要创建用户' width='130'>
|
||||
<template #default='{ row }'><el-switch v-model='row.create_remote' /></template>
|
||||
</el-table-column>
|
||||
<el-table-column label='账号密码' min-width='160'>
|
||||
<template #default='{ row }'><el-input v-model='row.password' type='password' show-password placeholder='默认使用用户密码' /></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='dialogVisible = false'>取消</el-button>
|
||||
@ -140,9 +163,10 @@
|
||||
<script setup lang='ts'>
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { permissionsApi } from '@/api/permissions'
|
||||
import { rolesApi } from '@/api/roles'
|
||||
import { serversApi } from '@/api/servers'
|
||||
import { usersApi } from '@/api/users'
|
||||
import { buildPermissionCascader, extractPermissionIds } from '@/composables/permission-tree'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
@ -165,6 +189,7 @@ const selectedUserIds = ref<number[]>([])
|
||||
const rows = ref<any[]>([])
|
||||
const roleOptions = ref<any[]>([])
|
||||
const permissionOptions = ref<any[]>([])
|
||||
const serverOptions = ref<any[]>([])
|
||||
const selectedPermissionNodes = ref<Array<number | string>>([])
|
||||
const batchAssignPermissionNodes = ref<Array<number | string>>([])
|
||||
const importFile = ref<File | null>(null)
|
||||
@ -175,7 +200,7 @@ const perPage = ref(20)
|
||||
const sortBy = ref('created_at')
|
||||
const sortOrder = ref<'asc' | 'desc'>('desc')
|
||||
|
||||
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
|
||||
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [], server_ids: [], server_bindings: [] })
|
||||
const batchAssignForm = reactive<{ role_ids: number[] }>({ role_ids: [] })
|
||||
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
|
||||
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
|
||||
@ -200,10 +225,12 @@ async function fetchList(): Promise<void> {
|
||||
|
||||
async function fetchOptions(): Promise<void> {
|
||||
try {
|
||||
const [rolesResponse]: any = await Promise.all([
|
||||
const [rolesResponse, serversResponse]: any = await Promise.all([
|
||||
rolesApi.list({ page: 1, per_page: 100 }),
|
||||
serversApi.list({ page: 1, per_page: 500 }),
|
||||
])
|
||||
roleOptions.value = rolesResponse.data.data || []
|
||||
serverOptions.value = (serversResponse.data.data || []).filter((item: any) => !item.parent_id)
|
||||
let nextPage = 1
|
||||
const allPermissions: any[] = []
|
||||
while (true) {
|
||||
@ -254,7 +281,7 @@ function handleSortChange(payload: { prop: string, order: 'ascending' | 'descend
|
||||
|
||||
function openCreate(): void {
|
||||
editingId.value = null
|
||||
Object.assign(form, { nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
|
||||
Object.assign(form, { nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [], server_ids: [], server_bindings: [] })
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -267,6 +294,13 @@ function openEdit(row: any): void {
|
||||
password: '',
|
||||
force_password_change: Boolean(row.force_password_change),
|
||||
role_ids: (row.roles || []).map((item: any) => item.id),
|
||||
server_ids: (row.server_user_bindings || []).map((item: any) => item.server_resource_id),
|
||||
server_bindings: (row.server_user_bindings || []).map((item: any) => ({
|
||||
server_resource_id: item.server_resource_id,
|
||||
username: item.username,
|
||||
create_remote: false,
|
||||
password: '',
|
||||
})),
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@ -281,9 +315,16 @@ async function submit(): Promise<void> {
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = { ...form }
|
||||
payload.server_bindings = form.server_bindings.map((item: any) => ({
|
||||
server_resource_id: item.server_resource_id,
|
||||
username: item.username,
|
||||
create_remote: Boolean(item.create_remote),
|
||||
password: item.password || undefined,
|
||||
}))
|
||||
if (!payload.password) {
|
||||
delete payload.password
|
||||
}
|
||||
delete payload.server_ids
|
||||
|
||||
if (editingId.value) {
|
||||
await usersApi.update(editingId.value, payload)
|
||||
@ -303,6 +344,79 @@ async function submit(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function syncServerBindingRows(): void {
|
||||
const defaultUsername = normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`)
|
||||
const current = new Map<number, any>(form.server_bindings.map((item: any) => [Number(item.server_resource_id), item]))
|
||||
form.server_bindings = form.server_ids.map((serverId: number) => {
|
||||
const existing = current.get(Number(serverId))
|
||||
if (existing) {
|
||||
return existing
|
||||
}
|
||||
|
||||
return {
|
||||
server_resource_id: serverId,
|
||||
username: defaultUsername,
|
||||
create_remote: true,
|
||||
password: '',
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => form.nickname, () => {
|
||||
const username = normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`)
|
||||
form.server_bindings.forEach((item: any) => {
|
||||
if (item.username) {
|
||||
item.username = username
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function serverBindingsLabel(row: any): string {
|
||||
const bindings = row.server_user_bindings || []
|
||||
if (!bindings.length) {
|
||||
return '-'
|
||||
}
|
||||
|
||||
return bindings
|
||||
.map((item: any) => `${item.server?.display_name || item.server?.name || `服务器#${item.server_resource_id}`}: ${item.username}`)
|
||||
.join(';')
|
||||
}
|
||||
|
||||
function serverLabel(serverId: number): string {
|
||||
const server = serverOptions.value.find((item: any) => Number(item.id) === Number(serverId))
|
||||
return server ? String(server.display_name || server.name || server.id) : `服务器#${serverId}`
|
||||
}
|
||||
|
||||
function normalizeServerUsername(value: string): string {
|
||||
const raw = String(value || '').trim()
|
||||
const ascii = raw
|
||||
.normalize('NFKD')
|
||||
.replace(/[^\x00-\x7F]/g, (char) => pinyinMap[char] || '')
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_-]+/g, '_')
|
||||
.replace(/^[^a-z_]+/, '')
|
||||
.replace(/_+/g, '_')
|
||||
.slice(0, 32)
|
||||
|
||||
return ascii || `user${Date.now().toString().slice(-6)}`
|
||||
}
|
||||
|
||||
const pinyinMap: Record<string, string> = {
|
||||
张: 'zhang', 王: 'wang', 李: 'li', 赵: 'zhao', 陈: 'chen', 刘: 'liu', 杨: 'yang', 黄: 'huang', 周: 'zhou', 吴: 'wu',
|
||||
徐: 'xu', 孙: 'sun', 胡: 'hu', 朱: 'zhu', 高: 'gao', 林: 'lin', 何: 'he', 郭: 'guo', 马: 'ma', 罗: 'luo',
|
||||
梁: 'liang', 宋: 'song', 郑: 'zheng', 谢: 'xie', 韩: 'han', 唐: 'tang', 冯: 'feng', 于: 'yu', 董: 'dong',
|
||||
萧: 'xiao', 程: 'cheng', 曹: 'cao', 袁: 'yuan', 邓: 'deng', 许: 'xu', 傅: 'fu', 沈: 'shen', 曾: 'zeng',
|
||||
彭: 'peng', 吕: 'lv', 苏: 'su', 卢: 'lu', 蒋: 'jiang', 蔡: 'cai', 贾: 'jia', 丁: 'ding', 魏: 'wei',
|
||||
薛: 'xue', 叶: 'ye', 阎: 'yan', 余: 'yu', 潘: 'pan', 杜: 'du', 戴: 'dai', 夏: 'xia', 钟: 'zhong',
|
||||
汪: 'wang', 田: 'tian', 任: 'ren', 姜: 'jiang', 范: 'fan', 方: 'fang', 石: 'shi', 姚: 'yao', 谭: 'tan',
|
||||
廖: 'liao', 邹: 'zou', 熊: 'xiong', 金: 'jin', 陆: 'lu', 郝: 'hao', 孔: 'kong', 白: 'bai', 崔: 'cui',
|
||||
康: 'kang', 毛: 'mao', 邱: 'qiu', 秦: 'qin', 江: 'jiang', 史: 'shi', 顾: 'gu', 侯: 'hou', 邵: 'shao',
|
||||
孟: 'meng', 龙: 'long', 万: 'wan', 段: 'duan', 漕: 'cao', 钱: 'qian', 汤: 'tang', 尹: 'yin', 黎: 'li',
|
||||
易: 'yi', 常: 'chang', 武: 'wu', 乔: 'qiao', 贺: 'he', 赖: 'lai', 龚: 'gong', 文: 'wen', 三: 'san',
|
||||
四: 'si', 五: 'wu', 明: 'ming', 华: 'hua', 强: 'qiang', 伟: 'wei', 芳: 'fang', 娜: 'na', 敏: 'min',
|
||||
静: 'jing', 丽: 'li', 刚: 'gang', 磊: 'lei', 军: 'jun', 洋: 'yang', 勇: 'yong', 艳: 'yan',
|
||||
}
|
||||
|
||||
async function submitPermissions(): Promise<void> {
|
||||
if (!permissionTargetUserId.value) {
|
||||
return
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user