Compare commits

...

No commits in common. "main" and "dev2" have entirely different histories.
main ... dev2

53 changed files with 725 additions and 701 deletions

34
.gitignore vendored
View File

@ -1,4 +1,32 @@
.idea ### IntelliJ IDEA ###
log 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 .DS_Store
config

View File

@ -1,68 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version> <version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>com.example</groupId> <groupId>com.example</groupId>
<artifactId>itbaima-monitor-client</artifactId> <artifactId>itbaima-monitor-client</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>itbaima-monitor-client</name> <name>itbaima-monitor-client</name>
<description>itbaima-monitor-client</description> <description>itbaima-monitor-client</description>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId> <artifactId>spring-boot-starter</artifactId>
</dependency> </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>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.8</version>
</dependency>
</dependencies>
<dependency> <build>
<groupId>org.projectlombok</groupId> <plugins>
<artifactId>lombok</artifactId> <plugin>
<optional>true</optional> <groupId>org.springframework.boot</groupId>
</dependency> <artifactId>spring-boot-maven-plugin</artifactId>
<dependency> <configuration>
<groupId>org.springframework.boot</groupId> <image>
<artifactId>spring-boot-starter-test</artifactId> <builder>paketobuildpacks/builder-jammy-base:latest</builder>
<scope>test</scope> </image>
</dependency> <excludes>
<dependency> <exclude>
<groupId>org.springframework.boot</groupId> <groupId>org.projectlombok</groupId>
<artifactId>spring-boot-starter-quartz</artifactId> <artifactId>lombok</artifactId>
</dependency> </exclude>
<dependency> </excludes>
<groupId>com.alibaba.fastjson2</groupId> </configuration>
<artifactId>fastjson2</artifactId> </plugin>
<version>2.0.37</version> </plugins>
</dependency> </build>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project> </project>

View File

@ -6,8 +6,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class MonitorClientApplication { public class MonitorClientApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(MonitorClientApplication.class, args); SpringApplication.run(MonitorClientApplication.class, args);
} }
} }

View File

@ -5,11 +5,17 @@ import lombok.extern.slf4j.Slf4j;
import org.quartz.*; import org.quartz.*;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import oshi.SystemInfo;
@Slf4j @Slf4j
@Configuration @Configuration
public class QuartzConfiguration { public class QuartzConfiguration {
@Bean
public SystemInfo info(){
return new SystemInfo();
}
@Bean @Bean
public JobDetail jobDetailFactoryBean() { public JobDetail jobDetailFactoryBean() {
return JobBuilder.newJob(MonitorJobBean.class) return JobBuilder.newJob(MonitorJobBean.class)
@ -20,10 +26,10 @@ public class QuartzConfiguration {
@Bean @Bean
public Trigger cronTriggerFactoryBean(JobDetail detail) { public Trigger cronTriggerFactoryBean(JobDetail detail) {
CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule("*/10 * * * * ?"); CronScheduleBuilder cron= CronScheduleBuilder.cronSchedule("*/10 * * * * ?");
return TriggerBuilder.newTrigger() return TriggerBuilder.newTrigger()
.forJob(detail) .forJob(detail)
.withIdentity("monitor-trigger") .withIdentity("monitor-timer")
.withSchedule(cron) .withSchedule(cron)
.build(); .build();
} }

View File

@ -2,6 +2,7 @@ package com.example.config;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.example.entity.ConnectionConfig; import com.example.entity.ConnectionConfig;
import com.example.entity.data.BaseDetail;
import com.example.utils.MonitorUtils; import com.example.utils.MonitorUtils;
import com.example.utils.NetUtils; import com.example.utils.NetUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -16,7 +17,6 @@ import java.io.FileInputStream;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Scanner; import java.util.Scanner;
@Slf4j @Slf4j
@ -39,56 +39,47 @@ public class ServerConfiguration implements ApplicationRunner {
} }
@Override @Override
public void run(ApplicationArguments args) { public void run(ApplicationArguments args) throws Exception {
log.info("正在向服务端更新基本系统信息..."); log.info("正在向服务端更新基本系统信息...");
net.updateBaseDetails(monitor.monitorBaseDetail()); BaseDetail detail = monitor.monitorBaseData();
net.updateBaseDetails(detail);
} }
private ConnectionConfig registerToServer() { private ConnectionConfig registerToServer() {
Scanner scanner = new Scanner(System.in); Scanner scanner = new Scanner(System.in);
String token, address, ifName; String token, address;
do { do {
log.info("请输入需要注册的服务端访问地址,地址类似于 'http://192.168.0.22:8080' 这种写法:"); log.info("请输入需要注册的服务端访问地址,地址类似于'http://localhost:8080'这种写法:");
address = scanner.nextLine(); address = scanner.nextLine();
log.info("请输入服务端生成的用于注册客户端的Token秘钥:"); log.info("请输入由服务端生成,用于访问服务端的Token秘钥:");
token = scanner.nextLine(); 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)); } while (!net.registerToServer(address, token));
ConnectionConfig config = new ConnectionConfig(address, token, ifName); ConnectionConfig connectionConfig = new ConnectionConfig(address, token);
this.saveConfigurationToFile(config); this.saveConfigurationToFile(connectionConfig);
return config; return connectionConfig;
} }
private void saveConfigurationToFile(ConnectionConfig config) { private void saveConfigurationToFile(ConnectionConfig config){
File dir = new File("config"); File dir = new File("config");
if(!dir.exists() && dir.mkdir()) if(!dir.exists() && dir.mkdir())
log.info("创建用于保存服务端连接信息的目录已完成"); log.info("正在创建配置目录用于保存服务端连接信息...");
File file = new File("config/server.json"); File file = new File("config/server.json");
try(FileWriter writer = new FileWriter(file)) { try(FileWriter writer = new FileWriter(file)) {
writer.write(JSONObject.from(config).toJSONString()); writer.write(JSONObject.from(config).toJSONString());
} catch (IOException e) { } catch (IOException e) {
log.error("保存配置文件时出现问题", e); log.error("保存配置文件时出现问题", e);
} }
log.info("服务端连接信息已保存成功"); log.info("服务端连接信息已保存成功!");
} }
private ConnectionConfig readConfigurationFromFile() { private ConnectionConfig readConfigurationFromFile(){
File configurationFile = new File("config/server.json"); File configFile = new File("config/server.json");
if(configurationFile.exists()) { if(configFile.exists()) {
try (FileInputStream stream = new FileInputStream(configurationFile)){ try (FileInputStream stream = new FileInputStream(configFile)){
String raw = new String(stream.readAllBytes(), StandardCharsets.UTF_8); String raw = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
return JSONObject.parseObject(raw).to(ConnectionConfig.class); return JSONObject.parseObject(raw).to(ConnectionConfig.class);
} catch (IOException e) { } catch (IOException e) {
log.error("读取配置文件时出", e); log.error("读取配置文件时出现问题", e);
} }
} }
return null; return null;

View File

@ -8,5 +8,4 @@ import lombok.Data;
public class ConnectionConfig { public class ConnectionConfig {
String address; String address;
String token; String token;
String networkInterface;
} }

View File

@ -1,22 +1,31 @@
package com.example.entity; package com.example.entity;
import com.alibaba.fastjson2.JSONObject; 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() { public boolean success(){
return code == 200; return code == 200;
} }
public JSONObject asJson() { public JSONObject ofJson() {
return JSONObject.from(data); return JSONObject.from(data);
} }
public String asString() { public String ofString() {
return data.toString(); return data.toString();
} }
public static Response errorResponse(Exception e) { 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;
} }
} }

View File

@ -1,4 +1,4 @@
package com.example.entity; package com.example.entity.data;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;

View File

@ -1,4 +1,4 @@
package com.example.entity; package com.example.entity.data;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;

View File

@ -1,6 +1,6 @@
package com.example.task; package com.example.task;
import com.example.entity.RuntimeDetail; import com.example.entity.data.RuntimeDetail;
import com.example.utils.MonitorUtils; import com.example.utils.MonitorUtils;
import com.example.utils.NetUtils; import com.example.utils.NetUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -20,7 +20,7 @@ public class MonitorJobBean extends QuartzJobBean {
@Override @Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException { protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
RuntimeDetail runtimeDetail = monitor.monitorRuntimeDetail(); RuntimeDetail runtimeDetail = monitor.monitorRuntimeData();
net.updateRuntimeDetails(runtimeDetail); net.updateRuntimeDetails(runtimeDetail);
} }
} }

View File

