添加主机身份类型.

This commit is contained in:
lijiahang
2024-04-17 10:11:36 +08:00
parent bc8e04b908
commit 339d86fc87
32 changed files with 350 additions and 118 deletions

View File

@@ -6,10 +6,17 @@
`2024-04-` `release`
* 🔨 优化 定时删除未引用的 `tag`
* 🔨 优化 命令执行日志时间不自增
* 🐞 修复 用户列表用户名显示错误
* 🌈 新增 定时删除未引用的 `tag`
* 🌈 新增 执行命令时可使用脚本文件执行
* 🌈 新增 主机身份添加类型字段
* 🔨 优化 文件传输列表进度显示
* 🔨 优化 命令执行日志持续时间
* 🔨 优化 tracker 监听文件可配置 `app.tracker`
* 🔨 优化 sftp 上传文件重复处理可配置 `app.sftp`
* 🔨 优化 用户状态调整交互逻辑
* 🔨 优化 角色状态调整交互逻辑
* 🔨 删除 用户锁定状态
[如何升级](/update/v1.0.5.md)

View File

@@ -1,10 +1,7 @@
## 功能排期 ⏳
* 优化文件传输列表进度显示
* 主机身份类型
* 终端断开连接后回车重新连接
* 使用文件执行命令
* 管理员也需要自行授权资产
* 快捷命令导入
* 文件夹书签
* 批量上传
* 站内消息

View File

@@ -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;
```

View File

@@ -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;
```

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = "用户名")

View File

@@ -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;

View File

@@ -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 = "用户名")

View File

@@ -31,6 +31,9 @@ public class HostIdentityVO implements Serializable {
@Schema(description = "名称")
private String name;
@Schema(description = "类型")
private String type;
@Schema(description = "用户名")
private String username;

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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>>([]);

View File

@@ -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>

View File

@@ -73,3 +73,9 @@ export const GrantTabs = [
component: HostIdentityGrant
},
];
// 身份类型 字典项
export const identityTypeKey = 'hostIdentityType';
// 加载的字典值
export const dictKeys = [identityTypeKey];

View File

@@ -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',

View File

@@ -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,

View File

@@ -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('创建成功');

View File

@@ -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,
});

View File

@@ -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();

View File

@@ -9,6 +9,10 @@ const fieldConfig = {
label: 'id',
dataIndex: 'id',
slotName: 'id',
}, {
label: '类型',
dataIndex: 'type',
slotName: 'type',
}, {
label: '用户名',
dataIndex: 'username',

View File

@@ -0,0 +1,11 @@
// 身份类型
export const IdentityType = {
PASSWORD: 'PASSWORD',
KEY: 'KEY',
};
// 身份类型 字典项
export const identityTypeKey = 'hostIdentityType';
// 加载的字典值
export const dictKeys = [identityTypeKey];

View File

@@ -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[]>;

View File

@@ -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',

View File

@@ -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 {

View File

@@ -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));
},

View File

@@ -107,7 +107,7 @@
{{ getDictValue(execJobStatusKey, record.status) }}
</a-tag>
</template>
<!-- 最近任务 -->
<!-- 最近执行 -->
<template #recentLog="{ record }">
<div class="flex-center" v-if="record.recentLogId && record.recentLogStatus">
<!-- 执行状态 -->

View File

@@ -38,7 +38,7 @@ const columns = [
align: 'center',
width: 112,
}, {
title: '最近任务',
title: '最近执行',
dataIndex: 'recentLog',
slotName: 'recentLog',
align: 'left',

View File

@@ -245,5 +245,6 @@
display: flex;
align-items: center;
justify-content: center;
border-radius: 2px;
}
</style>