Compare commits
No commits in common. "dev" and "main" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
.idea/
|
||||
log/
|
||||
.idea
|
||||
log
|
||||
.DS_Store
|
||||
config
|
||||
|
@ -1,25 +0,0 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.vo.request.RegisterServerVO;
|
||||
import com.example.service.ServerService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/server")
|
||||
public class ServerController {
|
||||
|
||||
@Resource
|
||||
ServerService service;
|
||||
|
||||
@PostMapping("/register")
|
||||
public RestBean<Void> register(@Valid @RequestBody RegisterServerVO vo) {
|
||||
return service.registerServer(vo) ?
|
||||
RestBean.success() : RestBean.failure(401, "注册码填写错误,注册主机失败");
|
||||
}
|
||||
}
|
@ -1,46 +0,0 @@
|
||||
package com.example.filter;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.JwtUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 用于对请求头中Jwt令牌进行校验的工具,为当前请求添加用户验证信息
|
||||
* 并将用户的ID存放在请求对象属性中,方便后续使用
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
JwtUtils utils;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
DecodedJWT jwt = utils.resolveJwt(authorization);
|
||||
if(jwt != null) {
|
||||
UserDetails user = utils.toUser(jwt);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.Server;
|
||||
import com.example.entity.vo.request.RegisterServerVO;
|
||||
|
||||
public interface ServerService extends IService<Server> {
|
||||
boolean registerServer(RegisterServerVO vo);
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Server;
|
||||
import com.example.entity.dto.ServerInfo;
|
||||
import com.example.entity.vo.request.RegisterServerVO;
|
||||
import com.example.mapper.ServerInfoMapper;
|
||||
import com.example.mapper.ServerMapper;
|
||||
import com.example.service.ServerService;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Random;
|
||||
|
||||
@Service
|
||||
public class ServerServiceImpl extends ServiceImpl<ServerMapper, Server> implements ServerService {
|
||||
|
||||
@Mapper
|
||||
ServerInfoMapper infoMapper;
|
||||
|
||||
@Override
|
||||
public boolean registerServer(RegisterServerVO vo) {
|
||||
if(this.verifyAccessToken(vo.getAccessToken())) {
|
||||
int id = createRandomId();
|
||||
Server server = new Server(id, "未命名服务器", new Date());
|
||||
ServerInfo info = new ServerInfo();
|
||||
BeanUtils.copyProperties(vo, info);
|
||||
info.setId(id);
|
||||
baseMapper.insert(server);
|
||||
infoMapper.insert(info);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private int createRandomId() {
|
||||
Random random = new Random();
|
||||
return random.nextInt(900000) + 100000;
|
||||
}
|
||||
|
||||
private boolean verifyAccessToken(String token) {
|
||||
return "123456".equals(token);
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
package com.example;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class MyProjectBackendApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
|
||||
}
|
||||
}
|
@ -1,36 +1,45 @@
|
||||
<?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"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<version>3.2.0</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>itbaima-monitor-client</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>my-project-backend</name>
|
||||
<description>my-project-backend</description>
|
||||
<name>itbaima-monitor-client</name>
|
||||
<description>itbaima-monitor-client</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</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>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-quartz</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.37</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.oshi</groupId>
|
||||
@ -38,4 +47,22 @@
|
||||
<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>
|
||||
|
@ -1,11 +1,13 @@
|
||||
package com.test;
|
||||
package com.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MonitorClientApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MonitorClientApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -1,21 +1,15 @@
|
||||
package com.test.config;
|
||||
package com.example.config;
|
||||
|
||||
import com.test.task.MonitorJobBean;
|
||||
import com.example.task.MonitorJobBean;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.quartz.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import oshi.SystemInfo;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class QuartzConfiguration {
|
||||
|
||||
@Bean
|
||||
public SystemInfo info(){
|
||||
return new SystemInfo();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JobDetail jobDetailFactoryBean() {
|
||||
return JobBuilder.newJob(MonitorJobBean.class)
|
||||
@ -26,10 +20,10 @@ public class QuartzConfiguration {
|
||||
|
||||
@Bean
|
||||
public Trigger cronTriggerFactoryBean(JobDetail detail) {
|
||||
CronScheduleBuilder cron= CronScheduleBuilder.cronSchedule("*/1 * * * * ?");
|
||||
CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule("*/10 * * * * ?");
|
||||
return TriggerBuilder.newTrigger()
|
||||
.forJob(detail)
|
||||
.withIdentity("monitor-timer")
|
||||
.withIdentity("monitor-trigger")
|
||||
.withSchedule(cron)
|
||||
.build();
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package com.example.config;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.utils.MonitorUtils;
|
||||
import com.example.utils.NetUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class ServerConfiguration implements ApplicationRunner {
|
||||
|
||||
@Resource
|
||||
NetUtils net;
|
||||
|
||||
@Resource
|
||||
MonitorUtils monitor;
|
||||
|
||||
@Bean
|
||||
ConnectionConfig connectionConfig() {
|
||||
log.info("正在加载服务端连接配置...");
|
||||
ConnectionConfig config = this.readConfigurationFromFile();
|
||||
if(config == null)
|
||||
config = this.registerToServer();
|
||||
return config;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
log.info("正在向服务端更新基本系统信息...");
|
||||
net.updateBaseDetails(monitor.monitorBaseDetail());
|
||||
}
|
||||
|
||||
private ConnectionConfig registerToServer() {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
String token, address, ifName;
|
||||
do {
|
||||
log.info("请输入需要注册的服务端访问地址,地址类似于 'http://192.168.0.22:8080' 这种写法:");
|
||||
address = scanner.nextLine();
|
||||
log.info("请输入服务端生成的用于注册客户端的Token秘钥:");
|
||||
token = scanner.nextLine();
|
||||
List<String> ifs = monitor.listNetworkInterfaceName();
|
||||
if(ifs.size() > 1) {
|
||||
log.info("检测到您的主机有多个网卡设备: {}", ifs);
|
||||
do {
|
||||
log.info("请选择需要监控的设备名称:");
|
||||
ifName = scanner.nextLine();
|
||||
} while (!ifs.contains(ifName));
|
||||
} else {
|
||||
ifName = ifs.get(0);
|
||||
}
|
||||
} while (!net.registerToServer(address, token));
|
||||
ConnectionConfig config = new ConnectionConfig(address, token, ifName);
|
||||
this.saveConfigurationToFile(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
private void saveConfigurationToFile(ConnectionConfig config) {
|
||||
File dir = new File("config");
|
||||
if(!dir.exists() && dir.mkdir())
|
||||
log.info("创建用于保存服务端连接信息的目录已完成");
|
||||
File file = new File("config/server.json");
|
||||
try(FileWriter writer = new FileWriter(file)) {
|
||||
writer.write(JSONObject.from(config).toJSONString());
|
||||
} catch (IOException e) {
|
||||
log.error("保存配置文件时出现问题", e);
|
||||
}
|
||||
log.info("服务端连接信息已保存成功!");
|
||||
}
|
||||
|
||||
private ConnectionConfig readConfigurationFromFile() {
|
||||
File configurationFile = new File("config/server.json");
|
||||
if(configurationFile.exists()) {
|
||||
try (FileInputStream stream = new FileInputStream(configurationFile)){
|
||||
String raw = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
return JSONObject.parseObject(raw).to(ConnectionConfig.class);
|
||||
} catch (IOException e) {
|
||||
log.error("读取配置文件时出错", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -1,15 +1,16 @@
|
||||
package com.test.entity;
|
||||
package com.example.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class SystemData {
|
||||
public class BaseDetail {
|
||||
String osArch;
|
||||
String osName;
|
||||
String osVersion;
|
||||
int osBit;
|
||||
String cpuName;
|
||||
int cpuCore;
|
||||
double memory;
|
||||
double disk;
|
@ -0,0 +1,12 @@
|
||||
package com.example.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class ConnectionConfig {
|
||||
String address;
|
||||
String token;
|
||||
String networkInterface;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.example.entity;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
|
||||
public record Response(int id, int code, Object data, String message) {
|
||||
|
||||
public boolean success() {
|
||||
return code == 200;
|
||||
}
|
||||
|
||||
public JSONObject asJson() {
|
||||
return JSONObject.from(data);
|
||||
}
|
||||
|
||||
public String asString() {
|
||||
return data.toString();
|
||||
}
|
||||
|
||||
public static Response errorResponse(Exception e) {
|
||||
return new Response(0, 500, null, e.getMessage());
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.example.entity;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class RuntimeDetail {
|
||||
long timestamp;
|
||||
double cpuUsage;
|
||||
double memoryUsage;
|
||||
double diskUsage;
|
||||
double networkUpload;
|
||||
double networkDownload;
|
||||
double diskRead;
|
||||
double diskWrite;
|
||||
}
|
@ -1,5 +1,9 @@
|
||||
package com.test.task;
|
||||
package com.example.task;
|
||||
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import com.example.utils.MonitorUtils;
|
||||
import com.example.utils.NetUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.quartz.JobExecutionContext;
|
||||
import org.quartz.JobExecutionException;
|
||||
import org.springframework.scheduling.quartz.QuartzJobBean;
|
||||
@ -8,8 +12,15 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class MonitorJobBean extends QuartzJobBean {
|
||||
|
||||
@Resource
|
||||
MonitorUtils monitor;
|
||||
|
||||
@Resource
|
||||
NetUtils net;
|
||||
|
||||
@Override
|
||||
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
|
||||
|
||||
RuntimeDetail runtimeDetail = monitor.monitorRuntimeDetail();
|
||||
net.updateRuntimeDetails(runtimeDetail);
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.example.entity.BaseDetail;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.CentralProcessor;
|
||||
import oshi.hardware.HWDiskStore;
|
||||
import oshi.hardware.HardwareAbstractionLayer;
|
||||
import oshi.hardware.NetworkIF;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MonitorUtils {
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
ConnectionConfig config;
|
||||
|
||||
private final SystemInfo info = new SystemInfo();
|
||||
private final Properties properties = System.getProperties();
|
||||
|
||||
public BaseDetail monitorBaseDetail() {
|
||||
OperatingSystem os = info.getOperatingSystem();
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 /1024;
|
||||
double diskSize = Arrays.stream(File.listRoots()).mapToLong(File::getTotalSpace).sum() / 1024.0 / 1024 / 1024;
|
||||
String ip = Objects.requireNonNull(this.findNetworkInterface(hardware)).getIPv4addr()[0];
|
||||
return new BaseDetail()
|
||||
.setOsArch(properties.getProperty("os.arch"))
|
||||
.setOsName(os.getFamily())
|
||||
.setOsVersion(os.getVersionInfo().getVersion())
|
||||
.setOsBit(os.getBitness())
|
||||
.setCpuName(hardware.getProcessor().getProcessorIdentifier().getName())
|
||||
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
|
||||
.setMemory(memory)
|
||||
.setDisk(diskSize)
|
||||
.setIp(ip);
|
||||
}
|
||||
|
||||
public RuntimeDetail monitorRuntimeDetail() {
|
||||
double statisticTime = 0.5;
|
||||
try {
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
NetworkIF networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
|
||||
CentralProcessor processor = hardware.getProcessor();
|
||||
double upload = networkInterface.getBytesSent(), download = networkInterface.getBytesRecv();
|
||||
double read = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum();
|
||||
double write = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum();
|
||||
long[] ticks = processor.getSystemCpuLoadTicks();
|
||||
Thread.sleep((long) (statisticTime * 1000));
|
||||
networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
|
||||
upload = (networkInterface.getBytesSent() - upload) / statisticTime;
|
||||
download = (networkInterface.getBytesRecv() - download) / statisticTime;
|
||||
read = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum() - read) / statisticTime;
|
||||
write = (hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum() - write) / statisticTime;
|
||||
double memory = (hardware.getMemory().getTotal() - hardware.getMemory().getAvailable()) / 1024.0 / 1024 / 1024;
|
||||
double disk = Arrays.stream(File.listRoots())
|
||||
.mapToLong(file -> file.getTotalSpace() - file.getFreeSpace()).sum() / 1024.0 / 1024 / 1024;
|
||||
return new RuntimeDetail()
|
||||
.setCpuUsage(this.calculateCpuUsage(processor, ticks))
|
||||
.setMemoryUsage(memory)
|
||||
.setDiskUsage(disk)
|
||||
.setNetworkUpload(upload / 1024)
|
||||
.setNetworkDownload(download / 1024)
|
||||
.setDiskRead(read / 1024/ 1024)
|
||||
.setDiskWrite(write / 1024 / 1024)
|
||||
.setTimestamp(new Date().getTime());
|
||||
} catch (Exception e) {
|
||||
log.error("读取运行时数据出现问题", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private double calculateCpuUsage(CentralProcessor processor, long[] prevTicks) {
|
||||
long[] ticks = processor.getSystemCpuLoadTicks();
|
||||
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.NICE.getIndex()];
|
||||
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.IRQ.getIndex()];
|
||||
long softIrq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
|
||||
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.STEAL.getIndex()];
|
||||
long cSys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
|
||||
long cUser = ticks[CentralProcessor.TickType.USER.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.USER.getIndex()];
|
||||
long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
|
||||
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()]
|
||||
- prevTicks[CentralProcessor.TickType.IDLE.getIndex()];
|
||||
long totalCpu = cUser + nice + cSys + idle + ioWait + irq + softIrq + steal;
|
||||
return (cSys + cUser) * 1.0 / totalCpu;
|
||||
}
|
||||
|
||||
public List<String> listNetworkInterfaceName() {
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
return hardware.getNetworkIFs()
|
||||
.stream()
|
||||
.map(NetworkIF::getName)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private NetworkIF findNetworkInterface(HardwareAbstractionLayer hardware) {
|
||||
try {
|
||||
String target = config.getNetworkInterface();
|
||||
List<NetworkIF> ifs = hardware.getNetworkIFs()
|
||||
.stream()
|
||||
.filter(inter -> inter.getName().equals(target))
|
||||
.toList();
|
||||
if (!ifs.isEmpty()) {
|
||||
return ifs.get(0);
|
||||
} else {
|
||||
throw new IOException("网卡信息错误,找不到网卡: " + target);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("读取网络接口信息时出错", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.BaseDetail;
|
||||
import com.example.entity.ConnectionConfig;
|
||||
import com.example.entity.Response;
|
||||
import com.example.entity.RuntimeDetail;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class NetUtils {
|
||||
private final HttpClient client = HttpClient.newHttpClient();
|
||||
|
||||
@Lazy
|
||||
@Resource
|
||||
ConnectionConfig config;
|
||||
|
||||
public boolean registerToServer(String address, String token) {
|
||||
log.info("正在像服务端注册,请稍后...");
|
||||
Response response = this.doGet("/register", address, token);
|
||||
if(response.success()) {
|
||||
log.info("客户端注册已完成!");
|
||||
} else {
|
||||
log.error("客户端注册失败: {}", response.message());
|
||||
}
|
||||
return response.success();
|
||||
}
|
||||
|
||||
private Response doGet(String url) {
|
||||
return this.doGet(url, config.getAddress(), config.getToken());
|
||||
}
|
||||
|
||||
private Response doGet(String url, String address, String token) {
|
||||
try {
|
||||
HttpRequest request = HttpRequest.newBuilder().GET()
|
||||
.uri(new URI(address + "/monitor" + url))
|
||||
.header("Authorization", token)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package com.test.util;
|
||||
|
||||
public interface DataGetter<T> {
|
||||
T getData();
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
package com.test.util;
|
||||
|
||||
import com.test.entity.SystemData;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import oshi.SystemInfo;
|
||||
import oshi.hardware.HWDiskStore;
|
||||
import oshi.hardware.HardwareAbstractionLayer;
|
||||
import oshi.hardware.NetworkIF;
|
||||
import oshi.software.os.OperatingSystem;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.NetworkInterface;
|
||||
import java.util.Properties;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SystemDataGetter implements DataGetter<SystemData>{
|
||||
|
||||
@Resource
|
||||
SystemInfo info;
|
||||
|
||||
private final Properties properties = System.getProperties();
|
||||
|
||||
public SystemData getData() {
|
||||
OperatingSystem os = info.getOperatingSystem();
|
||||
HardwareAbstractionLayer hardware = info.getHardware();
|
||||
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 / 1024;
|
||||
double diskSize = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getSize).sum() / 1024.0 / 1024 / 1024;
|
||||
try {
|
||||
String ip = null;
|
||||
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) {
|
||||
ip = network.getIPv4addr()[0];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new SystemData()
|
||||
.setOsBit(os.getBitness())
|
||||
.setOsArch(properties.getProperty("os.arch"))
|
||||
.setOsVersion(os.getVersionInfo().getVersion())
|
||||
.setOsName(os.getFamily())
|
||||
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
|
||||
.setMemory(memory)
|
||||
.setDisk(diskSize)
|
||||
.setIp(ip);
|
||||
} catch (IOException e) {
|
||||
log.error("读取系统网络配置时出错", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
|
@ -1,109 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="instance-card">
|
||||
<div style="display: flex;justify-content: space-between">
|
||||
<div>
|
||||
<div class="title">
|
||||
<span class="flag-icon flag-icon-cn"></span>
|
||||
<span style="margin: 0 10px">柏码后端云服务器</span>
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</div>
|
||||
<div class="os">
|
||||
操作系统: Ubuntu 20.04
|
||||
</div>
|
||||
</div>
|
||||
<div class="status">
|
||||
<i style="color: #18cb18" class="fa-solid fa-circle-play"></i>
|
||||
<span style="margin-left: 5px">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div class="network">
|
||||
<span style="margin-right: 10px">公网IP: 192.168.0.10</span>
|
||||
<i style="color: dodgerblue" class="fa-solid fa-copy"></i>
|
||||
</div>
|
||||
<div class="hardware">
|
||||
<i class="fa-solid fa-microchip"></i>
|
||||
<span style="margin-right: 10px"> 2 CPU</span>
|
||||
<i class="fa-solid fa-memory"></i>
|
||||
<span> 4 GB</span>
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div class="progress">
|
||||
<span>CPU: 2.5 %</span>
|
||||
<el-progress :percentage="2.5" :stroke-width="5" :show-text="false"/>
|
||||
</div>
|
||||
<div class="progress" style="margin-top: 7px">
|
||||
<span>内存: <b>1.2</b> GB</span>
|
||||
<el-progress :percentage="1.2/4 * 100" :stroke-width="5" :show-text="false"/>
|
||||
</div>
|
||||
<div class="network-flow">
|
||||
<div>网络流量</div>
|
||||
<div>
|
||||
<i class="fa-solid fa-arrow-up"></i>
|
||||
<span> 52 KB/s</span>
|
||||
<el-divider direction="vertical"/>
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
<span> 272 KB/s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.el-progress-bar__outer) {
|
||||
background-color: #18cb1822;
|
||||
}
|
||||
|
||||
:deep(.el-progress-bar__inner) {
|
||||
background-color: #18cb18;
|
||||
}
|
||||
|
||||
.dark .instance-card { color: #d9d9d9
|
||||
}
|
||||
.instance-card {
|
||||
width: 320px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-radius: 5px;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
color: #6b6b6b;
|
||||
|
||||
.os {
|
||||
font-size: 13px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.network {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.hardware {
|
||||
margin-top: 5px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.network-flow {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<el-container class="main-container">
|
||||
<el-header class="main-header">
|
||||
<el-image style="height: 30px"
|
||||
src="https://element-plus.org/images/element-plus-logo.svg"/>
|
||||
<div class="tabs">
|
||||
<tab-item v-for="item in tabs"
|
||||
:name="item.name" :active="item.id === tab"
|
||||
@click="changePage(item)"/>
|
||||
</div>
|
||||
<el-switch style="margin: 0 20px"
|
||||
v-model="dark" active-color="#424242"
|
||||
:active-action-icon="Moon"
|
||||
:inactive-action-icon="Sunny"/>
|
||||
<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-main class="main-content">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="el-fade-in-linear" mode="out-in">
|
||||
<component :is="Component"/>
|
||||
</transition>
|
||||
</router-view>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { logout } from '@/net'
|
||||
import router from "@/router";
|
||||
import {ref} from "vue";
|
||||
import TabItem from "@/component/TabItem.vue";
|
||||
import {Back, Moon, Sunny} from "@element-plus/icons-vue";
|
||||
import {useDark} from "@vueuse/core";
|
||||
|
||||
function userLogout() {
|
||||
logout(() => router.push("/"))
|
||||
}
|
||||
|
||||
const dark = ref(useDark())
|
||||
const tab = ref(1)
|
||||
const tabs = [
|
||||
{id: 1, name: '管理', route: 'manage'},
|
||||
{id: 2, name: '安全', route: 'security'}
|
||||
]
|
||||
function changePage(item) {
|
||||
tab.value = item.id
|
||||
router.push({name: item.route})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.main-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
|
||||
.main-header {
|
||||
height: 55px;
|
||||
background-color: var(--el-bg-color);
|
||||
border-bottom: solid 1px var(--el-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tabs {
|
||||
height: 55px;
|
||||
gap: 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: right;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border: solid 1px var(--el-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: 100%;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .main-content {
|
||||
background-color: #232323;
|
||||
}
|
||||
</style>
|
@ -1,33 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
import PreviewCard from "@/component/PreviewCard.vue";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="manage-main">
|
||||
<div class="title"><i class="fa-solid fa-server"></i> 管理主机列表</div>
|
||||
<div class="description">在这里管理所有已经注册的主机实例,实时监控主机运行状态,快速进行管理和操作。</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div style="display: flex;gap: 20px">
|
||||
<preview-card/>
|
||||
<preview-card/>
|
||||
<preview-card/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.manage-main {
|
||||
margin: 0 50px;
|
||||
|
||||
.title {
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 15px;
|
||||
color: grey;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -1,11 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -2,7 +2,9 @@
|
||||
<module type="GENERAL_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/log" />
|
||||
</content>
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -9,9 +9,9 @@
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>itbaima-monitor-backend</artifactId>
|
||||
<artifactId>itbaima-monitor-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>my-project-backend</name>
|
||||
<name>itbaima-monitor-server</name>
|
||||
<description>my-project-backend</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
@ -94,6 +94,21 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
<!-- InfluxDB客户端框架 -->
|
||||
<dependency>
|
||||
<groupId>com.influxdb</groupId>
|
||||
<artifactId>influxdb-client-java</artifactId>
|
||||
<version>6.6.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.jcraft</groupId>
|
||||
<artifactId>jsch</artifactId>
|
||||
<version>0.1.55</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MyProjectBackendApplication {
|
||||
public class MonitorServerApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MyProjectBackendApplication.class, args);
|
||||
SpringApplication.run(MonitorServerApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -53,9 +53,12 @@ public class SecurityConfiguration {
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(conf -> conf
|
||||
.requestMatchers("/terminal/**").permitAll()
|
||||
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||
.requestMatchers("/monitor/**").permitAll()
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
||||
.requestMatchers("/api/user/sub/**").hasRole(Const.ROLE_ADMIN)
|
||||
.anyRequest().hasAnyRole(Const.ROLE_ADMIN, Const.ROLE_NORMAL)
|
||||
)
|
||||
.formLogin(conf -> conf
|
||||
.loginProcessingUrl("/api/auth/login")
|
@ -0,0 +1,13 @@
|
||||
package com.example.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||
|
||||
@Configuration
|
||||
public class WebSocketConfiguration {
|
||||
@Bean
|
||||
public ServerEndpointExporter serverEndpointExporter() {
|
||||
return new ServerEndpointExporter();
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ public class AuthorizeController {
|
||||
@GetMapping("/ask-code")
|
||||
@Operation(summary = "请求邮件验证码")
|
||||
public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
|
||||
@RequestParam @Pattern(regexp = "(reset)") String type,
|
||||
@RequestParam @Pattern(regexp = "(reset|modify)") String type,
|
||||
HttpServletRequest request){
|
||||
return this.messageHandle(() ->
|
||||
accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr()));
|
@ -0,0 +1,39 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.dto.Client;
|
||||
import com.example.entity.vo.request.ClientDetailVO;
|
||||
import com.example.entity.vo.request.RuntimeDetailVO;
|
||||
import com.example.service.ClientService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/monitor")
|
||||
public class ClientController {
|
||||
|
||||
@Resource
|
||||
ClientService service;
|
||||
|
||||
@GetMapping("/register")
|
||||
public RestBean<Void> registerClient(@RequestHeader("Authorization") String token) {
|
||||
return service.verifyAndRegister(token) ?
|
||||
RestBean.success() : RestBean.failure(401, "客户端注册失败,请检查Token是否正确");
|
||||
}
|
||||
|
||||
@PostMapping("/detail")
|
||||
public RestBean<Void> updateClientDetails(@RequestAttribute(Const.ATTR_CLIENT) Client client,
|
||||
@RequestBody @Valid ClientDetailVO vo) {
|
||||
service.updateClientDetail(vo, client);
|
||||
return RestBean.success();
|
||||
}
|
||||
|
||||
@PostMapping("/runtime")
|
||||
public RestBean<Void> updateRuntimeDetails(@RequestAttribute(Const.ATTR_CLIENT) Client client,
|
||||
@RequestBody @Valid RuntimeDetailVO vo) {
|
||||
service.updateRuntimeDetail(vo, client);
|
||||
return RestBean.success();
|
||||
}
|
||||
}
|
@ -0,0 +1,166 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.request.RenameClientVO;
|
||||
import com.example.entity.vo.request.RenameNodeVO;
|
||||
import com.example.entity.vo.request.RuntimeDetailVO;
|
||||
import com.example.entity.vo.request.SshConnectionVO;
|
||||
import com.example.entity.vo.response.*;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.service.ClientService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/monitor")
|
||||
public class MonitorController {
|
||||
|
||||
@Resource
|
||||
ClientService service;
|
||||
|
||||
@Resource
|
||||
AccountService accountService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public RestBean<List<ClientPreviewVO>> listAllClient(@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
List<ClientPreviewVO> clients = service.listClients();
|
||||
if(this.isAdminAccount(userRole)) {
|
||||
return RestBean.success(clients);
|
||||
} else {
|
||||
List<Integer> ids = this.accountAccessClients(userId);
|
||||
return RestBean.success(clients.stream()
|
||||
.filter(vo -> ids.contains(vo.getId()))
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/simple-list")
|
||||
public RestBean<List<ClientSimpleVO>> simpleClientList(@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.isAdminAccount(userRole)) {
|
||||
return RestBean.success(service.listSimpleList());
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/rename")
|
||||
public RestBean<Void> renameClient(@RequestBody @Valid RenameClientVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, vo.getId())) {
|
||||
service.renameClient(vo);
|
||||
return RestBean.success();
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/node")
|
||||
public RestBean<Void> renameNode(@RequestBody @Valid RenameNodeVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, vo.getId())) {
|
||||
service.renameNode(vo);
|
||||
return RestBean.success();
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/details")
|
||||
public RestBean<ClientDetailsVO> details(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
return RestBean.success(service.clientDetails(clientId));
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/runtime-history")
|
||||
public RestBean<RuntimeHistoryVO> runtimeDetailsHistory(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
return RestBean.success(service.clientRuntimeDetailsHistory(clientId));
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/runtime-now")
|
||||
public RestBean<RuntimeDetailVO> runtimeDetailsNow(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
return RestBean.success(service.clientRuntimeDetailsNow(clientId));
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/register")
|
||||
public RestBean<String> registerToken(@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if (this.isAdminAccount(userRole)) {
|
||||
return RestBean.success(service.registerToken());
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/delete")
|
||||
public RestBean<String> deleteClient(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if (this.isAdminAccount(userRole)) {
|
||||
service.deleteClient(clientId);
|
||||
return RestBean.success();
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/ssh-save")
|
||||
public RestBean<Void> saveSshConnection(@RequestBody @Valid SshConnectionVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, vo.getId())) {
|
||||
service.saveClientSshConnection(vo);
|
||||
return RestBean.success();
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/ssh")
|
||||
public RestBean<SshSettingsVO> sshSettings(int clientId,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId,
|
||||
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
|
||||
if(this.permissionCheck(userId, userRole, clientId)) {
|
||||
return RestBean.success(service.sshSettings(clientId));
|
||||
} else {
|
||||
return RestBean.noPermission();
|
||||
}
|
||||
}
|
||||
|
||||
private List<Integer> accountAccessClients(int uid) {
|
||||
Account account = accountService.getById(uid);
|
||||
return account.getClientList();
|
||||
}
|
||||
|
||||
private boolean isAdminAccount(String role) {
|
||||
role = role.substring(5);
|
||||
return Const.ROLE_ADMIN.equals(role);
|
||||
}
|
||||
|
||||
private boolean permissionCheck(int uid, String role, int clientId) {
|
||||
if(this.isAdminAccount(role)) return true;
|
||||
return this.accountAccessClients(uid).contains(clientId);
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.vo.request.ChangePasswordVO;
|
||||
import com.example.entity.vo.request.CreateSubAccountVO;
|
||||
import com.example.entity.vo.request.ModifyEmailVO;
|
||||
import com.example.entity.vo.response.SubAccountVO;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
public class UserController {
|
||||
|
||||
@Resource
|
||||
AccountService service;
|
||||
|
||||
@PostMapping("/change-password")
|
||||
public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
|
||||
return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ?
|
||||
RestBean.success() : RestBean.failure(401, "原密码输入错误!");
|
||||
}
|
||||
|
||||
@PostMapping("/modify-email")
|
||||
public RestBean<Void> modifyEmail(@RequestAttribute(Const.ATTR_USER_ID) int id,
|
||||
@RequestBody @Valid ModifyEmailVO vo) {
|
||||
String result = service.modifyEmail(id, vo);
|
||||
if(result == null) {
|
||||
return RestBean.success();
|
||||
} else {
|
||||
return RestBean.failure(401, result);
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/sub/create")
|
||||
public RestBean<Void> createSubAccount(@RequestBody @Valid CreateSubAccountVO vo) {
|
||||
service.createSubAccount(vo);
|
||||
return RestBean.success();
|
||||
}
|
||||
|
||||
@GetMapping("/sub/delete")
|
||||
public RestBean<Void> deleteSubAccount(int uid,
|
||||
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
|
||||
if(uid == userId)
|
||||
return RestBean.failure(401, "非法参数");
|
||||
service.deleteSubAccount(uid);
|
||||
return RestBean.success();
|
||||
}
|
||||
|
||||
@GetMapping("/sub/list")
|
||||
public RestBean<List<SubAccountVO>> subAccountList() {
|
||||
return RestBean.success(service.listSubAccount());
|
||||
}
|
||||
}
|
@ -34,6 +34,10 @@ public record RestBean<T> (long id, int code, T data, String message) {
|
||||
return new RestBean<>(requestId(), code, null, message);
|
||||
}
|
||||
|
||||
public static <T> RestBean<T> noPermission() {
|
||||
return new RestBean<>(requestId(), 401, null, "权限不足,拒绝访问");
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速将当前实体转换为JSON字符串格式
|
||||
* @return JSON字符串
|
@ -1,5 +1,6 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
@ -7,7 +8,9 @@ import com.example.entity.BaseData;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 数据库中的用户信息
|
||||
@ -23,4 +26,10 @@ public class Account implements BaseData {
|
||||
String email;
|
||||
String role;
|
||||
Date registerTime;
|
||||
String clients;
|
||||
|
||||
public List<Integer> getClientList() {
|
||||
if(clients == null) return Collections.emptyList();
|
||||
return JSONArray.parse(clients).toList(Integer.class);
|
||||
}
|
||||
}
|
@ -2,17 +2,21 @@ package com.example.entity.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.example.entity.BaseData;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@TableName("db_client")
|
||||
@AllArgsConstructor
|
||||
@TableName("db_server")
|
||||
public class Server {
|
||||
public class Client implements BaseData {
|
||||
@TableId
|
||||
int id;
|
||||
Integer id;
|
||||
String name;
|
||||
Date time;
|
||||
String token;
|
||||
String location;
|
||||
String node;
|
||||
Date registerTime;
|
||||
}
|
@ -5,14 +5,15 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("db_server_info")
|
||||
public class ServerInfo {
|
||||
@TableName("db_client_detail")
|
||||
public class ClientDetail {
|
||||
@TableId
|
||||
int id;
|
||||
Integer id;
|
||||
String osArch;
|
||||
String osName;
|
||||
String osVersion;
|
||||
int osBit;
|
||||
String cpuName;
|
||||
int cpuCore;
|
||||
double memory;
|
||||
double disk;
|
@ -0,0 +1,16 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.example.entity.BaseData;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@TableName("db_client_ssh")
|
||||
public class ClientSsh implements BaseData {
|
||||
@TableId
|
||||
Integer id;
|
||||
Integer port;
|
||||
String username;
|
||||
String password;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.influxdb.annotations.Column;
|
||||
import com.influxdb.annotations.Measurement;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@Measurement(name = "runtime")
|
||||
public class RuntimeData {
|
||||
@Column(tag = true)
|
||||
int clientId;
|
||||
@Column(timestamp = true)
|
||||
Instant timestamp;
|
||||
@Column
|
||||
double cpuUsage;
|
||||
@Column
|
||||
double memoryUsage;
|
||||
@Column
|
||||
double diskUsage;
|
||||
@Column
|
||||
double networkUpload;
|
||||
@Column
|
||||
double networkDownload;
|
||||
@Column
|
||||
double diskRead;
|
||||
@Column
|
||||
double diskWrite;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class ChangePasswordVO {
|
||||
@Length(min = 6, max = 20)
|
||||
String password;
|
||||
@Length(min = 6, max = 20)
|
||||
String new_password;
|
||||
}
|
@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RegisterServerVO {
|
||||
public class ClientDetailVO {
|
||||
@NotNull
|
||||
String osArch;
|
||||
@NotNull
|
||||
@ -14,6 +14,8 @@ public class RegisterServerVO {
|
||||
@NotNull
|
||||
int osBit;
|
||||
@NotNull
|
||||
String cpuName;
|
||||
@NotNull
|
||||
int cpuCore;
|
||||
@NotNull
|
||||
double memory;
|
||||
@ -21,6 +23,4 @@ public class RegisterServerVO {
|
||||
double disk;
|
||||
@NotNull
|
||||
String ip;
|
||||
@NotNull
|
||||
String accessToken;
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class CreateSubAccountVO {
|
||||
@Length(min = 1, max = 10)
|
||||
String username;
|
||||
@Email
|
||||
String email;
|
||||
@Length(min = 6, max = 20)
|
||||
String password;
|
||||
@Size(min = 1)
|
||||
List<Integer> clients;
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class ModifyEmailVO {
|
||||
@Email
|
||||
String email;
|
||||
@Length(max = 6, min = 6)
|
||||
String code;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class RenameClientVO {
|
||||
@NotNull
|
||||
int id;
|
||||
@Length(min = 1, max = 10)
|
||||
String name;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class RenameNodeVO {
|
||||
int id;
|
||||
@Length(min = 1, max = 10)
|
||||
String node;
|
||||
@Pattern(regexp = "(cn|hk|jp|us|sg|kr|de)")
|
||||
String location;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RuntimeDetailVO {
|
||||
@NotNull
|
||||
long timestamp;
|
||||
@NotNull
|
||||
double cpuUsage;
|
||||
@NotNull
|
||||
double memoryUsage;
|
||||
@NotNull
|
||||
double diskUsage;
|
||||
@NotNull
|
||||
double networkUpload;
|
||||
@NotNull
|
||||
double networkDownload;
|
||||
@NotNull
|
||||
double diskRead;
|
||||
@NotNull
|
||||
double diskWrite;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
@Data
|
||||
public class SshConnectionVO {
|
||||
int id;
|
||||
int port;
|
||||
@NotNull
|
||||
@Length(min = 1)
|
||||
String username;
|
||||
@NotNull
|
||||
@Length(min = 1)
|
||||
String password;
|
||||
}
|
@ -10,6 +10,7 @@ import java.util.Date;
|
||||
@Data
|
||||
public class AuthorizeVO {
|
||||
String username;
|
||||
String email;
|
||||
String role;
|
||||
String token;
|
||||
Date expire;
|
@ -0,0 +1,19 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ClientDetailsVO {
|
||||
int id;
|
||||
String name;
|
||||
boolean online;
|
||||
String node;
|
||||
String location;
|
||||
String ip;
|
||||
String cpuName;
|
||||
String osName;
|
||||
String osVersion;
|
||||
double memory;
|
||||
int cpuCore;
|
||||
double disk;
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ClientPreviewVO {
|
||||
int id;
|
||||
boolean online;
|
||||
String name;
|
||||
String location;
|
||||
String osName;
|
||||
String osVersion;
|
||||
String ip;
|
||||
String cpuName;
|
||||
int cpuCore;
|
||||
double memory;
|
||||
double cpuUsage;
|
||||
double memoryUsage;
|
||||
double networkUpload;
|
||||
double networkDownload;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ClientSimpleVO {
|
||||
int id;
|
||||
String name;
|
||||
String location;
|
||||
String osName;
|
||||
String osVersion;
|
||||
String ip;
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class RuntimeHistoryVO {
|
||||
double disk;
|
||||
double memory;
|
||||
List<JSONObject> list = new LinkedList<>();
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SshSettingsVO {
|
||||
String ip;
|
||||
int port = 22;
|
||||
String username;
|
||||
String password;
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SubAccountVO {
|
||||
int id;
|
||||
String username;
|
||||
String email;
|
||||
JSONArray clientList;
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
package com.example.filter;
|
||||
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.dto.Client;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.service.ClientService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.JwtUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 用于对请求头中Jwt令牌进行校验的工具,为当前请求添加用户验证信息
|
||||
* 并将用户的ID存放在请求对象属性中,方便后续使用
|
||||
*/
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
JwtUtils utils;
|
||||
|
||||
@Resource
|
||||
ClientService service;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String authorization = request.getHeader("Authorization");
|
||||
String uri = request.getRequestURI();
|
||||
if(uri.startsWith("/monitor")) {
|
||||
if(!uri.endsWith("/register")) {
|
||||
Client client = service.findClientByToken(authorization);
|
||||
if(client == null) {
|
||||
response.setStatus(401);
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().write(RestBean.failure(401, "未注册").asJsonString());
|
||||
return;
|
||||
} else {
|
||||
request.setAttribute(Const.ATTR_CLIENT, client);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DecodedJWT jwt = utils.resolveJwt(authorization);
|
||||
if(jwt != null) {
|
||||
UserDetails user = utils.toUser(jwt);
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
|
||||
request.setAttribute(Const.ATTR_USER_ROLE, new ArrayList<>(user.getAuthorities()).get(0).getAuthority());
|
||||
|
||||
if(request.getRequestURI().startsWith("/terminal/") && !accessShell(
|
||||
(int) request.getAttribute(Const.ATTR_USER_ID),
|
||||
(String) request.getAttribute(Const.ATTR_USER_ROLE),
|
||||
Integer.parseInt(request.getRequestURI().substring(10)))) {
|
||||
response.setStatus(401);
|
||||
response.setCharacterEncoding("utf-8");
|
||||
response.getWriter().write(RestBean.failure(401, "无权访问").asJsonString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Resource
|
||||
AccountService accountService;
|
||||
|
||||
private boolean accessShell(int userId, String userRole, int clientId) {
|
||||
if(Const.ROLE_ADMIN.equals(userRole.substring(5))) {
|
||||
return true;
|
||||
} else {
|
||||
Account account = accountService.getById(userId);
|
||||
return account.getClientList().contains(clientId);
|
||||
}
|
||||
}
|
||||
}
|
@ -29,7 +29,8 @@ public class RequestLogFilter extends OncePerRequestFilter {
|
||||
@Resource
|
||||
SnowflakeIdGenerator generator;
|
||||
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs");
|
||||
private final Set<String> ignores = Set.of("/swagger-ui", "/v3/api-docs", "/monitor/runtime",
|
||||
"/api/monitor/list", "/api/monitor/runtime-now");
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
@ -36,6 +36,10 @@ public class MailQueueListener {
|
||||
createMessage("您的密码重置邮件",
|
||||
"你好,您正在执行重置密码操作,验证码: "+code+",有效时间3分钟,如非本人操作,请无视。",
|
||||
email);
|
||||
case "modify" ->
|
||||
createMessage("您的邮件修改验证邮件",
|
||||
"您好,您正在绑定新的电子邮件地址,验证码: "+code+",有效时间3分钟,如非本人操作,请无视",
|
||||
email);
|
||||
default -> null;
|
||||
};
|
||||
if(message == null) return;
|
@ -0,0 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.ClientDetail;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ClientDetailMapper extends BaseMapper<ClientDetail> {
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.Server;
|
||||
import com.example.entity.dto.Client;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ServerMapper extends BaseMapper<Server> {
|
||||
public interface ClientMapper extends BaseMapper<Client> {
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.ServerInfo;
|
||||
import com.example.entity.dto.ClientSsh;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ServerInfoMapper extends BaseMapper<ServerInfo> {
|
||||
public interface ClientSshMapper extends BaseMapper<ClientSsh> {
|
||||
}
|
@ -3,12 +3,22 @@ package com.example.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.request.ConfirmResetVO;
|
||||
import com.example.entity.vo.request.CreateSubAccountVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import com.example.entity.vo.request.ModifyEmailVO;
|
||||
import com.example.entity.vo.response.SubAccountVO;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AccountService extends IService<Account>, UserDetailsService {
|
||||
Account findAccountByNameOrEmail(String text);
|
||||
String registerEmailVerifyCode(String type, String email, String address);
|
||||
String resetEmailAccountPassword(EmailResetVO info);
|
||||
String resetConfirm(ConfirmResetVO info);
|
||||
boolean changePassword(int id, String oldPass, String newPass);
|
||||
void createSubAccount(CreateSubAccountVO vo);
|
||||
void deleteSubAccount(int uid);
|
||||
List<SubAccountVO> listSubAccount();
|
||||
String modifyEmail(int id, ModifyEmailVO vo);
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.Client;
|
||||
import com.example.entity.vo.request.*;
|
||||
import com.example.entity.vo.response.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ClientService extends IService<Client> {
|
||||
String registerToken();
|
||||
Client findClientById(int id);
|
||||
Client findClientByToken(String token);
|
||||
boolean verifyAndRegister(String token);
|
||||
void updateClientDetail(ClientDetailVO vo, Client client);
|
||||
void updateRuntimeDetail(RuntimeDetailVO vo, Client client);
|
||||
List<ClientPreviewVO> listClients();
|
||||
List<ClientSimpleVO> listSimpleList();
|
||||
void renameClient(RenameClientVO vo);
|
||||
void renameNode(RenameNodeVO vo);
|
||||
ClientDetailsVO clientDetails(int clientId);
|
||||
RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId);
|
||||
RuntimeDetailVO clientRuntimeDetailsNow(int clientId);
|
||||
void deleteClient(int clientId);
|
||||
void saveClientSshConnection(SshConnectionVO vo);
|
||||
SshSettingsVO sshSettings(int clientId);
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.request.ConfirmResetVO;
|
||||
import com.example.entity.vo.request.CreateSubAccountVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import com.example.entity.vo.request.ModifyEmailVO;
|
||||
import com.example.entity.vo.response.SubAccountVO;
|
||||
import com.example.mapper.AccountMapper;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
@ -19,6 +23,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@ -116,6 +122,61 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean changePassword(int id, String oldPass, String newPass) {
|
||||
Account account = this.getById(id);
|
||||
String password = account.getPassword();
|
||||
if(!passwordEncoder.matches(oldPass, password))
|
||||
return false;
|
||||
this.update(Wrappers.<Account>update().eq("id", id)
|
||||
.set("password", passwordEncoder.encode(newPass)));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createSubAccount(CreateSubAccountVO vo) {
|
||||
Account account = this.findAccountByNameOrEmail(vo.getEmail());
|
||||
if(account != null)
|
||||
throw new IllegalArgumentException("该电子邮件已被注册");
|
||||
account = this.findAccountByNameOrEmail(vo.getUsername());
|
||||
if(account != null)
|
||||
throw new IllegalArgumentException("该用户名已被注册");
|
||||
account = new Account(null, vo.getUsername(), passwordEncoder.encode(vo.getPassword()),
|
||||
vo.getEmail(), Const.ROLE_NORMAL, new Date(), JSONArray.copyOf(vo.getClients()).toJSONString());
|
||||
this.save(account);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteSubAccount(int uid) {
|
||||
this.removeById(uid);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SubAccountVO> listSubAccount() {
|
||||
return this.list(Wrappers.<Account>query().eq("role", Const.ROLE_NORMAL))
|
||||
.stream().map(account -> {
|
||||
SubAccountVO vo = account.asViewObject(SubAccountVO.class);
|
||||
vo.setClientList(JSONArray.parse(account.getClients()));
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String modifyEmail(int id, ModifyEmailVO vo) {
|
||||
String code = getEmailVerifyCode(vo.getEmail());
|
||||
if (code == null) return "请先获取验证码";
|
||||
if(!code.equals(vo.getCode())) return "验证码错误,请重新输入";
|
||||
this.deleteEmailVerifyCode(vo.getEmail());
|
||||
Account account = this.findAccountByNameOrEmail(vo.getEmail());
|
||||
if(account != null && account.getId() != id) return "该邮箱账号已经被其他账号绑定,无法完成操作";
|
||||
this.update()
|
||||
.set("email", vo.getEmail())
|
||||
.eq("id", id)
|
||||
.update();
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Redis中存储的邮件验证码
|
||||
* @param email 电邮
|
||||
@ -156,22 +217,4 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
.eq("email", text)
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定邮箱的用户是否已经存在
|
||||
* @param email 邮箱
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean existsAccountByEmail(String email){
|
||||
return this.baseMapper.exists(Wrappers.<Account>query().eq("email", email));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定用户名的用户是否已经存在
|
||||
* @param username 用户名
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean existsAccountByUsername(String username){
|
||||
return this.baseMapper.exists(Wrappers.<Account>query().eq("username", username));
|
||||
}
|
||||
}
|
@ -0,0 +1,210 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Client;
|
||||
import com.example.entity.dto.ClientDetail;
|
||||
import com.example.entity.dto.ClientSsh;
|
||||
import com.example.entity.vo.request.*;
|
||||
import com.example.entity.vo.response.*;
|
||||
import com.example.mapper.ClientDetailMapper;
|
||||
import com.example.mapper.ClientMapper;
|
||||
import com.example.mapper.ClientSshMapper;
|
||||
import com.example.service.ClientService;
|
||||
import com.example.utils.InfluxDbUtils;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> implements ClientService {
|
||||
|
||||
private String registerToken = this.generateNewToken();
|
||||
|
||||
private final Map<Integer, Client> clientIdCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, Client> clientTokenCache = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
ClientDetailMapper detailMapper;
|
||||
|
||||
@Resource
|
||||
InfluxDbUtils influx;
|
||||
|
||||
@Resource
|
||||
ClientSshMapper sshMapper;
|
||||
|
||||
@PostConstruct
|
||||
public void initClientCache() {
|
||||
clientTokenCache.clear();
|
||||
clientIdCache.clear();
|
||||
this.list().forEach(this::addClientCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String registerToken() {
|
||||
return registerToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClientById(int id) {
|
||||
return clientIdCache.get(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Client findClientByToken(String token) {
|
||||
return clientTokenCache.get(token);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verifyAndRegister(String token) {
|
||||
if (this.registerToken.equals(token)) {
|
||||
int id = this.randomClientId();
|
||||
Client client = new Client(id, "未命名主机", token, "cn", "未命名节点", new Date());
|
||||
if (this.save(client)) {
|
||||
registerToken = this.generateNewToken();
|
||||
this.addClientCache(client);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateClientDetail(ClientDetailVO vo, Client client) {
|
||||
ClientDetail detail = new ClientDetail();
|
||||
BeanUtils.copyProperties(vo, detail);
|
||||
detail.setId(client.getId());
|
||||
if(Objects.nonNull(detailMapper.selectById(client.getId()))) {
|
||||
detailMapper.updateById(detail);
|
||||
} else {
|
||||
detailMapper.insert(detail);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Integer, RuntimeDetailVO> currentRuntime = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void updateRuntimeDetail(RuntimeDetailVO vo, Client client) {
|
||||
currentRuntime.put(client.getId(), vo);
|
||||
influx.writeRuntimeData(client.getId(), vo);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientPreviewVO> listClients() {
|
||||
return clientIdCache.values().stream().map(client -> {
|
||||
ClientPreviewVO vo = client.asViewObject(ClientPreviewVO.class);
|
||||
BeanUtils.copyProperties(detailMapper.selectById(vo.getId()), vo);
|
||||
RuntimeDetailVO runtime = currentRuntime.get(client.getId());
|
||||
if(this.isOnline(runtime)) {
|
||||
BeanUtils.copyProperties(runtime, vo);
|
||||
vo.setOnline(true);
|
||||
}
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientSimpleVO> listSimpleList() {
|
||||
return clientIdCache.values().stream().map(client -> {
|
||||
ClientSimpleVO vo = client.asViewObject(ClientSimpleVO.class);
|
||||
BeanUtils.copyProperties(detailMapper.selectById(vo.getId()), vo);
|
||||
return vo;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renameClient(RenameClientVO vo) {
|
||||
this.update(Wrappers.<Client>update().eq("id", vo.getId()).set("name", vo.getName()));
|
||||
this.initClientCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void renameNode(RenameNodeVO vo) {
|
||||
this.update(Wrappers.<Client>update().eq("id", vo.getId())
|
||||
.set("node", vo.getNode()).set("location", vo.getLocation()));
|
||||
this.initClientCache();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientDetailsVO clientDetails(int clientId) {
|
||||
ClientDetailsVO vo = this.clientIdCache.get(clientId).asViewObject(ClientDetailsVO.class);
|
||||
BeanUtils.copyProperties(detailMapper.selectById(clientId), vo);
|
||||
vo.setOnline(this.isOnline(currentRuntime.get(clientId)));
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeHistoryVO clientRuntimeDetailsHistory(int clientId) {
|
||||
RuntimeHistoryVO vo = influx.readRuntimeData(clientId);
|
||||
ClientDetail detail = detailMapper.selectById(clientId);
|
||||
BeanUtils.copyProperties(detail, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RuntimeDetailVO clientRuntimeDetailsNow(int clientId) {
|
||||
return currentRuntime.get(clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteClient(int clientId) {
|
||||
this.removeById(clientId);
|
||||
detailMapper.deleteById(clientId);
|
||||
this.initClientCache();
|
||||
currentRuntime.remove(clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveClientSshConnection(SshConnectionVO vo) {
|
||||
Client client = clientIdCache.get(vo.getId());
|
||||
if(client == null) return;
|
||||
ClientSsh ssh = new ClientSsh();
|
||||
BeanUtils.copyProperties(vo, ssh);
|
||||
if(Objects.nonNull(sshMapper.selectById(client.getId()))) {
|
||||
sshMapper.updateById(ssh);
|
||||
} else {
|
||||
sshMapper.insert(ssh);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public SshSettingsVO sshSettings(int clientId) {
|
||||
ClientDetail detail = detailMapper.selectById(clientId);
|
||||
ClientSsh ssh = sshMapper.selectById(clientId);
|
||||
SshSettingsVO vo;
|
||||
if(ssh == null) {
|
||||
vo = new SshSettingsVO();
|
||||
} else {
|
||||
vo = ssh.asViewObject(SshSettingsVO.class);
|
||||
}
|
||||
vo.setIp(detail.getIp());
|
||||
return vo;
|
||||
}
|
||||
|
||||
private boolean isOnline(RuntimeDetailVO runtime) {
|
||||
return runtime != null && System.currentTimeMillis() - runtime.getTimestamp() < 60 * 1000;
|
||||
}
|
||||
|
||||
private void addClientCache(Client client) {
|
||||
clientIdCache.put(client.getId(), client);
|
||||
clientTokenCache.put(client.getToken(), client);
|
||||
}
|
||||
|
||||
private int randomClientId() {
|
||||
return new Random().nextInt(90000000) + 10000000;
|
||||
}
|
||||
|
||||
private String generateNewToken() {
|
||||
String CHARACTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
SecureRandom random = new SecureRandom();
|
||||
StringBuilder sb = new StringBuilder(24);
|
||||
for (int i = 0; i < 24; i++)
|
||||
sb.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@ -7,6 +7,8 @@ public final class Const {
|
||||
//JWT令牌
|
||||
public final static String JWT_BLACK_LIST = "jwt:blacklist:";
|
||||
public final static String JWT_FREQUENCY = "jwt:frequency:";
|
||||
//用户
|
||||
public final static String USER_BLACK_LIST = "user:blacklist:";
|
||||
//请求频率限制
|
||||
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
|
||||
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
||||
@ -18,9 +20,11 @@ public final class Const {
|
||||
public final static int ORDER_CORS = -102;
|
||||
//请求自定义属性
|
||||
public final static String ATTR_USER_ID = "userId";
|
||||
public final static String ATTR_USER_ROLE = "userRole";
|
||||
public final static String ATTR_CLIENT = "client";
|
||||
//消息队列
|
||||
public final static String MQ_MAIL = "mail";
|
||||
//用户角色
|
||||
public final static String ROLE_DEFAULT = "user";
|
||||
|
||||
public final static String ROLE_ADMIN = "admin";
|
||||
public final static String ROLE_NORMAL = "user";
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.entity.dto.RuntimeData;
|
||||
import com.example.entity.vo.request.RuntimeDetailVO;
|
||||
import com.example.entity.vo.response.RuntimeHistoryVO;
|
||||
import com.influxdb.client.InfluxDBClient;
|
||||
import com.influxdb.client.InfluxDBClientFactory;
|
||||
import com.influxdb.client.WriteApiBlocking;
|
||||
import com.influxdb.client.domain.WritePrecision;
|
||||
import com.influxdb.query.FluxRecord;
|
||||
import com.influxdb.query.FluxTable;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class InfluxDbUtils {
|
||||
|
||||
@Value("${spring.influx.url}")
|
||||
String url;
|
||||
@Value("${spring.influx.user}")
|
||||
String user;
|
||||
@Value("${spring.influx.password}")
|
||||
String password;
|
||||
|
||||
private final String BUCKET = "monitor";
|
||||
private final String ORG = "itbaima";
|
||||
|
||||
private InfluxDBClient client;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
client = InfluxDBClientFactory.create(url, user, password.toCharArray());
|
||||
}
|
||||
|
||||
public void writeRuntimeData(int clientId, RuntimeDetailVO vo) {
|
||||
RuntimeData data = new RuntimeData();
|
||||
BeanUtils.copyProperties(vo, data);
|
||||
data.setTimestamp(new Date(vo.getTimestamp()).toInstant());
|
||||
data.setClientId(clientId);
|
||||
WriteApiBlocking writeApi = client.getWriteApiBlocking();
|
||||
writeApi.writeMeasurement(BUCKET, ORG, WritePrecision.NS, data);
|
||||
}
|
||||
|
||||
public RuntimeHistoryVO readRuntimeData(int clientId) {
|
||||
RuntimeHistoryVO vo = new RuntimeHistoryVO();
|
||||
String query = """
|
||||
from(bucket: "%s")
|
||||
|> range(start: %s)
|
||||
|> filter(fn: (r) => r["_measurement"] == "runtime")
|
||||
|> filter(fn: (r) => r["clientId"] == "%s")
|
||||
""";
|
||||
String format = String.format(query, BUCKET, "-1h", clientId);
|
||||
List<FluxTable> tables = client.getQueryApi().query(format, ORG);
|
||||
int size = tables.size();
|
||||
if (size == 0) return vo;
|
||||
List<FluxRecord> records = tables.get(0).getRecords();
|
||||
for (int i = 0; i < records.size(); i++) {
|
||||
JSONObject object = new JSONObject();
|
||||
object.put("timestamp", records.get(i).getTime());
|
||||
for (int j = 0; j < size; j++) {
|
||||
FluxRecord record = tables.get(j).getRecords().get(i);
|
||||
object.put(record.getField(), record.getValue());
|
||||
}
|
||||
vo.getList().add(object);
|
||||
}
|
||||
return vo;
|
||||
}
|
||||
}
|
@ -109,6 +109,7 @@ public class JwtUtils {
|
||||
try {
|
||||
DecodedJWT verify = jwtVerifier.verify(token);
|
||||
if(this.isInvalidToken(verify.getId())) return null;
|
||||
if(this.isInvalidUser(verify.getClaim("id").asInt())) return null;
|
||||
Map<String, Claim> claims = verify.getClaims();
|
||||
return new Date().after(claims.get("exp").asDate()) ? null : verify;
|
||||
} catch (JWTVerificationException e) {
|
||||
@ -177,6 +178,14 @@ public class JwtUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void deleteUser(int uid) {
|
||||
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
|
||||
}
|
||||
|
||||
private boolean isInvalidUser(int uid){
|
||||
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token是否被列入Redis黑名单
|
||||
* @param uuid 令牌ID
|
@ -0,0 +1,155 @@
|
||||
package com.example.websocket;
|
||||
|
||||
import com.example.entity.dto.ClientDetail;
|
||||
import com.example.entity.dto.ClientSsh;
|
||||
import com.example.mapper.ClientDetailMapper;
|
||||
import com.example.mapper.ClientSshMapper;
|
||||
import com.jcraft.jsch.ChannelShell;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.websocket.*;
|
||||
import jakarta.websocket.server.PathParam;
|
||||
import jakarta.websocket.server.ServerEndpoint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@ServerEndpoint("/terminal/{clientId}")
|
||||
public class TerminalWebSocket {
|
||||
|
||||
private static ClientDetailMapper detailMapper;
|
||||
|
||||
@Resource
|
||||
public void setDetailMapper(ClientDetailMapper detailMapper) {
|
||||
TerminalWebSocket.detailMapper = detailMapper;
|
||||
}
|
||||
|
||||
private static ClientSshMapper sshMapper;
|
||||
|
||||
@Resource
|
||||
public void setSshMapper(ClientSshMapper sshMapper) {
|
||||
TerminalWebSocket.sshMapper = sshMapper;
|
||||
}
|
||||
|
||||
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
|
||||
private final ExecutorService service = Executors.newSingleThreadExecutor();
|
||||
|
||||
@OnOpen
|
||||
public void onOpen(Session session,
|
||||
@PathParam(value = "clientId") String clientId) throws Exception {
|
||||
ClientDetail detail = detailMapper.selectById(clientId);
|
||||
ClientSsh ssh = sshMapper.selectById(clientId);
|
||||
if(detail == null || ssh == null) {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "无法识别此主机"));
|
||||
return;
|
||||
}
|
||||
if(this.createSshConnection(session, ssh, detail.getIp())) {
|
||||
log.info("主机 {} 的SSH连接已创建", detail.getIp());
|
||||
}
|
||||
}
|
||||
|
||||
@OnMessage
|
||||
public void onMessage(Session session, String message) throws IOException {
|
||||
Shell shell = sessionMap.get(session);
|
||||
OutputStream output = shell.output;
|
||||
output.write(message.getBytes(StandardCharsets.UTF_8));
|
||||
output.flush();
|
||||
}
|
||||
|
||||
@OnClose
|
||||
public void onClose(Session session) throws IOException {
|
||||
Shell shell = sessionMap.get(session);
|
||||
if(shell != null) {
|
||||
shell.close();
|
||||
sessionMap.remove(session);
|
||||
log.info("主机 {} 的SSH连接已断开", shell.js.getHost());
|
||||
}
|
||||
}
|
||||
|
||||
@OnError
|
||||
public void onError(Session session, Throwable error) throws IOException {
|
||||
log.error("用户WebSocket连接出现错误", error);
|
||||
session.close();
|
||||
}
|
||||
|
||||
private boolean createSshConnection(Session session, ClientSsh ssh, String ip) throws IOException{
|
||||
try {
|
||||
JSch jSch = new JSch();
|
||||
com.jcraft.jsch.Session js = jSch.getSession(ssh.getUsername(), ip, ssh.getPort());
|
||||
js.setPassword(ssh.getPassword());
|
||||
js.setConfig("StrictHostKeyChecking", "no");
|
||||
js.setTimeout(3000);
|
||||
js.connect();
|
||||
ChannelShell channel = (ChannelShell) js.openChannel("shell");
|
||||
channel.setPtyType("xterm");
|
||||
channel.connect(1000);
|
||||
sessionMap.put(session, new Shell(session, js, channel));
|
||||
return true;
|
||||
} catch (JSchException e) {
|
||||
String message = e.getMessage();
|
||||
if(message.equals("Auth fail")) {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,
|
||||
"登录SSH失败,用户名或密码错误"));
|
||||
log.error("连接SSH失败,用户名或密码错误,登录失败");
|
||||
} else if(message.contains("Connection refused")) {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,
|
||||
"连接被拒绝,可能是没有启动SSH服务或是放开端口"));
|
||||
log.error("连接SSH失败,连接被拒绝,可能是没有启动SSH服务或是放开端口");
|
||||
} else {
|
||||
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, message));
|
||||
log.error("连接SSH时出现错误", e);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private class Shell {
|
||||
private final Session session;
|
||||
private final com.jcraft.jsch.Session js;
|
||||
private final ChannelShell channel;
|
||||
private final InputStream input;
|
||||
private final OutputStream output;
|
||||
|
||||
public Shell(Session session, com.jcraft.jsch.Session js, ChannelShell channel) throws IOException {
|
||||
this.js = js;
|
||||
this.session = session;
|
||||
this.channel = channel;
|
||||
this.input = channel.getInputStream();
|
||||
this.output = channel.getOutputStream();
|
||||
service.submit(this::read);
|
||||
}
|
||||
|
||||
private void read() {
|
||||
try {
|
||||
byte[] buffer = new byte[1024 * 1024];
|
||||
int i;
|
||||
while ((i = input.read(buffer)) != -1) {
|
||||
String text = new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8);
|
||||
session.getBasicRemote().sendText(text);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("读取SSH输入流时出现问题", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
input.close();
|
||||
output.close();
|
||||
channel.disconnect();
|
||||
js.disconnect();
|
||||
service.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,10 @@ springdoc:
|
||||
swagger-ui:
|
||||
operations-sorter: alpha
|
||||
spring:
|
||||
influx:
|
||||
url: http://localhost:8086
|
||||
user: admin
|
||||
password: 12345678
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
username: javastudy111@163.com
|
||||
@ -32,8 +36,8 @@ spring:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 3
|
||||
limit: 50
|
||||
period: 5
|
||||
limit: 100
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
@ -8,6 +8,10 @@ mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
spring:
|
||||
influx:
|
||||
url: http://localhost:8086
|
||||
user: admin
|
||||
password: 12345678
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
username: javastudy111@163.com
|
||||
@ -36,8 +40,8 @@ spring:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 3
|
||||
limit: 10
|
||||
period: 5
|
||||
limit: 100
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
@ -5,6 +5,5 @@
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="all" level="application" />
|
||||
</component>
|
||||
</module>
|
@ -11,10 +11,15 @@
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.3.9",
|
||||
"flag-icon-css": "^4.1.7",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.2.4",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-attach": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
@ -868,6 +873,20 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.4.3.tgz",
|
||||
"integrity": "sha512-mYKxLxhzy6zyTi/FaEbJMOZU1ULGEQHaeIeuMR5L+JnJTpz+YR03mnnpBhbR4+UYJAgiXgpyTVLffPAjOTLkZA==",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.4.4"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
},
|
||||
"node_modules/element-plus": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz",
|
||||
@ -1425,13 +1444,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/needle": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.2.0.tgz",
|
||||
"integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz",
|
||||
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
@ -1442,16 +1460,6 @@
|
||||
"node": ">= 4.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/needle/node_modules/debug": {
|
||||
"version": "3.2.7",
|
||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@ -1511,6 +1519,58 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia": {
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.1.7.tgz",
|
||||
"integrity": "sha512-+C2AHFtcFqjPih0zpYuvof37SFxMQ7OEG2zV9jRI12i9BOy3YQVAHwdKtyyc8pDcDyIc33WCIsZaCFWU7WWxGQ==",
|
||||
"dependencies": {
|
||||
"@vue/devtools-api": "^6.5.0",
|
||||
"vue-demi": ">=0.14.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.4.0",
|
||||
"typescript": ">=4.4.4",
|
||||
"vue": "^2.6.14 || ^3.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pinia-plugin-persistedstate": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-3.2.0.tgz",
|
||||
"integrity": "sha512-tZbNGf2vjAQcIm7alK40sE51Qu/m9oWr+rEgNm/2AWr1huFxj72CjvpQcIQzMknDBJEkQznCLAGtJTIcLKrKdw==",
|
||||
"peerDependencies": {
|
||||
"pinia": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pinia/node_modules/vue-demi": {
|
||||
"version": "0.14.6",
|
||||
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.6.tgz",
|
||||
"integrity": "sha512-8QA7wrYSHKaYgUxDA5ZC24w+eHm3sYCbp0EzcDwKqN3p6HqtTCGR/GVsPyZW92unff4UlcSh++lmqDWN3ZIq4w==",
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"vue-demi-fix": "bin/vue-demi-fix.js",
|
||||
"vue-demi-switch": "bin/vue-demi-switch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vue/composition-api": "^1.0.0-rc.1",
|
||||
"vue": "^3.0.0-0 || ^2.6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vue/composition-api": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pkg-types": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz",
|
||||
@ -1900,6 +1960,32 @@
|
||||
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
|
||||
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/xterm": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz",
|
||||
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||
},
|
||||
"node_modules/xterm-addon-attach": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmmirror.com/xterm-addon-attach/-/xterm-addon-attach-0.9.0.tgz",
|
||||
"integrity": "sha512-NykWWOsobVZPPK3P9eFkItrnBK9Lw0f94uey5zhqIVB1bhswdVBfl+uziEzSOhe2h0rT9wD0wOeAYsdSXeavPw==",
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz",
|
||||
"integrity": "sha512-0VxCNJ7AGOMCWeHVyTrGzUgrK4asT4ml9PEkeGirAkKNYXYzoPJCLvmyfdoOXcjTHPs10OZVMfD1Rwg16AZyYw==",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/zrender/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
}
|
||||
}
|
||||
}
|
@ -11,10 +11,15 @@
|
||||
"@element-plus/icons-vue": "^2.1.0",
|
||||
"@vueuse/core": "^10.3.0",
|
||||
"axios": "^1.4.0",
|
||||
"echarts": "^5.4.3",
|
||||
"element-plus": "^2.3.9",
|
||||
"flag-icon-css": "^4.1.7",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"vue": "^3.3.4",
|
||||
"vue-router": "^4.2.4"
|
||||
"vue-router": "^4.2.4",
|
||||
"xterm": "^5.3.0",
|
||||
"xterm-addon-attach": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
BIN
itbaima-monitor-web/public/cpu-icons/AMD.png
Normal file
BIN
itbaima-monitor-web/public/cpu-icons/AMD.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
BIN
itbaima-monitor-web/public/cpu-icons/Apple.png
Normal file
BIN
itbaima-monitor-web/public/cpu-icons/Apple.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
itbaima-monitor-web/public/cpu-icons/Intel.png
Normal file
BIN
itbaima-monitor-web/public/cpu-icons/Intel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
53
itbaima-monitor-web/src/assets/css/element.less
Normal file
53
itbaima-monitor-web/src/assets/css/element.less
Normal file
@ -0,0 +1,53 @@
|
||||
.is-success {
|
||||
.el-progress-bar__outer {
|
||||
background-color: #18cb1822 !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #18cb18 !important;
|
||||
}
|
||||
|
||||
.el-progress-circle__track {
|
||||
stroke: #18cb1822;
|
||||
}
|
||||
|
||||
.el-progress-circle__path {
|
||||
stroke: #18cb18 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-warning {
|
||||
.el-progress-bar__outer {
|
||||
background-color: #ffa04622;
|
||||
}
|
||||
|
||||
.el-progress-circle__track {
|
||||
stroke: #ffa04622;
|
||||
}
|
||||
|
||||
.el-progress-circle__path {
|
||||
stroke: #ffa046 !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #ffa046 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.is-exception {
|
||||
.el-progress-bar__outer {
|
||||
background-color: #ef4e4e22;
|
||||
}
|
||||
|
||||
.el-progress-circle__track {
|
||||
stroke: #ef4e4e22;
|
||||
}
|
||||
|
||||
.el-progress-circle__path {
|
||||
stroke: #ef4e4e !important;
|
||||
}
|
||||
|
||||
.el-progress-bar__inner {
|
||||
background-color: #ef4e4e !important;
|
||||
}
|
||||
}
|
278
itbaima-monitor-web/src/component/ClientDetails.vue
Normal file
278
itbaima-monitor-web/src/component/ClientDetails.vue
Normal file
@ -0,0 +1,278 @@
|
||||
<script setup>
|
||||
import {computed, reactive, watch} from "vue";
|
||||
import {get, post} from "@/net";
|
||||
import {copyIp, cpuNameToImage, fitByUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import RuntimeHistory from "@/component/RuntimeHistory.vue";
|
||||
import {Connection, Delete} from "@element-plus/icons-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 props = defineProps({
|
||||
id: Number,
|
||||
update: Function
|
||||
})
|
||||
const emits = defineEmits(['delete', 'terminal'])
|
||||
|
||||
const details = reactive({
|
||||
base: {},
|
||||
runtime: {
|
||||
list: []
|
||||
},
|
||||
editNode: false
|
||||
})
|
||||
const nodeEdit = reactive({
|
||||
name: '',
|
||||
location: ''
|
||||
})
|
||||
const enableNodeEdit = () => {
|
||||
details.editNode = true
|
||||
nodeEdit.name = details.base.node
|
||||
nodeEdit.location = details.base.location
|
||||
}
|
||||
const submitNodeEdit = () => {
|
||||
post('/api/monitor/node', {
|
||||
id: props.id,
|
||||
node: nodeEdit.name,
|
||||
location: nodeEdit.location
|
||||
}, () => {
|
||||
details.editNode = false
|
||||
updateDetails()
|
||||
ElMessage.success('节点信息已更新')
|
||||
})
|
||||
}
|
||||
|
||||
function deleteClient() {
|
||||
ElMessageBox.confirm('删除此主机后所有统计数据都将丢失,您确定要这样做吗?', '删除主机', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(() => {
|
||||
get(`/api/monitor/delete?clientId=${props.id}`, () => {
|
||||
emits('delete')
|
||||
props.update()
|
||||
ElMessage.success('主机已成功移除')
|
||||
})
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
function updateDetails() {
|
||||
props.update()
|
||||
init(props.id)
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
if(props.id !== -1 && details.runtime) {
|
||||
get(`/api/monitor/runtime-now?clientId=${props.id}`, data => {
|
||||
if(details.runtime.list.length >= 360)
|
||||
details.runtime.list.splice(0, 1)
|
||||
details.runtime.list.push(data)
|
||||
})
|
||||
}
|
||||
}, 10000)
|
||||
|
||||
const now = computed(() => details.runtime.list[details.runtime.list.length - 1])
|
||||
|
||||
const init = id => {
|
||||
if(id !== -1) {
|
||||
details.base = {}
|
||||
details.runtime = { list: [] }
|
||||
get(`/api/monitor/details?clientId=${id}`, data => Object.assign(details.base, data))
|
||||
get(`/api/monitor/runtime-history?clientId=${id}`, data => Object.assign(details.runtime, data))
|
||||
}
|
||||
}
|
||||
watch(() => props.id, init, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-scrollbar>
|
||||
<div class="client-details" v-loading="Object.keys(details.base).length === 0">
|
||||
<div v-if="Object.keys(details.base).length">
|
||||
<div style="display: flex;justify-content: space-between">
|
||||
<div class="title">
|
||||
<i class="fa-solid fa-server"></i>
|
||||
服务器信息
|
||||
</div>
|
||||
<div>
|
||||
<el-button :icon="Connection" type="info"
|
||||
@click="emits('terminal', id)" plain text>SSH远程连接</el-button>
|
||||
<el-button :icon="Delete" type="danger" style="margin-left: 0"
|
||||
@click="deleteClient" plain text>删除此主机</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div class="details-list">
|
||||
<div>
|
||||
<span>服务器ID</span>
|
||||
<span>{{details.base.id}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>服务器名称</span>
|
||||
<span>{{details.base.name}}</span>
|
||||
<i @click.stop="rename(details.base.id, details.base.name, updateDetails)"
|
||||
class="fa-solid fa-pen-to-square interact-item"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>运行状态</span>
|
||||
<span>
|
||||
<i style="color: #18cb18" class="fa-solid fa-circle-play" v-if="details.base.online"></i>
|
||||
<i style="color: #18cb18" class="fa-solid fa-circle-stop" v-else></i>
|
||||
{{details.base.online ? '运行中' : '离线'}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="!details.editNode">
|
||||
<span>服务器节点</span>
|
||||
<span :class="`flag-icon flag-icon-${details.base.location}`"></span>
|
||||
<span>{{details.base.node}}</span>
|
||||
<i @click.stop="enableNodeEdit"
|
||||
class="fa-solid fa-pen-to-square interact-item"/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span>服务器节点</span>
|
||||
<div style="display: inline-block;height: 15px">
|
||||
<div style="display: flex">
|
||||
<el-select v-model="nodeEdit.location" style="width: 80px" size="small">
|
||||
<el-option v-for="item in locations" :value="item.name">
|
||||
<span :class="`flag-icon flag-icon-${item.name}`"></span>
|
||||
{{item.desc}}
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-input v-model="nodeEdit.name" style="margin-left: 10px"
|
||||
size="small" placeholder="请输入节点名称..."/>
|
||||
<div style="margin-left: 10px">
|
||||
<i @click.stop="submitNodeEdit" class="fa-solid fa-check interact-item"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span>公网IP地址</span>
|
||||
<span>
|
||||
{{details.base.ip}}
|
||||
<i class="fa-solid fa-copy interact-item" style="color: dodgerblue" @click.stop="copyIp(details.base.ip)"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
<span>处理器</span>
|
||||
<span>{{details.base.cpuName}}</span>
|
||||
<el-image style="height: 20px;margin-left: 10px"
|
||||
:src="`/cpu-icons/${cpuNameToImage(details.base.cpuName)}`"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>硬件配置信息</span>
|
||||
<span>
|
||||
<i class="fa-solid fa-microchip"></i>
|
||||
<span style="margin-right: 10px">{{` ${details.base.cpuCore} CPU 核心数 /`}}</span>
|
||||
<i class="fa-solid fa-memory"></i>
|
||||
<span>{{` ${details.base.memory.toFixed(1)} GB 内存容量`}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span>操作系统</span>
|
||||
<i :style="{color: osNameToIcon(details.base.osName).color}"
|
||||
:class="`fa-brands ${osNameToIcon(details.base.osName).icon}`"></i>
|
||||
<span style="margin-left: 10px">{{`${details.base.osName} ${details.base.osVersion}`}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title" style="margin-top: 20px">
|
||||
<i class="fa-solid fa-gauge-high"></i>
|
||||
实时监控
|
||||
</div>
|
||||
<el-divider style="margin: 10px 0"/>
|
||||
<div v-if="details.base.online" v-loading="!details.runtime.list.length"
|
||||
style="min-height: 200px">
|
||||
<div style="display: flex" v-if="details.runtime.list.length">
|
||||
<el-progress type="dashboard" :width="100" :percentage="now.cpuUsage * 100"
|
||||
:status="percentageToStatus(now.cpuUsage * 100)">
|
||||
<div style="font-size: 17px;font-weight: bold;color: initial">CPU</div>
|
||||
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.cpuUsage * 100).toFixed(1) }}%</div>
|
||||
</el-progress>
|
||||
<el-progress style="margin-left: 20px" type="dashboard" :width="100"
|
||||
:percentage="now.memoryUsage / details.runtime.memory * 100"
|
||||
:status="percentageToStatus(now.memoryUsage / details.runtime.memory * 100)">
|
||||
<div style="font-size: 16px;font-weight: bold;color: initial">内存</div>
|
||||
<div style="font-size: 13px;color: grey;margin-top: 5px">{{ (now.memoryUsage).toFixed(1) }} GB</div>
|
||||
</el-progress>
|
||||
<div style="flex: 1;margin-left: 30px;display: flex;flex-direction: column;height: 80px">
|
||||
<div style="flex: 1;font-size: 14px">
|
||||
<div>实时网络速度</div>
|
||||
<div>
|
||||
<i style="color: orange" class="fa-solid fa-arrow-up"></i>
|
||||
<span>{{` ${fitByUnit(now.networkUpload, 'KB')}/s`}}</span>
|
||||
<el-divider direction="vertical"/>
|
||||
<i style="color: dodgerblue" class="fa-solid fa-arrow-down"></i>
|
||||
<span>{{` ${fitByUnit(now.networkDownload, 'KB')}/s`}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 13px;display: flex;justify-content: space-between">
|
||||
<div>
|
||||
<i class="fa-solid fa-hard-drive"></i>
|
||||
<span> 磁盘总容量</span>
|
||||
</div>
|
||||
<div>{{now.diskUsage.toFixed(1)}} GB / {{details.runtime.disk.toFixed(1)}} GB</div>
|
||||
</div>
|
||||
<el-progress type="line" :show-text="false"
|
||||
:status="percentageToStatus(now.diskUsage / details.runtime.disk * 100)"
|
||||
:percentage="now.diskUsage / details.runtime.disk * 100" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<runtime-history style="margin-top: 20px" :data="details.runtime.list"/>
|
||||
</div>
|
||||
<el-empty description="服务器处于离线状态,请检查服务器是否正常运行" v-else/>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.interact-item {
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
scale: 1.1;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.client-details {
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
|
||||
.title {
|
||||
color: dodgerblue;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.details-list {
|
||||
font-size: 14px;
|
||||
|
||||
& div {
|
||||
margin-bottom: 10px;
|
||||
|
||||
& span:first-child {
|
||||
color: gray;
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
width: 120px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
& span {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user