Compare commits
No commits in common. "main" and "dev2" have entirely different histories.
34
.gitignore
vendored
34
.gitignore
vendored
@ -1,4 +1,32 @@
|
||||
.idea
|
||||
log
|
||||
### IntelliJ IDEA ###
|
||||
out/
|
||||
.idea/
|
||||
log/
|
||||
config/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
config
|
||||
|
@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.0</version>
|
||||
<version>3.1.6</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
@ -21,7 +21,15 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.34</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
@ -32,19 +40,10 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.37</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
<artifactId>oshi-core</artifactId>
|
||||
<version>6.4.0</version>
|
||||
<version>6.4.8</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
@ -54,6 +53,9 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<image>
|
||||
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
|
||||
</image>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
|
@ -5,11 +5,17 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import oshi.SystemInfo;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class QuartzConfiguration {
|
||||
|
||||
@Bean
|
||||
public SystemInfo info(){
|
||||
return new SystemInfo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JobDetail jobDetailFactoryBean() {
|
||||
return JobBuilder.newJob(MonitorJobBean.class)
|
||||
@ -23,7 +29,7 @@ public class QuartzConfiguration {
|
||||
CronScheduleBuilder cron= CronScheduleBuilder.cronSchedule("*/10 * * * * ?");
|
||||
return TriggerBuilder.newTrigger()
|
||||
.forJob(detail)
|
||||
.withIdentity("monitor-trigger")
|
||||
.withIdentity("monitor-timer")
|
||||
.withSchedule(cron)
|
||||
.build();
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package com.example.config;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.entity.data.BaseDetail;
|
||||
import com.example.utils.MonitorUtils;
|
||||
import com.example.utils.NetUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
@ -16,7 +17,6 @@ import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
@Slf4j
|
||||
@ -39,56 +39,47 @@ public class ServerConfiguration implements ApplicationRunner {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
log.info("正在向服务端更新基本系统信息...");
|
||||
net.updateBaseDetails(monitor.monitorBaseDetail());
|
||||
BaseDetail detail = monitor.monitorBaseData();
|
||||
net.updateBaseDetails(detail);
|
||||
}
|
||||
|
||||
private ConnectionConfig registerToServer() {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
String token, address, ifName;
|
||||
String token, address;
|
||||
do {
|
||||
log.info("请输入需要注册的服务端访问地址,地址类似于 'http://192.168.0.22:8080' 这种写法:");
|
||||
log.info("请输入需要注册的服务端访问地址,地址类似于'http://localhost:8080'这种写法:");
|
||||
address = scanner.nextLine();
|
||||
log.info("请输入服务端生成的用于注册客户端的Token秘钥:");
|
||||
log.info("请输入由服务端生成,用于访问服务端的Token秘钥:");
|
||||
token = scanner.nextLine();
|
||||
List<String> ifs = monitor.listNetworkInterfaceName();
|
||||
if(ifs.size() > 1) {
|
||||
log.info("检测到您的主机有多个网卡设备: {}", ifs);
|
||||
do {
|
||||
log.info("请选择需要监控的设备名称:");
|
||||
ifName = scanner.nextLine();
|
||||
} while (!ifs.contains(ifName));
|
||||
} else {
|
||||
ifName = ifs.get(0);
|
||||
}
|
||||
} while (!net.registerToServer(address, token));
|
||||
ConnectionConfig config = new ConnectionConfig(address, token, ifName);
|
||||
this.saveConfigurationToFile(config);
|
||||
return config;
|
||||
ConnectionConfig connectionConfig = new ConnectionConfig(address, token);
|
||||
this.saveConfigurationToFile(connectionConfig);
|
||||
return connectionConfig;
|
||||
}
|
||||
|
||||
private void saveConfigurationToFile(ConnectionConfig config){
|
||||
File dir = new File("config");
|
||||
if(!dir.exists() && dir.mkdir())
|
||||
log.info("创建用于保存服务端连接信息的目录已完成");
|
||||
log.info("正在创建配置目录用于保存服务端连接信息...");
|
||||
File file = new File("config/server.json");
|
||||
try(FileWriter writer = new FileWriter(file)) {
|
||||
writer.write(JSONObject.from(config).toJSONString());
|
||||
} catch (IOException e) {
|
||||
log.error("保存配置文件时出现问题", e);
|
||||
}
|
||||
log.info("服务端连接信息已保存成功!");
|
||||
log.info("服务端连接信息已保存成功!");
|
||||
}
|
||||
|
||||
private ConnectionConfig readConfigurationFromFile(){
|
||||
File configurationFile = new File("config/server.json");
|
||||
if(configurationFile.exists()) {
|
||||
try (FileInputStream stream = new FileInputStream(configurationFile)){
|
||||
File configFile = new File("config/server.json");
|
||||
if(configFile.exists()) {
|
||||
try (FileInputStream stream = new FileInputStream(configFile)){
|
||||
String raw = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
return JSONObject.parseObject(raw).to(ConnectionConfig.class);
|
||||
} catch (IOException e) {
|
||||
log.error("读取配置文件时出错", e);
|
||||
log.error("读取配置文件时出现问题", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -8,5 +8,4 @@ import lombok.Data;
|
||||
public class ConnectionConfig {
|
||||
String address;
|
||||
String token;
|
||||
String networkInterface;
|
||||
}
|
||||
|
@ -1,22 +1,31 @@
|
||||
package com.example.entity;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
public record Response(int id, int code, Object data, String message) {
|
||||
@Data
|
||||
public class Response {
|
||||
int id;
|
||||
int code;
|
||||
Object data;
|
||||
String message;
|
||||
|
||||
public boolean success(){
|
||||
return code == 200;
|
||||
}
|
||||
|
||||
public JSONObject asJson() {
|
||||
public JSONObject ofJson() {
|
||||
return JSONObject.from(data);
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
public String ofString() {
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public static Response errorResponse(Exception e) {
|
||||
return new Response(0, 500, null, e.getMessage());
|
||||
Response response = new Response();
|
||||
response.setCode(500);
|
||||
response.setMessage(e.getMessage());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.example.entity;
|
||||
package com.example.entity.data;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
@ -1,4 +1,4 @@
|
||||
package com.example.entity;
|
||||
package com.example.entity.data;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
@ -1,6 +1,6 @@
|
||||
package com.example.task;
|
||||
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import com.example.entity.data.RuntimeDetail;
|
||||
import com.example.utils.MonitorUtils;
|
||||
import com.example.utils.NetUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
@ -20,7 +20,7 @@ public class MonitorJobBean extends QuartzJobBean {
|
||||
|
||||
@Override
|
||||
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
|
||||
RuntimeDetail runtimeDetail = monitor.monitorRuntimeDetail();
|
||||
RuntimeDetail runtimeDetail = monitor.monitorRuntimeData();
|
||||
net.updateRuntimeDetails(runtimeDetail);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,8 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.example.entity.BaseDetail;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import jakarta.annotation.Resource;
|
||||
import com.example.entity.data.BaseDetail;
|
||||
import com.example.entity.data.RuntimeDetail;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.CentralProcessor;
|
||||
@ -16,43 +13,26 @@ import oshi.software.os.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MonitorUtils {
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
ConnectionConfig config;
|
||||
|
||||
private final SystemInfo info = new SystemInfo();
|
||||
private final Properties properties = System.getProperties();
|
||||
|
||||
public BaseDetail monitorBaseDetail() {
|
||||
OperatingSystem os = info.getOperatingSystem();
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 /1024;
|
||||
double diskSize = Arrays.stream(File.listRoots()).mapToLong(File::getTotalSpace).sum() / 1024.0 / 1024 / 1024;
|
||||
String ip = Objects.requireNonNull(this.findNetworkInterface(hardware)).getIPv4addr()[0];
|
||||
return new BaseDetail()
|
||||
.setOsArch(properties.getProperty("os.arch"))
|
||||
.setOsName(os.getFamily())
|
||||
.setOsVersion(os.getVersionInfo().getVersion())
|
||||
.setOsBit(os.getBitness())
|
||||
.setCpuName(hardware.getProcessor().getProcessorIdentifier().getName())
|
||||
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
|
||||
.setMemory(memory)
|
||||
.setDisk(diskSize)
|
||||
.setIp(ip);
|
||||
}
|
||||
|
||||
public RuntimeDetail monitorRuntimeDetail() {
|
||||
public RuntimeDetail monitorRuntimeData() {
|
||||
double statisticTime = 0.5;
|
||||
try {
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
NetworkIF networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
|
||||
CentralProcessor processor = hardware.getProcessor();
|
||||
//开始读取各种运行时信息
|
||||
double upload = networkInterface.getBytesSent(), download = networkInterface.getBytesRecv();
|
||||
double read = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum();
|
||||
double write = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum();
|
||||
@ -76,7 +56,42 @@ public class MonitorUtils {
|
||||
.setDiskWrite(write / 1024 / 1024)
|
||||
.setTimestamp(new Date().getTime());
|
||||
}catch (Exception e) {
|
||||
log.error("读取运行时数据出现问题", e);
|
||||
log.error("读取运行时数据时出现问题", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public BaseDetail monitorBaseData() {
|
||||
OperatingSystem os = info.getOperatingSystem();
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 / 1024;
|
||||
double diskSize = Arrays.stream(File.listRoots()).mapToLong(File::getTotalSpace).sum() / 1024.0 / 1024 / 1024;
|
||||
String ip = Objects.requireNonNull(this.findNetworkInterface(hardware)).getIPv4addr()[0];
|
||||
return new BaseDetail()
|
||||
.setOsBit(os.getBitness())
|
||||
.setOsArch(properties.getProperty("os.arch"))
|
||||
.setOsVersion(os.getVersionInfo().getVersion())
|
||||
.setOsName(os.getFamily())
|
||||
.setCpuName(hardware.getProcessor().getProcessorIdentifier().getName())
|
||||
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
|
||||
.setMemory(memory)
|
||||
.setDisk(diskSize)
|
||||
.setIp(ip);
|
||||
}
|
||||
|
||||
private NetworkIF findNetworkInterface(HardwareAbstractionLayer hardware) {
|
||||
try {
|
||||
for (NetworkIF network : hardware.getNetworkIFs()) {
|
||||
String[] iPv4addr = network.getIPv4addr();
|
||||
NetworkInterface ni = network.queryNetworkInterface();
|
||||
if(!ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual()
|
||||
&& (ni.getName().startsWith("eth") || ni.getName().startsWith("en"))
|
||||
&& iPv4addr.length > 0) {
|
||||
return network;
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("读取网络接口信息时出错", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@ -102,30 +117,4 @@ public class MonitorUtils {
|
||||
long totalCpu = cUser + nice + cSys + idle + ioWait + irq + softIrq + steal;
|
||||
return (cSys + cUser) * 1.0 / totalCpu;
|
||||
}
|
||||
|
||||
public List<String> listNetworkInterfaceName() {
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
return hardware.getNetworkIFs()
|
||||
.stream()
|
||||
.map(NetworkIF::getName)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private NetworkIF findNetworkInterface(HardwareAbstractionLayer hardware) {
|
||||
try {
|
||||
String target = config.getNetworkInterface();
|
||||
List<NetworkIF> ifs = hardware.getNetworkIFs()
|
||||
.stream()
|
||||
.filter(inter -> inter.getName().equals(target))
|
||||
.toList();
|
||||
if (!ifs.isEmpty()) {
|
||||
return ifs.get(0);
|
||||
} else {
|
||||
throw new IOException("网卡信息错误,找不到网卡: " + target);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("读取网络接口信息时出错", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,10 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.BaseDetail;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.entity.Response;
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import com.example.entity.data.BaseDetail;
|
||||
import com.example.entity.data.RuntimeDetail;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
@ -18,23 +18,56 @@ import java.net.http.HttpResponse;
|
||||
@Slf4j
|
||||
@Component
|
||||
public class NetUtils {
|
||||
|
||||
private final HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
ConnectionConfig config;
|
||||
private ConnectionConfig config;
|
||||
|
||||
public boolean registerToServer(String address, String token) {
|
||||
log.info("正在像服务端注册,请稍后...");
|
||||
log.info("正在向服务端注册,请稍后...");
|
||||
Response response = this.doGet("/register", address, token);
|
||||
if(response.success()) {
|
||||
log.info("客户端注册已完成!");
|
||||
if(!response.success()) {
|
||||
log.error("注册客户端失败: {}", response.getMessage());
|
||||
} else {
|
||||
log.error("客户端注册失败: {}", response.message());
|
||||
log.info("客户端注册已完成!");
|
||||
}
|
||||
return response.success();
|
||||
}
|
||||
|
||||
public void updateBaseDetails(BaseDetail detail){
|
||||
Response response = this.doPost("/detail", detail);
|
||||
if(response.success()) {
|
||||
log.info("系统基本信息更新已完成");
|
||||
} else {
|
||||
log.error("系统基本信息更新失败: {}", response.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRuntimeDetails(RuntimeDetail detail) {
|
||||
Response response = this.doPost("/runtime", detail);
|
||||
if(!response.success()) {
|
||||
log.warn("更新运行时状态时,接收到服务端的异常响应内容: {}", response.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private Response doPost(String url, Object data) {
|
||||
try {
|
||||
String rawData = JSONObject.from(data).toJSONString();
|
||||
HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(rawData))
|
||||
.uri(new URI(config.getAddress() + "/monitor" + url))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Authorization", config.getToken())
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return JSONObject.parseObject(response.body()).to(Response.class);
|
||||
} catch (Exception e) {
|
||||
log.error("在发起服务端请求时出现错误", e);
|
||||
return Response.errorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Response doGet(String url) {
|
||||
return this.doGet(url, config.getAddress(), config.getToken());
|
||||
}
|
||||
@ -48,39 +81,7 @@ public class NetUtils {
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return JSONObject.parseObject(response.body()).to(Response.class);
|
||||
} catch (Exception e) {
|
||||
log.error("在发起服务端请求时出现问题", e);
|
||||
return Response.errorResponse(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void updateBaseDetails(BaseDetail detail) {
|
||||
Response response = this.doPost("/detail", detail);
|
||||
if(response.success()) {
|
||||
log.info("系统基本信息已更新完成");
|
||||
} else {
|
||||
log.error("系统基本信息更新失败: {}", response.message());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateRuntimeDetails(RuntimeDetail detail) {
|
||||
Response response = this.doPost("/runtime", detail);
|
||||
if(!response.success()) {
|
||||
log.warn("更新运行时状态时,接收到服务端的异常响应内容: {}", response.message());
|
||||
}
|
||||
}
|
||||
|
||||
private Response doPost(String url, Object data) {
|
||||
try {
|
||||
String rawData = JSONObject.from(data).toJSONString();
|
||||
HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(rawData))
|
||||
.uri(new URI(config.getAddress() + "/monitor" + url))
|
||||
.header("Authorization", config.getToken())
|
||||
.header("Content-Type", "application/json")
|
||||
.build();
|
||||
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
return JSONObject.parseObject(response.body()).to(Response.class);
|
||||
} catch (Exception e) {
|
||||
log.error("在发起服务端请求时出现问题", e);
|
||||
log.error("在发起服务端请求时出现错误", e);
|
||||
return Response.errorResponse(e);
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,6 @@
|
||||
<artifactId>itbaima-monitor-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>itbaima-monitor-server</name>
|
||||
<description>my-project-backend</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
@ -94,7 +93,7 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
<!-- InfluxDB客户端框架 -->
|
||||
<!-- InfluxDB数据库 -->
|
||||
<dependency>
|
||||
<groupId>com.influxdb</groupId>
|
||||
<artifactId>influxdb-client-java</artifactId>
|
||||
@ -109,6 +108,11 @@
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.1.55</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>commons-io</groupId>
|
||||
<artifactId>commons-io</artifactId>
|
||||
<version>2.12.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
|
@ -29,13 +29,13 @@ public class MonitorController {
|
||||
@GetMapping("/list")
|
||||
public RestBean<List<ClientPreviewVO>> listAllClient(@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
List<ClientPreviewVO> clients = service.listClients();
|
||||
List<Integer> clients = this.accountAccessClients(userId);
|
||||
List<ClientPreviewVO> data = service.listClients();
|
||||
if(this.isAdminAccount(userRole)) {
|
||||
return RestBean.success(clients);
|
||||
return RestBean.success(data);
|
||||
} else {
|
||||
List<Integer> ids = this.accountAccessClients(userId);
|
||||
return RestBean.success(clients.stream()
|
||||
.filter(vo -> ids.contains(vo.getId()))
|
||||
return RestBean.success(data.stream()
|
||||
.filter(vo -> clients.contains(vo.getId()))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@ public class MonitorController {
|
||||
}
|
||||
|
||||
@GetMapping("/details")
|
||||
public RestBean<ClientDetailsVO> details(int clientId,
|
||||
public RestBean<ClientDetailsVO> clientDetails(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
@ -85,7 +85,7 @@ public class MonitorController {
|
||||
}
|
||||
|
||||
@GetMapping("/runtime-history")
|
||||
public RestBean<RuntimeHistoryVO> runtimeDetailsHistory(int clientId,
|
||||
public RestBean<RuntimeDetailsVO> runtimeDetailsHistory(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
|
@ -24,7 +24,7 @@ public class UserController {
|
||||
public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
|
||||
return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ?
|
||||
RestBean.success() : RestBean.failure(401, "原密码输入错误!");
|
||||
RestBean.success() : RestBean.failure(401, "原密码输入错误");
|
||||
}
|
||||
|
||||
@PostMapping("/modify-email")
|
||||
@ -45,8 +45,7 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/sub/delete")
|
||||
public RestBean<Void> deleteSubAccount(int uid,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
|
||||
public RestBean<Void> deleteSubAccount(int uid, @RequestAttribute(Const.ATTR_USER_ID) int userId) {
|
||||
if(uid == userId)
|
||||
return RestBean.failure(401, "非法参数");
|
||||
service.deleteSubAccount(uid);
|
||||
|
@ -16,7 +16,7 @@ public class Client implements BaseData {
|
||||
Integer id;
|
||||
String name;
|
||||
String token;
|
||||
String location;
|
||||
String node;
|
||||
String location;
|
||||
Date registerTime;
|
||||
}
|
||||
|
@ -11,8 +11,6 @@ import java.time.Instant;
|
||||
public class RuntimeData {
|
||||
@Column(tag = true)
|
||||
int clientId;
|
||||
@Column(timestamp = true)
|
||||
Instant timestamp;
|
||||
@Column
|
||||
double cpuUsage;
|
||||
@Column
|
||||
@ -27,4 +25,6 @@ public class RuntimeData {
|
||||
double diskRead;
|
||||
@Column
|
||||
double diskWrite;
|
||||
@Column(timestamp = true)
|
||||
Instant timestamp;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
|
@ -7,6 +7,7 @@ import org.hibernate.validator.constraints.Length;
|
||||
@Data
|
||||
public class SshConnectionVO {
|
||||
int id;
|
||||
@NotNull
|
||||
int port;
|
||||
@NotNull
|
||||
@Length(min = 1)
|
||||
|
@ -7,8 +7,8 @@ import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class RuntimeHistoryVO {
|
||||
double disk;
|
||||
public class RuntimeDetailsVO {
|
||||
double memory;
|
||||
double disk;
|
||||
List<JSONObject> list = new LinkedList<>();
|
||||
}
|
@ -2,9 +2,7 @@ package com.example.filter;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.dto.Client;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.service.ClientService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.JwtUtils;
|
||||
@ -44,16 +42,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
String uri = request.getRequestURI();
|
||||
if(uri.startsWith("/monitor")) {
|
||||
if(!uri.endsWith("/register")) {
|
||||
Client client = service.findClientByToken(authorization);
|
||||
Client client = service.findClient(authorization);
|
||||
if(client == null) {
|
||||
response.setStatus(401);
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().write(RestBean.failure(401, "未注册").asJsonString());
|
||||
response.getWriter().write(RestBean.failure(401, "未授权").asJsonString());
|
||||
return;
|
||||
} else {
|
||||
request.setAttribute(Const.ATTR_CLIENT, client);
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
DecodedJWT jwt = utils.resolveJwt(authorization);
|
||||
if(jwt != null) {
|
||||
@ -64,30 +63,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
||||
request.setAttribute(Const.ATTR_USER_ROLE, new ArrayList<>(user.getAuthorities()).get(0).getAuthority());
|
||||
|
||||
if(request.getRequestURI().startsWith("/terminal/") && !accessShell(
|
||||
(int) request.getAttribute(Const.ATTR_USER_ID),
|
||||
(String) request.getAttribute(Const.ATTR_USER_ROLE),
|
||||
Integer.parseInt(request.getRequestURI().substring(10)))) {
|
||||
response.setStatus(401);
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().write(RestBean.failure(401, "无权访问").asJsonString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Resource
|
||||
AccountService accountService;
|
||||
|
||||
private boolean accessShell(int userId, String userRole, int clientId) {
|
||||
if(Const.ROLE_ADMIN.equals(userRole.substring(5))) {
|
||||
return true;
|
||||
} else {
|
||||
Account account = accountService.getById(userId);
|
||||
return account.getClientList().contains(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,8 @@ public class RequestLogFilter extends OncePerRequestFilter {
|
||||
@Resource
|
||||
SnowflakeIdGenerator generator;
|
||||
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs", "/monitor/runtime",
|
||||
"/api/monitor/list", "/api/monitor/runtime-now");
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs",
|
||||
"/monitor/runtime", "/api/monitor/list", "/api/monitor/runtime");
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
|
@ -38,7 +38,7 @@ public class MailQueueListener {
|
||||
email);
|
||||
case "modify" ->
|
||||
createMessage("您的邮件修改验证邮件",
|
||||
"您好,您正在绑定新的电子邮件地址,验证码: "+code+",有效时间3分钟,如非本人操作,请无视",
|
||||
"您好,您正在绑定新的电子邮件地址,验证码: "+code+",有效时间3分钟,如非本人操作,请无视。",
|
||||
email);
|
||||
default -> null;
|
||||
};
|
||||
|
@ -9,8 +9,8 @@ import java.util.List;
|
||||
|
||||
public interface ClientService extends IService<Client> {
|
||||
String registerToken();
|
||||
Client findClientById(int id);
|
||||
Client findClientByToken(String token);
|
||||
Client findClient(String token);
|
||||
Client findClient(Integer id);
|
||||
boolean verifyAndRegister(String token);
|
||||
void updateClientDetail(ClientDetailVO vo, Client client);
|
||||
void updateRuntimeDetail(RuntimeDetailVO vo, Client client);
|
||||
@ -19,7 +19,7 @@ public interface ClientService extends IService<Client> {
|
||||
void renameClient(RenameClientVO vo);
|
||||
void renameNode(RenameNodeVO vo);
|
||||
ClientDetailsVO clientDetails(int clientId);
|
||||
RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId);
|
||||
RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId);
|
||||
RuntimeDetailVO clientRuntimeDetailsNow(int clientId);
|
||||
void deleteClient(int clientId);
|
||||
void saveClientSshConnection(SshConnectionVO vo);
|
||||
|
@ -13,6 +13,7 @@ import com.example.mapper.AccountMapper;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.FlowUtils;
|
||||
import com.example.utils.JwtUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -51,6 +52,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
@Resource
|
||||
FlowUtils flow;
|
||||
|
||||
@Resource
|
||||
JwtUtils jwt;
|
||||
|
||||
/**
|
||||
* 从数据库中通过用户名或邮箱查找用户详细信息
|
||||
* @param username 用户名
|
||||
@ -135,29 +139,25 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
|
||||
@Override
|
||||
public void createSubAccount(CreateSubAccountVO vo) {
|
||||
Account account = this.findAccountByNameOrEmail(vo.getEmail());
|
||||
if(account != null)
|
||||
throw new IllegalArgumentException("该电子邮件已被注册");
|
||||
account = this.findAccountByNameOrEmail(vo.getUsername());
|
||||
if(account != null)
|
||||
throw new IllegalArgumentException("该用户名已被注册");
|
||||
account = new Account(null, vo.getUsername(), passwordEncoder.encode(vo.getPassword()),
|
||||
vo.getEmail(), Const.ROLE_NORMAL, new Date(), JSONArray.copyOf(vo.getClients()).toJSONString());
|
||||
Account account = new Account(null, vo.getUsername(),
|
||||
passwordEncoder.encode(vo.getPassword()),
|
||||
vo.getEmail(), Const.ROLE_NORMAL, new Date(),
|
||||
JSONArray.copyOf(vo.getClients()).toJSONString());
|
||||
this.save(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSubAccount(int uid) {
|
||||
this.removeById(uid);
|
||||
|
||||
jwt.deleteUser(uid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SubAccountVO> listSubAccount() {
|
||||
return this.list(Wrappers.<Account>query().eq("role", Const.ROLE_NORMAL))
|
||||
return this.list(Wrappers.<Account>query().eq("role", "user"))
|
||||
.stream().map(account -> {
|
||||
SubAccountVO vo = account.asViewObject(SubAccountVO.class);
|
||||
vo.setClientList(JSONArray.parse(account.getClients()));
|
||||
vo.setClientList(JSONArray.copyOf(account.getClientList()));
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
@ -169,7 +169,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if(!code.equals(vo.getCode())) return "验证码错误,请重新输入";
|
||||
this.deleteEmailVerifyCode(vo.getEmail());
|
||||
Account account = this.findAccountByNameOrEmail(vo.getEmail());
|
||||
if(account != null && account.getId() != id) return "该邮箱账号已经被其他账号绑定,无法完成操作";
|
||||
if(account != null && account.getId() != id) return "该邮件已被其他账号绑定,无法完成操作";
|
||||
this.update()
|
||||
.set("email", vo.getEmail())
|
||||
.eq("id", id)
|
||||
|
@ -14,62 +14,64 @@ import com.example.service.ClientService;
|
||||
import com.example.utils.InfluxDbUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
|
||||
|
||||
private String registerToken = this.generateNewToken();
|
||||
|
||||
private final Map<Integer, Client> clientIdCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, Client> clientTokenCache = new ConcurrentHashMap<>();
|
||||
private final HashMap<Integer, Client> clientIdCache = new HashMap<>();
|
||||
private final HashMap<String, Client> clientTokenCache = new HashMap<>();
|
||||
|
||||
@Resource
|
||||
ClientDetailMapper detailMapper;
|
||||
private ClientDetailMapper detailMapper;
|
||||
|
||||
@Resource
|
||||
InfluxDbUtils influx;
|
||||
private ClientSshMapper sshMapper;
|
||||
|
||||
@Resource
|
||||
ClientSshMapper sshMapper;
|
||||
private InfluxDbUtils influx;
|
||||
|
||||
@PostConstruct
|
||||
public void initClientCache() {
|
||||
clientTokenCache.clear();
|
||||
clientIdCache.clear();
|
||||
clientTokenCache.clear();
|
||||
this.list().forEach(this::addClientCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClient(Integer id) {
|
||||
return clientIdCache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClient(String token) {
|
||||
return clientTokenCache.get(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerToken() {
|
||||
return registerToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClientById(int id) {
|
||||
return clientIdCache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClientByToken(String token) {
|
||||
return clientTokenCache.get(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyAndRegister(String token) {
|
||||
public synchronized boolean verifyAndRegister(String token) {
|
||||
if(this.registerToken.equals(token)) {
|
||||
int id = this.randomClientId();
|
||||
Client client = new Client(id, "未命名主机", token, "cn", "未命名节点", new Date());
|
||||
Client client = new Client(id, "未命名主机", token, "未命名节点","cn", new Date());
|
||||
if(this.save(client)) {
|
||||
registerToken = this.generateNewToken();
|
||||
this.addClientCache(client);
|
||||
return true;
|
||||
}
|
||||
log.warn("在注册客户端时Token验证成功,但客户端数据插入失败,请检查问题原因!");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -86,12 +88,13 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Integer, RuntimeDetailVO> currentRuntime = new ConcurrentHashMap<>();
|
||||
private final Map<Integer, RuntimeDetailVO> lastRuntime = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void updateRuntimeDetail(RuntimeDetailVO vo, Client client) {
|
||||
currentRuntime.put(client.getId(), vo);
|
||||
influx.writeRuntimeData(client.getId(), vo);
|
||||
RuntimeDetailVO oldData = lastRuntime.put(client.getId(), vo);
|
||||
if(oldData != null)
|
||||
influx.writeRuntimeData(client.getId(), oldData);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -99,7 +102,7 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
|
||||
return clientIdCache.values().stream().map(client -> {
|
||||
ClientPreviewVO vo = client.asViewObject(ClientPreviewVO.class);
|
||||
BeanUtils.copyProperties(detailMapper.selectById(vo.getId()), vo);
|
||||
RuntimeDetailVO runtime = currentRuntime.get(client.getId());
|
||||
RuntimeDetailVO runtime = lastRuntime.get(client.getId());
|
||||
if(this.isOnline(runtime)) {
|
||||
BeanUtils.copyProperties(runtime, vo);
|
||||
vo.setOnline(true);
|
||||
@ -132,36 +135,36 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
|
||||
|
||||
@Override
|
||||
public ClientDetailsVO clientDetails(int clientId) {
|
||||
ClientDetailsVO vo = this.clientIdCache.get(clientId).asViewObject(ClientDetailsVO.class);
|
||||
ClientDetailsVO vo = this.getById(clientId).asViewObject(ClientDetailsVO.class);
|
||||
BeanUtils.copyProperties(detailMapper.selectById(clientId), vo);
|
||||
vo.setOnline(this.isOnline(currentRuntime.get(clientId)));
|
||||
vo.setOnline(this.isOnline(lastRuntime.get(clientId)));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId) {
|
||||
RuntimeHistoryVO vo = influx.readRuntimeData(clientId);
|
||||
ClientDetail detail = detailMapper.selectById(clientId);
|
||||
BeanUtils.copyProperties(detail, vo);
|
||||
public RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId) {
|
||||
RuntimeDetailsVO vo = influx.readRuntimeData(clientId);
|
||||
ClientDetail client = detailMapper.selectById(clientId);
|
||||
BeanUtils.copyProperties(client, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeDetailVO clientRuntimeDetailsNow(int clientId) {
|
||||
return currentRuntime.get(clientId);
|
||||
return lastRuntime.get(clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteClient(int clientId) {
|
||||
this.removeById(clientId);
|
||||
baseMapper.deleteById(clientId);
|
||||
detailMapper.deleteById(clientId);
|
||||
this.initClientCache();
|
||||
currentRuntime.remove(clientId);
|
||||
lastRuntime.remove(clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveClientSshConnection(SshConnectionVO vo) {
|
||||
Client client = clientIdCache.get(vo.getId());
|
||||
Client client = this.getById(vo.getId());
|
||||
if(client == null) return;
|
||||
ClientSsh ssh = new ClientSsh();
|
||||
BeanUtils.copyProperties(vo, ssh);
|
||||
|
@ -7,8 +7,8 @@ public final class Const {
|
||||
//JWT令牌
|
||||
public final static String JWT_BLACK_LIST = "jwt:blacklist:";
|
||||
public final static String JWT_FREQUENCY = "jwt:frequency:";
|
||||
//用户
|
||||
public final static String USER_BLACK_LIST = "user:blacklist:";
|
||||
|
||||
public final static String USER_BLACK_LIST = "user:blacklist";
|
||||
//请求频率限制
|
||||
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
|
||||
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
||||
|
@ -3,7 +3,7 @@ package com.example.utils;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.dto.RuntimeData;
|
||||
import com.example.entity.vo.request.RuntimeDetailVO;
|
||||
import com.example.entity.vo.response.RuntimeHistoryVO;
|
||||
import com.example.entity.vo.response.RuntimeDetailsVO;
|
||||
import com.influxdb.client.InfluxDBClient;
|
||||
import com.influxdb.client.InfluxDBClientFactory;
|
||||
import com.influxdb.client.WriteApiBlocking;
|
||||
@ -34,7 +34,7 @@ public class InfluxDbUtils {
|
||||
private InfluxDBClient client;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
private void init() {
|
||||
client = InfluxDBClientFactory.create(url, user, password.toCharArray());
|
||||
}
|
||||
|
||||
@ -47,8 +47,8 @@ public class InfluxDbUtils {
|
||||
writeApi.writeMeasurement(BUCKET, ORG, WritePrecision.NS, data);
|
||||
}
|
||||
|
||||
public RuntimeHistoryVO readRuntimeData(int clientId) {
|
||||
RuntimeHistoryVO vo = new RuntimeHistoryVO();
|
||||
public RuntimeDetailsVO readRuntimeData(int clientId) {
|
||||
RuntimeDetailsVO vo = new RuntimeDetailsVO();
|
||||
String query = """
|
||||
from(bucket: "%s")
|
||||
|> range(start: %s)
|
||||
@ -64,7 +64,10 @@ public class InfluxDbUtils {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("timestamp", records.get(i).getTime());
|
||||
for (int j = 0; j < size; j++) {
|
||||
FluxRecord record = tables.get(j).getRecords().get(i);
|
||||
FluxRecord record = tables
|
||||
.get(j)
|
||||
.getRecords()
|
||||
.get(i);
|
||||
object.put(record.getField(), record.getValue());
|
||||
}
|
||||
vo.getList().add(object);
|
||||
|
@ -141,6 +141,14 @@ public class JwtUtils {
|
||||
return claims.get("id").asInt();
|
||||
}
|
||||
|
||||
public void deleteUser(int uid) {
|
||||
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
private boolean isInvalidUser(int uid){
|
||||
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率检测,防止用户高频申请Jwt令牌,并且采用阶段封禁机制
|
||||
* 如果已经提示无法登录的情况下用户还在刷,那么就封禁更长时间
|
||||
@ -178,14 +186,6 @@ public class JwtUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void deleteUser(int uid) {
|
||||
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
private boolean isInvalidUser(int uid){
|
||||
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token是否被列入Redis黑名单
|
||||
* @param uuid 令牌ID
|
||||
|
@ -4,6 +4,7 @@ import com.example.entity.dto.ClientDetail;
|
||||
import com.example.entity.dto.ClientSsh;
|
||||
import com.example.mapper.ClientDetailMapper;
|
||||
import com.example.mapper.ClientSshMapper;
|
||||
import com.jcraft.jsch.Channel;
|
||||
import com.jcraft.jsch.ChannelShell;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
@ -28,6 +29,8 @@ import java.util.concurrent.Executors;
|
||||
@Component
|
||||
@ServerEndpoint("/terminal/{clientId}")
|
||||
public class TerminalWebSocket {
|
||||
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
|
||||
private final ExecutorService service = Executors.newSingleThreadExecutor();
|
||||
|
||||
private static ClientDetailMapper detailMapper;
|
||||
|
||||
@ -43,9 +46,6 @@ public class TerminalWebSocket {
|
||||
TerminalWebSocket.sshMapper = sshMapper;
|
||||
}
|
||||
|
||||
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
|
||||
private final ExecutorService service = Executors.newSingleThreadExecutor();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session,
|
||||
@PathParam(value="clientId") String clientId) throws Exception {
|
||||
@ -61,10 +61,10 @@ public class TerminalWebSocket {
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(Session session, String message) throws IOException {
|
||||
public void onMessage(Session session, String message) throws Exception {
|
||||
Shell shell = sessionMap.get(session);
|
||||
OutputStream output = shell.output;
|
||||
output.write(message.getBytes(StandardCharsets.UTF_8));
|
||||
output.write(message.getBytes());
|
||||
output.flush();
|
||||
}
|
||||
|
||||
@ -118,11 +118,11 @@ public class TerminalWebSocket {
|
||||
private class Shell {
|
||||
private final Session session;
|
||||
private final com.jcraft.jsch.Session js;
|
||||
private final ChannelShell channel;
|
||||
private final Channel channel;
|
||||
private final InputStream input;
|
||||
private final OutputStream output;
|
||||
|
||||
public Shell(Session session, com.jcraft.jsch.Session js, ChannelShell channel) throws IOException {
|
||||
public Shell(Session session, com.jcraft.jsch.Session js, Channel channel) throws IOException {
|
||||
this.js = js;
|
||||
this.session = session;
|
||||
this.channel = channel;
|
||||
@ -133,14 +133,14 @@ public class TerminalWebSocket {
|
||||
|
||||
private void read() {
|
||||
try {
|
||||
byte[] buffer = new byte[1024 * 1024];
|
||||
byte[] buffer = new byte[1024];
|
||||
int i;
|
||||
while ((i = input.read(buffer)) != -1) {
|
||||
String text = new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8);
|
||||
session.getBasicRemote().sendText(text);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取SSH输入流时出现问题", e);
|
||||
log.error("读取SSH输入流时出现错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ spring:
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
username: javastudy111@163.com
|
||||
password: VKQFYZMUSUZGSGEG
|
||||
password: AHPYEXHWLAHUCLQE
|
||||
rabbitmq:
|
||||
addresses: localhost
|
||||
username: admin
|
||||
@ -36,8 +36,8 @@ spring:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 5
|
||||
limit: 100
|
||||
period: 3
|
||||
limit: 50
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
||||
|
@ -22,7 +22,7 @@ spring:
|
||||
password: admin
|
||||
virtual-host: /
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/test
|
||||
url: jdbc:mysql://localhost:3306/monitor
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
@ -40,8 +40,8 @@ spring:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 5
|
||||
limit: 100
|
||||
period: 3
|
||||
limit: 20
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
||||
|
3
itbaima-monitor-web/.vscode/extensions.json
vendored
3
itbaima-monitor-web/.vscode/extensions.json
vendored
@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
# my-project-frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
@ -2,8 +2,11 @@
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="index" level="application" />
|
||||
</component>
|
||||
</module>
|
17
itbaima-monitor-web/package-lock.json
generated
17
itbaima-monitor-web/package-lock.json
generated
@ -1444,12 +1444,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/needle": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz",
|
||||
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.2.0.tgz",
|
||||
"integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
@ -1460,6 +1461,16 @@
|
||||
"node": ">= 4.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/needle/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
@ -1,10 +1,6 @@
|
||||
.is-success {
|
||||
.el-progress-bar__outer {
|
||||
background-color: #18cb1822 !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #18cb18 !important;
|
||||
background-color: #18cb1822;
|
||||
}
|
||||
|
||||
.el-progress-circle__track {
|
||||
@ -14,6 +10,10 @@
|
||||
.el-progress-circle__path {
|
||||
stroke: #18cb18 !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #18cb18 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-warning {
|
||||
@ -51,3 +51,4 @@
|
||||
background-color: #ef4e4e !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,24 +1,20 @@
|
||||
<script setup>
|
||||
import {computed, reactive, watch} from "vue";
|
||||
import {get, post} from "@/net";
|
||||
import {copyIp, cpuNameToImage, fitByUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import RuntimeHistory from "@/component/RuntimeHistory.vue";
|
||||
import {cpuNameToImage, fitToRightByteUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
|
||||
import RuntimeHistroy from "@/component/RuntimeHistroy.vue";
|
||||
import {Connection, Delete} from "@element-plus/icons-vue";
|
||||
import {useStore} from "@/store";
|
||||
|
||||
const locations = [
|
||||
{name: 'cn', desc: '中国大陆'},
|
||||
{name: 'hk', desc: '香港'},
|
||||
{name: 'jp', desc: '日本'},
|
||||
{name: 'us', desc: '美国'},
|
||||
{name: 'sg', desc: '新加坡'},
|
||||
{name: 'kr', desc: '韩国'},
|
||||
{name: 'de', desc: '德国'}
|
||||
]
|
||||
const store = useStore()
|
||||
const locations = store.locations
|
||||
|
||||
const props = defineProps({
|
||||
id: Number,
|
||||
update: Function
|
||||
update: Function,
|
||||
show: Boolean
|
||||
})
|
||||
const emits = defineEmits(['delete', 'terminal'])
|
||||
|
||||
@ -29,6 +25,7 @@ const details = reactive({
|
||||
},
|
||||
editNode: false
|
||||
})
|
||||
|
||||
const nodeEdit = reactive({
|
||||
name: '',
|
||||
location: ''
|
||||
@ -50,8 +47,18 @@ const submitNodeEdit = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const { copy } = useClipboard()
|
||||
const copyIp = () => {
|
||||
copy(details.base.ip).then(() => ElMessage.success('成功复制IP地址到剪贴板'))
|
||||
}
|
||||
|
||||
function updateDetails() {
|
||||
props.update()
|
||||
init(props.id)
|
||||
}
|
||||
|
||||
function deleteClient() {
|
||||
ElMessageBox.confirm('删除此主机后所有统计数据都将丢失,您确定要这样做吗?', '删除主机', {
|
||||
ElMessageBox.confirm('删除此主机后所有统计数据都将丢失,您确定要这样做吗?', '删除主机', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@ -64,17 +71,13 @@ function deleteClient() {
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function updateDetails() {
|
||||
props.update()
|
||||
init(props.id)
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if(props.id !== -1 && details.runtime) {
|
||||
if(props.show && details.runtime) {
|
||||
get(`/api/monitor/runtime-now?clientId=${props.id}`, data => {
|
||||
if(details.runtime.list.length >= 360)
|
||||
if(details.runtime.list[0].timestamp !== data.timestamp) {
|
||||
details.runtime.list.splice(0, 1)
|
||||
details.runtime.list.push(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
}, 10000)
|
||||
@ -82,32 +85,31 @@ setInterval(() => {
|
||||
const now = computed(() => details.runtime.list[details.runtime.list.length - 1])
|
||||
|
||||
const init = id => {
|
||||
if(id !== -1) {
|
||||
if(id < 0) return
|
||||
details.base = {}
|
||||
details.runtime = { list: [] }
|
||||
get(`/api/monitor/details?clientId=${id}`, data => Object.assign(details.base, data))
|
||||
get(`/api/monitor/runtime-history?clientId=${id}`, data => Object.assign(details.runtime, data))
|
||||
}
|
||||
get(`/api/monitor/runtime-history?clientId=${props.id}`,
|
||||
data => Object.assign(details.runtime, data))
|
||||
}
|
||||
watch(() => props.id, init, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div class="client-details" v-loading="Object.keys(details.base).length === 0">
|
||||
<div class="client-details"
|
||||
v-loading="Object.keys(details.base).length === 0">
|
||||
<div v-if="Object.keys(details.base).length">
|
||||
<div style="display: flex;justify-content: space-between">
|
||||
<div style="margin-bottom: 15px">
|
||||
<el-button :icon="Connection" type="info" size="small" plain
|
||||
@click="emits('terminal', id)">SSH远程连接</el-button>
|
||||
<el-button :icon="Delete" type="danger" size="small"
|
||||
@click="deleteClient" plain>删除此主机</el-button>
|
||||
</div>
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-server"></i>
|
||||
服务器信息
|
||||
</div>
|
||||
<div>
|
||||
<el-button :icon="Connection" type="info"
|
||||
@click="emits('terminal', id)" plain text>SSH远程连接</el-button>
|
||||
<el-button :icon="Delete" type="danger" style="margin-left: 0"
|
||||
@click="deleteClient" plain text>删除此主机</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div class="details-list">
|
||||
<div>
|
||||
@ -124,7 +126,7 @@ watch(() => props.id, init, { immediate: true })
|
||||
<span>运行状态</span>
|
||||
<span>
|
||||
<i style="color: #18cb18" class="fa-solid fa-circle-play" v-if="details.base.online"></i>
|
||||
<i style="color: #18cb18" class="fa-solid fa-circle-stop" v-else></i>
|
||||
<i style="color: #b7b7b7" class="fa-solid fa-circle-stop" v-else></i>
|
||||
{{details.base.online ? '运行中' : '离线'}}
|
||||
</span>
|
||||
</div>
|
||||
@ -157,7 +159,7 @@ watch(() => props.id, init, { immediate: true })
|
||||
<span>公网IP地址</span>
|
||||
<span>
|
||||
{{details.base.ip}}
|
||||
<i class="fa-solid fa-copy interact-item" style="color: dodgerblue" @click.stop="copyIp(details.base.ip)"></i>
|
||||
<i class="fa-solid fa-copy interact-item" style="color: dodgerblue" @click.stop="copyIp"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
@ -179,7 +181,7 @@ watch(() => props.id, init, { immediate: true })
|
||||
<span>操作系统</span>
|
||||
<i :style="{color: osNameToIcon(details.base.osName).color}"
|
||||
:class="`fa-brands ${osNameToIcon(details.base.osName).icon}`"></i>
|
||||
<span style="margin-left: 10px">{{`${details.base.osName} ${details.base.osVersion}`}}</span>
|
||||
<span>{{` ${details.base.osName} ${details.base.osVersion}`}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title" style="margin-top: 20px">
|
||||
@ -195,8 +197,8 @@ watch(() => props.id, init, { immediate: true })
|
||||
<div style="font-size: 17px;font-weight: bold;color: initial">CPU</div>
|
||||
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.cpuUsage * 100).toFixed(1) }}%</div>
|
||||
</el-progress>
|
||||
<el-progress style="margin-left: 20px" type="dashboard" :width="100"
|
||||
:percentage="now.memoryUsage / details.runtime.memory * 100"
|
||||
<el-progress style="margin-left: 20px"
|
||||
type="dashboard" :width="100" :percentage="now.memoryUsage / details.runtime.memory * 100"
|
||||
:status="percentageToStatus(now.memoryUsage / details.runtime.memory * 100)">
|
||||
<div style="font-size: 16px;font-weight: bold;color: initial">内存</div>
|
||||
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.memoryUsage).toFixed(1) }} GB</div>
|
||||
@ -206,27 +208,27 @@ watch(() => props.id, init, { immediate: true })
|
||||
<div>实时网络速度</div>
|
||||
<div>
|
||||
<i style="color: orange" class="fa-solid fa-arrow-up"></i>
|
||||
<span>{{` ${fitByUnit(now.networkUpload, 'KB')}/s`}}</span>
|
||||
<el-divider direction="vertical"/>
|
||||
<span>{{` ${fitToRightByteUnit(now.networkUpload, 'KB')}/s`}}</span>
|
||||
<el-divider style="margin: 0 20px" direction="vertical"/>
|
||||
<i style="color: dodgerblue" class="fa-solid fa-arrow-down"></i>
|
||||
<span>{{` ${fitByUnit(now.networkDownload, 'KB')}/s`}}</span>
|
||||
<span>{{` ${fitToRightByteUnit(now.networkDownload, 'KB')}/s`}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 13px;display: flex;justify-content: space-between">
|
||||
<div>
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
<span> 磁盘总容量</span>
|
||||
<span> 磁盘</span>
|
||||
</div>
|
||||
<div>{{now.diskUsage.toFixed(1)}} GB / {{details.runtime.disk.toFixed(1)}} GB</div>
|
||||
</div>
|
||||
<el-progress type="line" :show-text="false"
|
||||
:status="percentageToStatus(now.diskUsage / details.runtime.disk * 100)"
|
||||
:percentage="now.diskUsage / details.runtime.disk * 100" />
|
||||
<el-progress type="line" :status="percentageToStatus(now.diskUsage / details.runtime.disk * 100)"
|
||||
:percentage="now.diskUsage / details.runtime.disk * 100" :show-text="false"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<runtime-history style="margin-top: 20px" :data="details.runtime.list"/>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<runtime-histroy :data="details.runtime.list"/>
|
||||
</div>
|
||||
<el-empty description="服务器处于离线状态,请检查服务器是否正常运行" v-else/>
|
||||
</div>
|
||||
@ -235,16 +237,6 @@ watch(() => props.id, init, { immediate: true })
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.interact-item {
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
scale: 1.1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.client-details {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
@ -258,11 +250,21 @@ watch(() => props.id, init, { immediate: true })
|
||||
.details-list {
|
||||
font-size: 14px;
|
||||
|
||||
.interact-item {
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
scale: 1.1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
& div {
|
||||
margin-bottom: 10px;
|
||||
|
||||
& span:first-child {
|
||||
color: gray;
|
||||
color: grey;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
width: 120px;
|
||||
@ -275,4 +277,12 @@ watch(() => props.id, init, { immediate: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
color: #5e5e5e;
|
||||
}
|
||||
|
||||
.dark span{
|
||||
color: #d9d9d9;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,23 +1,24 @@
|
||||
<script setup>
|
||||
import {reactive, ref} from "vue";
|
||||
import {Lock, Message, User} from "@element-plus/icons-vue";
|
||||
import {reactive, ref} from "vue";
|
||||
import {osNameToIcon} from "@/tools";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {post} from "@/net";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
defineProps({
|
||||
clients: Array
|
||||
})
|
||||
|
||||
const emits = defineEmits(['create'])
|
||||
|
||||
const formRef = ref()
|
||||
const valid = ref(false)
|
||||
const onValidate = (prop, isValid) => valid.value = isValid
|
||||
const form = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
})
|
||||
const formRef = ref()
|
||||
const valid = ref(false)
|
||||
const onValidate = (prop, isValid) => valid.value = isValid
|
||||
|
||||
const validateUsername = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
@ -55,7 +56,7 @@ const onCheck = (state, id) => {
|
||||
}
|
||||
|
||||
function createSubAccount() {
|
||||
formRef.value.validate(isValid => {
|
||||
formRef.value.validate((isValid) => {
|
||||
if(checkedClients.length === 0) {
|
||||
ElMessage.warning('请至少选择一个服务器用于子账户进行管理')
|
||||
return
|
||||
|
@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import {copyIp, fitByUnit, osNameToIcon, percentageToStatus, rename} from '@/tools'
|
||||
import {copyContent, fitToRightByteUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
|
||||
|
||||
const props = defineProps({
|
||||
defineProps({
|
||||
data: Object,
|
||||
update: Function
|
||||
})
|
||||
@ -11,10 +11,11 @@ const props = defineProps({
|
||||
<div class="instance-card">
|
||||
<div style="display: flex;justify-content: space-between">
|
||||
<div>
|
||||
<div class="name">
|
||||
<div class="title">
|
||||
<span :class="`flag-icon flag-icon-${data.location}`"></span>
|
||||
<span style="margin: 0 5px">{{ data.name }}</span>
|
||||
<i class="fa-solid fa-pen-to-square interact-item" @click.stop="rename(data.id, data.name, update)"></i>
|
||||
<span style="margin: 0 10px">{{ data.name }}</span>
|
||||
<i @click.stop="rename(data.id, data.name, update)"
|
||||
class="fa-solid fa-pen-to-square interact-item"/>
|
||||
</div>
|
||||
<div class="os">
|
||||
操作系统:
|
||||
@ -35,7 +36,8 @@ const props = defineProps({
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div class="network">
|
||||
<span style="margin-right: 10px">公网IP: {{data.ip}}</span>
|
||||
<i class="fa-solid fa-copy interact-item" @click.stop="copyIp(data.ip)" style="color: dodgerblue"></i>
|
||||
<i class="fa-solid fa-copy interact-item" style="color: dodgerblue"
|
||||
@click.stop="copyContent(data.ip, '成功复制IP地址到剪贴板')"></i>
|
||||
</div>
|
||||
<div class="cpu">
|
||||
<span style="margin-right: 10px">处理器: {{data.cpuName}}</span>
|
||||
@ -48,29 +50,43 @@ const props = defineProps({
|
||||
</div>
|
||||
<div class="progress">
|
||||
<span>{{ `CPU: ${(data.cpuUsage * 100).toFixed(1)} %` }}</span>
|
||||
<el-progress :status="percentageToStatus(data.cpuUsage * 100)"
|
||||
:percentage="data.cpuUsage * 100" :stroke-width="5" :show-text="false"/>
|
||||
<el-progress :percentage="data.cpuUsage * 100" :stroke-width="5" :show-text="false"
|
||||
:status="percentageToStatus(data.cpuUsage * 100)"/>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<div class="progress" style="margin-top: 7px">
|
||||
<span>内存: <b>{{ data.memoryUsage.toFixed(1) }}</b> GB</span>
|
||||
<el-progress :status="percentageToStatus(data.memoryUsage/data.memory * 100)"
|
||||
:percentage="data.memoryUsage/data.memory * 100" :stroke-width="5" :show-text="false"/>
|
||||
<el-progress :percentage="data.memoryUsage/data.memory * 100" :stroke-width="5" :show-text="false"
|
||||
:status="percentageToStatus(data.memoryUsage/data.memory * 100)"/>
|
||||
</div>
|
||||
<div class="network-flow">
|
||||
<div>网络流量</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
<span>{{` ${fitByUnit(data.networkUpload, 'KB')}/s`}}</span>
|
||||
<span>{{` ${fitToRightByteUnit(data.networkUpload, 'KB')}/s`}}</span>
|
||||
<el-divider direction="vertical"/>
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
<span>{{` ${fitByUnit(data.networkDownload, 'KB')}/s`}}</span>
|
||||
<span>{{` ${fitToRightByteUnit(data.networkDownload, 'KB')}/s`}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.dark .instance-card { color: #d9d9d9 }
|
||||
.dark .instance-card { color: #d9d9d9
|
||||
}
|
||||
.instance-card {
|
||||
width: 320px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
color: #6b6b6b;
|
||||
transition: scale .3s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
scale: 1.02;
|
||||
}
|
||||
|
||||
.interact-item {
|
||||
transition: .3s;
|
||||
@ -82,52 +98,38 @@ const props = defineProps({
|
||||
}
|
||||
}
|
||||
|
||||
.instance-card {
|
||||
width: 320px;
|
||||
padding: 15px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #606060;
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
scale: 1.02;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
.os {
|
||||
font-size: 13px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.os {
|
||||
font-size: 13px;
|
||||
color: grey;
|
||||
.title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.network {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.cpu {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hardware {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-top: 10px;
|
||||
margin-top: 15px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cpu {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.network-flow {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
|
@ -3,20 +3,21 @@ import * as echarts from "echarts";
|
||||
import {onMounted, watch} from "vue";
|
||||
import {defaultOption, doubleSeries, singleSeries} from "@/echarts";
|
||||
|
||||
const charts = []
|
||||
|
||||
const props = defineProps({
|
||||
data: Object
|
||||
})
|
||||
|
||||
const localTimeLine = list => list.map(item => new Date(item.timestamp).toLocaleString())
|
||||
const charts = []
|
||||
|
||||
const localTimeLine = (list) => list.map(item => new Date(item.timestamp).toLocaleString())
|
||||
|
||||
|
||||
function updateCpuUsage(list) {
|
||||
const chart = charts[0]
|
||||
let data = list.map(item => (item.cpuUsage * 100).toFixed(1))
|
||||
let data = list.map(item => (item.cpuUsage * 100).toFixed(1));
|
||||
const option = defaultOption('CPU(%)', localTimeLine(list))
|
||||
singleSeries(option, 'CPU使用率(%)', data, ['#72c4fe', '#72d5fe', '#2b6fd733'])
|
||||
chart.setOption(option)
|
||||
chart.setOption(option);
|
||||
}
|
||||
|
||||
function updateMemoryUsage(list) {
|
||||
@ -30,8 +31,8 @@ function updateMemoryUsage(list) {
|
||||
function updateNetworkUsage(list) {
|
||||
const chart = charts[2]
|
||||
let data = [
|
||||
list.map(item => item.networkUpload),
|
||||
list.map(item => item.networkDownload)
|
||||
list.map(item => item.networkUpload.toFixed(1)),
|
||||
list.map(item => item.networkDownload.toFixed(1))
|
||||
]
|
||||
const option = defaultOption('网络(KB/s)', localTimeLine(list))
|
||||
doubleSeries(option, ['上传(KB/s)', '下载(KB/s)'], data, [
|
||||
@ -41,6 +42,7 @@ function updateNetworkUsage(list) {
|
||||
chart.setOption(option);
|
||||
}
|
||||
|
||||
|
||||
function updateDiskUsage(list) {
|
||||
const chart = charts[3]
|
||||
let data = [
|
@ -1,9 +1,8 @@
|
||||
<script setup>
|
||||
import {onBeforeUnmount, onMounted, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {AttachAddon} from "xterm-addon-attach/src/AttachAddon";
|
||||
import {Terminal} from "xterm";
|
||||
import "xterm/css/xterm.css";
|
||||
import {AttachAddon} from "xterm-addon-attach/src/AttachAddon";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
id: Number
|
||||
@ -40,7 +39,7 @@ const term = new Terminal({
|
||||
term.loadAddon(attachAddon);
|
||||
|
||||
onMounted(() => {
|
||||
term.open(terminalRef.value)
|
||||
term.open(terminalRef.value);
|
||||
term.focus()
|
||||
})
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
<script setup>
|
||||
import {reactive, ref, watch} from "vue";
|
||||
import {get, post} from "@/net";
|
||||
import {ElMessage} from "element-plus";
|
||||
import "xterm/css/xterm.css";
|
||||
import Terminal from "@/component/Terminal.vue";
|
||||
|
||||
const props = defineProps({
|
||||
id: Number
|
||||
id: Number,
|
||||
show: Boolean
|
||||
})
|
||||
|
||||
const connection = reactive({
|
||||
@ -15,6 +16,8 @@ const connection = reactive({
|
||||
password: ''
|
||||
})
|
||||
|
||||
const state = ref(1)
|
||||
|
||||
const rules = {
|
||||
port: [
|
||||
{ required: true, message: '请输入端口', trigger: ['blur', 'change'] },
|
||||
@ -27,7 +30,6 @@ const rules = {
|
||||
]
|
||||
}
|
||||
|
||||
const state = ref(1)
|
||||
const formRef = ref()
|
||||
|
||||
function saveConnection() {
|
||||
@ -42,12 +44,13 @@ function saveConnection() {
|
||||
}
|
||||
|
||||
watch(() => props.id, id => {
|
||||
state.value = 1
|
||||
if(id !== -1) {
|
||||
if(id === -1) return
|
||||
connection.ip = ''
|
||||
state.value = 1
|
||||
get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
|
||||
}
|
||||
}, {immediate: true})
|
||||
|
||||
watch(() => props.show, () => state.value = 1)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -3,9 +3,9 @@ import App from './App.vue'
|
||||
import router from './router'
|
||||
import axios from "axios";
|
||||
|
||||
import '@/assets/css/element.less'
|
||||
import 'flag-icon-css/css/flag-icons.min.css'
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import '@/assets/css/element.less'
|
||||
import {createPinia} from "pinia";
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
|
||||
|
||||
|
@ -76,9 +76,7 @@ function login(username, password, remember, success, failure = defaultFailure){
|
||||
}, (data) => {
|
||||
storeAccessToken(remember, data.token, data.expire)
|
||||
const store = useStore()
|
||||
store.user.role = data.role
|
||||
store.user.username = data.username
|
||||
store.user.email = data.email
|
||||
Object.assign(store.user, data)
|
||||
ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`)
|
||||
success(data)
|
||||
}, failure)
|
||||
|
@ -7,7 +7,16 @@ export const useStore = defineStore('general', {
|
||||
role: '',
|
||||
username: '',
|
||||
email: ''
|
||||
}
|
||||
}, locations: [
|
||||
{name: 'cn', desc: '中国大陆'},
|
||||
{name: 'hk', desc: '香港'},
|
||||
{name: 'jp', desc: '日本'},
|
||||
{name: 'us', desc: '美国'},
|
||||
{name: 'sg', desc: '新加坡'},
|
||||
{name: 'kr', desc: '韩国'},
|
||||
{name: 'de', desc: '德国'}
|
||||
]
|
||||
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
@ -2,7 +2,7 @@ import {useClipboard} from "@vueuse/core";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {post} from "@/net";
|
||||
|
||||
function fitByUnit(value, unit) {
|
||||
function fitToRightByteUnit(value, unit) {
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
|
||||
let index = units.indexOf(unit)
|
||||
while (((value < 1 && value !== 0) || value >= 1024) && (index >= 0 || index < units.length)) {
|
||||
@ -51,15 +51,17 @@ function cpuNameToImage(name) {
|
||||
}
|
||||
|
||||
const { copy } = useClipboard()
|
||||
const copyIp = ip => copy(ip).then(() => ElMessage.success('成功复制IP地址到剪贴板'))
|
||||
const copyContent = (text, message) => {
|
||||
copy(text).then(() => ElMessage.success(message))
|
||||
}
|
||||
|
||||
function rename(id, name, after) {
|
||||
ElMessageBox.prompt('请输入新的服务器主机名称', '修改名称', {
|
||||
const rename = (id, name, after) => ElMessageBox.prompt('请输入新的服务器主机名称', '修改名称', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: name,
|
||||
inputPattern: /^[a-zA-Z0-9_\u4e00-\u9fa5]{1,10}$/,
|
||||
inputErrorMessage: '名称只能包含中英文字符、数字和下划线',
|
||||
inputPattern:
|
||||
/^[a-zA-Z0-9_\u4e00-\u9fa5]{1,10}$/,
|
||||
inputErrorMessage: '名称长度不能超过10个字符,且只能包含中英文字符、数字和下划线',
|
||||
}).then(({ value }) => post('/api/monitor/rename', {
|
||||
id: id,
|
||||
name: value
|
||||
@ -67,7 +69,6 @@ function rename(id, name, after) {
|
||||
ElMessage.success('主机名称已更新')
|
||||
after()
|
||||
})
|
||||
)
|
||||
}
|
||||
).catch(() => {})
|
||||
|
||||
export { fitByUnit, percentageToStatus, cpuNameToImage, osNameToIcon, rename, copyIp }
|
||||
export { fitToRightByteUnit, percentageToStatus, copyContent, rename, osNameToIcon, cpuNameToImage }
|
||||
|
@ -4,8 +4,10 @@
|
||||
<el-image style="height: 30px"
|
||||
src="https://element-plus.org/images/element-plus-logo.svg"/>
|
||||
<div class="tabs">
|
||||
<tab-item v-for="item in tabs" :name="item.name"
|
||||
:active="item.id === tab" @click="changePage(item)"/>
|
||||
<tab-item v-for="item in tabs"
|
||||
:name="item.name" :active="item.id === tab"
|
||||
@click="changePage(item)"/>
|
||||
</div>
|
||||
<el-switch style="margin: 0 20px"
|
||||
v-model="dark" active-color="#424242"
|
||||
:active-action-icon="Moon"
|
||||
@ -30,7 +32,6 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
@ -48,20 +49,19 @@
|
||||
import { logout } from '@/net'
|
||||
import router from "@/router";
|
||||
import {Back, Moon, Sunny} from "@element-plus/icons-vue";
|
||||
import TabItem from "@/component/TabItem.vue";
|
||||
import {ref} from "vue";
|
||||
import {useDark} from "@vueuse/core";
|
||||
import TabItem from "@/component/TabItem.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useStore} from "@/store";
|
||||
|
||||
const store = useStore()
|
||||
|
||||
function userLogout() {
|
||||
logout(() => router.push("/"))
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const dark = ref(useDark())
|
||||
const tabs = [
|
||||
{id: 1, name: '管理', route: 'manage'},
|
||||
{id: 2, name: '安全', route: 'security'}
|
||||
]
|
||||
const defaultIndex = () => {
|
||||
for (let tab of tabs) {
|
||||
if(route.name === tab.route)
|
||||
@ -69,15 +69,17 @@ const defaultIndex = () => {
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
const dark = ref(useDark())
|
||||
const tabs = [
|
||||
{id: 1, name: '管理', route: 'manage'},
|
||||
{id: 2, name: '安全', route: 'security'}
|
||||
]
|
||||
const tab = ref(defaultIndex())
|
||||
function changePage(item) {
|
||||
tab.value = item.id
|
||||
router.push({name: item.route})
|
||||
}
|
||||
|
||||
function userLogout() {
|
||||
logout(() => router.push("/"))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -95,11 +97,15 @@ function userLogout() {
|
||||
.tabs {
|
||||
height: 55px;
|
||||
gap: 10px;
|
||||
flex: 1px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: solid 1px var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
|
@ -1,46 +1,33 @@
|
||||
<script setup>
|
||||
import PreviewCard from "@/component/PreviewCard.vue";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import {get} from "@/net";
|
||||
import {computed, reactive, ref} from "vue";
|
||||
import ClientDetails from "@/component/ClientDetails.vue";
|
||||
import RegisterCard from "@/component/RegisterCard.vue";
|
||||
import {Plus} from "@element-plus/icons-vue";
|
||||
import RegisterCard from "@/component/RegisterCard.vue";
|
||||
import {useRoute} from "vue-router";
|
||||
import {useStore} from "@/store";
|
||||
import TerminalWindow from "@/component/TerminalWindow.vue";
|
||||
|
||||
const locations = [
|
||||
{name: 'cn', desc: '中国大陆'},
|
||||
{name: 'hk', desc: '香港'},
|
||||
{name: 'jp', desc: '日本'},
|
||||
{name: 'us', desc: '美国'},
|
||||
{name: 'sg', desc: '新加坡'},
|
||||
{name: 'kr', desc: '韩国'},
|
||||
{name: 'de', desc: '德国'}
|
||||
]
|
||||
const checkedNodes = ref([])
|
||||
|
||||
const list = ref([])
|
||||
const store = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const updateList = () => {
|
||||
if(route.name === 'manage') {
|
||||
get('/api/monitor/list', data => list.value = data)
|
||||
}
|
||||
}
|
||||
setInterval(updateList, 10000)
|
||||
updateList()
|
||||
const list = ref([])
|
||||
const checkedNodes = ref([])
|
||||
const locations = store.locations
|
||||
|
||||
const detail = reactive({
|
||||
show: false,
|
||||
id: -1
|
||||
})
|
||||
const displayClientDetails = (id) => {
|
||||
detail.show = true
|
||||
const displayClientDetails = id => {
|
||||
detail.id = id
|
||||
detail.show = true
|
||||
}
|
||||
const register = reactive({
|
||||
show: false,
|
||||
token: ''
|
||||
})
|
||||
const refreshToken = () => get('/api/monitor/register', code => register.token = code)
|
||||
|
||||
const clientList = computed(() => {
|
||||
if(checkedNodes.value.length === 0) {
|
||||
@ -50,20 +37,21 @@ const clientList = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const register = reactive({
|
||||
show: false,
|
||||
token: ''
|
||||
})
|
||||
const refreshToken = () => get('/api/monitor/register', token => register.token = token)
|
||||
const updateList = () => {
|
||||
if(route.name === 'manage')
|
||||
get('/api/monitor/list', data => list.value = data)
|
||||
}
|
||||
setInterval(updateList, 10000)
|
||||
updateList()
|
||||
|
||||
function openTerminal(id) {
|
||||
const openTerminal = id => {
|
||||
terminal.show = true
|
||||
terminal.id = id
|
||||
detail.show = false
|
||||
}
|
||||
const terminal = reactive({
|
||||
show: false,
|
||||
id: -1
|
||||
id: detail.id
|
||||
})
|
||||
</script>
|
||||
|
||||
@ -72,11 +60,11 @@ const terminal = reactive({
|
||||
<div style="display: flex;justify-content: space-between;align-items: end">
|
||||
<div>
|
||||
<div class="title"><i class="fa-solid fa-server"></i> 管理主机列表</div>
|
||||
<div class="desc">在这里管理所有已经注册的主机实例,实时监控主机运行状态,快速进行管理和操作。</div>
|
||||
<div class="description">在这里管理所有已经注册的主机实例,实时监控主机运行状态,快速进行管理和操作。</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button :icon="Plus" type="primary" plain :disabled="!store.isAdmin"
|
||||
@click="register.show = true">添加新主机</el-button>
|
||||
<el-button :icon="Plus" type="primary" @click="register.show = true"
|
||||
plain :disabled="!store.isAdmin">添加新主机</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
@ -88,22 +76,22 @@ const terminal = reactive({
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</div>
|
||||
<div class="card-list" v-if="list.length">
|
||||
<preview-card v-for="item in clientList" :data="item" :update="updateList"
|
||||
<div class="card-list">
|
||||
<preview-card :update="updateList" :data="item" v-for="item in clientList"
|
||||
@click="displayClientDetails(item.id)"/>
|
||||
</div>
|
||||
<el-empty description="还没有任何主机哦,点击右上角添加一个吧" v-else/>
|
||||
<el-drawer size="520" :show-close="false" v-model="detail.show"
|
||||
:with-header="false" v-if="list.length" @close="detail.id = -1">
|
||||
<client-details :id="detail.id" :update="updateList" @delete="updateList" @terminal="openTerminal"/>
|
||||
<client-details :id="detail.id" :update="updateList" :show="detail.show"
|
||||
@delete="detail.show = false" @terminal="openTerminal"/>
|
||||
</el-drawer>
|
||||
<el-drawer v-model="register.show" direction="btt" :with-header="false"
|
||||
style="width: 600px;margin: 10px auto" size="320" @open="refreshToken">
|
||||
<el-drawer style="width: 600px;margin: 10px auto" v-model="register.show"
|
||||
direction="btt" :show-close="false" :with-header="false"
|
||||
:size="320" @open="refreshToken">
|
||||
<register-card :token="register.token"/>
|
||||
</el-drawer>
|
||||
<el-drawer style="width: 800px" :size="520" direction="btt"
|
||||
@close="terminal.id = -1"
|
||||
v-model="terminal.show" :close-on-click-modal="false">
|
||||
<el-drawer style="width: 800px" :size="520" direction="btt" v-model="terminal.show"
|
||||
:close-on-click-modal="false" @close="terminal.show = false;terminal.id = -1">
|
||||
<template #header>
|
||||
<div>
|
||||
<div style="font-size: 18px;color: dodgerblue;font-weight: bold;">SSH远程连接</div>
|
||||
@ -112,7 +100,7 @@ const terminal = reactive({
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<terminal-window :id="terminal.id"/>
|
||||
<terminal-window :id="terminal.id" :show="terminal.show"/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@ -144,15 +132,24 @@ const terminal = reactive({
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.desc {
|
||||
.description {
|
||||
font-size: 15px;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
|
||||
.card-list {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes move {
|
||||
from {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<script setup>
|
||||
import {reactive, ref} from "vue";
|
||||
import {Delete, Lock, Plus, Refresh, Switch} from "@element-plus/icons-vue";
|
||||
import {get, logout, post} from "@/net";
|
||||
import {ElMessage} from "element-plus";
|
||||
import {Delete, Lock, Plus, Refresh, Switch} from "@element-plus/icons-vue";
|
||||
import router from "@/router";
|
||||
import CreateSubAccount from "@/component/CreateSubAccount.vue";
|
||||
import {useStore} from "@/store";
|
||||
@ -19,16 +19,6 @@ const form = reactive({
|
||||
new_password_repeat: '',
|
||||
})
|
||||
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== form.new_password) {
|
||||
callback(new Error("两次输入的密码不一致"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const emailForm = reactive({
|
||||
email: store.user.email,
|
||||
code: ''
|
||||
@ -66,6 +56,16 @@ function modifyEmail() {
|
||||
})
|
||||
}
|
||||
|
||||
const validatePassword = (rule, value, callback) => {
|
||||
if (value === '') {
|
||||
callback(new Error('请再次输入密码'))
|
||||
} else if (value !== form.new_password) {
|
||||
callback(new Error("两次输入的密码不一致"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const rules = {
|
||||
password: [
|
||||
{ required: true, message: '请输入原来的密码', trigger: 'blur' },
|
||||
@ -85,29 +85,28 @@ const rules = {
|
||||
}
|
||||
|
||||
function resetPassword() {
|
||||
formRef.value.validate(isValid => {
|
||||
formRef.value.validate((isValid) => {
|
||||
if(isValid) {
|
||||
post('/api/user/change-password', form, () => {
|
||||
ElMessage.success('密码修改成功,请重新登录!')
|
||||
ElMessage.success('密码修改成功,请重新登录!')
|
||||
logout(() => router.push('/'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const simpleList = ref([])
|
||||
if(store.isAdmin) {
|
||||
get('/api/monitor/simple-list', list => {
|
||||
simpleList.value = list
|
||||
initSubAccounts()
|
||||
})
|
||||
}
|
||||
|
||||
const accounts = ref([])
|
||||
const initSubAccounts = () =>
|
||||
get('/api/user/sub/list', list => accounts.value = list)
|
||||
|
||||
const createAccount = ref(false)
|
||||
const simpleList = ref([])
|
||||
if(store.isAdmin) {
|
||||
get('/api/monitor/simple-list', list => {
|
||||
simpleList.value = list
|
||||
initSubAccounts()
|
||||
})
|
||||
}
|
||||
|
||||
function deleteAccount(id) {
|
||||
get(`/api/user/sub/delete?uid=${id}`, () => {
|
||||
@ -195,14 +194,15 @@ function deleteAccount(id) {
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-empty :image-size="100" description="还没有任何子用户哦" v-if="store.isAdmin">
|
||||
<el-button :icon="Plus" type="primary" plain
|
||||
@click="createAccount = true">添加子用户</el-button>
|
||||
<el-button :icon="Plus" type="primary"
|
||||
@click="createAccount = true" plain>添加子用户</el-button>
|
||||
</el-empty>
|
||||
<el-empty :image-size="100" description="子账户只能由管理员账号进行操作" v-else/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-drawer v-model="createAccount" size="350" :with-header="false">
|
||||
<create-sub-account :clients="simpleList" @create="createAccount = false;initSubAccounts()"/>
|
||||
<create-sub-account @create="createAccount = false;initSubAccounts()" :clients="simpleList"/>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
@ -210,9 +210,9 @@ function deleteAccount(id) {
|
||||
<style scoped>
|
||||
.info-card {
|
||||
border-radius: 7px;
|
||||
height: fit-content;
|
||||
padding: 15px 20px;
|
||||
background-color: var(--el-bg-color);
|
||||
height: fit-content;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
|
@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="GENERAL_MODULE" version="4">
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/config" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/log" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -11,7 +11,7 @@
|
||||
Target Server Version : 80200 (8.2.0)
|
||||
File Encoding : 65001
|
||||
|
||||
Date: 14/12/2023 18:44:11
|
||||
Date: 05/12/2023 16:27:48
|
||||
*/
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
@ -27,12 +27,12 @@ CREATE TABLE `db_account` (
|
||||
`email` varchar(255) DEFAULT NULL,
|
||||
`password` varchar(255) DEFAULT NULL,
|
||||
`role` varchar(255) DEFAULT NULL,
|
||||
`clients` json DEFAULT NULL,
|
||||
`register_time` datetime DEFAULT NULL,
|
||||
`clients` json DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `unique_email` (`email`),
|
||||
UNIQUE KEY `unique_username` (`username`)
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- ----------------------------
|
||||
-- Table structure for db_client
|
||||
@ -42,8 +42,8 @@ CREATE TABLE `db_client` (
|
||||
`id` int NOT NULL,
|
||||
`name` varchar(255) DEFAULT NULL,
|
||||
`token` varchar(255) DEFAULT NULL,
|
||||
`location` varchar(255) DEFAULT NULL,
|
||||
`node` varchar(255) DEFAULT NULL,
|
||||
`location` varchar(255) DEFAULT NULL,
|
||||
`register_time` datetime DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
Loading…
x
Reference in New Issue
Block a user