Compare commits

...

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

53 changed files with 725 additions and 701 deletions

34
.gitignore vendored
View File

@ -1,4 +1,32 @@
.idea
log
### IntelliJ IDEA ###
out/
.idea/
log/
config/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store
config

View File

@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<version>3.1.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
@ -21,7 +21,15 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.34</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
@ -32,19 +40,10 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.37</version>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.0</version>
<version>6.4.8</version>
</dependency>
</dependencies>
@ -54,6 +53,9 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-base:latest</builder>
</image>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>

View File

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

View File

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

View File

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

View File

@ -1,22 +1,31 @@
package com.example.entity;
import com.alibaba.fastjson2.JSONObject;
import lombok.Data;
public record Response(int id, int code, Object data, String message) {
@Data
public class Response {
int id;
int code;
Object data;
String message;
public boolean success() {
public boolean success(){
return code == 200;
}
public JSONObject asJson() {
public JSONObject ofJson() {
return JSONObject.from(data);
}
public String asString() {
public String ofString() {
return data.toString();
}
public static Response errorResponse(Exception e) {
return new Response(0, 500, null, e.getMessage());
Response response = new Response();
response.setCode(500);
response.setMessage(e.getMessage());
return response;
}
}

View File

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

View File

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

View File

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

View File

@ -1,11 +1,8 @@
package com.example.utils;
import com.example.entity.BaseDetail;
import com.example.entity.ConnectionConfig;
import com.example.entity.RuntimeDetail;
import jakarta.annotation.Resource;
import com.example.entity.data.BaseDetail;
import com.example.entity.data.RuntimeDetail;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
@ -16,43 +13,26 @@ import oshi.software.os.OperatingSystem;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.net.NetworkInterface;
import java.util.Arrays;
import java.util.Date;
import java.util.Objects;
import java.util.Properties;
@Slf4j
@Component
public class MonitorUtils {
@Lazy
@Resource
ConnectionConfig config;
private final SystemInfo info = new SystemInfo();
private final Properties properties = System.getProperties();
public BaseDetail monitorBaseDetail() {
OperatingSystem os = info.getOperatingSystem();
HardwareAbstractionLayer hardware = info.getHardware();
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 /1024;
double diskSize = Arrays.stream(File.listRoots()).mapToLong(File::getTotalSpace).sum() / 1024.0 / 1024 / 1024;
String ip = Objects.requireNonNull(this.findNetworkInterface(hardware)).getIPv4addr()[0];
return new BaseDetail()
.setOsArch(properties.getProperty("os.arch"))
.setOsName(os.getFamily())
.setOsVersion(os.getVersionInfo().getVersion())
.setOsBit(os.getBitness())
.setCpuName(hardware.getProcessor().getProcessorIdentifier().getName())
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
.setMemory(memory)
.setDisk(diskSize)
.setIp(ip);
}
public RuntimeDetail monitorRuntimeDetail() {
public RuntimeDetail monitorRuntimeData() {
double statisticTime = 0.5;
try {
HardwareAbstractionLayer hardware = info.getHardware();
NetworkIF networkInterface = Objects.requireNonNull(this.findNetworkInterface(hardware));
CentralProcessor processor = hardware.getProcessor();
//开始读取各种运行时信息
double upload = networkInterface.getBytesSent(), download = networkInterface.getBytesRecv();
double read = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getReadBytes).sum();
double write = hardware.getDiskStores().stream().mapToLong(HWDiskStore::getWriteBytes).sum();
@ -72,11 +52,46 @@ public class MonitorUtils {
.setDiskUsage(disk)
.setNetworkUpload(upload / 1024)
.setNetworkDownload(download / 1024)
.setDiskRead(read / 1024/ 1024)
.setDiskRead(read / 1024 / 1024)
.setDiskWrite(write / 1024 / 1024)
.setTimestamp(new Date().getTime());
} catch (Exception e) {
log.error("读取运行时数据出现问题", e);
}catch (Exception e) {
log.error("读取运行时数据时出现问题", e);
}
return null;
}
public BaseDetail monitorBaseData() {
OperatingSystem os = info.getOperatingSystem();
HardwareAbstractionLayer hardware = info.getHardware();
double memory = hardware.getMemory().getTotal() / 1024.0 / 1024 / 1024;
double diskSize = Arrays.stream(File.listRoots()).mapToLong(File::getTotalSpace).sum() / 1024.0 / 1024 / 1024;
String ip = Objects.requireNonNull(this.findNetworkInterface(hardware)).getIPv4addr()[0];
return new BaseDetail()
.setOsBit(os.getBitness())
.setOsArch(properties.getProperty("os.arch"))
.setOsVersion(os.getVersionInfo().getVersion())
.setOsName(os.getFamily())
.setCpuName(hardware.getProcessor().getProcessorIdentifier().getName())
.setCpuCore(hardware.getProcessor().getLogicalProcessorCount())
.setMemory(memory)
.setDisk(diskSize)
.setIp(ip);
}
private NetworkIF findNetworkInterface(HardwareAbstractionLayer hardware) {
try {
for (NetworkIF network : hardware.getNetworkIFs()) {
String[] iPv4addr = network.getIPv4addr();
NetworkInterface ni = network.queryNetworkInterface();
if(!ni.isLoopback() && !ni.isPointToPoint() && ni.isUp() && !ni.isVirtual()
&& (ni.getName().startsWith("eth") || ni.getName().startsWith("en"))
&& iPv4addr.length > 0) {
return network;
}
}
} catch (IOException e) {
log.error("读取网络接口信息时出错", e);
}
return null;
}
@ -102,30 +117,4 @@ public class MonitorUtils {
long totalCpu = cUser + nice + cSys + idle + ioWait + irq + softIrq + steal;
return (cSys + cUser) * 1.0 / totalCpu;
}
public List<String> listNetworkInterfaceName() {
HardwareAbstractionLayer hardware = info.getHardware();
return hardware.getNetworkIFs()
.stream()
.map(NetworkIF::getName)
.toList();
}
private NetworkIF findNetworkInterface(HardwareAbstractionLayer hardware) {
try {
String target = config.getNetworkInterface();
List<NetworkIF> ifs = hardware.getNetworkIFs()
.stream()
.filter(inter -> inter.getName().equals(target))
.toList();
if (!ifs.isEmpty()) {
return ifs.get(0);
} else {
throw new IOException("网卡信息错误,找不到网卡: " + target);
}
} catch (IOException e) {
log.error("读取网络接口信息时出错", e);
}
return null;
}
}

View File

@ -1,10 +1,10 @@
package com.example.utils;
import com.alibaba.fastjson2.JSONObject;
import com.example.entity.BaseDetail;
import com.example.entity.ConnectionConfig;
import com.example.entity.Response;
import com.example.entity.RuntimeDetail;
import com.example.entity.data.BaseDetail;
import com.example.entity.data.RuntimeDetail;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
@ -18,23 +18,56 @@ import java.net.http.HttpResponse;
@Slf4j
@Component
public class NetUtils {
private final HttpClient client = HttpClient.newHttpClient();
@Lazy
@Resource
ConnectionConfig config;
private ConnectionConfig config;
public boolean registerToServer(String address, String token) {
log.info("正在服务端注册,请稍后...");
log.info("正在服务端注册,请稍后...");
Response response = this.doGet("/register", address, token);
if(response.success()) {
log.info("客户端注册已完成!");
if(!response.success()) {
log.error("注册客户端失败: {}", response.getMessage());
} else {
log.error("客户端注册失败: {}", response.message());
log.info("客户端注册已完成!");
}
return response.success();
}
public void updateBaseDetails(BaseDetail detail){
Response response = this.doPost("/detail", detail);
if(response.success()) {
log.info("系统基本信息更新已完成");
} else {
log.error("系统基本信息更新失败: {}", response.getMessage());
}
}
public void updateRuntimeDetails(RuntimeDetail detail) {
Response response = this.doPost("/runtime", detail);
if(!response.success()) {
log.warn("更新运行时状态时,接收到服务端的异常响应内容: {}", response.getMessage());
}
}
private Response doPost(String url, Object data) {
try {
String rawData = JSONObject.from(data).toJSONString();
HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(rawData))
.uri(new URI(config.getAddress() + "/monitor" + url))
.header("Content-Type", "application/json")
.header("Authorization", config.getToken())
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return JSONObject.parseObject(response.body()).to(Response.class);
} catch (Exception e) {
log.error("在发起服务端请求时出现错误", e);
return Response.errorResponse(e);
}
}
private Response doGet(String url) {
return this.doGet(url, config.getAddress(), config.getToken());
}
@ -48,39 +81,7 @@ public class NetUtils {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return JSONObject.parseObject(response.body()).to(Response.class);
} catch (Exception e) {
log.error("在发起服务端请求时出现问题", e);
return Response.errorResponse(e);
}
}
public void updateBaseDetails(BaseDetail detail) {
Response response = this.doPost("/detail", detail);
if(response.success()) {
log.info("系统基本信息已更新完成");
} else {
log.error("系统基本信息更新失败: {}", response.message());
}
}
public void updateRuntimeDetails(RuntimeDetail detail) {
Response response = this.doPost("/runtime", detail);
if(!response.success()) {
log.warn("更新运行时状态时,接收到服务端的异常响应内容: {}", response.message());
}
}
private Response doPost(String url, Object data) {
try {
String rawData = JSONObject.from(data).toJSONString();
HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(rawData))
.uri(new URI(config.getAddress() + "/monitor" + url))
.header("Authorization", config.getToken())
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
return JSONObject.parseObject(response.body()).to(Response.class);
} catch (Exception e) {
log.error("在发起服务端请求时出现问题", e);
log.error("在发起服务端请求时出现错误", e);
return Response.errorResponse(e);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,9 +2,7 @@ package com.example.filter;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.example.entity.RestBean;
import com.example.entity.dto.Account;
import com.example.entity.dto.Client;
import com.example.service.AccountService;
import com.example.service.ClientService;
import com.example.utils.Const;
import com.example.utils.JwtUtils;
@ -44,16 +42,17 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
String uri = request.getRequestURI();
if(uri.startsWith("/monitor")) {
if(!uri.endsWith("/register")) {
Client client = service.findClientByToken(authorization);
Client client = service.findClient(authorization);
if(client == null) {
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.getWriter().write(RestBean.failure(401, "注册").asJsonString());
response.getWriter().write(RestBean.failure(401, "授权").asJsonString());
return;
} else {
request.setAttribute(Const.ATTR_CLIENT, client);
}
}
filterChain.doFilter(request, response);
} else {
DecodedJWT jwt = utils.resolveJwt(authorization);
if(jwt != null) {
@ -64,30 +63,8 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
SecurityContextHolder.getContext().setAuthentication(authentication);
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
request.setAttribute(Const.ATTR_USER_ROLE, new ArrayList<>(user.getAuthorities()).get(0).getAuthority());
if(request.getRequestURI().startsWith("/terminal/") && !accessShell(
(int) request.getAttribute(Const.ATTR_USER_ID),
(String) request.getAttribute(Const.ATTR_USER_ROLE),
Integer.parseInt(request.getRequestURI().substring(10)))) {
response.setStatus(401);
response.setCharacterEncoding("utf-8");
response.getWriter().write(RestBean.failure(401, "无权访问").asJsonString());
return;
}
}
}
filterChain.doFilter(request, response);
}
@Resource
AccountService accountService;
private boolean accessShell(int userId, String userRole, int clientId) {
if(Const.ROLE_ADMIN.equals(userRole.substring(5))) {
return true;
} else {
Account account = accountService.getById(userId);
return account.getClientList().contains(clientId);
}
}
}

View File

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

View File

@ -38,7 +38,7 @@ public class MailQueueListener {
email);
case "modify" ->
createMessage("您的邮件修改验证邮件",
"您好,您正在绑定新的电子邮件地址,验证码: "+code+"有效时间3分钟如非本人操作请无视",
"您好,您正在绑定新的电子邮件地址,验证码: "+code+"有效时间3分钟如非本人操作请无视",
email);
default -> null;
};

View File

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

View File

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

View File

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

View File

@ -7,8 +7,8 @@ public final class Const {
//JWT令牌
public final static String JWT_BLACK_LIST = "jwt:blacklist:";
public final static String JWT_FREQUENCY = "jwt:frequency:";
//用户
public final static String USER_BLACK_LIST = "user:blacklist:";
public final static String USER_BLACK_LIST = "user:blacklist";
//请求频率限制
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
public final static String FLOW_LIMIT_BLOCK = "flow:block:";

View File

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

View File

@ -141,6 +141,14 @@ public class JwtUtils {
return claims.get("id").asInt();
}
public void deleteUser(int uid) {
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
}
private boolean isInvalidUser(int uid){
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
}
/**
* 频率检测防止用户高频申请Jwt令牌并且采用阶段封禁机制
* 如果已经提示无法登录的情况下用户还在刷那么就封禁更长时间
@ -178,14 +186,6 @@ public class JwtUtils {
return true;
}
public void deleteUser(int uid) {
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
}
private boolean isInvalidUser(int uid){
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
}
/**
* 验证Token是否被列入Redis黑名单
* @param uuid 令牌ID

View File

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

View File

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

View File

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

View File

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

View File

@ -1,29 +0,0 @@
# my-project-frontend
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
## Customize configuration
See [Vite Configuration Reference](https://vitejs.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

View File

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

View File

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

View File

@ -1,10 +1,6 @@
.is-success {
.el-progress-bar__outer {
background-color: #18cb1822 !important;
}
.el-progress-bar__inner {
background-color: #18cb18 !important;
background-color: #18cb1822;
}
.el-progress-circle__track {
@ -14,6 +10,10 @@
.el-progress-circle__path {
stroke: #18cb18 !important;
}
.el-progress-bar__inner {
background-color: #18cb18 !important;
}
}
.is-warning {
@ -51,3 +51,4 @@
background-color: #ef4e4e !important;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,8 +4,10 @@
<el-image style="height: 30px"
src="https://element-plus.org/images/element-plus-logo.svg"/>
<div class="tabs">
<tab-item v-for="item in tabs" :name="item.name"
:active="item.id === tab" @click="changePage(item)"/>
<tab-item v-for="item in tabs"
:name="item.name" :active="item.id === tab"
@click="changePage(item)"/>
</div>
<el-switch style="margin: 0 20px"
v-model="dark" active-color="#424242"
:active-action-icon="Moon"
@ -30,7 +32,6 @@
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-content">
<router-view v-slot="{ Component }">
@ -48,20 +49,19 @@
import { logout } from '@/net'
import router from "@/router";
import {Back, Moon, Sunny} from "@element-plus/icons-vue";
import TabItem from "@/component/TabItem.vue";
import {ref} from "vue";
import {useDark} from "@vueuse/core";
import TabItem from "@/component/TabItem.vue";
import {useRoute} from "vue-router";
import {useStore} from "@/store";
const store = useStore()
function userLogout() {
logout(() => router.push("/"))
}
const route = useRoute()
const dark = ref(useDark())
const tabs = [
{id: 1, name: '管理', route: 'manage'},
{id: 2, name: '安全', route: 'security'}
]
const defaultIndex = () => {
for (let tab of tabs) {
if(route.name === tab.route)
@ -69,15 +69,17 @@ const defaultIndex = () => {
}
return 1
}
const dark = ref(useDark())
const tabs = [
{id: 1, name: '管理', route: 'manage'},
{id: 2, name: '安全', route: 'security'}
]
const tab = ref(defaultIndex())
function changePage(item) {
tab.value = item.id
router.push({name: item.route})
}
function userLogout() {
logout(() => router.push("/"))
}
</script>
<style scoped>
@ -95,11 +97,15 @@ function userLogout() {
.tabs {
height: 55px;
gap: 10px;
flex: 1px;
flex: 1;
display: flex;
align-items: center;
justify-content: right;
}
.avatar {
border: solid 1px var(--el-border-color);
}
}
.main-content {

View File

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

View File

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

View File

@ -1,10 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="GENERAL_MODULE" version="4">
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/config" />
<excludeFolder url="file://$MODULE_DIR$/log" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

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