Compare commits

...

15 Commits

Author SHA1 Message Date
lijiahangmax
78bf636cdb Merge pull request #2 from lijiahangmax/dev
merge v1.0.1
2024-03-06 00:06:04 +08:00
lijiahangmax
ba338c15de 🔖 升级版本. 2024-03-06 00:03:13 +08:00
lijiahang
93407460d8 登录配置化. 2024-03-05 19:31:00 +08:00
lijiahang
554c62abf7 SFTP 操作日志. 2024-03-05 18:07:26 +08:00
lijiahangmax
a75ead9a58 sftp 操作日志. 2024-03-05 00:02:06 +08:00
lijiahangmax
f1ade4e182 💄 优化表格样式. 2024-03-04 22:27:39 +08:00
lijiahang
462e77f936 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	orion-ops-ui/src/views/user/operator-log/components/operator-log-table.vue
2024-03-04 19:11:54 +08:00
lijiahang
ba955571a3 清空操作日志. 2024-03-04 19:10:55 +08:00
lijiahang
d1e94a49e0 清空操作日志. 2024-03-04 19:04:51 +08:00
lijiahang
b9127967d0 清空主机连接日志. 2024-03-04 18:28:07 +08:00
lijiahang
0538d2aa26 清空主机连接日志. 2024-03-04 15:14:45 +08:00
lijiahang
0f8eebf53c 🎨 修改前端代码样式. 2024-03-04 12:28:55 +08:00
lijiahangmax
0f9c3db9cc 💄 连接日志表格样式更新. 2024-03-03 23:28:17 +08:00
lijiahangmax
b424dd02db 强制下线终端. 2024-03-03 00:24:00 +08:00
lijiahangmax
f1d14b4a12 📝 修改文档. 2024-03-02 13:22:22 +08:00
123 changed files with 3416 additions and 694 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# orion-ops-pro <small>1.0.0</small>
# orion-ops-pro <small>1.0.1</small>
> 一款开箱即用的运维平台。

View File

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

View File

@@ -4,9 +4,11 @@
## 未开始 ⏳
* 资产管理表结构优化
* 批量执行
* 定时执行
* 站内消息
* 后端配置动态配置
* 终端背景图片
* 资产授权 UI 改版
* RDP 远程桌面
* 接入 config 后端动态配置

View File

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

View File

@@ -1,3 +1,14 @@
### 连接日志
在主机终端页面打开的 `SSH` `SFTP` 连接都会记录下来, 这里默认只展示 `SSH` 连接记录, 可以展开条件进行修改。
在主机终端页面打开的 `SSH` `SFTP` 连接都会记录下来
* 详情: 查看连接详情
* 断开: 断开连接
* 删除: 删除连接记录
* 清理: 根据条件清理数据
### SFTP 操作日志
查看用户 SFTP 操作日志, 是从用户操作日志中过滤查询。
* 删除: 删除操作日志

View File

@@ -25,3 +25,4 @@
记录用户在系统内的操作日志。
* 详情: 查看操作的参数以及留痕信息
* 清理: 根据条件清理数据

View File

@@ -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. 修改前端配置

View File

@@ -17,7 +17,7 @@
可以在执行命令的第一行设置 `set -e`
作用是: 当执行出现意料之外的情况时, 立即退出
> ##### 5. 在调度任务、应用构建、应用发布 命令执行成功的依据是什么?
> ##### 5. 在调度任务、批量执行 命令执行成功的依据是什么?
是获取命令的 `exitcode` 判断是否为 `0` 如果非0则代表命令执行失败
同理, 在命令的最后一行设置 `exit 1` 结果将会是失败, 可以用此来中断后续流程

View File

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

View File

@@ -85,4 +85,8 @@ public interface ErrorMessage {
String FILE_ABSENT = "文件不存在";
String LOG_ABSENT = "日志不存在";
String ILLEGAL_STATUS = "当前状态不支持此操作";
}

View File

@@ -47,4 +47,12 @@ public interface FieldConst {
String MOD = "mod";
String COUNT = "count";
String LOCATION = "location";
String USER_AGENT = "userAgent";
String ERROR_MESSAGE = "errorMessage";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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": "登录失败锁定时间 (分)."
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -24,6 +24,11 @@ public enum HostConnectStatusEnum {
*/
FAILED,
/**
* 强制下线
*/
FORCE_OFFLINE,
;
public static HostConnectStatusEnum of(String type) {

View File

@@ -28,7 +28,7 @@ public enum OutputTypeEnum {
/**
* 关闭连接
*/
CLOSE("cl", "${type}|${sessionId}|${msg}"),
CLOSE("cl", "${type}|${sessionId}|${forceClose}|${msg}"),
/**
* pong

View File

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

View File

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

View File

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

View File

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

View File

@@ -38,4 +38,9 @@ public interface ITerminalSession extends SafeCloseable {
*/
void keepAlive();
/**
* 强制下线
*/
void forceOffline();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,9 +14,4 @@ Authorization: {{token}}
"endTime": ""
}
### 查询登录日志
GET {{baseUrl}}/infra/operator-log/login-history?username=admin
Content-Type: application/json
Authorization: {{token}}
###

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
/**
* 登录
*

View File

@@ -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);
/**
* 查询用户登录日志
*

View File

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

View File

@@ -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) {
// 条件

View File

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

View File

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

View File

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

View File

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

View File

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

View 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' });
}
});
}

View File

@@ -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';
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -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']"

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 () => {

View File

@@ -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',
};
// 主机连接状态 字典项

View File

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

View File

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

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

View 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];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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