feat: 获取终端 accessToken.
This commit is contained in:
@@ -50,7 +50,7 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(value = Exception.class)
|
@ExceptionHandler(value = Exception.class)
|
||||||
public HttpWrapper<?> defaultExceptionHandler(Exception ex) {
|
public HttpWrapper<?> defaultExceptionHandler(Exception ex) {
|
||||||
log.error("defaultExceptionHandler", ex);
|
log.error("defaultExceptionHandler", ex);
|
||||||
return ErrorCode.INTERNAL_SERVER_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.INTERNAL_SERVER_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- http 异常 --------------------
|
// -------------------- http 异常 --------------------
|
||||||
@@ -98,7 +98,7 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(value = MaxUploadSizeExceededException.class)
|
@ExceptionHandler(value = MaxUploadSizeExceededException.class)
|
||||||
public HttpWrapper<?> maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) {
|
public HttpWrapper<?> maxUploadSizeExceededExceptionHandler(MaxUploadSizeExceededException ex) {
|
||||||
log.error("maxUploadSizeExceededExceptionHandler", ex);
|
log.error("maxUploadSizeExceededExceptionHandler", ex);
|
||||||
return ErrorCode.PAYLOAD_TOO_LARGE.wrapper(ex.getMessage());
|
return ErrorCode.PAYLOAD_TOO_LARGE.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------- 框架异常 --------------------
|
// -------------------- 框架异常 --------------------
|
||||||
@@ -132,7 +132,7 @@ public class GlobalExceptionHandler {
|
|||||||
})
|
})
|
||||||
public HttpWrapper<?> timeoutExceptionHandler(Exception ex) {
|
public HttpWrapper<?> timeoutExceptionHandler(Exception ex) {
|
||||||
log.error("timeoutExceptionHandler", ex);
|
log.error("timeoutExceptionHandler", ex);
|
||||||
return ErrorCode.REQUEST_TIMEOUT.wrapper(ex.getMessage());
|
return ErrorCode.REQUEST_TIMEOUT.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = {
|
@ExceptionHandler(value = {
|
||||||
@@ -142,7 +142,7 @@ public class GlobalExceptionHandler {
|
|||||||
})
|
})
|
||||||
public HttpWrapper<?> interruptExceptionHandler(Exception ex) {
|
public HttpWrapper<?> interruptExceptionHandler(Exception ex) {
|
||||||
log.error("interruptExceptionHandler", ex);
|
log.error("interruptExceptionHandler", ex);
|
||||||
return ErrorCode.INTERRUPT_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.INTERRUPT_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = {
|
@ExceptionHandler(value = {
|
||||||
@@ -151,7 +151,7 @@ public class GlobalExceptionHandler {
|
|||||||
})
|
})
|
||||||
public HttpWrapper<?> ioExceptionHandler(Exception ex) {
|
public HttpWrapper<?> ioExceptionHandler(Exception ex) {
|
||||||
log.error("ioExceptionHandler", ex);
|
log.error("ioExceptionHandler", ex);
|
||||||
return ErrorCode.IO_EXCEPTION.wrapper(ex.getMessage());
|
return ErrorCode.IO_EXCEPTION.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = SQLException.class)
|
@ExceptionHandler(value = SQLException.class)
|
||||||
@@ -178,7 +178,7 @@ public class GlobalExceptionHandler {
|
|||||||
})
|
})
|
||||||
public HttpWrapper<?> sftpExceptionHandler(Exception ex) {
|
public HttpWrapper<?> sftpExceptionHandler(Exception ex) {
|
||||||
log.error("sftpExceptionHandler", ex);
|
log.error("sftpExceptionHandler", ex);
|
||||||
return ErrorCode.SFTP_EXCEPTION.wrapper(ex.getMessage());
|
return ErrorCode.SFTP_EXCEPTION.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = ParseRuntimeException.class)
|
@ExceptionHandler(value = ParseRuntimeException.class)
|
||||||
@@ -186,22 +186,22 @@ public class GlobalExceptionHandler {
|
|||||||
log.error("parseExceptionHandler", ex);
|
log.error("parseExceptionHandler", ex);
|
||||||
if (Exceptions.isCausedBy(ex, EncryptedDocumentException.class)) {
|
if (Exceptions.isCausedBy(ex, EncryptedDocumentException.class)) {
|
||||||
// excel 密码错误
|
// excel 密码错误
|
||||||
return ErrorCode.EXCEL_PASSWORD_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.EXCEL_PASSWORD_ERROR.wrapper();
|
||||||
} else {
|
} else {
|
||||||
return ErrorCode.PASER_FAILED.wrapper(ex.getMessage());
|
return ErrorCode.PASER_FAILED.wrapper();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = EncryptException.class)
|
@ExceptionHandler(value = EncryptException.class)
|
||||||
public HttpWrapper<?> encryptExceptionHandler(Exception ex) {
|
public HttpWrapper<?> encryptExceptionHandler(Exception ex) {
|
||||||
log.error("encryptExceptionHandler", ex);
|
log.error("encryptExceptionHandler", ex);
|
||||||
return ErrorCode.ENCRYPT_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.ENCRYPT_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = DecryptException.class)
|
@ExceptionHandler(value = DecryptException.class)
|
||||||
public HttpWrapper<?> decryptExceptionHandler(Exception ex) {
|
public HttpWrapper<?> decryptExceptionHandler(Exception ex) {
|
||||||
log.error("decryptExceptionHandler", ex);
|
log.error("decryptExceptionHandler", ex);
|
||||||
return ErrorCode.DECRYPT_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.DECRYPT_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = {HttpRequestException.class})
|
@ExceptionHandler(value = {HttpRequestException.class})
|
||||||
@@ -213,7 +213,7 @@ public class GlobalExceptionHandler {
|
|||||||
@ExceptionHandler(value = VcsException.class)
|
@ExceptionHandler(value = VcsException.class)
|
||||||
public HttpWrapper<?> vcsExceptionHandler(Exception ex) {
|
public HttpWrapper<?> vcsExceptionHandler(Exception ex) {
|
||||||
log.error("vcsExceptionHandler", ex);
|
log.error("vcsExceptionHandler", ex);
|
||||||
return ErrorCode.VCS_OPETATOR_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.VCS_OPETATOR_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = {
|
@ExceptionHandler(value = {
|
||||||
@@ -222,30 +222,31 @@ public class GlobalExceptionHandler {
|
|||||||
})
|
})
|
||||||
public HttpWrapper<?> taskExceptionHandler(Exception ex) {
|
public HttpWrapper<?> taskExceptionHandler(Exception ex) {
|
||||||
log.error("taskExceptionHandler", ex);
|
log.error("taskExceptionHandler", ex);
|
||||||
return ErrorCode.TASK_EXECUTE_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.TASK_EXECUTE_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = ConnectionRuntimeException.class)
|
@ExceptionHandler(value = ConnectionRuntimeException.class)
|
||||||
public HttpWrapper<?> connectionExceptionHandler(Exception ex) {
|
public HttpWrapper<?> connectionExceptionHandler(Exception ex) {
|
||||||
log.error("connectionExceptionHandler", ex);
|
log.error("connectionExceptionHandler", ex);
|
||||||
return ErrorCode.CONNECT_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.CONNECT_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = UnsafeException.class)
|
@ExceptionHandler(value = UnsafeException.class)
|
||||||
public HttpWrapper<?> unsafeExceptionHandler(Exception ex) {
|
public HttpWrapper<?> unsafeExceptionHandler(Exception ex) {
|
||||||
log.error("unsafeExceptionHandler", ex);
|
log.error("unsafeExceptionHandler", ex);
|
||||||
return ErrorCode.UNSAFE_OPERATOR.wrapper(ex.getMessage());
|
return ErrorCode.UNSAFE_OPERATOR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = LogException.class)
|
@ExceptionHandler(value = LogException.class)
|
||||||
public HttpWrapper<?> logExceptionHandler(LogException ex) {
|
public HttpWrapper<?> logExceptionHandler(LogException ex) {
|
||||||
log.error("logExceptionHandler", ex);
|
log.error("logExceptionHandler", ex);
|
||||||
return ErrorCode.INTERNAL_SERVER_ERROR.wrapper(ex.getMessage());
|
return ErrorCode.INTERNAL_SERVER_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = ParseCronException.class)
|
@ExceptionHandler(value = ParseCronException.class)
|
||||||
public HttpWrapper<?> parseCronExceptionHandler(ParseCronException ex) {
|
public HttpWrapper<?> parseCronExceptionHandler(ParseCronException ex) {
|
||||||
return ErrorCode.EXPRESSION_ERROR.wrapper(ex.getMessage());
|
log.error("parseCronExceptionHandler", ex);
|
||||||
|
return ErrorCode.EXPRESSION_ERROR.wrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExceptionHandler(value = CodeArgumentException.class)
|
@ExceptionHandler(value = CodeArgumentException.class)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
### 获取主机终端连接 token
|
||||||
|
POST {{baseUrl}}/asset/host-terminal/access
|
||||||
|
Content-Type: application/json
|
||||||
|
Authorization: {{token}}
|
||||||
|
|
||||||
|
{
|
||||||
|
"hostId": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
###
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.orion.ops.module.asset.controller;
|
||||||
|
|
||||||
|
import com.orion.ops.framework.biz.operator.log.core.annotation.OperatorLog;
|
||||||
|
import com.orion.ops.framework.security.core.utils.SecurityUtils;
|
||||||
|
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.HostTerminalConnectRequest;
|
||||||
|
import com.orion.ops.module.asset.service.HostTerminalService;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
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 javax.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主机终端 api
|
||||||
|
*
|
||||||
|
* @author Jiahang Li
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2023-9-20 11:55
|
||||||
|
*/
|
||||||
|
@Tag(name = "asset - 主机终端服务")
|
||||||
|
@Slf4j
|
||||||
|
@Validated
|
||||||
|
@RestWrapper
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/asset/host-terminal")
|
||||||
|
@SuppressWarnings({"ELValidationInJSP", "SpringElInspection"})
|
||||||
|
public class HostTerminalController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HostTerminalService hostTerminalService;
|
||||||
|
|
||||||
|
@OperatorLog(HostTerminalOperatorType.ACCESS)
|
||||||
|
@PostMapping("/access")
|
||||||
|
@Operation(summary = "获取主机终端连接 token")
|
||||||
|
@PreAuthorize("@ss.hasPermission('asset:host-terminal:access')")
|
||||||
|
public String getHostAccessToken(@Validated @RequestBody HostTerminalConnectRequest request) {
|
||||||
|
return hostTerminalService.getHostAccessToken(request.getHostId(), SecurityUtils.getLoginUserId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.orion.ops.module.asset.define.cache;
|
||||||
|
|
||||||
|
import com.orion.lang.define.cache.key.CacheKeyBuilder;
|
||||||
|
import com.orion.lang.define.cache.key.CacheKeyDefine;
|
||||||
|
import com.orion.lang.define.cache.key.struct.RedisCacheStruct;
|
||||||
|
import com.orion.ops.module.asset.entity.dto.HostSshConnectDTO;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主机终端服务缓存 key
|
||||||
|
*
|
||||||
|
* @author Jiahang Li
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2023/12/27 18:13
|
||||||
|
*/
|
||||||
|
public interface HostTerminalCacheKeyDefine {
|
||||||
|
|
||||||
|
CacheKeyDefine HOST_TERMINAL_CONNECT = new CacheKeyBuilder()
|
||||||
|
.key("host:terminal:connect:{}")
|
||||||
|
.desc("主机终端连接信息 ${token}")
|
||||||
|
.type(HostSshConnectDTO.class)
|
||||||
|
.struct(RedisCacheStruct.STRING)
|
||||||
|
.timeout(3, TimeUnit.MINUTES)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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.L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主机终端 操作日志类型
|
||||||
|
*
|
||||||
|
* @author Jiahang Li
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2023/10/10 17:30
|
||||||
|
*/
|
||||||
|
@Module("asset:host-terminal")
|
||||||
|
public class HostTerminalOperatorType extends InitializingOperatorTypes {
|
||||||
|
|
||||||
|
public static final String ACCESS = "host-terminal:access";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public OperatorType[] types() {
|
||||||
|
return new OperatorType[]{
|
||||||
|
new OperatorType(L, ACCESS, "连接主机终端 <sb>${hostName}</sb>"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.orion.ops.module.asset.entity.dto;
|
package com.orion.ops.module.asset.entity.dto;
|
||||||
|
|
||||||
import com.orion.ops.module.asset.entity.domain.HostKeyDO;
|
import com.orion.ops.framework.desensitize.core.annotation.Desensitize;
|
||||||
import com.orion.ops.module.asset.enums.HostSshAuthTypeEnum;
|
import com.orion.ops.framework.desensitize.core.annotation.DesensitizeObject;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
@@ -19,12 +19,22 @@ import lombok.NoArgsConstructor;
|
|||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@DesensitizeObject
|
||||||
@Schema(name = "HostSshConnectDTO", description = "主机连接参数")
|
@Schema(name = "HostSshConnectDTO", description = "主机连接参数")
|
||||||
public class HostSshConnectDTO {
|
public class HostSshConnectDTO {
|
||||||
|
|
||||||
|
@Schema(description = "token")
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "userId")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
@Schema(description = "hostId")
|
@Schema(description = "hostId")
|
||||||
private Long hostId;
|
private Long hostId;
|
||||||
|
|
||||||
|
@Schema(description = "hostName")
|
||||||
|
private String hostName;
|
||||||
|
|
||||||
@Schema(description = "主机地址")
|
@Schema(description = "主机地址")
|
||||||
private String address;
|
private String address;
|
||||||
|
|
||||||
@@ -34,19 +44,26 @@ public class HostSshConnectDTO {
|
|||||||
@Schema(description = "超时时间")
|
@Schema(description = "超时时间")
|
||||||
private Integer timeout;
|
private Integer timeout;
|
||||||
|
|
||||||
@Schema(description = "认证方式")
|
|
||||||
private HostSshAuthTypeEnum authType;
|
|
||||||
|
|
||||||
@Schema(description = "用户名")
|
@Schema(description = "用户名")
|
||||||
private String username;
|
private String username;
|
||||||
|
|
||||||
|
@Desensitize(toEmpty = true)
|
||||||
@Schema(description = "密码")
|
@Schema(description = "密码")
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
@Schema(description = "主机秘钥")
|
@Schema(description = "秘钥id")
|
||||||
private HostKeyDO key;
|
private Long keyId;
|
||||||
|
|
||||||
// @Schema(description = "")
|
@Desensitize(toEmpty = true)
|
||||||
// private ;
|
@Schema(description = "公钥文本")
|
||||||
|
private String publicKey;
|
||||||
|
|
||||||
|
@Desensitize(toEmpty = true)
|
||||||
|
@Schema(description = "私钥文本")
|
||||||
|
private String privateKey;
|
||||||
|
|
||||||
|
@Desensitize(toEmpty = true)
|
||||||
|
@Schema(description = "私钥密码")
|
||||||
|
private String privateKeyPassword;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.orion.ops.module.asset.entity.request.host;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主机终端连接 请求对象
|
||||||
|
*
|
||||||
|
* @author Jiahang Li
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2023-9-20 11:55
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Schema(name = "HostTerminalConnectRequest", description = "主机终端连接 请求对象")
|
||||||
|
public class HostTerminalConnectRequest implements Serializable {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Schema(description = "hostId")
|
||||||
|
private Long hostId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package com.orion.ops.module.asset.service;
|
|
||||||
|
|
||||||
import com.orion.net.host.SessionStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 主机连接服务
|
|
||||||
*
|
|
||||||
* @author Jiahang Li
|
|
||||||
* @version 1.0.0
|
|
||||||
* @since 2023/12/26 14:22
|
|
||||||
*/
|
|
||||||
public interface HostConnectService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开主机会话
|
|
||||||
* 鉴权并且读取用户配置
|
|
||||||
*
|
|
||||||
* @param hostId hostId
|
|
||||||
* @param userId userId
|
|
||||||
* @return session
|
|
||||||
*/
|
|
||||||
SessionStore openSessionStore(Long hostId, Long userId);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开主机会话
|
|
||||||
* 使用默认配置 不鉴权
|
|
||||||
*
|
|
||||||
* @param hostId hostId
|
|
||||||
* @return session
|
|
||||||
*/
|
|
||||||
SessionStore openSessionStore(Long hostId);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.orion.ops.module.asset.service;
|
||||||
|
|
||||||
|
import com.orion.net.host.SessionStore;
|
||||||
|
import com.orion.ops.module.asset.entity.dto.HostSshConnectDTO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主机终端服务
|
||||||
|
*
|
||||||
|
* @author Jiahang Li
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2023/12/26 14:22
|
||||||
|
*/
|
||||||
|
public interface HostTerminalService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主机终端连接 token
|
||||||
|
*
|
||||||
|
* @param hostId hostId
|
||||||
|
* @param userId userId
|
||||||
|
* @return session
|
||||||
|
*/
|
||||||
|
String getHostAccessToken(Long hostId, Long userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 token 获取主机终端连接信息
|
||||||
|
*
|
||||||
|
* @param token token
|
||||||
|
* @return config
|
||||||
|
*/
|
||||||
|
HostSshConnectDTO getConnectInfoByToken(String token);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用默认配置打开主机会话
|
||||||
|
*
|
||||||
|
* @param hostId hostId
|
||||||
|
* @return session
|
||||||
|
*/
|
||||||
|
SessionStore openSessionStore(Long hostId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开主机会话
|
||||||
|
*
|
||||||
|
* @param conn conn
|
||||||
|
* @return session
|
||||||
|
*/
|
||||||
|
SessionStore openSessionStore(HostSshConnectDTO conn);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
package com.orion.ops.module.asset.service.impl;
|
package com.orion.ops.module.asset.service.impl;
|
||||||
|
|
||||||
import com.orion.lang.exception.AuthenticationException;
|
import com.orion.lang.exception.AuthenticationException;
|
||||||
|
import com.orion.lang.id.UUIds;
|
||||||
import com.orion.lang.utils.Exceptions;
|
import com.orion.lang.utils.Exceptions;
|
||||||
import com.orion.lang.utils.Strings;
|
import com.orion.lang.utils.Strings;
|
||||||
import com.orion.net.host.SessionHolder;
|
import com.orion.net.host.SessionHolder;
|
||||||
import com.orion.net.host.SessionStore;
|
import com.orion.net.host.SessionStore;
|
||||||
|
import com.orion.ops.framework.biz.operator.log.core.uitls.OperatorLogs;
|
||||||
import com.orion.ops.framework.common.constant.Const;
|
import com.orion.ops.framework.common.constant.Const;
|
||||||
import com.orion.ops.framework.common.constant.ErrorMessage;
|
import com.orion.ops.framework.common.constant.ErrorMessage;
|
||||||
import com.orion.ops.framework.common.utils.CryptoUtils;
|
import com.orion.ops.framework.common.utils.CryptoUtils;
|
||||||
import com.orion.ops.framework.common.utils.Valid;
|
import com.orion.ops.framework.common.utils.Valid;
|
||||||
|
import com.orion.ops.framework.redis.core.utils.RedisStrings;
|
||||||
import com.orion.ops.module.asset.dao.HostDAO;
|
import com.orion.ops.module.asset.dao.HostDAO;
|
||||||
import com.orion.ops.module.asset.dao.HostIdentityDAO;
|
import com.orion.ops.module.asset.dao.HostIdentityDAO;
|
||||||
import com.orion.ops.module.asset.dao.HostKeyDAO;
|
import com.orion.ops.module.asset.dao.HostKeyDAO;
|
||||||
|
import com.orion.ops.module.asset.define.cache.HostTerminalCacheKeyDefine;
|
||||||
import com.orion.ops.module.asset.entity.domain.HostDO;
|
import com.orion.ops.module.asset.entity.domain.HostDO;
|
||||||
import com.orion.ops.module.asset.entity.domain.HostIdentityDO;
|
import com.orion.ops.module.asset.entity.domain.HostIdentityDO;
|
||||||
import com.orion.ops.module.asset.entity.domain.HostKeyDO;
|
import com.orion.ops.module.asset.entity.domain.HostKeyDO;
|
||||||
@@ -23,10 +27,12 @@ import com.orion.ops.module.asset.enums.HostSshAuthTypeEnum;
|
|||||||
import com.orion.ops.module.asset.handler.host.config.model.HostSshConfigModel;
|
import com.orion.ops.module.asset.handler.host.config.model.HostSshConfigModel;
|
||||||
import com.orion.ops.module.asset.handler.host.extra.model.HostSshExtraModel;
|
import com.orion.ops.module.asset.handler.host.extra.model.HostSshExtraModel;
|
||||||
import com.orion.ops.module.asset.service.HostConfigService;
|
import com.orion.ops.module.asset.service.HostConfigService;
|
||||||
import com.orion.ops.module.asset.service.HostConnectService;
|
import com.orion.ops.module.asset.service.HostConnectLogService;
|
||||||
import com.orion.ops.module.asset.service.HostExtraService;
|
import com.orion.ops.module.asset.service.HostExtraService;
|
||||||
|
import com.orion.ops.module.asset.service.HostTerminalService;
|
||||||
import com.orion.ops.module.infra.api.DataPermissionApi;
|
import com.orion.ops.module.infra.api.DataPermissionApi;
|
||||||
import com.orion.ops.module.infra.api.SystemUserApi;
|
import com.orion.ops.module.infra.api.SystemUserApi;
|
||||||
|
import com.orion.ops.module.infra.entity.dto.user.SystemUserDTO;
|
||||||
import com.orion.ops.module.infra.enums.DataPermissionTypeEnum;
|
import com.orion.ops.module.infra.enums.DataPermissionTypeEnum;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -44,7 +50,7 @@ import java.util.Optional;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class HostConnectServiceImpl implements HostConnectService {
|
public class HostTerminalServiceImpl implements HostTerminalService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private HostConfigService hostConfigService;
|
private HostConfigService hostConfigService;
|
||||||
@@ -55,6 +61,9 @@ public class HostConnectServiceImpl implements HostConnectService {
|
|||||||
@Resource
|
@Resource
|
||||||
private AssetAuthorizedDataServiceImpl assetAuthorizedDataService;
|
private AssetAuthorizedDataServiceImpl assetAuthorizedDataService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private HostConnectLogService hostConnectLogService;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private HostDAO hostDAO;
|
private HostDAO hostDAO;
|
||||||
|
|
||||||
@@ -71,11 +80,14 @@ public class HostConnectServiceImpl implements HostConnectService {
|
|||||||
private SystemUserApi systemUserApi;
|
private SystemUserApi systemUserApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SessionStore openSessionStore(Long hostId, Long userId) {
|
public String getHostAccessToken(Long hostId, Long userId) {
|
||||||
log.info("HostConnectService.openSessionStore-withUser hostId: {}, userId: {}", hostId, userId);
|
log.info("HostConnectService.getHostAccessToken hostId: {}, userId: {}", hostId, userId);
|
||||||
// 查询主机
|
// 查询主机
|
||||||
HostDO host = hostDAO.selectById(hostId);
|
HostDO host = hostDAO.selectById(hostId);
|
||||||
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
|
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
|
||||||
|
// 查询用户
|
||||||
|
SystemUserDTO user = systemUserApi.getUserById(userId);
|
||||||
|
Valid.notNull(user, ErrorMessage.USER_ABSENT);
|
||||||
// 查询主机配置
|
// 查询主机配置
|
||||||
HostSshConfigModel config = hostConfigService.getHostConfig(hostId, HostConfigTypeEnum.SSH);
|
HostSshConfigModel config = hostConfigService.getHostConfig(hostId, HostConfigTypeEnum.SSH);
|
||||||
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
|
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
|
||||||
@@ -104,8 +116,24 @@ public class HostConnectServiceImpl implements HostConnectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 连接
|
String token = UUIds.random32();
|
||||||
return this.openSessionStoreWithHost(host, config, extra);
|
// 获取连接配置
|
||||||
|
HostSshConnectDTO connect = this.getHostConnectInfo(host, config, extra);
|
||||||
|
connect.setUserId(userId);
|
||||||
|
connect.setUsername(user.getUsername());
|
||||||
|
connect.setToken(token);
|
||||||
|
// 设置缓存
|
||||||
|
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_CONNECT.format(token);
|
||||||
|
RedisStrings.setJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_CONNECT, connect);
|
||||||
|
// 设置日志参数
|
||||||
|
OperatorLogs.add(connect);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HostSshConnectDTO getConnectInfoByToken(String token) {
|
||||||
|
String key = HostTerminalCacheKeyDefine.HOST_TERMINAL_CONNECT.format(token);
|
||||||
|
return RedisStrings.getJson(key, HostTerminalCacheKeyDefine.HOST_TERMINAL_CONNECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -115,102 +143,36 @@ public class HostConnectServiceImpl implements HostConnectService {
|
|||||||
HostDO host = hostDAO.selectById(hostId);
|
HostDO host = hostDAO.selectById(hostId);
|
||||||
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
|
Valid.notNull(host, ErrorMessage.HOST_ABSENT);
|
||||||
// 查询主机配置
|
// 查询主机配置
|
||||||
HostSshConfigModel config = hostConfigService.getHostConfig(hostId, HostConfigTypeEnum.SSH);
|
HostSshConfigModel model = hostConfigService.getHostConfig(hostId, HostConfigTypeEnum.SSH);
|
||||||
Valid.notNull(config, ErrorMessage.CONFIG_ABSENT);
|
Valid.notNull(model, ErrorMessage.CONFIG_ABSENT);
|
||||||
// 连接
|
// 获取配置
|
||||||
return this.openSessionStoreWithHost(host, config, null);
|
HostSshConnectDTO connect = this.getHostConnectInfo(host, model, null);
|
||||||
|
// 打开连接
|
||||||
|
return this.openSessionStore(connect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Override
|
||||||
* 打开主机会话
|
public SessionStore openSessionStore(HostSshConnectDTO conn) {
|
||||||
*
|
|
||||||
* @param host host
|
|
||||||
* @param config config
|
|
||||||
* @param extra extra
|
|
||||||
* @return session
|
|
||||||
*/
|
|
||||||
private SessionStore openSessionStoreWithHost(HostDO host,
|
|
||||||
HostSshConfigModel config,
|
|
||||||
HostSshExtraModel extra) {
|
|
||||||
// 获取认证方式
|
|
||||||
HostSshAuthTypeEnum authType = HostSshAuthTypeEnum.of(config.getAuthType());
|
|
||||||
HostExtraSshAuthTypeEnum extraAuthType = Optional.ofNullable(extra)
|
|
||||||
.map(HostSshExtraModel::getAuthType)
|
|
||||||
.map(HostExtraSshAuthTypeEnum::of)
|
|
||||||
.orElse(HostExtraSshAuthTypeEnum.DEFAULT);
|
|
||||||
if (HostExtraSshAuthTypeEnum.CUSTOM_KEY.equals(extraAuthType)) {
|
|
||||||
// 自定义秘钥
|
|
||||||
authType = HostSshAuthTypeEnum.KEY;
|
|
||||||
config.setKeyId(extra.getKeyId());
|
|
||||||
if (extra.getUsername() != null) {
|
|
||||||
config.setUsername(extra.getUsername());
|
|
||||||
}
|
|
||||||
} else if (HostExtraSshAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) {
|
|
||||||
// 自定义身份
|
|
||||||
authType = HostSshAuthTypeEnum.IDENTITY;
|
|
||||||
config.setIdentityId(extra.getIdentityId());
|
|
||||||
}
|
|
||||||
// 填充认证信息
|
|
||||||
HostSshConnectDTO conn = new HostSshConnectDTO();
|
|
||||||
conn.setHostId(host.getId());
|
|
||||||
conn.setAddress(host.getAddress());
|
|
||||||
conn.setPort(config.getPort());
|
|
||||||
conn.setTimeout(config.getConnectTimeout());
|
|
||||||
conn.setAuthType(authType);
|
|
||||||
conn.setUsername(config.getUsername());
|
|
||||||
// 填充身份信息
|
|
||||||
if (HostSshAuthTypeEnum.PASSWORD.equals(authType)) {
|
|
||||||
conn.setPassword(config.getPassword());
|
|
||||||
} else if (HostSshAuthTypeEnum.KEY.equals(authType)) {
|
|
||||||
// 秘钥认证
|
|
||||||
HostKeyDO key = hostKeyDAO.selectById(config.getKeyId());
|
|
||||||
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
|
||||||
conn.setKey(key);
|
|
||||||
} else if (HostSshAuthTypeEnum.IDENTITY.equals(authType)) {
|
|
||||||
// 身份认证
|
|
||||||
HostIdentityDO identity = hostIdentityDAO.selectById(config.getIdentityId());
|
|
||||||
Valid.notNull(identity, ErrorMessage.IDENTITY_ABSENT);
|
|
||||||
if (identity.getKeyId() != null) {
|
|
||||||
// 秘钥认证
|
|
||||||
HostKeyDO key = hostKeyDAO.selectById(config.getKeyId());
|
|
||||||
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
|
||||||
conn.setKey(key);
|
|
||||||
}
|
|
||||||
conn.setUsername(identity.getUsername());
|
|
||||||
conn.setPassword(identity.getPassword());
|
|
||||||
}
|
|
||||||
// 连接
|
|
||||||
return this.openSessionStoreWithConfig(conn);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 打开主机会话
|
|
||||||
*
|
|
||||||
* @param conn conn
|
|
||||||
* @return session
|
|
||||||
*/
|
|
||||||
private SessionStore openSessionStoreWithConfig(HostSshConnectDTO conn) {
|
|
||||||
Long hostId = conn.getHostId();
|
Long hostId = conn.getHostId();
|
||||||
String address = conn.getAddress();
|
String address = conn.getAddress();
|
||||||
String username = conn.getUsername();
|
String username = conn.getUsername();
|
||||||
log.info("HostConnectService-openSessionStore-start hostId: {}, address: {}, username: {}", hostId, address, username);
|
log.info("HostConnectService-openSessionStore-start hostId: {}, address: {}, username: {}", hostId, address, username);
|
||||||
try {
|
try {
|
||||||
SessionHolder sessionHolder = new SessionHolder();
|
SessionHolder sessionHolder = new SessionHolder();
|
||||||
HostKeyDO key = conn.getKey();
|
final boolean useKey = conn.getKeyId() != null;
|
||||||
final boolean useKey = key != null;
|
|
||||||
// 使用秘钥认证
|
// 使用秘钥认证
|
||||||
if (useKey) {
|
if (useKey) {
|
||||||
// 加载秘钥
|
// 加载秘钥
|
||||||
String publicKey = Optional.ofNullable(key.getPublicKey())
|
String publicKey = Optional.ofNullable(conn.getPublicKey())
|
||||||
.map(CryptoUtils::decryptAsString)
|
.map(CryptoUtils::decryptAsString)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
String privateKey = Optional.ofNullable(key.getPrivateKey())
|
String privateKey = Optional.ofNullable(conn.getPrivateKey())
|
||||||
.map(CryptoUtils::decryptAsString)
|
.map(CryptoUtils::decryptAsString)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
String password = Optional.ofNullable(key.getPassword())
|
String password = Optional.ofNullable(conn.getPrivateKeyPassword())
|
||||||
.map(CryptoUtils::decryptAsString)
|
.map(CryptoUtils::decryptAsString)
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
sessionHolder.addIdentityValue(String.valueOf(key.getId()),
|
sessionHolder.addIdentityValue(String.valueOf(conn.getKeyId()),
|
||||||
privateKey,
|
privateKey,
|
||||||
publicKey,
|
publicKey,
|
||||||
password);
|
password);
|
||||||
@@ -240,4 +202,68 @@ public class HostConnectServiceImpl implements HostConnectService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取主机会话连接配置
|
||||||
|
*
|
||||||
|
* @param host host
|
||||||
|
* @param config config
|
||||||
|
* @param extra extra
|
||||||
|
* @return session
|
||||||
|
*/
|
||||||
|
private HostSshConnectDTO getHostConnectInfo(HostDO host,
|
||||||
|
HostSshConfigModel config,
|
||||||
|
HostSshExtraModel extra) {
|
||||||
|
// 获取认证方式
|
||||||
|
HostSshAuthTypeEnum authType = HostSshAuthTypeEnum.of(config.getAuthType());
|
||||||
|
HostExtraSshAuthTypeEnum extraAuthType = Optional.ofNullable(extra)
|
||||||
|
.map(HostSshExtraModel::getAuthType)
|
||||||
|
.map(HostExtraSshAuthTypeEnum::of)
|
||||||
|
.orElse(HostExtraSshAuthTypeEnum.DEFAULT);
|
||||||
|
if (HostExtraSshAuthTypeEnum.CUSTOM_KEY.equals(extraAuthType)) {
|
||||||
|
// 自定义秘钥
|
||||||
|
authType = HostSshAuthTypeEnum.KEY;
|
||||||
|
config.setKeyId(extra.getKeyId());
|
||||||
|
if (extra.getUsername() != null) {
|
||||||
|
config.setUsername(extra.getUsername());
|
||||||
|
}
|
||||||
|
} else if (HostExtraSshAuthTypeEnum.CUSTOM_IDENTITY.equals(extraAuthType)) {
|
||||||
|
// 自定义身份
|
||||||
|
authType = HostSshAuthTypeEnum.IDENTITY;
|
||||||
|
config.setIdentityId(extra.getIdentityId());
|
||||||
|
}
|
||||||
|
Long keyId = null;
|
||||||
|
// 填充认证信息
|
||||||
|
HostSshConnectDTO conn = new HostSshConnectDTO();
|
||||||
|
conn.setHostId(host.getId());
|
||||||
|
conn.setHostName(host.getName());
|
||||||
|
conn.setAddress(host.getAddress());
|
||||||
|
conn.setPort(config.getPort());
|
||||||
|
conn.setTimeout(config.getConnectTimeout());
|
||||||
|
conn.setUsername(config.getUsername());
|
||||||
|
// 填充身份信息
|
||||||
|
if (HostSshAuthTypeEnum.PASSWORD.equals(authType)) {
|
||||||
|
conn.setPassword(config.getPassword());
|
||||||
|
} else if (HostSshAuthTypeEnum.KEY.equals(authType)) {
|
||||||
|
// 秘钥认证
|
||||||
|
keyId = config.getKeyId();
|
||||||
|
} else if (HostSshAuthTypeEnum.IDENTITY.equals(authType)) {
|
||||||
|
// 身份认证
|
||||||
|
HostIdentityDO identity = hostIdentityDAO.selectById(config.getIdentityId());
|
||||||
|
Valid.notNull(identity, ErrorMessage.IDENTITY_ABSENT);
|
||||||
|
keyId = identity.getKeyId();
|
||||||
|
conn.setUsername(identity.getUsername());
|
||||||
|
conn.setPassword(identity.getPassword());
|
||||||
|
}
|
||||||
|
// 设置秘钥信息
|
||||||
|
if (keyId != null) {
|
||||||
|
HostKeyDO key = hostKeyDAO.selectById(keyId);
|
||||||
|
Valid.notNull(key, ErrorMessage.KEY_ABSENT);
|
||||||
|
conn.setPublicKey(key.getPublicKey());
|
||||||
|
conn.setPrivateKey(key.getPrivateKey());
|
||||||
|
conn.setPrivateKeyPassword(key.getPassword());
|
||||||
|
}
|
||||||
|
// 连接
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ INSERT INTO `system_menu` VALUES (106, 105, '查询字典配置值', 'infra:dict
|
|||||||
INSERT INTO `system_menu` VALUES (107, 105, '创建字典配置值', 'infra:dict-value:create', 3, 220, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
INSERT INTO `system_menu` VALUES (107, 105, '创建字典配置值', 'infra:dict-value:create', 3, 220, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (108, 105, '修改字典配置值', 'infra:dict-value:update', 3, 230, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
INSERT INTO `system_menu` VALUES (108, 105, '修改字典配置值', 'infra:dict-value:update', 3, 230, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (109, 105, '删除字典配置值', 'infra:dict-value:delete', 3, 240, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
INSERT INTO `system_menu` VALUES (109, 105, '删除字典配置值', 'infra:dict-value:delete', 3, 240, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-17 11:38:18', '2023-10-27 01:16:10', NULL, '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (120, 97, '查询字典配置项', 'infra:dict-key:create', 3, 100, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-20 11:27:12', '2023-10-27 01:16:10', '1', '1', 0);
|
INSERT INTO `system_menu` VALUES (120, 97, '查询字典配置项', 'infra:dict-key:query', 3, 100, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-20 11:27:12', '2023-10-27 01:16:10', '1', '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (121, 97, '刷新缓存', 'infra:dict-key:management:refresh-cache', 3, 140, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-27 15:50:04', '2023-12-27 12:40:12', '1', '1', 0);
|
INSERT INTO `system_menu` VALUES (121, 97, '刷新缓存', 'infra:dict-key:management:refresh-cache', 3, 140, 1, 1, 1, 0, NULL, NULL, NULL, '2023-10-27 15:50:04', '2023-12-27 12:40:12', '1', '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (122, 5, '操作日志', NULL, 2, 30, 1, 1, 1, 0, 'IconCalendarClock', NULL, 'userOperatorLog', '2023-11-01 14:09:36', '2023-11-01 14:09:36', '1', '1', 0);
|
INSERT INTO `system_menu` VALUES (122, 5, '操作日志', NULL, 2, 30, 1, 1, 1, 0, 'IconCalendarClock', NULL, 'userOperatorLog', '2023-11-01 14:09:36', '2023-11-01 14:09:36', '1', '1', 0);
|
||||||
INSERT INTO `system_menu` VALUES (123, 122, '查询操作日志', 'infra:operator-log:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2023-11-02 11:22:54', '2023-11-02 11:22:54', '1', '1', 0);
|
INSERT INTO `system_menu` VALUES (123, 122, '查询操作日志', 'infra:operator-log:query', 3, 10, 1, 1, 1, 0, NULL, NULL, NULL, '2023-11-02 11:22:54', '2023-11-02 11:22:54', '1', '1', 0);
|
||||||
|
|||||||
Reference in New Issue
Block a user