@ -1,11 +1,8 @@
package com.example.utils; package com.example.utils;
import com.example.entity.BaseDetail; import com.example.entity.data.BaseDetail;
import com.example.entity.ConnectionConfig; import com.example.entity.data.RuntimeDetail;
import com.example.entity.RuntimeDetail;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import oshi.SystemInfo; import oshi.SystemInfo;
import oshi.hardware.CentralProcessor; import oshi.hardware.CentralProcessor;
@ -16,43 +13,26 @@ import oshi.software.os.OperatingSystem;
import java.io.File; import java.io.File;
import java.io.IOException; 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 @Slf4j
@Component @Component
public class MonitorUtils { public class MonitorUtils {
@Lazy
@Resource
ConnectionConfig config;
private final SystemInfo info = new SystemInfo(); private final SystemInfo info = new SystemInfo();
private final Properties properties = System.getProperties(); private final Properties properties = System.getProperties();
public BaseDetail monitorBaseDetail() { public RuntimeDetail monitorRuntimeData() {
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() {
double statisticTime = 0.5; double statisticTime = 0.5;
try { try {
HardwareAbstractionLayer hardware = info.getHardware(); HardwareAbstractionLayer hardware = info.getHardware();
NetworkIF networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware)); NetworkIF networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
CentralProcessor processor = hardware.getProcessor(); CentralProcessor processor = hardware.getProcessor();
//开始读取各种运行时信息
double upload = networkInterface.getBytesSent(), download = networkInterface.getBytesRecv(); double upload = networkInterface.getBytesSent(), download = networkInterface.getBytesRecv();
double read = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum(); double read = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum();
double write = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum(); double write = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum();
@ -60,23 +40,58 @@ public class MonitorUtils {
Thread.sleep((long) (statisticTime * 1000)); Thread.sleep((long) (statisticTime * 1000));
networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware)); networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
upload = (networkInterface.getBytesSent() - upload) / statisticTime; upload = (networkInterface.getBytesSent() - upload) / statisticTime;
download = (networkInterface.getBytesRecv() - download) / statisticTime; download = (networkInterface.getBytesRecv() - download) / statisticTime;
read = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum() - read) / statisticTime; read = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum() - read) / statisticTime;
write = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum() - write) / statisticTime; write = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum() - write) / statisticTime;
double memory = (hardware.getMemory().getTotal() - hardware.getMemory().getAvailable()) / 1024.0 / 1024 / 1024; double memory = (hardware.getMemory().getTotal() - hardware.getMemory().getAvailable()) / 1024.0 / 1024 / 1024;
double disk = Arrays.stream(File.listRoots()) double disk = Arrays.stream(File.listRoots())
.mapToLong(file -> file.getTotalSpace() - file.getFreeSpace()).sum() / 1024.0 / 1024 / 1024; .mapToLong(file -> file.getTotalSpace() - file.getFreeSpace()).sum() / 1024.0 / 1024 / 1024;
return new RuntimeDetail() return new RuntimeDetail()
.setCpuUsage(this.calculateCpuUsage(processor, ticks)) .setCpuUsage(this.calculateCpuUsage(processor, ticks))
.setMemoryUsage(memory) .setMemoryUsage(memory)
.setDiskUsage(disk) .setDiskUsage(disk)
.setNetworkUpload(upload / 1024) .setNetworkUpload(upload / 1024)
.setNetworkDownload(download / 1024) .setNetworkDownload(download / 1024)
.setDiskRead(read / 1024/ 1024) .setDiskRead(read / 1024 / 1024)
.setDiskWrite(write / 1024 / 1024) .setDiskWrite(write / 1024 / 1024)
.setTimestamp(new Date().getTime()); .setTimestamp(new Date().getTime());
} catch (Exception e) { }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; return null;
} }
@ -102,30 +117,4 @@ public class MonitorUtils {
long totalCpu = cUser + nice + cSys + idle + ioWait + irq + softIrq + steal; long totalCpu = cUser + nice + cSys + idle + ioWait + irq + softIrq + steal;
return (cSys + cUser) * 1.0 / totalCpu; 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;
}
} }

View File

@ -1,10 +1,10 @@
package com.example.utils; package com.example.utils;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.example.entity.BaseDetail;
import com.example.entity.ConnectionConfig; import com.example.entity.ConnectionConfig;
import com.example.entity.Response; 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 jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -18,23 +18,56 @@ import java.net.http.HttpResponse;
@Slf4j @Slf4j
@Component @Component
public class NetUtils { public class NetUtils {
private final HttpClient client = HttpClient.newHttpClient(); private final HttpClient client = HttpClient.newHttpClient();
@Lazy @Lazy
@Resource @Resource
ConnectionConfig config; private ConnectionConfig config;
public boolean registerToServer(String address, String token) { public boolean registerToServer(String address, String token) {
log.info("正在服务端注册,请稍后..."); log.info("正在服务端注册,请稍后...");
Response response = this.doGet("/register", address, token); Response response = this.doGet("/register", address, token);
if(response.success()) { if(!response.success()) {
log.info("客户端注册已完成!"); log.error("注册客户端失败: {}", response.getMessage());
} else { } else {
log.error("客户端注册失败: {}", response.message()); log.info("客户端注册已完成!");
} }
return response.success(); 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) { private Response doGet(String url) {
return this.doGet(url, config.getAddress(), config.getToken()); return this.doGet(url, config.getAddress(), config.getToken());
} }
@ -48,39 +81,7 @@ public class NetUtils {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString()); HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return JSONObject.parseObject(response.body()).to(Response.class); return JSONObject.parseObject(response.body()).to(Response.class);
} catch (Exception e) { } catch (Exception e) {
log.error("在发起服务端请求时出现问题", 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);
return Response.errorResponse(e); return Response.errorResponse(e);
} }
} }

View File

@ -12,7 +12,6 @@
<artifactId>itbaima-monitor-server</artifactId> <artifactId>itbaima-monitor-server</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>itbaima-monitor-server</name> <name>itbaima-monitor-server</name>
<description>my-project-backend</description>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
</properties> </properties>
@ -94,7 +93,7 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version> <version>2.1.0</version>
</dependency> </dependency>
<!-- InfluxDB客户端框架 --> <!-- InfluxDB数据库 -->
<dependency> <dependency>
<groupId>com.influxdb</groupId> <groupId>com.influxdb</groupId>
<artifactId>influxdb-client-java</artifactId> <artifactId>influxdb-client-java</artifactId>
@ -109,6 +108,11 @@
<artifactId>jsch</artifactId> <artifactId>jsch</artifactId>
<version>0.1.55</version> <version>0.1.55</version>
</dependency> </dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.12.0</version>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>

View File

@ -29,20 +29,20 @@ public class MonitorController {
@GetMapping("/list") @GetMapping("/list")
public RestBean<List<ClientPreviewVO>> listAllClient(@RequestAttribute(Const.ATTR_USER_ID) int userId, public RestBean<List<ClientPreviewVO>> listAllClient(@RequestAttribute(Const.ATTR_USER_ID) int userId,
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) { @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)) { if(this.isAdminAccount(userRole)) {
return RestBean.success(clients); return RestBean.success(data);
} else { } else {
List<Integer> ids = this.accountAccessClients(userId); return RestBean.success(data.stream()
return RestBean.success(clients.stream() .filter(vo -> clients.contains(vo.getId()))
.filter(vo -> ids.contains(vo.getId()))
.toList()); .toList());
} }
} }
@GetMapping("/simple-list") @GetMapping("/simple-list")
public RestBean<List<ClientSimpleVO>> simpleClientList(@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) { public RestBean<List<ClientSimpleVO>> simpleClientList(@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
if(this.isAdminAccount(userRole)) { if (this.isAdminAccount(userRole)) {
return RestBean.success(service.listSimpleList()); return RestBean.success(service.listSimpleList());
} else { } else {
return RestBean.noPermission(); return RestBean.noPermission();
@ -74,9 +74,9 @@ public class MonitorController {
} }
@GetMapping("/details") @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_ID) int userId,
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) { @RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
if(this.permissionCheck(userId, userRole, clientId)) { if(this.permissionCheck(userId, userRole, clientId)) {
return RestBean.success(service.clientDetails(clientId)); return RestBean.success(service.clientDetails(clientId));
} else { } else {
@ -85,7 +85,7 @@ public class MonitorController {
} }
@GetMapping("/runtime-history") @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_ID) int userId,
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) { @RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
if(this.permissionCheck(userId, userRole, clientId)) { if(this.permissionCheck(userId, userRole, clientId)) {

View File

@ -24,14 +24,14 @@ public class UserController {
public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo, public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo,
@RequestAttribute(Const.ATTR_USER_ID) int userId) { @RequestAttribute(Const.ATTR_USER_ID) int userId) {
return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ? return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ?
RestBean.success() : RestBean.failure(401, "原密码输入错误"); RestBean.success() : RestBean.failure(401, "原密码输入错误");
} }
@PostMapping("/modify-email") @PostMapping("/modify-email")
public RestBean<Void> modifyEmail(@RequestAttribute(Const.ATTR_USER_ID) int id, public RestBean<Void> modifyEmail(@RequestAttribute(Const.ATTR_USER_ID) int id,
@RequestBody @Valid ModifyEmailVO vo) { @RequestBody @Valid ModifyEmailVO vo){
String result = service.modifyEmail(id, vo); String result = service.modifyEmail(id, vo);
if(result == null) { if (result == null) {
return RestBean.success(); return RestBean.success();
} else { } else {
return RestBean.failure(401, result); return RestBean.failure(401, result);
@ -45,8 +45,7 @@ public class UserController {
} }
@GetMapping("/sub/delete") @GetMapping("/sub/delete")
public RestBean<Void> deleteSubAccount(int uid, public RestBean<Void> deleteSubAccount(int uid, @RequestAttribute(Const.ATTR_USER_ID) int userId) {
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
if(uid == userId) if(uid == userId)
return RestBean.failure(401, "非法参数"); return RestBean.failure(401, "非法参数");
service.deleteSubAccount(uid); service.deleteSubAccount(uid);

View File

@ -16,7 +16,7 @@ public class Client implements BaseData {
Integer id; Integer id;
String name; String name;
String token; String token;
String location;
String node; String node;
String location;
Date registerTime; Date registerTime;
} }

View File

@ -11,8 +11,6 @@ import java.time.Instant;
public class RuntimeData { public class RuntimeData {
@Column(tag = true) @Column(tag = true)
int clientId; int clientId;
@Column(timestamp = true)
Instant timestamp;
@Column @Column
double cpuUsage; double cpuUsage;
@Column @Column
@ -27,4 +25,6 @@ public class RuntimeData {
double diskRead; double diskRead;
@Column @Column
double diskWrite; double diskWrite;
@Column(timestamp = true)
Instant timestamp;
} }

View File

@ -1,7 +1,6 @@
package com.example.entity.vo.request; package com.example.entity.vo.request;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Pattern;
import lombok.Data; import lombok.Data;
import org.hibernate.validator.constraints.Length; import org.hibernate.validator.constraints.Length;

View File

@ -1,6 +1,5 @@
package com.example.entity.vo.request; package com.example.entity.vo.request;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;

View File

@ -7,6 +7,7 @@ import org.hibernate.validator.constraints.Length;
@Data @Data
public class SshConnectionVO { public class SshConnectionVO {
int id; int id;
@NotNull
int port; int port;
@NotNull @NotNull
@Length(min = 1) @Length(min = 1)

View File

@ -7,8 +7,8 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
@Data @Data
public class RuntimeHistoryVO { public class RuntimeDetailsVO {
double disk;
double memory; double memory;
double disk;
List<JSONObject> list = new LinkedList<>(); List<JSONObject> list = new LinkedList<>();
} }

View File

@ -2,9 +2,7 @@ package com.example.filter;
import com.auth0.jwt.interfaces.DecodedJWT; import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.entity.RestBean; import com.example.entity.RestBean;
import com.example.entity.dto.Account;
import com.example.entity.dto.Client; import com.example.entity.dto.Client;
import com.example.service.AccountService;
import com.example.service.ClientService; import com.example.service.ClientService;
import com.example.utils.Const; import com.example.utils.Const;
import com.example.utils.JwtUtils; import com.example.utils.JwtUtils;
@ -44,16 +42,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String uri = request.getRequestURI(); String uri = request.getRequestURI();
if(uri.startsWith("/monitor")) { if(uri.startsWith("/monitor")) {
if(!uri.endsWith("/register")) { if(!uri.endsWith("/register")) {
Client client = service.findClientByToken(authorization); Client client = service.findClient(authorization);
if(client == null) { if(client == null) {
response.setStatus(401); response.setStatus(401);
response.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8");
response.getWriter().write(RestBean.failure(401, "注册").asJsonString()); response.getWriter().write(RestBean.failure(401, "授权").asJsonString());
return; return;
} else { } else {
request.setAttribute(Const.ATTR_CLIENT, client); request.setAttribute(Const.ATTR_CLIENT, client);
} }
} }
filterChain.doFilter(request, response);
} else { } else {
DecodedJWT jwt = utils.resolveJwt(authorization); DecodedJWT jwt = utils.resolveJwt(authorization);
if(jwt != null) { if(jwt != null) {
@ -64,30 +63,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication); SecurityContextHolder.getContext().setAuthentication(authentication);
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt)); request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
request.setAttribute(Const.ATTR_USER_ROLE, new ArrayList<>(user.getAuthorities()).get(0).getAuthority()); 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);
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);
} }
} }
} }

View File

@ -29,8 +29,8 @@ public class RequestLogFilter extends OncePerRequestFilter {
@Resource @Resource
SnowflakeIdGenerator generator; SnowflakeIdGenerator generator;
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs", "/monitor/runtime", private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs",
"/api/monitor/list", "/api/monitor/runtime-now"); "/monitor/runtime", "/api/monitor/list", "/api/monitor/runtime");
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

View File

@ -37,9 +37,9 @@ public class MailQueueListener {
"你好,您正在执行重置密码操作,验证码: "+code+"有效时间3分钟如非本人操作请无视。", "你好,您正在执行重置密码操作,验证码: "+code+"有效时间3分钟如非本人操作请无视。",
email); email);
case "modify" -> case "modify" ->
createMessage("您的邮件修改验证邮件", createMessage("您的邮件修改验证邮件",
"您好,您正在绑定新的电子邮件地址,验证码: "+code+"有效时间3分钟如非本人操作请无视", "您好,您正在绑定新的电子邮件地址,验证码: "+code+"有效时间3分钟如非本人操作请无视",
email); email);
default -> null; default -> null;
}; };
if(message == null) return; if(message == null) return;

