✨ 添加主机身份类型.
This commit is contained in:
@@ -6,10 +6,17 @@
|
||||
|
||||
`2024-04-` `release`
|
||||
|
||||
* 🔨 优化 定时删除未引用的 `tag`
|
||||
* 🔨 优化 命令执行日志时间不自增
|
||||
* 🐞 修复 用户列表用户名显示错误
|
||||
* 🌈 新增 定时删除未引用的 `tag`
|
||||
* 🌈 新增 执行命令时可使用脚本文件执行
|
||||
* 🌈 新增 主机身份添加类型字段
|
||||
* 🔨 优化 文件传输列表进度显示
|
||||
* 🔨 优化 命令执行日志持续时间
|
||||
* 🔨 优化 tracker 监听文件可配置 `app.tracker`
|
||||
* 🔨 优化 sftp 上传文件重复处理可配置 `app.sftp`
|
||||
* 🔨 优化 用户状态调整交互逻辑
|
||||
* 🔨 优化 角色状态调整交互逻辑
|
||||
* 🔨 删除 用户锁定状态
|
||||
|
||||
[如何升级](/update/v1.0.5.md)
|
||||
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
## 功能排期 ⏳
|
||||
|
||||
* 优化文件传输列表进度显示
|
||||
* 主机身份类型
|
||||
* 终端断开连接后回车重新连接
|
||||
* 使用文件执行命令
|
||||
* 管理员也需要自行授权资产
|
||||
* 快捷命令导入
|
||||
* 文件夹书签
|
||||
* 批量上传
|
||||
* 站内消息
|
||||
|
||||
@@ -3,9 +3,33 @@
|
||||
> sql 脚本 - DDL
|
||||
|
||||
```sql
|
||||
ALTER TABLE `system_user`
|
||||
MODIFY COLUMN `status` tinyint(0) NULL DEFAULT 1 COMMENT '用户状态 0停用 1启用' AFTER `email`;
|
||||
|
||||
ALTER TABLE `host_identity`
|
||||
ADD COLUMN `type` char(12) NULL COMMENT '类型' AFTER `name`;
|
||||
|
||||
ALTER TABLE `exec_log`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_job`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_template`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_host_log`
|
||||
ADD COLUMN `script_path` varchar(512) NULL COMMENT '脚本路径' AFTER `log_path`;
|
||||
```
|
||||
|
||||
> sql 脚本 - DML
|
||||
|
||||
```sql
|
||||
-- 初始化主机身份类型
|
||||
UPDATE `host_identity` SET type = IF(key_id IS NOT NULL, 'KEY', 'PASSWORD');
|
||||
-- 重新设置用户状态
|
||||
UPDATE `system_user` SET status = 0 WHERE status = 2;
|
||||
DELETE FROM `dict_value` WHERE id = 19;
|
||||
-- 设置主机配置中的 osType
|
||||
UPDATE host_config SET config = JSON_SET(config, '$.osType', 'LINUX') WHERE type = 'ssh' AND deleted = 0;
|
||||
```
|
||||
|
||||
@@ -3,22 +3,9 @@
|
||||
> sql 脚本 - DDL
|
||||
|
||||
```sql
|
||||
ALTER TABLE `exec_log`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_job`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_template`
|
||||
ADD COLUMN `script_exec` tinyint(0) NULL DEFAULT 0 COMMENT '是否使用脚本执行' AFTER `timeout`;
|
||||
|
||||
ALTER TABLE `exec_host_log`
|
||||
ADD COLUMN `script_path` varchar(512) NULL COMMENT '脚本路径' AFTER `log_path`;
|
||||
```
|
||||
|
||||
> sql 脚本 - DML
|
||||
|
||||
```sql
|
||||
-- 设置主机配置中的 osType
|
||||
UPDATE host_config SET config = JSON_SET(config, '$.osType', 'LINUX') WHERE type = 'ssh' AND deleted = 0;
|
||||
```
|
||||
|
||||
@@ -31,6 +31,10 @@ public class HostIdentityDO extends BaseDO {
|
||||
@TableField("name")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "类型")
|
||||
@TableField("type")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
@TableField("username")
|
||||
private String username;
|
||||
|
||||
@@ -30,6 +30,9 @@ public class HostIdentityCacheDTO implements LongCacheIdModel, Serializable {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
|
||||
@@ -29,6 +29,11 @@ public class HostIdentityCreateRequest implements Serializable {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 12)
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 128)
|
||||
@Schema(description = "用户名")
|
||||
|
||||
@@ -31,6 +31,10 @@ public class HostIdentityQueryRequest extends PageRequest {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@Size(max = 12)
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Size(max = 128)
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@@ -34,6 +34,11 @@ public class HostIdentityUpdateRequest implements UpdatePasswordAction {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 12)
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@NotBlank
|
||||
@Size(max = 128)
|
||||
@Schema(description = "用户名")
|
||||
|
||||
@@ -31,6 +31,9 @@ public class HostIdentityVO implements Serializable {
|
||||
@Schema(description = "名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.orion.ops.module.asset.enums;
|
||||
|
||||
/**
|
||||
* 主机身份类型
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023/9/21 19:01
|
||||
*/
|
||||
public enum HostIdentityTypeEnum {
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
PASSWORD,
|
||||
|
||||
/**
|
||||
* 秘钥
|
||||
*/
|
||||
KEY,
|
||||
|
||||
;
|
||||
|
||||
public static HostIdentityTypeEnum of(String type) {
|
||||
if (type == null) {
|
||||
return null;
|
||||
}
|
||||
for (HostIdentityTypeEnum value : values()) {
|
||||
if (value.name().equals(type)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import com.orion.ops.module.asset.entity.request.host.HostIdentityCreateRequest;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostIdentityQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostIdentityUpdateRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostIdentityVO;
|
||||
import com.orion.ops.module.asset.enums.HostIdentityTypeEnum;
|
||||
import com.orion.ops.module.asset.service.HostIdentityService;
|
||||
import com.orion.ops.module.infra.api.DataExtraApi;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -64,7 +65,7 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
public Long createHostIdentity(HostIdentityCreateRequest request) {
|
||||
log.info("HostIdentityService-createHostIdentity request: {}", JSON.toJSONString(request));
|
||||
// 检查秘钥是否存在
|
||||
this.checkKeyIdPresent(request.getKeyId());
|
||||
this.checkCreateParams(request);
|
||||
// 转换
|
||||
HostIdentityDO record = HostIdentityConvert.MAPPER.to(request);
|
||||
// 查询数据是否冲突
|
||||
@@ -85,19 +86,25 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
@Override
|
||||
public Integer updateHostIdentityById(HostIdentityUpdateRequest request) {
|
||||
log.info("HostIdentityService-updateHostIdentityById request: {}", JSON.toJSONString(request));
|
||||
// 查询
|
||||
// 验证参数
|
||||
Long id = Valid.notNull(request.getId(), ErrorMessage.ID_MISSING);
|
||||
HostIdentityTypeEnum type = Valid.valid(HostIdentityTypeEnum::of, request.getType());
|
||||
if (HostIdentityTypeEnum.KEY.equals(type)) {
|
||||
// 秘钥认证
|
||||
this.checkKeyId(request.getKeyId());
|
||||
}
|
||||
// 查询主机身份
|
||||
HostIdentityDO record = hostIdentityDAO.selectById(id);
|
||||
Valid.notNull(record, ErrorMessage.DATA_ABSENT);
|
||||
// 检查秘钥是否存在
|
||||
this.checkKeyIdPresent(request.getKeyId());
|
||||
// 转换
|
||||
HostIdentityDO updateRecord = HostIdentityConvert.MAPPER.to(request);
|
||||
// 查询数据是否冲突
|
||||
this.checkHostIdentityPresent(updateRecord);
|
||||
// 设置密码
|
||||
String newPassword = PasswordModifier.getEncryptNewPassword(request);
|
||||
updateRecord.setPassword(newPassword);
|
||||
if (HostIdentityTypeEnum.PASSWORD.equals(type)) {
|
||||
// 设置密码
|
||||
String newPassword = PasswordModifier.getEncryptNewPassword(request);
|
||||
updateRecord.setPassword(newPassword);
|
||||
}
|
||||
// 更新
|
||||
LambdaUpdateWrapper<HostIdentityDO> wrapper = Wrappers.<HostIdentityDO>lambdaUpdate()
|
||||
.set(HostIdentityDO::getKeyId, request.getKeyId())
|
||||
@@ -105,10 +112,7 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
int effect = hostIdentityDAO.update(updateRecord, wrapper);
|
||||
log.info("HostIdentityService-updateHostIdentityById effect: {}", effect);
|
||||
// 删除缓存
|
||||
if (!record.getName().equals(updateRecord.getName()) ||
|
||||
!record.getUsername().equals(updateRecord.getUsername())) {
|
||||
RedisMaps.delete(HostCacheKeyDefine.HOST_IDENTITY);
|
||||
}
|
||||
RedisMaps.delete(HostCacheKeyDefine.HOST_IDENTITY);
|
||||
return effect;
|
||||
}
|
||||
|
||||
@@ -155,6 +159,7 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
}
|
||||
// 设置秘钥名称
|
||||
List<Long> keyIdList = dataGrid.stream()
|
||||
.filter(s -> HostIdentityTypeEnum.KEY.name().equals(s.getType()))
|
||||
.map(HostIdentityVO::getKeyId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
@@ -212,14 +217,28 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查秘钥是否存在
|
||||
* 检查创建参数
|
||||
*
|
||||
* @param request request
|
||||
*/
|
||||
private void checkCreateParams(HostIdentityCreateRequest request) {
|
||||
HostIdentityTypeEnum type = Valid.valid(HostIdentityTypeEnum::of, request.getType());
|
||||
if (HostIdentityTypeEnum.PASSWORD.equals(type)) {
|
||||
// 密码认证
|
||||
Valid.notBlank(request.getPassword(), ErrorMessage.PARAM_MISSING);
|
||||
} else if (HostIdentityTypeEnum.KEY.equals(type)) {
|
||||
// 秘钥认证
|
||||
this.checkKeyId(request.getKeyId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 keyId 是否存在
|
||||
*
|
||||
* @param keyId keyId
|
||||
*/
|
||||
private void checkKeyIdPresent(Long keyId) {
|
||||
if (keyId == null) {
|
||||
return;
|
||||
}
|
||||
private void checkKeyId(Long keyId) {
|
||||
Valid.notNull(keyId, ErrorMessage.PARAM_MISSING);
|
||||
Valid.notNull(hostKeyDAO.selectById(keyId), ErrorMessage.KEY_ABSENT);
|
||||
}
|
||||
|
||||
@@ -234,6 +253,7 @@ public class HostIdentityServiceImpl implements HostIdentityService {
|
||||
return hostIdentityDAO.wrapper()
|
||||
.eq(HostIdentityDO::getId, request.getId())
|
||||
.like(HostIdentityDO::getName, request.getName())
|
||||
.eq(HostIdentityDO::getType, request.getType())
|
||||
.like(HostIdentityDO::getUsername, request.getUsername())
|
||||
.eq(HostIdentityDO::getKeyId, request.getKeyId())
|
||||
.and(Strings.isNotEmpty(searchValue), c -> c
|
||||
|
||||
@@ -243,52 +243,62 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
||||
private HostTerminalConnectDTO getHostConnectInfo(HostDO host,
|
||||
HostSshConfigModel config,
|
||||
HostSshExtraModel extra) {
|
||||
// 获取认证方式
|
||||
HostSshAuthTypeEnum authType = HostSshAuthTypeEnum.of(config.getAuthType());
|
||||
HostExtraSshAuthTypeEnum extraAuthType = Optional.ofNullable(extra)
|
||||
.map(HostSshExtraModel::getAuthType)
|
||||
.map(HostExtraSshAuthTypeEnum::of)
|
||||
.orElse(HostExtraSshAuthTypeEnum.DEFAULT);
|
||||
if (HostExtraSshAuthTypeEnum.CUSTOM_KEY.equals(extraAuthType)) {
|
||||
// 自定义秘钥
|
||||
authType = HostSshAuthTypeEnum.KEY;
|
||||
config.setKeyId(extra.getKeyId());
|
||||
if (extra.getUsername() != null) {
|
||||
config.setUsername(extra.getUsername());
|
||||
}
|
||||
} else if (HostExtraSshAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) {
|
||||
// 自定义身份
|
||||
authType = HostSshAuthTypeEnum.IDENTITY;
|
||||
config.setIdentityId(extra.getIdentityId());
|
||||
}
|
||||
Long keyId = null;
|
||||
// 填充认证信息
|
||||
HostTerminalConnectDTO conn = new HostTerminalConnectDTO();
|
||||
conn.setHostId(host.getId());
|
||||
conn.setHostName(host.getName());
|
||||
conn.setHostAddress(host.getAddress());
|
||||
conn.setPort(config.getPort());
|
||||
conn.setTimeout(config.getConnectTimeout());
|
||||
conn.setCharset(config.getCharset());
|
||||
conn.setFileNameCharset(config.getFileNameCharset());
|
||||
conn.setFileContentCharset(config.getFileContentCharset());
|
||||
conn.setTimeout(config.getConnectTimeout());
|
||||
conn.setUsername(config.getUsername());
|
||||
// 填充身份信息
|
||||
if (HostSshAuthTypeEnum.PASSWORD.equals(authType)) {
|
||||
conn.setPassword(config.getPassword());
|
||||
} else if (HostSshAuthTypeEnum.KEY.equals(authType)) {
|
||||
// 秘钥认证
|
||||
keyId = config.getKeyId();
|
||||
} else if (HostSshAuthTypeEnum.IDENTITY.equals(authType)) {
|
||||
|
||||
// 获取自定义认证方式
|
||||
HostExtraSshAuthTypeEnum extraAuthType = Optional.ofNullable(extra)
|
||||
.map(HostSshExtraModel::getAuthType)
|
||||
.map(HostExtraSshAuthTypeEnum::of)
|
||||
.orElse(null);
|
||||
if (HostExtraSshAuthTypeEnum.CUSTOM_KEY.equals(extraAuthType)) {
|
||||
// 自定义秘钥
|
||||
config.setAuthType(HostSshAuthTypeEnum.KEY.name());
|
||||
config.setKeyId(extra.getKeyId());
|
||||
if (extra.getUsername() != null) {
|
||||
config.setUsername(extra.getUsername());
|
||||
}
|
||||
} else if (HostExtraSshAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) {
|
||||
// 自定义身份
|
||||
config.setAuthType(HostSshAuthTypeEnum.IDENTITY.name());
|
||||
config.setIdentityId(extra.getIdentityId());
|
||||
}
|
||||
|
||||
// 身份认证
|
||||
HostSshAuthTypeEnum authType = HostSshAuthTypeEnum.of(config.getAuthType());
|
||||
if (HostSshAuthTypeEnum.IDENTITY.equals(authType)) {
|
||||
// 身份认证
|
||||
HostIdentityDO identity = hostIdentityDAO.selectById(config.getIdentityId());
|
||||
Valid.notNull(identity, ErrorMessage.IDENTITY_ABSENT);
|
||||
keyId = identity.getKeyId();
|
||||
conn.setUsername(identity.getUsername());
|
||||
conn.setPassword(identity.getPassword());
|
||||
config.setUsername(identity.getUsername());
|
||||
HostIdentityTypeEnum identityType = HostIdentityTypeEnum.of(identity.getType());
|
||||
if (HostIdentityTypeEnum.PASSWORD.equals(identityType)) {
|
||||
// 密码类型
|
||||
authType = HostSshAuthTypeEnum.PASSWORD;
|
||||
config.setPassword(identity.getPassword());
|
||||
} else if (HostIdentityTypeEnum.KEY.equals(identityType)) {
|
||||
// 秘钥类型
|
||||
authType = HostSshAuthTypeEnum.KEY;
|
||||
config.setKeyId(identity.getKeyId());
|
||||
}
|
||||
}
|
||||
// 设置秘钥信息
|
||||
if (keyId != null) {
|
||||
|
||||
// 填充认证信息
|
||||
conn.setUsername(config.getUsername());
|
||||
if (HostSshAuthTypeEnum.PASSWORD.equals(authType)) {
|
||||
// 密码认证
|
||||
conn.setPassword(config.getPassword());
|
||||
} else if (HostSshAuthTypeEnum.KEY.equals(authType)) {
|
||||
// 秘钥认证
|
||||
Long keyId = config.getKeyId();
|
||||
HostKeyDO key = hostKeyDAO.selectById(keyId);
|
||||
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
||||
conn.setKeyId(keyId);
|
||||
@@ -296,7 +306,6 @@ public class HostTerminalServiceImpl implements HostTerminalService {
|
||||
conn.setPrivateKey(key.getPrivateKey());
|
||||
conn.setPrivateKeyPassword(key.getPassword());
|
||||
}
|
||||
// 连接
|
||||
return conn;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import axios from 'axios';
|
||||
*/
|
||||
export interface HostIdentityCreateRequest {
|
||||
name?: string;
|
||||
type?: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
keyId?: number;
|
||||
@@ -27,6 +28,7 @@ export interface HostIdentityQueryRequest extends Pagination {
|
||||
searchValue?: string;
|
||||
id?: number;
|
||||
name?: string;
|
||||
type?: string;
|
||||
username?: string;
|
||||
keyId?: number;
|
||||
}
|
||||
@@ -37,6 +39,7 @@ export interface HostIdentityQueryRequest extends Pagination {
|
||||
export interface HostIdentityQueryResponse extends TableData {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
username: string;
|
||||
password: string;
|
||||
keyId: number;
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
// 定时查询执行状态
|
||||
if (record.status === execStatus.WAITING ||
|
||||
record.status === execStatus.RUNNING) {
|
||||
// 等待一秒后先查询一下状态
|
||||
setTimeout(fetchTaskStatus, 1000);
|
||||
// 注册状态轮询
|
||||
statusIntervalId.value = setInterval(fetchTaskStatus, 5000);
|
||||
}
|
||||
@@ -92,8 +94,8 @@
|
||||
if (hostStatus) {
|
||||
host.status = hostStatus.status;
|
||||
host.startTime = hostStatus.startTime;
|
||||
// 使用时间
|
||||
host.finishTime = host.finishTime || Date.now();
|
||||
// 结束时间绑定了使用时间 如果未完成则使用当前时间
|
||||
host.finishTime = hostStatus.finishTime || Date.now();
|
||||
host.exitStatus = hostStatus.exitStatus;
|
||||
host.errorMessage = hostStatus.errorMessage;
|
||||
}
|
||||
|
||||
@@ -15,11 +15,24 @@
|
||||
:pagination="false"
|
||||
:bordered="false"
|
||||
@row-click="clickRow">
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<a-tag color="arcoblue" v-if="record.keyId">
|
||||
{{ hostKeys.find(s => s.id === record.keyId)?.name }}
|
||||
</a-tag>
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === 'KEY'">
|
||||
<a-tag color="arcoblue" v-if="record.keyId">
|
||||
{{ hostKeys.find(s => s.id === record.keyId)?.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</grant-layout>
|
||||
@@ -38,11 +51,12 @@
|
||||
import type { HostKeyQueryResponse } from '@/api/asset/host-key';
|
||||
import { ref, onMounted } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { getAuthorizedHostIdentity, grantHostIdentity } from '@/api/asset/asset-data-grant';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { hostIdentityColumns } from '../types/table.columns';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { useRowSelection } from '@/types/table';
|
||||
import { getAuthorizedHostIdentity, grantHostIdentity } from '@/api/asset/asset-data-grant';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { hostIdentityColumns } from '../types/table.columns';
|
||||
import { identityTypeKey } from '../types/const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import GrantLayout from './grant-layout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -51,6 +65,7 @@
|
||||
|
||||
const cacheStore = useCacheStore();
|
||||
const rowSelection = useRowSelection();
|
||||
const { getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const selectedKeys = ref<Array<number>>([]);
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onBeforeMount, onUnmounted, ref } from 'vue';
|
||||
import { useCacheStore } from '@/store';
|
||||
import { GrantTabs } from './types/const';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { GrantTabs, dictKeys } from './types/const';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
@@ -35,9 +35,10 @@
|
||||
|
||||
const activeKey = ref();
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('users', 'roles', 'hosts', 'hostGroups', 'hostKeys', 'hostIdentities');
|
||||
// 加载字典项
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 跳转到指定页
|
||||
@@ -48,6 +49,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
cacheStore.reset('users', 'roles', 'hosts', 'hostGroups', 'hostKeys', 'hostIdentities');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -73,3 +73,9 @@ export const GrantTabs = [
|
||||
component: HostIdentityGrant
|
||||
},
|
||||
];
|
||||
|
||||
// 身份类型 字典项
|
||||
export const identityTypeKey = 'hostIdentityType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [identityTypeKey];
|
||||
|
||||
@@ -14,11 +14,14 @@ export const hostKeyColumns = [
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
@@ -27,6 +30,7 @@ export const hostKeyColumns = [
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
@@ -46,23 +50,23 @@ export const hostIdentityColumns = [
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 98,
|
||||
}, {
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '主机秘钥',
|
||||
dataIndex: 'keyId',
|
||||
slotName: 'keyId',
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
}, {
|
||||
title: '修改时间',
|
||||
dataIndex: 'updateTime',
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(identityTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" allow-clear />
|
||||
@@ -68,6 +75,12 @@
|
||||
<template #title="{ record }">
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 用户名 -->
|
||||
<template #username="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.username)">
|
||||
@@ -76,13 +89,14 @@
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<template v-if="record.keyId">
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === IdentityType.KEY">
|
||||
<!-- 可查看详情 -->
|
||||
<a-tooltip v-if="hasAnyPermission(['asset:host-key:detail', 'asset:host-key:update'])"
|
||||
content="点击查看详情">
|
||||
<a-tag :checked="true"
|
||||
checkable
|
||||
@click="emits('openKeyView',{id: record.keyId})">
|
||||
@click="emits('openKeyView', { id: record.keyId })">
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
@@ -91,6 +105,10 @@
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 拓展操作 -->
|
||||
<template #extra="{ record }">
|
||||
@@ -151,8 +169,10 @@
|
||||
import { deleteHostIdentity, getHostIdentityPage } from '@/api/asset/host-identity';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { useDictStore } from '@/store';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openKeyView']);
|
||||
@@ -161,6 +181,7 @@
|
||||
|
||||
const cardColLayout = useColLayout();
|
||||
const pagination = usePagination();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { hasAnyPermission } = usePermission();
|
||||
|
||||
@@ -168,6 +189,7 @@
|
||||
const formModel = reactive<HostIdentityQueryRequest>({
|
||||
searchValue: undefined,
|
||||
id: undefined,
|
||||
type: undefined,
|
||||
name: undefined,
|
||||
username: undefined,
|
||||
keyId: undefined,
|
||||
|
||||
@@ -23,12 +23,20 @@
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-radio-group v-model="formModel.type"
|
||||
type="button"
|
||||
class="usn"
|
||||
:options="toRadioOptions(identityTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- 用户密码 -->
|
||||
<a-form-item field="password"
|
||||
<a-form-item v-if="formModel.type === IdentityType.PASSWORD"
|
||||
field="password"
|
||||
label="用户密码"
|
||||
:rules="passwordRules">
|
||||
<a-input-password v-model="formModel.password"
|
||||
@@ -42,10 +50,10 @@
|
||||
checked-text="使用新密码"
|
||||
unchecked-text="使用原密码" />
|
||||
</a-form-item>
|
||||
<!-- 秘钥id -->
|
||||
<a-form-item field="keyId"
|
||||
label="主机秘钥"
|
||||
extra="密码和秘钥二选一 优先使用秘钥">
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item v-if="formModel.type === IdentityType.KEY"
|
||||
field="keyId"
|
||||
label="主机秘钥">
|
||||
<host-key-selector v-model="formModel.keyId" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -68,8 +76,11 @@
|
||||
import formRules from '../types/form.rules';
|
||||
import { createHostIdentity, updateHostIdentity } from '@/api/asset/host-identity';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const { toRadioOptions, getDictValue } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
@@ -79,6 +90,7 @@
|
||||
const defaultForm = (): HostIdentityUpdateRequest => {
|
||||
return {
|
||||
id: undefined,
|
||||
type: IdentityType.PASSWORD,
|
||||
name: undefined,
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
@@ -139,15 +151,6 @@
|
||||
return false;
|
||||
}
|
||||
if (isAddHandle.value) {
|
||||
if (!formModel.value.password && !formModel.value.keyId) {
|
||||
formRef.value.setFields({
|
||||
password: {
|
||||
status: 'error',
|
||||
message: '创建时密码和秘钥不能同时为空'
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 新增
|
||||
await createHostIdentity(formModel.value);
|
||||
Message.success('创建成功');
|
||||
|
||||
@@ -17,6 +17,13 @@
|
||||
<a-form-item field="name" label="名称">
|
||||
<a-input v-model="formModel.name" placeholder="请输入名称" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(identityTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item field="username" label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" allow-clear />
|
||||
@@ -80,6 +87,12 @@
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 类型 -->
|
||||
<template #type="{ record }">
|
||||
<a-tag :color="getDictValue(identityTypeKey, record.type, 'color')">
|
||||
{{ getDictValue(identityTypeKey, record.type) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 用户名 -->
|
||||
<template #username="{ record }">
|
||||
<span class="span-blue text-copy" @click="copy(record.username)">
|
||||
@@ -88,13 +101,14 @@
|
||||
</template>
|
||||
<!-- 秘钥名称 -->
|
||||
<template #keyId="{ record }">
|
||||
<template v-if="record.keyId">
|
||||
<!-- 有秘钥 -->
|
||||
<template v-if="record.keyId && record.type === IdentityType.KEY">
|
||||
<!-- 可查看详情 -->
|
||||
<a-tooltip v-if="hasAnyPermission(['asset:host-key:detail', 'asset:host-key:update'])"
|
||||
content="点击查看详情">
|
||||
<a-tag :checked="true"
|
||||
checkable
|
||||
@click="emits('openKeyView',{id: record.keyId})">
|
||||
@click="emits('openKeyView', { id: record.keyId })">
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</a-tooltip>
|
||||
@@ -103,6 +117,10 @@
|
||||
{{ record.keyName }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 无秘钥 -->
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
@@ -147,21 +165,24 @@
|
||||
import useLoading from '@/hooks/loading';
|
||||
import usePermission from '@/hooks/permission';
|
||||
import { copy } from '@/hooks/copy';
|
||||
import { useDictStore } from '@/store';
|
||||
import { usePagination } from '@/types/table';
|
||||
import { GrantKey, GrantRouteName } from '@/views/asset/grant/types/const';
|
||||
import { IdentityType, identityTypeKey } from '../types/const';
|
||||
import HostKeySelector from '@/components/asset/host-key/selector/index.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate', 'openKeyView']);
|
||||
|
||||
const tableRenderData = ref<HostIdentityQueryResponse[]>([]);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { hasAnyPermission } = usePermission();
|
||||
|
||||
const tableRenderData = ref<HostIdentityQueryResponse[]>([]);
|
||||
const formModel = reactive<HostIdentityQueryRequest>({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
type: undefined,
|
||||
username: undefined,
|
||||
keyId: undefined,
|
||||
});
|
||||
|
||||
@@ -28,8 +28,9 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { useAppStore, useCacheStore } from '@/store';
|
||||
import { ref, computed, onUnmounted, onBeforeMount } from 'vue';
|
||||
import { useAppStore, useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import HostIdentityCardList from './components/host-identity-card-list.vue';
|
||||
import HostIdentityTable from './components/host-identity-table.vue';
|
||||
import HostIdentityFormModal from './components/host-identity-form-modal.vue';
|
||||
@@ -62,6 +63,12 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 加载字典值
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
});
|
||||
|
||||
// 卸载时清除 cache
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
|
||||
@@ -9,6 +9,10 @@ const fieldConfig = {
|
||||
label: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
}, {
|
||||
label: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
}, {
|
||||
label: '用户名',
|
||||
dataIndex: 'username',
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 身份类型
|
||||
export const IdentityType = {
|
||||
PASSWORD: 'PASSWORD',
|
||||
KEY: 'KEY',
|
||||
};
|
||||
|
||||
// 身份类型 字典项
|
||||
export const identityTypeKey = 'hostIdentityType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [identityTypeKey];
|
||||
|
||||
@@ -8,6 +8,16 @@ export const name = [{
|
||||
message: '名称长度不能大于64位'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const type = [{
|
||||
required: true,
|
||||
message: '请选择类型'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const keyId = [{
|
||||
required: true,
|
||||
message: '请选择秘钥'
|
||||
}] as FieldRule[];
|
||||
|
||||
export const username = [{
|
||||
required: true,
|
||||
message: '请输入用户名'
|
||||
@@ -18,5 +28,7 @@ export const username = [{
|
||||
|
||||
export default {
|
||||
name,
|
||||
type,
|
||||
keyId,
|
||||
username,
|
||||
} as Record<string, FieldRule | FieldRule[]>;
|
||||
|
||||
@@ -13,6 +13,13 @@ const columns = [
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 138,
|
||||
}, {
|
||||
title: '用户名',
|
||||
dataIndex: 'username',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
:title="title"
|
||||
:width="470"
|
||||
:width="520"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
:ok-button-props="{ disabled: loading || isViewHandler }"
|
||||
@@ -241,7 +241,7 @@
|
||||
|
||||
.keygen-alert {
|
||||
margin: 0 0 12px 16px;
|
||||
width: 408px;
|
||||
width: calc(100% - 16px);
|
||||
}
|
||||
|
||||
.password-input {
|
||||
|
||||
@@ -13,11 +13,14 @@ const columns = [
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
slotName: 'name',
|
||||
ellipsis: true,
|
||||
tooltip: true
|
||||
}, {
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
slotName: 'createTime',
|
||||
align: 'center',
|
||||
width: 198,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.createTime));
|
||||
},
|
||||
@@ -26,6 +29,7 @@ const columns = [
|
||||
dataIndex: 'updateTime',
|
||||
slotName: 'updateTime',
|
||||
align: 'center',
|
||||
width: 198,
|
||||
render: ({ record }) => {
|
||||
return dateFormat(new Date(record.updateTime));
|
||||
},
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
{{ getDictValue(execJobStatusKey, record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 最近任务 -->
|
||||
<!-- 最近执行 -->
|
||||
<template #recentLog="{ record }">
|
||||
<div class="flex-center" v-if="record.recentLogId && record.recentLogStatus">
|
||||
<!-- 执行状态 -->
|
||||
|
||||
@@ -38,7 +38,7 @@ const columns = [
|
||||
align: 'center',
|
||||
width: 112,
|
||||
}, {
|
||||
title: '最近任务',
|
||||
title: '最近执行',
|
||||
dataIndex: 'recentLog',
|
||||
slotName: 'recentLog',
|
||||
align: 'left',
|
||||
|
||||
@@ -245,5 +245,6 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user