Init Commit
This commit is contained in:
commit
b91c790f8c
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
.idea/
|
||||
log/
|
7
README.md
Normal file
7
README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# 前后端分离项目(JWT方案)
|
||||
采用SpringBoot3 + Vue3编写的前后端分离模版项目,集成多种技术栈,使用JWT校验方案。
|
||||
***
|
||||
### 后端功能与技术点
|
||||
* 用户注册、用户登录、重置密码
|
||||
|
||||
### 前端功能与技术点
|
33
my-project-backend/.gitignore
vendored
Normal file
33
my-project-backend/.gitignore
vendored
Normal file
@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
153
my-project-backend/pom.xml
Normal file
153
my-project-backend/pom.xml
Normal file
@ -0,0 +1,153 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>my-project-backend</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>my-project-backend</name>
|
||||
<description>my-project-backend</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- 邮件发送模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-mail</artifactId>
|
||||
</dependency>
|
||||
<!-- 接口参数校验模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<!-- 授权校验模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<!-- 基础Web模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Redis交互模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- Mybatis-Plus框架 -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.3.1</version>
|
||||
</dependency>
|
||||
<!-- MySQL驱动 -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- Lombok框架 -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- 测试模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- 消息队列模块 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-amqp</artifactId>
|
||||
</dependency>
|
||||
<!-- FastJSON2框架 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.25</version>
|
||||
</dependency>
|
||||
<!-- Jwt令牌生成校验框架 -->
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<profiles>
|
||||
<!-- 开发环境 -->
|
||||
<profile>
|
||||
<id>dev</id>
|
||||
<activation>
|
||||
<activeByDefault>true</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<environment>dev</environment>
|
||||
</properties>
|
||||
</profile>
|
||||
<!-- 生产环境 -->
|
||||
<profile>
|
||||
<id>prod</id>
|
||||
<activation>
|
||||
<activeByDefault>false</activeByDefault>
|
||||
</activation>
|
||||
<properties>
|
||||
<environment>prod</environment>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.graalvm.buildtools</groupId>
|
||||
<artifactId>native-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<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>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<excludes>
|
||||
<exclude>application*.yml</exclude>
|
||||
</excludes>
|
||||
</resource>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
<includes>
|
||||
<include>application.yml</include>
|
||||
<include>application-${environment}.yml</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,13 @@
|
||||
package com.example;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class MyProjectBackendApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MyProjectBackendApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.example.config;
|
||||
|
||||
import org.springframework.amqp.core.Queue;
|
||||
import org.springframework.amqp.core.QueueBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* RabbitMQ消息队列配置
|
||||
*/
|
||||
@Configuration
|
||||
public class RabbitConfiguration {
|
||||
@Bean("mailQueue")
|
||||
public Queue queue(){
|
||||
return QueueBuilder
|
||||
.durable("mail")
|
||||
.build();
|
||||
}
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
package com.example.config;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.response.AuthorizeVO;
|
||||
import com.example.filter.JwtAuthenticationFilter;
|
||||
import com.example.filter.RequestLogFilter;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.JwtUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* SpringSecurity相关配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SecurityConfiguration {
|
||||
|
||||
@Resource
|
||||
JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Resource
|
||||
RequestLogFilter requestLogFilter;
|
||||
|
||||
@Resource
|
||||
JwtUtils utils;
|
||||
|
||||
@Resource
|
||||
AccountService service;
|
||||
|
||||
/**
|
||||
* 针对于 SpringSecurity 6 的新版配置方法
|
||||
* @param http 配置器
|
||||
* @return 自动构建的内置过滤器链
|
||||
* @throws Exception 可能的异常
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(conf -> conf
|
||||
.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/error").permitAll()
|
||||
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
|
||||
)
|
||||
.formLogin(conf -> conf
|
||||
.loginProcessingUrl("/api/auth/login")
|
||||
.failureHandler(this::handleProcess)
|
||||
.successHandler(this::handleProcess)
|
||||
.permitAll()
|
||||
)
|
||||
.logout(conf -> conf
|
||||
.logoutUrl("/api/auth/logout")
|
||||
.logoutSuccessHandler(this::onLogoutSuccess)
|
||||
)
|
||||
.exceptionHandling(conf -> conf
|
||||
.accessDeniedHandler(this::handleProcess)
|
||||
.authenticationEntryPoint(this::handleProcess)
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(conf -> conf
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.addFilterBefore(requestLogFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(jwtAuthenticationFilter, RequestLogFilter.class)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将多种类型的Handler整合到同一个方法中,包含:
|
||||
* - 登录成功
|
||||
* - 登录失败
|
||||
* - 未登录拦截/无权限拦截
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
* @param exceptionOrAuthentication 异常或是验证实体
|
||||
* @throws IOException 可能的异常
|
||||
*/
|
||||
private void handleProcess(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object exceptionOrAuthentication) throws IOException {
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
PrintWriter writer = response.getWriter();
|
||||
if(exceptionOrAuthentication instanceof AccessDeniedException exception) {
|
||||
writer.write(RestBean
|
||||
.forbidden(exception.getMessage()).asJsonString());
|
||||
} else if(exceptionOrAuthentication instanceof Exception exception) {
|
||||
writer.write(RestBean
|
||||
.unauthorized(exception.getMessage()).asJsonString());
|
||||
} else if(exceptionOrAuthentication instanceof Authentication authentication){
|
||||
User user = (User) authentication.getPrincipal();
|
||||
Account account = service.findAccountByNameOrEmail(user.getUsername());
|
||||
String jwt = utils.createJwt(user, account.getUsername(), account.getId());
|
||||
if(jwt == null) {
|
||||
writer.write(RestBean.forbidden("登录验证频繁,请稍后再试").asJsonString());
|
||||
} else {
|
||||
AuthorizeVO vo = account.asViewObject(AuthorizeVO.class, o -> o.setToken(jwt));
|
||||
writer.write(RestBean.success(vo).asJsonString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录处理,将对应的Jwt令牌列入黑名单不再使用
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
* @param authentication 验证实体
|
||||
* @throws IOException 可能的异常
|
||||
*/
|
||||
private void onLogoutSuccess(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Authentication authentication) throws IOException {
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
PrintWriter writer = response.getWriter();
|
||||
String authorization = request.getHeader("Authorization");
|
||||
if(utils.invalidateJwt(authorization)) {
|
||||
writer.write(RestBean.success("退出登录成功").asJsonString());
|
||||
return;
|
||||
}
|
||||
writer.write(RestBean.failure(400, "退出登录失败").asJsonString());
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.example.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* 一般Web服务相关配置
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder(){
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
package com.example.controller;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.entity.vo.request.EmailRegisterVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import com.example.service.AccountService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 用于验证相关Controller包含用户的注册、重置密码等操作
|
||||
*/
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthorizeController {
|
||||
|
||||
@Resource
|
||||
AccountService accountService;
|
||||
|
||||
/**
|
||||
* 请求邮件验证码
|
||||
* @param email 邮件
|
||||
* @param request 请求
|
||||
* @return 是否请求成功
|
||||
*/
|
||||
@PostMapping("/ask-code")
|
||||
public RestBean<Void> askVerifyCode(@RequestParam @Email String email,
|
||||
HttpServletRequest request){
|
||||
return this.messageHandle(() ->
|
||||
accountService.registerEmailVerifyCode(String.valueOf(email), request.getRemoteAddr()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行用户注册操作,需要先请求邮件验证码
|
||||
* @param vo 注册信息
|
||||
* @return 是否注册成功
|
||||
*/
|
||||
@PostMapping("/register")
|
||||
public RestBean<Void> register(@Valid EmailRegisterVO vo){
|
||||
return this.messageHandle(() ->
|
||||
accountService.registerEmailAccount(vo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行密码重置操作
|
||||
* @param vo 密码重置信息
|
||||
* @return 是否操作成功
|
||||
*/
|
||||
@PostMapping("/reset-password")
|
||||
public RestBean<Void> reset(@Valid EmailResetVO vo){
|
||||
return this.messageHandle(() ->
|
||||
accountService.resetEmailAccountPassword(vo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对于返回值为String作为错误信息的方法进行统一处理
|
||||
* @param action 具体操作
|
||||
* @return 响应结果
|
||||
* @param <T> 响应结果类型
|
||||
*/
|
||||
private <T> RestBean<T> messageHandle(Supplier<String> action){
|
||||
String message = action.get();
|
||||
if(message == null)
|
||||
return RestBean.success();
|
||||
else
|
||||
return RestBean.failure(400, message);
|
||||
}
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.example.controller.exception;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;
|
||||
import org.springframework.boot.web.error.ErrorAttributeOptions;
|
||||
import org.springframework.boot.web.servlet.error.ErrorAttributes;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 专用用于处理错误页面的Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping({"${server.error.path:${error.path:/error}}"})
|
||||
public class ErrorPageController extends AbstractErrorController {
|
||||
|
||||
public ErrorPageController(ErrorAttributes errorAttributes) {
|
||||
super(errorAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有错误在这里统一处理,自动解析状态码和原因
|
||||
* @param request 请求
|
||||
* @return 失败响应
|
||||
*/
|
||||
@RequestMapping
|
||||
public RestBean<Void> error(HttpServletRequest request) {
|
||||
HttpStatus status = this.getStatus(request);
|
||||
Map<String, Object> errorAttributes = this.getErrorAttributes(request, this.getAttributeOptions());
|
||||
String message = this.convertErrorMessage(status)
|
||||
.orElse(errorAttributes.get("message").toString());
|
||||
return RestBean.failure(status.value(), message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对于一些特殊的状态码,错误信息转换
|
||||
* @param status 状态码
|
||||
* @return 错误信息
|
||||
*/
|
||||
private Optional<String> convertErrorMessage(HttpStatus status){
|
||||
String value = switch (status.value()) {
|
||||
case 400 -> "请求参数有误";
|
||||
case 404 -> "请求的接口不存在";
|
||||
case 405 -> "请求方法错误";
|
||||
case 500 -> "内部错误,请联系管理员";
|
||||
default -> null;
|
||||
};
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误属性获取选项,这里额外添加了错误消息和异常类型
|
||||
* @return 选项
|
||||
*/
|
||||
private ErrorAttributeOptions getAttributeOptions(){
|
||||
return ErrorAttributeOptions
|
||||
.defaults()
|
||||
.including(ErrorAttributeOptions.Include.MESSAGE,
|
||||
ErrorAttributeOptions.Include.EXCEPTION);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.example.controller.exception;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import jakarta.validation.ValidationException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
/**
|
||||
* 用于接口参数校验处理的控制器
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class ValidationController {
|
||||
|
||||
/**
|
||||
* 与SpringBoot保持一致,校验不通过打印警告信息,而不是直接抛出异常
|
||||
* @param exception 验证异常
|
||||
* @return 校验结果
|
||||
*/
|
||||
@ExceptionHandler(ValidationException.class)
|
||||
public RestBean<Void> validateError(ValidationException exception) {
|
||||
log.warn("Resolved [{}: {}]", exception.getClass().getName(), exception.getMessage());
|
||||
return RestBean.failure(400, "请求参数有误");
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
package com.example.entity;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 用于DTO快速转换VO实现,只需将DTO类继承此类即可使用
|
||||
*/
|
||||
public interface BaseData {
|
||||
/**
|
||||
* 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
|
||||
* @param clazz 指定VO类型
|
||||
* @param consumer 返回VO对象之前可以使用Lambda进行额外处理
|
||||
* @return 指定VO对象
|
||||
* @param <V> 指定VO类型
|
||||
*/
|
||||
default <V> V asViewObject(Class<V> clazz, Consumer<V> consumer) {
|
||||
V v = this.asViewObject(clazz);
|
||||
consumer.accept(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定的VO类并将当前DTO对象中的所有成员变量值直接复制到VO对象中
|
||||
* @param clazz 指定VO类型
|
||||
* @return 指定VO对象
|
||||
* @param <V> 指定VO类型
|
||||
*/
|
||||
default <V> V asViewObject(Class<V> clazz) {
|
||||
try {
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
Constructor<V> constructor = clazz.getConstructor();
|
||||
V v = constructor.newInstance();
|
||||
Arrays.asList(fields).forEach(field -> convert(field, v));
|
||||
return v;
|
||||
} catch (ReflectiveOperationException exception) {
|
||||
Logger logger = LoggerFactory.getLogger(BaseData.class);
|
||||
logger.error("在VO与DTO转换时出现了一些错误", exception);
|
||||
throw new RuntimeException(exception.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部使用,快速将当前类中目标对象字段同名字段的值复制到目标对象字段上
|
||||
* @param field 目标对象字段
|
||||
* @param target 目标对象
|
||||
*/
|
||||
private void convert(Field field, Object target){
|
||||
try {
|
||||
Field source = this.getClass().getDeclaredField(field.getName());
|
||||
field.setAccessible(true);
|
||||
source.setAccessible(true);
|
||||
field.set(target, source.get(this));
|
||||
} catch (IllegalAccessException | NoSuchFieldException ignored) {}
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
package com.example.entity;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.alibaba.fastjson2.JSONWriter;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 响应实体类封装,Rest风格
|
||||
* @param code 状态码
|
||||
* @param data 响应数据
|
||||
* @param message 其他消息
|
||||
* @param <T> 响应数据类型
|
||||
*/
|
||||
public record RestBean<T> (long id, int code, T data, String message) {
|
||||
public static <T> RestBean<T> success(T data){
|
||||
return new RestBean<>(requestId(), 200, data, "请求成功");
|
||||
}
|
||||
|
||||
public static <T> RestBean<T> success(){
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> RestBean<T> forbidden(String message){
|
||||
return failure(403, message);
|
||||
}
|
||||
|
||||
public static <T> RestBean<T> unauthorized(String message){
|
||||
return failure(401, message);
|
||||
}
|
||||
|
||||
public static <T> RestBean<T> failure(int code, String message){
|
||||
return new RestBean<>(requestId(), code, null, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速将当前实体转换为JSON字符串格式
|
||||
* @return JSON字符串
|
||||
*/
|
||||
public String asJsonString() {
|
||||
return JSONObject.toJSONString(this, JSONWriter.Feature.WriteNulls);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求ID方便快速定位错误
|
||||
* @return ID
|
||||
*/
|
||||
private static long requestId(){
|
||||
String requestId = Optional.ofNullable(MDC.get("reqId")).orElse("0");
|
||||
return Long.parseLong(requestId);
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.example.entity.dto;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.example.entity.BaseData;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 数据库中的用户信息
|
||||
*/
|
||||
@Data
|
||||
@TableName("db_account")
|
||||
@AllArgsConstructor
|
||||
public class Account implements BaseData {
|
||||
@TableId(type = IdType.AUTO)
|
||||
Integer id;
|
||||
String username;
|
||||
String password;
|
||||
String email;
|
||||
String role;
|
||||
Date registerTime;
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 用户注册表单信息
|
||||
*/
|
||||
@Data
|
||||
public class EmailRegisterVO {
|
||||
@Email
|
||||
String email;
|
||||
@Length(max = 6, min = 6)
|
||||
String code;
|
||||
@Length(min = 1, max = 10)
|
||||
String username;
|
||||
@Length(min = 6, max = 20)
|
||||
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=]).*$")
|
||||
String password;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.example.entity.vo.request;
|
||||
|
||||
import jakarta.validation.constraints.Email;
|
||||
import lombok.Data;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
/**
|
||||
* 密码重置表单实体
|
||||
*/
|
||||
@Data
|
||||
public class EmailResetVO {
|
||||
@Email
|
||||
String email;
|
||||
@Length(max = 6, min = 6)
|
||||
String code;
|
||||
@Length(min = 6, max = 20)
|
||||
//@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=]).*$")
|
||||
String password;
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.example.entity.vo.response;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录验证成功的用户信息响应
|
||||
*/
|
||||
@Data
|
||||
public class AuthorizeVO {
|
||||
String username;
|
||||
String role;
|
||||
String token;
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
package com.example.filter;
|
||||
|
||||
import com.example.utils.Const;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 跨域配置过滤器,仅处理跨域,添加跨域响应头
|
||||
*/
|
||||
@Component
|
||||
@Order(Const.ORDER_CORS)
|
||||
public class CorsFilter extends HttpFilter {
|
||||
|
||||
@Value("${spring.web.cors.origin}")
|
||||
String origin;
|
||||
|
||||
@Value("${spring.web.cors.credentials}")
|
||||
boolean credentials;
|
||||
|
||||
@Value("${spring.web.cors.methods}")
|
||||
String methods;
|
||||
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
chain.doFilter(request, response);
|
||||
this.addCorsHeader(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加所有跨域相关响应头
|
||||
* @param request 请求
|
||||
* @param response 响应
|
||||
*/
|
||||
private void addCorsHeader(HttpServletRequest request, HttpServletResponse response) {
|
||||
response.addHeader("Access-Control-Allow-Origin", this.resolveMethod());
|
||||
response.addHeader("Access-Control-Allow-Methods", this.resolveOrigin(request));
|
||||
if(credentials) {
|
||||
response.addHeader("Access-Control-Allow-Credentials", "true");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置文件中的请求方法
|
||||
* @return 解析得到的请求头值
|
||||
*/
|
||||
private String resolveMethod(){
|
||||
return methods;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析配置文件中的请求原始站点
|
||||
* @param request 请求
|
||||
* @return 解析得到的请求头值
|
||||
*/
|
||||
private String resolveOrigin(HttpServletRequest request){
|
||||
return origin;
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
package com.example.filter;
|
||||
|
||||
import com.example.entity.RestBean;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.FlowUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
/**
|
||||
* 限流控制过滤器
|
||||
* 防止用户高频请求接口,借助Redis进行限流
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@Order(Const.ORDER_FLOW_LIMIT)
|
||||
public class FlowLimitingFilter extends HttpFilter {
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
//指定时间内最大请求次数限制
|
||||
@Value("${spring.web.flow.limit}")
|
||||
int limit;
|
||||
//计数时间周期
|
||||
@Value("${spring.web.flow.period}")
|
||||
int period;
|
||||
//超出请求限制封禁时间
|
||||
@Value("${spring.web.flow.block}")
|
||||
int block;
|
||||
|
||||
@Resource
|
||||
FlowUtils utils;
|
||||
|
||||
@Override
|
||||
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
String address = request.getRemoteAddr();
|
||||
if (!tryCount(address))
|
||||
this.writeBlockMessage(response);
|
||||
else
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试对指定IP地址请求计数,如果被限制则无法继续访问
|
||||
* @param address 请求IP地址
|
||||
* @return 是否操作成功
|
||||
*/
|
||||
private boolean tryCount(String address) {
|
||||
synchronized (address.intern()) {
|
||||
if(Boolean.TRUE.equals(template.hasKey(Const.FLOW_LIMIT_BLOCK + address)))
|
||||
return false;
|
||||
String counterKey = Const.FLOW_LIMIT_COUNTER + address;
|
||||
String blockKey = Const.FLOW_LIMIT_BLOCK + address;
|
||||
return utils.limitPeriodCheck(counterKey, blockKey, block, limit, period);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为响应编写拦截内容,提示用户操作频繁
|
||||
* @param response 响应
|
||||
* @throws IOException 可能的异常
|
||||
*/
|
||||
private void writeBlockMessage(HttpServletResponse response) throws IOException {
|
||||
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||
response.setContentType("application/json;charset=utf-8");
|
||||
PrintWriter writer = response.getWriter();
|
||||
writer.write(RestBean.forbidden("操作频繁,请稍后再试").asJsonString());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
package com.example.filter;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.SnowflakeIdGenerator;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.slf4j.MDC;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求日志过滤器,用于记录所有用户请求信息
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RequestLogFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
SnowflakeIdGenerator generator;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
long startTime = System.currentTimeMillis();
|
||||
this.logRequestStart(request);
|
||||
ContentCachingResponseWrapper wrapper = new ContentCachingResponseWrapper(response);
|
||||
filterChain.doFilter(request, wrapper);
|
||||
this.logRequestEnd(wrapper, startTime);
|
||||
wrapper.copyBodyToResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求结束时的日志打印,包含处理耗时以及响应结果
|
||||
* @param wrapper 用于读取响应结果的包装类
|
||||
* @param startTime 起始时间
|
||||
*/
|
||||
public void logRequestEnd(ContentCachingResponseWrapper wrapper, long startTime){
|
||||
long time = System.currentTimeMillis() - startTime;
|
||||
int status = wrapper.getStatus();
|
||||
String content = status != 200 ?
|
||||
status + " 错误" : new String(wrapper.getContentAsByteArray());
|
||||
log.info("请求处理耗时: {}ms | 响应结果: {}", time, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求开始时的日志打印,包含请求全部信息,以及对应用户角色
|
||||
* @param request 请求
|
||||
*/
|
||||
public void logRequestStart(HttpServletRequest request){
|
||||
long reqId = generator.nextId();
|
||||
MDC.put("reqId", String.valueOf(reqId));
|
||||
JSONObject object = new JSONObject();
|
||||
request.getParameterMap().forEach((k, v) -> object.put(k, v.length > 0 ? v[0] : null));
|
||||
Object id = request.getAttribute(Const.ATTR_USER_ID);
|
||||
if(id != null) {
|
||||
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
|
||||
log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: {} (UID: {}) | 角色: {} | 请求参数列表: {}",
|
||||
request.getServletPath(), request.getMethod(), request.getRemoteAddr(),
|
||||
user.getUsername(), id, user.getAuthorities(), object);
|
||||
} else {
|
||||
log.info("请求URL: \"{}\" ({}) | 远程IP地址: {} │ 身份: 未验证 | 请求参数列表: {}",
|
||||
request.getServletPath(), request.getMethod(), request.getRemoteAddr(), object);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.example.listener;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
|
||||
import org.springframework.amqp.rabbit.annotation.RabbitListener;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用于处理邮件发送的消息队列监听器
|
||||
*/
|
||||
@Component
|
||||
@RabbitListener(queues = "mail")
|
||||
public class MailQueueListener {
|
||||
|
||||
@Resource
|
||||
JavaMailSender sender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
String username;
|
||||
|
||||
/**
|
||||
* 处理邮件发送
|
||||
* @param data 邮件信息
|
||||
*/
|
||||
@RabbitHandler
|
||||
public void sendMailMessage(Map<String, Object> data) {
|
||||
String email = data.get("email").toString();
|
||||
Integer code = (Integer) data.get("code");
|
||||
SimpleMailMessage message = switch (data.get("type").toString()) {
|
||||
case "register" ->
|
||||
createMessage("欢迎注册我们的网站",
|
||||
"您的邮件注册验证码为: "+code+",有效时间3分钟,为了保障您的账户安全,请勿向他人泄露验证码信息。",
|
||||
email);
|
||||
case "reset" ->
|
||||
createMessage("您的密码重置邮件",
|
||||
"你好,您正在执行重置密码操作,验证码: "+code+",有效时间3分钟,如非本人操作,请无视。",
|
||||
email);
|
||||
default -> null;
|
||||
};
|
||||
if(message == null) return;
|
||||
sender.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速封装简单邮件消息实体
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
* @param email 收件人
|
||||
* @return 邮件实体
|
||||
*/
|
||||
private SimpleMailMessage createMessage(String title, String content, String email){
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
message.setSubject(title);
|
||||
message.setText(content);
|
||||
message.setTo(email);
|
||||
message.setFrom(username);
|
||||
return message;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.example.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.example.entity.dto.Account;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface AccountMapper extends BaseMapper<Account> {
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.example.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.request.EmailRegisterVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
|
||||
public interface AccountService extends IService<Account>, UserDetailsService {
|
||||
Account findAccountByNameOrEmail(String text);
|
||||
String registerEmailVerifyCode(String email, String address);
|
||||
String registerEmailAccount(EmailRegisterVO info);
|
||||
String resetEmailAccountPassword(EmailResetVO info);
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
package com.example.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.example.entity.dto.Account;
|
||||
import com.example.entity.vo.request.EmailRegisterVO;
|
||||
import com.example.entity.vo.request.EmailResetVO;
|
||||
import com.example.mapper.AccountMapper;
|
||||
import com.example.service.AccountService;
|
||||
import com.example.utils.Const;
|
||||
import com.example.utils.FlowUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.amqp.core.AmqpTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 账户信息处理相关服务
|
||||
*/
|
||||
@Service
|
||||
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
||||
|
||||
//验证邮件发送冷却时间限制,秒为单位
|
||||
@Value("${spring.web.verify.mail-limit}")
|
||||
int verifyLimit;
|
||||
|
||||
@Resource
|
||||
AmqpTemplate rabbitTemplate;
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Resource
|
||||
PasswordEncoder passwordEncoder;
|
||||
|
||||
@Resource
|
||||
FlowUtils flow;
|
||||
|
||||
/**
|
||||
* 从数据库中通过用户名或邮箱查找用户详细信息
|
||||
* @param username 用户名
|
||||
* @return 用户详细信息
|
||||
* @throws UsernameNotFoundException 如果用户未找到则抛出此异常
|
||||
*/
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
Account account = this.findAccountByNameOrEmail(username);
|
||||
if(account == null)
|
||||
throw new UsernameNotFoundException("用户名或密码错误");
|
||||
return User
|
||||
.withUsername(username)
|
||||
.password(account.getPassword())
|
||||
.roles(account.getRole())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成注册验证码存入Redis中,并将邮件发送请求提交到消息队列等待发送
|
||||
* @param email 邮件地址
|
||||
* @param address 请求IP地址
|
||||
* @return 操作结果,null表示正常,否则为错误原因
|
||||
*/
|
||||
public String registerEmailVerifyCode(String email, String address){
|
||||
synchronized (address.intern()) {
|
||||
if(!this.verifyLimit(address))
|
||||
return "请求频繁,请稍后再试";
|
||||
Random random = new Random();
|
||||
int code = random.nextInt(899999) + 100000;
|
||||
Map<String, Object> data = Map.of("type","register","email", email, "code", code);
|
||||
rabbitTemplate.convertAndSend(Const.MQ_MAIL, data);
|
||||
stringRedisTemplate.opsForValue()
|
||||
.set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件验证码注册账号操作,需要检查验证码是否正确以及邮箱、用户名是否存在重名
|
||||
* @param info 注册基本信息
|
||||
* @return 操作结果,null表示正常,否则为错误原因
|
||||
*/
|
||||
public String registerEmailAccount(EmailRegisterVO info){
|
||||
String email = info.getEmail();
|
||||
String code = this.getEmailVerifyCode(email);
|
||||
if(code == null) return "请先获取验证码";
|
||||
if(!code.equals(info.getCode())) return "验证码错误,请重新输入";
|
||||
if(this.existsAccountByEmail(email)) return "该邮件地址已被注册";
|
||||
String username = info.getUsername();
|
||||
if(this.existsAccountByUsername(username)) return "该用户名已被他人使用,请重新更换";
|
||||
String password = passwordEncoder.encode(info.getPassword());
|
||||
Account account = new Account(null, info.getUsername(),
|
||||
password, email, Const.ROLE_DEFAULT, new Date());
|
||||
if(!this.save(account)) {
|
||||
return "内部错误,注册失败";
|
||||
} else {
|
||||
this.deleteEmailVerifyCode(email);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮件验证码重置密码操作,需要检查验证码是否正确
|
||||
* @param info 重置基本信息
|
||||
* @return 操作结果,null表示正常,否则为错误原因
|
||||
*/
|
||||
@Override
|
||||
public String resetEmailAccountPassword(EmailResetVO info) {
|
||||
String email = info.getEmail();
|
||||
String code = this.getEmailVerifyCode(email);
|
||||
if(code == null) return "请先获取验证码";
|
||||
if(!code.equals(info.getCode())) return "验证码错误,请重新输入";
|
||||
String password = passwordEncoder.encode(info.getPassword());
|
||||
boolean update = this.update().eq("email", email).set("password", password).update();
|
||||
return update ? null : "更新失败,请联系管理员";
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除Redis中存储的邮件验证码
|
||||
* @param email 电邮
|
||||
*/
|
||||
private void deleteEmailVerifyCode(String email){
|
||||
String key = Const.VERIFY_EMAIL_DATA + email;
|
||||
stringRedisTemplate.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Redis中存储的邮件验证码
|
||||
* @param email 电邮
|
||||
* @return 验证码
|
||||
*/
|
||||
private String getEmailVerifyCode(String email){
|
||||
String key = Const.VERIFY_EMAIL_DATA + email;
|
||||
return stringRedisTemplate.opsForValue().get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对IP地址进行邮件验证码获取限流
|
||||
* @param address 地址
|
||||
* @return 是否通过验证
|
||||
*/
|
||||
private boolean verifyLimit(String address) {
|
||||
String key = Const.VERIFY_EMAIL_LIMIT + address;
|
||||
return flow.limitOnceCheck(key, verifyLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过用户名或邮件地址查找用户
|
||||
* @param text 用户名或邮件
|
||||
* @return 账户实体
|
||||
*/
|
||||
public Account findAccountByNameOrEmail(String text){
|
||||
return this.query()
|
||||
.eq("username", text).or()
|
||||
.eq("email", text)
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定邮箱的用户是否已经存在
|
||||
* @param email 邮箱
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean existsAccountByEmail(String email){
|
||||
return this.baseMapper.exists(Wrappers.<Account>query().eq("email", email));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定用户名的用户是否已经存在
|
||||
* @param username 用户名
|
||||
* @return 是否存在
|
||||
*/
|
||||
private boolean existsAccountByUsername(String username){
|
||||
return this.baseMapper.exists(Wrappers.<Account>query().eq("username", username));
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
package com.example.utils;
|
||||
|
||||
/**
|
||||
* 一些常量字符串整合
|
||||
*/
|
||||
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 FLOW_LIMIT_COUNTER = "flow:counter:";
|
||||
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
|
||||
//邮件验证码
|
||||
public final static String VERIFY_EMAIL_LIMIT = "verify:email:limit:";
|
||||
public final static String VERIFY_EMAIL_DATA = "verify:email:data:";
|
||||
//过滤器优先级
|
||||
public final static int ORDER_FLOW_LIMIT = -101;
|
||||
public final static int ORDER_CORS = -102;
|
||||
//请求自定义属性
|
||||
public final static String ATTR_USER_ID = "userId";
|
||||
//消息队列
|
||||
public final static String MQ_MAIL = "mail";
|
||||
//用户角色
|
||||
public final static String ROLE_DEFAULT = "user";
|
||||
|
||||
}
|
@ -0,0 +1,90 @@
|
||||
package com.example.utils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 限流通用工具
|
||||
* 针对于不同的情况进行限流操作,支持限流升级
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class FlowUtils {
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
|
||||
/**
|
||||
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求,如3秒内不能再次发起请求
|
||||
* @param key 键
|
||||
* @param blockTime 限制时间
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
public boolean limitOnceCheck(String key, int blockTime){
|
||||
return this.internalCheck(key, 1, blockTime, (overclock) -> false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对于单次频率限制,请求成功后,在冷却时间内不得再次进行请求
|
||||
* 如3秒内不能再次发起请求,如果不听劝阻继续发起请求,将限制更长时间
|
||||
* @param key 键
|
||||
* @param frequency 请求频率
|
||||
* @param baseTime 基础限制时间
|
||||
* @param upgradeTime 升级限制时间
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
public boolean limitOnceUpgradeCheck(String key, int frequency, int baseTime, int upgradeTime){
|
||||
return this.internalCheck(key, frequency, baseTime, (overclock) -> {
|
||||
if (overclock)
|
||||
template.opsForValue().set(key, "1", upgradeTime, TimeUnit.SECONDS);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 针对于在时间段内多次请求限制,如3秒内限制请求20次,超出频率则封禁一段时间
|
||||
* @param counterKey 计数键
|
||||
* @param blockKey 封禁键
|
||||
* @param blockTime 封禁时间
|
||||
* @param frequency 请求频率
|
||||
* @param period 计数周期
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
public boolean limitPeriodCheck(String counterKey, String blockKey, int blockTime, int frequency, int period){
|
||||
return this.internalCheck(counterKey, frequency, period, (overclock) -> {
|
||||
if (overclock)
|
||||
template.opsForValue().set(blockKey, "", blockTime, TimeUnit.SECONDS);
|
||||
return !overclock;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部使用请求限制主要逻辑
|
||||
* @param key 计数键
|
||||
* @param frequency 请求频率
|
||||
* @param period 计数周期
|
||||
* @param action 限制行为与策略
|
||||
* @return 是否通过限流检查
|
||||
*/
|
||||
private boolean internalCheck(String key, int frequency, int period, LimitAction action){
|
||||
if (Boolean.TRUE.equals(template.hasKey(key))) {
|
||||
Long value = Optional.ofNullable(template.opsForValue().increment(key)).orElse(0L);
|
||||
return action.run(value > frequency);
|
||||
} else {
|
||||
template.opsForValue().set(key, "1", period, TimeUnit.SECONDS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部使用,限制行为与策略
|
||||
*/
|
||||
private interface LimitAction {
|
||||
boolean run(boolean overclock);
|
||||
}
|
||||
}
|
180
my-project-backend/src/main/java/com/example/utils/JwtUtils.java
Normal file
180
my-project-backend/src/main/java/com/example/utils/JwtUtils.java
Normal file
@ -0,0 +1,180 @@
|
||||
package com.example.utils;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.JWTVerifier;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
import com.auth0.jwt.exceptions.JWTVerificationException;
|
||||
import com.auth0.jwt.interfaces.Claim;
|
||||
import com.auth0.jwt.interfaces.DecodedJWT;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用于处理Jwt令牌的工具类
|
||||
*/
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
|
||||
//用于给Jwt令牌签名校验的秘钥
|
||||
@Value("${spring.security.jwt.key}")
|
||||
private String key;
|
||||
//令牌的过期时间,以小时为单位
|
||||
@Value("${spring.security.jwt.expire}")
|
||||
private int expire;
|
||||
//为用户生成Jwt令牌的冷却时间,防止刷接口频繁登录生成令牌,以秒为单位
|
||||
@Value("${spring.security.jwt.limit.base}")
|
||||
private int limit_base;
|
||||
//用户如果继续恶意刷令牌,更严厉的封禁时间
|
||||
@Value("${spring.security.jwt.limit.upgrade}")
|
||||
private int limit_upgrade;
|
||||
//判定用户在冷却时间内,继续恶意刷令牌的次数
|
||||
@Value("${spring.security.jwt.limit.frequency}")
|
||||
private int limit_frequency;
|
||||
|
||||
@Resource
|
||||
StringRedisTemplate template;
|
||||
|
||||
@Resource
|
||||
FlowUtils utils;
|
||||
|
||||
/**
|
||||
* 让指定Jwt令牌失效
|
||||
* @param headerToken 请求头中携带的令牌
|
||||
* @return 是否操作成功
|
||||
*/
|
||||
public boolean invalidateJwt(String headerToken){
|
||||
String token = this.convertToken(headerToken);
|
||||
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
|
||||
try {
|
||||
DecodedJWT verify = jwtVerifier.verify(token);
|
||||
return deleteToken(verify.getId(), verify.getExpiresAt());
|
||||
} catch (JWTVerificationException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据UserDetails生成对应的Jwt令牌
|
||||
* @param user 用户信息
|
||||
* @return 令牌
|
||||
*/
|
||||
public String createJwt(UserDetails user, String username, int userId) {
|
||||
if(this.frequencyCheck(userId)) {
|
||||
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
Date now = calendar.getTime();
|
||||
calendar.add(Calendar.HOUR, expire);
|
||||
return JWT.create()
|
||||
.withJWTId(UUID.randomUUID().toString())
|
||||
.withClaim("id", userId)
|
||||
.withClaim("name", username)
|
||||
.withClaim("authorities", user.getAuthorities()
|
||||
.stream()
|
||||
.map(GrantedAuthority::getAuthority).toList())
|
||||
.withExpiresAt(calendar.getTime())
|
||||
.withIssuedAt(now)
|
||||
.sign(algorithm);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Jwt令牌
|
||||
* @param headerToken 请求头中携带的令牌
|
||||
* @return DecodedJWT
|
||||
*/
|
||||
public DecodedJWT resolveJwt(String headerToken){
|
||||
String token = this.convertToken(headerToken);
|
||||
if(token == null) return null;
|
||||
Algorithm algorithm = Algorithm.HMAC256(key);
|
||||
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
|
||||
try {
|
||||
DecodedJWT verify = jwtVerifier.verify(token);
|
||||
if(this.isInvalidToken(verify.getId())) return null;
|
||||
Map<String, Claim> claims = verify.getClaims();
|
||||
return new Date().after(claims.get("exp").asDate()) ? null : verify;
|
||||
} catch (JWTVerificationException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将jwt对象中的内容封装为UserDetails
|
||||
* @param jwt 已解析的Jwt对象
|
||||
* @return UserDetails
|
||||
*/
|
||||
public UserDetails toUser(DecodedJWT jwt) {
|
||||
Map<String, Claim> claims = jwt.getClaims();
|
||||
return User
|
||||
.withUsername(claims.get("name").asString())
|
||||
.password("******")
|
||||
.authorities(claims.get("authorities").asArray(String.class))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将jwt对象中的用户ID提取出来
|
||||
* @param jwt 已解析的Jwt对象
|
||||
* @return 用户ID
|
||||
*/
|
||||
public Integer toId(DecodedJWT jwt) {
|
||||
Map<String, Claim> claims = jwt.getClaims();
|
||||
return claims.get("id").asInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率检测,防止用户高频申请Jwt令牌,并且采用阶段封禁机制
|
||||
* 如果已经提示无法登录的情况下用户还在刷,那么就封禁更长时间
|
||||
* @param userId 用户ID
|
||||
* @return 是否通过频率检测
|
||||
*/
|
||||
private boolean frequencyCheck(int userId){
|
||||
String key = Const.JWT_FREQUENCY + userId;
|
||||
return utils.limitOnceUpgradeCheck(key, limit_frequency, limit_base, limit_upgrade);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并转换请求头中的Token令牌
|
||||
* @param headerToken 请求头中的Token
|
||||
* @return 转换后的令牌
|
||||
*/
|
||||
private String convertToken(String headerToken){
|
||||
if(headerToken == null || !headerToken.startsWith("Bearer "))
|
||||
return null;
|
||||
return headerToken.substring(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Token列入Redis黑名单中
|
||||
* @param uuid 令牌ID
|
||||
* @param time 过期时间
|
||||
* @return 是否操作成功
|
||||
*/
|
||||
private boolean deleteToken(String uuid, Date time){
|
||||
if(this.isInvalidToken(uuid))
|
||||
return false;
|
||||
Date now = new Date();
|
||||
long expire = Math.max(time.getTime() - now.getTime(), 0);
|
||||
template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证Token是否被列入Redis黑名单
|
||||
* @param uuid 令牌ID
|
||||
* @return 是否操作成功
|
||||
*/
|
||||
private boolean isInvalidToken(String uuid){
|
||||
return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid));
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
package com.example.utils;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 雪花算法ID生成器
|
||||
*/
|
||||
@Component
|
||||
public class SnowflakeIdGenerator {
|
||||
private static final long START_TIMESTAMP = 1691087910202L;
|
||||
|
||||
private static final long DATA_CENTER_ID_BITS = 5L;
|
||||
private static final long WORKER_ID_BITS = 5L;
|
||||
private static final long SEQUENCE_BITS = 12L;
|
||||
|
||||
private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
|
||||
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
|
||||
private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
|
||||
|
||||
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS;
|
||||
private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
|
||||
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;
|
||||
|
||||
private final long dataCenterId;
|
||||
private final long workerId;
|
||||
private long lastTimestamp = -1L;
|
||||
private long sequence = 0L;
|
||||
|
||||
public SnowflakeIdGenerator(){
|
||||
this(1, 1);
|
||||
}
|
||||
|
||||
private SnowflakeIdGenerator(long dataCenterId, long workerId) {
|
||||
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
|
||||
throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
|
||||
}
|
||||
if (workerId > MAX_WORKER_ID || workerId < 0) {
|
||||
throw new IllegalArgumentException("Worker ID can't be greater than " + MAX_WORKER_ID + " or less than 0");
|
||||
}
|
||||
this.dataCenterId = dataCenterId;
|
||||
this.workerId = workerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成一个新的雪花算法ID加锁
|
||||
* @return 雪花ID
|
||||
*/
|
||||
public synchronized long nextId() {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
if (timestamp < lastTimestamp) {
|
||||
throw new IllegalStateException("Clock moved backwards. Refusing to generate ID.");
|
||||
}
|
||||
if (timestamp == lastTimestamp) {
|
||||
sequence = (sequence + 1) & MAX_SEQUENCE;
|
||||
if (sequence == 0) {
|
||||
timestamp = getNextTimestamp(lastTimestamp);
|
||||
}
|
||||
} else {
|
||||
sequence = 0L;
|
||||
}
|
||||
lastTimestamp = timestamp;
|
||||
return ((timestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT) |
|
||||
(dataCenterId << DATA_CENTER_ID_SHIFT) |
|
||||
(workerId << WORKER_ID_SHIFT) |
|
||||
sequence;
|
||||
}
|
||||
|
||||
private long getCurrentTimestamp() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
private long getNextTimestamp(long lastTimestamp) {
|
||||
long timestamp = getCurrentTimestamp();
|
||||
while (timestamp <= lastTimestamp) {
|
||||
timestamp = getCurrentTimestamp();
|
||||
}
|
||||
return timestamp;
|
||||
}
|
||||
}
|
40
my-project-backend/src/main/resources/application-dev.yml
Normal file
40
my-project-backend/src/main/resources/application-dev.yml
Normal file
@ -0,0 +1,40 @@
|
||||
# 开发环境配置
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
username: javastudy111@163.com
|
||||
password: VKQFYZMUSUZGSGEG
|
||||
rabbitmq:
|
||||
addresses: localhost
|
||||
username: admin
|
||||
password: admin
|
||||
virtual-host: /
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/test
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
security:
|
||||
jwt:
|
||||
key: 'abcdefghijklmn'
|
||||
expire: 72
|
||||
limit:
|
||||
base: 10
|
||||
upgrade: 300
|
||||
frequency: 30
|
||||
filter:
|
||||
order: -100
|
||||
web:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 3
|
||||
limit: 10
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
||||
credentials: false
|
||||
methods: '*'
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
37
my-project-backend/src/main/resources/application-prod.yml
Normal file
37
my-project-backend/src/main/resources/application-prod.yml
Normal file
@ -0,0 +1,37 @@
|
||||
#生产环境配置
|
||||
spring:
|
||||
mail:
|
||||
host: smtp.163.com
|
||||
username: javastudy111@163.com
|
||||
password: VKQFYZMUSUZGSGEG
|
||||
rabbitmq:
|
||||
addresses: localhost
|
||||
username: admin
|
||||
password: admin
|
||||
virtual-host: /
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/test
|
||||
username: root
|
||||
password: 123456
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
security:
|
||||
jwt:
|
||||
key: 'abcdefghijklmn'
|
||||
expire: 72
|
||||
limit:
|
||||
base: 10
|
||||
upgrade: 300
|
||||
frequency: 30
|
||||
filter:
|
||||
order: -100
|
||||
web:
|
||||
verify:
|
||||
mail-limit: 60
|
||||
flow:
|
||||
period: 3
|
||||
limit: 10
|
||||
block: 30
|
||||
cors:
|
||||
origin: '*'
|
||||
credentials: false
|
||||
methods: '*'
|
3
my-project-backend/src/main/resources/application.yml
Normal file
3
my-project-backend/src/main/resources/application.yml
Normal file
@ -0,0 +1,3 @@
|
||||
spring:
|
||||
profiles:
|
||||
active: '@environment@'
|
34
my-project-backend/src/main/resources/logback-spring.xml
Normal file
34
my-project-backend/src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,34 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<configuration>
|
||||
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
|
||||
|
||||
<property name="CONSOLE_LOG_PATTERN"
|
||||
value="%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr([%15.15t]){faint} %clr(\(%X{reqId:-system}\)){faint} %clr(--){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
|
||||
<property name="FILE_LOG_PATTERN"
|
||||
value="%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%t] \(%X{reqId:-system}\) %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}"/>
|
||||
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||
<charset>${CONSOLE_LOG_CHARSET}</charset>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<encoder>
|
||||
<pattern>${FILE_LOG_PATTERN}</pattern>
|
||||
<charset>${FILE_LOG_CHARSET}</charset>
|
||||
</encoder>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||
<FileNamePattern>log/%d{yyyy-MM-dd}-spring-%i.log</FileNamePattern>
|
||||
<cleanHistoryOnStart>true</cleanHistoryOnStart>
|
||||
<maxHistory>7</maxHistory>
|
||||
<maxFileSize>10MB</maxFileSize>
|
||||
</rollingPolicy>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE"/>
|
||||
<appender-ref ref="FILE"/>
|
||||
</root>
|
||||
</configuration>
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
28
my-project-frontend/.gitignore
vendored
Normal file
28
my-project-frontend/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
3
my-project-frontend/.vscode/extensions.json
vendored
Normal file
3
my-project-frontend/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
29
my-project-frontend/README.md
Normal file
29
my-project-frontend/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# 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
|
||||
```
|
13
my-project-frontend/index.html
Normal file
13
my-project-frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
9
my-project-frontend/my-project-frontend.iml
Normal file
9
my-project-frontend/my-project-frontend.iml
Normal file
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
691
my-project-frontend/package-lock.json
generated
Normal file
691
my-project-frontend/package-lock.json
generated
Normal file
@ -0,0 +1,691 @@
|
||||
{
|
||||
"name": "my-project-frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "my-project-frontend",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.4.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.22.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.22.7.tgz",
|
||||
"integrity": "sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q==",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.17.tgz",
|
||||
"integrity": "sha512-wHsmJG/dnL3OkpAcwbgoBTTMHVi4Uyou3F5mf58ZtmUyIKfcdA7TROav/6tCzET4A3QW2Q2FC+eFneMU+iyOxg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-9np+YYdNDed5+Jgr1TdWBsozZ85U1Oa3xW0c7TWqH0y2aGghXtZsuT8nYRbzOMcl0bXZXjOGbksoTtVOlWrRZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-O+FeWB/+xya0aLg23hHEM2E3hbfwZzjqumKMSIqcHbNvDa+dza2D0yLuymRBQQnC34CWrsJUXyH2MG5VnLd6uw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-M9uJ9VSB1oli2BE/dJs3zVr9kcCBBsE883prage1NWz6pBS++1oNn/7soPNS3+1DGj0FrkSvnED4Bmlu1VAE9g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-XDre+J5YeIJDMfp3n0279DFNrGCXlxOuGsWIkRb1NThMZ0BsrWXoTg23Jer7fEXQ9Ye5QjrvXpxnhzl3bHtk0g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-cjTzGa3QlNfERa0+ptykyxs5A6FEUQQF0MuilYXYBGdBxD3vxJcKnzDlhDCa1VAJCmAxed6mYhA2KaJIbtiNuQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-sOxEvR8d7V7Kw8QqzxWc7bFfnWnGdaFBut1dRUYtu+EIRXefBc/eIsiUiShnW0hM3FmQ5Zf27suDuHsKgZ5QrA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.17.tgz",
|
||||
"integrity": "sha512-2d3Lw6wkwgSLC2fIvXKoMNGVaeY8qdN0IC3rfuVxJp89CRfA3e3VqWifGDfuakPmp90+ZirmTfye1n4ncjv2lg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-c9w3tE7qA3CYWjT+M3BMbwMt+0JYOp3vCMKgVBrCl1nwjAlOMYzEo+gG7QaZ9AtqZFj5MbUc885wuBBmu6aADQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.17.tgz",
|
||||
"integrity": "sha512-1DS9F966pn5pPnqXYz16dQqWIB0dmDfAQZd6jSSpiT9eX1NzKh07J6VKR3AoXXXEk6CqZMojiVDSZi1SlmKVdg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.17.tgz",
|
||||
"integrity": "sha512-EvLsxCk6ZF0fpCB6w6eOI2Fc8KW5N6sHlIovNe8uOFObL2O+Mr0bflPHyHwLT6rwMg9r77WOAWb2FqCQrVnwFg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.17.tgz",
|
||||
"integrity": "sha512-e0bIdHA5p6l+lwqTE36NAW5hHtw2tNRmHlGBygZC14QObsA3bD4C6sXLJjvnDIjSKhW1/0S3eDy+QmX/uZWEYQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.17.tgz",
|
||||
"integrity": "sha512-BAAilJ0M5O2uMxHYGjFKn4nJKF6fNCdP1E0o5t5fvMYYzeIqy2JdAP88Az5LHt9qBoUa4tDaRpfWt21ep5/WqQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.17.tgz",
|
||||
"integrity": "sha512-Wh/HW2MPnC3b8BqRSIme/9Zhab36PPH+3zam5pqGRH4pE+4xTrVLx2+XdGp6fVS3L2x+DrsIcsbMleex8fbE6g==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.17.tgz",
|
||||
"integrity": "sha512-j/34jAl3ul3PNcK3pfI0NSlBANduT2UO5kZ7FCaK33XFv3chDhICLY8wJJWIhiQ+YNdQ9dxqQctRg2bvrMlYgg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-QM50vJ/y+8I60qEmFxMoxIx4de03pGo2HwxdBeFd4nMh364X6TIBZ6VQ5UQmPbQWUVWHWws5MmJXlHAXvJEmpQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-/jGlhWR7Sj9JPZHzXyyMZ1RFMkNPjC6QIAan0sDOtIo2TYk3tZn5UDrkE0XgsTQCxWTTOcMPf9p6Rh2hXtl5TQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-rSEeYaGgyGGf4qZM2NonMhMOP/5EHp4u9ehFiBrg7stH6BYEEjlkVREuDEcQ0LfIl53OXLxNbfuIj7mr5m29TA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-Y7ZBbkLqlSgn4+zot4KUNYst0bFoO68tRgI6mY2FIM+b7ZbyNVtNbDP5y8qlu4/knZZ73fgJDlXID+ohY5zt5g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.17.tgz",
|
||||
"integrity": "sha512-bwPmTJsEQcbZk26oYpc4c/8PvTY3J5/QK8jM19DVlEsAB41M39aWovWoHtNm78sd6ip6prilxeHosPADXtEJFw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.17.tgz",
|
||||
"integrity": "sha512-H/XaPtPKli2MhW+3CQueo6Ni3Avggi6hP/YvgkEe1aSaxw+AeO8MFjq8DlgfTd9Iz4Yih3QCZI6YLMoyccnPRg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.17.tgz",
|
||||
"integrity": "sha512-fGEb8f2BSA3CW7riJVurug65ACLuQAzKq0SSqkY2b2yHHH0MzDfbLyKIGzHwOI/gkHcxM/leuSW6D5w/LMNitA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@vitejs/plugin-vue": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.2.3.tgz",
|
||||
"integrity": "sha512-R6JDUfiZbJA9cMiguQ7jxALsgiprjBeHL5ikpXfJCH62pPHtI+JdJ5xWj6Ev73yXSlYl86+blXn1kZHQ7uElxw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^4.0.0",
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.3.4.tgz",
|
||||
"integrity": "sha512-cquyDNvZ6jTbf/+x+AgM2Arrp6G4Dzbb0R64jiG804HRMfRiFXWI6kqUVqZ6ZR0bQhIoQjB4+2bhNtVwndW15g==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.21.3",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.3.4.tgz",
|
||||
"integrity": "sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.3.4.tgz",
|
||||
"integrity": "sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/reactivity-transform": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.0",
|
||||
"postcss": "^8.1.10",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.3.4.tgz",
|
||||
"integrity": "sha512-m0v6oKpup2nMSehwA6Uuu+j+wEwcy7QmwMkVNVfrV9P2qE5KshC6RwOCq8fjGS/Eak/uNb8AaWekfiXxbBB6gQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.3.4.tgz",
|
||||
"integrity": "sha512-kLTDLwd0B1jG08NBF3R5rqULtv/f8x3rOFByTDz4J53ttIQEDmALqKqXY0J+XQeN0aV2FBxY8nJDf88yvOPAqQ==",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity-transform": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity-transform/-/reactivity-transform-3.3.4.tgz",
|
||||
"integrity": "sha512-MXgwjako4nu5WFLAjpBnCj/ieqcjE2aJBINUNQzkZQfzIZA4xn+0fV1tIYBJvvva3N3OvKGofRLvQIwEQPpaXw==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.20.15",
|
||||
"@vue/compiler-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.3.4.tgz",
|
||||
"integrity": "sha512-R+bqxMN6pWO7zGI4OMlmvePOdP2c93GsHFM/siJI7O2nxFRzj55pLwkpCedEY+bTMgp5miZ8CxfIZo3S+gFqvA==",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.3.4.tgz",
|
||||
"integrity": "sha512-Aj5bTJ3u5sFsUckRghsNjVTtxZQ1OyMWCr5dZRAPijF/0Vy4xEoRCwLyHXcj4D0UFbJ4lbx3gPTgg06K/GnPnQ==",
|
||||
"dependencies": {
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"@vue/shared": "3.3.4",
|
||||
"csstype": "^3.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.3.4.tgz",
|
||||
"integrity": "sha512-Q6jDDzR23ViIb67v+vM1Dqntu+HUexQcsWKhhQa4ARVzxOY2HbC7QRW/ggkDBd5BU+uM1sV6XOAP0b216o34JQ==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.3.4.tgz",
|
||||
"integrity": "sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz",
|
||||
"integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ=="
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.18.17",
|
||||
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.17.tgz",
|
||||
"integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.18.17",
|
||||
"@esbuild/android-arm64": "0.18.17",
|
||||
"@esbuild/android-x64": "0.18.17",
|
||||
"@esbuild/darwin-arm64": "0.18.17",
|
||||
"@esbuild/darwin-x64": "0.18.17",
|
||||
"@esbuild/freebsd-arm64": "0.18.17",
|
||||
"@esbuild/freebsd-x64": "0.18.17",
|
||||
"@esbuild/linux-arm": "0.18.17",
|
||||
"@esbuild/linux-arm64": "0.18.17",
|
||||
"@esbuild/linux-ia32": "0.18.17",
|
||||
"@esbuild/linux-loong64": "0.18.17",
|
||||
"@esbuild/linux-mips64el": "0.18.17",
|
||||
"@esbuild/linux-ppc64": "0.18.17",
|
||||
"@esbuild/linux-riscv64": "0.18.17",
|
||||
"@esbuild/linux-s390x": "0.18.17",
|
||||
"@esbuild/linux-x64": "0.18.17",
|
||||
"@esbuild/netbsd-x64": "0.18.17",
|
||||
"@esbuild/openbsd-x64": "0.18.17",
|
||||
"@esbuild/sunos-x64": "0.18.17",
|
||||
"@esbuild/win32-arm64": "0.18.17",
|
||||
"@esbuild/win32-ia32": "0.18.17",
|
||||
"@esbuild/win32-x64": "0.18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-walker": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
|
||||
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.2",
|
||||
"resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.2.tgz",
|
||||
"integrity": "sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.6.tgz",
|
||||
"integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.0.0.tgz",
|
||||
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.27",
|
||||
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.4.27.tgz",
|
||||
"integrity": "sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ==",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "3.27.0",
|
||||
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.27.0.tgz",
|
||||
"integrity": "sha512-aOltLCrYZ0FhJDm7fCqwTjIUEVjWjcydKBV/Zeid6Mn8BWgDCUBBWT5beM5ieForYNo/1ZHuGJdka26kvQ3Gzg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.18.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "4.4.8",
|
||||
"resolved": "https://registry.npmmirror.com/vite/-/vite-4.4.8.tgz",
|
||||
"integrity": "sha512-LONawOUUjxQridNWGQlNizfKH89qPigK36XhMI7COMGztz8KNY0JHim7/xDd71CZwGT4HtSRgI7Hy+RlhG0Gvg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"postcss": "^8.4.26",
|
||||
"rollup": "^3.25.2"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.3.4.tgz",
|
||||
"integrity": "sha512-VTyEYn3yvIeY1Py0WaYGZsXnz3y5UnGi62GjVEqvEGPl6nxbOrCXbVOTQWBEJUqAyTUk2uJ5JLVnYJ6ZzGbrSw==",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.3.4",
|
||||
"@vue/compiler-sfc": "3.3.4",
|
||||
"@vue/runtime-dom": "3.3.4",
|
||||
"@vue/server-renderer": "3.3.4",
|
||||
"@vue/shared": "3.3.4"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
my-project-frontend/package.json
Normal file
17
my-project-frontend/package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "my-project-frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.2.3",
|
||||
"vite": "^4.4.6"
|
||||
}
|
||||
}
|
BIN
my-project-frontend/public/favicon.ico
Normal file
BIN
my-project-frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
47
my-project-frontend/src/App.vue
Normal file
47
my-project-frontend/src/App.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<script setup>
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
import TheWelcome from './components/TheWelcome.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header>
|
||||
<img alt="Vue logo" class="logo" src="./assets/logo.svg" width="125" height="125" />
|
||||
|
||||
<div class="wrapper">
|
||||
<HelloWorld msg="You did it!" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<TheWelcome />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
margin: 0 auto 2rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
header {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
padding-right: calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0 2rem 0 0;
|
||||
}
|
||||
|
||||
header .wrapper {
|
||||
display: flex;
|
||||
place-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
73
my-project-frontend/src/assets/base.css
Normal file
73
my-project-frontend/src/assets/base.css
Normal file
@ -0,0 +1,73 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition: color 0.5s, background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
|
||||
Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
1
my-project-frontend/src/assets/logo.svg
Normal file
1
my-project-frontend/src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
35
my-project-frontend/src/assets/main.css
Normal file
35
my-project-frontend/src/assets/main.css
Normal file
@ -0,0 +1,35 @@
|
||||
@import './base.css';
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
a:hover {
|
||||
background-color: hsla(160, 100%, 37%, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
body {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
}
|
44
my-project-frontend/src/components/HelloWorld.vue
Normal file
44
my-project-frontend/src/components/HelloWorld.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
msg: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="greetings">
|
||||
<h1 class="green">{{ msg }}</h1>
|
||||
<h3>
|
||||
You’ve successfully created a project with
|
||||
<a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
|
||||
</h3>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
font-weight: 500;
|
||||
font-size: 2.6rem;
|
||||
position: relative;
|
||||
top: -10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.greetings h1,
|
||||
.greetings h3 {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
86
my-project-frontend/src/components/TheWelcome.vue
Normal file
86
my-project-frontend/src/components/TheWelcome.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import WelcomeItem from './WelcomeItem.vue'
|
||||
import DocumentationIcon from './icons/IconDocumentation.vue'
|
||||
import ToolingIcon from './icons/IconTooling.vue'
|
||||
import EcosystemIcon from './icons/IconEcosystem.vue'
|
||||
import CommunityIcon from './icons/IconCommunity.vue'
|
||||
import SupportIcon from './icons/IconSupport.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<DocumentationIcon />
|
||||
</template>
|
||||
<template #heading>Documentation</template>
|
||||
|
||||
Vue’s
|
||||
<a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
|
||||
provides you with all information you need to get started.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<ToolingIcon />
|
||||
</template>
|
||||
<template #heading>Tooling</template>
|
||||
|
||||
This project is served and bundled with
|
||||
<a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
|
||||
recommended IDE setup is
|
||||
<a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
|
||||
you need to test your components and web pages, check out
|
||||
<a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
|
||||
<a href="https://on.cypress.io/component" target="_blank">Cypress Component Testing</a>.
|
||||
|
||||
<br />
|
||||
|
||||
More instructions are available in <code>README.md</code>.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<EcosystemIcon />
|
||||
</template>
|
||||
<template #heading>Ecosystem</template>
|
||||
|
||||
Get official tools and libraries for your project:
|
||||
<a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
|
||||
<a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
|
||||
<a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
|
||||
<a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
|
||||
you need more resources, we suggest paying
|
||||
<a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
|
||||
a visit.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<CommunityIcon />
|
||||
</template>
|
||||
<template #heading>Community</template>
|
||||
|
||||
Got stuck? Ask your question on
|
||||
<a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
|
||||
Discord server, or
|
||||
<a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
|
||||
>StackOverflow</a
|
||||
>. You should also subscribe to
|
||||
<a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
|
||||
the official
|
||||
<a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
|
||||
twitter account for latest news in the Vue world.
|
||||
</WelcomeItem>
|
||||
|
||||
<WelcomeItem>
|
||||
<template #icon>
|
||||
<SupportIcon />
|
||||
</template>
|
||||
<template #heading>Support Vue</template>
|
||||
|
||||
As an independent project, Vue relies on community backing for its sustainability. You can help
|
||||
us by
|
||||
<a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
|
||||
</WelcomeItem>
|
||||
</template>
|
87
my-project-frontend/src/components/WelcomeItem.vue
Normal file
87
my-project-frontend/src/components/WelcomeItem.vue
Normal file
@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="item">
|
||||
<i>
|
||||
<slot name="icon"></slot>
|
||||
</i>
|
||||
<div class="details">
|
||||
<h3>
|
||||
<slot name="heading"></slot>
|
||||
</h3>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.item {
|
||||
margin-top: 2rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.details {
|
||||
flex: 1;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
display: flex;
|
||||
place-items: center;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.4rem;
|
||||
color: var(--color-heading);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.item {
|
||||
margin-top: 0;
|
||||
padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
|
||||
}
|
||||
|
||||
i {
|
||||
top: calc(50% - 25px);
|
||||
left: -26px;
|
||||
position: absolute;
|
||||
border: 1px solid var(--color-border);
|
||||
background: var(--color-background);
|
||||
border-radius: 8px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.item:before {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:after {
|
||||
content: ' ';
|
||||
border-left: 1px solid var(--color-border);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(50% + 25px);
|
||||
height: calc(50% - 25px);
|
||||
}
|
||||
|
||||
.item:first-of-type:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.item:last-of-type:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||
<path
|
||||
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
7
my-project-frontend/src/components/icons/IconSupport.vue
Normal file
7
my-project-frontend/src/components/icons/IconSupport.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||
<path
|
||||
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
19
my-project-frontend/src/components/icons/IconTooling.vue
Normal file
19
my-project-frontend/src/components/icons/IconTooling.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
aria-hidden="true"
|
||||
role="img"
|
||||
class="iconify iconify--mdi"
|
||||
width="24"
|
||||
height="24"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
6
my-project-frontend/src/main.js
Normal file
6
my-project-frontend/src/main.js
Normal file
@ -0,0 +1,6 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
16
my-project-frontend/vite.config.js
Normal file
16
my-project-frontend/vite.config.js
Normal file
@ -0,0 +1,16 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user