View File

@ -9,8 +9,8 @@ import java.util.List;
public interface ClientService extends IService<Client> { public interface ClientService extends IService<Client> {
String registerToken(); String registerToken();
Client findClientById(int id); Client findClient(String token);
Client findClientByToken(String token); Client findClient(Integer id);
boolean verifyAndRegister(String token); boolean verifyAndRegister(String token);
void updateClientDetail(ClientDetailVO vo, Client client); void updateClientDetail(ClientDetailVO vo, Client client);
void updateRuntimeDetail(RuntimeDetailVO vo, Client client); void updateRuntimeDetail(RuntimeDetailVO vo, Client client);
@ -19,7 +19,7 @@ public interface ClientService extends IService<Client> {
void renameClient(RenameClientVO vo); void renameClient(RenameClientVO vo);
void renameNode(RenameNodeVO vo); void renameNode(RenameNodeVO vo);
ClientDetailsVO clientDetails(int clientId); ClientDetailsVO clientDetails(int clientId);
RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId); RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId);
RuntimeDetailVO clientRuntimeDetailsNow(int clientId); RuntimeDetailVO clientRuntimeDetailsNow(int clientId);
void deleteClient(int clientId); void deleteClient(int clientId);
void saveClientSshConnection(SshConnectionVO vo); void saveClientSshConnection(SshConnectionVO vo);

View File

@ -13,6 +13,7 @@ import com.example.mapper.AccountMapper;
import com.example.service.AccountService; import com.example.service.AccountService;
import com.example.utils.Const; import com.example.utils.Const;
import com.example.utils.FlowUtils; import com.example.utils.FlowUtils;
import com.example.utils.JwtUtils;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.amqp.core.AmqpTemplate; import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@ -51,6 +52,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Resource @Resource
FlowUtils flow; FlowUtils flow;
@Resource
JwtUtils jwt;
/** /**
* 从数据库中通过用户名或邮箱查找用户详细信息 * 从数据库中通过用户名或邮箱查找用户详细信息
* @param username 用户名 * @param username 用户名
@ -126,7 +130,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
public boolean changePassword(int id, String oldPass, String newPass) { public boolean changePassword(int id, String oldPass, String newPass) {
Account account = this.getById(id); Account account = this.getById(id);
String password = account.getPassword(); String password = account.getPassword();
if(!passwordEncoder.matches(oldPass, password)) if (!passwordEncoder.matches(oldPass, password))
return false; return false;
this.update(Wrappers.<Account>update().eq("id", id) this.update(Wrappers.<Account>update().eq("id", id)
.set("password", passwordEncoder.encode(newPass))); .set("password", passwordEncoder.encode(newPass)));
@ -135,29 +139,25 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Override @Override
public void createSubAccount(CreateSubAccountVO vo) { public void createSubAccount(CreateSubAccountVO vo) {
Account account = this.findAccountByNameOrEmail(vo.getEmail()); Account account = new Account(null, vo.getUsername(),
if(account != null) passwordEncoder.encode(vo.getPassword()),
throw new IllegalArgumentException("该电子邮件已被注册"); vo.getEmail(), Const.ROLE_NORMAL, new Date(),
account = this.findAccountByNameOrEmail(vo.getUsername()); JSONArray.copyOf(vo.getClients()).toJSONString());
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());
this.save(account); this.save(account);
} }
@Override @Override
public void deleteSubAccount(int uid) { public void deleteSubAccount(int uid) {
this.removeById(uid); this.removeById(uid);
jwt.deleteUser(uid);
} }
@Override @Override
public List<SubAccountVO> listSubAccount() { 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 -> { .stream().map(account -> {
SubAccountVO vo = account.asViewObject(SubAccountVO.class); SubAccountVO vo = account.asViewObject(SubAccountVO.class);
vo.setClientList(JSONArray.parse(account.getClients())); vo.setClientList(JSONArray.copyOf(account.getClientList()));
return vo; return vo;
}).toList(); }).toList();
} }
@ -165,11 +165,11 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Override @Override
public String modifyEmail(int id, ModifyEmailVO vo) { public String modifyEmail(int id, ModifyEmailVO vo) {
String code = getEmailVerifyCode(vo.getEmail()); String code = getEmailVerifyCode(vo.getEmail());
if (code == null) return "请先获取验证码"; if(code == null) return "请先获取验证码";
if(!code.equals(vo.getCode())) return "验证码错误,请重新输入"; if(!code.equals(vo.getCode())) return "验证码错误,请重新输入";
this.deleteEmailVerifyCode(vo.getEmail()); this.deleteEmailVerifyCode(vo.getEmail());
Account account = this.findAccountByNameOrEmail(vo.getEmail()); Account account = this.findAccountByNameOrEmail(vo.getEmail());
if(account != null && account.getId() != id) return "该邮箱账号已经被其他账号绑定,无法完成操作"; if(account != null && account.getId() != id) return "该邮件已被其他账号绑定,无法完成操作";
this.update() this.update()
.set("email", vo.getEmail()) .set("email", vo.getEmail())
.eq("id", id) .eq("id", id)

View File

@ -14,62 +14,64 @@ import com.example.service.ClientService;
import com.example.utils.InfluxDbUtils; import com.example.utils.InfluxDbUtils;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Slf4j
@Service @Service
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService { public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
private String registerToken = this.generateNewToken(); private String registerToken = this.generateNewToken();
private final Map<Integer, Client> clientIdCache = new ConcurrentHashMap<>(); private final HashMap<Integer, Client> clientIdCache = new HashMap<>();
private final Map<String, Client> clientTokenCache = new ConcurrentHashMap<>(); private final HashMap<String, Client> clientTokenCache = new HashMap<>();
@Resource @Resource
ClientDetailMapper detailMapper; private ClientDetailMapper detailMapper;
@Resource @Resource
InfluxDbUtils influx; private ClientSshMapper sshMapper;
@Resource @Resource
ClientSshMapper sshMapper; private InfluxDbUtils influx;
@PostConstruct @PostConstruct
public void initClientCache() { public void initClientCache() {
clientTokenCache.clear();
clientIdCache.clear(); clientIdCache.clear();
clientTokenCache.clear();
this.list().forEach(this::addClientCache); 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 @Override
public String registerToken() { public String registerToken() {
return registerToken; return registerToken;
} }
@Override @Override
public Client findClientById(int id) { public synchronized boolean verifyAndRegister(String token) {
return clientIdCache.get(id); if(this.registerToken.equals(token)) {
}
@Override
public Client findClientByToken(String token) {
return clientTokenCache.get(token);
}
@Override
public boolean verifyAndRegister(String token) {
if (this.registerToken.equals(token)) {
int id = this.randomClientId(); 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)) { if(this.save(client)) {
registerToken = this.generateNewToken(); registerToken = this.generateNewToken();
this.addClientCache(client); this.addClientCache(client);
return true; return true;
} }
log.warn("在注册客户端时Token验证成功但客户端数据插入失败请检查问题原因");
} }
return false; 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 @Override
public void updateRuntimeDetail(RuntimeDetailVO vo, Client client) { public void updateRuntimeDetail(RuntimeDetailVO vo, Client client) {
currentRuntime.put(client.getId(), vo); RuntimeDetailVO oldData = lastRuntime.put(client.getId(), vo);
influx.writeRuntimeData(client.getId(), vo); if(oldData != null)
influx.writeRuntimeData(client.getId(), oldData);
} }
@Override @Override
@ -99,7 +102,7 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
return clientIdCache.values().stream().map(client -> { return clientIdCache.values().stream().map(client -> {
ClientPreviewVO vo = client.asViewObject(ClientPreviewVO.class); ClientPreviewVO vo = client.asViewObject(ClientPreviewVO.class);
BeanUtils.copyProperties(detailMapper.selectById(vo.getId()), vo); BeanUtils.copyProperties(detailMapper.selectById(vo.getId()), vo);
RuntimeDetailVO runtime = currentRuntime.get(client.getId()); RuntimeDetailVO runtime = lastRuntime.get(client.getId());
if(this.isOnline(runtime)) { if(this.isOnline(runtime)) {
BeanUtils.copyProperties(runtime, vo); BeanUtils.copyProperties(runtime, vo);
vo.setOnline(true); vo.setOnline(true);
@ -132,36 +135,36 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
@Override @Override
public ClientDetailsVO clientDetails(int clientId) { 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); BeanUtils.copyProperties(detailMapper.selectById(clientId), vo);
vo.setOnline(this.isOnline(currentRuntime.get(clientId))); vo.setOnline(this.isOnline(lastRuntime.get(clientId)));
return vo; return vo;
} }
@Override @Override
public RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId) { public RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId) {
RuntimeHistoryVO vo = influx.readRuntimeData(clientId); RuntimeDetailsVO vo = influx.readRuntimeData(clientId);
ClientDetail detail = detailMapper.selectById(clientId); ClientDetail client = detailMapper.selectById(clientId);
BeanUtils.copyProperties(detail, vo); BeanUtils.copyProperties(client, vo);
return vo; return vo;
} }
@Override @Override
public RuntimeDetailVO clientRuntimeDetailsNow(int clientId) { public RuntimeDetailVO clientRuntimeDetailsNow(int clientId) {
return currentRuntime.get(clientId); return lastRuntime.get(clientId);
} }
@Override @Override
public void deleteClient(int clientId) { public void deleteClient(int clientId) {
this.removeById(clientId); baseMapper.deleteById(clientId);
detailMapper.deleteById(clientId); detailMapper.deleteById(clientId);
this.initClientCache(); this.initClientCache();
currentRuntime.remove(clientId); lastRuntime.remove(clientId);
} }
@Override @Override
public void saveClientSshConnection(SshConnectionVO vo) { public void saveClientSshConnection(SshConnectionVO vo) {
Client client = clientIdCache.get(vo.getId()); Client client = this.getById(vo.getId());
if(client == null) return; if(client == null) return;
ClientSsh ssh = new ClientSsh(); ClientSsh ssh = new ClientSsh();
BeanUtils.copyProperties(vo, ssh); BeanUtils.copyProperties(vo, ssh);
@ -195,7 +198,7 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
clientTokenCache.put(client.getToken(), client); clientTokenCache.put(client.getToken(), client);
} }
private int randomClientId() { private int randomClientId(){
return new Random().nextInt(90000000) + 10000000; return new Random().nextInt(90000000) + 10000000;
} }

View File

@ -7,8 +7,8 @@ public final class Const {
//JWT令牌 //JWT令牌
public final static String JWT_BLACK_LIST = "jwt:blacklist:"; public final static String JWT_BLACK_LIST = "jwt:blacklist:";
public final static String JWT_FREQUENCY = "jwt:frequency:"; 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_COUNTER = "flow:counter:";
public final static String FLOW_LIMIT_BLOCK = "flow:block:"; public final static String FLOW_LIMIT_BLOCK = "flow:block:";

View File

@ -3,7 +3,7 @@ package com.example.utils;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.example.entity.dto.RuntimeData; import com.example.entity.dto.RuntimeData;
import com.example.entity.vo.request.RuntimeDetailVO; 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.InfluxDBClient;
import com.influxdb.client.InfluxDBClientFactory; import com.influxdb.client.InfluxDBClientFactory;
import com.influxdb.client.WriteApiBlocking; import com.influxdb.client.WriteApiBlocking;
@ -34,7 +34,7 @@ public class InfluxDbUtils {
private InfluxDBClient client; private InfluxDBClient client;
@PostConstruct @PostConstruct
public void init() { private void init() {
client = InfluxDBClientFactory.create(url, user, password.toCharArray()); client = InfluxDBClientFactory.create(url, user, password.toCharArray());
} }
@ -47,8 +47,8 @@ public class InfluxDbUtils {
writeApi.writeMeasurement(BUCKET, ORG, WritePrecision.NS, data); writeApi.writeMeasurement(BUCKET, ORG, WritePrecision.NS, data);
} }
public RuntimeHistoryVO readRuntimeData(int clientId) { public RuntimeDetailsVO readRuntimeData(int clientId) {
RuntimeHistoryVO vo = new RuntimeHistoryVO(); RuntimeDetailsVO vo = new RuntimeDetailsVO();
String query = """ String query = """
from(bucket: "%s") from(bucket: "%s")
|> range(start: %s) |> range(start: %s)
@ -64,7 +64,10 @@ public class InfluxDbUtils {
JSONObject object = new JSONObject(); JSONObject object = new JSONObject();
object.put("timestamp", records.get(i).getTime()); object.put("timestamp", records.get(i).getTime());
for (int j = 0; j < size; j++) { 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()); object.put(record.getField(), record.getValue());
} }
vo.getList().add(object); vo.getList().add(object);

View File

@ -141,6 +141,14 @@ public class JwtUtils {
return claims.get("id").asInt(); 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令牌并且采用阶段封禁机制 * 频率检测防止用户高频申请Jwt令牌并且采用阶段封禁机制
* 如果已经提示无法登录的情况下用户还在刷那么就封禁更长时间 * 如果已经提示无法登录的情况下用户还在刷那么就封禁更长时间
@ -178,14 +186,6 @@ public class JwtUtils {
return true; 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黑名单 * 验证Token是否被列入Redis黑名单
* @param uuid 令牌ID * @param uuid 令牌ID

View File

@ -4,6 +4,7 @@ import com.example.entity.dto.ClientDetail;
import com.example.entity.dto.ClientSsh; import com.example.entity.dto.ClientSsh;
import com.example.mapper.ClientDetailMapper; import com.example.mapper.ClientDetailMapper;
import com.example.mapper.ClientSshMapper; import com.example.mapper.ClientSshMapper;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelShell; import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException; import com.jcraft.jsch.JSchException;
@ -28,6 +29,8 @@ import java.util.concurrent.Executors;
@Component @Component
@ServerEndpoint("/terminal/{clientId}") @ServerEndpoint("/terminal/{clientId}")
public class TerminalWebSocket { public class TerminalWebSocket {
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
private final ExecutorService service = Executors.newSingleThreadExecutor();
private static ClientDetailMapper detailMapper; private static ClientDetailMapper detailMapper;
@ -43,28 +46,25 @@ public class TerminalWebSocket {
TerminalWebSocket.sshMapper = sshMapper; TerminalWebSocket.sshMapper = sshMapper;
} }
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
private final ExecutorService service = Executors.newSingleThreadExecutor();
@OnOpen @OnOpen
public void onOpen(Session session, public void onOpen(Session session,
@PathParam(value = "clientId") String clientId) throws Exception { @PathParam(value="clientId") String clientId) throws Exception {
ClientDetail detail = detailMapper.selectById(clientId); ClientDetail detail = detailMapper.selectById(clientId);
ClientSsh ssh = sshMapper.selectById(clientId); ClientSsh ssh = sshMapper.selectById(clientId);
if(detail == null || ssh == null) { if(detail == null || ssh == null) {
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "无法识别此主机")); session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "无法识别此主机"));
return; return;
} }
if(this.createSshConnection(session, ssh, detail.getIp())) { if (this.createSshConnection(session, ssh, detail.getIp())) {
log.info("主机 {} 的SSH连接已创建", detail.getIp()); log.info("主机 {} 的SSH连接已创建", detail.getIp());
} }
} }
@OnMessage @OnMessage
public void onMessage(Session session, String message) throws IOException { public void onMessage(Session session, String message) throws Exception {
Shell shell = sessionMap.get(session); Shell shell = sessionMap.get(session);
OutputStream output = shell.output; OutputStream output = shell.output;
output.write(message.getBytes(StandardCharsets.UTF_8)); output.write(message.getBytes());
output.flush(); output.flush();
} }
@ -84,7 +84,7 @@ public class TerminalWebSocket {
session.close(); session.close();
} }
private boolean createSshConnection(Session session, ClientSsh ssh, String ip) throws IOException{ private boolean createSshConnection(Session session, ClientSsh ssh, String ip) throws IOException {
try { try {
JSch jSch = new JSch(); JSch jSch = new JSch();
com.jcraft.jsch.Session js = jSch.getSession(ssh.getUsername(), ip, ssh.getPort()); com.jcraft.jsch.Session js = jSch.getSession(ssh.getUsername(), ip, ssh.getPort());
@ -118,11 +118,11 @@ public class TerminalWebSocket {
private class Shell { private class Shell {
private final Session session; private final Session session;
private final com.jcraft.jsch.Session js; private final com.jcraft.jsch.Session js;
private final ChannelShell channel; private final Channel channel;
private final InputStream input; private final InputStream input;
private final OutputStream output; 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.js = js;
this.session = session; this.session = session;
this.channel = channel; this.channel = channel;
@ -133,14 +133,14 @@ public class TerminalWebSocket {
private void read() { private void read() {
try { try {
byte[] buffer = new byte[1024 * 1024]; byte[] buffer = new byte[1024];
int i; int i;
while ((i = input.read(buffer)) != -1) { while ((i = input.read(buffer)) != -1) {
String text = new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8); String text = new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8);
session.getBasicRemote().sendText(text); session.getBasicRemote().sendText(text);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("读取SSH输入流时出现问题", e); log.error("读取SSH输入流时出现错误", e);
} }
} }

View File

@ -11,7 +11,7 @@ spring:
mail: mail:
host: smtp.163.com host: smtp.163.com
username: javastudy111@163.com username: javastudy111@163.com
password: VKQFYZMUSUZGSGEG password: AHPYEXHWLAHUCLQE
rabbitmq: rabbitmq:
addresses: localhost addresses: localhost
username: admin username: admin
@ -36,8 +36,8 @@ spring:
verify: verify:
mail-limit: 60 mail-limit: 60
flow: flow:
period: 5 period: 3
limit: 100 limit: 50
block: 30 block: 30
cors: cors:
origin: '*' origin: '*'

View File

@ -22,7 +22,7 @@ spring:
password: admin password: admin
virtual-host: / virtual-host: /
datasource: datasource:
url: jdbc:mysql://localhost:3306/test url: jdbc:mysql://localhost:3306/monitor
username: root username: root
password: 123456 password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.cj.jdbc.Driver
@ -40,8 +40,8 @@ spring:
verify: verify:
mail-limit: 60 mail-limit: 60
flow: flow:
period: 5 period: 3
limit: 100 limit: 20
block: 30 block: 30
cors: cors:
origin: '*' origin: '*'

View File

@ -1,3 +0,0 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

View File

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

View File

@ -2,8 +2,11 @@
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.vscode" />
</content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="index" level="application" />
</component> </component>
</module> </module>

View File

@ -1444,12 +1444,13 @@
} }
}, },
"node_modules/needle": { "node_modules/needle": {
"version": "3.3.1", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/needle/-/needle-3.2.0.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"debug": "^3.2.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"sax": "^1.2.4" "sax": "^1.2.4"
}, },
@ -1460,6 +1461,16 @@
"node": ">= 4.4.x" "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": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",

View File

@ -1,53 +1,54 @@
.is-success { .is-success {
.el-progress-bar__outer { .el-progress-bar__outer {
background-color: #18cb1822 !important; background-color: #18cb1822;
} }
.el-progress-bar__inner { .el-progress-circle__track {
background-color: #18cb18 !important; stroke: #18cb1822;
} }
.el-progress-circle__track { .el-progress-circle__path {
stroke: #18cb1822; stroke: #18cb18 !important;
} }
.el-progress-circle__path { .el-progress-bar__inner {
stroke: #18cb18 !important; background-color: #18cb18 !important;
} }
} }
.is-warning { .is-warning {
.el-progress-bar__outer { .el-progress-bar__outer {
background-color: #ffa04622; background-color: #ffa04622;
} }
.el-progress-circle__track { .el-progress-circle__track {
stroke: #ffa04622; stroke: #ffa04622;
} }
.el-progress-circle__path { .el-progress-circle__path {
stroke: #ffa046 !important; stroke: #ffa046 !important;
} }
.el-progress-bar__inner { .el-progress-bar__inner {
background-color: #ffa046 !important; background-color: #ffa046 !important;
} }
} }
.is-exception { .is-exception {
.el-progress-bar__outer { .el-progress-bar__outer {
background-color: #ef4e4e22; background-color: #ef4e4e22;
} }
.el-progress-circle__track { .el-progress-circle__track {
stroke: #ef4e4e22; stroke: #ef4e4e22;
} }
.el-progress-circle__path { .el-progress-circle__path {
stroke: #ef4e4e !important; stroke: #ef4e4e !important;
} }
.el-progress-bar__inner { .el-progress-bar__inner {
background-color: #ef4e4e !important; background-color: #ef4e4e !important;
} }
} }

View File

@ -1,24 +1,20 @@
<script setup> <script setup>
import {computed, reactive, watch} from "vue"; import {computed, reactive, watch} from "vue";
import {get, post} from "@/net"; 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 {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 {Connection, Delete} from "@element-plus/icons-vue";
import {useStore} from "@/store";
const locations = [ const store = useStore()
{name: 'cn', desc: '中国大陆'}, const locations = store.locations
{name: 'hk', desc: '香港'},
{name: 'jp', desc: '日本'},
{name: 'us', desc: '美国'},
{name: 'sg', desc: '新加坡'},
{name: 'kr', desc: '韩国'},
{name: 'de', desc: '德国'}
]
const props = defineProps({ const props = defineProps({
id: Number, id: Number,
update: Function update: Function,
show: Boolean
}) })
const emits = defineEmits(['delete', 'terminal']) const emits = defineEmits(['delete', 'terminal'])
@ -29,6 +25,7 @@ const details = reactive({
}, },
editNode: false editNode: false
}) })
const nodeEdit = reactive({ const nodeEdit = reactive({
name: '', name: '',
location: '' location: ''
@ -50,11 +47,21 @@ const submitNodeEdit = () => {
}) })
} }
const { copy } = useClipboard()
const copyIp = () => {
copy(details.base.ip).then(() => ElMessage.success('成功复制IP地址到剪贴板'))
}
function updateDetails() {
props.update()
init(props.id)
}
function deleteClient() { function deleteClient() {
ElMessageBox.confirm('删除此主机后所有统计数据都将丢失,您确定要这样做吗?', '删除主机', { ElMessageBox.confirm('删除此主机后所有统计数据都将丢失,您确定要这样做吗?', '删除主机', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
type: 'warning', type: 'warning',
}).then(() => { }).then(() => {
get(`/api/monitor/delete?clientId=${props.id}`, () => { get(`/api/monitor/delete?clientId=${props.id}`, () => {
emits('delete') emits('delete')
@ -64,17 +71,13 @@ function deleteClient() {
}).catch(() => {}) }).catch(() => {})
} }
function updateDetails() {
props.update()
init(props.id)
}
setInterval(() => { setInterval(() => {
if(props.id !== -1 && details.runtime) { if(props.show && details.runtime) {
get(`/api/monitor/runtime-now?clientId=${props.id}`, data => { 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.splice(0, 1)
details.runtime.list.push(data) details.runtime.list.push(data)
}
}) })
} }
}, 10000) }, 10000)
@ -82,31 +85,30 @@ setInterval(() => {
const now = computed(() => details.runtime.list[details.runtime.list.length - 1]) const now = computed(() => details.runtime.list[details.runtime.list.length - 1])
const init = id => { const init = id => {
if(id !== -1) { if(id < 0) return
details.base = {} details.base = {}
details.runtime = { list: [] } details.runtime = { list: [] }
get(`/api/monitor/details?clientId=${id}`, data => Object.assign(details.base, data)) 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 }) watch(() => props.id, init, { immediate: true })
</script> </script>
<template> <template>
<el-scrollbar> <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 v-if="Object.keys(details.base).length">
<div style="display: flex;justify-content: space-between"> <div style="margin-bottom: 15px">
<div class="title"> <el-button :icon="Connection" type="info" size="small" plain
<i class="fa-solid fa-server"></i> @click="emits('terminal', id)">SSH远程连接</el-button>
服务器信息 <el-button :icon="Delete" type="danger" size="small"
</div> @click="deleteClient" plain>删除此主机</el-button>
<div> </div>
<el-button :icon="Connection" type="info" <div class="title">
@click="emits('terminal', id)" plain text>SSH远程连接</el-button> <i class="fa-solid fa-server"></i>
<el-button :icon="Delete" type="danger" style="margin-left: 0" 服务器信息
@click="deleteClient" plain text>删除此主机</el-button>
</div>
</div> </div>
<el-divider style="margin: 10px 0"/> <el-divider style="margin: 10px 0"/>
<div class="details-list"> <div class="details-list">
@ -123,10 +125,10 @@ watch(() => props.id, init, { immediate: true })
<div> <div>
<span>运行状态</span> <span>运行状态</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-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 ? '运行中' : '离线'}} {{details.base.online ? '运行中' : '离线'}}
</span> </span>
</div> </div>
<div v-if="!details.editNode"> <div v-if="!details.editNode">
<span>服务器节点</span> <span>服务器节点</span>
@ -157,7 +159,7 @@ watch(() => props.id, init, { immediate: true })
<span>公网IP地址</span> <span>公网IP地址</span>
<span> <span>
{{details.base.ip}} {{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> </span>
</div> </div>
<div style="display: flex"> <div style="display: flex">
@ -169,17 +171,17 @@ watch(() => props.id, init, { immediate: true })
<div> <div>
<span>硬件配置信息</span> <span>硬件配置信息</span>
<span> <span>
<i class="fa-solid fa-microchip"></i> <i class="fa-solid fa-microchip"></i>
<span style="margin-right: 10px">{{` ${details.base.cpuCore} CPU 核心数 /`}}</span> <span style="margin-right: 10px">{{` ${details.base.cpuCore} CPU 核心数 /`}}</span>
<i class="fa-solid fa-memory"></i> <i class="fa-solid fa-memory"></i>
<span>{{` ${details.base.memory.toFixed(1)} GB 内存容量`}}</span> <span>{{` ${details.base.memory.toFixed(1)} GB 内存容量`}}</span>
</span> </span>
</div> </div>
<div> <div>
<span>操作系统</span> <span>操作系统</span>
<i :style="{color: osNameToIcon(details.base.osName).color}" <i :style="{color: osNameToIcon(details.base.osName).color}"
:class="`fa-brands ${osNameToIcon(details.base.osName).icon}`"></i> :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> </div>
<div class="title" style="margin-top: 20px"> <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: 17px;font-weight: bold;color: initial">CPU</div>
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.cpuUsage * 100).toFixed(1) }}%</div> <div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.cpuUsage * 100).toFixed(1) }}%</div>
</el-progress> </el-progress>
<el-progress style="margin-left: 20px" type="dashboard" :width="100" <el-progress style="margin-left: 20px"
:percentage="now.memoryUsage / details.runtime.memory * 100" type="dashboard" :width="100" :percentage="now.memoryUsage / details.runtime.memory * 100"
:status="percentageToStatus(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: 16px;font-weight: bold;color: initial">内存</div>
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.memoryUsage).toFixed(1) }} GB</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>实时网络速度</div>
<div> <div>
<i style="color: orange" class="fa-solid fa-arrow-up"></i> <i style="color: orange" class="fa-solid fa-arrow-up"></i>
<span>{{` ${fitByUnit(now.networkUpload, 'KB')}/s`}}</span> <span>{{` ${fitToRightByteUnit(now.networkUpload, 'KB')}/s`}}</span>
<el-divider direction="vertical"/> <el-divider style="margin: 0 20px" direction="vertical"/>
<i style="color: dodgerblue" class="fa-solid fa-arrow-down"></i> <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>
<div> <div>
<div style="font-size: 13px;display: flex;justify-content: space-between"> <div style="font-size: 13px;display: flex;justify-content: space-between">
<div> <div>
<i class="fa-solid fa-hard-drive"></i> <i class="fa-solid fa-hard-drive"></i>
<span> 磁盘总容量</span> <span> 磁盘</span>
</div> </div>
<div>{{now.diskUsage.toFixed(1)}} GB / {{details.runtime.disk.toFixed(1)}} GB</div> <div>{{now.diskUsage.toFixed(1)}} GB / {{details.runtime.disk.toFixed(1)}} GB</div>
</div> </div>
<el-progress type="line" :show-text="false" <el-progress type="line" :status="percentageToStatus(now.diskUsage / details.runtime.disk * 100)"
:status="percentageToStatus(now.diskUsage / details.runtime.disk * 100)" :percentage="now.diskUsage / details.runtime.disk * 100" :show-text="false"/>
:percentage="now.diskUsage / details.runtime.disk * 100" />
</div> </div>
</div> </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> </div>
<el-empty description="服务器处于离线状态,请检查服务器是否正常运行" v-else/> <el-empty description="服务器处于离线状态,请检查服务器是否正常运行" v-else/>
</div> </div>
@ -235,16 +237,6 @@ watch(() => props.id, init, { immediate: true })
</template> </template>
<style scoped> <style scoped>
.interact-item {
transition: .3s;
&:hover {
cursor: pointer;
scale: 1.1;
opacity: 0.8;
}
}
.client-details { .client-details {
height: 100%; height: 100%;
padding: 20px; padding: 20px;
@ -258,11 +250,21 @@ watch(() => props.id, init, { immediate: true })
.details-list { .details-list {
font-size: 14px; font-size: 14px;
.interact-item {
transition: .3s;
&:hover {
cursor: pointer;
scale: 1.1;
opacity: 0.8;
}
}
& div { & div {
margin-bottom: 10px; margin-bottom: 10px;
& span:first-child { & span:first-child {
color: gray; color: grey;
font-size: 13px; font-size: 13px;
font-weight: normal; font-weight: normal;
width: 120px; width: 120px;
@ -275,4 +277,12 @@ watch(() => props.id, init, { immediate: true })
} }
} }
} }
span {
color: #5e5e5e;
}
.dark span{
color: #d9d9d9;
}
</style> </style>

View File

@ -1,23 +1,24 @@
<script setup> <script setup>
import {reactive, ref} from "vue";
import {Lock, Message, User} from "@element-plus/icons-vue"; import {Lock, Message, User} from "@element-plus/icons-vue";
import {reactive, ref} from "vue";
import {osNameToIcon} from "@/tools"; import {osNameToIcon} from "@/tools";
import {ElMessage} from "element-plus";
import {post} from "@/net"; import {post} from "@/net";
import {ElMessage} from "element-plus";
defineProps({ defineProps({
clients: Array clients: Array
}) })
const emits = defineEmits(['create']) const emits = defineEmits(['create'])
const formRef = ref()
const valid = ref(false)
const onValidate = (prop, isValid) => valid.value = isValid
const form = reactive({ const form = reactive({
username: '', username: '',
email: '', email: '',
password: '', password: '',
}) })
const formRef = ref()
const valid = ref(false)
const onValidate = (prop, isValid) => valid.value = isValid
const validateUsername = (rule, value, callback) => { const validateUsername = (rule, value, callback) => {
if (value === '') { if (value === '') {
@ -55,7 +56,7 @@ const onCheck = (state, id) => {
} }
function createSubAccount() { function createSubAccount() {
formRef.value.validate(isValid => { formRef.value.validate((isValid) => {
if(checkedClients.length === 0) { if(checkedClients.length === 0) {
ElMessage.warning('请至少选择一个服务器用于子账户进行管理') ElMessage.warning('请至少选择一个服务器用于子账户进行管理')
return return

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import {copyIp, fitByUnit, osNameToIcon, percentageToStatus, rename} from '@/tools' import {copyContent, fitToRightByteUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
const props = defineProps({ defineProps({
data: Object, data: Object,
update: Function update: Function
}) })
@ -11,10 +11,11 @@ const props = defineProps({
<div class="instance-card"> <div class="instance-card">
<div style="display: flex;justify-content: space-between"> <div style="display: flex;justify-content: space-between">
<div> <div>
<div class="name"> <div class="title">
<span :class="`flag-icon flag-icon-${data.location}`"></span> <span :class="`flag-icon flag-icon-${data.location}`"></span>
<span style="margin: 0 5px">{{ data.name }}</span> <span style="margin: 0 10px">{{ data.name }}</span>
<i class="fa-solid fa-pen-to-square interact-item" @click.stop="rename(data.id, data.name, update)"></i> <i @click.stop="rename(data.id, data.name, update)"
class="fa-solid fa-pen-to-square interact-item"/>
</div> </div>
<div class="os"> <div class="os">
操作系统: 操作系统:
@ -35,7 +36,8 @@ const props = defineProps({
<el-divider style="margin: 10px 0"/> <el-divider style="margin: 10px 0"/>
<div class="network"> <div class="network">
<span style="margin-right: 10px">公网IP: {{data.ip}}</span> <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>
<div class="cpu"> <div class="cpu">
<span style="margin-right: 10px">处理器: {{data.cpuName}}</span> <span style="margin-right: 10px">处理器: {{data.cpuName}}</span>
@ -47,62 +49,53 @@ const props = defineProps({
<span>{{` ${data.memory.toFixed(1)} GB`}}</span> <span>{{` ${data.memory.toFixed(1)} GB`}}</span>
</div> </div>
<div class="progress"> <div class="progress">
<span>{{`CPU: ${(data.cpuUsage * 100).toFixed(1)}%`}}</span> <span>{{ `CPU: ${(data.cpuUsage * 100).toFixed(1)} %` }}</span>
<el-progress :status="percentageToStatus(data.cpuUsage * 100)" <el-progress :percentage="data.cpuUsage * 100" :stroke-width="5" :show-text="false"
:percentage="data.cpuUsage * 100" :stroke-width="5" :show-text="false"/> :status="percentageToStatus(data.cpuUsage * 100)"/>
</div> </div>
<div class="progress"> <div class="progress" style="margin-top: 7px">
<span>内存: <b>{{data.memoryUsage.toFixed(1)}}</b> GB</span> <span>内存: <b>{{ data.memoryUsage.toFixed(1) }}</b> GB</span>
<el-progress :status="percentageToStatus(data.memoryUsage/data.memory * 100)" <el-progress :percentage="data.memoryUsage/data.memory * 100" :stroke-width="5" :show-text="false"
:percentage="data.memoryUsage/data.memory * 100" :stroke-width="5" :show-text="false"/> :status="percentageToStatus(data.memoryUsage/data.memory * 100)"/>
</div> </div>
<div class="network-flow"> <div class="network-flow">
<div>网络流量</div> <div>网络流量</div>
<div> <div>
<i class="fa-solid fa-arrow-up"></i> <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"/> <el-divider direction="vertical"/>
<i class="fa-solid fa-arrow-down"></i> <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> </div>
</div> </div>
</template> </template>
<style scoped> <style scoped>
.dark .instance-card { color: #d9d9d9 } .dark .instance-card { color: #d9d9d9
.interact-item {
transition: .3s;
&:hover {
cursor: pointer;
scale: 1.1;
opacity: 0.8;
}
} }
.instance-card { .instance-card {
width: 320px; width: 320px;
padding: 15px;
background-color: var(--el-bg-color); background-color: var(--el-bg-color);
border-radius: 5px; border-radius: 5px;
padding: 15px;
box-sizing: border-box; box-sizing: border-box;
color: #606060; color: #6b6b6b;
transition: .3s; transition: scale .3s;
&:hover { &:hover {
cursor: pointer; cursor: pointer;
scale: 1.02; scale: 1.02;
} }
.name { .interact-item {
font-size: 15px; transition: .3s;
font-weight: bold;
}
.status { &:hover {
font-size: 14px; cursor: pointer;
scale: 1.1;
opacity: 0.8;
}
} }
.os { .os {
@ -110,24 +103,33 @@ const props = defineProps({
color: grey; color: grey;
} }
.status {
font-size: 14px;
}
.title {
font-size: 15px;
font-weight: bold;
}
.network { .network {
font-size: 13px; font-size: 13px;
} }
.cpu {
font-size: 13px;
}
.hardware { .hardware {
margin-top: 5px; margin-top: 5px;
font-size: 13px; font-size: 13px;
} }
.progress { .progress {
margin-top: 10px; margin-top: 15px;
font-size: 12px; font-size: 12px;
} }
.cpu {
font-size: 13px;
}
.network-flow { .network-flow {
margin-top: 10px; margin-top: 10px;
font-size: 12px; font-size: 12px;

View File

@ -3,20 +3,21 @@ import * as echarts from "echarts";
import {onMounted, watch} from "vue"; import {onMounted, watch} from "vue";
import {defaultOption, doubleSeries, singleSeries} from "@/echarts"; import {defaultOption, doubleSeries, singleSeries} from "@/echarts";
const charts = []
const props = defineProps({ const props = defineProps({
data: Object 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) { function updateCpuUsage(list) {
const chart = charts[0] 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)) const option = defaultOption('CPU(%)', localTimeLine(list))
singleSeries(option, 'CPU使用率(%)', data, ['#72c4fe', '#72d5fe', '#2b6fd733']) singleSeries(option, 'CPU使用率(%)', data, ['#72c4fe', '#72d5fe', '#2b6fd733'])
chart.setOption(option) chart.setOption(option);
} }
function updateMemoryUsage(list) { function updateMemoryUsage(list) {
@ -30,8 +31,8 @@ function updateMemoryUsage(list) {
function updateNetworkUsage(list) { function updateNetworkUsage(list) {
const chart = charts[2] const chart = charts[2]
let data = [ let data = [
list.map(item => item.networkUpload), list.map(item => item.networkUpload.toFixed(1)),
list.map(item => item.networkDownload) list.map(item => item.networkDownload.toFixed(1))
] ]
const option = defaultOption('网络(KB/s)', localTimeLine(list)) const option = defaultOption('网络(KB/s)', localTimeLine(list))
doubleSeries(option, ['上传(KB/s)', '下载(KB/s)'], data, [ doubleSeries(option, ['上传(KB/s)', '下载(KB/s)'], data, [
@ -41,6 +42,7 @@ function updateNetworkUsage(list) {
chart.setOption(option); chart.setOption(option);
} }
function updateDiskUsage(list) { function updateDiskUsage(list) {
const chart = charts[3] const chart = charts[3]
let data = [ let data = [

View File

@ -1,9 +1,8 @@
<script setup> <script setup>
import {onBeforeUnmount, onMounted, ref} from "vue"; import {onBeforeUnmount, onMounted, ref} from "vue";
import {ElMessage} from "element-plus";
import {AttachAddon} from "xterm-addon-attach/src/AttachAddon";
import {Terminal} from "xterm"; 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({ const props = defineProps({
id: Number id: Number
@ -40,7 +39,7 @@ const term = new Terminal({
term.loadAddon(attachAddon); term.loadAddon(attachAddon);
onMounted(() => { onMounted(() => {
term.open(terminalRef.value) term.open(terminalRef.value);
term.focus() term.focus()
}) })

View File

@ -1,11 +1,12 @@
<script setup> <script setup>
import {reactive, ref, watch} from "vue"; import {reactive, ref, watch} from "vue";
import {get, post} from "@/net"; import {get, post} from "@/net";
import {ElMessage} from "element-plus"; import "xterm/css/xterm.css";
import Terminal from "@/component/Terminal.vue"; import Terminal from "@/component/Terminal.vue";
const props = defineProps({ const props = defineProps({
id: Number id: Number,
show: Boolean
}) })
const connection = reactive({ const connection = reactive({
@ -15,6 +16,8 @@ const connection = reactive({
password: '' password: ''
}) })
const state = ref(1)
const rules = { const rules = {
port: [ port: [
{ required: true, message: '请输入端口', trigger: ['blur', 'change'] }, { required: true, message: '请输入端口', trigger: ['blur', 'change'] },
@ -27,7 +30,6 @@ const rules = {
] ]
} }
const state = ref(1)
const formRef = ref() const formRef = ref()
function saveConnection() { function saveConnection() {
@ -42,12 +44,13 @@ function saveConnection() {
} }
watch(() => props.id, id => { watch(() => props.id, id => {
if(id === -1) return
connection.ip = ''
state.value = 1 state.value = 1
if(id !== -1) { get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
connection.ip = '' }, {immediate: true})
get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
} watch(() => props.show, () => state.value = 1)
}, { immediate: true })
</script> </script>
<template> <template>

View File

@ -3,9 +3,9 @@ import App from './App.vue'
import router from './router' import router from './router'
import axios from "axios"; import axios from "axios";
import '@/assets/css/element.less'
import 'flag-icon-css/css/flag-icons.min.css' import 'flag-icon-css/css/flag-icons.min.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import '@/assets/css/element.less'
import {createPinia} from "pinia"; import {createPinia} from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

View File

@ -76,9 +76,7 @@ function login(username, password, remember, success, failure = defaultFailure){
}, (data) => { }, (data) => {
storeAccessToken(remember, data.token, data.expire) storeAccessToken(remember, data.token, data.expire)
const store = useStore() const store = useStore()
store.user.role = data.role Object.assign(store.user, data)
store.user.username = data.username
store.user.email = data.email
ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`) ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`)
success(data) success(data)
}, failure) }, failure)

View File

@ -7,7 +7,16 @@ export const useStore = defineStore('general', {
role: '', role: '',
username: '', username: '',
email: '' email: ''
} }, locations: [
{name: 'cn', desc: '中国大陆'},
{name: 'hk', desc: '香港'},
{name: 'jp', desc: '日本'},
{name: 'us', desc: '美国'},
{name: 'sg', desc: '新加坡'},
{name: 'kr', desc: '韩国'},
{name: 'de', desc: '德国'}
]
} }
}, },
getters: { getters: {

View File

@ -2,7 +2,7 @@ import {useClipboard} from "@vueuse/core";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import {post} from "@/net"; import {post} from "@/net";
function fitByUnit(value, unit) { function fitToRightByteUnit(value, unit) {
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'] const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']
let index = units.indexOf(unit) let index = units.indexOf(unit)
while (((value < 1 && value !== 0) || value >= 1024) && (index >= 0 || index < units.length)) { while (((value < 1 && value !== 0) || value >= 1024) && (index >= 0 || index < units.length)) {
@ -51,23 +51,24 @@ function cpuNameToImage(name) {
} }
const { copy } = useClipboard() 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('请输入新的服务器主机名称', '修改名称', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputValue: name,
inputPattern: /^[a-zA-Z0-9_\u4e00-\u9fa5]{1,10}$/,
inputErrorMessage: '名称只能包含中英文字符、数字和下划线',
}).then(({ value }) => post('/api/monitor/rename', {
id: id,
name: value
}, () => {
ElMessage.success('主机名称已更新')
after()
})
)
} }
export { fitByUnit, percentageToStatus, cpuNameToImage, osNameToIcon, rename, copyIp } const rename = (id, name, after) => ElMessageBox.prompt('请输入新的服务器主机名称', '修改名称', {
confirmButtonText: '确认',
cancelButtonText: '取消',
inputValue: name,
inputPattern:
/^[a-zA-Z0-9_\u4e00-\u9fa5]{1,10}$/,
inputErrorMessage: '名称长度不能超过10个字符且只能包含中英文字符、数字和下划线',
}).then(({ value }) => post('/api/monitor/rename', {
id: id,
name: value
}, () => {
ElMessage.success('主机名称已更新')
after()
})
).catch(() => {})
export { fitToRightByteUnit, percentageToStatus, copyContent, rename, osNameToIcon, cpuNameToImage }

View File

@ -4,33 +4,34 @@
<el-image style="height: 30px" <el-image style="height: 30px"
src="https://element-plus.org/images/element-plus-logo.svg"/> src="https://element-plus.org/images/element-plus-logo.svg"/>
<div class="tabs"> <div class="tabs">
<tab-item v-for="item in tabs" :name="item.name" <tab-item v-for="item in tabs"
:active="item.id === tab" @click="changePage(item)"/> :name="item.name" :active="item.id === tab"
<el-switch style="margin: 0 20px" @click="changePage(item)"/>
v-model="dark" active-color="#424242"
:active-action-icon="Moon"
:inactive-action-icon="Sunny"/>
<div style="text-align: right;line-height: 16px;margin-right: 10px">
<div>
<el-tag type="success" v-if="store.isAdmin" size="small">管理员</el-tag>
<el-tag v-else size="small">子账户</el-tag>
{{store.user.username}}
</div>
<div style="font-size: 13px;color: grey">{{store.user.email}}</div>
</div>
<el-dropdown>
<el-avatar class="avatar"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="userLogout">
<el-icon><Back/></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div> </div>
<el-switch style="margin: 0 20px"
v-model="dark" active-color="#424242"
:active-action-icon="Moon"
:inactive-action-icon="Sunny"/>
<div style="text-align: right;line-height: 16px;margin-right: 10px">
<div>
<el-tag type="success" v-if="store.isAdmin" size="small">管理员</el-tag>
<el-tag v-else size="small">子账户</el-tag>
{{store.user.username}}
</div>
<div style="font-size: 13px;color: grey">{{store.user.email}}</div>
</div>
<el-dropdown>
<el-avatar class="avatar"
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"/>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="userLogout">
<el-icon><Back/></el-icon>
退出登录
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-header> </el-header>
<el-main class="main-content"> <el-main class="main-content">
<router-view v-slot="{ Component }"> <router-view v-slot="{ Component }">
@ -48,20 +49,19 @@
import { logout } from '@/net' import { logout } from '@/net'
import router from "@/router"; import router from "@/router";
import {Back, Moon, Sunny} from "@element-plus/icons-vue"; import {Back, Moon, Sunny} from "@element-plus/icons-vue";
import TabItem from "@/component/TabItem.vue";
import {ref} from "vue"; import {ref} from "vue";
import {useDark} from "@vueuse/core"; import {useDark} from "@vueuse/core";
import TabItem from "@/component/TabItem.vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {useStore} from "@/store"; import {useStore} from "@/store";
const store = useStore() const store = useStore()
function userLogout() {
logout(() => router.push("/"))
}
const route = useRoute() const route = useRoute()
const dark = ref(useDark())
const tabs = [
{id: 1, name: '管理', route: 'manage'},
{id: 2, name: '安全', route: 'security'}
]
const defaultIndex = () => { const defaultIndex = () => {
for (let tab of tabs) { for (let tab of tabs) {
if(route.name === tab.route) if(route.name === tab.route)
@ -69,15 +69,17 @@ const defaultIndex = () => {
} }
return 1 return 1
} }
const dark = ref(useDark())
const tabs = [
{id: 1, name: '管理', route: 'manage'},
{id: 2, name: '安全', route: 'security'}
]
const tab = ref(defaultIndex()) const tab = ref(defaultIndex())
function changePage(item) { function changePage(item) {
tab.value = item.id tab.value = item.id
router.push({name: item.route}) router.push({name: item.route})
} }
function userLogout() {
logout(() => router.push("/"))
}
</script> </script>
<style scoped> <style scoped>
@ -95,11 +97,15 @@ function userLogout() {
.tabs { .tabs {
height: 55px; height: 55px;
gap: 10px; gap: 10px;
flex: 1px; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: right; justify-content: right;
} }
.avatar {
border: solid 1px var(--el-border-color);
}
} }
.main-content { .main-content {

View File

@ -1,46 +1,33 @@
<script setup> <script setup>
import PreviewCard from "@/component/PreviewCard.vue"; import PreviewCard from "@/component/PreviewCard.vue";
import {computed, reactive, ref} from "vue";
import {get} from "@/net"; import {get} from "@/net";
import {computed, reactive, ref} from "vue";
import ClientDetails from "@/component/ClientDetails.vue"; import ClientDetails from "@/component/ClientDetails.vue";
import RegisterCard from "@/component/RegisterCard.vue";
import {Plus} from "@element-plus/icons-vue"; import {Plus} from "@element-plus/icons-vue";
import RegisterCard from "@/component/RegisterCard.vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {useStore} from "@/store"; import {useStore} from "@/store";
import TerminalWindow from "@/component/TerminalWindow.vue"; 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 store = useStore()
const route = useRoute() const route = useRoute()
const list = ref([])
const updateList = () => { const checkedNodes = ref([])
if(route.name === 'manage') { const locations = store.locations
get('/api/monitor/list', data => list.value = data)
}
}
setInterval(updateList, 10000)
updateList()
const detail = reactive({ const detail = reactive({
show: false, show: false,
id: -1 id: -1
}) })
const displayClientDetails = (id) => { const displayClientDetails = id => {
detail.show = true
detail.id = 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(() => { const clientList = computed(() => {
if(checkedNodes.value.length === 0) { if(checkedNodes.value.length === 0) {
@ -50,20 +37,21 @@ const clientList = computed(() => {
} }
}) })
const register = reactive({ const updateList = () => {
show: false, if(route.name === 'manage')
token: '' get('/api/monitor/list', data => list.value = data)
}) }
const refreshToken = () => get('/api/monitor/register', token => register.token = token) setInterval(updateList, 10000)
updateList()
function openTerminal(id) { const openTerminal = id => {
terminal.show = true terminal.show = true
terminal.id = id terminal.id = id
detail.show = false detail.show = false
} }
const terminal = reactive({ const terminal = reactive({
show: false, show: false,
id: -1 id: detail.id
}) })
</script> </script>
@ -72,11 +60,11 @@ const terminal = reactive({
<div style="display: flex;justify-content: space-between;align-items: end"> <div style="display: flex;justify-content: space-between;align-items: end">
<div> <div>
<div class="title"><i class="fa-solid fa-server"></i> 管理主机列表</div> <div class="title"><i class="fa-solid fa-server"></i> 管理主机列表</div>
<div class="desc">在这里管理所有已经注册的主机实例实时监控主机运行状态快速进行管理和操作</div> <div class="description">在这里管理所有已经注册的主机实例实时监控主机运行状态快速进行管理和操作</div>
</div> </div>
<div> <div>
<el-button :icon="Plus" type="primary" plain :disabled="!store.isAdmin" <el-button :icon="Plus" type="primary" @click="register.show = true"
@click="register.show = true">添加新主机</el-button> plain :disabled="!store.isAdmin">添加新主机</el-button>
</div> </div>
</div> </div>
<el-divider style="margin: 10px 0"/> <el-divider style="margin: 10px 0"/>
@ -88,22 +76,22 @@ const terminal = reactive({
</el-checkbox> </el-checkbox>
</el-checkbox-group> </el-checkbox-group>
</div> </div>
<div class="card-list" v-if="list.length"> <div class="card-list">
<preview-card v-for="item in clientList" :data="item" :update="updateList" <preview-card :update="updateList" :data="item" v-for="item in clientList"
@click="displayClientDetails(item.id)"/> @click="displayClientDetails(item.id)"/>
</div> </div>
<el-empty description="还没有任何主机哦,点击右上角添加一个吧" v-else/>
<el-drawer size="520" :show-close="false" v-model="detail.show" <el-drawer size="520" :show-close="false" v-model="detail.show"
:with-header="false" v-if="list.length" @close="detail.id = -1"> :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>
<el-drawer v-model="register.show" direction="btt" :with-header="false" <el-drawer style="width: 600px;margin: 10px auto" v-model="register.show"
style="width: 600px;margin: 10px auto" size="320" @open="refreshToken"> direction="btt" :show-close="false" :with-header="false"
:size="320" @open="refreshToken">
<register-card :token="register.token"/> <register-card :token="register.token"/>
</el-drawer> </el-drawer>
<el-drawer style="width: 800px" :size="520" direction="btt" <el-drawer style="width: 800px" :size="520" direction="btt" v-model="terminal.show"
@close="terminal.id = -1" :close-on-click-modal="false" @close="terminal.show = false;terminal.id = -1">
v-model="terminal.show" :close-on-click-modal="false">
<template #header> <template #header>
<div> <div>
<div style="font-size: 18px;color: dodgerblue;font-weight: bold;">SSH远程连接</div> <div style="font-size: 18px;color: dodgerblue;font-weight: bold;">SSH远程连接</div>
@ -112,7 +100,7 @@ const terminal = reactive({
</div> </div>
</div> </div>
</template> </template>
<terminal-window :id="terminal.id"/> <terminal-window :id="terminal.id" :show="terminal.show"/>
</el-drawer> </el-drawer>
</div> </div>
</template> </template>
@ -144,15 +132,24 @@ const terminal = reactive({
font-weight: bold; font-weight: bold;
} }
.desc { .description {
font-size: 15px; font-size: 15px;
color: grey; color: grey;
} }
.card-list {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
} }
.card-list { @keyframes move {
display: flex; from {
gap: 20px; transform: translateX(-100%);
flex-wrap: wrap; }
to {
transform: translateX(0);
}
} }
</style> </style>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import {reactive, ref} from "vue"; import {reactive, ref} from "vue";
import {Delete, Lock, Plus, Refresh, Switch} from "@element-plus/icons-vue";
import {get, logout, post} from "@/net"; import {get, logout, post} from "@/net";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {Delete, Lock, Plus, Refresh, Switch} from "@element-plus/icons-vue";
import router from "@/router"; import router from "@/router";
import CreateSubAccount from "@/component/CreateSubAccount.vue"; import CreateSubAccount from "@/component/CreateSubAccount.vue";
import {useStore} from "@/store"; import {useStore} from "@/store";
@ -19,16 +19,6 @@ const form = reactive({
new_password_repeat: '', 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({ const emailForm = reactive({
email: store.user.email, email: store.user.email,
code: '' 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 = { const rules = {
password: [ password: [
{ required: true, message: '请输入原来的密码', trigger: 'blur' }, { required: true, message: '请输入原来的密码', trigger: 'blur' },
@ -85,29 +85,28 @@ const rules = {
} }
function resetPassword() { function resetPassword() {
formRef.value.validate(isValid => { formRef.value.validate((isValid) => {
if(isValid) { if(isValid) {
post('/api/user/change-password', form, () => { post('/api/user/change-password', form, () => {
ElMessage.success('密码修改成功,请重新登录!') ElMessage.success('密码修改成功,请重新登录')
logout(() => router.push('/')) logout(() => router.push('/'))
}) })
} }
}) })
} }
const simpleList = ref([])
if(store.isAdmin) {
get('/api/monitor/simple-list', list => {
simpleList.value = list
initSubAccounts()
})
}
const accounts = ref([]) const accounts = ref([])
const initSubAccounts = () => const initSubAccounts = () =>
get('/api/user/sub/list', list => accounts.value = list) get('/api/user/sub/list', list => accounts.value = list)
const createAccount = ref(false) const createAccount = ref(false)
const simpleList = ref([])
if(store.isAdmin) {
get('/api/monitor/simple-list', list => {
simpleList.value = list
initSubAccounts()
})
}
function deleteAccount(id) { function deleteAccount(id) {
get(`/api/user/sub/delete?uid=${id}`, () => { get(`/api/user/sub/delete?uid=${id}`, () => {
@ -195,14 +194,15 @@ function deleteAccount(id) {
</div> </div>
<div v-else> <div v-else>
<el-empty :image-size="100" description="还没有任何子用户哦" v-if="store.isAdmin"> <el-empty :image-size="100" description="还没有任何子用户哦" v-if="store.isAdmin">
<el-button :icon="Plus" type="primary" plain <el-button :icon="Plus" type="primary"
@click="createAccount = true">添加子用户</el-button> @click="createAccount = true" plain>添加子用户</el-button>
</el-empty> </el-empty>
<el-empty :image-size="100" description="子账户只能由管理员账号进行操作" v-else/> <el-empty :image-size="100" description="子账户只能由管理员账号进行操作" v-else/>
</div> </div>
</div> </div>
<el-drawer v-model="createAccount" size="350" :with-header="false"> <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> </el-drawer>
</div> </div>
</template> </template>
@ -210,9 +210,9 @@ function deleteAccount(id) {
<style scoped> <style scoped>
.info-card { .info-card {
border-radius: 7px; border-radius: 7px;
height: fit-content;
padding: 15px 20px; padding: 15px 20px;
background-color: var(--el-bg-color); background-color: var(--el-bg-color);
height: fit-content;
.title { .title {
font-size: 18px; font-size: 18px;

View File

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?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"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <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" /> <excludeFolder url="file://$MODULE_DIR$/log" />
</content> </content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@ -11,7 +11,7 @@
Target Server Version : 80200 (8.2.0) Target Server Version : 80200 (8.2.0)
File Encoding : 65001 File Encoding : 65001
Date: 14/12/2023 18:44:11 Date: 05/12/2023 16:27:48
*/ */
SET NAMES utf8mb4; SET NAMES utf8mb4;
@ -27,12 +27,12 @@ CREATE TABLE `db_account` (
`email` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL,
`role` varchar(255) DEFAULT NULL, `role` varchar(255) DEFAULT NULL,
`clients` json DEFAULT NULL,
`register_time` datetime DEFAULT NULL, `register_time` datetime DEFAULT NULL,
`clients` json DEFAULT NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
UNIQUE KEY `unique_email` (`email`), UNIQUE KEY `unique_email` (`email`),
UNIQUE KEY `unique_username` (`username`) 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 -- Table structure for db_client
@ -42,8 +42,8 @@ CREATE TABLE `db_client` (
`id` int NOT NULL, `id` int NOT NULL,
`name` varchar(255) DEFAULT NULL, `name` varchar(255) DEFAULT NULL,
`token` varchar(255) DEFAULT NULL, `token` varchar(255) DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`node` varchar(255) DEFAULT NULL, `node` varchar(255) DEFAULT NULL,
`location` varchar(255) DEFAULT NULL,
`register_time` datetime DEFAULT NULL, `register_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`) PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;