Compare commits

...

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

121 changed files with 573 additions and 3573 deletions

6
.gitignore vendored
View File

@ -1,4 +1,2 @@
.idea .idea/
log log/
.DS_Store
config

View File

@ -9,9 +9,9 @@
<relativePath/> <!-- lookup parent from repository --> <relativePath/> <!-- lookup parent from repository -->
</parent> </parent>
<groupId>com.example</groupId> <groupId>com.example</groupId>
<artifactId>itbaima-monitor-server</artifactId> <artifactId>itbaima-monitor-backend</artifactId>
<version>0.0.1-SNAPSHOT</version> <version>0.0.1-SNAPSHOT</version>
<name>itbaima-monitor-server</name> <name>my-project-backend</name>
<description>my-project-backend</description> <description>my-project-backend</description>
<properties> <properties>
<java.version>17</java.version> <java.version>17</java.version>
@ -94,21 +94,6 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version> <version>2.1.0</version>
</dependency> </dependency>
<!-- InfluxDB客户端框架 -->
<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> </dependencies>
<profiles> <profiles>

View File

@ -4,10 +4,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class MonitorServerApplication { public class MyProjectBackendApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(MonitorServerApplication.class, args); SpringApplication.run(MyProjectBackendApplication.class, args);
} }
} }

View File

