feat(用户绑定): 完善用户绑定
This commit is contained in:
parent
bb17029279
commit
9f7f6c19e5
@ -34,6 +34,21 @@ export const serversApi = {
|
||||
updateBoundSystemUserPassword(id: number, data: Record<string, unknown>) {
|
||||
return request.patch(`/servers/${id}/bound-system-user/password`, data)
|
||||
},
|
||||
updateDefaultEnvironment(id: number, content: string) {
|
||||
return request.put(`/servers/${id}/default-environment`, { content })
|
||||
},
|
||||
updateAllUserEnvironments(id: number, content: string) {
|
||||
return request.put(`/servers/${id}/system-users/environment`, { content })
|
||||
},
|
||||
updateDefaultUserGroups(id: number, groups: string[]) {
|
||||
return request.put(`/servers/${id}/default-user-groups`, { groups })
|
||||
},
|
||||
userEnvironment(id: number, username: string) {
|
||||
return request.get(`/servers/${id}/system-users/${encodeURIComponent(username)}/environment`)
|
||||
},
|
||||
updateUserEnvironment(id: number, username: string, content: string) {
|
||||
return request.put(`/servers/${id}/system-users/${encodeURIComponent(username)}/environment`, { content })
|
||||
},
|
||||
createSystemGroup(id: number, data: Record<string, unknown>) {
|
||||
return request.post(`/servers/${id}/system-groups`, data)
|
||||
},
|
||||
|
||||
@ -25,6 +25,9 @@ export const usersApi = {
|
||||
syncServerBindings(id: number, serverBindings: Array<Record<string, unknown>>) {
|
||||
return request.put(`/users/${id}/server-bindings`, { server_bindings: serverBindings })
|
||||
},
|
||||
checkServerUser(params: { server_resource_id: number; username: string }) {
|
||||
return request.get('/users/server-bindings/check', { params })
|
||||
},
|
||||
importUsers(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
@ -17,6 +17,8 @@ const service: AxiosInstance = axios.create({
|
||||
},
|
||||
})
|
||||
|
||||
let authExpiredMessageShown = false
|
||||
|
||||
service.interceptors.request.use((config) => {
|
||||
const token = getToken()
|
||||
const requestUrl = String(config.url || '')
|
||||
@ -36,7 +38,13 @@ service.interceptors.response.use(
|
||||
|
||||
if (status === 401) {
|
||||
removeToken()
|
||||
if (!authExpiredMessageShown) {
|
||||
authExpiredMessageShown = true
|
||||
ElMessage.error('登录已过期,请重新登录')
|
||||
window.setTimeout(() => {
|
||||
authExpiredMessageShown = false
|
||||
}, 1500)
|
||||
}
|
||||
if (location.hash !== '#/login') {
|
||||
location.href = '/#/login'
|
||||
}
|
||||
|
||||
@ -31,6 +31,9 @@
|
||||
<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' plain @click='openDefaultUserGroupsDialog(server)'>默认用户组</el-button>
|
||||
<el-button size='small' plain @click='openDefaultEnvironmentDialog(server)'>默认环境变量</el-button>
|
||||
<el-button size='small' plain @click='openAllUserEnvironmentDialog(server)'>设置所有用户变量</el-button>
|
||||
<el-button size='small' type='danger' @click='removeRow(server)'>删除服务器</el-button>
|
||||
</div>
|
||||
|
||||
@ -143,11 +146,12 @@
|
||||
<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'>
|
||||
<el-table-column label='操作' width='420' 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' @click='openUserEnvironmentDialog(row)'>修改环境变量</el-button>
|
||||
<el-button size='small' type='danger' @click='removeSystemUser(row)'>删除</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@ -202,6 +206,20 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='defaultUserGroupsDialogVisible' :title='`默认用户组 - ${defaultUserGroupsTarget?.display_name || defaultUserGroupsTarget?.name || ""}`' width='520px'>
|
||||
<el-form v-loading='defaultUserGroupsLoading' :model='defaultUserGroupsForm' label-width='95px'>
|
||||
<el-form-item label='用户组'>
|
||||
<el-select v-model='defaultUserGroupsForm.groups' multiple collapse-tags clearable filterable class='w-full'>
|
||||
<el-option v-for='group in defaultUserGroupsOptions' :key='group.groupname' :label='group.groupname' :value='group.groupname' />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='defaultUserGroupsDialogVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='defaultUserGroupsSaving' :disabled='defaultUserGroupsLoading' @click='submitDefaultUserGroups'>保存</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' />
|
||||
@ -260,6 +278,18 @@
|
||||
<el-button type='primary' :loading='updatingBoundPassword' @click='submitBoundPassword'>提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='environmentDialogVisible' :title='environmentDialogTitle' width='640px'>
|
||||
<el-form v-loading='environmentLoading' :model='environmentForm' label-width='110px'>
|
||||
<el-form-item label='环境变量'>
|
||||
<el-input v-model='environmentForm.content' type='textarea' :rows='10' :disabled='environmentLoading' placeholder='例如:export PATH=/opt/bin:$PATH' />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='environmentDialogVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='environmentSaving' :disabled='environmentLoading' @click='submitEnvironment'>保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
|
||||
<el-card v-loading='opsLoading' style="margin-top: 1rem;">
|
||||
@ -381,6 +411,8 @@ const systemUsersDialogVisible = ref(false)
|
||||
const systemUserFormVisible = ref(false)
|
||||
const passwordDialogVisible = ref(false)
|
||||
const boundPasswordDialogVisible = ref(false)
|
||||
const environmentDialogVisible = ref(false)
|
||||
const defaultUserGroupsDialogVisible = ref(false)
|
||||
const protocolDialogVisible = ref(false)
|
||||
const softwareDialogVisible = ref(false)
|
||||
const softwareFormDialogVisible = ref(false)
|
||||
@ -390,6 +422,10 @@ const copyingTempPassword = ref(false)
|
||||
const updatingBoundPassword = ref(false)
|
||||
const systemUsersLoading = ref(false)
|
||||
const systemUsersSaving = ref(false)
|
||||
const environmentLoading = ref(false)
|
||||
const environmentSaving = ref(false)
|
||||
const defaultUserGroupsLoading = ref(false)
|
||||
const defaultUserGroupsSaving = ref(false)
|
||||
const savingSystemUserGroups = reactive<Record<string, boolean>>({})
|
||||
const opsLoading = ref(false)
|
||||
const opsSaving = ref(false)
|
||||
@ -408,27 +444,42 @@ 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 environmentTarget = ref<any | null>(null)
|
||||
const defaultUserGroupsTarget = 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: '', user_api_base_url: '', user_api_token: '', 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: '', default_user_groups: [], 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 passwordForm = reactive<any>({ password: '' })
|
||||
const boundPasswordForm = reactive<any>({ password: '' })
|
||||
const environmentForm = reactive<any>({ mode: 'default', content: '', username: '' })
|
||||
const defaultUserGroupsForm = reactive<any>({ groups: [] })
|
||||
const protocolForm = reactive<any>({ name: '', description: '' })
|
||||
const softwareForm = reactive<any>({ name: '', 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 defaultUserGroupsOptions = 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 environmentDialogTitle = computed(() => {
|
||||
if (environmentForm.mode === 'all') {
|
||||
return `设置所有用户变量 - ${environmentTarget.value?.display_name || environmentTarget.value?.name || ''}`
|
||||
}
|
||||
if (environmentForm.mode === 'user') {
|
||||
return `修改环境变量 - ${environmentForm.username || ''}`
|
||||
}
|
||||
|
||||
return `默认环境变量 - ${environmentTarget.value?.display_name || environmentTarget.value?.name || ''}`
|
||||
})
|
||||
const opsSaveButtonLabel = computed(() => (forceSaveOpsReady.value ? '强制保存' : '保存我的软件选择'))
|
||||
const resourceProtocolOptions = computed<string[]>(() => {
|
||||
return opsProtocols.value
|
||||
@ -469,6 +520,9 @@ function resetForm(): void {
|
||||
internal_ip: '',
|
||||
user_api_base_url: '',
|
||||
user_api_token: '',
|
||||
default_environment_variables: '',
|
||||
all_user_environment_variables: '',
|
||||
default_user_groups: [],
|
||||
asset_id: null,
|
||||
account_id: null,
|
||||
protocol: resourceProtocolOptions.value[0] || '',
|
||||
@ -515,6 +569,9 @@ function openEdit(row: any): void {
|
||||
internal_ip: row.internal_ip,
|
||||
user_api_base_url: row.user_api_base_url || '',
|
||||
user_api_token: '',
|
||||
default_environment_variables: row.default_environment_variables || '',
|
||||
all_user_environment_variables: row.all_user_environment_variables || '',
|
||||
default_user_groups: [...(row.default_user_groups || [])],
|
||||
asset_id: row.asset_id,
|
||||
account_id: row.account_id,
|
||||
protocol: row.protocols?.[0] || 'SSH',
|
||||
@ -544,6 +601,9 @@ async function submit(): Promise<void> {
|
||||
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,
|
||||
default_environment_variables: form.type === 'server' ? form.default_environment_variables || '' : null,
|
||||
all_user_environment_variables: form.type === 'server' ? form.all_user_environment_variables || '' : null,
|
||||
default_user_groups: form.type === 'server' ? form.default_user_groups || [] : [],
|
||||
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,
|
||||
@ -689,7 +749,7 @@ 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 = row.server_username || saved?.account_name || ''
|
||||
useForm.account_name = saved?.account_name || row.server_username || ''
|
||||
useForm.password = saved?.password || ''
|
||||
useForm.remember = Boolean(saved)
|
||||
useForm.last_temp_password = ''
|
||||
@ -722,6 +782,7 @@ async function submitBoundPassword(): Promise<void> {
|
||||
useForm.password = boundPasswordForm.password
|
||||
}
|
||||
ElMessage.success('绑定账号密码已更新')
|
||||
await fetchList()
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '修改绑定账号密码失败')
|
||||
@ -730,6 +791,121 @@ async function submitBoundPassword(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function openDefaultUserGroupsDialog(server: any): Promise<void> {
|
||||
defaultUserGroupsTarget.value = server
|
||||
defaultUserGroupsOptions.value = []
|
||||
defaultUserGroupsForm.groups = [...(server.default_user_groups || [])]
|
||||
defaultUserGroupsDialogVisible.value = true
|
||||
defaultUserGroupsLoading.value = true
|
||||
try {
|
||||
const response: any = await serversApi.systemUsersMeta(server.id)
|
||||
defaultUserGroupsOptions.value = response.data.groups || []
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '加载服务器用户组失败')
|
||||
} finally {
|
||||
defaultUserGroupsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitDefaultUserGroups(): Promise<void> {
|
||||
if (!defaultUserGroupsTarget.value?.id) {
|
||||
return
|
||||
}
|
||||
defaultUserGroupsSaving.value = true
|
||||
try {
|
||||
const groups = Array.from(new Set<string>((defaultUserGroupsForm.groups || []).map((group: string) => String(group))))
|
||||
const response: any = await serversApi.updateDefaultUserGroups(defaultUserGroupsTarget.value.id, groups)
|
||||
defaultUserGroupsTarget.value.default_user_groups = response.data.default_user_groups || groups
|
||||
defaultUserGroupsDialogVisible.value = false
|
||||
ElMessage.success('默认用户组已保存')
|
||||
await fetchList()
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '保存默认用户组失败')
|
||||
} finally {
|
||||
defaultUserGroupsSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openDefaultEnvironmentDialog(server: any): void {
|
||||
environmentTarget.value = server
|
||||
Object.assign(environmentForm, {
|
||||
mode: 'default',
|
||||
username: '',
|
||||
content: server.default_environment_variables || '',
|
||||
})
|
||||
environmentDialogVisible.value = true
|
||||
}
|
||||
|
||||
function openAllUserEnvironmentDialog(server: any): void {
|
||||
environmentTarget.value = server
|
||||
Object.assign(environmentForm, {
|
||||
mode: 'all',
|
||||
username: '',
|
||||
content: server.all_user_environment_variables || '',
|
||||
})
|
||||
environmentDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function openUserEnvironmentDialog(row: any): Promise<void> {
|
||||
if (!systemUsersServer.value?.id) {
|
||||
return
|
||||
}
|
||||
environmentTarget.value = systemUsersServer.value
|
||||
Object.assign(environmentForm, {
|
||||
mode: 'user',
|
||||
username: row.username,
|
||||
content: '',
|
||||
})
|
||||
environmentDialogVisible.value = true
|
||||
environmentLoading.value = true
|
||||
try {
|
||||
const response: any = await serversApi.userEnvironment(systemUsersServer.value.id, row.username)
|
||||
environmentForm.content = response.data.content || ''
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '读取环境变量失败')
|
||||
} finally {
|
||||
environmentLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function submitEnvironment(): Promise<void> {
|
||||
if (!environmentTarget.value?.id) {
|
||||
return
|
||||
}
|
||||
environmentSaving.value = true
|
||||
try {
|
||||
if (environmentForm.mode === 'all') {
|
||||
const response: any = await serversApi.updateAllUserEnvironments(environmentTarget.value.id, environmentForm.content || '')
|
||||
const result = response.data || {}
|
||||
environmentTarget.value.all_user_environment_variables = result.all_user_environment_variables || environmentForm.content || ''
|
||||
const failedUsers = result.failed_users || []
|
||||
if (failedUsers.length) {
|
||||
const failedNames = failedUsers.map((item: any) => item.username || item.message || item.code).filter(Boolean).join('、')
|
||||
ElMessage.warning(`已设置 ${result.updated_count || 0} 个用户,${result.failed_count || failedUsers.length} 个失败:${failedNames}`)
|
||||
} else {
|
||||
ElMessage.success(`已设置所有用户变量,共 ${result.updated_count || 0} 个用户`)
|
||||
}
|
||||
} else if (environmentForm.mode === 'user') {
|
||||
await serversApi.updateUserEnvironment(environmentTarget.value.id, environmentForm.username, environmentForm.content || '')
|
||||
ElMessage.success('用户环境变量已更新')
|
||||
await loadSystemUsersMeta()
|
||||
} else {
|
||||
await serversApi.updateDefaultEnvironment(environmentTarget.value.id, environmentForm.content || '')
|
||||
ElMessage.success('默认环境变量已保存')
|
||||
await fetchList()
|
||||
}
|
||||
environmentDialogVisible.value = false
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '保存环境变量失败')
|
||||
} finally {
|
||||
environmentSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function openSystemUsersDialog(server: any): Promise<void> {
|
||||
systemUsersServer.value = server
|
||||
systemUsersTab.value = 'users'
|
||||
@ -769,7 +945,7 @@ function bindingLabel(username: string): string {
|
||||
|
||||
function openCreateSystemUser(): void {
|
||||
systemUserFormMode.value = 'user'
|
||||
Object.assign(systemUserForm, { username: '', password: '', groups: [], groupname: '' })
|
||||
Object.assign(systemUserForm, { username: '', password: '', groups: [...(systemUsersServer.value?.default_user_groups || [])], groupname: '' })
|
||||
systemUserFormVisible.value = true
|
||||
}
|
||||
|
||||
@ -920,6 +1096,16 @@ async function requestUseResource(action: 'connect' | 'copy'): Promise<{ url: st
|
||||
tempPassword: String(response?.data?.temp_password || ''),
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (Number(error?.code || 0) === 423 && error?.data?.reason === 'server_password_change_required') {
|
||||
boundPasswordTarget.value = {
|
||||
id: error.data.server_resource_id,
|
||||
server_username: error.data.username,
|
||||
}
|
||||
boundPasswordForm.password = ''
|
||||
boundPasswordDialogVisible.value = true
|
||||
ElMessage.warning(error?.message || '请先修改服务器账号密码')
|
||||
return null
|
||||
}
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || (action === 'copy' ? '获取临时密码失败' : '资源访问失败'))
|
||||
return null
|
||||
|
||||
@ -61,23 +61,29 @@
|
||||
</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 :data='serverBindingRows' border size='small' class='mb-3'>
|
||||
<el-table-column label='服务器' min-width='160'>
|
||||
<template #default='{ row }'>{{ serverLabel(row.server_resource_id) }}</template>
|
||||
<template #default='{ row }'>{{ row.server_label }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label='服务器用户名' min-width='160'>
|
||||
<template #default='{ row }'><el-input v-model='row.username' /></template>
|
||||
<el-table-column label='绑定账号' min-width='160'>
|
||||
<template #default='{ row }'>{{ 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 label='状态' min-width='150'>
|
||||
<template #default='{ row }'>
|
||||
<el-tag v-if='row.action === "bind"' type='warning'>待绑定</el-tag>
|
||||
<el-tag v-else-if='row.action === "unbind"' type='info'>待解绑</el-tag>
|
||||
<el-tag v-else-if='row.username' type='success'>已绑定</el-tag>
|
||||
<span v-else>-</span>
|
||||
</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 label='操作' width='160'>
|
||||
<template #default='{ row }'>
|
||||
<div class='btn-gap-8 btn-gap-8--nowrap'>
|
||||
<el-button v-if='row.action === "unbind"' size='small' @click='cancelUnbind(row)'>撤销</el-button>
|
||||
<el-button v-else-if='row.username' size='small' type='danger' plain @click='openUnbindDialog(row)'>解绑</el-button>
|
||||
<el-button v-else size='small' type='primary' plain @click='openBindDialog(row)'>绑定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form>
|
||||
@ -87,6 +93,25 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='bindDialogVisible' :title='`绑定服务器账号 - ${bindingTarget?.server_label || ""}`' width='520px'>
|
||||
<el-form :model='bindForm' label-width='95px'>
|
||||
<el-form-item label='绑定账号'>
|
||||
<el-input v-model='bindForm.username' placeholder='请输入服务器账号' @blur='checkBindUsername' />
|
||||
</el-form-item>
|
||||
<el-alert v-if='bindForm.checked && bindForm.remote_exists' type='success' :closable='false' show-icon title='服务器上已存在该用户,保存后将直接绑定。' />
|
||||
<template v-if='bindForm.checked && !bindForm.remote_exists'>
|
||||
<el-alert type='warning' :closable='false' show-icon title='服务器上不存在该用户,保存时会新建并绑定。' class='mb-3' />
|
||||
<el-form-item label='初始密码'>
|
||||
<el-input v-model='bindForm.password' type='password' show-password placeholder='默认123456,可不填' />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click='bindDialogVisible = false'>取消</el-button>
|
||||
<el-button type='primary' :loading='checkingServerUser' @click='confirmBind'>确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model='permissionDialogVisible' title='用户直授权限分配' width='92vw' top='4vh'>
|
||||
<div class='permission-panel-wrap'>
|
||||
<el-cascader-panel v-model='selectedPermissionNodes' :options='permissionCascader' :props='cascaderProps' class='permission-panel' />
|
||||
@ -163,7 +188,7 @@
|
||||
<script setup lang='ts'>
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { computed, onMounted, reactive, ref } from 'vue'
|
||||
import { permissionsApi } from '@/api/permissions'
|
||||
import { rolesApi } from '@/api/roles'
|
||||
import { serversApi } from '@/api/servers'
|
||||
@ -178,6 +203,7 @@ const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const savingPermissions = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const bindDialogVisible = ref(false)
|
||||
const permissionDialogVisible = ref(false)
|
||||
const importDialogVisible = ref(false)
|
||||
const batchAssignDialogVisible = ref(false)
|
||||
@ -185,11 +211,14 @@ const editingId = ref<number | null>(null)
|
||||
const permissionTargetUserId = ref<number | null>(null)
|
||||
const importing = ref(false)
|
||||
const batchAssigning = ref(false)
|
||||
const checkingServerUser = ref(false)
|
||||
const selectedUserIds = ref<number[]>([])
|
||||
const rows = ref<any[]>([])
|
||||
const roleOptions = ref<any[]>([])
|
||||
const permissionOptions = ref<any[]>([])
|
||||
const serverOptions = ref<any[]>([])
|
||||
const serverBindingRows = ref<any[]>([])
|
||||
const bindingTarget = ref<any | null>(null)
|
||||
const selectedPermissionNodes = ref<Array<number | string>>([])
|
||||
const batchAssignPermissionNodes = ref<Array<number | string>>([])
|
||||
const importFile = ref<File | null>(null)
|
||||
@ -200,7 +229,8 @@ 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: [], server_ids: [], server_bindings: [] })
|
||||
const form = reactive<any>({ nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
|
||||
const bindForm = reactive<any>({ username: '', password: '', checked: false, remote_exists: false })
|
||||
const batchAssignForm = reactive<{ role_ids: number[] }>({ role_ids: [] })
|
||||
const cascaderProps = { multiple: true, emitPath: false, checkStrictly: false }
|
||||
const permissionCascader = computed(() => buildPermissionCascader(permissionOptions.value))
|
||||
@ -281,7 +311,8 @@ 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: [], server_ids: [], server_bindings: [] })
|
||||
Object.assign(form, { nickname: '', email: '', phone: '', password: '', force_password_change: false, role_ids: [] })
|
||||
resetServerBindingRows([])
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -294,14 +325,8 @@ 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: '',
|
||||
})),
|
||||
})
|
||||
resetServerBindingRows(row.server_user_bindings || [])
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
@ -315,16 +340,19 @@ async function submit(): Promise<void> {
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = { ...form }
|
||||
payload.server_bindings = form.server_bindings.map((item: any) => ({
|
||||
payload.server_bindings = serverBindingRows.value.filter((item: any) => item.action === 'bind').map((item: any) => ({
|
||||
server_resource_id: item.server_resource_id,
|
||||
username: item.username,
|
||||
create_remote: Boolean(item.create_remote),
|
||||
password: item.password || undefined,
|
||||
}))
|
||||
payload.server_unbindings = serverBindingRows.value.filter((item: any) => item.action === 'unbind').map((item: any) => ({
|
||||
server_resource_id: item.server_resource_id,
|
||||
username: item.original_username || item.username || undefined,
|
||||
delete_remote: Boolean(item.delete_remote),
|
||||
}))
|
||||
if (!payload.password) {
|
||||
delete payload.password
|
||||
}
|
||||
delete payload.server_ids
|
||||
|
||||
if (editingId.value) {
|
||||
await usersApi.update(editingId.value, payload)
|
||||
@ -344,32 +372,111 @@ 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
|
||||
}
|
||||
function resetServerBindingRows(bindings: any[]): void {
|
||||
const bindingMap = new Map<number, any>(bindings.map((item: any) => [Number(item.server_resource_id), item]))
|
||||
serverBindingRows.value = serverOptions.value.map((server: any) => {
|
||||
const binding = bindingMap.get(Number(server.id))
|
||||
|
||||
return {
|
||||
server_resource_id: serverId,
|
||||
username: defaultUsername,
|
||||
create_remote: true,
|
||||
server_resource_id: server.id,
|
||||
server_label: server.display_name || server.name || `服务器#${server.id}`,
|
||||
username: binding?.username || '',
|
||||
original_username: binding?.username || '',
|
||||
password: '',
|
||||
action: '',
|
||||
delete_remote: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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 openBindDialog(row: any): void {
|
||||
bindingTarget.value = row
|
||||
Object.assign(bindForm, {
|
||||
username: row.username || normalizeServerUsername(form.nickname || form.email || `user${editingId.value || ''}`),
|
||||
password: '',
|
||||
checked: false,
|
||||
remote_exists: false,
|
||||
})
|
||||
bindDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function checkBindUsername(): Promise<void> {
|
||||
if (!bindingTarget.value?.server_resource_id || !bindForm.username?.trim()) {
|
||||
bindForm.checked = false
|
||||
bindForm.remote_exists = false
|
||||
return
|
||||
}
|
||||
|
||||
checkingServerUser.value = true
|
||||
try {
|
||||
const response: any = await usersApi.checkServerUser({
|
||||
server_resource_id: bindingTarget.value.server_resource_id,
|
||||
username: bindForm.username,
|
||||
})
|
||||
bindForm.checked = true
|
||||
bindForm.remote_exists = Boolean(response.data.exists)
|
||||
} catch (error: any) {
|
||||
const first = error?.errors ? (Object.values(error.errors)[0] as string[])?.[0] : null
|
||||
ElMessage.error(first || error?.message || '检查服务器账号失败')
|
||||
} finally {
|
||||
checkingServerUser.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmBind(): Promise<void> {
|
||||
if (!bindingTarget.value) {
|
||||
return
|
||||
}
|
||||
if (!bindForm.username?.trim()) {
|
||||
ElMessage.warning('请输入服务器账号')
|
||||
return
|
||||
}
|
||||
if (!bindForm.checked) {
|
||||
await checkBindUsername()
|
||||
}
|
||||
|
||||
Object.assign(bindingTarget.value, {
|
||||
username: bindForm.username,
|
||||
password: bindForm.remote_exists ? '' : (bindForm.password || '123456'),
|
||||
action: 'bind',
|
||||
delete_remote: false,
|
||||
})
|
||||
bindDialogVisible.value = false
|
||||
}
|
||||
|
||||
async function openUnbindDialog(row: any): Promise<void> {
|
||||
const result = await ElMessageBox.confirm(
|
||||
`是否同时删除服务器用户 ${row.username}?只删除用户,不删除用户文件夹。`,
|
||||
'解绑服务器账号',
|
||||
{
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: '删除服务器用户',
|
||||
cancelButtonText: '不删除,仅解绑',
|
||||
},
|
||||
).then(() => true).catch((action) => {
|
||||
if (action === 'cancel') {
|
||||
return false
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
if (result === null) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.assign(row, {
|
||||
action: 'unbind',
|
||||
delete_remote: result,
|
||||
})
|
||||
}
|
||||
|
||||
function cancelUnbind(row: any): void {
|
||||
Object.assign(row, {
|
||||
action: '',
|
||||
delete_remote: false,
|
||||
})
|
||||
}
|
||||
|
||||
function serverBindingsLabel(row: any): string {
|
||||
const bindings = row.server_user_bindings || []
|
||||
@ -382,11 +489,6 @@ function serverBindingsLabel(row: any): string {
|
||||
.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
|
||||
|
||||
@ -62,7 +62,9 @@ router.beforeEach(async (to) => {
|
||||
removeToken()
|
||||
authStore.clearAuth()
|
||||
loaded = false
|
||||
if (code !== 401) {
|
||||
ElMessage.error('登录状态无效,请重新登录')
|
||||
}
|
||||
return '/login'
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user