Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78bf636cdb | ||
|
|
ba338c15de | ||
|
|
93407460d8 | ||
|
|
554c62abf7 | ||
|
|
a75ead9a58 | ||
|
|
f1ade4e182 | ||
|
|
462e77f936 | ||
|
|
ba955571a3 | ||
|
|
d1e94a49e0 | ||
|
|
b9127967d0 | ||
|
|
0538d2aa26 | ||
|
|
0f8eebf53c | ||
|
|
0f9c3db9cc | ||
|
|
b424dd02db | ||
|
|
f1d14b4a12 |
@@ -28,7 +28,7 @@
|
||||
|
||||
<br/>
|
||||
|
||||
当前版本: **1.0.0**
|
||||
当前版本: **1.0.1**
|
||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
version: '3.3'
|
||||
services:
|
||||
orion-ops-pro:
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.0
|
||||
image: registry.cn-hangzhou.aliyuncs.com/lijiahangmax/orion-ops-pro:1.0.1
|
||||
ports:
|
||||
- 1081:80
|
||||
environment:
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
mv ../../orion-ops-launch/target/orion-ops-launch.jar ./
|
||||
mv ../../orion-ops-ui/dist ./dist
|
||||
docker build -t orion-ops-pro:1.0.0 .
|
||||
docker build -t orion-ops-pro:1.0.1 .
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<br/>
|
||||
|
||||
当前版本: **1.0.0**
|
||||
当前版本: **1.0.1**
|
||||
github: https://github.com/lijiahangmax/orion-ops-pro
|
||||
gitee: https://gitee.com/lijiahangmax/orion-ops-pro
|
||||
文档: https://lijiahangmax.gitee.io/orion-ops-pro/#/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# orion-ops-pro <small>1.0.0</small>
|
||||
# orion-ops-pro <small>1.0.1</small>
|
||||
|
||||
> 一款开箱即用的运维平台。
|
||||
|
||||
|
||||
@@ -1,17 +1,30 @@
|
||||
> 版本号严格遵循 Semver 规范。
|
||||
|
||||
[//]: # (🐞修复)
|
||||
## v1.0.1
|
||||
|
||||
## 1.0.0
|
||||
`2024-03-06` `release`
|
||||
|
||||
🐞 修复 用户操作日志条件重置后类型框数据不正常的问题
|
||||
🩰 修改 主机连接日志 UI
|
||||
🌈 新增 SFTP 使用日志列表
|
||||
🌈 新增 主机连接日志强制下线会话
|
||||
🌈 新增 主机连接日志删除/清理
|
||||
🌈 新增 用户操作日志日志删除/清理
|
||||
🌈 新增 用户操作日志日志删除/清理
|
||||
🔨 优化 用户锁定次数/时间可配置
|
||||
|
||||
[如何升级](/about/update.md?id=_v101)
|
||||
|
||||
## v1.0.0
|
||||
|
||||
`2024-03-01` `release`
|
||||
|
||||
🌈 用户自定义终端标签颜色
|
||||
🌈 新增 用户自定义终端标签颜色
|
||||
🔨 拓展数据模块添加缓存
|
||||
|
||||
[如何升级](/about/update.md?id=_100)
|
||||
[如何升级](/about/update.md?id=_v100)
|
||||
|
||||
## 1.0.0-beta.1
|
||||
## v1.0.0-beta.1
|
||||
|
||||
`2024-02-28` `preview`
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
|
||||
## 未开始 ⏳
|
||||
|
||||
* 资产管理表结构优化
|
||||
* 批量执行
|
||||
* 定时执行
|
||||
* 站内消息
|
||||
* 后端配置动态配置
|
||||
* 终端背景图片
|
||||
* 资产授权 UI 改版
|
||||
* RDP 远程桌面
|
||||
* 接入 config 后端动态配置
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
⚡ 注意: 应用不支持跨版本升级, 可以进行多次升级
|
||||
|
||||
## 1.0.0
|
||||
## v1.0.1
|
||||
|
||||
> sql 脚本
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS `command_template`;
|
||||
ALTER TABLE `operator_log` ADD INDEX `idx_type`(`type`);
|
||||
-- 菜单配置
|
||||
DELETE FROM `system_menu` WHERE id IN (148, 149);
|
||||
INSERT INTO `system_menu` VALUES (148, 152, '连接日志', NULL, 2, 10, 1, 1, 1, 0, 'IconLink', NULL, 'assetAuditConnectLog', '2023-12-26 22:53:07', '2024-03-05 23:31:23', NULL, '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (149, 148, '查询连接日志', 'asset:host-connect-log:management:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2023-12-26 22:53:08', '2024-03-04 13:40:42', NULL, '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (152, 0, '运维审计', NULL, 1, 410, 1, 1, 1, 0, 'IconSafe', NULL, 'assetAudit', '2024-01-04 17:54:56', '2024-03-05 23:31:10', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (153, 148, '删除连接日志', 'asset:host-connect-log:management:delete', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-04 13:39:46', '2024-03-04 13:40:29', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (154, 148, '清空连接日志', 'asset:host-connect-log:management:clear', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-04 13:40:05', '2024-03-04 13:40:34', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (155, 148, '强制断开连接', 'asset:host-connect-log:management:force-offline', 3, 40, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-04 13:41:02', '2024-03-05 23:32:01', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (156, 122, '删除操作日志', 'infra:operator-log:delete', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-04 17:06:55', '2024-03-04 17:08:22', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (157, 122, '清空操作日志', 'infra:operator-log:clear', 3, 30, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-04 17:07:25', '2024-03-04 17:08:27', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (158, 152, 'SFTP 操作日志', NULL, 2, 20, 1, 1, 1, 0, 'IconFile', NULL, 'assetAuditSftpLog', '2024-03-05 15:30:13', '2024-03-05 23:31:32', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (159, 158, '查询 SFTP 操作日志', 'asset:host-sftp-log:management:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-05 15:31:02', '2024-03-05 15:57:20', '1', '1', 0);
|
||||
INSERT INTO `system_menu` VALUES (160, 158, '删除 SFTP 操作日志', 'asset:host-sftp-log:management:delete', 3, 20, 1, 1, 1, 0, NULL, NULL, NULL, '2024-03-05 15:31:17', '2024-03-05 15:57:30', '1', '1', 0);
|
||||
-- 字典配置项
|
||||
INSERT INTO `dict_key` VALUES (33, 'sftpOperatorType', 'STRING', '[]', 'SFTP 操作类型', '2024-03-05 16:49:54', '2024-03-05 16:49:54', '1', '1', 0);
|
||||
-- 字典配置值
|
||||
INSERT INTO `dict_value` VALUES (214, 28, 'hostConnectStatus', 'FORCE_OFFLINE', '强制下线', '{\"color\": \"rgb(var(--red-6))\"}', 40, '2024-03-04 12:51:13', '2024-03-04 12:51:13', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (215, 1, 'operatorLogModule', 'asset:host-connect-log', '主机连接日志', '{}', 2060, '2024-03-04 13:43:33', '2024-03-04 13:43:33', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (216, 2, 'operatorLogType', 'host-connect-log:delete', '删除记录', '{}', 10, '2024-03-04 13:44:34', '2024-03-04 13:44:34', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (217, 2, 'operatorLogType', 'host-connect-log:clear', '清空记录', '{}', 20, '2024-03-04 13:45:07', '2024-03-04 14:22:08', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (218, 2, 'operatorLogType', 'host-connect-log:force-offline', '强制下线', '{}', 30, '2024-03-04 13:45:36', '2024-03-04 13:45:36', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (219, 1, 'operatorLogModule', 'infra:operator-log', '操作日志', '{}', 1060, '2024-03-04 16:32:11', '2024-03-04 16:32:11', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (220, 2, 'operatorLogType', 'operator-log:delete', '删除操作日志', '{}', 10, '2024-03-04 16:33:11', '2024-03-04 16:33:44', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (221, 2, 'operatorLogType', 'operator-log:clear', '清空操作日志', '{}', 20, '2024-03-04 16:33:31', '2024-03-04 16:33:31', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (222, 2, 'operatorLogType', 'host-terminal:delete-sftp-log', '删除SFTP操作日志', '{}', 15, '2024-03-05 15:28:00', '2024-03-05 17:40:47', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (223, 33, 'sftpOperatorType', 'host-terminal:sftp-mkdir', '创建文件夹', '{}', 10, '2024-03-05 16:50:17', '2024-03-05 16:50:17', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (224, 33, 'sftpOperatorType', 'host-terminal:sftp-touch', '创建文件', '{}', 20, '2024-03-05 16:50:27', '2024-03-05 16:50:27', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (225, 33, 'sftpOperatorType', 'host-terminal:sftp-move', '移动文件', '{}', 30, '2024-03-05 16:50:41', '2024-03-05 16:50:41', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (226, 33, 'sftpOperatorType', 'host-terminal:sftp-remove', '删除文件', '{}', 40, '2024-03-05 16:50:53', '2024-03-05 16:50:53', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (227, 33, 'sftpOperatorType', 'host-terminal:sftp-truncate', '截断文件', '{}', 50, '2024-03-05 16:51:04', '2024-03-05 16:51:04', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (228, 33, 'sftpOperatorType', 'host-terminal:sftp-chmod', '文件提权', '{}', 60, '2024-03-05 16:51:15', '2024-03-05 16:51:15', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (229, 33, 'sftpOperatorType', 'host-terminal:sftp-set-content', '修改文件内容', '{}', 70, '2024-03-05 16:51:30', '2024-03-05 16:51:48', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (230, 33, 'sftpOperatorType', 'host-terminal:sftp-upload', '上传文件', '{}', 80, '2024-03-05 16:52:06', '2024-03-05 16:52:06', '1', '1', 0);
|
||||
INSERT INTO `dict_value` VALUES (231, 33, 'sftpOperatorType', 'host-terminal:sftp-download', '下载文件', '{}', 90, '2024-03-05 16:52:18', '2024-03-05 16:52:18', '1', '1', 0);
|
||||
```
|
||||
|
||||
## v1.0.0
|
||||
|
||||
> sql 脚本
|
||||
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
### 连接日志
|
||||
|
||||
在主机终端页面打开的 `SSH` `SFTP` 连接都会记录下来, 这里默认只展示 `SSH` 连接记录, 可以展开条件进行修改。
|
||||
在主机终端页面打开的 `SSH` `SFTP` 连接都会记录下来。
|
||||
|
||||
* 详情: 查看连接详情
|
||||
* 断开: 断开连接
|
||||
* 删除: 删除连接记录
|
||||
* 清理: 根据条件清理数据
|
||||
|
||||
### SFTP 操作日志
|
||||
|
||||
查看用户 SFTP 操作日志, 是从用户操作日志中过滤查询。
|
||||
|
||||
* 删除: 删除操作日志
|
||||
|
||||
@@ -25,3 +25,4 @@
|
||||
记录用户在系统内的操作日志。
|
||||
|
||||
* 详情: 查看操作的参数以及留痕信息
|
||||
* 清理: 根据条件清理数据
|
||||
|
||||
@@ -39,6 +39,8 @@ orion-ops-pro/orion-ops-launch/src/main/resources/application-prod.yaml
|
||||
cd orion-ops-pro
|
||||
# 编译
|
||||
mvn -U clean install -DskipTests
|
||||
# 启动
|
||||
com.orion.ops.launch.LaunchApplication
|
||||
```
|
||||
|
||||
4. 修改前端配置
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
可以在执行命令的第一行设置 `set -e`
|
||||
作用是: 当执行出现意料之外的情况时, 立即退出
|
||||
|
||||
> ##### 5. 在调度任务、应用构建、应用发布 命令执行成功的依据是什么?
|
||||
> ##### 5. 在调度任务、批量执行 命令执行成功的依据是什么?
|
||||
|
||||
是获取命令的 `exitcode` 判断是否为 `0` 如果非0则代表命令执行失败
|
||||
同理, 在命令的最后一行设置 `exit 1` 结果将会是失败, 可以用此来中断后续流程
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<url>https://github.com/lijiahangmax/orion-ops-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>1.0.0</revision>
|
||||
<revision>1.0.1</revision>
|
||||
<spring.boot.version>2.7.17</spring.boot.version>
|
||||
<spring.boot.admin.version>2.7.15</spring.boot.admin.version>
|
||||
<flatten.maven.plugin.version>1.5.0</flatten.maven.plugin.version>
|
||||
|
||||
@@ -85,4 +85,8 @@ public interface ErrorMessage {
|
||||
|
||||
String FILE_ABSENT = "文件不存在";
|
||||
|
||||
String LOG_ABSENT = "日志不存在";
|
||||
|
||||
String ILLEGAL_STATUS = "当前状态不支持此操作";
|
||||
|
||||
}
|
||||
|
||||
@@ -47,4 +47,12 @@ public interface FieldConst {
|
||||
|
||||
String MOD = "mod";
|
||||
|
||||
String COUNT = "count";
|
||||
|
||||
String LOCATION = "location";
|
||||
|
||||
String USER_AGENT = "userAgent";
|
||||
|
||||
String ERROR_MESSAGE = "errorMessage";
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public interface OrionOpsProConst {
|
||||
/**
|
||||
* 同 ${orion.version} 迭代时候需要手动更改
|
||||
*/
|
||||
String VERSION = "1.0.0";
|
||||
String VERSION = "1.0.1";
|
||||
|
||||
String GITHUB = "https://github.com/lijiahangmax/orion-ops-pro";
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.hibernate.validator.constraints.Range;
|
||||
* @since 2023/7/12 23:14
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "公共页码请求")
|
||||
public class PageRequest implements IPageRequest {
|
||||
|
||||
@Range(min = 1, max = 10000, groups = Page.class)
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
show-time
|
||||
allow-clear />
|
||||
#else
|
||||
<a-input v-model="formModel.${field.propertyName}" placeholder="请输入${field.comment}" allow-clear />
|
||||
<a-input v-model="formModel.${field.propertyName}"
|
||||
placeholder="请输入${field.comment}"
|
||||
allow-clear />
|
||||
#end
|
||||
#end
|
||||
</a-form-item>
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
placeholder="请选择${field.comment}"
|
||||
show-time />
|
||||
#else
|
||||
<a-input v-model="formModel.${field.propertyName}" placeholder="请输入${field.comment}" allow-clear/>
|
||||
<a-input v-model="formModel.${field.propertyName}"
|
||||
placeholder="请输入${field.comment}"
|
||||
allow-clear/>
|
||||
#end
|
||||
#end
|
||||
</a-form-item>
|
||||
|
||||
@@ -39,7 +39,9 @@
|
||||
placeholder="请选择${field.comment}"
|
||||
show-time />
|
||||
#else
|
||||
<a-input v-model="formModel.${field.propertyName}" placeholder="请输入${field.comment}" allow-clear />
|
||||
<a-input v-model="formModel.${field.propertyName}"
|
||||
placeholder="请输入${field.comment}"
|
||||
allow-clear />
|
||||
#end
|
||||
#end
|
||||
</a-form-item>
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
#foreach($field in ${table.fields})
|
||||
<!-- $field.comment -->
|
||||
<a-form-item field="${field.propertyName}" label="${field.comment}" label-col-flex="50px">
|
||||
@@ -27,7 +27,9 @@
|
||||
show-time
|
||||
allow-clear />
|
||||
#else
|
||||
<a-input v-model="formModel.${field.propertyName}" placeholder="请输入${field.comment}" allow-clear />
|
||||
<a-input v-model="formModel.${field.propertyName}"
|
||||
placeholder="请输入${field.comment}"
|
||||
allow-clear />
|
||||
#end
|
||||
#end
|
||||
</a-form-item>
|
||||
@@ -48,8 +50,8 @@
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 新增 -->
|
||||
<a-button type="primary"
|
||||
v-permission="['${package.ModuleName}:${typeHyphen}:create']"
|
||||
<a-button v-permission="['${package.ModuleName}:${typeHyphen}:create']"
|
||||
type="primary"
|
||||
@click="emits('openAdd')">
|
||||
新增
|
||||
<template #icon>
|
||||
@@ -58,9 +60,9 @@
|
||||
</a-button>
|
||||
#if($vue.enableRowSelection)
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm position="br"
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
|
||||
position="br"
|
||||
type="warning"
|
||||
:content="`确认删除选中的${selectedKeys.length}条记录吗?`"
|
||||
@ok="deleteSelectRows">
|
||||
<a-button v-permission="['${package.ModuleName}:${typeHyphen}:delete']"
|
||||
type="secondary"
|
||||
@@ -78,9 +80,7 @@
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
#if($vue.enableRowSelection)
|
||||
@@ -104,9 +104,9 @@
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 修改 -->
|
||||
<a-button type="text"
|
||||
<a-button v-permission="['${package.ModuleName}:${typeHyphen}:update']"
|
||||
type="text"
|
||||
size="mini"
|
||||
v-permission="['${package.ModuleName}:${typeHyphen}:update']"
|
||||
@click="emits('openUpdate', record)">
|
||||
修改
|
||||
</a-button>
|
||||
@@ -184,7 +184,7 @@
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await batchDelete${vue.featureEntity}(selectedKeys.value);
|
||||
Message.success(`成功删除${selectedKeys.value.length}条数据`);
|
||||
Message.success(`成功删除 ${selectedKeys.value.length} 条数据`);
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.ScanOptions;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -165,6 +166,18 @@ public class RedisUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置过期时间
|
||||
*
|
||||
* @param key key
|
||||
* @param timeout timeout
|
||||
* @param unit unit
|
||||
*/
|
||||
public static void setExpire(String key, long timeout, TimeUnit unit) {
|
||||
// 设置过期时间
|
||||
redisTemplate.expire(key, timeout, unit);
|
||||
}
|
||||
|
||||
public static void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {
|
||||
if (RedisUtils.redisTemplate != null) {
|
||||
// unmodified
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"groups": [
|
||||
{
|
||||
"name": "app.authentication",
|
||||
"type": "com.orion.ops.module.infra.config.AppAuthenticationConfig",
|
||||
"sourceType": "com.orion.ops.module.infra.config.AppAuthenticationConfig"
|
||||
}
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "app.authentication.allowMultiDevice",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "是否允许多端登录."
|
||||
},
|
||||
{
|
||||
"name": "app.authentication.allowRefresh",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "是否允许凭证续签."
|
||||
},
|
||||
{
|
||||
"name": "app.authentication.maxRefreshCount",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "凭证续签最大次数."
|
||||
},
|
||||
{
|
||||
"name": "app.authentication.loginFailedLockCount",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "登录失败锁定次数."
|
||||
},
|
||||
{
|
||||
"name": "app.authentication.loginFailedLockTime",
|
||||
"type": "java.lang.Integer",
|
||||
"description": "登录失败锁定时间 (分)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -118,6 +118,21 @@ logging:
|
||||
level:
|
||||
com.orion.ops.launch.controller.BootstrapController: INFO
|
||||
|
||||
# 应用配置
|
||||
app:
|
||||
authentication:
|
||||
# 是否允许多端登录
|
||||
allow-multi-device: true
|
||||
# 是否允许凭证续签
|
||||
allow-refresh: true
|
||||
# 凭证续签最大次数
|
||||
max-refresh-count: 3
|
||||
# 登录失败锁定次数
|
||||
login-failed-lock-count: 5
|
||||
# 登录失败锁定时间 (分)
|
||||
login-failed-lock-time: 30
|
||||
|
||||
# orion framework config
|
||||
orion:
|
||||
# 版本
|
||||
version: @revision@
|
||||
@@ -151,7 +166,6 @@ orion:
|
||||
asset:
|
||||
group: "asset - 资产模块"
|
||||
path: "asset"
|
||||
|
||||
logging:
|
||||
# 全局日志打印
|
||||
printer:
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
package com.orion.ops.module.asset.controller;
|
||||
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.OperatorLog;
|
||||
import com.orion.ops.framework.common.validator.group.Id;
|
||||
import com.orion.ops.framework.common.validator.group.Page;
|
||||
import com.orion.ops.framework.log.core.annotation.IgnoreLog;
|
||||
import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
|
||||
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||
import com.orion.ops.module.asset.define.operator.HostConnectLogOperatorType;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostConnectLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostConnectLogVO;
|
||||
import com.orion.ops.module.asset.service.HostConnectLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
@@ -55,5 +56,36 @@ public class HostConnectLogController {
|
||||
return hostConnectLogService.getLatestConnectHostId(request);
|
||||
}
|
||||
|
||||
@OperatorLog(HostConnectLogOperatorType.DELETE)
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除主机连接日志")
|
||||
@Parameter(name = "idList", description = "idList", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('asset:host-connect-log:management:delete')")
|
||||
public Integer deleteHostConnectLog(@RequestParam("idList") List<Long> idList) {
|
||||
return hostConnectLogService.deleteHostConnectLog(idList);
|
||||
}
|
||||
|
||||
@PostMapping("/query-count")
|
||||
@Operation(summary = "查询主机连接日志数量")
|
||||
public Long getHostConnectLogCount(@RequestBody HostConnectLogQueryRequest request) {
|
||||
return hostConnectLogService.getHostConnectLogCount(request);
|
||||
}
|
||||
|
||||
@OperatorLog(HostConnectLogOperatorType.CLEAR)
|
||||
@PostMapping("/clear")
|
||||
@Operation(summary = "清空主机连接日志")
|
||||
@PreAuthorize("@ss.hasPermission('asset:host-connect-log:management:clear')")
|
||||
public Integer clearHostConnectLog(@RequestBody HostConnectLogQueryRequest request) {
|
||||
return hostConnectLogService.clearHostConnectLog(request);
|
||||
}
|
||||
|
||||
@OperatorLog(HostConnectLogOperatorType.FORCE_OFFLINE)
|
||||
@PutMapping("/force-offline")
|
||||
@Operation(summary = "强制断开主机连接")
|
||||
@PreAuthorize("@ss.hasPermission('asset:host-connect-log:management:force-offline')")
|
||||
public Integer forceOffline(@Validated(Id.class) @RequestBody HostConnectLogQueryRequest request) {
|
||||
return hostConnectLogService.forceOffline(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
### 分页查询 SFTP 操作日志
|
||||
POST {{baseUrl}}/asset/host-sftp-log/query
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
{
|
||||
"page": 1,
|
||||
"limit": 10
|
||||
}
|
||||
|
||||
|
||||
### 删除 SFTP 操作日志
|
||||
DELETE {{baseUrl}}/asset/host-sftp-log/delete?idList=1,2,3
|
||||
Authorization: {{token}}
|
||||
|
||||
|
||||
###
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.orion.ops.module.asset.controller;
|
||||
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.OperatorLog;
|
||||
import com.orion.ops.framework.common.validator.group.Page;
|
||||
import com.orion.ops.framework.log.core.annotation.IgnoreLog;
|
||||
import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
|
||||
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||
import com.orion.ops.module.asset.define.operator.HostTerminalOperatorType;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostSftpLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostSftpLogVO;
|
||||
import com.orion.ops.module.asset.service.HostSftpLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志服务 api
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-12-26 22:09
|
||||
*/
|
||||
@Tag(name = "asset - SFTP 操作日志服务")
|
||||
@Slf4j
|
||||
@Validated
|
||||
@RestWrapper
|
||||
@RestController
|
||||
@RequestMapping("/asset/host-sftp-log")
|
||||
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
|
||||
public class HostSftpLogController {
|
||||
|
||||
@Resource
|
||||
private HostSftpLogService hostSftpLogService;
|
||||
|
||||
@IgnoreLog(IgnoreLogMode.RET)
|
||||
@PostMapping("/query")
|
||||
@Operation(summary = "分页查询 SFTP 操作日志")
|
||||
@PreAuthorize("@ss.hasAnyPermission('infra:operator-log:query', 'asset:host-sftp-log:management:query')")
|
||||
public DataGrid<HostSftpLogVO> getHostSftpLogPage(@Validated(Page.class) @RequestBody HostSftpLogQueryRequest request) {
|
||||
return hostSftpLogService.getHostSftpLogPage(request);
|
||||
}
|
||||
|
||||
@OperatorLog(HostTerminalOperatorType.DELETE_SFTP_LOG)
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除 SFTP 操作日志")
|
||||
@Parameter(name = "idList", description = "idList", required = true)
|
||||
@PreAuthorize("@ss.hasAnyPermission('infra:operator-log:delete', 'asset:host-sftp-log:management:delete')")
|
||||
public Integer deleteHostSftpLog(@RequestParam("idList") List<Long> idList) {
|
||||
return hostSftpLogService.deleteHostSftpLog(idList);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.orion.ops.module.asset.convert;
|
||||
|
||||
import com.orion.ops.module.asset.entity.vo.HostSftpLogVO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogDTO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.Mapping;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 内部对象转换器
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-12-26 22:09
|
||||
*/
|
||||
@Mapper
|
||||
public interface HostSftpLogConvert {
|
||||
|
||||
HostSftpLogConvert MAPPER = Mappers.getMapper(HostSftpLogConvert.class);
|
||||
|
||||
@Mapping(target = "extra", ignore = true)
|
||||
HostSftpLogVO to(OperatorLogDTO request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.orion.ops.module.asset.define.operator;
|
||||
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.Module;
|
||||
import com.orion.ops.framework.biz.operator.log.core.factory.InitializingOperatorTypes;
|
||||
import com.orion.ops.framework.biz.operator.log.core.model.OperatorType;
|
||||
|
||||
import static com.orion.ops.framework.biz.operator.log.core.enums.OperatorRiskLevel.*;
|
||||
|
||||
/**
|
||||
* 主机连接日志 操作日志类型
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/2 14:37
|
||||
*/
|
||||
@Module("asset:host-connect-log")
|
||||
public class HostConnectLogOperatorType extends InitializingOperatorTypes {
|
||||
|
||||
public static final String DELETE = "host-connect-log:delete";
|
||||
|
||||
public static final String CLEAR = "host-connect-log:clear";
|
||||
|
||||
public static final String FORCE_OFFLINE = "host-connect-log:force-offline";
|
||||
|
||||
@Override
|
||||
public OperatorType[] types() {
|
||||
return new OperatorType[]{
|
||||
new OperatorType(H, DELETE, "删除主机连接记录 <sb>${count}</sb> 条"),
|
||||
new OperatorType(H, CLEAR, "清空主机连接记录 <sb>${count}</sb> 条"),
|
||||
new OperatorType(M, FORCE_OFFLINE, "强制下线主机连接 <sb>${hostName}</sb>"),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.orion.ops.module.asset.define.operator;
|
||||
|
||||
import com.orion.lang.utils.collect.Lists;
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.Module;
|
||||
import com.orion.ops.framework.biz.operator.log.core.factory.InitializingOperatorTypes;
|
||||
import com.orion.ops.framework.biz.operator.log.core.model.OperatorType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.orion.ops.framework.biz.operator.log.core.enums.OperatorRiskLevel.*;
|
||||
|
||||
/**
|
||||
@@ -18,6 +21,8 @@ public class HostTerminalOperatorType extends InitializingOperatorTypes {
|
||||
|
||||
public static final String CONNECT = "host-terminal:connect";
|
||||
|
||||
public static final String DELETE_SFTP_LOG = "host-terminal:delete-sftp-log";
|
||||
|
||||
public static final String SFTP_MKDIR = "host-terminal:sftp-mkdir";
|
||||
|
||||
public static final String SFTP_TOUCH = "host-terminal:sftp-touch";
|
||||
@@ -36,10 +41,23 @@ public class HostTerminalOperatorType extends InitializingOperatorTypes {
|
||||
|
||||
public static final String SFTP_DOWNLOAD = "host-terminal:sftp-download";
|
||||
|
||||
public static final List<String> SFTP_TYPES = Lists.of(
|
||||
SFTP_MKDIR,
|
||||
SFTP_TOUCH,
|
||||
SFTP_MOVE,
|
||||
SFTP_REMOVE,
|
||||
SFTP_TRUNCATE,
|
||||
SFTP_CHMOD,
|
||||
SFTP_SET_CONTENT,
|
||||
SFTP_UPLOAD,
|
||||
SFTP_DOWNLOAD
|
||||
);
|
||||
|
||||
@Override
|
||||
public OperatorType[] types() {
|
||||
return new OperatorType[]{
|
||||
new OperatorType(L, CONNECT, "连接主机 ${connectType} <sb>${hostName}</sb>"),
|
||||
new OperatorType(H, DELETE_SFTP_LOG, "删除 SFTP 操作日志 <sb>${count}</sb> 条"),
|
||||
new OperatorType(L, SFTP_MKDIR, "创建文件夹 ${hostName} <sb>${path}</sb>"),
|
||||
new OperatorType(L, SFTP_TOUCH, "创建文件 ${hostName} <sb>${path}</sb>"),
|
||||
new OperatorType(M, SFTP_MOVE, "移动文件 ${hostName} <sb>${path}</sb> 至 <sb>${target}</sb>"),
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.orion.ops.module.asset.entity.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 主机连接日志推展信息对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024-3-12 23:31
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(name = "HostConnectLogExtraDTO", description = "主机连接日志推展信息对象")
|
||||
public class HostConnectLogExtraDTO {
|
||||
|
||||
@Schema(description = "hostId")
|
||||
private Long hostId;
|
||||
|
||||
@Schema(description = "主机名称")
|
||||
private String hostName;
|
||||
|
||||
@Schema(description = "连接类型")
|
||||
private String connectType;
|
||||
|
||||
@Schema(description = "traceId")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "channelId")
|
||||
private String channelId;
|
||||
|
||||
@Schema(description = "sessionId")
|
||||
private String sessionId;
|
||||
|
||||
@Schema(description = "请求地址")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "请求位置")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "ua")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
@@ -23,6 +23,9 @@ import java.util.Date;
|
||||
@Schema(name = "HostConnectLogQueryRequest", description = "主机连接日志 查询请求对象")
|
||||
public class HostConnectLogQueryRequest extends PageRequest {
|
||||
|
||||
@Schema(description = "id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户id")
|
||||
private Long userId;
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.orion.ops.module.asset.entity.request.host;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.orion.ops.framework.common.entity.PageRequest;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 查询请求对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024-3-4 22:59
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "HostSftpLogQueryRequest", description = "SFTP 操作日志 查询请求对象")
|
||||
public class HostSftpLogQueryRequest extends PageRequest {
|
||||
|
||||
@Schema(description = "用户id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "hostId")
|
||||
private Long hostId;
|
||||
|
||||
@Size(max = 64)
|
||||
@Schema(description = "操作类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "操作结果 0失败 1成功")
|
||||
private Integer result;
|
||||
|
||||
@Schema(description = "开始时间-区间")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date[] startTimeRange;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.orion.ops.module.asset.entity.vo;
|
||||
|
||||
import com.orion.ops.module.asset.entity.dto.HostConnectLogExtraDTO;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -46,9 +47,6 @@ public class HostConnectLogVO implements Serializable {
|
||||
@Schema(description = "类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "token")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "状态")
|
||||
private String status;
|
||||
|
||||
@@ -59,18 +57,6 @@ public class HostConnectLogVO implements Serializable {
|
||||
private Date endTime;
|
||||
|
||||
@Schema(description = "额外信息")
|
||||
private String extraInfo;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
@Schema(description = "修改时间")
|
||||
private Date updateTime;
|
||||
|
||||
@Schema(description = "创建人")
|
||||
private String creator;
|
||||
|
||||
@Schema(description = "修改人")
|
||||
private String updater;
|
||||
private HostConnectLogExtraDTO extra;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.orion.ops.module.asset.entity.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 实体对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-10-10 17:08
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(name = "HostSftpLogVO", description = "SFTP 操作日志 实体对象")
|
||||
public class HostSftpLogVO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "主机id")
|
||||
private Long hostId;
|
||||
|
||||
@Schema(description = "主机名称")
|
||||
private String hostName;
|
||||
|
||||
@Schema(description = "主机地址")
|
||||
private String hostAddress;
|
||||
|
||||
@Schema(description = "操作文件")
|
||||
private String[] paths;
|
||||
|
||||
@Schema(description = "请求ip")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "请求地址")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "userAgent")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "操作类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "参数")
|
||||
private Map<String, Object> extra;
|
||||
|
||||
@Schema(description = "操作结果 0失败 1成功")
|
||||
private Integer result;
|
||||
|
||||
@Schema(description = "开始时间")
|
||||
private Date startTime;
|
||||
|
||||
}
|
||||
@@ -24,6 +24,11 @@ public enum HostConnectStatusEnum {
|
||||
*/
|
||||
FAILED,
|
||||
|
||||
/**
|
||||
* 强制下线
|
||||
*/
|
||||
FORCE_OFFLINE,
|
||||
|
||||
;
|
||||
|
||||
public static HostConnectStatusEnum of(String type) {
|
||||
|
||||
@@ -28,7 +28,7 @@ public enum OutputTypeEnum {
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
CLOSE("cl", "${type}|${sessionId}|${msg}"),
|
||||
CLOSE("cl", "${type}|${sessionId}|${forceClose}|${msg}"),
|
||||
|
||||
/**
|
||||
* pong
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.orion.ops.module.asset.handler.host.terminal.handler;
|
||||
|
||||
import com.orion.lang.utils.collect.Maps;
|
||||
import com.orion.ops.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
import com.orion.ops.framework.common.constant.Const;
|
||||
import com.orion.ops.framework.common.enums.BooleanBit;
|
||||
import com.orion.ops.module.asset.define.operator.HostTerminalOperatorType;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
|
||||
@@ -54,7 +53,7 @@ public class SftpRemoveHandler extends AbstractTerminalHandler<SftpBaseRequest>
|
||||
.build());
|
||||
// 保存操作日志
|
||||
Map<String, Object> extra = Maps.newMap();
|
||||
extra.put(OperatorLogs.PATH, String.join(Const.COMMA, paths));
|
||||
extra.put(OperatorLogs.PATH, payload.getPath());
|
||||
this.saveOperatorLog(payload, channel,
|
||||
extra, HostTerminalOperatorType.SFTP_REMOVE,
|
||||
startTime, ex);
|
||||
|
||||
@@ -173,7 +173,7 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
|
||||
String username = WebSockets.getAttr(channel, ExtraFieldConst.USERNAME);
|
||||
// 额外参数
|
||||
Map<String, Object> extra = Maps.newMap();
|
||||
extra.put(OperatorLogs.ID, hostId);
|
||||
extra.put(OperatorLogs.HOST_ID, hostId);
|
||||
extra.put(OperatorLogs.HOST_NAME, hostName);
|
||||
extra.put(OperatorLogs.CONNECT_TYPE, connectType.name());
|
||||
extra.put(OperatorLogs.CHANNEL_ID, channel.getId());
|
||||
@@ -194,6 +194,13 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
|
||||
.token(sessionId)
|
||||
.extra(extra)
|
||||
.build();
|
||||
// 填充其他信息
|
||||
extra.put(OperatorLogs.TRACE_ID, logModel.getTraceId());
|
||||
extra.put(OperatorLogs.ADDRESS, logModel.getAddress());
|
||||
extra.put(OperatorLogs.LOCATION, logModel.getLocation());
|
||||
extra.put(OperatorLogs.USER_AGENT, logModel.getUserAgent());
|
||||
extra.put(OperatorLogs.ERROR_MESSAGE, logModel.getErrorMessage());
|
||||
// 记录连接日志
|
||||
hostConnectLogService.create(connectType, connectLog);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@ import com.orion.lang.exception.ConnectionRuntimeException;
|
||||
import com.orion.lang.exception.TimeoutException;
|
||||
import com.orion.lang.exception.argument.InvalidArgumentException;
|
||||
import com.orion.lang.utils.Exceptions;
|
||||
import com.orion.lang.utils.collect.Maps;
|
||||
import com.orion.lang.utils.io.Streams;
|
||||
import com.orion.net.host.SessionStore;
|
||||
import com.orion.ops.framework.common.constant.ErrorMessage;
|
||||
import com.orion.ops.framework.common.constant.ExtraFieldConst;
|
||||
import com.orion.ops.framework.common.enums.BooleanBit;
|
||||
import com.orion.ops.framework.websocket.core.utils.WebSockets;
|
||||
import com.orion.ops.module.asset.entity.dto.HostTerminalConnectDTO;
|
||||
@@ -28,6 +30,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 连接主机处理器
|
||||
@@ -74,7 +77,9 @@ public class TerminalConnectHandler extends AbstractTerminalHandler<TerminalConn
|
||||
} catch (Exception e) {
|
||||
ex = e;
|
||||
// 修改连接状态为失败
|
||||
hostConnectLogService.updateStatusByToken(sessionId, HostConnectStatusEnum.FAILED);
|
||||
Map<String, Object> extra = Maps.newMap(4);
|
||||
extra.put(ExtraFieldConst.ERROR_MESSAGE, this.getConnectErrorMessage(e));
|
||||
hostConnectLogService.updateStatusByToken(sessionId, HostConnectStatusEnum.FAILED, extra);
|
||||
}
|
||||
// 返回连接状态
|
||||
this.send(channel,
|
||||
|
||||
@@ -23,6 +23,9 @@ import lombok.experimental.SuperBuilder;
|
||||
@Schema(name = "TerminalCloseResponse", description = "主机连接关闭响应 实体对象")
|
||||
public class TerminalCloseResponse extends TerminalBasePayload {
|
||||
|
||||
@Schema(description = "是否为强制关闭")
|
||||
private Integer forceClose;
|
||||
|
||||
@Schema(description = "关闭信息")
|
||||
private String msg;
|
||||
|
||||
|
||||
@@ -38,4 +38,9 @@ public interface ITerminalSession extends SafeCloseable {
|
||||
*/
|
||||
void keepAlive();
|
||||
|
||||
/**
|
||||
* 强制下线
|
||||
*/
|
||||
void forceOffline();
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.orion.lang.utils.io.Streams;
|
||||
import com.orion.net.host.SessionStore;
|
||||
import com.orion.net.host.ssh.shell.ShellExecutor;
|
||||
import com.orion.ops.framework.common.constant.Const;
|
||||
import com.orion.ops.framework.common.enums.BooleanBit;
|
||||
import com.orion.ops.framework.websocket.core.utils.WebSockets;
|
||||
import com.orion.ops.module.asset.define.AssetThreadPools;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.constant.TerminalMessage;
|
||||
@@ -53,7 +54,7 @@ public class SshSession extends TerminalSession implements ISshSession {
|
||||
executor.size(cols, rows);
|
||||
executor.terminalType(terminalType);
|
||||
executor.streamHandler(this::streamHandler);
|
||||
executor.callback(this::eofCallback);
|
||||
executor.callback(this::close);
|
||||
executor.connect();
|
||||
// 开始监听输出
|
||||
AssetThreadPools.TERMINAL_STDOUT.execute(executor);
|
||||
@@ -122,20 +123,4 @@ public class SshSession extends TerminalSession implements ISshSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* eof 回调
|
||||
*/
|
||||
private void eofCallback() {
|
||||
log.info("terminal eof回调 {}, forClose: {}", sessionId, this.close);
|
||||
// 发送关闭信息
|
||||
TerminalCloseResponse resp = TerminalCloseResponse.builder()
|
||||
.type(OutputTypeEnum.CLOSE.getType())
|
||||
.sessionId(this.sessionId)
|
||||
.msg(TerminalMessage.CLOSED_CONNECTION)
|
||||
.build();
|
||||
WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp));
|
||||
// 需要调用关闭 - 可能是 logout 需要手动触发
|
||||
this.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package com.orion.ops.module.asset.handler.host.terminal.session;
|
||||
|
||||
import com.orion.ops.framework.common.enums.BooleanBit;
|
||||
import com.orion.ops.framework.websocket.core.utils.WebSockets;
|
||||
import com.orion.ops.module.asset.enums.HostConnectStatusEnum;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.constant.TerminalMessage;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.enums.OutputTypeEnum;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.model.TerminalConfig;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.model.response.TerminalCloseResponse;
|
||||
import com.orion.ops.module.asset.service.HostConnectLogService;
|
||||
import com.orion.spring.SpringHolder;
|
||||
import lombok.Getter;
|
||||
@@ -28,6 +33,8 @@ public abstract class TerminalSession implements ITerminalSession {
|
||||
|
||||
protected volatile boolean close;
|
||||
|
||||
protected volatile boolean forceOffline;
|
||||
|
||||
public TerminalSession(String sessionId, WebSocketSession channel, TerminalConfig config) {
|
||||
this.sessionId = sessionId;
|
||||
this.channel = channel;
|
||||
@@ -39,11 +46,48 @@ public abstract class TerminalSession implements ITerminalSession {
|
||||
*/
|
||||
protected abstract void releaseResource();
|
||||
|
||||
/**
|
||||
* 发送关闭消息
|
||||
*/
|
||||
protected void sendCloseMessage() {
|
||||
log.info("TerminalSession close {}, forClose: {}, forceOffline: {}", sessionId, this.close, this.forceOffline);
|
||||
// 发送关闭信息
|
||||
TerminalCloseResponse resp = TerminalCloseResponse.builder()
|
||||
.type(OutputTypeEnum.CLOSE.getType())
|
||||
.sessionId(this.sessionId)
|
||||
.forceClose(BooleanBit.of(this.forceOffline).getValue())
|
||||
.msg(this.forceOffline ? TerminalMessage.FORCED_OFFLINE : TerminalMessage.CLOSED_CONNECTION)
|
||||
.build();
|
||||
WebSockets.sendText(channel, OutputTypeEnum.CLOSE.format(resp));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
log.info("terminal close {}", sessionId);
|
||||
// 检查并且关闭
|
||||
if (this.checkAndClose()) {
|
||||
// 修改状态
|
||||
SpringHolder.getBean(HostConnectLogService.class)
|
||||
.updateStatusByToken(sessionId, HostConnectStatusEnum.COMPLETE, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void forceOffline() {
|
||||
log.info("terminal forceOffline {}", sessionId);
|
||||
this.forceOffline = true;
|
||||
// 关闭
|
||||
this.checkAndClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并且关闭会话
|
||||
*
|
||||
* @return close
|
||||
*/
|
||||
private boolean checkAndClose() {
|
||||
if (close) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
this.close = true;
|
||||
// 释放资源
|
||||
@@ -52,8 +96,13 @@ public abstract class TerminalSession implements ITerminalSession {
|
||||
} catch (Exception e) {
|
||||
log.error("terminal release error {}", sessionId, e);
|
||||
}
|
||||
// 修改状态
|
||||
SpringHolder.getBean(HostConnectLogService.class).updateStatusByToken(sessionId, HostConnectStatusEnum.COMPLETE);
|
||||
// 发送关闭信息
|
||||
try {
|
||||
this.sendCloseMessage();
|
||||
} catch (Exception e) {
|
||||
log.error("terminal send close error {}", sessionId, e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.orion.ops.module.asset.enums.HostConnectStatusEnum;
|
||||
import com.orion.ops.module.asset.enums.HostConnectTypeEnum;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
/**
|
||||
@@ -40,8 +41,10 @@ public interface HostConnectLogService {
|
||||
*
|
||||
* @param token token
|
||||
* @param status status
|
||||
* @param extra extra
|
||||
* @return effect
|
||||
*/
|
||||
void updateStatusByToken(String token, HostConnectStatusEnum status);
|
||||
Integer updateStatusByToken(String token, HostConnectStatusEnum status, Map<String, Object> extra);
|
||||
|
||||
/**
|
||||
* 查询用户最近连接的主机
|
||||
@@ -60,4 +63,36 @@ public interface HostConnectLogService {
|
||||
*/
|
||||
Future<List<Long>> getLatestConnectHostIdAsync(HostConnectTypeEnum type, Long userId);
|
||||
|
||||
/**
|
||||
* 删除主机连接日志
|
||||
*
|
||||
* @param idList idList
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteHostConnectLog(List<Long> idList);
|
||||
|
||||
/**
|
||||
* 获取主机连接日志数量
|
||||
*
|
||||
* @param request request
|
||||
* @return count
|
||||
*/
|
||||
Long getHostConnectLogCount(HostConnectLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 清空主机连接日志
|
||||
*
|
||||
* @param request request
|
||||
* @return effect
|
||||
*/
|
||||
Integer clearHostConnectLog(HostConnectLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 强制断开主机连接
|
||||
*
|
||||
* @param request request
|
||||
* @return effect
|
||||
*/
|
||||
Integer forceOffline(HostConnectLogQueryRequest request);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.orion.ops.module.asset.service;
|
||||
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostSftpLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostSftpLogVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 服务类
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-12-26 22:09
|
||||
*/
|
||||
public interface HostSftpLogService {
|
||||
|
||||
/**
|
||||
* 分页查询 SFTP 操作日志
|
||||
*
|
||||
* @param request request
|
||||
* @return rows
|
||||
*/
|
||||
DataGrid<HostSftpLogVO> getHostSftpLogPage(HostSftpLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 删除 SFTP 操作日志
|
||||
*
|
||||
* @param idList idList
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteHostSftpLog(List<Long> idList);
|
||||
|
||||
}
|
||||
@@ -5,16 +5,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.orion.lang.constant.Const;
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.lang.utils.Arrays1;
|
||||
import com.orion.ops.framework.mybatis.core.query.Conditions;
|
||||
import com.orion.lang.utils.Valid;
|
||||
import com.orion.ops.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
import com.orion.ops.framework.common.constant.ErrorMessage;
|
||||
import com.orion.ops.framework.security.core.utils.SecurityUtils;
|
||||
import com.orion.ops.module.asset.convert.HostConnectLogConvert;
|
||||
import com.orion.ops.module.asset.dao.HostConnectLogDAO;
|
||||
import com.orion.ops.module.asset.entity.domain.HostConnectLogDO;
|
||||
import com.orion.ops.module.asset.entity.dto.HostConnectLogExtraDTO;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostConnectLogCreateRequest;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostConnectLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostConnectLogVO;
|
||||
import com.orion.ops.module.asset.enums.HostConnectStatusEnum;
|
||||
import com.orion.ops.module.asset.enums.HostConnectTypeEnum;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.manager.TerminalManager;
|
||||
import com.orion.ops.module.asset.handler.host.terminal.session.ITerminalSession;
|
||||
import com.orion.ops.module.asset.service.HostConnectLogService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
@@ -23,6 +28,7 @@ import org.springframework.stereotype.Service;
|
||||
import javax.annotation.Resource;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
@@ -40,6 +46,9 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
|
||||
@Resource
|
||||
private HostConnectLogDAO hostConnectLogDAO;
|
||||
|
||||
@Resource
|
||||
private TerminalManager terminalManager;
|
||||
|
||||
@Override
|
||||
public void create(HostConnectTypeEnum type, HostConnectLogCreateRequest request) {
|
||||
HostConnectLogDO record = HostConnectLogConvert.MAPPER.to(request);
|
||||
@@ -62,16 +71,54 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
|
||||
// 查询
|
||||
return hostConnectLogDAO.of(wrapper)
|
||||
.page(request)
|
||||
.dataGrid(HostConnectLogConvert.MAPPER::to);
|
||||
.dataGrid(s -> {
|
||||
HostConnectLogVO vo = HostConnectLogConvert.MAPPER.to(s);
|
||||
vo.setExtra(JSON.parseObject(s.getExtraInfo(), HostConnectLogExtraDTO.class));
|
||||
return vo;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateStatusByToken(String token, HostConnectStatusEnum status) {
|
||||
log.info("HostConnectLogService-updateStatusByToken token: {}, status: {}", token, status);
|
||||
public Integer updateStatusByToken(String token, HostConnectStatusEnum status, Map<String, Object> partial) {
|
||||
log.info("HostConnectLogService-updateStatusByToken start token: {}, status: {}", token, status);
|
||||
// 查询
|
||||
HostConnectLogDO record = hostConnectLogDAO.of()
|
||||
.createWrapper()
|
||||
.eq(HostConnectLogDO::getToken, token)
|
||||
.orderByDesc(HostConnectLogDO::getId)
|
||||
.then()
|
||||
.getOne();
|
||||
if (record == null) {
|
||||
log.info("HostConnectLogService-updateStatusByToken no record token: {}", token);
|
||||
return Const.N_0;
|
||||
}
|
||||
return this.updateStatus(record, status, partial);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
*
|
||||
* @param record record
|
||||
* @param status status
|
||||
* @param partial partial
|
||||
* @return effect
|
||||
*/
|
||||
private int updateStatus(HostConnectLogDO record, HostConnectStatusEnum status, Map<String, Object> partial) {
|
||||
// 更新
|
||||
HostConnectLogDO update = new HostConnectLogDO();
|
||||
update.setId(record.getId());
|
||||
update.setStatus(status.name());
|
||||
update.setEndTime(new Date());
|
||||
hostConnectLogDAO.update(update, Conditions.eq(HostConnectLogDO::getToken, token));
|
||||
if (partial != null) {
|
||||
Map<String, Object> extra = JSON.parseObject(record.getExtraInfo());
|
||||
if (extra == null) {
|
||||
extra = partial;
|
||||
} else {
|
||||
extra.putAll(partial);
|
||||
}
|
||||
update.setExtraInfo(JSON.toJSONString(extra));
|
||||
}
|
||||
return hostConnectLogDAO.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,6 +133,53 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
|
||||
return CompletableFuture.completedFuture(hostIdList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deleteHostConnectLog(List<Long> idList) {
|
||||
log.info("HostConnectLogService.deleteHostConnectLog start {}", JSON.toJSONString(idList));
|
||||
int effect = hostConnectLogDAO.deleteBatchIds(idList);
|
||||
log.info("HostConnectLogService.deleteHostConnectLog finish {}", effect);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.COUNT, effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getHostConnectLogCount(HostConnectLogQueryRequest request) {
|
||||
return hostConnectLogDAO.selectCount(this.buildQueryWrapper(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer clearHostConnectLog(HostConnectLogQueryRequest request) {
|
||||
log.info("HostConnectLogService.clearHostConnectLog start {}", JSON.toJSONString(request));
|
||||
// 删除
|
||||
LambdaQueryWrapper<HostConnectLogDO> wrapper = this.buildQueryWrapper(request);
|
||||
int effect = hostConnectLogDAO.delete(wrapper);
|
||||
log.info("HostConnectLogService.clearHostConnectLog finish {}", effect);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.COUNT, effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer forceOffline(HostConnectLogQueryRequest request) {
|
||||
Long id = request.getId();
|
||||
// 查询数据是否存在
|
||||
HostConnectLogDO record = hostConnectLogDAO.selectById(id);
|
||||
Valid.notNull(record, ErrorMessage.LOG_ABSENT);
|
||||
Valid.eq(record.getStatus(), HostConnectStatusEnum.CONNECTING.name(), ErrorMessage.ILLEGAL_STATUS);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.HOST_NAME, record.getHostName());
|
||||
// 获取会话
|
||||
HostConnectLogExtraDTO extra = JSON.parseObject(record.getExtraInfo(), HostConnectLogExtraDTO.class);
|
||||
ITerminalSession session = terminalManager.getSession(extra.getChannelId(), extra.getSessionId());
|
||||
if (session != null) {
|
||||
// 关闭会话
|
||||
session.forceOffline();
|
||||
}
|
||||
// 更新状态
|
||||
return this.updateStatus(record, HostConnectStatusEnum.FORCE_OFFLINE, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询 wrapper
|
||||
*
|
||||
@@ -94,6 +188,7 @@ public class HostConnectLogServiceImpl implements HostConnectLogService {
|
||||
*/
|
||||
private LambdaQueryWrapper<HostConnectLogDO> buildQueryWrapper(HostConnectLogQueryRequest request) {
|
||||
return hostConnectLogDAO.wrapper()
|
||||
.eq(HostConnectLogDO::getId, request.getId())
|
||||
.eq(HostConnectLogDO::getUserId, request.getUserId())
|
||||
.eq(HostConnectLogDO::getHostId, request.getHostId())
|
||||
.like(HostConnectLogDO::getHostAddress, request.getHostAddress())
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.orion.ops.module.asset.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.lang.utils.Arrays1;
|
||||
import com.orion.lang.utils.Strings;
|
||||
import com.orion.ops.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
import com.orion.ops.framework.common.constant.ExtraFieldConst;
|
||||
import com.orion.ops.module.asset.convert.HostSftpLogConvert;
|
||||
import com.orion.ops.module.asset.define.operator.HostTerminalOperatorType;
|
||||
import com.orion.ops.module.asset.entity.request.host.HostSftpLogQueryRequest;
|
||||
import com.orion.ops.module.asset.entity.vo.HostSftpLogVO;
|
||||
import com.orion.ops.module.asset.service.HostSftpLogService;
|
||||
import com.orion.ops.module.infra.api.OperatorLogApi;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogDTO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogQueryDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 服务实现类
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/4 23:35
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HostSftpLogServiceImpl implements HostSftpLogService {
|
||||
|
||||
@Resource
|
||||
private OperatorLogApi operatorLogApi;
|
||||
|
||||
@Override
|
||||
public DataGrid<HostSftpLogVO> getHostSftpLogPage(HostSftpLogQueryRequest request) {
|
||||
// 查询
|
||||
OperatorLogQueryDTO query = this.buildQueryInfo(request);
|
||||
DataGrid<OperatorLogDTO> dataGrid = operatorLogApi.getOperatorLogPage(query);
|
||||
// 转换
|
||||
List<HostSftpLogVO> rows = dataGrid.stream()
|
||||
.map(s -> {
|
||||
JSONObject extra = JSON.parseObject(s.getExtra());
|
||||
HostSftpLogVO vo = HostSftpLogConvert.MAPPER.to(s);
|
||||
vo.setHostId(extra.getLong(ExtraFieldConst.HOST_ID));
|
||||
vo.setHostName(extra.getString(ExtraFieldConst.HOST_NAME));
|
||||
vo.setHostAddress(extra.getString(ExtraFieldConst.ADDRESS));
|
||||
vo.setPaths(extra.getString(ExtraFieldConst.PATH).split("\\|"));
|
||||
vo.setExtra(extra);
|
||||
return vo;
|
||||
}).collect(Collectors.toList());
|
||||
// 返回
|
||||
DataGrid<HostSftpLogVO> result = new DataGrid<>();
|
||||
result.setRows(rows);
|
||||
result.setPage(dataGrid.getPage());
|
||||
result.setLimit(dataGrid.getLimit());
|
||||
result.setSize(dataGrid.getSize());
|
||||
result.setTotal(dataGrid.getTotal());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deleteHostSftpLog(List<Long> idList) {
|
||||
log.info("HostSftpLogService.deleteSftpLog start {}", JSON.toJSONString(idList));
|
||||
Integer effect = operatorLogApi.deleteOperatorLog(idList);
|
||||
log.info("HostSftpLogService.deleteSftpLog finish {}", effect);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.COUNT, effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询对象
|
||||
*
|
||||
* @param request request
|
||||
* @return query
|
||||
*/
|
||||
private OperatorLogQueryDTO buildQueryInfo(HostSftpLogQueryRequest request) {
|
||||
Long hostId = request.getHostId();
|
||||
String type = request.getType();
|
||||
// 构建参数
|
||||
OperatorLogQueryDTO query = OperatorLogQueryDTO.builder()
|
||||
.userId(request.getUserId())
|
||||
.result(request.getResult())
|
||||
.startTimeStart(Arrays1.getIfPresent(request.getStartTimeRange(), 0))
|
||||
.startTimeEnd(Arrays1.getIfPresent(request.getStartTimeRange(), 1))
|
||||
.build();
|
||||
query.setPage(request.getPage());
|
||||
query.setLimit(request.getLimit());
|
||||
if (Strings.isBlank(type)) {
|
||||
// 查询全部 SFTP 类型
|
||||
query.setTypeList(HostTerminalOperatorType.SFTP_TYPES);
|
||||
} else {
|
||||
query.setType(type);
|
||||
}
|
||||
// 模糊查询
|
||||
if (hostId != null) {
|
||||
query.setExtra("\"hostId\": " + hostId + ",");
|
||||
}
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.orion.ops.module.infra.api;
|
||||
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogDTO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogQueryDTO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 操作日志服务
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/4 23:11
|
||||
*/
|
||||
public interface OperatorLogApi {
|
||||
|
||||
/**
|
||||
* 分页查询操作日志
|
||||
*
|
||||
* @param request request
|
||||
* @return rows
|
||||
*/
|
||||
DataGrid<OperatorLogDTO> getOperatorLogPage(OperatorLogQueryDTO request);
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
*
|
||||
* @param idList idList
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteOperatorLog(List<Long> idList);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.orion.ops.module.infra.entity.dto.operator;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 操作日志 业务对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-10-10 17:08
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(name = "OperatorLogDTO", description = "操作日志 业务对象")
|
||||
public class OperatorLogDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户id")
|
||||
private Long userId;
|
||||
|
||||
@Schema(description = "用户名")
|
||||
private String username;
|
||||
|
||||
@Schema(description = "traceId")
|
||||
private String traceId;
|
||||
|
||||
@Schema(description = "请求ip")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "请求地址")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "userAgent")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "风险等级")
|
||||
private String riskLevel;
|
||||
|
||||
@Schema(description = "模块")
|
||||
private String module;
|
||||
|
||||
@Schema(description = "操作类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "日志")
|
||||
private String logInfo;
|
||||
|
||||
@Schema(description = "参数")
|
||||
private String extra;
|
||||
|
||||
@Schema(description = "操作结果 0失败 1成功")
|
||||
private Integer result;
|
||||
|
||||
@Schema(description = "错误信息")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "返回值")
|
||||
private String returnValue;
|
||||
|
||||
@Schema(description = "操作时间")
|
||||
private Integer duration;
|
||||
|
||||
@Schema(description = "开始时间")
|
||||
private Date startTime;
|
||||
|
||||
@Schema(description = "结束时间")
|
||||
private Date endTime;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.orion.ops.module.infra.entity.dto.operator;
|
||||
|
||||
import com.orion.ops.framework.common.entity.PageRequest;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.*;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 操作日志 查询对象
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-10-10 17:08
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Schema(name = "OperatorLogQueryDTO", description = "操作日志 查询对象")
|
||||
public class OperatorLogQueryDTO extends PageRequest {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "id")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "用户id")
|
||||
private Long userId;
|
||||
|
||||
@Size(max = 32)
|
||||
@Schema(description = "模块")
|
||||
private String module;
|
||||
|
||||
@Size(max = 1)
|
||||
@Schema(description = "风险等级")
|
||||
private String riskLevel;
|
||||
|
||||
@Size(max = 64)
|
||||
@Schema(description = "操作类型")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "操作类型")
|
||||
private List<String> typeList;
|
||||
|
||||
@Schema(description = "参数")
|
||||
private String extra;
|
||||
|
||||
@Schema(description = "操作结果 0失败 1成功")
|
||||
private Integer result;
|
||||
|
||||
@Schema(description = "开始时间区间 - 开始")
|
||||
private Date startTimeStart;
|
||||
|
||||
@Schema(description = "开始时间区间 - 结束")
|
||||
private Date startTimeEnd;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.orion.ops.module.infra.api.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.framework.common.utils.Valid;
|
||||
import com.orion.ops.module.infra.api.OperatorLogApi;
|
||||
import com.orion.ops.module.infra.convert.OperatorLogProviderConvert;
|
||||
import com.orion.ops.module.infra.dao.OperatorLogDAO;
|
||||
import com.orion.ops.module.infra.entity.domain.OperatorLogDO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogDTO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogQueryDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 操作日志服务实现
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/4 23:18
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class OperatorLogApiImpl implements OperatorLogApi {
|
||||
|
||||
@Resource
|
||||
private OperatorLogDAO operatorLogDAO;
|
||||
|
||||
@Override
|
||||
public DataGrid<OperatorLogDTO> getOperatorLogPage(OperatorLogQueryDTO request) {
|
||||
Valid.valid(request);
|
||||
return operatorLogDAO.of()
|
||||
.page(request)
|
||||
.wrapper(this.buildQueryWrapper(request))
|
||||
.dataGrid(OperatorLogProviderConvert.MAPPER::to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deleteOperatorLog(List<Long> idList) {
|
||||
return operatorLogDAO.deleteBatchIds(idList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询 wrapper
|
||||
*
|
||||
* @param request request
|
||||
* @return wrapper
|
||||
*/
|
||||
private LambdaQueryWrapper<OperatorLogDO> buildQueryWrapper(OperatorLogQueryDTO request) {
|
||||
return operatorLogDAO.wrapper()
|
||||
.eq(OperatorLogDO::getId, request.getId())
|
||||
.eq(OperatorLogDO::getUserId, request.getUserId())
|
||||
.eq(OperatorLogDO::getRiskLevel, request.getRiskLevel())
|
||||
.eq(OperatorLogDO::getModule, request.getModule())
|
||||
.eq(OperatorLogDO::getType, request.getType())
|
||||
.in(OperatorLogDO::getType, request.getTypeList())
|
||||
.eq(OperatorLogDO::getResult, request.getResult())
|
||||
.like(OperatorLogDO::getExtra, request.getExtra())
|
||||
.ge(OperatorLogDO::getStartTime, request.getStartTimeStart())
|
||||
.le(OperatorLogDO::getStartTime, request.getStartTimeEnd())
|
||||
.orderByDesc(OperatorLogDO::getId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.orion.ops.module.infra.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 应用认证配置
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024/3/5 18:26
|
||||
*/
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties("app.authentication")
|
||||
public class AppAuthenticationConfig {
|
||||
|
||||
/**
|
||||
* 是否允许多端登录
|
||||
*/
|
||||
private Boolean allowMultiDevice;
|
||||
|
||||
/**
|
||||
* 是否允许凭证续签
|
||||
*/
|
||||
private Boolean allowRefresh;
|
||||
|
||||
/**
|
||||
* 凭证续签最大次数
|
||||
*/
|
||||
private Integer maxRefreshCount;
|
||||
|
||||
/**
|
||||
* 登录失败锁定次数
|
||||
*/
|
||||
private Integer loginFailedLockCount;
|
||||
|
||||
/**
|
||||
* 登录失败锁定时间 (分)
|
||||
*/
|
||||
private Integer loginFailedLockTime;
|
||||
|
||||
}
|
||||
@@ -14,9 +14,4 @@ Authorization: {{token}}
|
||||
"endTime": ""
|
||||
}
|
||||
|
||||
### 查询登录日志
|
||||
GET {{baseUrl}}/infra/operator-log/login-history?username=admin
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
###
|
||||
@@ -1,16 +1,17 @@
|
||||
package com.orion.ops.module.infra.controller;
|
||||
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.OperatorLog;
|
||||
import com.orion.ops.framework.common.validator.group.Page;
|
||||
import com.orion.ops.framework.log.core.annotation.IgnoreLog;
|
||||
import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
|
||||
import com.orion.ops.framework.security.core.utils.SecurityUtils;
|
||||
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||
import com.orion.ops.module.infra.define.operator.OperatorLogOperatorType;
|
||||
import com.orion.ops.module.infra.entity.request.operator.OperatorLogQueryRequest;
|
||||
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
|
||||
import com.orion.ops.module.infra.entity.vo.OperatorLogVO;
|
||||
import com.orion.ops.module.infra.service.OperatorLogService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -47,12 +48,27 @@ public class OperatorLogController {
|
||||
return operatorLogService.getOperatorLogPage(request);
|
||||
}
|
||||
|
||||
@IgnoreLog(IgnoreLogMode.RET)
|
||||
@GetMapping("/login-history")
|
||||
@Operation(summary = "查询用户登录日志")
|
||||
@PreAuthorize("@ss.hasPermission('infra:system-user:login-history')")
|
||||
public List<LoginHistoryVO> getLoginHistory(@RequestParam("username") String username) {
|
||||
return operatorLogService.getLoginHistory(username);
|
||||
@OperatorLog(OperatorLogOperatorType.DELETE)
|
||||
@DeleteMapping("/delete")
|
||||
@Operation(summary = "删除操作日志")
|
||||
@Parameter(name = "idList", description = "idList", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('infra:operator-log:delete')")
|
||||
public Integer deleteOperatorLog(@RequestParam("idList") List<Long> idList) {
|
||||
return operatorLogService.deleteOperatorLog(idList);
|
||||
}
|
||||
|
||||
@PostMapping("/query-count")
|
||||
@Operation(summary = "查询操作日志数量")
|
||||
public Long getOperatorLogCount(@RequestBody OperatorLogQueryRequest request) {
|
||||
return operatorLogService.getOperatorLogCount(request);
|
||||
}
|
||||
|
||||
@OperatorLog(OperatorLogOperatorType.CLEAR)
|
||||
@PostMapping("/clear")
|
||||
@Operation(summary = "清空操作日志")
|
||||
@PreAuthorize("@ss.hasPermission('infra:operator-log:clear')")
|
||||
public Integer clearOperatorLog(@RequestBody OperatorLogQueryRequest request) {
|
||||
return operatorLogService.clearOperatorLog(request);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -67,4 +67,10 @@ DELETE {{baseUrl}}/infra/system-user/delete?id=1
|
||||
Authorization: {{token}}
|
||||
|
||||
|
||||
### 查询登录日志
|
||||
GET {{baseUrl}}/infra/system-user/login-history?username=admin
|
||||
Content-Type: application/json
|
||||
Authorization: {{token}}
|
||||
|
||||
|
||||
###
|
||||
|
||||
@@ -10,8 +10,10 @@ import com.orion.ops.framework.log.core.enums.IgnoreLogMode;
|
||||
import com.orion.ops.framework.web.core.annotation.RestWrapper;
|
||||
import com.orion.ops.module.infra.define.operator.SystemUserOperatorType;
|
||||
import com.orion.ops.module.infra.entity.request.user.*;
|
||||
import com.orion.ops.module.infra.entity.vo.LoginHistoryVO;
|
||||
import com.orion.ops.module.infra.entity.vo.SystemUserVO;
|
||||
import com.orion.ops.module.infra.entity.vo.UserSessionVO;
|
||||
import com.orion.ops.module.infra.service.OperatorLogService;
|
||||
import com.orion.ops.module.infra.service.SystemUserManagementService;
|
||||
import com.orion.ops.module.infra.service.SystemUserRoleService;
|
||||
import com.orion.ops.module.infra.service.SystemUserService;
|
||||
@@ -51,6 +53,9 @@ public class SystemUserController {
|
||||
@Resource
|
||||
private SystemUserManagementService systemUserManagementService;
|
||||
|
||||
@Resource
|
||||
private OperatorLogService operatorLogService;
|
||||
|
||||
@OperatorLog(SystemUserOperatorType.CREATE)
|
||||
@PostMapping("/create")
|
||||
@Operation(summary = "创建用户")
|
||||
@@ -159,5 +164,12 @@ public class SystemUserController {
|
||||
return HttpWrapper.ok();
|
||||
}
|
||||
|
||||
}
|
||||
@IgnoreLog(IgnoreLogMode.RET)
|
||||
@GetMapping("/login-history")
|
||||
@Operation(summary = "查询用户登录日志")
|
||||
@PreAuthorize("@ss.hasPermission('infra:system-user:login-history')")
|
||||
public List<LoginHistoryVO> getLoginHistory(@RequestParam("username") String username) {
|
||||
return operatorLogService.getLoginHistory(username);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.orion.ops.module.infra.convert;
|
||||
|
||||
import com.orion.ops.module.infra.entity.domain.OperatorLogDO;
|
||||
import com.orion.ops.module.infra.entity.dto.operator.OperatorLogDTO;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
/**
|
||||
* 操作日志 对外对象转换器
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2023-10-10 17:08
|
||||
*/
|
||||
@Mapper
|
||||
public interface OperatorLogProviderConvert {
|
||||
|
||||
OperatorLogProviderConvert MAPPER = Mappers.getMapper(OperatorLogProviderConvert.class);
|
||||
|
||||
OperatorLogDTO to(OperatorLogDO domain);
|
||||
|
||||
}
|
||||
@@ -38,7 +38,6 @@ public interface UserCacheKeyDefine {
|
||||
.desc("用户登录失败次数 ${username}")
|
||||
.type(Integer.class)
|
||||
.struct(RedisCacheStruct.STRING)
|
||||
.timeout(3, TimeUnit.DAYS)
|
||||
.build();
|
||||
|
||||
CacheKeyDefine LOGIN_TOKEN = new CacheKeyBuilder()
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.orion.ops.module.infra.define.operator;
|
||||
|
||||
import com.orion.ops.framework.biz.operator.log.core.annotation.Module;
|
||||
import com.orion.ops.framework.biz.operator.log.core.factory.InitializingOperatorTypes;
|
||||
import com.orion.ops.framework.biz.operator.log.core.model.OperatorType;
|
||||
|
||||
import static com.orion.ops.framework.biz.operator.log.core.enums.OperatorRiskLevel.H;
|
||||
|
||||
/**
|
||||
* 操作日志 操作日志类型
|
||||
*
|
||||
* @author Jiahang Li
|
||||
* @version 1.0.0
|
||||
* @since 2024-3-4 16:20
|
||||
*/
|
||||
@Module("infra:operator-log")
|
||||
public class OperatorLogOperatorType extends InitializingOperatorTypes {
|
||||
|
||||
public static final String DELETE = "operator-log:delete";
|
||||
|
||||
public static final String CLEAR = "operator-log:clear";
|
||||
|
||||
@Override
|
||||
public OperatorType[] types() {
|
||||
return new OperatorType[]{
|
||||
new OperatorType(H, DELETE, "删除操作日志 <sb>${count}</sb> 条"),
|
||||
new OperatorType(H, CLEAR, "清空操作日志 <sb>${count}</sb> 条"),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,16 +16,6 @@ import javax.servlet.http.HttpServletRequest;
|
||||
*/
|
||||
public interface AuthenticationService {
|
||||
|
||||
// TODO 配置化
|
||||
// 允许多端登录
|
||||
boolean allowMultiDevice = true;
|
||||
// 允许凭证续签
|
||||
boolean allowRefresh = true;
|
||||
// 凭证续签最大次数
|
||||
int maxRefreshCount = 3;
|
||||
// 失败锁定次数
|
||||
int maxFailedLoginCount = 5;
|
||||
|
||||
/**
|
||||
* 登录
|
||||
*
|
||||
|
||||
@@ -32,6 +32,30 @@ public interface OperatorLogService {
|
||||
*/
|
||||
DataGrid<OperatorLogVO> getOperatorLogPage(OperatorLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 删除操作日志
|
||||
*
|
||||
* @param idList idList
|
||||
* @return effect
|
||||
*/
|
||||
Integer deleteOperatorLog(List<Long> idList);
|
||||
|
||||
/**
|
||||
* 查询操作日志数量
|
||||
*
|
||||
* @param request request
|
||||
* @return count
|
||||
*/
|
||||
Long getOperatorLogCount(OperatorLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 清空操作日志
|
||||
*
|
||||
* @param request request
|
||||
* @return effect
|
||||
*/
|
||||
Integer clearOperatorLog(OperatorLogQueryRequest request);
|
||||
|
||||
/**
|
||||
* 查询用户登录日志
|
||||
*
|
||||
|
||||
@@ -16,6 +16,7 @@ import com.orion.ops.framework.common.utils.Valid;
|
||||
import com.orion.ops.framework.redis.core.utils.RedisStrings;
|
||||
import com.orion.ops.framework.redis.core.utils.RedisUtils;
|
||||
import com.orion.ops.framework.security.core.utils.SecurityUtils;
|
||||
import com.orion.ops.module.infra.config.AppAuthenticationConfig;
|
||||
import com.orion.ops.module.infra.convert.SystemUserConvert;
|
||||
import com.orion.ops.module.infra.dao.SystemUserDAO;
|
||||
import com.orion.ops.module.infra.dao.SystemUserRoleDAO;
|
||||
@@ -39,6 +40,7 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -51,6 +53,9 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
|
||||
@Resource
|
||||
private AppAuthenticationConfig appAuthenticationConfig;
|
||||
|
||||
@Resource
|
||||
private SystemUserDAO systemUserDAO;
|
||||
|
||||
@@ -95,7 +100,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
String userAgent = Servlets.getUserAgent(servletRequest);
|
||||
long current = System.currentTimeMillis();
|
||||
// 不允许多端登录
|
||||
if (!allowMultiDevice) {
|
||||
if (!appAuthenticationConfig.getAllowMultiDevice()) {
|
||||
// 无效化其他缓存
|
||||
this.invalidOtherDeviceToken(user.getId(), current, remoteAddr, location, userAgent);
|
||||
}
|
||||
@@ -157,7 +162,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
return JSON.parseObject(loginCache, LoginTokenDTO.class);
|
||||
}
|
||||
// loginToken 不存在 需要查询 refreshToken
|
||||
if (!allowRefresh) {
|
||||
if (!appAuthenticationConfig.getAllowRefresh()) {
|
||||
return null;
|
||||
}
|
||||
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(pair.getKey(), pair.getValue());
|
||||
@@ -172,7 +177,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
refresh.setRefreshCount(refreshCount);
|
||||
// 设置登录缓存
|
||||
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, refresh);
|
||||
if (refreshCount < maxRefreshCount) {
|
||||
if (refreshCount < appAuthenticationConfig.getMaxRefreshCount()) {
|
||||
// 小于续签最大次数 则再次设置 refreshToken
|
||||
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, refresh);
|
||||
} else {
|
||||
@@ -214,7 +219,8 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
// 检查登录失败次数
|
||||
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(request.getUsername());
|
||||
String failedCount = redisTemplate.opsForValue().get(failedCountKey);
|
||||
if (failedCount != null && Integer.parseInt(failedCount) >= maxFailedLoginCount) {
|
||||
if (failedCount != null
|
||||
&& Integer.parseInt(failedCount) >= appAuthenticationConfig.getLoginFailedLockCount()) {
|
||||
throw Exceptions.argument(ErrorMessage.MAX_LOGIN_FAILED);
|
||||
}
|
||||
}
|
||||
@@ -235,23 +241,23 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
// 刷新登录失败缓存
|
||||
String failedCountKey = UserCacheKeyDefine.LOGIN_FAILED_COUNT.format(request.getUsername());
|
||||
Long failedLoginCount = redisTemplate.opsForValue().increment(failedCountKey);
|
||||
RedisUtils.setExpire(failedCountKey, UserCacheKeyDefine.LOGIN_FAILED_COUNT);
|
||||
// 锁定用户
|
||||
if (failedLoginCount >= maxFailedLoginCount) {
|
||||
// 更新用户表
|
||||
SystemUserDO updateUser = new SystemUserDO();
|
||||
updateUser.setId(user.getId());
|
||||
updateUser.setStatus(UserStatusEnum.LOCKED.getStatus());
|
||||
systemUserDAO.updateById(updateUser);
|
||||
// 修改缓存状态
|
||||
String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
|
||||
String userInfoCache = redisTemplate.opsForValue().get(userInfoKey);
|
||||
if (userInfoCache != null) {
|
||||
LoginUser loginUser = JSON.parseObject(userInfoCache, LoginUser.class);
|
||||
loginUser.setStatus(UserStatusEnum.LOCKED.getStatus());
|
||||
RedisStrings.setJson(userInfoKey, UserCacheKeyDefine.USER_INFO, loginUser);
|
||||
}
|
||||
}
|
||||
RedisUtils.setExpire(failedCountKey, appAuthenticationConfig.getLoginFailedLockTime(), TimeUnit.MINUTES);
|
||||
// // 锁定用户
|
||||
// if (failedLoginCount >= appAuthenticationConfig.getLoginFailedLockCount()) {
|
||||
// // 更新用户表
|
||||
// SystemUserDO updateUser = new SystemUserDO();
|
||||
// updateUser.setId(user.getId());
|
||||
// updateUser.setStatus(UserStatusEnum.LOCKED.getStatus());
|
||||
// systemUserDAO.updateById(updateUser);
|
||||
// // 修改缓存状态
|
||||
// String userInfoKey = UserCacheKeyDefine.USER_INFO.format(user.getId());
|
||||
// String userInfoCache = redisTemplate.opsForValue().get(userInfoKey);
|
||||
// if (userInfoCache != null) {
|
||||
// LoginUser loginUser = JSON.parseObject(userInfoCache, LoginUser.class);
|
||||
// loginUser.setStatus(UserStatusEnum.LOCKED.getStatus());
|
||||
// RedisStrings.setJson(userInfoKey, UserCacheKeyDefine.USER_INFO, loginUser);
|
||||
// }
|
||||
// }
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -337,7 +343,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
}
|
||||
}
|
||||
// 删除续签信息
|
||||
if (allowRefresh) {
|
||||
if (appAuthenticationConfig.getAllowRefresh()) {
|
||||
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
|
||||
}
|
||||
}
|
||||
@@ -365,7 +371,7 @@ public class AuthenticationServiceImpl implements AuthenticationService {
|
||||
.build();
|
||||
RedisStrings.setJson(loginKey, UserCacheKeyDefine.LOGIN_TOKEN, loginValue);
|
||||
// 生成 refreshToken
|
||||
if (allowRefresh) {
|
||||
if (appAuthenticationConfig.getAllowRefresh()) {
|
||||
String refreshKey = UserCacheKeyDefine.LOGIN_REFRESH.format(id, loginTime);
|
||||
RedisStrings.setJson(refreshKey, UserCacheKeyDefine.LOGIN_REFRESH, loginValue);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.orion.ops.module.infra.service.impl;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.orion.lang.define.wrapper.DataGrid;
|
||||
import com.orion.lang.utils.Arrays1;
|
||||
import com.orion.ops.framework.biz.operator.log.core.model.OperatorLogModel;
|
||||
import com.orion.ops.framework.biz.operator.log.core.utils.OperatorLogs;
|
||||
import com.orion.ops.framework.common.constant.Const;
|
||||
import com.orion.ops.module.infra.convert.OperatorLogConvert;
|
||||
import com.orion.ops.module.infra.dao.OperatorLogDAO;
|
||||
@@ -51,6 +53,33 @@ public class OperatorLogServiceImpl implements OperatorLogService {
|
||||
.dataGrid(OperatorLogConvert.MAPPER::to);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer deleteOperatorLog(List<Long> idList) {
|
||||
log.info("OperatorLogService.deleteOperatorLog start {}", JSON.toJSONString(idList));
|
||||
int effect = operatorLogDAO.deleteBatchIds(idList);
|
||||
log.info("OperatorLogService.deleteOperatorLog finish {}", effect);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.COUNT, effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getOperatorLogCount(OperatorLogQueryRequest request) {
|
||||
return operatorLogDAO.selectCount(this.buildQueryWrapper(request));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer clearOperatorLog(OperatorLogQueryRequest request) {
|
||||
log.info("OperatorLogService.clearOperatorLog start {}", JSON.toJSONString(request));
|
||||
// 删除
|
||||
LambdaQueryWrapper<OperatorLogDO> wrapper = this.buildQueryWrapper(request);
|
||||
int effect = operatorLogDAO.delete(wrapper);
|
||||
log.info("OperatorLogService.clearOperatorLog finish {}", effect);
|
||||
// 设置日志参数
|
||||
OperatorLogs.add(OperatorLogs.COUNT, effect);
|
||||
return effect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<LoginHistoryVO> getLoginHistory(String username) {
|
||||
// 条件
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.orion.ops.framework.redis.core.utils.RedisStrings;
|
||||
import com.orion.ops.framework.redis.core.utils.RedisUtils;
|
||||
import com.orion.ops.framework.redis.core.utils.barrier.CacheBarriers;
|
||||
import com.orion.ops.framework.security.core.utils.SecurityUtils;
|
||||
import com.orion.ops.module.infra.config.AppAuthenticationConfig;
|
||||
import com.orion.ops.module.infra.convert.SystemUserConvert;
|
||||
import com.orion.ops.module.infra.dao.OperatorLogDAO;
|
||||
import com.orion.ops.module.infra.dao.SystemRoleDAO;
|
||||
@@ -49,6 +50,9 @@ import java.util.stream.Collectors;
|
||||
@Service
|
||||
public class SystemUserServiceImpl implements SystemUserService {
|
||||
|
||||
@Resource
|
||||
private AppAuthenticationConfig appAuthenticationConfig;
|
||||
|
||||
@Resource
|
||||
private SystemUserDAO systemUserDAO;
|
||||
|
||||
@@ -274,7 +278,7 @@ public class SystemUserServiceImpl implements SystemUserService {
|
||||
// 删除登录缓存
|
||||
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_TOKEN.format(id, "*"));
|
||||
// 删除续签信息
|
||||
if (AuthenticationService.allowRefresh) {
|
||||
if (appAuthenticationConfig.getAllowRefresh()) {
|
||||
RedisUtils.scanKeysDelete(UserCacheKeyDefine.LOGIN_REFRESH.format(id, "*"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion/api'
|
||||
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion/keep-alive'
|
||||
VITE_APP_VERSION= '1.0.0'
|
||||
VITE_APP_VERSION= '1.0.1'
|
||||
VITE_SFTP_PREVIEW_MB= 2
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
VITE_API_BASE_URL= '/orion/api'
|
||||
VITE_WS_BASE_URL= '/orion/keep-alive'
|
||||
VITE_APP_VERSION= '1.0.0'
|
||||
VITE_APP_VERSION= '1.0.1'
|
||||
VITE_SFTP_PREVIEW_MB= 2
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "orion-ops-pro-ui",
|
||||
"description": "Orion Ops Pro for Vue",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"private": true,
|
||||
"author": "Jiahang Li",
|
||||
"license": "Apache 2.0",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { DataGrid, Pagination } from '@/types/global';
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
/**
|
||||
* 主机连接日志查询请求
|
||||
*/
|
||||
export interface HostConnectLogQueryRequest extends Pagination {
|
||||
id?: number;
|
||||
userId?: number;
|
||||
hostId?: number;
|
||||
hostAddress?: string;
|
||||
@@ -21,6 +23,7 @@ export interface HostConnectLogQueryRequest extends Pagination {
|
||||
export interface HostConnectLogQueryResponse extends TableData {
|
||||
id: number;
|
||||
userId: number;
|
||||
username: number;
|
||||
hostId: number;
|
||||
hostName: string;
|
||||
hostAddress: string;
|
||||
@@ -29,11 +32,20 @@ export interface HostConnectLogQueryResponse extends TableData {
|
||||
status: string;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
extraInfo: string;
|
||||
createTime: number;
|
||||
updateTime: number;
|
||||
creator: string;
|
||||
updater: string;
|
||||
extra: HostConnectLogExtra;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主机连接日志拓展对象
|
||||
*/
|
||||
export interface HostConnectLogExtra {
|
||||
traceId: string;
|
||||
channelId: string;
|
||||
sessionId: string;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,3 +64,36 @@ export function getLatestConnectHostId(type: string, limit: number) {
|
||||
limit
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除主机连接日志
|
||||
*/
|
||||
export function deleteHostConnectLog(idList: Array<number>) {
|
||||
return axios.delete('/asset/host-connect-log/delete', {
|
||||
params: { idList },
|
||||
paramsSerializer: params => {
|
||||
return qs.stringify(params, { arrayFormat: 'comma' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询主机连接日志数量
|
||||
*/
|
||||
export function getHostConnectLogCount(request: HostConnectLogQueryRequest) {
|
||||
return axios.post<number>('/asset/host-connect-log/query-count', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空主机连接日志
|
||||
*/
|
||||
export function clearHostConnectLog(request: HostConnectLogQueryRequest) {
|
||||
return axios.post<number>('/asset/host-connect-log/clear', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制断开主机连接
|
||||
*/
|
||||
export function hostForceOffline(request: HostConnectLogQueryRequest) {
|
||||
return axios.put('/asset/host-connect-log/force-offline', request);
|
||||
}
|
||||
|
||||
62
orion-ops-ui/src/api/asset/host-sftp-log.ts
Normal file
62
orion-ops-ui/src/api/asset/host-sftp-log.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import type { DataGrid, Pagination } from '@/types/global';
|
||||
import type { TableData } from '@arco-design/web-vue/es/table/interface';
|
||||
import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 查询请求
|
||||
*/
|
||||
export interface HostSftpLogQueryRequest extends Pagination {
|
||||
userId?: number;
|
||||
hostId?: number;
|
||||
type?: string;
|
||||
result?: number;
|
||||
startTimeRange?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 查询响应
|
||||
*/
|
||||
export interface HostSftpLogQueryResponse extends TableData {
|
||||
id: number;
|
||||
userId: number;
|
||||
username: number;
|
||||
hostId: number;
|
||||
hostName: string;
|
||||
hostAddress: string;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
paths: string[];
|
||||
type: string;
|
||||
result: string;
|
||||
startTime: number;
|
||||
extra: HostSftpLogExtra;
|
||||
}
|
||||
|
||||
/**
|
||||
* SFTP 操作日志 拓展对象
|
||||
*/
|
||||
export interface HostSftpLogExtra {
|
||||
mod: number;
|
||||
target: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询 SFTP 操作日志
|
||||
*/
|
||||
export function getHostSftpLogPage(request: HostSftpLogQueryRequest) {
|
||||
return axios.post<DataGrid<HostSftpLogQueryResponse>>('/asset/host-sftp-log/query', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 SFTP 操作日志
|
||||
*/
|
||||
export function deleteHostSftpLog(idList: Array<number>) {
|
||||
return axios.delete('/asset/host-sftp-log/delete', {
|
||||
params: { idList },
|
||||
paramsSerializer: params => {
|
||||
return qs.stringify(params, { arrayFormat: 'comma' });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataGrid } from '@/types/global';
|
||||
import type { LoginHistoryQueryResponse, OperatorLogQueryRequest, OperatorLogQueryResponse } from './operator-log';
|
||||
import type { UserQueryResponse, UserSessionOfflineRequest, UserSessionQueryResponse, UserUpdateRequest } from './user';
|
||||
import type { OperatorLogQueryRequest, OperatorLogQueryResponse } from './operator-log';
|
||||
import type { LoginHistoryQueryResponse, UserQueryResponse, UserSessionOfflineRequest, UserSessionQueryResponse, UserUpdateRequest } from './user';
|
||||
import axios from 'axios';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { DataGrid, Pagination } from '@/types/global';
|
||||
import axios from 'axios';
|
||||
import qs from 'query-string';
|
||||
|
||||
/**
|
||||
* 操作日志查询参数
|
||||
@@ -40,19 +41,6 @@ export interface OperatorLogQueryResponse {
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录日志查询响应
|
||||
*/
|
||||
export interface LoginHistoryQueryResponse {
|
||||
id: number;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
result: number;
|
||||
errorMessage: string;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页操作日志
|
||||
*/
|
||||
@@ -61,8 +49,27 @@ export function getOperatorLogPage(request: OperatorLogQueryRequest) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录日志
|
||||
* 删除操作日志
|
||||
*/
|
||||
export function getLoginHistory(username: string) {
|
||||
return axios.get<LoginHistoryQueryResponse[]>('/infra/operator-log/login-history', { params: { username } });
|
||||
export function deleteOperatorLog(idList: Array<number>) {
|
||||
return axios.delete('/infra/operator-log/delete', {
|
||||
params: { idList },
|
||||
paramsSerializer: params => {
|
||||
return qs.stringify(params, { arrayFormat: 'comma' });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询操作日志数量
|
||||
*/
|
||||
export function getOperatorLogCount(request: OperatorLogQueryRequest) {
|
||||
return axios.post<number>('/infra/operator-log/query-count', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空操作日志
|
||||
*/
|
||||
export function clearOperatorLog(request: OperatorLogQueryRequest) {
|
||||
return axios.post<number>('/infra/operator-log/clear', request);
|
||||
}
|
||||
|
||||
@@ -77,6 +77,19 @@ export interface UserSessionOfflineRequest {
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录日志查询响应
|
||||
*/
|
||||
export interface LoginHistoryQueryResponse {
|
||||
id: number;
|
||||
address: string;
|
||||
location: string;
|
||||
userAgent: string;
|
||||
result: number;
|
||||
errorMessage: string;
|
||||
createTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
@@ -160,3 +173,10 @@ export function getUserSessionList(id: number) {
|
||||
export function offlineUserSession(request: UserSessionOfflineRequest) {
|
||||
return axios.put('/infra/system-user/session/offline', request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询登录日志
|
||||
*/
|
||||
export function getLoginHistory(username: string) {
|
||||
return axios.get<LoginHistoryQueryResponse[]>('/infra/system-user/login-history', { params: { username } });
|
||||
}
|
||||
|
||||
@@ -230,6 +230,20 @@ body {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.text-ellipsis {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-copy {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: rgb(var(--arcoblue-6))
|
||||
}
|
||||
}
|
||||
|
||||
.copy-left, .copy-right {
|
||||
color: rgb(var(--arcoblue-6));
|
||||
cursor: pointer;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<a-layout-footer class="footer">
|
||||
<a-space direction="vertical" size="medium">
|
||||
<a-space direction="vertical" size="small">
|
||||
<a-space size="large">
|
||||
<!-- <a-link target="_blank" href="https://github.com/lijiahangmax/orion-ops-pro">官网</a-link> -->
|
||||
<!-- <a-link target="_blank" href="https://github.com/lijiahangmax/orion-ops-pro">教程</a-link> -->
|
||||
|
||||
@@ -58,10 +58,11 @@
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteNode(node.key)">
|
||||
<span v-permission="['asset:host-group:delete']"
|
||||
class="tree-icon" title="删除">
|
||||
<icon-delete />
|
||||
</span>
|
||||
<span v-permission="['asset:host-group:delete']"
|
||||
class="tree-icon"
|
||||
title="删除">
|
||||
<icon-delete />
|
||||
</span>
|
||||
</a-popconfirm>
|
||||
<!-- 新增 -->
|
||||
<span v-permission="['asset:host-group:create']"
|
||||
|
||||
@@ -29,9 +29,7 @@
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
|
||||
23
orion-ops-ui/src/router/routes/modules/asset-audit.ts
Normal file
23
orion-ops-ui/src/router/routes/modules/asset-audit.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
|
||||
const ASSET_AUDIT: AppRouteRecordRaw =
|
||||
{
|
||||
name: 'assetAudit',
|
||||
path: '/asset-audit',
|
||||
component: DEFAULT_LAYOUT,
|
||||
children: [
|
||||
{
|
||||
name: 'assetAuditConnectLog',
|
||||
path: '/asset-audit/connect-log',
|
||||
component: () => import('@/views/asset-audit/connect-log/index.vue'),
|
||||
},
|
||||
{
|
||||
name: 'assetAuditSftpLog',
|
||||
path: '/asset-audit/sftp-log',
|
||||
component: () => import('@/views/asset-audit/sftp-log/index.vue'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default ASSET_AUDIT;
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
|
||||
const HOST_AUDIT: AppRouteRecordRaw =
|
||||
{
|
||||
name: 'hostAudit',
|
||||
path: '/host-audit',
|
||||
component: DEFAULT_LAYOUT,
|
||||
children: [
|
||||
{
|
||||
name: 'hostAuditConnectLog',
|
||||
path: '/host-audit/connect-log',
|
||||
component: () => import('@/views/host-audit/connect-log/index.vue'),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default HOST_AUDIT;
|
||||
@@ -0,0 +1,173 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form"
|
||||
title-align="start"
|
||||
title="清空主机连接日志"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="清空"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
label-align="right"
|
||||
:style="{ width: '460px' }"
|
||||
:label-col-props="{ span: 5 }"
|
||||
:wrapper-col-props="{ span: 19 }">
|
||||
<!-- 连接用户 -->
|
||||
<a-form-item field="userId" label="连接用户">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 连接主机 -->
|
||||
<a-form-item field="hostId" label="连接主机">
|
||||
<host-selector v-model="formModel.hostId"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="hostAddress" label="主机地址">
|
||||
<a-input v-model="formModel.hostAddress"
|
||||
placeholder="请输入主机地址"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 连接状态 -->
|
||||
<a-form-item field="status" label="连接状态">
|
||||
<a-select v-model="formModel.status"
|
||||
placeholder="请选择状态"
|
||||
:options="toOptions(connectStatusKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 连接类型 -->
|
||||
<a-form-item field="type" label="连接类型">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(connectTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 开始时间 -->
|
||||
<a-form-item field="startTimeRange" label="开始时间">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
style="width: 100%"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetAuditConnectLogClearModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConnectLogQueryRequest } from '@/api/asset/host-connect-log';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { connectStatusKey, connectTypeKey } from '../types/const';
|
||||
import { getHostConnectLogCount, clearHostConnectLog } from '@/api/asset/host-connect-log';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { useDictStore } from '@/store';
|
||||
import UserSelector from '@/components/user/user/user-selector.vue';
|
||||
import HostSelector from '@/components/asset/host/host-selector.vue';
|
||||
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const defaultForm = (): HostConnectLogQueryRequest => {
|
||||
return {
|
||||
userId: undefined,
|
||||
hostId: undefined,
|
||||
hostAddress: undefined,
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const formModel = ref<HostConnectLogQueryRequest>({});
|
||||
|
||||
const emits = defineEmits(['clear']);
|
||||
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
// 打开
|
||||
const open = (record: any) => {
|
||||
renderForm({ ...defaultForm(), ...record });
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = (record: any) => {
|
||||
formModel.value = Object.assign({}, record);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 获取总数量
|
||||
const { data } = await getHostConnectLogCount(formModel.value);
|
||||
if (data) {
|
||||
// 清空
|
||||
doClear(data);
|
||||
} else {
|
||||
// 无数据
|
||||
Message.warning('当前条件未查询到数据');
|
||||
}
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 执行删除
|
||||
const doClear = (count: number) => {
|
||||
Modal.confirm({
|
||||
title: '删除清空',
|
||||
content: `确定要删除 ${count} 条数据吗? 确定后将立即删除且无法恢复!`,
|
||||
onOk: async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 调用删除
|
||||
await clearHostConnectLog(formModel.value);
|
||||
emits('clear');
|
||||
// 清空
|
||||
setVisible(false);
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,149 @@
|
||||
<template>
|
||||
<a-drawer v-model:visible="visible"
|
||||
title="主机连接日志详情"
|
||||
:width="428"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="关闭"
|
||||
:hide-cancel="true"
|
||||
@cancel="handleClose">
|
||||
<a-descriptions class="detail-container"
|
||||
size="large"
|
||||
:label-style="{ display: 'flex', width: '90px' }"
|
||||
:column="1">
|
||||
<!-- id -->
|
||||
<a-descriptions-item label="id">
|
||||
{{ record.id }}
|
||||
</a-descriptions-item>
|
||||
<!-- 连接用户 -->
|
||||
<a-descriptions-item label="连接用户">
|
||||
<span>({{ record.userId }}) {{ record.username }}</span>
|
||||
</a-descriptions-item>
|
||||
<!-- 连接主机 -->
|
||||
<a-descriptions-item label="连接主机">
|
||||
<span>({{ record.hostId }}) {{ record.hostName }}</span>
|
||||
<br>
|
||||
<span class="host-address text-copy"
|
||||
:title="record.hostAddress"
|
||||
@click="copy(record.hostAddress)">
|
||||
{{ record.hostAddress }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<!-- 连接类型 -->
|
||||
<a-descriptions-item label="连接类型">
|
||||
{{ getDictValue(connectTypeKey, record.type) }}
|
||||
</a-descriptions-item>
|
||||
<!-- 连接状态 -->
|
||||
<a-descriptions-item label="连接状态">
|
||||
{{ getDictValue(connectStatusKey, record.status) }}
|
||||
</a-descriptions-item>
|
||||
<!-- 留痕地址 -->
|
||||
<a-descriptions-item label="留痕地址">
|
||||
<span>{{ record.extra?.location }}</span>
|
||||
<br>
|
||||
<span class="connect-address text-copy"
|
||||
:title="record.extra?.address"
|
||||
@click="copy(record.extra?.address)">
|
||||
{{ record.extra?.address }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<!-- userAgent -->
|
||||
<a-descriptions-item label="userAgent">
|
||||
{{ record.extra?.userAgent }}
|
||||
</a-descriptions-item>
|
||||
<!-- 错误信息 -->
|
||||
<a-descriptions-item v-if="record.extra?.errorMessage" label="错误信息">
|
||||
{{ record.extra?.errorMessage }}
|
||||
</a-descriptions-item>
|
||||
<!-- 开始时间 -->
|
||||
<a-descriptions-item label="开始时间">
|
||||
{{ dateFormat(new Date(record.startTime)) }}
|
||||
</a-descriptions-item>
|
||||
<!-- 结束时间 -->
|
||||
<a-descriptions-item label="结束时间">
|
||||
{{ dateFormat(new Date(record.endTime)) }}
|
||||
</a-descriptions-item>
|
||||
<!-- traceId -->
|
||||
<a-descriptions-item label="traceId">
|
||||
<span class="text-copy" @click="copy(record.extra?.traceId)">
|
||||
{{ record.extra?.traceId }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<!-- channelId -->
|
||||
<a-descriptions-item label="channelId">
|
||||
<span class="text-copy" @click="copy(record.extra?.channelId)">
|
||||
{{ record.extra?.channelId }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
<!-- sessionId -->
|
||||
<a-descriptions-item label="sessionId">
|
||||
<span class="text-copy" @click="copy(record.extra?.sessionId)">
|
||||
{{ record.extra?.sessionId }}
|
||||
</span>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetAuditConnectLogDetailDrawer'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConnectLogQueryResponse } from '@/api/asset/host-connect-log';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { connectStatusKey, connectTypeKey } from '../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import { dateFormat } from '@/utils';
|
||||
import useCopy from '@/hooks/copy';
|
||||
|
||||
const { getDictValue } = useDictStore();
|
||||
const { copy } = useCopy();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const record = ref<HostConnectLogQueryResponse>({} as HostConnectLogQueryResponse);
|
||||
|
||||
const emits = defineEmits(['clear']);
|
||||
|
||||
const { toOptions } = useDictStore();
|
||||
|
||||
// 打开
|
||||
const open = (s: any) => {
|
||||
record.value = s;
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
:deep(.arco-descriptions-item-value) {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.host-address, .connect-address {
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
color: rgb(var(--arcoblue-6));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,319 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
:itemOptions="{ 5: { span: 2 } }"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 连接用户 -->
|
||||
<a-form-item field="userId" label="连接用户" label-col-flex="50px">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 连接主机 -->
|
||||
<a-form-item field="hostId" label="连接主机" label-col-flex="50px">
|
||||
<host-selector v-model="formModel.hostId"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="hostAddress" label="主机地址" label-col-flex="50px">
|
||||
<a-input v-model="formModel.hostAddress" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 状态 -->
|
||||
<a-form-item field="status" label="状态" label-col-flex="50px">
|
||||
<a-select v-model="formModel.status"
|
||||
placeholder="请选择状态"
|
||||
:options="toOptions(connectStatusKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型" label-col-flex="50px">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(connectTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 开始时间 -->
|
||||
<a-form-item field="startTimeRange" label="开始时间" label-col-flex="50px">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
style="width: 100%"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
主机连接日志
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 清空 -->
|
||||
<a-button v-permission="['asset:host-connect-log:management:clear']"
|
||||
status="danger"
|
||||
@click="openClear">
|
||||
清空
|
||||
<template #icon>
|
||||
<icon-close />
|
||||
</template>
|
||||
</a-button>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
|
||||
position="br"
|
||||
type="warning"
|
||||
@ok="deleteSelectRows">
|
||||
<a-button v-permission="['asset:host-connect-log:management:delete']"
|
||||
type="secondary"
|
||||
status="danger"
|
||||
:disabled="selectedKeys.length === 0">
|
||||
删除
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 连接用户 -->
|
||||
<template #username="{ record }">
|
||||
{{ record.username }}
|
||||
</template>
|
||||
<!-- 连接主机 -->
|
||||
<template #hostName="{ record }">
|
||||
<span class="host-name" :title="record.hostName">
|
||||
{{ record.hostName }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="host-address text-copy"
|
||||
:title="record.hostAddress"
|
||||
@click="copy(record.hostAddress)">
|
||||
{{ record.hostAddress }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<span class="circle" :style="{
|
||||
background: getDictValue(connectStatusKey, record.status, 'color')
|
||||
}" />
|
||||
{{ getDictValue(connectStatusKey, record.status) }}
|
||||
</template>
|
||||
<!-- 留痕地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="connect-location" :title="record.extra?.location">
|
||||
{{ record.extra?.location }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="connect-address text-copy"
|
||||
:title="record.extra?.address"
|
||||
@click="copy(record.extra?.address)">
|
||||
{{ record.extra?.address }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 详情 -->
|
||||
<a-button type="text"
|
||||
size="mini"
|
||||
@click="openDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<!-- 下线 -->
|
||||
<a-popconfirm v-if="record.status === HostConnectStatus.CONNECTING"
|
||||
content="确认要强制下线吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="forceOffline(record)">
|
||||
<a-button v-permission="['asset:host-connect-log:management:force-offline']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
下线
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['asset:host-connect-log:management:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
<!-- 清空模态框 -->
|
||||
<connect-log-clear-modal ref="clearModal"
|
||||
@clear="fetchTableData" />
|
||||
<!-- 详情模态框 -->
|
||||
<connect-log-detail-drawer ref="detailModal" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetAuditConnectLogTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConnectLogQueryRequest, HostConnectLogQueryResponse } from '@/api/asset/host-connect-log';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { deleteHostConnectLog, getHostConnectLogPage, hostForceOffline } from '@/api/asset/host-connect-log';
|
||||
import { connectStatusKey, connectTypeKey, HostConnectStatus } from '../types/const';
|
||||
import { usePagination, useRowSelection } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import columns from '../types/table.columns';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useCopy from '@/hooks/copy';
|
||||
import UserSelector from '@/components/user/user/user-selector.vue';
|
||||
import HostSelector from '@/components/asset/host/host-selector.vue';
|
||||
import ConnectLogClearModal from './connect-log-clear-modal.vue';
|
||||
import ConnectLogDetailDrawer from './connect-log-detail-drawer.vue';
|
||||
|
||||
const tableRenderData = ref<HostConnectLogQueryResponse[]>([]);
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
const clearModal = ref();
|
||||
const detailModal = ref();
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { copy } = useCopy();
|
||||
|
||||
const formModel = reactive<HostConnectLogQueryRequest>({
|
||||
userId: undefined,
|
||||
hostId: undefined,
|
||||
hostAddress: undefined,
|
||||
type: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostConnectLogQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostConnectLogPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
selectedKeys.value = [];
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
// 打开清空
|
||||
const openClear = () => {
|
||||
clearModal.value?.open({ ...formModel });
|
||||
};
|
||||
|
||||
// 打开详情
|
||||
const openDetail = (record: HostConnectLogQueryResponse) => {
|
||||
detailModal.value?.open(record);
|
||||
};
|
||||
|
||||
// 强制下线
|
||||
const forceOffline = async (record: HostConnectLogQueryResponse) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await hostForceOffline({ id: record.id });
|
||||
record.status = HostConnectStatus.FORCE_OFFLINE;
|
||||
record.endTime = Date.now();
|
||||
Message.success('已下线');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
const deleteSelectRows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostConnectLog(selectedKeys.value);
|
||||
Message.success(`成功删除 ${selectedKeys.value.length} 条数据`);
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostConnectLog([id]);
|
||||
Message.success('删除成功');
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.host-name, .connect-location {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.host-address, .connect-address {
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
@@ -7,19 +7,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostAuditConnectLog'
|
||||
name: 'assetAuditConnectLog'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import ConnectLogTable from './components/connect-log-table.vue';
|
||||
import { ref, onBeforeMount, onUnmounted } from 'vue';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import ConnectLogTable from './components/connect-log-table.vue';
|
||||
|
||||
const render = ref(false);
|
||||
const table = ref();
|
||||
const modal = ref();
|
||||
|
||||
// 加载字典配置
|
||||
onBeforeMount(async () => {
|
||||
@@ -1,7 +1,15 @@
|
||||
// 主机连接类型
|
||||
export const HostConnectType = {
|
||||
SSH: 'SSH',
|
||||
SFTP: 'SFTP'
|
||||
SFTP: 'SFTP',
|
||||
};
|
||||
|
||||
// 主机连接状态
|
||||
export const HostConnectStatus = {
|
||||
CONNECTING: 'CONNECTING',
|
||||
COMPLETE: 'COMPLETE',
|
||||
FAILED: 'FAILED',
|
||||
FORCE_OFFLINE: 'FORCE_OFFLINE',
|
||||
};
|
||||
|
||||
// 主机连接状态 字典项
|
||||
@@ -3,64 +3,54 @@ import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: 'id',
|
||||
dataIndex: 'id',
|
||||
slotName: 'id',
|
||||
width: 70,
|
||||
align: 'left',
|
||||
fixed: 'left',
|
||||
}, {
|
||||
title: '连接用户',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
width: 140,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '连接主机',
|
||||
dataIndex: 'hostName',
|
||||
slotName: 'hostName',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '主机地址',
|
||||
dataIndex: 'hostAddress',
|
||||
slotName: 'hostAddress',
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 68,
|
||||
width: 74,
|
||||
align: 'left',
|
||||
}, {
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
slotName: 'status',
|
||||
align: 'left',
|
||||
width: 90,
|
||||
width: 106,
|
||||
}, {
|
||||
title: 'token',
|
||||
dataIndex: 'token',
|
||||
slotName: 'token',
|
||||
title: '留痕地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
width: 156,
|
||||
align: 'left',
|
||||
width: 120,
|
||||
ellipsis: true,
|
||||
tooltip: true,
|
||||
}, {
|
||||
title: '连接时间',
|
||||
dataIndex: 'connectTime',
|
||||
slotName: 'connectTime',
|
||||
align: 'left',
|
||||
width: 310,
|
||||
width: 318,
|
||||
render: ({ record }) => {
|
||||
return (record.startTime && dateFormat(new Date(record.startTime)))
|
||||
+ ' - '
|
||||
+ (record.endTime && dateFormat(new Date(record.endTime)) || '现在');
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 180,
|
||||
align: 'left',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
:itemOptions="{ 4: { span: 2 } }"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 操作用户 -->
|
||||
<a-form-item field="userId" label="操作用户" label-col-flex="50px">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 操作主机 -->
|
||||
<a-form-item field="hostId" label="操作主机" label-col-flex="50px">
|
||||
<host-selector v-model="formModel.hostId"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 操作类型 -->
|
||||
<a-form-item field="type" label="操作类型" label-col-flex="50px">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(sftpOperatorTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 执行结果 -->
|
||||
<a-form-item field="result" label="执行结果" label-col-flex="50px">
|
||||
<a-select v-model="formModel.result"
|
||||
placeholder="请选择执行结果"
|
||||
:options="toOptions(sftpOperatorResultKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 开始时间 -->
|
||||
<a-form-item field="startTimeRange" label="开始时间" label-col-flex="50px">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
style="width: 100%"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
SFTP 操作日志
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle">
|
||||
<a-space>
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm :content="`确认删除选中的 ${selectedKeys.length} 条记录吗?`"
|
||||
position="br"
|
||||
type="warning"
|
||||
@ok="deleteSelectRows">
|
||||
<a-button v-permission="['infra:operator-log:delete', 'asset:host-sftp-log:management:delete']"
|
||||
type="secondary"
|
||||
status="danger"
|
||||
:disabled="selectedKeys.length === 0">
|
||||
删除
|
||||
<template #icon>
|
||||
<icon-delete />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
ref="tableRef"
|
||||
:loading="loading"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 操作用户 -->
|
||||
<template #username="{ record }">
|
||||
{{ record.username }}
|
||||
</template>
|
||||
<!-- 操作主机 -->
|
||||
<template #hostName="{ record }">
|
||||
<span class="host-name" :title="record.hostName">
|
||||
{{ record.hostName }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="host-address text-copy"
|
||||
:title="record.hostAddress"
|
||||
@click="copy(record.hostAddress)">
|
||||
{{ record.hostAddress }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作类型 -->
|
||||
<template #type="{ record }">
|
||||
{{ getDictValue(sftpOperatorTypeKey, record.type) }}
|
||||
</template>
|
||||
<!-- 操作文件 -->
|
||||
<template #paths="{ record }">
|
||||
<div class="paths-wrapper">
|
||||
<span v-for="path in record.paths"
|
||||
class="path-wrapper text-ellipsis text-copy"
|
||||
:title="path"
|
||||
@click="copy(path)">
|
||||
{{ path }}
|
||||
</span>
|
||||
<!-- 移动目标路径 -->
|
||||
<span class="sub-text" v-if="SftpOperatorType.SFTP_MOVE === record.type">
|
||||
移动到 {{ record.extra?.target }}
|
||||
</span>
|
||||
<!-- 提权信息 -->
|
||||
<span class="sub-text" v-if="SftpOperatorType.SFTP_CHMOD === record.type">
|
||||
提权 {{ record.extra?.mod }} {{ permission10toString(record.extra?.mod as number) }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 执行结果 -->
|
||||
<template #result="{ record }">
|
||||
<a-tag :color="getDictValue(sftpOperatorResultKey, record.result, 'color')">
|
||||
{{ getDictValue(sftpOperatorResultKey, record.result) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<!-- 留痕地址 -->
|
||||
<template #address="{ record }">
|
||||
<span class="operator-location" :title="record.location">
|
||||
{{ record.location }}
|
||||
</span>
|
||||
<br>
|
||||
<span class="operator-address text-copy"
|
||||
:title="record.address"
|
||||
@click="copy(record.address)">
|
||||
{{ record.address }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 操作 -->
|
||||
<template #handle="{ record }">
|
||||
<div class="table-handle-wrapper">
|
||||
<!-- 删除 -->
|
||||
<a-popconfirm content="确认删除这条记录吗?"
|
||||
position="left"
|
||||
type="warning"
|
||||
@ok="deleteRow(record)">
|
||||
<a-button v-permission="['infra:operator-log:delete', 'asset:host-sftp-log:management:delete']"
|
||||
type="text"
|
||||
size="mini"
|
||||
status="danger">
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</div>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetAuditSftpLogTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostSftpLogQueryRequest, HostSftpLogQueryResponse } from '@/api/asset/host-sftp-log';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { getHostSftpLogPage, deleteHostSftpLog } from '@/api/asset/host-sftp-log';
|
||||
import { sftpOperatorTypeKey, sftpOperatorResultKey, SftpOperatorType } from '../types/const';
|
||||
import { usePagination, useRowSelection } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import columns from '../types/table.columns';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useCopy from '@/hooks/copy';
|
||||
import UserSelector from '@/components/user/user/user-selector.vue';
|
||||
import HostSelector from '@/components/asset/host/host-selector.vue';
|
||||
import { permission10toString } from '@/utils/file';
|
||||
|
||||
const tableRenderData = ref<HostSftpLogQueryResponse[]>([]);
|
||||
const selectedKeys = ref<number[]>([]);
|
||||
|
||||
const pagination = usePagination();
|
||||
const rowSelection = useRowSelection();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { copy } = useCopy();
|
||||
|
||||
const formModel = reactive<HostSftpLogQueryRequest>({
|
||||
userId: undefined,
|
||||
hostId: undefined,
|
||||
type: undefined,
|
||||
result: undefined,
|
||||
startTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostSftpLogQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostSftpLogPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
selectedKeys.value = [];
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
// 删除选中行
|
||||
const deleteSelectRows = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostSftpLog(selectedKeys.value);
|
||||
Message.success(`成功删除 ${selectedKeys.value.length} 条数据`);
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除当前行
|
||||
const deleteRow = async ({ id }: {
|
||||
id: number
|
||||
}) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
// 调用删除接口
|
||||
await deleteHostSftpLog([id]);
|
||||
Message.success('删除成功');
|
||||
selectedKeys.value = [];
|
||||
// 重新加载数据
|
||||
fetchTableData();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.host-name, .operator-location {
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.host-address, .operator-address, .sub-text {
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.paths-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.path-wrapper {
|
||||
display: block;
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
39
orion-ops-ui/src/views/asset-audit/sftp-log/index.vue
Normal file
39
orion-ops-ui/src/views/asset-audit/sftp-log/index.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="layout-container" v-if="render">
|
||||
<!-- 列表-表格 -->
|
||||
<sftp-log-table />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'assetAuditSftpLog'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount, onUnmounted } from 'vue';
|
||||
import { useCacheStore, useDictStore } from '@/store';
|
||||
import { dictKeys } from './types/const';
|
||||
import SftpLogTable from './components/sftp-log-table.vue';
|
||||
|
||||
const render = ref(false);
|
||||
|
||||
// 加载字典配置
|
||||
onBeforeMount(async () => {
|
||||
const dictStore = useDictStore();
|
||||
await dictStore.loadKeys(dictKeys);
|
||||
render.value = true;
|
||||
});
|
||||
|
||||
// 重置缓存
|
||||
onUnmounted(() => {
|
||||
const cacheStore = useCacheStore();
|
||||
cacheStore.reset('users', 'hosts');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
14
orion-ops-ui/src/views/asset-audit/sftp-log/types/const.ts
Normal file
14
orion-ops-ui/src/views/asset-audit/sftp-log/types/const.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// sftp 操作类型
|
||||
export const SftpOperatorType = {
|
||||
SFTP_MOVE: 'host-terminal:sftp-move',
|
||||
SFTP_CHMOD: 'host-terminal:sftp-chmod',
|
||||
};
|
||||
|
||||
// sftp 操作类型 字典项
|
||||
export const sftpOperatorTypeKey = 'sftpOperatorType';
|
||||
|
||||
// sftp 操作结果 字典项
|
||||
export const sftpOperatorResultKey = 'operatorLogResult';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [sftpOperatorTypeKey, sftpOperatorResultKey];
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { TableColumnData } from '@arco-design/web-vue/es/table/interface';
|
||||
import { dateFormat } from '@/utils';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '操作用户',
|
||||
dataIndex: 'username',
|
||||
slotName: 'username',
|
||||
width: 140,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '操作主机',
|
||||
dataIndex: 'hostName',
|
||||
slotName: 'hostName',
|
||||
width: 180,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '操作类型',
|
||||
dataIndex: 'type',
|
||||
slotName: 'type',
|
||||
width: 116,
|
||||
align: 'left',
|
||||
}, {
|
||||
title: '操作文件',
|
||||
dataIndex: 'paths',
|
||||
slotName: 'paths',
|
||||
align: 'left',
|
||||
}, {
|
||||
title: '执行结果',
|
||||
dataIndex: 'result',
|
||||
slotName: 'result',
|
||||
align: 'left',
|
||||
width: 88,
|
||||
}, {
|
||||
title: '留痕地址',
|
||||
dataIndex: 'address',
|
||||
slotName: 'address',
|
||||
width: 156,
|
||||
align: 'left',
|
||||
ellipsis: true,
|
||||
}, {
|
||||
title: '操作时间',
|
||||
dataIndex: 'startTime',
|
||||
slotName: 'startTime',
|
||||
align: 'center',
|
||||
width: 180,
|
||||
render: ({ record }) => {
|
||||
return (record.startTime && dateFormat(new Date(record.startTime)));
|
||||
},
|
||||
}, {
|
||||
title: '操作',
|
||||
slotName: 'handle',
|
||||
width: 80,
|
||||
align: 'center',
|
||||
fixed: 'right',
|
||||
},
|
||||
] as TableColumnData[];
|
||||
|
||||
export default columns;
|
||||
@@ -6,7 +6,6 @@
|
||||
<!-- 主机身份表格 -->
|
||||
<a-table row-key="id"
|
||||
class="host-identity-main-table"
|
||||
label-align="left"
|
||||
:columns="hostIdentityColumns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<!-- 主机秘钥表格 -->
|
||||
<a-table row-key="id"
|
||||
class="host-key-main-table"
|
||||
label-align="left"
|
||||
:columns="hostKeyColumns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id" label-col-flex="50px">
|
||||
<a-input-number v-model="formModel.id"
|
||||
@@ -72,9 +72,7 @@
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="id" label-col-flex="30px">
|
||||
<a-input-number v-model="formModel.id"
|
||||
@@ -64,9 +64,7 @@
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
label-align="left"
|
||||
@submit="fetchTableData"
|
||||
@reset="fetchTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- id -->
|
||||
<a-form-item field="id" label="主机id" label-col-flex="50px">
|
||||
<a-input-number v-model="formModel.id"
|
||||
@@ -91,9 +91,7 @@
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
title="操作日志"
|
||||
:header-style="{ paddingBottom: '0' }"
|
||||
:body-style="{ padding: '8px 20px 8px 20px' }">
|
||||
<operator-log-table :visible-user="false"
|
||||
:visible-handle="false"
|
||||
:current="true" />
|
||||
<operator-log-simple-table :current="true"
|
||||
:handle-column="false" />
|
||||
</a-card>
|
||||
</div>
|
||||
<a-grid class="right-side"
|
||||
@@ -39,7 +38,7 @@
|
||||
import Banner from './components/banner.vue';
|
||||
import QuickOperation from './components/quick-operation.vue';
|
||||
import Docs from './components/docs.vue';
|
||||
import OperatorLogTable from '@/views/user/operator-log/components/operator-log-table.vue';
|
||||
import OperatorLogSimpleTable from '@/views/user/operator-log/components/operator-log-simple-table.vue';
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
<template>
|
||||
<!-- 搜索 -->
|
||||
<a-card class="general-card table-search-card">
|
||||
<query-header :model="formModel"
|
||||
label-align="left"
|
||||
:itemOptions="{ 6: { span: 2 } }"
|
||||
@submit="fetchTableData"
|
||||
@reset="resetTableData"
|
||||
@keyup.enter="() => fetchTableData()">
|
||||
<!-- 连接用户 -->
|
||||
<a-form-item field="userId" label="连接用户" label-col-flex="50px">
|
||||
<user-selector v-model="formModel.userId"
|
||||
placeholder="请选择用户"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 连接主机 -->
|
||||
<a-form-item field="hostId" label="连接主机" label-col-flex="50px">
|
||||
<host-selector v-model="formModel.hostId"
|
||||
placeholder="请选择主机"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 主机地址 -->
|
||||
<a-form-item field="hostAddress" label="主机地址" label-col-flex="50px">
|
||||
<a-input v-model="formModel.hostAddress" placeholder="请输入主机地址" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 状态 -->
|
||||
<a-form-item field="status" label="状态" label-col-flex="50px">
|
||||
<a-select v-model="formModel.status"
|
||||
placeholder="请选择状态"
|
||||
:options="toOptions(connectStatusKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 类型 -->
|
||||
<a-form-item field="type" label="类型" label-col-flex="50px">
|
||||
<a-select v-model="formModel.type"
|
||||
placeholder="请选择类型"
|
||||
:options="toOptions(connectTypeKey)"
|
||||
allow-clear />
|
||||
</a-form-item>
|
||||
<!-- token -->
|
||||
<a-form-item field="token" label="token" label-col-flex="50px">
|
||||
<a-input v-model="formModel.token" placeholder="请输入token" allow-clear />
|
||||
</a-form-item>
|
||||
<!-- 开始时间 -->
|
||||
<a-form-item field="startTimeRange" label="开始时间" label-col-flex="50px">
|
||||
<a-range-picker v-model="formModel.startTimeRange"
|
||||
style="width: 100%"
|
||||
:time-picker-props="{ defaultValue: ['00:00:00', '23:59:59'] }"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss" />
|
||||
</a-form-item>
|
||||
</query-header>
|
||||
</a-card>
|
||||
<!-- 表格 -->
|
||||
<a-card class="general-card table-card">
|
||||
<template #title>
|
||||
<!-- 左侧操作 -->
|
||||
<div class="table-left-bar-handle">
|
||||
<!-- 标题 -->
|
||||
<div class="table-title">
|
||||
主机连接日志 - 用户
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
<div class="table-right-bar-handle" />
|
||||
</template>
|
||||
<!-- table -->
|
||||
<a-table row-key="id"
|
||||
class="table-wrapper-8"
|
||||
ref="tableRef"
|
||||
label-align="left"
|
||||
:loading="loading"
|
||||
:columns="columns"
|
||||
:data="tableRenderData"
|
||||
:pagination="pagination"
|
||||
@page-change="(page) => fetchTableData(page, pagination.pageSize)"
|
||||
@page-size-change="(size) => fetchTableData(1, size)"
|
||||
:bordered="false">
|
||||
<!-- 连接用户 -->
|
||||
<template #username="{ record }">
|
||||
{{ record.userId }} - {{ record.username }}
|
||||
</template>
|
||||
<!-- 连接主机 -->
|
||||
<template #hostName="{ record }">
|
||||
{{ record.hostId }} - {{ record.hostName }}
|
||||
</template>
|
||||
<!-- 主机地址 -->
|
||||
<template #hostAddress="{ record }">
|
||||
<span class="copy-left" title="复制" @click="copy(record.hostAddress)">
|
||||
<icon-copy />
|
||||
</span>
|
||||
<span>{{ record.hostAddress }}</span>
|
||||
</template>
|
||||
<!-- 状态 -->
|
||||
<template #status="{ record }">
|
||||
<span class="circle" :style="{
|
||||
background: getDictValue(connectStatusKey, record.status, 'color')
|
||||
}" />
|
||||
{{ getDictValue(connectStatusKey, record.status) }}
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostAuditConnectLogTable'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostConnectLogQueryRequest, HostConnectLogQueryResponse } from '@/api/asset/host-connect-log';
|
||||
import { reactive, ref, onMounted } from 'vue';
|
||||
import { getHostConnectLogPage } from '@/api/asset/host-connect-log';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import columns from '../types/table.columns';
|
||||
import { connectStatusKey, connectTypeKey, HostConnectType } from '../types/const';
|
||||
import { usePagination } from '@/types/table';
|
||||
import { useDictStore } from '@/store';
|
||||
import useCopy from '@/hooks/copy';
|
||||
import UserSelector from '@/components/user/user/user-selector.vue';
|
||||
import HostSelector from '@/components/asset/host/host-selector.vue';
|
||||
|
||||
const emits = defineEmits(['openAdd', 'openUpdate']);
|
||||
|
||||
const tableRenderData = ref<HostConnectLogQueryResponse[]>([]);
|
||||
|
||||
const pagination = usePagination();
|
||||
const { loading, setLoading } = useLoading();
|
||||
const { toOptions, getDictValue } = useDictStore();
|
||||
const { copy } = useCopy();
|
||||
|
||||
const formModel = reactive<HostConnectLogQueryRequest>({
|
||||
userId: undefined,
|
||||
hostId: undefined,
|
||||
hostAddress: undefined,
|
||||
type: HostConnectType.SSH,
|
||||
token: undefined,
|
||||
status: undefined,
|
||||
startTimeRange: undefined,
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const doFetchTableData = async (request: HostConnectLogQueryRequest) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostConnectLogPage(request);
|
||||
tableRenderData.value = data.rows;
|
||||
pagination.total = data.total;
|
||||
pagination.current = request.page;
|
||||
pagination.pageSize = request.limit;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置
|
||||
const resetTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
formModel.type = HostConnectType.SSH;
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
// 切换页码
|
||||
const fetchTableData = (page = 1, limit = pagination.pageSize, form = formModel) => {
|
||||
doFetchTableData({ page, limit, ...form });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchTableData();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -44,8 +44,12 @@
|
||||
</a-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 已关闭-右侧操作 -->
|
||||
<div v-if="isClose" class="sftp-table-header-right">
|
||||
<span class="close-message">{{ closeMessage }}</span>
|
||||
</div>
|
||||
<!-- 路径编辑模式-右侧操作 -->
|
||||
<a-space v-if="pathEditable" class="sftp-table-header-right">
|
||||
<a-space v-else-if="pathEditable" class="sftp-table-header-right">
|
||||
<!-- 进入 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
@@ -185,9 +189,11 @@
|
||||
import { openSftpCreateModalKey, openSftpUploadModalKey } from '../../types/terminal.const';
|
||||
|
||||
const props = defineProps<{
|
||||
isClose: boolean;
|
||||
closeMessage: string | undefined;
|
||||
currentPath: string;
|
||||
session: ISftpSession | undefined,
|
||||
selectedFiles: Array<string>
|
||||
session: ISftpSession | undefined;
|
||||
selectedFiles: Array<string>;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:selectedFiles', 'loadFile', 'download']);
|
||||
@@ -322,6 +328,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.close-message {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding-left: 16px;
|
||||
color: rgb(var(--red-6));
|
||||
}
|
||||
|
||||
.header-action-icon {
|
||||
font-size: 16px;
|
||||
padding: 4px;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<a-table row-key="path"
|
||||
ref="tableRef"
|
||||
class="sftp-table"
|
||||
label-align="left"
|
||||
:columns="columns"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
:row-selection="rowSelection"
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
<!-- 表头 -->
|
||||
<sftp-table-header class="sftp-table-header"
|
||||
v-model:selected-files="selectFiles"
|
||||
:is-close="closed"
|
||||
:close-message="closeMessage"
|
||||
:current-path="currentPath"
|
||||
:session="session"
|
||||
@load-file="loadFiles"
|
||||
@@ -89,6 +91,8 @@
|
||||
const fileList = ref<Array<SftpFile>>([]);
|
||||
const selectFiles = ref<Array<string>>([]);
|
||||
const splitSize = ref(1);
|
||||
const closed = ref(false);
|
||||
const closeMessage = ref('');
|
||||
const editorView = ref(false);
|
||||
const editorRef = ref();
|
||||
const editorFileName = ref('');
|
||||
@@ -184,6 +188,14 @@
|
||||
return success;
|
||||
};
|
||||
|
||||
// 关闭回调
|
||||
const onClose = (forceClose: string, msg: string) => {
|
||||
console.log(forceClose);
|
||||
console.log(msg);
|
||||
closed.value = true;
|
||||
closeMessage.value = msg;
|
||||
};
|
||||
|
||||
// 接收列表回调
|
||||
const resolveList = (result: string, path: string, list: Array<SftpFile>) => {
|
||||
setTableLoading(false);
|
||||
@@ -240,6 +252,7 @@
|
||||
session.value = await sessionManager.openSftp(props.tab, {
|
||||
setLoading: setTableLoading,
|
||||
connectCallback,
|
||||
onClose,
|
||||
resolveList,
|
||||
resolveSftpMkdir: resolveFileAction,
|
||||
resolveSftpTouch: resolveFileAction,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user