Merge branch 'master' of gitee.com:dromara/Jpom into dev

Signed-off-by: 健身的码农 <fangzhong.top@gmail.com>
This commit is contained in:
健身的码农 2023-03-13 09:24:41 +00:00 committed by Gitee
commit 0c6bafd350
74 changed files with 1302 additions and 176 deletions

2
.env
View File

@ -1,3 +1,3 @@
JPOM_VERSION=2.10.28
JPOM_VERSION=2.10.29
# Server Token 生产部署请更换
SERVER_TOKEN=7094f673-2c53-4fc1-82e7-86e528449d97

View File

@ -29,14 +29,14 @@ stages:
artifacts:
- name: all_zip
path:
- modules/server/target/server-2.10.28-release.zip
- modules/agent/target/agent-2.10.28-release.zip
- modules/server/target/server-2.10.29-release.zip
- modules/agent/target/agent-2.10.29-release.zip
- name: server_zip
path:
- modules/server/target/server-2.10.28-release.zip
- modules/server/target/server-2.10.29-release.zip
- name: agent_zip
path:
- modules/agent/target/agent-2.10.28-release.zip
- modules/agent/target/agent-2.10.29-release.zip
settings: []
strategy:
retry: '0'
@ -50,7 +50,7 @@ stages:
name: publish_general_artifacts
displayName: 合并打包
dependArtifact: all_zip
artifactName: jpom-2.10.28
artifactName: jpom-2.10.29
strategy:
retry: '0'
strategy:

View File

