diff --git a/components.d.ts b/components.d.ts index d9d92cf..1df8123 100644 --- a/components.d.ts +++ b/components.d.ts @@ -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'] diff --git a/src/api/servers.ts b/src/api/servers.ts index 7836d64..7c6c596 100644 --- a/src/api/servers.ts +++ b/src/api/servers.ts @@ -19,6 +19,30 @@ export const serversApi = { syncUserPermissions(id: number, users: Array>, 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) { + 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) { + return request.patch(`/servers/${id}/system-users/${encodeURIComponent(username)}/password`, data) + }, + updateBoundSystemUserPassword(id: number, data: Record) { + return request.patch(`/servers/${id}/bound-system-user/password`, data) + }, + createSystemGroup(id: number, data: Record) { + 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) { return request.post(`/servers/${id}/use`, data) }, diff --git a/src/api/users.ts b/src/api/users.ts index 193bf1a..cb4836e 100644 --- a/src/api/users.ts +++ b/src/api/users.ts @@ -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>) { + return request.put(`/users/${id}/server-bindings`, { server_bindings: serverBindings }) + }, importUsers(file: File) { const formData = new FormData() formData.append('file', file) diff --git a/src/pages/ServersPage.vue b/src/pages/ServersPage.vue index c2ba9e0..4a95931 100644 --- a/src/pages/ServersPage.vue +++ b/src/pages/ServersPage.vue @@ -21,6 +21,8 @@ {{ server.display_name || server.name }} {{ server.name }} {{ server.internal_ip }} + {{ server.server_username }} + 改密 @@ -28,6 +30,7 @@ 编辑服务器 添加资源 分配用户权限 + 管理用户及用户组 删除服务器 @@ -42,6 +45,9 @@ + + + @@ -86,6 +92,12 @@ + + + + + + @@ -106,6 +118,92 @@ + +
+ 新增服务器用户 + 新增用户组 + 刷新 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + @@ -149,6 +247,21 @@ 连接并访问 + + + + +
{{ boundPasswordTarget?.server_username || '-' }}
+
+ + + +
+ +
@@ -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>({}) const opsLoading = ref(false) const opsSaving = ref(false) const forceSaveOpsReady = ref(false) @@ -290,17 +411,29 @@ const permissionMatrixRows = ref([]) const permissionResourceOptions = ref([]) const activeServers = ref([]) const useTargetRow = ref(null) +const systemUsersServer = ref(null) +const passwordTarget = ref(null) +const boundPasswordTarget = ref(null) const opsProtocols = ref([]) const softwareProtocol = ref(null) const opsLinkLoading = ref>({}) const opsSavedSelections = reactive>({}) const typeOptions = [{ label: '服务器', value: 'server' }, { label: '资源', value: 'resource' }] -const form = reactive({ 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({ 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({ username: '', password: '', groups: [], shell: '/bin/bash', home_dir: '', groupname: '' }) const useForm = reactive({ protocol: 'SSH', account_name: '', password: '', remember: false, last_temp_password: '' }) +const passwordForm = reactive({ password: '' }) +const boundPasswordForm = reactive({ password: '' }) const protocolForm = reactive({ name: '', bastion_protocol_id: 2, description: '', sort: 0, is_active: true }) const softwareForm = reactive({ name: '', client_path: '', sort: 0, is_active: true }) const opsSelections = reactive>({}) +const systemUserGroupSelections = reactive>({}) +const systemUsers = ref([]) +const systemGroups = ref([]) +const systemUserBindings = ref([]) +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(() => { @@ -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 { 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 { + 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 { + systemUsersServer.value = server + systemUsersTab.value = 'users' + systemUsersDialogVisible.value = true + await loadSystemUsersMeta() +} + +async function loadSystemUsersMeta(): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { 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; diff --git a/src/pages/UsersPage.vue b/src/pages/UsersPage.vue index 26a7057..7a11fed 100644 --- a/src/pages/UsersPage.vue +++ b/src/pages/UsersPage.vue @@ -20,6 +20,9 @@ + + + @@ -45,7 +48,7 @@ @size-change='handleSizeChange' /> - + @@ -57,6 +60,26 @@
+ 服务器账号绑定 + + + + + + + + + + + + + + + + + + +