Compare commits
2 Commits
5ec6df4af8
...
bb17029279
| Author | SHA1 | Date | |
|---|---|---|---|
| bb17029279 | |||
| 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']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTable: typeof import('element-plus/es')['ElTable']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
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']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
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) {
|
syncUserPermissions(id: number, users: Array<Record<string, unknown>>, partial = false) {
|
||||||
return request.put(`/servers/${id}/user-permissions`, { users, partial })
|
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>) {
|
useResource(id: number, data: Record<string, unknown>) {
|
||||||
return request.post(`/servers/${id}/use`, data)
|
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[] }) {
|
syncBatchAssignments(payload: { user_ids: number[]; role_ids: number[]; permission_ids: number[] }) {
|
||||||
return request.put('/users/batch-assignments', payload)
|
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) {
|
importUsers(file: File) {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|||||||
@ -21,6 +21,8 @@
|
|||||||
<span class='server-name'>{{ server.display_name || server.name }}</span>
|
<span class='server-name'>{{ server.display_name || server.name }}</span>
|
||||||
<span class='server-code'>{{ server.name }}</span>
|
<span class='server-code'>{{ server.name }}</span>
|
||||||
<span class='server-ip'>{{ server.internal_ip }}</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -28,6 +30,7 @@
|
|||||||
<el-button size='small' @click='openEdit(server)'>编辑服务器</el-button>
|
<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='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='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>
|
<el-button size='small' type='danger' @click='removeRow(server)'>删除服务器</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -42,6 +45,9 @@
|
|||||||
<el-table-column label='协议' width='120' sortable :sort-method='sortByProtocol'>
|
<el-table-column label='协议' width='120' sortable :sort-method='sortByProtocol'>
|
||||||
<template #default='{ row }'>{{ row.protocols?.[0] || '-' }}</template>
|
<template #default='{ row }'>{{ row.protocols?.[0] || '-' }}</template>
|
||||||
</el-table-column>
|
</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>
|
<el-table-column label='状态' width='90' prop='is_active' sortable>
|
||||||
<template #default='{ row }'><el-tag :type='row.is_active ? "success" : "info"'>{{ row.is_active ? '启用' :
|
<template #default='{ row }'><el-tag :type='row.is_active ? "success" : "info"'>{{ row.is_active ? '启用' :
|
||||||
'停用' }}</el-tag></template>
|
'停用' }}</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='内网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'
|
<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>
|
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'
|
<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>
|
:min='1' class='w-full' /></el-form-item>
|
||||||
<el-form-item label='协议' v-if='form.type === "resource"'>
|
<el-form-item label='协议' v-if='form.type === "resource"'>
|
||||||
@ -106,6 +118,90 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
||||||
|
</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-dialog v-model='permissionDialogVisible' :title='permissionDialogTitle' width='1100px'>
|
||||||
<el-table :data='permissionMatrixRows' max-height='520' border>
|
<el-table :data='permissionMatrixRows' max-height='520' border>
|
||||||
<el-table-column prop='nickname' label='用户名' min-width='150' fixed='left' />
|
<el-table-column prop='nickname' label='用户名' min-width='150' fixed='left' />
|
||||||
@ -149,6 +245,21 @@
|
|||||||
<el-button type='primary' :loading='usingResource' @click='submitUseResource'>连接并访问</el-button>
|
<el-button type='primary' :loading='usingResource' @click='submitUseResource'>连接并访问</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</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>
|
||||||
|
|
||||||
<el-card v-loading='opsLoading' style="margin-top: 1rem;">
|
<el-card v-loading='opsLoading' style="margin-top: 1rem;">
|
||||||
@ -205,11 +316,7 @@
|
|||||||
<el-dialog v-model='protocolDialogVisible' :title='editingProtocolId ? "编辑协议" : "新增协议"' width='520px'>
|
<el-dialog v-model='protocolDialogVisible' :title='editingProtocolId ? "编辑协议" : "新增协议"' width='520px'>
|
||||||
<el-form :model='protocolForm' label-width='90px'>
|
<el-form :model='protocolForm' label-width='90px'>
|
||||||
<el-form-item label='协议名称'><el-input v-model='protocolForm.name' /></el-form-item>
|
<el-form-item label='协议名称'><el-input v-model='protocolForm.name' /></el-form-item>
|
||||||
<el-form-item label='协议ID'><el-input-number v-model='protocolForm.bastion_protocol_id' :min='1'
|
|
||||||
class='w-full' /></el-form-item>
|
|
||||||
<el-form-item label='描述'><el-input v-model='protocolForm.description' /></el-form-item>
|
<el-form-item label='描述'><el-input v-model='protocolForm.description' /></el-form-item>
|
||||||
<el-form-item label='排序'><el-input-number v-model='protocolForm.sort' :min='0' class='w-full' /></el-form-item>
|
|
||||||
<el-form-item label='启用'><el-switch v-model='protocolForm.is_active' /></el-form-item>
|
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click='protocolDialogVisible = false'>取消</el-button>
|
<el-button @click='protocolDialogVisible = false'>取消</el-button>
|
||||||
@ -223,8 +330,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<el-table :data='softwareProtocol?.softwares || []' border>
|
<el-table :data='softwareProtocol?.softwares || []' border>
|
||||||
<el-table-column prop='name' label='软件名称' min-width='160' />
|
<el-table-column prop='name' label='软件名称' min-width='160' />
|
||||||
<el-table-column prop='client_path' label='ClientPath' min-width='220' />
|
|
||||||
<el-table-column prop='sort' label='排序' width='80' />
|
|
||||||
<el-table-column label='启用' width='80'>
|
<el-table-column label='启用' width='80'>
|
||||||
<template #default='{ row }'><el-tag :type='row.is_active ? "success" : "info"'>{{ row.is_active ? '是' : '否'
|
<template #default='{ row }'><el-tag :type='row.is_active ? "success" : "info"'>{{ row.is_active ? '是' : '否'
|
||||||
}}</el-tag></template>
|
}}</el-tag></template>
|
||||||
@ -238,17 +343,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click='softwareDialogVisible = false'>关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-divider />
|
<el-dialog v-model='softwareFormDialogVisible' :title='editingSoftwareId ? "编辑软件" : "新增软件"' width='520px'>
|
||||||
<el-form :model='softwareForm' label-width='90px'>
|
<el-form :model='softwareForm' label-width='90px'>
|
||||||
<el-form-item label='软件名称'><el-input v-model='softwareForm.name' /></el-form-item>
|
<el-form-item label='软件名称'><el-input v-model='softwareForm.name' /></el-form-item>
|
||||||
<el-form-item label='ClientPath'><el-input v-model='softwareForm.client_path'
|
|
||||||
placeholder='可为空' /></el-form-item>
|
|
||||||
<el-form-item label='排序'><el-input-number v-model='softwareForm.sort' :min='0' class='w-full' /></el-form-item>
|
|
||||||
<el-form-item label='启用'><el-switch v-model='softwareForm.is_active' /></el-form-item>
|
<el-form-item label='启用'><el-switch v-model='softwareForm.is_active' /></el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click='softwareDialogVisible = false'>关闭</el-button>
|
<el-button @click='softwareFormDialogVisible = false'>取消</el-button>
|
||||||
<el-button type='primary' :loading='opsSaving' @click='submitSoftware'>{{ editingSoftwareId ? '更新软件' : '新增软件'
|
<el-button type='primary' :loading='opsSaving' @click='submitSoftware'>{{ editingSoftwareId ? '更新软件' : '新增软件'
|
||||||
}}</el-button>
|
}}</el-button>
|
||||||
</template>
|
</template>
|
||||||
@ -271,11 +377,20 @@ const saving = ref(false)
|
|||||||
const dialogVisible = ref(false)
|
const dialogVisible = ref(false)
|
||||||
const permissionDialogVisible = ref(false)
|
const permissionDialogVisible = ref(false)
|
||||||
const useDialogVisible = 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 protocolDialogVisible = ref(false)
|
||||||
const softwareDialogVisible = ref(false)
|
const softwareDialogVisible = ref(false)
|
||||||
|
const softwareFormDialogVisible = ref(false)
|
||||||
const savingPermissions = ref(false)
|
const savingPermissions = ref(false)
|
||||||
const usingResource = ref(false)
|
const usingResource = ref(false)
|
||||||
const copyingTempPassword = 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 opsLoading = ref(false)
|
||||||
const opsSaving = ref(false)
|
const opsSaving = ref(false)
|
||||||
const forceSaveOpsReady = ref(false)
|
const forceSaveOpsReady = ref(false)
|
||||||
@ -290,17 +405,29 @@ const permissionMatrixRows = ref<any[]>([])
|
|||||||
const permissionResourceOptions = ref<any[]>([])
|
const permissionResourceOptions = ref<any[]>([])
|
||||||
const activeServers = ref<string[]>([])
|
const activeServers = ref<string[]>([])
|
||||||
const useTargetRow = ref<any | null>(null)
|
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 opsProtocols = ref<any[]>([])
|
||||||
const softwareProtocol = ref<any | null>(null)
|
const softwareProtocol = ref<any | null>(null)
|
||||||
const opsLinkLoading = ref<Record<number, boolean>>({})
|
const opsLinkLoading = ref<Record<number, boolean>>({})
|
||||||
const opsSavedSelections = reactive<Record<string, number | null>>({})
|
const opsSavedSelections = reactive<Record<string, number | null>>({})
|
||||||
|
|
||||||
const typeOptions = [{ label: '服务器', value: 'server' }, { label: '资源', value: 'resource' }]
|
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: [], groupname: '' })
|
||||||
const useForm = reactive<any>({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' })
|
const useForm = reactive<any>({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' })
|
||||||
const protocolForm = reactive<any>({ name: '', bastion_protocol_id: 2, description: '', sort: 0, is_active: true })
|
const passwordForm = reactive<any>({ password: '' })
|
||||||
const softwareForm = reactive<any>({ name: '', client_path: '', sort: 0, is_active: true })
|
const boundPasswordForm = reactive<any>({ password: '' })
|
||||||
|
const protocolForm = reactive<any>({ name: '', description: '' })
|
||||||
|
const softwareForm = reactive<any>({ name: '', is_active: true })
|
||||||
const opsSelections = reactive<Record<string, number | null>>({})
|
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 permissionDialogTitle = computed(() => (permissionMode.value === 'server_assign' ? '服务器资源用户权限分配' : '资源已有用户权限修改'))
|
||||||
const opsSaveButtonLabel = computed(() => (forceSaveOpsReady.value ? '强制保存' : '保存我的软件选择'))
|
const opsSaveButtonLabel = computed(() => (forceSaveOpsReady.value ? '强制保存' : '保存我的软件选择'))
|
||||||
const resourceProtocolOptions = computed<string[]>(() => {
|
const resourceProtocolOptions = computed<string[]>(() => {
|
||||||
@ -340,6 +467,8 @@ function resetForm(): void {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
parent_id: null,
|
parent_id: null,
|
||||||
internal_ip: '',
|
internal_ip: '',
|
||||||
|
user_api_base_url: '',
|
||||||
|
user_api_token: '',
|
||||||
asset_id: null,
|
asset_id: null,
|
||||||
account_id: null,
|
account_id: null,
|
||||||
protocol: resourceProtocolOptions.value[0] || '',
|
protocol: resourceProtocolOptions.value[0] || '',
|
||||||
@ -384,6 +513,8 @@ function openEdit(row: any): void {
|
|||||||
display_name: row.display_name || '',
|
display_name: row.display_name || '',
|
||||||
parent_id: row.parent_id,
|
parent_id: row.parent_id,
|
||||||
internal_ip: row.internal_ip,
|
internal_ip: row.internal_ip,
|
||||||
|
user_api_base_url: row.user_api_base_url || '',
|
||||||
|
user_api_token: '',
|
||||||
asset_id: row.asset_id,
|
asset_id: row.asset_id,
|
||||||
account_id: row.account_id,
|
account_id: row.account_id,
|
||||||
protocol: row.protocols?.[0] || 'SSH',
|
protocol: row.protocols?.[0] || 'SSH',
|
||||||
@ -411,6 +542,8 @@ async function submit(): Promise<void> {
|
|||||||
name: form.name,
|
name: form.name,
|
||||||
display_name: form.display_name || form.name,
|
display_name: form.display_name || form.name,
|
||||||
internal_ip: form.type === 'server' ? form.internal_ip : null,
|
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,
|
asset_id: form.type === 'server' ? form.asset_id : null,
|
||||||
account_id: form.type === 'resource' ? form.account_id : null,
|
account_id: form.type === 'resource' ? form.account_id : null,
|
||||||
protocol: form.type === 'resource' ? form.protocol : null,
|
protocol: form.type === 'resource' ? form.protocol : null,
|
||||||
@ -556,13 +689,183 @@ function openUseResourceDialog(row: any): void {
|
|||||||
const serverId = Number(row.parent_id || row.id || 0)
|
const serverId = Number(row.parent_id || row.id || 0)
|
||||||
const saved = getServerCredential(currentUserId.value, serverId)
|
const saved = getServerCredential(currentUserId.value, serverId)
|
||||||
useForm.protocol = row.protocols?.[0] || 'SSH'
|
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.password = saved?.password || ''
|
||||||
useForm.remember = Boolean(saved)
|
useForm.remember = Boolean(saved)
|
||||||
useForm.last_temp_password = ''
|
useForm.last_temp_password = ''
|
||||||
useDialogVisible.value = true
|
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: [], groupname: '' })
|
||||||
|
systemUserFormVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateSystemGroup(): void {
|
||||||
|
systemUserFormMode.value = 'group'
|
||||||
|
Object.assign(systemUserForm, { username: '', password: '', groups: [], 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,
|
||||||
|
})
|
||||||
|
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> {
|
async function submitUseResource(): Promise<void> {
|
||||||
const result = await requestUseResource('connect')
|
const result = await requestUseResource('connect')
|
||||||
if (!result) {
|
if (!result) {
|
||||||
@ -679,7 +982,7 @@ async function fetchOpsMeta(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetProtocolForm(): void {
|
function resetProtocolForm(): void {
|
||||||
Object.assign(protocolForm, { name: '', bastion_protocol_id: 2, description: '', sort: 0, is_active: true })
|
Object.assign(protocolForm, { name: '', description: '' })
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateProtocol(): void {
|
function openCreateProtocol(): void {
|
||||||
@ -692,10 +995,7 @@ function openEditProtocol(row: any): void {
|
|||||||
editingProtocolId.value = row.id
|
editingProtocolId.value = row.id
|
||||||
Object.assign(protocolForm, {
|
Object.assign(protocolForm, {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
bastion_protocol_id: row.bastion_protocol_id || 2,
|
|
||||||
description: row.description || '',
|
description: row.description || '',
|
||||||
sort: row.sort || 0,
|
|
||||||
is_active: Boolean(row.is_active),
|
|
||||||
})
|
})
|
||||||
protocolDialogVisible.value = true
|
protocolDialogVisible.value = true
|
||||||
}
|
}
|
||||||
@ -705,10 +1005,7 @@ async function submitProtocol(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: protocolForm.name,
|
name: protocolForm.name,
|
||||||
bastion_protocol_id: Number(protocolForm.bastion_protocol_id || 2),
|
|
||||||
description: protocolForm.description || '',
|
description: protocolForm.description || '',
|
||||||
sort: protocolForm.sort || 0,
|
|
||||||
is_active: Boolean(protocolForm.is_active),
|
|
||||||
}
|
}
|
||||||
if (editingProtocolId.value) {
|
if (editingProtocolId.value) {
|
||||||
await serversApi.updateOpsProtocol(editingProtocolId.value, payload)
|
await serversApi.updateOpsProtocol(editingProtocolId.value, payload)
|
||||||
@ -736,27 +1033,28 @@ async function removeProtocol(row: any): Promise<void> {
|
|||||||
|
|
||||||
function resetSoftwareForm(): void {
|
function resetSoftwareForm(): void {
|
||||||
editingSoftwareId.value = null
|
editingSoftwareId.value = null
|
||||||
Object.assign(softwareForm, { name: '', client_path: '', sort: 0, is_active: true })
|
Object.assign(softwareForm, { name: '', is_active: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSoftwareDialog(row: any): void {
|
function openSoftwareDialog(row: any): void {
|
||||||
softwareProtocol.value = row
|
softwareProtocol.value = row
|
||||||
resetSoftwareForm()
|
resetSoftwareForm()
|
||||||
|
softwareFormDialogVisible.value = false
|
||||||
softwareDialogVisible.value = true
|
softwareDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openCreateSoftware(): void {
|
function openCreateSoftware(): void {
|
||||||
resetSoftwareForm()
|
resetSoftwareForm()
|
||||||
|
softwareFormDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEditSoftware(row: any): void {
|
function openEditSoftware(row: any): void {
|
||||||
editingSoftwareId.value = row.id
|
editingSoftwareId.value = row.id
|
||||||
Object.assign(softwareForm, {
|
Object.assign(softwareForm, {
|
||||||
name: row.name,
|
name: row.name,
|
||||||
client_path: row.client_path || '',
|
|
||||||
sort: row.sort || 0,
|
|
||||||
is_active: Boolean(row.is_active),
|
is_active: Boolean(row.is_active),
|
||||||
})
|
})
|
||||||
|
softwareFormDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async function submitSoftware(): Promise<void> {
|
async function submitSoftware(): Promise<void> {
|
||||||
@ -768,8 +1066,6 @@ async function submitSoftware(): Promise<void> {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: softwareForm.name,
|
name: softwareForm.name,
|
||||||
client_path: softwareForm.client_path || '',
|
|
||||||
sort: softwareForm.sort || 0,
|
|
||||||
is_active: Boolean(softwareForm.is_active),
|
is_active: Boolean(softwareForm.is_active),
|
||||||
}
|
}
|
||||||
if (editingSoftwareId.value) {
|
if (editingSoftwareId.value) {
|
||||||
@ -779,6 +1075,7 @@ async function submitSoftware(): Promise<void> {
|
|||||||
await serversApi.createOpsSoftware(softwareProtocol.value.id, payload)
|
await serversApi.createOpsSoftware(softwareProtocol.value.id, payload)
|
||||||
ElMessage.success('软件创建成功')
|
ElMessage.success('软件创建成功')
|
||||||
}
|
}
|
||||||
|
softwareFormDialogVisible.value = false
|
||||||
resetSoftwareForm()
|
resetSoftwareForm()
|
||||||
await fetchOpsMeta()
|
await fetchOpsMeta()
|
||||||
softwareProtocol.value = opsProtocols.value.find((item) => item.id === softwareProtocol.value?.id) || null
|
softwareProtocol.value = opsProtocols.value.find((item) => item.id === softwareProtocol.value?.id) || null
|
||||||
@ -916,6 +1213,15 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.system-user-toolbar {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.system-users-dialog .el-dialog__body) {
|
||||||
|
max-height: 78vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.server-collapse) {
|
:deep(.server-collapse) {
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
|||||||
@ -20,6 +20,9 @@
|
|||||||
<el-table-column label='角色' min-width='160'>
|
<el-table-column label='角色' min-width='160'>
|
||||||
<template #default='{ row }'>{{ (row.roles || []).map((r:any) => r.name).join(', ') || '-' }}</template>
|
<template #default='{ row }'>{{ (row.roles || []).map((r:any) => r.name).join(', ') || '-' }}</template>
|
||||||
</el-table-column>
|
</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'>
|
<el-table-column label='直授权限' min-width='220'>
|
||||||
<template #default='{ row }'>{{ (row.permissions || []).map((p:any) => p.name).join(', ') || '-' }}</template>
|
<template #default='{ row }'>{{ (row.permissions || []).map((p:any) => p.name).join(', ') || '-' }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@ -45,7 +48,7 @@
|
|||||||
@size-change='handleSizeChange'
|
@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 :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.nickname' /></el-form-item>
|
||||||
<el-form-item label='邮箱'><el-input v-model='form.email' /></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-option v-for='item in roleOptions' :key='item.id' :label='item.name' :value='item.id' />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</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>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click='dialogVisible = false'>取消</el-button>
|
<el-button @click='dialogVisible = false'>取消</el-button>
|
||||||
@ -140,9 +163,10 @@
|
|||||||
<script setup lang='ts'>
|
<script setup lang='ts'>
|
||||||
import { UploadFilled } from '@element-plus/icons-vue'
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 { permissionsApi } from '@/api/permissions'
|
||||||
import { rolesApi } from '@/api/roles'
|
import { rolesApi } from '@/api/roles'
|
||||||
|
import { serversApi } from '@/api/servers'
|
||||||
import { usersApi } from '@/api/users'
|
import { usersApi } from '@/api/users'
|
||||||
import { buildPermissionCascader, extractPermissionIds } from '@/composables/permission-tree'
|
import { buildPermissionCascader, extractPermissionIds } from '@/composables/permission-tree'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
@ -165,6 +189,7 @@ const selectedUserIds = ref<number[]>([])
|
|||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
const roleOptions = ref<any[]>([])
|
const roleOptions = ref<any[]>([])
|
||||||
const permissionOptions = ref<any[]>([])
|
const permissionOptions = ref<any[]>([])
|
||||||
|
const serverOptions = ref<any[]>([])
|
||||||
const selectedPermissionNodes = ref<Array<number | string>>([])
|
const selectedPermissionNodes = ref<Array<number | string>>([])
|
||||||
const batchAssignPermissionNodes = ref<Array<number | string>>([])
|
const batchAssignPermissionNodes = ref<Array<number | string>>([])
|
||||||
const importFile = ref<File | null>(null)
|
const importFile = ref<File | null>(null)
|
||||||
@ -175,7 +200,7 @@ const perPage = ref(20)
|
|||||||
const sortBy = ref('created_at')
|
const sortBy = ref('created_at')
|
||||||
const sortOrder = ref<'asc' | 'desc'>('desc')
|
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 batchAssignForm = reactive<{ role_ids: number[] }>({ role_ids: [] })
|
||||||
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
|
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
|
||||||
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
|
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
|
||||||
@ -200,10 +225,12 @@ async function fetchList(): Promise<void> {
|
|||||||
|
|
||||||
async function fetchOptions(): Promise<void> {
|
async function fetchOptions(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const [rolesResponse]: any = await Promise.all([
|
const [rolesResponse, serversResponse]: any = await Promise.all([
|
||||||
rolesApi.list({ page: 1, per_page: 100 }),
|
rolesApi.list({ page: 1, per_page: 100 }),
|
||||||
|
serversApi.list({ page: 1, per_page: 500 }),
|
||||||
])
|
])
|
||||||
roleOptions.value = rolesResponse.data.data || []
|
roleOptions.value = rolesResponse.data.data || []
|
||||||
|
serverOptions.value = (serversResponse.data.data || []).filter((item: any) => !item.parent_id)
|
||||||
let nextPage = 1
|
let nextPage = 1
|
||||||
const allPermissions: any[] = []
|
const allPermissions: any[] = []
|
||||||
while (true) {
|
while (true) {
|
||||||
@ -254,7 +281,7 @@ function handleSortChange(payload: { prop: string, order: 'ascending' | 'descend
|
|||||||
|
|
||||||
function openCreate(): void {
|
function openCreate(): void {
|
||||||
editingId.value = null
|
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
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,6 +294,13 @@ function openEdit(row: any): void {
|
|||||||
password: '',
|
password: '',
|
||||||
force_password_change: Boolean(row.force_password_change),
|
force_password_change: Boolean(row.force_password_change),
|
||||||
role_ids: (row.roles || []).map((item: any) => item.id),
|
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
|
dialogVisible.value = true
|
||||||
}
|
}
|
||||||
@ -281,9 +315,16 @@ async function submit(): Promise<void> {
|
|||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
const payload: any = { ...form }
|
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) {
|
if (!payload.password) {
|
||||||
delete payload.password
|
delete payload.password
|
||||||
}
|
}
|
||||||
|
delete payload.server_ids
|
||||||
|
|
||||||
if (editingId.value) {
|
if (editingId.value) {
|
||||||
await usersApi.update(editingId.value, payload)
|
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> {
|
async function submitPermissions(): Promise<void> {
|
||||||
if (!permissionTargetUserId.value) {
|
if (!permissionTargetUserId.value) {
|
||||||
return
|
return
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user