@ -53,12 +53,9 @@ public class SecurityConfiguration {
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http return http
.authorizeHttpRequests(conf -> conf .authorizeHttpRequests(conf -> conf
.requestMatchers("/terminal/**").permitAll()
.requestMatchers("/api/auth/**", "/error").permitAll() .requestMatchers("/api/auth/**", "/error").permitAll()
.requestMatchers("/monitor/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/user/sub/**").hasRole(Const.ROLE_ADMIN) .anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
.anyRequest().hasAnyRole(Const.ROLE_ADMIN, Const.ROLE_NORMAL)
) )
.formLogin(conf -> conf .formLogin(conf -> conf
.loginProcessingUrl("/api/auth/login") .loginProcessingUrl("/api/auth/login")

View File

@ -38,7 +38,7 @@ public class AuthorizeController {
@GetMapping("/ask-code") @GetMapping("/ask-code")
@Operation(summary = "请求邮件验证码") @Operation(summary = "请求邮件验证码")
public RestBean<Void> askVerifyCode(@RequestParam @Email String email, public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
@RequestParam @Pattern(regexp = "(reset|modify)") String type, @RequestParam @Pattern(regexp = "(reset)") String type,
HttpServletRequest request){ HttpServletRequest request){
return this.messageHandle(() -> return this.messageHandle(() ->
accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr())); accountService.registerEmailVerifyCode(type, String.valueOf(email), request.getRemoteAddr()));

View File

@ -0,0 +1,25 @@
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, "注册码填写错误,注册主机失败");
}
}

View File

@ -34,10 +34,6 @@ public record RestBean<T> (long id, int code, T data, String message) {
return new RestBean<>(requestId(), code, null, message); return new RestBean<>(requestId(), code, null, message);
} }
public static <T> RestBean<T> noPermission() {
return new RestBean<>(requestId(), 401, null, "权限不足,拒绝访问");
}
/** /**
* 快速将当前实体转换为JSON字符串格式 * 快速将当前实体转换为JSON字符串格式
* @return JSON字符串 * @return JSON字符串

View File

@ -1,6 +1,5 @@
package com.example.entity.dto; package com.example.entity.dto;
import com.alibaba.fastjson2.JSONArray;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
@ -8,9 +7,7 @@ import com.example.entity.BaseData;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.List;
/** /**
* 数据库中的用户信息 * 数据库中的用户信息
@ -26,10 +23,4 @@ public class Account implements BaseData {
String email; String email;
String role; String role;
Date registerTime; Date registerTime;
String clients;
public List<Integer> getClientList() {
if(clients == null) return Collections.emptyList();
return JSONArray.parse(clients).toList(Integer.class);
}
} }

View File

@ -2,21 +2,17 @@ package com.example.entity.dto;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.example.entity.BaseData;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;
@Data @Data
@TableName("db_client")
@AllArgsConstructor @AllArgsConstructor
public class Client implements BaseData { @TableName("db_server")
public class Server {
@TableId @TableId
Integer id; int id;
String name; String name;
String token; Date time;
String location;
String node;
Date registerTime;
} }

View File

@ -5,15 +5,14 @@ import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
@Data @Data
@TableName("db_client_detail") @TableName("db_server_info")
public class ClientDetail { public class ServerInfo {
@TableId @TableId
Integer id; int id;
String osArch; String osArch;
String osName; String osName;
String osVersion; String osVersion;
int osBit; int osBit;
String cpuName;
int cpuCore; int cpuCore;
double memory; double memory;
double disk; double disk;

View File

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

View File

@ -4,7 +4,7 @@ import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
@Data @Data
public class ClientDetailVO { public class RegisterServerVO {
@NotNull @NotNull
String osArch; String osArch;
@NotNull @NotNull
@ -14,8 +14,6 @@ public class ClientDetailVO {
@NotNull @NotNull
int osBit; int osBit;
@NotNull @NotNull
String cpuName;
@NotNull
int cpuCore; int cpuCore;
@NotNull @NotNull
double memory; double memory;
@ -23,4 +21,6 @@ public class ClientDetailVO {
double disk; double disk;
@NotNull @NotNull
String ip; String ip;
@NotNull
String accessToken;
} }

View File

@ -10,7 +10,6 @@ import java.util.Date;
@Data @Data
public class AuthorizeVO { public class AuthorizeVO {
String username; String username;
String email;
String role; String role;
String token; String token;
Date expire; Date expire;

View File

@ -0,0 +1,46 @@
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);
}
}

View File

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

View File

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

View File

@ -1,9 +1,9 @@
package com.example.mapper; package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.ClientSsh; import com.example.entity.dto.ServerInfo;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface ClientSshMapper extends BaseMapper<ClientSsh> { public interface ServerInfoMapper extends BaseMapper<ServerInfo> {
} }

View File

@ -1,9 +1,9 @@
package com.example.mapper; package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.Client; import com.example.entity.dto.Server;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface ClientMapper extends BaseMapper<Client> { public interface ServerMapper extends BaseMapper<Server> {
} }

View File

@ -3,22 +3,12 @@ package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.Account; import com.example.entity.dto.Account;
import com.example.entity.vo.request.ConfirmResetVO; 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.EmailResetVO;
import com.example.entity.vo.request.ModifyEmailVO;
import com.example.entity.vo.response.SubAccountVO;
import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List;
public interface AccountService extends IService<Account>, UserDetailsService { public interface AccountService extends IService<Account>, UserDetailsService {
Account findAccountByNameOrEmail(String text); Account findAccountByNameOrEmail(String text);
String registerEmailVerifyCode(String type, String email, String address); String registerEmailVerifyCode(String type, String email, String address);
String resetEmailAccountPassword(EmailResetVO info); String resetEmailAccountPassword(EmailResetVO info);
String resetConfirm(ConfirmResetVO 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);
} }

View File

@ -0,0 +1,9 @@
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);
}

View File

@ -1,14 +1,10 @@
package com.example.service.impl; package com.example.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.Account; import com.example.entity.dto.Account;
import com.example.entity.vo.request.ConfirmResetVO; 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.EmailResetVO;
import com.example.entity.vo.request.ModifyEmailVO;
import com.example.entity.vo.response.SubAccountVO;
import com.example.mapper.AccountMapper; import com.example.mapper.AccountMapper;
import com.example.service.AccountService; import com.example.service.AccountService;
import com.example.utils.Const; import com.example.utils.Const;
@ -23,8 +19,6 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -122,61 +116,6 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return null; 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中存储的邮件验证码 * 移除Redis中存储的邮件验证码
* @param email 电邮 * @param email 电邮
@ -217,4 +156,22 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
.eq("email", text) .eq("email", text)
.one(); .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));
}
} }

View File

@ -0,0 +1,47 @@
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);
}
}

View File

@ -7,8 +7,6 @@ public final class Const {
//JWT令牌 //JWT令牌
public final static String JWT_BLACK_LIST = "jwt:blacklist:"; public final static String JWT_BLACK_LIST = "jwt:blacklist:";
public final static String JWT_FREQUENCY = "jwt:frequency:"; public final static String JWT_FREQUENCY = "jwt:frequency:";
//用户
public final static String USER_BLACK_LIST = "user:blacklist:";
//请求频率限制 //请求频率限制
public final static String FLOW_LIMIT_COUNTER = "flow:counter:"; public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
public final static String FLOW_LIMIT_BLOCK = "flow:block:"; public final static String FLOW_LIMIT_BLOCK = "flow:block:";
@ -20,11 +18,9 @@ public final class Const {
public final static int ORDER_CORS = -102; public final static int ORDER_CORS = -102;
//请求自定义属性 //请求自定义属性
public final static String ATTR_USER_ID = "userId"; 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 MQ_MAIL = "mail";
//用户角色 //用户角色
public final static String ROLE_ADMIN = "admin"; public final static String ROLE_DEFAULT = "user";
public final static String ROLE_NORMAL = "user";
} }

View File

@ -109,7 +109,6 @@ public class JwtUtils {
try { try {
DecodedJWT verify = jwtVerifier.verify(token); DecodedJWT verify = jwtVerifier.verify(token);
if(this.isInvalidToken(verify.getId())) return null; if(this.isInvalidToken(verify.getId())) return null;
if(this.isInvalidUser(verify.getClaim("id").asInt())) return null;
Map<String, Claim> claims = verify.getClaims(); Map<String, Claim> claims = verify.getClaims();
return new Date().after(claims.get("exp").asDate()) ? null : verify; return new Date().after(claims.get("exp").asDate()) ? null : verify;
} catch (JWTVerificationException e) { } catch (JWTVerificationException e) {
@ -178,14 +177,6 @@ public class JwtUtils {
return true; return true;
} }
public void deleteUser(int uid) {
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
}
private boolean isInvalidUser(int uid){
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
}
/** /**
* 验证Token是否被列入Redis黑名单 * 验证Token是否被列入Redis黑名单
* @param uuid 令牌ID * @param uuid 令牌ID

View File

@ -4,10 +4,6 @@ springdoc:
swagger-ui: swagger-ui:
operations-sorter: alpha operations-sorter: alpha
spring: spring:
influx:
url: http://localhost:8086
user: admin
password: 12345678
mail: mail:
host: smtp.163.com host: smtp.163.com
username: javastudy111@163.com username: javastudy111@163.com
@ -36,8 +32,8 @@ spring:
verify: verify:
mail-limit: 60 mail-limit: 60
flow: flow:
period: 5 period: 3
limit: 100 limit: 50
block: 30 block: 30
cors: cors:
origin: '*' origin: '*'

View File

@ -8,10 +8,6 @@ mybatis-plus:
configuration: configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
spring: spring:
influx:
url: http://localhost:8086
user: admin
password: 12345678
mail: mail:
host: smtp.163.com host: smtp.163.com
username: javastudy111@163.com username: javastudy111@163.com
@ -40,8 +36,8 @@ spring:
verify: verify:
mail-limit: 60 mail-limit: 60
flow: flow:
period: 5 period: 3
limit: 100 limit: 10
block: 30 block: 30
cors: cors:
origin: '*' origin: '*'

View File

@ -0,0 +1,13 @@
package com.example;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MyProjectBackendApplicationTests {
@Test
void contextLoads() {
}
}

View File

@ -1,68 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
<modelVersion>4.0.0</modelVersion> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<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>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> <parent>
<groupId>org.projectlombok</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>lombok</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<optional>true</optional> <version>3.1.2</version>
</dependency> <relativePath/> <!-- lookup parent from repository -->
<dependency> </parent>
<groupId>org.springframework.boot</groupId> <groupId>com.example</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>itbaima-monitor-client</artifactId>
<scope>test</scope> <version>0.0.1-SNAPSHOT</version>
</dependency> <name>my-project-backend</name>
<dependency> <description>my-project-backend</description>
<groupId>org.springframework.boot</groupId> <properties>
<artifactId>spring-boot-starter-quartz</artifactId> <java.version>17</java.version>
</dependency> </properties>
<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>
</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>
<dependencies>
<dependency>
<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>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.github.oshi</groupId>
<artifactId>oshi-core</artifactId>
<version>6.4.0</version>
</dependency>
</dependencies>
</project> </project>

View File

@ -1,96 +0,0 @@
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;
}
}

View File

@ -1,12 +0,0 @@
package com.example.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class ConnectionConfig {
String address;
String token;
String networkInterface;
}

View File

@ -1,22 +0,0 @@
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());
}
}

View File

@ -1,17 +0,0 @@
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;
}

View File

@ -1,131 +0,0 @@
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;
}
}

View File

@ -1,87 +0,0 @@
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);
}
}
}

View File

@ -1,13 +1,11 @@
package com.example; package com.test;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication @SpringBootApplication
public class MonitorClientApplication { public class MonitorClientApplication {
public static void main(String[] args) {
public static void main(String[] args) { SpringApplication.run(MonitorClientApplication.class, args);
SpringApplication.run(MonitorClientApplication.class, args); }
}
} }

View File

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

View File

@ -1,16 +1,15 @@
package com.example.entity; package com.test.entity;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
@Data @Data
@Accessors(chain = true) @Accessors(chain = true)
public class BaseDetail { public class SystemData {
String osArch; String osArch;
String osName; String osName;
String osVersion; String osVersion;
int osBit; int osBit;
String cpuName;
int cpuCore; int cpuCore;
double memory; double memory;
double disk; double disk;

View File

@ -1,9 +1,5 @@
package com.example.task; package com.test.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.JobExecutionContext;
import org.quartz.JobExecutionException; import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean; import org.springframework.scheduling.quartz.QuartzJobBean;
@ -12,15 +8,8 @@ import org.springframework.stereotype.Component;
@Component @Component
public class MonitorJobBean extends QuartzJobBean { public class MonitorJobBean extends QuartzJobBean {
@Resource
MonitorUtils monitor;
@Resource
NetUtils net;
@Override @Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException { protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
RuntimeDetail runtimeDetail = monitor.monitorRuntimeDetail();
net.updateRuntimeDetails(runtimeDetail);
} }
} }

View File

@ -0,0 +1,5 @@
package com.test.util;
public interface DataGetter<T> {
T getData();
}

View File

@ -0,0 +1,57 @@
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;
}
}
}

View File

@ -5,5 +5,6 @@
<content url="file://$MODULE_DIR$" /> <content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="all" level="application" />
</component> </component>
</module> </module>

View File

@ -11,15 +11,10 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.3.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"echarts": "^5.4.3",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"flag-icon-css": "^4.1.7", "flag-icon-css": "^4.1.7",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"vue": "^3.3.4", "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": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",
@ -873,20 +868,6 @@
"node": ">=0.4.0" "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": { "node_modules/element-plus": {
"version": "2.3.9", "version": "2.3.9",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz", "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.3.9.tgz",
@ -1444,12 +1425,13 @@
} }
}, },
"node_modules/needle": { "node_modules/needle": {
"version": "3.3.1", "version": "3.2.0",
"resolved": "https://registry.npmmirror.com/needle/-/needle-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/needle/-/needle-3.2.0.tgz",
"integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"dependencies": { "dependencies": {
"debug": "^3.2.6",
"iconv-lite": "^0.6.3", "iconv-lite": "^0.6.3",
"sax": "^1.2.4" "sax": "^1.2.4"
}, },
@ -1460,6 +1442,16 @@
"node": ">= 4.4.x" "node": ">= 4.4.x"
} }
}, },
"node_modules/needle/node_modules/debug": {
"version": "3.2.7",
"resolved": "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"dev": true,
"optional": true,
"dependencies": {
"ms": "^2.1.1"
}
},
"node_modules/normalize-path": { "node_modules/normalize-path": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz",
@ -1519,58 +1511,6 @@
"node": ">=6" "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": { "node_modules/pkg-types": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.0.3.tgz",
@ -1960,32 +1900,6 @@
"resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz", "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz",
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==", "integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
"dev": true "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=="
} }
} }
} }

View File

@ -11,15 +11,10 @@
"@element-plus/icons-vue": "^2.1.0", "@element-plus/icons-vue": "^2.1.0",
"@vueuse/core": "^10.3.0", "@vueuse/core": "^10.3.0",
"axios": "^1.4.0", "axios": "^1.4.0",
"echarts": "^5.4.3",
"element-plus": "^2.3.9", "element-plus": "^2.3.9",
"flag-icon-css": "^4.1.7", "flag-icon-css": "^4.1.7",
"pinia": "^2.1.7",
"pinia-plugin-persistedstate": "^3.2.0",
"vue": "^3.3.4", "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": { "devDependencies": {
"@vitejs/plugin-vue": "^4.2.3", "@vitejs/plugin-vue": "^4.2.3",

View File

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,109 @@
<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>

View File

@ -3,19 +3,13 @@ import App from './App.vue'
import router from './router' import router from './router'
import axios from "axios"; import axios from "axios";
import '@/assets/css/element.less'
import 'flag-icon-css/css/flag-icons.min.css' import 'flag-icon-css/css/flag-icons.min.css'
import 'element-plus/theme-chalk/dark/css-vars.css' import 'element-plus/theme-chalk/dark/css-vars.css'
import {createPinia} from "pinia";
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
axios.defaults.baseURL = 'http://localhost:8080' axios.defaults.baseURL = 'http://localhost:8080'
const app = createApp(App) const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
pinia.use(piniaPluginPersistedstate)
app.use(router) app.use(router)
app.mount('#app') app.mount('#app')

View File

@ -1,6 +1,5 @@
import axios from "axios"; import axios from "axios";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {useStore} from "@/store";
const authItemName = "authorize" const authItemName = "authorize"
@ -75,10 +74,6 @@ function login(username, password, remember, success, failure = defaultFailure){
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
}, (data) => { }, (data) => {
storeAccessToken(remember, data.token, data.expire) storeAccessToken(remember, data.token, data.expire)
const store = useStore()
store.user.role = data.role
store.user.username = data.username
store.user.email = data.email
ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`) ElMessage.success(`登录成功,欢迎 ${data.username} 来到我们的系统`)
success(data) success(data)
}, failure) }, failure)

View File

@ -0,0 +1,97 @@
<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>

View File

@ -2,7 +2,7 @@
<div style="width: 100vw;height: 100vh;overflow: hidden;display: flex"> <div style="width: 100vw;height: 100vh;overflow: hidden;display: flex">
<div style="flex: 1"> <div style="flex: 1">
<el-image style="width: 100%;height: 100%" fit="cover" <el-image style="width: 100%;height: 100%" fit="cover"
src="https://img1.baidu.com/it/u=4097856652,4033702227&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800"/> src="https://img0.baidu.com/it/u=1201094067,1609021147&fm=253&fmt=auto&app=120&f=JPEG?w=1422&h=800"/>
</div> </div>
<div class="welcome-title"> <div class="welcome-title">
<div style="font-size: 30px;font-weight: bold">欢迎来到我们的学习平台</div> <div style="font-size: 30px;font-weight: bold">欢迎来到我们的学习平台</div>

View File

@ -0,0 +1,33 @@
<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>

View File

@ -0,0 +1,11 @@
<script setup>
</script>
<template>
</template>
<style scoped>
</style>

View File

@ -2,9 +2,7 @@
<module type="GENERAL_MODULE" version="4"> <module type="GENERAL_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output /> <exclude-output />
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$" />
<excludeFolder url="file://$MODULE_DIR$/log" />
</content>
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@ -1,13 +0,0 @@
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();
}
}

View File

@ -1,39 +0,0 @@
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();
}
}

View File

@ -1,166 +0,0 @@
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);
}
}

View File

@ -1,60 +0,0 @@
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());
}
}

View File

@ -1,16 +0,0 @@
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;
}

View File

@ -1,30 +0,0 @@
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;
}

View File

@ -1,12 +0,0 @@
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;
}

View File

@ -1,20 +0,0 @@
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;
}

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,14 +0,0 @@
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;
}

View File

@ -1,25 +0,0 @@
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;
}

View File

@ -1,17 +0,0 @@
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;
}

View File

@ -1,19 +0,0 @@
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;
}

View File

@ -1,21 +0,0 @@
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;
}

View File

@ -1,13 +0,0 @@
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;
}

View File

@ -1,14 +0,0 @@
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<>();
}

View File

@ -1,11 +0,0 @@
package com.example.entity.vo.response;
import lombok.Data;
@Data
public class SshSettingsVO {
String ip;
int port = 22;
String username;
String password;
}

View File

@ -1,12 +0,0 @@
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;
}

View File

@ -1,93 +0,0 @@
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);
}
}
}

View File

@ -1,9 +0,0 @@
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> {
}

View File

@ -1,27 +0,0 @@
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);
}

Some files were not shown because too many files have changed in this diff Show More