@ -1,11 +1,13 @@
# 🚀 版本日志
## 2.10.29
## 2.10.29 (2023-03-10)
### 🐣 新增功能
1. 【server】新增 导入仓库支持 `gitea` 系统
(感谢 [@Smith](https://gitee.com/autools) [Gitee pr 173](https://gitee.com/dromara/Jpom/pulls/173)
2. 【server】新增 用户登录日志(取消用户登录生成操作日志的执行日志)
3. 【server】新增 在线工具验证 cron 表达式 (感谢@奇奇)
### 🐞 解决BUG、优化功能
@ -14,6 +16,12 @@
3. 【agent】优化 节点分发配置白名单到插件端需要验证合法性
4. 【server】优化 docker 创建容器忽略未配置存储选项参数(感谢@D¹⁹⁹¹
5. 【server】优化 docker 管理裁剪功能独立菜单
6. 【server】修复 资产管理未记录操作日志的问题
7. 【server】优化 操作日志存储用户名、工作空间名字段
8. 【server】优化 容器构建查询可用标签容器相关提示
9. 【server】优化 构建历史列表页面在小屏幕数据显示不全
(感谢 [@一只羊](https://gitee.com/hjdyzy) [Gitee issues I6LLA0](https://gitee.com/dromara/Jpom/issues/I6LLA0)
10. 【server】修复 在线构建发布到集群无法正常选择集群服务(感谢@心光)
------

View File

@ -1 +1 @@
2.10.28
2.10.29

View File

@ -30,7 +30,7 @@
<parent>
<groupId>io.jpom.agent-transport</groupId>
<artifactId>jpom-agent-transport-parent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -30,7 +30,7 @@
<parent>
<groupId>io.jpom.agent-transport</groupId>
<artifactId>jpom-agent-transport-parent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<packaging>pom</packaging>
@ -38,7 +38,7 @@
<module>agent-transport-http</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<version>2.10.28</version>
<version>2.10.29</version>
<groupId>io.jpom.agent-transport</groupId>
<artifactId>jpom-agent-transport-parent</artifactId>
<name>Jpom Agent Transport</name>

View File

@ -29,12 +29,12 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>agent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<name>Jpom Agent</name>
<properties>
<start-class>io.jpom.JpomAgentApplication</start-class>

View File

@ -29,13 +29,13 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>Jpom Common</name>
<artifactId>common</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<dependencies>

View File

@ -53,7 +53,7 @@ public class WebAopLog {
this.aopLogInterface = SpringUtil.getBeansOfType(AopLogInterface.class).values();
}
@Pointcut("execution(public * io.jpom.controller..*.*(..))")
@Pointcut("execution(public * io.jpom..*.*.controller..*.*(..))")
public void webLog() {
//
}

View File

@ -8,4 +8,4 @@
| |
|_|
➜ Jpom \ (•◡•) / (v2.10.28)
➜ Jpom \ (•◡•) / (v2.10.29)

View File

@ -29,7 +29,7 @@ LABEL maintainer="bwcx-jzy <bwcx_jzy@163.com>"
LABEL documentation="https://jpom.top"
ENV JPOM_HOME /usr/local/jpom-server
ENV JPOM_PKG_VERSION 2.10.28
ENV JPOM_PKG_VERSION 2.10.29
ENV JPOM_PKG server-${JPOM_PKG_VERSION}-release.tar.gz
ENV SHA1_NAME server-${JPOM_PKG_VERSION}-release.tar.gz.sha1

View File

@ -29,13 +29,13 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<name>Jpom Server</name>
<artifactId>server</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<properties>
<start-class>io.jpom.JpomServerApplication</start-class>
</properties>

View File

@ -753,11 +753,11 @@ public class BuildExecuteService {
List<DockerInfoModel> dockerInfoModels = buildExecuteService
.dockerInfoService
.queryByTag(buildInfoModel.getWorkspaceId(), fromTag);
DockerInfoModel dockerInfoModel = CollUtil.getFirst(dockerInfoModels);
Assert.notNull(dockerInfoModel, "没有可用的 docker server");
logRecorder.system("use docker {}", dockerInfoModel.getName());
Map<String, Object> map = buildExecuteService.machineDockerServer.dockerParameter(dockerInfoModels);
Assert.notNull(map, fromTag + " 没有可用的 docker server");
logRecorder.system("use docker {}", map.get("name"));
String workingDir = "/home/jpom/";
Map<String, Object> map = buildExecuteService.machineDockerServer.dockerParameter(dockerInfoModel);
map.put("runsOn", dockerYmlDsl.getRunsOn());
map.put("workingDir", workingDir);
map.put("tempDir", JpomApplication.getInstance().getTempPath());

View File

@ -147,7 +147,7 @@ public class ReleaseManage {
} else if (releaseMethod == BuildReleaseMethod.LocalCommand.getCode()) {
return this.localCommand();
} else if (releaseMethod == BuildReleaseMethod.DockerImage.getCode()) {
this.doDockerImage();
return this.doDockerImage();
} else if (releaseMethod == BuildReleaseMethod.No.getCode()) {
return true;
} else {
@ -199,7 +199,7 @@ public class ReleaseManage {
}).collect(Collectors.joining(StrUtil.COMMA));
}
private void doDockerImage() {
private boolean doDockerImage() {
// 生成临时目录
File tempPath = FileUtil.file(JpomApplication.getInstance().getTempPath(), "build_temp", "docker_image", this.buildExtraModule.getId() + StrUtil.DASHED + this.buildNumberId);
try {
@ -221,7 +221,7 @@ public class ReleaseManage {
File dockerfile = FileUtil.file(tempPath, dockerFile);
if (!FileUtil.isFile(dockerfile)) {
logRecorder.systemError("仓库目录下没有找到 Dockerfile 文件: {}", dockerFile);
return;
return false;
}
File baseDir = FileUtil.file(tempPath, list.size() == 1 ? StrUtil.SLASH : CollUtil.get(list, 0));
//
@ -230,10 +230,10 @@ public class ReleaseManage {
List<DockerInfoModel> dockerInfoModels = buildExecuteService
.dockerInfoService
.queryByTag(this.buildExtraModule.getWorkspaceId(), fromTag);
DockerInfoModel dockerInfoModel = CollUtil.getFirst(dockerInfoModels);
if (dockerInfoModel == null) {
logRecorder.systemError("没有可用的 docker server");
return;
Map<String, Object> map = buildExecuteService.machineDockerServer.dockerParameter(dockerInfoModels);
if (map == null) {
logRecorder.systemError("{} 没有可用的 docker server", fromTag);
return false;
}
//String dockerBuildArgs = this.buildExtraModule.getDockerBuildArgs();
for (DockerInfoModel infoModel : dockerInfoModels) {
@ -244,9 +244,7 @@ public class ReleaseManage {
if (pushToRepository != null && pushToRepository) {
List<String> repositoryList = StrUtil.splitTrim(dockerTag, StrUtil.COMMA);
for (String repositoryItem : repositoryList) {
logRecorder.system("start push to repository in({}),{} {}", dockerInfoModel.getName(), StrUtil.emptyToDefault(dockerInfoModel.getRegistryUrl(), StrUtil.EMPTY), repositoryItem);
Map<String, Object> map = buildExecuteService.machineDockerServer.dockerParameter(dockerInfoModel);
//dockerInfoModel.toParameter();
logRecorder.system("start push to repository in({}),{} {}", map.get("name"), StrUtil.emptyToDefault((String) map.get("registryUrl"), StrUtil.EMPTY), repositoryItem);
//
map.put("repository", repositoryItem);
Consumer<String> logConsumer = s -> logRecorder.info(s);
@ -264,6 +262,7 @@ public class ReleaseManage {
} finally {
CommandUtil.systemFastDel(tempPath);
}
return true;
}
private void updateSwarmService(String dockerTag, String swarmId, String serviceName) {

View File

@ -43,12 +43,12 @@ import io.jpom.common.interceptor.NotLogin;
import io.jpom.common.validator.ValidatorConfig;
import io.jpom.common.validator.ValidatorItem;
import io.jpom.common.validator.ValidatorRule;
import io.jpom.func.user.server.UserLoginLogServer;
import io.jpom.model.data.WorkspaceModel;
import io.jpom.model.dto.UserLoginDto;
import io.jpom.model.user.UserModel;
import io.jpom.permission.ClassFeature;
import io.jpom.permission.Feature;
import io.jpom.permission.MethodFeature;
import io.jpom.service.user.UserBindWorkspaceService;
import io.jpom.service.user.UserService;
import io.jpom.system.ServerConfig;
@ -89,14 +89,17 @@ public class LoginControl extends BaseServerController {
private final UserBindWorkspaceService userBindWorkspaceService;
private final ServerConfig.UserConfig userConfig;
private final ServerConfig.WebConfig webConfig;
private final UserLoginLogServer userLoginLogServer;
public LoginControl(UserService userService,
UserBindWorkspaceService userBindWorkspaceService,
ServerConfig serverConfig) {
ServerConfig serverConfig,
UserLoginLogServer userLoginLogServer) {
this.userService = userService;
this.userBindWorkspaceService = userBindWorkspaceService;
this.userConfig = serverConfig.getUser();
this.webConfig = serverConfig.getWeb();
this.userLoginLogServer = userLoginLogServer;
}
/**
@ -155,31 +158,34 @@ public class LoginControl extends BaseServerController {
/**
* 登录接口
*
* @param userName 登录名
* @param userPwd 登录密码
* @param code 验证码
* @param loginName 登录名
* @param userPwd 登录密码
* @param code 验证码
* @return json
*/
@PostMapping(value = "userLogin", produces = MediaType.APPLICATION_JSON_VALUE)
@NotLogin
@Feature(method = MethodFeature.EXECUTE, resultCode = {200, 201}, logResponse = false)
public JsonMessage<Object> userLogin(
@ValidatorConfig(value = {
@ValidatorItem(value = ValidatorRule.NOT_EMPTY, msg = "请输入登录信息")
}) String userName,
}) String loginName,
@ValidatorConfig(value = {
@ValidatorItem(value = ValidatorRule.NOT_EMPTY, msg = "请输入登录信息")
}) String userPwd,
String code) {
String code, HttpServletRequest request) {
if (this.ipLock()) {
return new JsonMessage<>(400, "尝试次数太多,请稍后再来");
}
synchronized (userName.intern()) {
UserModel userModel = userService.getByKey(userName);
synchronized (loginName.intern()) {
UserModel userModel = userService.getByKey(loginName);
if (userModel == null) {
this.ipError();
return new JsonMessage<>(400, "登录失败,请输入正确的密码和账号,多次失败将锁定账号");
}
if (userModel.getStatus() != null && userModel.getStatus() == 0) {
userLoginLogServer.fail(userModel, 4, false, request);
return new JsonMessage<>(ServerConst.ACCOUNT_LOCKED, ServerConst.ACCOUNT_LOCKED_TIP);
}
if (!webConfig.isDisabledGuide()) {
// 获取验证码
String sCode = getSessionAttribute(LOGIN_CODE);
@ -193,27 +199,31 @@ public class LoginControl extends BaseServerController {
String msg = DateUtil.formatBetween(lockTime * 1000, BetweenFormatter.Level.SECOND);
updateModel = userModel.errorLock(userConfig.getAlwaysLoginError());
this.ipError();
userLoginLogServer.fail(userModel, 2, false, request);
return new JsonMessage<>(400, "该账户登录失败次数过多,已被锁定" + msg + ",请不要再次尝试");
}
// 验证
if (userService.simpleLogin(userName, userPwd) != null) {
updateModel = UserModel.unLock(userName);
if (userService.simpleLogin(loginName, userPwd) != null) {
updateModel = UserModel.unLock(loginName);
this.ipSuccess();
// 判断是否开启 两步验证
boolean bindMfa = userService.hasBindMfa(userName);
boolean bindMfa = userService.hasBindMfa(loginName);
if (bindMfa) {
//
JSONObject jsonObject = new JSONObject();
String uuid = IdUtil.fastSimpleUUID();
MFA_TOKEN.put(uuid, userName);
MFA_TOKEN.put(uuid, loginName);
jsonObject.put("tempToken", uuid);
userLoginLogServer.success(userModel, 5, true, request);
return new JsonMessage<>(201, "请输入两步验证码", jsonObject);
}
UserLoginDto userLoginDto = this.createToken(userModel);
userLoginLogServer.success(userModel, false, request);
return new JsonMessage<>(200, "登录成功", userLoginDto);
} else {
updateModel = userModel.errorLock(userConfig.getAlwaysLoginError());
this.ipError();
userLoginLogServer.fail(userModel, 1, false, request);
return new JsonMessage<>(501, "登录失败,请输入正确的密码和账号,多次失败将锁定账号");
}
} finally {
@ -240,7 +250,7 @@ public class LoginControl extends BaseServerController {
@GetMapping(value = "mfa_verify", produces = MediaType.APPLICATION_JSON_VALUE)
@NotLogin
public JsonMessage<UserLoginDto> mfaVerify(String token, String code) {
public JsonMessage<UserLoginDto> mfaVerify(String token, String code, HttpServletRequest request) {
String userId = MFA_TOKEN.get(token);
if (StrUtil.isEmpty(userId)) {
return new JsonMessage<>(201, "登录信息已经过期请重新登录");
@ -251,6 +261,7 @@ public class LoginControl extends BaseServerController {
//
UserLoginDto userLoginDto = this.createToken(userModel);
MFA_TOKEN.remove(token);
userLoginLogServer.success(userModel, true, request);
return JsonMessage.success("登录成功", userLoginDto);
}
@ -290,6 +301,7 @@ public class LoginControl extends BaseServerController {
return new JsonMessage<>(ServerConst.AUTHORIZE_TIME_OUT_CODE, "没有对应的用户");
}
UserLoginDto userLoginDto = userService.getUserJwtId(userModel);
userLoginLogServer.success(userModel, 3, true, request);
return JsonMessage.success("", userLoginDto);
}

View File

@ -0,0 +1,75 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.controller;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.cron.pattern.CronPatternUtil;
import io.jpom.common.JsonMessage;
import io.jpom.common.validator.ValidatorItem;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author bwcx_jzy
* @since 2023/3/10
*/
@RestController
@RequestMapping(value = "/tools")
public class ToolsController {
@GetMapping(value = "cron", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonMessage<List<Long>> cron(@ValidatorItem String cron, @ValidatorItem int count, String date, boolean isMatchSecond) {
Date startDate = null;
Date endDate = null;
if (StrUtil.isNotEmpty(date)) {
List<String> split = StrUtil.splitTrim(date, "~");
try {
startDate = DateUtil.parse(split.get(0));
startDate = DateUtil.beginOfDay(startDate);
endDate = DateUtil.parse(split.get(1));
endDate = DateUtil.endOfDay(endDate);
} catch (Exception e) {
return new JsonMessage<>(405, "日期格式错误:" + e.getMessage());
}
}
try {
List<Date> dateList;
if (startDate != null) {
dateList = CronPatternUtil.matchedDates(cron, startDate, endDate, count, isMatchSecond);
} else {
dateList = CronPatternUtil.matchedDates(cron, DateTime.now(), count, isMatchSecond);
}
return JsonMessage.success("", dateList.stream().map(Date::getTime).collect(Collectors.toList()));
} catch (Exception e) {
return new JsonMessage<>(405, "cron 表达式不正确," + e.getMessage());
}
}
}

View File

@ -284,7 +284,7 @@ public class BuildInfoController extends BaseServerController {
//
String workspaceId = dockerInfoService.getCheckUserWorkspace(request);
int count = dockerInfoService.countByTag(workspaceId, fromTag);
Assert.state(count > 0, "docker tag 填写不正确,没有找到任何docker");
Assert.state(count > 0, fromTag + " 没有找到任何 docker。可能docker tag 填写不正确,需要为 docker 配置标签");
}
}

View File

@ -1,3 +1,25 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.controller.build.repository;
import cn.hutool.db.Page;

View File

@ -1,3 +1,25 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.controller.build.repository;
import cn.hutool.core.convert.Convert;

View File

@ -1,3 +1,25 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.controller.build.repository;
import cn.hutool.core.convert.Convert;

View File

@ -1,3 +1,25 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.controller.build.repository;
import cn.hutool.core.convert.Convert;

View File

@ -33,10 +33,16 @@ import io.jpom.common.JsonMessage;
import io.jpom.common.interceptor.PermissionInterceptor;
import io.jpom.common.validator.ValidatorItem;
import io.jpom.common.validator.ValidatorRule;
import io.jpom.func.user.model.UserLoginLogModel;
import io.jpom.func.user.server.UserLoginLogServer;
import io.jpom.model.data.MailAccountModel;
import io.jpom.model.data.WorkspaceModel;
import io.jpom.model.log.UserOperateLogV1;
import io.jpom.model.user.UserModel;
import io.jpom.monitor.EmailUtil;
import io.jpom.permission.Feature;
import io.jpom.permission.MethodFeature;
import io.jpom.service.dblog.DbUserOperateLogService;
import io.jpom.service.system.SystemParametersServer;
import io.jpom.service.user.UserBindWorkspaceService;
import io.jpom.service.user.UserService;
@ -49,7 +55,9 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import top.jpom.model.PageResultDto;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -71,15 +79,21 @@ public class UserBasicInfoController extends BaseServerController {
private final UserBindWorkspaceService userBindWorkspaceService;
private final UserService userService;
private final ServerConfig.UserConfig userConfig;
private final UserLoginLogServer userLoginLogServer;
private final DbUserOperateLogService dbUserOperateLogService;
public UserBasicInfoController(SystemParametersServer systemParametersServer,
UserBindWorkspaceService userBindWorkspaceService,
UserService userService,
ServerConfig serverConfig) {
ServerConfig serverConfig,
UserLoginLogServer userLoginLogServer,
DbUserOperateLogService dbUserOperateLogService) {
this.systemParametersServer = systemParametersServer;
this.userBindWorkspaceService = userBindWorkspaceService;
this.userService = userService;
this.userConfig = serverConfig.getUser();
this.userLoginLogServer = userLoginLogServer;
this.dbUserOperateLogService = dbUserOperateLogService;
}
@ -221,4 +235,30 @@ public class UserBasicInfoController extends BaseServerController {
userService.bindMfa(user.getId(), mfa);
return JsonMessage.success("绑定成功");
}
/**
* 登录日志列表
*
* @return json
*/
@RequestMapping(value = "list-login-log-data", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<UserLoginLogModel>> listLoginLogData(HttpServletRequest request) {
UserModel user = getUser();
PageResultDto<UserLoginLogModel> pageResult = userLoginLogServer.listPageByUserId(request, user.getId());
return JsonMessage.success("", pageResult);
}
/**
* 操作日志
*
* @return json
*/
@RequestMapping(value = "list-operate-log-data", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<UserOperateLogV1>> listOperateLogData(HttpServletRequest request) {
UserModel user = getUser();
PageResultDto<UserOperateLogV1> pageResult = dbUserOperateLogService.listPageByUserId(request, user.getId());
return JsonMessage.success("", pageResult);
}
}

View File

@ -36,6 +36,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import top.jpom.model.PageResultDto;
import javax.servlet.http.HttpServletRequest;
/**
* 用户操作日志
*
@ -61,8 +63,8 @@ public class UserOptLogController extends BaseServerController {
*/
@RequestMapping(value = "list_data.json", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<UserOperateLogV1>> listData() {
PageResultDto<UserOperateLogV1> pageResult = dbUserOperateLogService.listPage(getRequest());
return JsonMessage.success("获取成功", pageResult);
public JsonMessage<PageResultDto<UserOperateLogV1>> listData(HttpServletRequest request) {
PageResultDto<UserOperateLogV1> pageResult = dbUserOperateLogService.listPage(request);
return JsonMessage.success("", pageResult);
}
}

View File

@ -156,7 +156,7 @@ public class MachineDockerModel extends BaseGroupNameModel {
public Map<String, Object> toParameter() {
Map<String, Object> parameter = new HashMap<>(10);
parameter.put("dockerHost", this.getHost());
// parameter.put("apiVersion", this.getApiVersion());
parameter.put("name", this.getName());
parameter.put("registryUsername", this.getRegistryUsername());
parameter.put("registryPassword", this.getRegistryPassword());
parameter.put("registryEmail", this.getRegistryEmail());

View File

@ -270,6 +270,29 @@ public class MachineDockerServer extends BaseDbService<MachineDockerModel> imple
return machineDockerModel.toParameter();
}
/**
* 跟进 docker 列表找到一个可用的 docker 信息
*
* @param dockerInfoModels docker 列表
* @return map
*/
public Map<String, Object> dockerParameter(List<DockerInfoModel> dockerInfoModels) {
for (DockerInfoModel dockerInfoModel : dockerInfoModels) {
String machineDockerId = dockerInfoModel.getMachineDockerId();
MachineDockerModel machineDockerModel = this.getByKey(machineDockerId, false);
if (machineDockerModel != null) {
Integer status = machineDockerModel.getStatus();
if (status != null && status == 1) {
Map<String, Object> parameter = machineDockerModel.toParameter();
// 更新名称
parameter.put("name", dockerInfoModel.getName());
return parameter;
}
}
}
return null;
}
/**
* 通过集群 id 获取 docker 管理参数
*
@ -300,6 +323,9 @@ public class MachineDockerServer extends BaseDbService<MachineDockerModel> imple
}
public MachineDockerModel tryMachineDockerBySwarmId(String swarmId) {
if (StrUtil.isEmpty(swarmId)) {
return null;
}
//
MachineDockerModel dockerInfoModel = new MachineDockerModel();
dockerInfoModel.setSwarmId(swarmId);

View File

@ -0,0 +1,68 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.func.user.controller;
import io.jpom.common.BaseServerController;
import io.jpom.common.JsonMessage;
import io.jpom.func.user.model.UserLoginLogModel;
import io.jpom.func.user.server.UserLoginLogServer;
import io.jpom.permission.ClassFeature;
import io.jpom.permission.Feature;
import io.jpom.permission.MethodFeature;
import io.jpom.permission.SystemPermission;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import top.jpom.model.PageResultDto;
import javax.servlet.http.HttpServletRequest;
/**
* @author bwcx_jzy
* @since 2023/3/9
*/
@RestController
@RequestMapping(value = "/user/login-log")
@Feature(cls = ClassFeature.USER_LOGIN_LOG)
@SystemPermission
public class UserLoginLogController extends BaseServerController {
private final UserLoginLogServer userLoginLogServer;
public UserLoginLogController(UserLoginLogServer userLoginLogServer) {
this.userLoginLogServer = userLoginLogServer;
}
/**
* 登录日志列表
*
* @return json
*/
@RequestMapping(value = "list-data", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
@Feature(method = MethodFeature.LIST)
public JsonMessage<PageResultDto<UserLoginLogModel>> listData(HttpServletRequest request) {
PageResultDto<UserLoginLogModel> pageResult = userLoginLogServer.listPage(request);
return JsonMessage.success("", pageResult);
}
}

View File

@ -0,0 +1,77 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.func.user.model;
import io.jpom.model.BaseUserModifyDbModel;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import top.jpom.h2db.TableName;
/**
* @author bwcx_jzy
* @since 2023/3/9
*/
@EqualsAndHashCode(callSuper = true)
@TableName(value = "USER_LOGIN_LOG", name = "用户登录日志")
@Data
@NoArgsConstructor
public class UserLoginLogModel extends BaseUserModifyDbModel {
/**
* 操作ip
*/
private String ip;
/**
* 用户名称
*/
private String username;
/**
* 浏览器标识
*/
private String userAgent;
/**
* 是否使用 mfa
*/
private Boolean useMfa;
/**
* 是否成功
*/
private Boolean success;
/**
* 错误原因
* <p>
* 0 正常登录
* 1 密码错误
* 2 被锁定
* 3 续期
* 4 账号被禁用
* 5 登录成功但是需要 mfa 验证
*/
private Integer operateCode;
}

View File

@ -0,0 +1,111 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Code Technology Studio
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package io.jpom.func.user.server;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.Header;
import io.jpom.func.user.model.UserLoginLogModel;
import io.jpom.model.user.UserModel;
import io.jpom.service.h2db.BaseDbService;
import org.springframework.stereotype.Service;
import top.jpom.model.PageResultDto;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @author bwcx_jzy
* @since 2023/3/9
*/
@Service
public class UserLoginLogServer extends BaseDbService<UserLoginLogModel> {
/**
* 查询指定用户的登录日志
*
* @param request 请求信息
* @param userId 用户id
* @return page
*/
public PageResultDto<UserLoginLogModel> listPageByUserId(HttpServletRequest request, String userId) {
Map<String, String> paramMap = ServletUtil.getParamMap(request);
paramMap.put("modifyUser", userId);
return super.listPage(paramMap);
}
/**
* 记录登录日志
*
* @param userModel 用户
* @param success 是否成功
* @param useMfa 是否使用 mfa
* @param request 请求信息
*/
public void log(UserModel userModel, boolean success, boolean useMfa, int operateCode, HttpServletRequest request) {
UserLoginLogModel userLoginLogModel = new UserLoginLogModel();
userLoginLogModel.setModifyUser(userModel.getId());
userLoginLogModel.setUsername(userModel.getName());
userLoginLogModel.setSuccess(success);
userLoginLogModel.setUseMfa(useMfa);
userLoginLogModel.setOperateCode(operateCode);
userLoginLogModel.setIp(ServletUtil.getClientIP(request));
userLoginLogModel.setUserAgent(ServletUtil.getHeader(request, Header.USER_AGENT.getValue(), CharsetUtil.CHARSET_UTF_8));
this.insert(userLoginLogModel);
}
/**
* 记录登录日志
*
* @param userModel 用户
* @param useMfa 是否使用 mfa
* @param request 请求信息
*/
public void success(UserModel userModel, boolean useMfa, HttpServletRequest request) {
this.success(userModel, 0, useMfa, request);
}
/**
* 记录登录日志
*
* @param userModel 用户
* @param useMfa 是否使用 mfa
* @param request 请求信息
*/
public void success(UserModel userModel, int code, boolean useMfa, HttpServletRequest request) {
this.log(userModel, true, useMfa, code, request);
}
/**
* 记录登录日志
*
* @param userModel 用户
* @param useMfa 是否使用 mfa
* @param code 错误码
* @param request 请求信息
*/
public void fail(UserModel userModel, int code, boolean useMfa, HttpServletRequest request) {
this.log(userModel, false, useMfa, code, request);
}
}

View File

@ -61,12 +61,6 @@ public class UserOperateLogV1 extends BaseWorkspaceModel {
* 完整消息
*/
private String resultMsg;
/**
* 操作id
* 用于socket 回话回调更新
*/
@Deprecated
private String reqId;
/**
* 请求参数
*/
@ -86,10 +80,10 @@ public class UserOperateLogV1 extends BaseWorkspaceModel {
private String classFeature;
private String methodFeature;
/**
* 工作空间名称
*/
private String workspaceName;
@Override
public void setId(String id) {
super.setId(id);
this.setReqId(id);
}
private String username;
}

View File

@ -22,6 +22,10 @@
*/
package io.jpom.permission;
import io.jpom.func.assets.server.MachineDockerServer;
import io.jpom.func.assets.server.MachineNodeServer;
import io.jpom.func.assets.server.MachineSshServer;
import io.jpom.func.user.server.UserLoginLogServer;
import io.jpom.service.dblog.*;
import io.jpom.service.docker.DockerInfoService;
import io.jpom.service.docker.DockerSwarmInfoService;
@ -84,14 +88,15 @@ public enum ClassFeature {
BUILD_REPOSITORY("仓库信息", RepositoryService.class),
USER("用户管理", UserService.class),
USER_LOG("操作日志", DbUserOperateLogService.class),
USER_LOGIN_LOG("登录日志", UserLoginLogServer.class),
USER_PERMISSION_GROUP("权限分组", UserPermissionGroupServer.class),
SYSTEM_EMAIL("邮箱配置"),
SYSTEM_CACHE("系统缓存"),
SYSTEM_LOG("系统日志"),
SYSTEM_UPGRADE("在线升级"),
SYSTEM_ASSETS_MACHINE("机器资产管理"),
SYSTEM_ASSETS_MACHINE_SSH("SSH资产管理"),
SYSTEM_ASSETS_MACHINE_DOCKER("DOCKER资产管理"),
SYSTEM_ASSETS_MACHINE("机器资产管理", MachineNodeServer.class),
SYSTEM_ASSETS_MACHINE_SSH("SSH资产管理", MachineSshServer.class),
SYSTEM_ASSETS_MACHINE_DOCKER("DOCKER资产管理", MachineDockerServer.class),
SYSTEM_CONFIG("服务端系统配置"),
SYSTEM_EXT_CONFIG("系统配置目录"),
SYSTEM_CONFIG_IP("系统配置IP白名单"),

View File

@ -49,13 +49,6 @@ public @interface Feature {
*/
MethodFeature method() default MethodFeature.NULL;
/**
* 只记录哪些 状态码
*
* @return code
*/
int[] resultCode() default {};
/**
* 是否记录响应 日志
*

View File

@ -80,6 +80,19 @@ public class DbUserOperateLogService extends BaseWorkspaceService<UserOperateLog
this.workspaceService = workspaceService;
}
/**
* 查询指定用户的操作日志
*
* @param request 请求信息
* @param userId 用户id
* @return page
*/
public PageResultDto<UserOperateLogV1> listPageByUserId(HttpServletRequest request, String userId) {
Map<String, String> paramMap = ServletUtil.getParamMap(request);
paramMap.put("userId", userId);
return super.listPage(paramMap);
}
/**
* 根据 数据ID 节点ID 查询相关数据名称
*
@ -247,6 +260,19 @@ public class DbUserOperateLogService extends BaseWorkspaceService<UserOperateLog
public void insert(UserOperateLogV1 userOperateLogV1, OperateLogController.CacheInfo cacheInfo) {
super.insert(userOperateLogV1);
ThreadUtil.execute(() -> {
// 更新用户名和工作空间名
try {
UserOperateLogV1 update = new UserOperateLogV1();
update.setId(userOperateLogV1.getId());
UserModel userModel = userService.getByKey(userOperateLogV1.getUserId());
Optional.ofNullable(userModel).ifPresent(userModel1 -> update.setUsername(userModel1.getName()));
WorkspaceModel workspaceModel = workspaceService.getByKey(userOperateLogV1.getWorkspaceId());
Optional.ofNullable(workspaceModel).ifPresent(workspaceModel1 -> update.setWorkspaceName(workspaceModel1.getName()));
this.update(update);
} catch (Exception e) {
log.error("更新操作日志失败", e);
}
// 检查操作监控
try {
Map<String, Object> monitor = this.checkMonitor(userOperateLogV1, cacheInfo);
if (monitor != null) {

View File

@ -132,7 +132,7 @@ public class TriggerTokenLogServer extends BaseDbService<TriggerTokenLogBean> im
* @param userId 用户ID
* @return token
*/
public String createToken(String type, String dataId, String userId) {
private String createToken(String type, String dataId, String userId) {
TriggerTokenLogBean trigger = new TriggerTokenLogBean();
String uuid = IdUtil.fastSimpleUUID();
trigger.setId(uuid);

View File

@ -24,7 +24,6 @@ package io.jpom.system.init;
import cn.hutool.core.date.SystemClock;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.db.Entity;
@ -109,10 +108,8 @@ public class OperateLogController implements AopLogInterface {
cacheInfo.setClassFeature(classFeature);
cacheInfo.setMethodFeature(methodFeature);
cacheInfo.setOptTime(SystemClock.now());
cacheInfo.setResultCode(feature.resultCode());
cacheInfo.setLogResponse(feature.logResponse());
//
return cacheInfo;
}
@ -235,11 +232,6 @@ public class OperateLogController implements AopLogInterface {
try {
JsonMessage<?> jsonMessage = JSONObject.parseObject(json, JsonMessage.class);
int code = jsonMessage.getCode();
int[] resultCode = cacheInfo.getResultCode();
if (ArrayUtil.isNotEmpty(resultCode) && !ArrayUtil.contains(resultCode, code)) {
// 忽略
return;
}
userOperateLogV1.setOptStatus(code);
} catch (Exception ignored) {
}
@ -301,7 +293,6 @@ public class OperateLogController implements AopLogInterface {
private String dataId;
private String userAgent;
private String reqData;
private int[] resultCode;
private Boolean logResponse;
/**
* 操作到数据到名称相关 map

View File

@ -136,5 +136,16 @@
"title": "操作监控"
}
]
},
{
"title": "在线工具",
"icon_v3": "tool",
"id": "tools",
"childs": [
{
"id": "cronTools",
"title": "Cron表达式"
}
]
}
]

View File

@ -57,6 +57,11 @@
"id": "user_log",
"title": "操作日志",
"role": "system"
},
{
"id": "user_login_log",
"title": "登录日志",
"role": "system"
}
]
},

View File

@ -12,4 +12,7 @@ DROP,DOCKER_SWARM_INFO,status,
DROP,DOCKER_SWARM_INFO,failureMsg,
DROP,DOCKER_INFO,status
DROP,DOCKER_INFO,failureMsg
ADD,MACHINE_NODE_INFO,transportEncryption,TINYINT,,,传输加密方式 0 不加密 1 BASE64 2 AES
DROP,USEROPERATELOGV1,reqId
ADD,USEROPERATELOGV1,workspaceName,String,50,,工作空间名
ADD,USEROPERATELOGV1,username,String,50,,用户名
ADD,MACHINE_NODE_INFO,transportEncryption,TINYINT,,,传输加密方式 0 不加密 1 BASE64 2 AES
1 alterType,tableName,name,type,len,defaultValue,comment,notNull
12 DROP,DOCKER_SWARM_INFO,failureMsg,
13 DROP,DOCKER_INFO,status
14 DROP,DOCKER_INFO,failureMsg
15 ADD,MACHINE_NODE_INFO,transportEncryption,TINYINT,,,传输加密方式 0 不加密 1 BASE64 2 AES DROP,USEROPERATELOGV1,reqId
16 ADD,USEROPERATELOGV1,workspaceName,String,50,,工作空间名
17 ADD,USEROPERATELOGV1,username,String,50,,用户名
18 ADD,MACHINE_NODE_INFO,transportEncryption,TINYINT,,,传输加密方式 0 不加密 1 BASE64 2 AES

View File

@ -307,7 +307,6 @@ OUT_GIVING,status,Integer,,0,false,false,状态{0: 未分发; 1: 分发中; 2:
OUT_GIVING,secondaryDirectory,String,200,,false,false,二级目录,
OUT_GIVING,uploadCloseFirst,TINYINT,,0,false,false,是否清空旧包发布,
USEROPERATELOGV1,id,String,50,,true,true,id,操作日志
USEROPERATELOGV1,reqId,String,50,,false,false,请求ID,
USEROPERATELOGV1,ip,String,80,,false,false,客户端IP地址,
USEROPERATELOGV1,userId,String,30,,false,false,操作的用户ID,
USEROPERATELOGV1,resultMsg,TEXT,,,false,false,操作的结果信息,

1 tableName name type len defaultValue notNull primaryKey comment tableComment
307 OUT_GIVING secondaryDirectory String 200 false false 二级目录
308 OUT_GIVING uploadCloseFirst TINYINT 0 false false 是否清空旧包发布
309 USEROPERATELOGV1 id String 50 true true id 操作日志
USEROPERATELOGV1 reqId String 50 false false 请求ID
310 USEROPERATELOGV1 ip String 80 false false 客户端IP地址
311 USEROPERATELOGV1 userId String 30 false false 操作的用户ID
312 USEROPERATELOGV1 resultMsg TEXT false false 操作的结果信息

View File

@ -84,3 +84,13 @@ MACHINE_DOCKER_INFO,registryUsername,String,255,,false,false,仓库账号,
MACHINE_DOCKER_INFO,registryPassword,String,255,,false,false,仓库密码,
MACHINE_DOCKER_INFO,registryEmail,String,255,,false,false,仓库邮箱,
MACHINE_DOCKER_INFO,registryUrl,String,255,,false,false,仓库地址,
USER_LOGIN_LOG,id,String,50,,true,true,id,用户登录日志
USER_LOGIN_LOG,createTimeMillis,Long,,,false,false,数据创建时间,
USER_LOGIN_LOG,modifyTimeMillis,Long,,,false,false,数据修改时间,
USER_LOGIN_LOG,modifyUser,String,50,,false,false,修改人,
USER_LOGIN_LOG,ip,String,80,,false,false,客户端IP地址,
USER_LOGIN_LOG,userAgent,TEXT,,,false,false,浏览器标识,
USER_LOGIN_LOG,useMfa,TINYINT,,,false,false,是否使用 mfa,
USER_LOGIN_LOG,success,TINYINT,,,false,false,是否登录成功,
USER_LOGIN_LOG,operateCode,TINYINT,,,false,false,操作状态码(备注码),
USER_LOGIN_LOG,username,String,50,,false,false,昵称,

1 tableName name type len defaultValue notNull primaryKey comment tableComment
84 MACHINE_DOCKER_INFO registryPassword String 255 false false 仓库密码
85 MACHINE_DOCKER_INFO registryEmail String 255 false false 仓库邮箱
86 MACHINE_DOCKER_INFO registryUrl String 255 false false 仓库地址
87 USER_LOGIN_LOG id String 50 true true id 用户登录日志
88 USER_LOGIN_LOG createTimeMillis Long false false 数据创建时间
89 USER_LOGIN_LOG modifyTimeMillis Long false false 数据修改时间
90 USER_LOGIN_LOG modifyUser String 50 false false 修改人
91 USER_LOGIN_LOG ip String 80 false false 客户端IP地址
92 USER_LOGIN_LOG userAgent TEXT false false 浏览器标识
93 USER_LOGIN_LOG useMfa TINYINT false false 是否使用 mfa
94 USER_LOGIN_LOG success TINYINT false false 是否登录成功
95 USER_LOGIN_LOG operateCode TINYINT false false 操作状态码(备注码)
96 USER_LOGIN_LOG username String 50 false false 昵称

View File

@ -20,7 +20,17 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.cron.CronUtil;
import cn.hutool.cron.pattern.CronPattern;
import cn.hutool.cron.pattern.CronPatternUtil;
import org.junit.Test;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
/**
* Created by jiangzeyin on 2019/3/4.
@ -36,4 +46,25 @@ public class TestCron {
CronUtil.restart();
// System.out.println(JpomApplicationEvent.getPid());
}
@Test
public void test() {
String cron = "0 0 23 ? * 5 ";
CronPattern cronPattern = CronPattern.of(cron);
// Date date = CronPatternUtil.nextDateAfter(cronPattern, DateUtil.offsetDay(DateTime.now(), -1), false);
List<Date> dateList = CronPatternUtil.matchedDates(cron, DateUtil.offsetDay(DateTime.now(), -1), 10, true);
for (Date date1 : dateList) {
System.out.println(DateUtil.format(date1, DatePattern.NORM_DATETIME_FORMAT));
}
}
@Test
public void test2() {
Calendar calendar = DateTime.now().toCalendar();
System.out.println(calendar.get(Calendar.DAY_OF_WEEK) - 1);
}
}

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<packaging>pom</packaging>
@ -39,7 +39,7 @@
<module>storage-module-mysql</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<version>2.10.28</version>
<version>2.10.29</version>
<groupId>io.jpom.storage-module</groupId>
<artifactId>jpom-storage-module-parent</artifactId>
<name>Jpom storage module</name>

View File

@ -30,7 +30,7 @@
<parent>
<groupId>io.jpom.storage-module</groupId>
<artifactId>jpom-storage-module-parent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -30,7 +30,7 @@
<parent>
<groupId>io.jpom.storage-module</groupId>
<artifactId>jpom-storage-module-parent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -30,7 +30,7 @@
<parent>
<groupId>io.jpom.storage-module</groupId>
<artifactId>jpom-storage-module-parent</artifactId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-plugins-parent</artifactId>
<groupId>io.jpom.plugins</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-plugins-parent</artifactId>
<groupId>io.jpom.plugins</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-plugins-parent</artifactId>
<groupId>io.jpom.plugins</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<packaging>pom</packaging>
@ -42,7 +42,7 @@
<module>encrypt</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<version>2.10.28</version>
<version>2.10.29</version>
<groupId>io.jpom.plugins</groupId>
<artifactId>jpom-plugins-parent</artifactId>
<name>Jpom Plugins</name>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-plugins-parent</artifactId>
<groupId>io.jpom.plugins</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-plugins-parent</artifactId>
<groupId>io.jpom.plugins</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -29,7 +29,7 @@
<parent>
<artifactId>jpom-parent</artifactId>
<groupId>io.jpom</groupId>
<version>2.10.28</version>
<version>2.10.29</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -50,7 +50,7 @@
简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件
</description>
<inceptionYear>2017</inceptionYear>
<version>2.10.28</version>
<version>2.10.29</version>
<url>https://gitee.com/dromara/Jpom</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

View File

@ -31,12 +31,12 @@
# docker buildx create --use
# 服务端
docker buildx build --platform linux/amd64,linux/arm64 -t jpomdocker/jpom:2.10.28 -t jpomdocker/jpom:latest -f ./modules/server/DockerfileRelease --push .
docker buildx build --platform linux/amd64,linux/arm64 -t jpomdocker/jpom:2.10.29 -t jpomdocker/jpom:latest -f ./modules/server/DockerfileRelease --push .
#
#docker buildx build --platform linux/amd64,linux/arm64 -t jpomdocker/jpom:latest -f ./modules/server/DockerfileRelease --push .
# docker logs --tail="100" jpom-server
# docker run -d -p 2122:2122 --name jpom-server -v /etc/localtime:/etc/localtime:ro -v jpom-server-vol:/usr/local/jpom-server jpomdocker/jpom:mac-arm-2.10.28
# docker run -d -p 2122:2122 --name jpom-server -v /etc/localtime:/etc/localtime:ro -v jpom-server-vol:/usr/local/jpom-server jpomdocker/jpom:mac-arm-2.10.29
# docker run -d -p 2122:2122 --name jpom-server -v D:/home/jpom-server/logs:/usr/local/jpom-server/logs -v D:/home/jpom-server/data:/usr/local/jpom-server/data -v D:/home/jpom-server/conf:/usr/local/jpom-server/conf jpomdocker/jpom
# docker stop jpom-server
# docker rm jpom-server

View File

@ -24,7 +24,7 @@
# 版本
jpom_version=2.10.28
jpom_version=2.10.29
#Mirror_Host=download.fastgit.org
#Mirror_Host=hub.fastgit.xyz

View File

@ -1,6 +1,6 @@
{
"name": "jpom-vue",
"version": "2.10.28",
"version": "2.10.29",
"private": true,
"scripts": {
"serve": "vue-cli-service --max-old-space-size=900 serve --mode dev",

13
web-vue/src/api/tools.js Normal file
View File

@ -0,0 +1,13 @@
import axios from "./config";
/**
*
* @param data
*/
export function cronTools(data) {
return axios({
url: "/tools/cron",
method: "get",
params: data,
});
}

View File

@ -0,0 +1,18 @@
import axios from "../config";
export function userLoginLgin(params) {
return axios({
url: "/user/login-log/list-data",
method: "post",
data: params,
});
}
export const operateCodeMap = {
0: "正常登录",
1: "密码错误",
2: "账号被锁定",
3: "自动续期",
4: "账号被禁用",
5: "登录成功,需要验证 MFA",
};

View File

@ -238,3 +238,19 @@ export function demoInfo() {
params: {},
});
}
export function listLoginLog(params) {
return axios({
url: "/user/list-login-log-data",
method: "post",
data: params,
});
}
export function listOperaterLog(params) {
return axios({
url: "/user/list-operate-log-data",
method: "post",
data: params,
});
}

View File

@ -128,9 +128,9 @@ export default {
buildLogVisible: false,
tableSelections: [],
columns: [
{ title: "构建名称", dataIndex: "buildName", /*width: 120,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "构建 ID", dataIndex: "buildNumberId", width: 90, align: "center", ellipsis: true, scopedSlots: { customRender: "buildNumberId" } },
{ title: "备注", dataIndex: "buildRemark", /*width: 120,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "构建名称", dataIndex: "buildName", width: 120, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "构建 ID", dataIndex: "buildNumberId", width: "90px", align: "center", ellipsis: true, scopedSlots: { customRender: "buildNumberId" } },
{ title: "备注", dataIndex: "buildRemark", width: 120, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "状态", dataIndex: "status", width: "100px", align: "center", ellipsis: true, scopedSlots: { customRender: "status" } },
{ title: "触发类型", dataIndex: "triggerBuildType", align: "center", width: "100px", ellipsis: true, scopedSlots: { customRender: "triggerBuildType" } },
@ -151,7 +151,7 @@ export default {
},
{ title: "发布方式", dataIndex: "releaseMethod", width: "100px", ellipsis: true, scopedSlots: { customRender: "releaseMethod" } },
{ title: "构建人", dataIndex: "modifyUser", width: "130px", ellipsis: true, scopedSlots: { customRender: "modifyUser" } },
{ title: "操作", dataIndex: "operation", scopedSlots: { customRender: "operation" }, width: "200px", align: "center" },
{ title: "操作", dataIndex: "operation", scopedSlots: { customRender: "operation" }, width: "200px", align: "center", fixed: "right" },
],
};
},

View File

@ -1168,16 +1168,17 @@ export default {
afterOptListSimple,
buildConfirmVisible: false,
columns: [
{ title: "名称", dataIndex: "name", sorter: true, ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "名称", dataIndex: "name", sorter: true, width: 200, ellipsis: true, scopedSlots: { customRender: "name" } },
{ title: "分组", dataIndex: "group", width: 100, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{
title: "分支/标签",
dataIndex: "branchName",
ellipsis: true,
width: 100,
scopedSlots: { customRender: "branchName" },
},
{ title: "产物", dataIndex: "resultDirFile", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "方式", dataIndex: "buildMode", align: "center", width: "80px", ellipsis: true, scopedSlots: { customRender: "buildMode" } },
{ title: "产物", dataIndex: "resultDirFile", width: 100, ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "方式", dataIndex: "buildMode", align: "center", width: "80px", sorter: true, ellipsis: true, scopedSlots: { customRender: "buildMode" } },
{ title: "状态", dataIndex: "status", align: "center", width: "100px", ellipsis: true, scopedSlots: { customRender: "status" } },
{
title: "构建 ID",
@ -1200,28 +1201,24 @@ export default {
dataIndex: "modifyTimeMillis",
sorter: true,
customRender: (text) => {
if (!text) {
return "";
}
return parseTime(text);
},
width: 170,
width: "170px",
},
{
title: "其他信息",
title: "发布方式",
dataIndex: "releaseMethod",
width: 100,
width: "100px",
ellipsis: true,
scopedSlots: { customRender: "releaseMethod" },
},
// {
// title: "",
// dataIndex: "resultDirFile",
// ellipsis: true,
// width: 100,
// scopedSlots: { customRender: "resultDirFile" },
// },
// { title: "", width: 100, dataIndex: "script", ellipsis: true, scopedSlots: { customRender: "script" } },
{
title: "定时构建",
dataIndex: "autoBuildCron",
width: 100,
ellipsis: true,
scopedSlots: { customRender: "tooltip" },
},
{
title: "操作",
dataIndex: "operation",
@ -1804,9 +1801,10 @@ export default {
//
selectSwarm() {
this.swarmServiceListOptions = [];
this.tempExtraData = { ...this.tempExtraData, dockerSwarmServiceName: undefined };
if (this.tempExtraData.dockerSwarmId) {
//
dockerSwarmServicesList({
dockerSwarmServicesList("", {
id: this.tempExtraData.dockerSwarmId,
}).then((res) => {
if (res.code === 200) {

View File

@ -46,7 +46,7 @@
<a-input v-model="temp.name" placeholder="容器名称" />
</a-form-model-item>
<a-form-model-item label="标签" prop="tagInput">
<a-form-model-item label="标签" prop="tagInput" help="标签用于容器构建选择容器功能fromTag">
<template>
<div>
<a-tooltip :key="index" :title="tag" v-for="(tag, index) in temp.tagsArray">

View File

@ -31,21 +31,19 @@
</a-sub-menu>
<a-menu-divider />
<a-menu-item @click="handleUpdatePwd">
<a-button type="link">
<a-space><a-icon type="lock" />安全管理</a-space>
</a-button>
<a-button type="link" icon="lock"> 安全管理 </a-button>
</a-menu-item>
<a-menu-divider />
<a-menu-item @click="handleUpdateUser">
<a-button type="link">
<a-space><a-icon type="profile" />用户资料</a-space>
</a-button>
<a-button type="link" icon="profile"> 用户资料 </a-button>
</a-menu-item>
<a-menu-divider />
<a-menu-item>
<a-button @click="customize" type="link">
<a-space><a-icon type="skin" /> 个性配置</a-space>
</a-button>
<a-menu-item @click="handleUserlog">
<a-button type="link" icon="bars"> 操作日志 </a-button>
</a-menu-item>
<a-menu-divider />
<a-menu-item @click="customize">
<a-button type="link" icon="skin"> 个性配置 </a-button>
</a-menu-item>
<a-menu-divider />
<a-menu-item @click="logOut">
@ -277,6 +275,10 @@
</a-row>
</a-space>
</a-modal>
<!-- 查看操作日志 -->
<a-modal destroyOnClose v-model="viewLogVisible" :width="'90vw'" title="操作日志" :footer="null" :maskClosable="false">
<user-log v-if="viewLogVisible"></user-log>
</a-modal>
</div>
</template>
<script>
@ -286,8 +288,12 @@ import QRCode from "qrcodejs2";
import sha1 from "js-sha1";
import Vue from "vue";
import { MFA_APP_TIP_ARRAY } from "@/utils/const";
import UserLog from "./user-log.vue";
export default {
components: {
UserLog,
},
props: {
mode: {
type: String,
@ -336,6 +342,7 @@ export default {
},
MFA_APP_TIP_ARRAY,
bindMfaTip: false,
viewLogVisible: false,
};
},
computed: {
@ -693,6 +700,9 @@ export default {
});
});
},
handleUserlog() {
this.viewLogVisible = true;
},
},
};
</script>

View File

@ -0,0 +1,226 @@
<template>
<div>
<a-tabs>
<a-tab-pane key="1" tab="操作日志">
<!-- 数据表格 -->
<a-table
size="middle"
:data-source="operatelist"
:columns="operatecolumns"
:pagination="operatecpagination"
bordered
rowKey="id"
@change="
(pagination, filters, sorter) => {
this.operatelistQuery = CHANGE_PAGE(this.operatelistQuery, { pagination, sorter });
this.operaterloadData();
}
"
>
<template slot="title">
<a-space>
<a-select show-search option-filter-prop="children" v-model="operatelistQuery.classFeature" allowClear placeholder="操作功能" class="search-input-item">
<a-select-option v-for="item in classFeature" :key="item.value">{{ item.title }}</a-select-option>
</a-select>
<a-select show-search option-filter-prop="children" v-model="operatelistQuery.methodFeature" allowClear placeholder="操作方法" class="search-input-item">
<a-select-option v-for="item in methodFeature" :key="item.value">{{ item.title }}</a-select-option>
</a-select>
<a-range-picker
class="search-input-item"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
@change="
(value, dateString) => {
this.operatelistQuery.createTimeMillis = `${dateString[0]} ~ ${dateString[1]}`;
}
"
/>
<a-tooltip title="按住 Ctr 或者 Alt/Option 键点击按钮快速回到第一页">
<a-button type="primary" :loading="operateloading" @click="operaterloadData">搜索</a-button>
</a-tooltip>
</a-space>
</template>
<a-tooltip slot="classFeature" slot-scope="text" placement="topLeft" :title="classFeatureMap[text]">
<span>{{ classFeatureMap[text] }}</span>
</a-tooltip>
<a-tooltip slot="methodFeature" slot-scope="text" placement="topLeft" :title="methodFeatureMap[text]">
<span>{{ methodFeatureMap[text] }}</span>
</a-tooltip>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="username" slot-scope="text, item" placement="topLeft" :title="text">
<span>{{ text || item.userId }}</span>
</a-tooltip>
<a-tooltip slot="optStatus" slot-scope="text" placement="topLeft" :title="`默认状态码为 200 表示执行成功,部分操作状态码可能为 0,状态码为 0 的操作大部分为没有操作结果或者异步执行`">
<span>{{ text }}</span>
</a-tooltip>
</a-table>
</a-tab-pane>
<a-tab-pane key="2" tab="登录日志">
<a-table
size="middle"
:data-source="loginlist"
:columns="logincolumns"
:pagination="loginpagination"
bordered
rowKey="id"
@change="
(pagination, filters, sorter) => {
this.loginlistQuery = CHANGE_PAGE(this.loginlistQuery, { pagination, sorter });
this.loginloadData();
}
"
>
<template slot="title">
<a-space>
<a-input v-model="loginlistQuery['%username%']" @pressEnter="loginloadData" placeholder="用户名" class="search-input-item" />
<a-input v-model="loginlistQuery['%ip%']" @pressEnter="loginloadData" placeholder="登录IP" class="search-input-item" />
<a-range-picker
class="search-input-item"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
@change="
(value, dateString) => {
this.loginlistQuery.createTimeMillis = `${dateString[0]} ~ ${dateString[1]}`;
}
"
/>
<a-tooltip title="按住 Ctr 或者 Alt/Option 键点击按钮快速回到第一页">
<a-button type="primary" :loading="loginloading" @click="loginloadData">搜索</a-button>
</a-tooltip>
</a-space>
</template>
<a-tooltip slot="success" slot-scope="text" placement="topLeft" :title="text ? '成功' : '失败'">
<a-tag v-if="text" color="green">成功</a-tag>
<a-tag v-else color="pink">失败</a-tag>
</a-tooltip>
<a-tooltip slot="useMfa" slot-scope="text" placement="topLeft" :title="text ? '使用' : '未使用'">
<a-tag>{{ text ? "使用" : "未使用" }}</a-tag>
</a-tooltip>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="operateCode" slot-scope="text" placement="topLeft" :title="operateCode[text] || '未知'">
{{ operateCode[text] || "未知" }}
</a-tooltip>
</a-table>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script>
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime } from "@/utils/const";
import { listOperaterLog, listLoginLog } from "@/api/user/user";
import { getMonitorOperateTypeList } from "@/api/monitor";
import { operateCodeMap } from "@/api/user/user-login-log";
export default {
data() {
return {
operateloading: false,
operatelist: [],
operatelistQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
methodFeature: [],
classFeature: [],
methodFeatureMap: {},
classFeatureMap: {},
operatecolumns: [
{ title: "操作者", dataIndex: "username", ellipsis: true, scopedSlots: { customRender: "username" } },
{ title: "IP", dataIndex: "ip", ellipsis: true, width: "130px" },
{ title: "节点", dataIndex: "nodeId", width: 120, ellipsis: true, scopedSlots: { customRender: "nodeId" } },
{ title: "数据名称", dataIndex: "dataName", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "工作空间名", dataIndex: "workspaceName", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
// { title: " ID", dataIndex: "dataId", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "操作功能", dataIndex: "classFeature", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "classFeature" } },
{ title: "操作方法", dataIndex: "methodFeature", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "methodFeature" } },
{ title: "状态码", dataIndex: "optStatus", width: 90, scopedSlots: { customRender: "optStatus" } },
{
title: "操作时间",
dataIndex: "createTimeMillis",
sorter: true,
customRender: (text, item) => {
return parseTime(text || item.optTime);
},
width: "170px",
},
],
loginloading: false,
loginlist: [],
operateCode: operateCodeMap,
loginlistQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
logincolumns: [
{ title: "用户ID", dataIndex: "modifyUser", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "用户名称", dataIndex: "username", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "IP", dataIndex: "ip", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "浏览器", dataIndex: "userAgent", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "是否成功", dataIndex: "success", ellipsis: true, width: "100px", scopedSlots: { customRender: "success" } },
{ title: "是否使用MFA", dataIndex: "useMfa", ellipsis: true, width: "130px", scopedSlots: { customRender: "useMfa" } },
{ title: "结果描述", dataIndex: "operateCode", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "operateCode" } },
{
title: "登录时间",
dataIndex: "createTimeMillis",
sorter: true,
customRender: (text, item) => {
return parseTime(text || item.optTime);
},
width: "170px",
},
],
};
},
computed: {
operatecpagination() {
return COMPUTED_PAGINATION(this.operatelistQuery);
},
loginpagination() {
return COMPUTED_PAGINATION(this.loginlistQuery);
},
},
created() {
this.operaterloadData();
this.loginloadData();
getMonitorOperateTypeList().then((res) => {
this.methodFeature = res.data.methodFeature;
this.classFeature = res.data.classFeature;
res.data.methodFeature.forEach((item) => {
this.methodFeatureMap[item.value] = item.title;
});
res.data.classFeature.forEach((item) => {
this.classFeatureMap[item.value] = item.title;
});
});
},
methods: {
CHANGE_PAGE,
//
operaterloadData(pointerEvent) {
this.operateloading = true;
this.operatelistQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.operatelistQuery.page;
listOperaterLog(this.operatelistQuery).then((res) => {
if (res.code === 200) {
this.operatelist = res.data.result;
this.operatelistQuery.total = res.data.total;
}
this.operateloading = false;
});
},
loginloadData(pointerEvent) {
this.loginloading = true;
this.loginlistQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.loginlistQuery.page;
listLoginLog(this.loginlistQuery).then((res) => {
if (res.code === 200) {
this.loginlist = res.data.result;
this.loginlistQuery.total = res.data.total;
}
this.loginloading = false;
});
},
},
};
</script>

View File

@ -30,8 +30,8 @@
<br />
<template v-if="this.action === 'login'">
<a-form-model ref="loginForm" :label-col="{ span: 0 }" :model="loginForm" :rules="rules" @submit="handleLogin">
<a-form-model-item :wrapper-col="{ span: 24 }" prop="userName">
<a-input v-model="loginForm.userName" placeholder="用户名" />
<a-form-model-item :wrapper-col="{ span: 24 }" prop="loginName">
<a-input v-model="loginForm.loginName" placeholder="用户名" />
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 24 }" prop="userPwd">
<a-input-password v-model="loginForm.userPwd" placeholder="密码" />
@ -73,7 +73,7 @@ export default {
data() {
return {
loginForm: {
userName: "",
loginName: "",
userPwd: "",
code: "",
},
@ -83,7 +83,7 @@ export default {
dynamicBg: localStorage.getItem("dynamicBg") === "true",
loginTitle: "登录JPOM",
rules: {
userName: [{ required: true, message: "请输入用户名" }],
loginName: [{ required: true, message: "请输入用户名" }],
userPwd: [{ required: true, message: "请输入密码" }],
code: [{ required: true, message: "请输入验证码" }],
mfaCode: [
@ -147,7 +147,7 @@ export default {
message: "温馨提示",
description: h("div", null, [h("p", { domProps: { innerHTML: res.msg } }, null)]),
});
this.loginForm.userName = res.data.user;
this.loginForm.loginName = res.data.user;
}
});
},

View File

@ -10,6 +10,10 @@
<a-tooltip slot="time" slot-scope="text" placement="topLeft" :title="parseTime(text)">
<span>{{ parseTime(text) }}</span>
</a-tooltip>
<a-tooltip slot="cron" slot-scope="text" placement="topLeft" :title="text">
<!-- <a-icon type="unordered-list" /> -->
<a-button type="link" style="padding: 0px" size="small" @click="toCronTaskList(text)"> {{ text }} <a-icon type="unordered-list" /> </a-button>
</a-tooltip>
</a-table>
</div>
</template>
@ -58,6 +62,7 @@ export default {
{
title: "cron",
dataIndex: "cron",
scopedSlots: { customRender: "cron" },
// sorter: (a, b) => (a && b ? a.localeCompare(b, "zh-CN") : 0),
// sortDirections: ["descend", "ascend"],
},
@ -100,6 +105,20 @@ export default {
refresh() {
this.$emit("refresh");
},
// cron
toCronTaskList(cron) {
const newpage = this.$router.resolve({
name: "cron-task-list",
path: "/tools/cron",
query: {
...this.$route.query,
sPid: "tools",
sId: "cronTools",
cron: cron,
},
});
window.open(newpage.href, "_blank");
},
},
};
</script>

View File

@ -56,6 +56,7 @@
<a-modal destroyOnClose v-model="configMenuVisible" :title="`${temp.name} 工作空间菜单`" @ok="onSubmitMenus" :maskClosable="false">
<a-form-model ref="editWhiteForm" :model="menusConfigData">
<a-row type="flex" justify="center">
<a-alert :message="`菜单配置只对非超级管理员生效`" style="margin-top: 10px; margin-bottom: 20px" banner />
<a-col :span="12">
<a-card title="服务端菜单" :bordered="false">
<a-tree show-icon v-if="menusConfigData.serverMenus" checkable :tree-data="menusConfigData.serverMenus" :replaceFields="replaceFields" v-model="menusConfigData.serverMenuKeys">

View File

@ -1,24 +0,0 @@
<template>
<code-editor v-model="code" showTool @checkJson="checkJson" :options="{ mode: 'application/json' }"> </code-editor>
</template>
<script>
import codeEditor from "@/components/codeEditor";
export default {
components: {
codeEditor,
},
data() {
return {
code: "",
};
},
methods: {
checkJson() {
console.log(11);
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,153 @@
<template>
<div>
<a-row justify="center" type="flex">
<a-col :span="18">
<a-space direction="vertical" style="display: inline">
<a-alert message="此工具用于检查 cron 表达式是否正确,以及计划运行时间" type="info" />
<a-collapse>
<a-collapse-panel key="1" header="更多说明">
定时任务表达式<br />
表达式类似于Linux的crontab表达式表达式使用空格分成5个部分按顺序依次为
<ol>
<li><strong></strong> 范围0~59</li>
<li><strong></strong> 范围0~23</li>
<li><strong></strong> 范围1~31<strong>"L"</strong> 表示月的最后一天</li>
<li><strong></strong> 范围1~12同时支持不区分大小写的别名"jan","feb", "mar", "apr", "may","jun", "jul", "aug", "sep","oct", "nov", "dec"</li>
<li><strong></strong> 范围0 (Sunday)~6(Saturday)7也可以表示周日同时支持不区分大小写的别名"sun","mon", "tue", "wed", "thu","fri", "sat"<strong>"L"</strong> 表示周六</li>
</ol>
<p>为了兼容Quartz表达式同时支持6位和7位表达式其中<br /></p>
<pre>
当为6位时第一位表示<strong></strong> 范围0~59但是第一位不做匹配
当为7位时最后一位表示<strong></strong> 范围1970~2099但是第7位不做解析也不做匹配
</pre
>
<p>
当定时任务运行到的时间匹配这些表达式后任务被启动<br />
注意
</p>
<pre>
当isMatchSecond为 true 时才会匹配秒部分
默认都是关闭的
</pre
>
<p>对于每一个子表达式同样支持以下形式</p>
<ul>
<li><strong>*</strong> 表示匹配这个位置所有的时间</li>
<li><strong>?</strong> 表示匹配这个位置任意的时间"*"作用一致</li>
<li><strong>*&#47;2</strong> 表示间隔时间例如在分上表示每两分钟同样*可以使用数字列表代替逗号分隔</li>
<li><strong>2-8</strong> 表示连续区间例如在分上表示2,3,4,5,6,7,8</li>
<li><strong>2,3,5,8</strong> 表示列表</li>
<li><strong>cronA | cronB</strong> 表示多个定时表达式</li>
</ul>
注意在每一个子表达式中优先级
<pre>
间隔/ &gt; 区间- &gt; 列表,
</pre
>
<p>
例如 2,3,6/3由于/优先级高因此相当于2,3,(6/3)结果与 2,3,6等价<br />
<br />
</p>
<p>一些例子</p>
<ul>
<li><strong>5 * * * *</strong> 每个点钟的5分执行00:05,01:05</li>
<li><strong>* * * * *</strong> 每分钟执行</li>
<li><strong>*&#47;2 * * * *</strong> 每两分钟执行</li>
<li><strong>* 12 * * *</strong> 12点的每分钟执行</li>
<li><strong>59 11 * * 1,2</strong> 每周一和周二的11:59执行</li>
<li><strong>3-18&#47;5 * * * *</strong> 3~18每5分钟执行一次即0:03, 0:08, 0:13, 0:18, 1:03, 1:08</li>
</ul>
</a-collapse-panel>
</a-collapse>
<a-form-model :model="temp" ref="form" :rules="rules" :label-col="{ span: 4 }" :wrapper-col="{ span: 18 }">
<a-form-model-item label="cron表达式" prop="cron">
<a-input v-model="temp.cron" placeholder="请输入要检查的 cron 表达式" />
</a-form-model-item>
<a-form-model-item label="计划次数" prop="count">
<a-input-number v-model="temp.count" :min="1" placeholder="请输入获取的计划运行次数" style="width: 100%" />
</a-form-model-item>
<a-form-model-item label="匹配秒">
<a-switch v-model="temp.isMatchSecond" checked-children="" un-checked-children="" />
</a-form-model-item>
<a-form-model-item label="时间范围" prop="date" help="默认是当前时间到今年结束">
<a-range-picker format="YYYY-MM-DD" valueFormat="YYYY-MM-DD" separator="至" v-model="temp.date" style="width: 100%" />
</a-form-model-item>
<a-form-model-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="onSubmit"> 检查 </a-button>
</a-form-model-item>
</a-form-model>
</a-space>
</a-col>
<a-col :span="10">
<a-list bordered :data-source="resultList" :locale="locale">
<a-list-item slot="renderItem" slot-scope="item">
{{ parseTime(item, "{y}-{m}-{d} {h}:{i}:{s} 周{a}") }}
</a-list-item>
<div slot="header">结果</div>
</a-list>
</a-col>
</a-row>
</div>
</template>
<script>
import { cronTools } from "@/api/tools";
import { parseTime } from "@/utils/const";
export default {
data() {
return {
temp: {
count: 10,
},
locale: {
emptyText: "暂无数据",
},
resultList: [],
//
rules: {
cron: [{ required: true, message: "请输入要检查的 cron 表达式", trigger: "blur" }],
count: [{ required: true, message: "请输入获取的计划运行次数", trigger: "blur" }],
},
};
},
mounted() {
const cron = this.$route.query.cron;
if (cron) {
this.temp = { ...this.temp, cron: cron };
this.$nextTick(() => {
this.onSubmit();
});
}
},
methods: {
parseTime,
onSubmit() {
this.$refs["form"].validate((valid) => {
if (!valid) {
return false;
}
this.resultList = [];
this.locale = {
emptyText: "暂无数据",
};
const temp = { ...this.temp, date: this.temp.date && this.temp.date[0] + " ~ " + this.temp.date[1] };
cronTools(temp).then((res) => {
// console.log(res);
this.resultList = res.data || [];
this.locale = {
emptyText: res.msg,
};
});
});
},
},
};
</script>

View File

@ -34,6 +34,11 @@
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="username" slot-scope="text, item" placement="topLeft" :title="text">
<span>{{ text || item.userId }}</span>
</a-tooltip>
<a-tooltip slot="optStatus" slot-scope="text" placement="topLeft" :title="`默认状态码为 200 表示执行成功,部分操作状态码可能为 0,状态码为 0 的操作大部分为没有操作结果或者异步执行`">
<span>{{ text }}</span>
</a-tooltip>
@ -84,11 +89,12 @@ export default {
detailVisible: false,
detailData: [],
columns: [
{ title: "操作者", dataIndex: "userId" },
{ title: "操作者", dataIndex: "username", ellipsis: true, scopedSlots: { customRender: "username" } },
{ title: "IP", dataIndex: "ip" /*width: 130*/ },
{ title: "节点", dataIndex: "nodeId", width: 120, ellipsis: true, scopedSlots: { customRender: "nodeId" } },
{ title: "数据名称", dataIndex: "dataName", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "数据 ID", dataIndex: "dataId", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "工作空间名", dataIndex: "workspaceName", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
// { title: " ID", dataIndex: "dataId", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "操作功能", dataIndex: "classFeature", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "classFeature" } },
{ title: "操作方法", dataIndex: "methodFeature", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "methodFeature" } },
{ title: "状态码", dataIndex: "optStatus", width: 90, scopedSlots: { customRender: "optStatus" } },
@ -99,7 +105,7 @@ export default {
customRender: (text, item) => {
return parseTime(text || item.optTime);
},
width: 160,
width: "170px",
},
{ title: "操作", align: "center", dataIndex: "operation", scopedSlots: { customRender: "operation" }, width: 80 },
],
@ -183,6 +189,7 @@ export default {
} catch (e) {
console.error(e);
}
this.detailData.push({ title: "数据Id", description: this.temp.dataId });
this.detailData.push({ title: "浏览器标识", description: this.temp.userAgent });
this.detailData.push({ title: "请求参数", json: true, value: this.temp.reqData });
this.detailData.push({ title: "响应结果", json: true, value: this.temp.resultMsg });

View File

@ -0,0 +1,102 @@
<template>
<div class="full-content">
<!-- 数据表格 -->
<a-table size="middle" :data-source="list" :columns="columns" :pagination="pagination" bordered :rowKey="(record, index) => index" @change="change">
<template slot="title">
<a-space>
<a-input v-model="listQuery['%username%']" @pressEnter="loadData" placeholder="用户名" class="search-input-item" />
<a-input v-model="listQuery['%ip%']" @pressEnter="loadData" placeholder="登录IP" class="search-input-item" />
<a-range-picker class="search-input-item" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" @change="onchangeTime" />
<a-tooltip title="按住 Ctr 或者 Alt/Option 键点击按钮快速回到第一页">
<a-button type="primary" :loading="loading" @click="loadData">搜索</a-button>
</a-tooltip>
</a-space>
</template>
<a-tooltip slot="success" slot-scope="text" placement="topLeft" :title="text ? '成功' : '失败'">
<a-tag v-if="text" color="green">成功</a-tag>
<a-tag v-else color="pink">失败</a-tag>
</a-tooltip>
<a-tooltip slot="useMfa" slot-scope="text" placement="topLeft" :title="text ? '使用' : '未使用'">
<a-tag>{{ text ? "使用" : "未使用" }}</a-tag>
</a-tooltip>
<a-tooltip slot="tooltip" slot-scope="text" placement="topLeft" :title="text">
<span>{{ text }}</span>
</a-tooltip>
<a-tooltip slot="operateCode" slot-scope="text" placement="topLeft" :title="operateCode[text] || '未知'">
{{ operateCode[text] || "未知" }}
</a-tooltip>
</a-table>
</div>
</template>
<script>
import { userLoginLgin, operateCodeMap } from "@/api/user/user-login-log";
import { CHANGE_PAGE, COMPUTED_PAGINATION, PAGE_DEFAULT_LIST_QUERY, parseTime } from "@/utils/const";
export default {
components: {},
data() {
return {
loading: false,
list: [],
operateCode: operateCodeMap,
listQuery: Object.assign({}, PAGE_DEFAULT_LIST_QUERY),
columns: [
{ title: "用户ID", dataIndex: "modifyUser", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "用户名称", dataIndex: "username", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "IP", dataIndex: "ip", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "浏览器", dataIndex: "userAgent", ellipsis: true, scopedSlots: { customRender: "tooltip" } },
{ title: "是否成功", dataIndex: "success", ellipsis: true, width: "100px", scopedSlots: { customRender: "success" } },
{ title: "是否使用MFA", dataIndex: "useMfa", ellipsis: true, width: "130px", scopedSlots: { customRender: "useMfa" } },
{ title: "结果描述", dataIndex: "operateCode", /*width: 240,*/ ellipsis: true, scopedSlots: { customRender: "operateCode" } },
{
title: "登录时间",
dataIndex: "createTimeMillis",
sorter: true,
customRender: (text, item) => {
return parseTime(text || item.optTime);
},
width: "170px",
},
],
};
},
computed: {
pagination() {
return COMPUTED_PAGINATION(this.listQuery);
},
},
created() {
this.loadData();
},
methods: {
//
loadData(pointerEvent) {
this.loading = true;
this.listQuery.page = pointerEvent?.altKey || pointerEvent?.ctrlKey ? 1 : this.listQuery.page;
userLoginLgin(this.listQuery).then((res) => {
if (res.code === 200) {
this.list = res.data.result;
this.listQuery.total = res.data.total;
}
this.loading = false;
});
},
//
change(pagination, filters, sorter) {
this.listQuery = CHANGE_PAGE(this.listQuery, { pagination, sorter });
this.loadData();
},
//
onchangeTime(value, dateString) {
this.listQuery.createTimeMillis = `${dateString[0]} ~ ${dateString[1]}`;
},
},
};
</script>
<style scoped></style>

View File

@ -129,6 +129,11 @@ const children = [
name: "script-env-list",
component: () => import("../pages/script/env"),
},
{
path: "/tools/cron",
name: "cron-tools",
component: () => import("../pages/tools/cron"),
},
];
const management = [
@ -162,7 +167,11 @@ const management = [
name: "operation-log",
component: () => import("../pages/user/operation-log"),
},
{
path: "/user/login-log",
name: "user-login-log",
component: () => import("../pages/user/user-login-log"),
},
// 工作空间
{
path: "/system/workspace",

View File

@ -31,6 +31,7 @@ const routeMenuMap = {
userList: "/user/list",
permission_group: "/user/permission-group",
user_log: "/operation/log",
user_login_log: "/user/login-log",
monitorConfigEmail: "/system/mail",
cacheManage: "/system/cache",
logManage: "/system/log",
@ -46,6 +47,7 @@ const routeMenuMap = {
machine_ssh_info: "/system/assets/ssh-list",
machine_docker_info: "/system/assets/docker-list",
configWorkspaceEnv: "/script/env-list",
cronTools: "/tools/cron",
};
export default routeMenuMap;

View File

@ -108,9 +108,11 @@ export const uploadPieces = ({ file, uploadCallback, success, process, error })
**/
const concurrentUpload = () => {
const startTime = new Date().getTime();
// 设置初始化进度(避免第一份分片卡顿)
process(0.01, 1, total, new Date().getTime() - startTime);
concurrentExecution(chunkList, uploadFileConcurrent, (curItem) => {
return new Promise((resolve, reject) => {
const { chunk, end } = getChunkInfo(file, curItem, chunkSize);
const { chunk } = getChunkInfo(file, curItem, chunkSize);
const chunkInfo = {
chunk,
currentChunk: curItem,
@ -125,7 +127,7 @@ export const uploadPieces = ({ file, uploadCallback, success, process, error })
uploaded.push(chunkInfo.currentChunk + 1);
const sd = parseInt((uploaded.length / chunkInfo.chunkCount) * 100);
// console.log(chunk);
process(sd, end, total, new Date().getTime() - startTime);
process(sd, Math.min(uploaded.length * chunkSize, total), total, new Date().getTime() - startTime);
//
/***
* 创建文件上传参数