Compare commits
No commits in common. "main" and "dev2" have entirely different histories.
34
.gitignore
vendored
34
.gitignore
vendored
@ -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
|
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -8,5 +8,4 @@ import lombok.Data;
|
|||||||
public class ConnectionConfig {
|
public class ConnectionConfig {
|
||||||
String address;
|
String address;
|
||||||
String token;
|
String token;
|
||||||
String networkInterface;
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
@ -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;
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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)) {
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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<>();
|
||||||
}
|
}
|
@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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:";
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: '*'
|
||||||
|
@ -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: '*'
|
||||||
|
3
itbaima-monitor-web/.vscode/extensions.json
vendored
3
itbaima-monitor-web/.vscode/extensions.json
vendored
@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
# my-project-frontend
|
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
|
||||||
|
|
||||||
## Customize configuration
|
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
@ -2,8 +2,11 @@
|
|||||||
<module type="WEB_MODULE" version="4">
|
<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>
|
17
itbaima-monitor-web/package-lock.json
generated
17
itbaima-monitor-web/package-lock.json
generated
@ -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",
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
@ -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 = [
|
@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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'
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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: {
|
||||||
|
@ -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 }
|
||||||
|
@ -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 {
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user