feat:优化邮件验证功能并添加日志记录
- 优化邮件验证逻辑,增加对 verifyCode 是否为空的判断 - 添加 log4j2 配置文件,实现日志记录功能 - 新增 WebSocket 相关配置和处理类 - 优化 TokenInterceptor 类,使用 Lombok 注解 - 新增 TraceIdAspect 方面类,用于添加 traceId 到日志 - 更新 RedisServiceImpl 类,简化 exists 方法的实现 - 新增 SchedulerConfig 配置类,用于配置定时任务 - 更新 SecurityConfig 配置类,添加跨域请求配置
This commit is contained in:
parent
ae5b7cbd1a
commit
8455307731
31
qodana.yaml
Normal file
31
qodana.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
# Qodana analysis is configured by qodana.yaml file #
|
||||||
|
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||||
|
#-------------------------------------------------------------------------------#
|
||||||
|
version: "1.0"
|
||||||
|
|
||||||
|
#Specify inspection profile for code analysis
|
||||||
|
profile:
|
||||||
|
name: qodana.starter
|
||||||
|
|
||||||
|
#Enable inspections
|
||||||
|
#include:
|
||||||
|
# - name: <SomeEnabledInspectionId>
|
||||||
|
|
||||||
|
#Disable inspections
|
||||||
|
#exclude:
|
||||||
|
# - name: <SomeDisabledInspectionId>
|
||||||
|
# paths:
|
||||||
|
# - <path/where/not/run/inspection>
|
||||||
|
|
||||||
|
projectJDK: "17" #(Applied in CI/CD pipeline)
|
||||||
|
|
||||||
|
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#bootstrap: sh ./prepare-qodana.sh
|
||||||
|
|
||||||
|
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||||
|
#plugins:
|
||||||
|
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||||
|
|
||||||
|
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||||
|
linter: jetbrains/qodana-jvm:2025.1
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.example.copykamanotes.aspect;
|
||||||
|
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.annotation.Before;
|
||||||
|
import org.slf4j.MDC;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class PutTraceIdAspect {
|
||||||
|
private static final String TRACE_ID_KEY = "traceId";
|
||||||
|
|
||||||
|
@Before("execution(* com.example.copykamanotes..*(..))")
|
||||||
|
public void addTraceIdToLog() {
|
||||||
|
if (MDC.get(TRACE_ID_KEY) == null) {
|
||||||
|
String traceId = UUID.randomUUID().toString();
|
||||||
|
MDC.put(TRACE_ID_KEY, traceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
package com.example.copykamanotes.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableScheduling
|
||||||
|
public class SchedulerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ThreadPoolTaskScheduler taskScheduler() {
|
||||||
|
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||||
|
scheduler.setPoolSize(10);
|
||||||
|
scheduler.setThreadNamePrefix("task-");
|
||||||
|
return scheduler;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -9,6 +9,9 @@ import org.springframework.security.config.annotation.web.configurers.AbstractHt
|
|||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.security.web.SecurityFilterChain;
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@ -29,6 +32,19 @@ public class SecurityConfig {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
|
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
|
||||||
|
configuration.setAllowedOrigins(Arrays.asList("*"));
|
||||||
|
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||||
|
configuration.setAllowCredentials(true);
|
||||||
|
|
||||||
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
source.registerCorsConfiguration("/**", configuration);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder() {
|
return new BCryptPasswordEncoder() {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
package com.example.copykamanotes.config;
|
package com.example.copykamanotes.config;
|
||||||
|
|
||||||
import com.example.copykamanotes.filter.TraceIdFilter;
|
import com.example.copykamanotes.filter.TraceIdFilter;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
@ -14,9 +14,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
|||||||
import com.example.copykamanotes.interceptor.TokenInterceptor;
|
import com.example.copykamanotes.interceptor.TokenInterceptor;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
@Autowired
|
|
||||||
private TokenInterceptor tokenInterceptor;
|
private final TokenInterceptor tokenInterceptor;
|
||||||
|
|
||||||
@Value("${upload.path:D:/kamaNotes/upload")
|
@Value("${upload.path:D:/kamaNotes/upload")
|
||||||
private String uploadPath;
|
private String uploadPath;
|
||||||
|
|||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.example.copykamanotes.config;
|
||||||
|
|
||||||
|
import com.example.copykamanotes.utils.JwtUtil;
|
||||||
|
import com.example.copykamanotes.websocket.MessageWebSocketHandler;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocket
|
||||||
|
public class WebSocketConfig {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public WebSocketConfig(JwtUtil jwtUtil, ObjectMapper objectMapper) {
|
||||||
|
this.jwtUtil = jwtUtil;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
|
registry.addHandler(messageWebSocketHandler(), "ws/message")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public MessageWebSocketHandler messageWebSocketHandler() {
|
||||||
|
return new MessageWebSocketHandler(jwtUtil, objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ServletServerContainerFactoryBean createWebSocketContainer() {
|
||||||
|
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
|
||||||
|
container.setMaxTextMessageBufferSize(8192);
|
||||||
|
container.setMaxBinaryMessageBufferSize(8192);
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -11,7 +11,7 @@ import jakarta.servlet.ServletResponse;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class TraceIdFilter implements Filter {
|
public class TraceIdFilter implements Filter {
|
||||||
private static final String TRACE_ID_KEY = "trace_id";
|
private static final String TRACE_ID_KEY = "traceId";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import com.example.copykamanotes.utils.JwtUtil;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
@ -19,7 +20,7 @@ public class TokenInterceptor implements HandlerInterceptor {
|
|||||||
private JwtUtil jwtUtil;
|
private JwtUtil jwtUtil;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) throws Exception {
|
||||||
String token = request.getHeader("Authorization");
|
String token = request.getHeader("Authorization");
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
|||||||
@ -1,10 +1,19 @@
|
|||||||
package com.example.copykamanotes.model.base;
|
package com.example.copykamanotes.model.base;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PaginationApiResponse类用于处理分页的API响应。
|
* PaginationApiResponse类用于处理分页的API响应。
|
||||||
* @param <T> 泛型类型,表示分页数据类型。
|
* @param <T> 泛型类型,表示分页数据类型。
|
||||||
*/
|
*/
|
||||||
|
@Getter
|
||||||
public class PaginationApiResponse<T> extends ApiResponse<T> {
|
public class PaginationApiResponse<T> extends ApiResponse<T> {
|
||||||
|
/**
|
||||||
|
* -- GETTER --
|
||||||
|
* 获取分页对象。
|
||||||
|
*
|
||||||
|
* @return 分页对象
|
||||||
|
*/
|
||||||
// 分页对象
|
// 分页对象
|
||||||
private final Pagination pagination;
|
private final Pagination pagination;
|
||||||
|
|
||||||
@ -13,12 +22,4 @@ public class PaginationApiResponse<T> extends ApiResponse<T> {
|
|||||||
this.pagination = pagination;
|
this.pagination = pagination;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取分页对象。
|
|
||||||
*
|
|
||||||
* @return 分页对象
|
|
||||||
*/
|
|
||||||
public Pagination getPagination() {
|
|
||||||
return pagination;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -81,15 +81,17 @@ public class EmailServiceImpl implements EmailService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verifyCode.getExpiredAt().isBefore(LocalDateTime.now())) {
|
if (verifyCode != null && verifyCode.getExpiredAt().isBefore(LocalDateTime.now())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyCode.getCode().equals(code)) {
|
if (verifyCode != null && !verifyCode.getCode().equals(code)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
emailVerifyCodeMapper.markAsUsed(verifyCode.getId());
|
if (verifyCode != null) {
|
||||||
|
emailVerifyCodeMapper.markAsUsed(verifyCode.getId());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,7 +32,7 @@ public class RedisServiceImpl implements RedisService {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean exists(String key) {
|
public boolean exists(String key) {
|
||||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
return redisTemplate.hasKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@ -73,7 +73,7 @@ public class MarkdownAST {
|
|||||||
StringBuilder text = new StringBuilder();
|
StringBuilder text = new StringBuilder();
|
||||||
|
|
||||||
if (node instanceof Text) {
|
if (node instanceof Text) {
|
||||||
text.append(((Text) node).getChars());
|
text.append((node).getChars());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (Node child : node.getChildren()) {
|
for (Node child : node.getChildren()) {
|
||||||
|
|||||||
@ -2,12 +2,10 @@ package com.example.copykamanotes.utils;
|
|||||||
|
|
||||||
public class MarkdownUtils {
|
public class MarkdownUtils {
|
||||||
public static boolean needCollapsed(String markdown) {
|
public static boolean needCollapsed(String markdown) {
|
||||||
MarkdownAST ast = new MarkdownAST(markdown);
|
return MarkdownUtil.needCollapsed(markdown);
|
||||||
return ast.shouldCollapse(250);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String extractIntroduction(String markdown) {
|
public static String extractIntroduction(String markdown) {
|
||||||
MarkdownAST ast = new MarkdownAST(markdown);
|
return MarkdownUtil.extractIntroduction(markdown);
|
||||||
return ast.extractIntroduction(250);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,87 @@
|
|||||||
|
package com.example.copykamanotes.websocket;
|
||||||
|
|
||||||
|
import com.example.copykamanotes.utils.JwtUtil;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MessageWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
|
||||||
|
private final JwtUtil jwtUtil;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
private static final Map<Long, WebSocketSession> USER_SESSIONS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
|
||||||
|
Long userId = getUserIdFromSession(session);
|
||||||
|
if (userId != null) {
|
||||||
|
log.info("User disconnected: {}", userId);
|
||||||
|
USER_SESSIONS.put(userId, session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
||||||
|
try {
|
||||||
|
String messageText = (String) message.getPayload();
|
||||||
|
log.debug("Received message: {}", messageText);
|
||||||
|
Long userId = getUserIdFromSession(session);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error handling message: {}", message.getPayload(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
|
Long userId = getUserIdFromSession(session);
|
||||||
|
if (userId != null) {
|
||||||
|
log.info("用户[{}]断开WebSocket连接", userId);
|
||||||
|
USER_SESSIONS.remove(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessage(Long userId, String message) {
|
||||||
|
WebSocketSession session = USER_SESSIONS.get(userId);
|
||||||
|
if (session != null && session.isOpen()) {
|
||||||
|
try {
|
||||||
|
String messageText = objectMapper.writeValueAsString(message);
|
||||||
|
session.sendMessage(new TextMessage(messageText));
|
||||||
|
log.debug("Message sent to user: {}", userId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error sending message to user: {}", userId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserIdFromSession(WebSocketSession session) {
|
||||||
|
try {
|
||||||
|
String token = session.getHandshakeHeaders().getFirst("Authorization");
|
||||||
|
if (token != null && token.startsWith("Bearer ")) {
|
||||||
|
String jwtToken = token.substring(7);
|
||||||
|
return jwtUtil.getUserIdFromToken(jwtToken);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error getting user ID from session: {}", session.getId(), e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getOnlineUserCount() {
|
||||||
|
return USER_SESSIONS.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUserOnline(Long userId) {
|
||||||
|
return USER_SESSIONS.containsKey(userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
src/main/resources/log4j2-spring.xml
Normal file
45
src/main/resources/log4j2-spring.xml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN" monitorInterval="30">
|
||||||
|
<!--
|
||||||
|
status="WARN": Log4j2 自身的日志输出级别(仅用来调试日志系统本身的配置)
|
||||||
|
monitorInterval="30": 每隔 30 秒自动检测配置文件是否有修改,若有修改会重新加载
|
||||||
|
-->
|
||||||
|
<Appenders>
|
||||||
|
<!-- 控制台输出 -->
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout
|
||||||
|
pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%highlight{%-5level}] [%thread] [traceId=%X{traceId}] %logger{36} - %msg%n" />
|
||||||
|
</Console>
|
||||||
|
|
||||||
|
<!-- 轮盘日志 -->
|
||||||
|
<RollingFile name="File"
|
||||||
|
fileName="logs/app.log"
|
||||||
|
filePattern="logs/app-%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
|
<PatternLayout
|
||||||
|
pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%highlight{%-5level}] [%thread] [traceId=%X{traceId}] %logger{36} - %msg%n" />
|
||||||
|
<Policies>
|
||||||
|
<!-- 时间滚动策略,每日创建一个日志文件 -->
|
||||||
|
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
|
||||||
|
<!-- 大小滚动策略,单个文件达到 10MB 切分 -->
|
||||||
|
<SizeBasedTriggeringPolicy size="10MB" />
|
||||||
|
</Policies>
|
||||||
|
<!-- 控制保留多少历史文件,超过数量会自动删除 -->
|
||||||
|
<DefaultRolloverStrategy max="30" />
|
||||||
|
</RollingFile>
|
||||||
|
</Appenders>
|
||||||
|
|
||||||
|
<!-- 2. 配置日志记录器(Logger) -->
|
||||||
|
<Loggers>
|
||||||
|
<!-- 指定某些包或类的日志级别,如果需要可以继续添加 -->
|
||||||
|
<Logger name="com.kama" level="debug" additivity="false">
|
||||||
|
<AppenderRef ref="Console" />
|
||||||
|
<AppenderRef ref="File" />
|
||||||
|
</Logger>
|
||||||
|
|
||||||
|
<!-- 3. Root Logger,默认日志级别为 info -->
|
||||||
|
<Root level="info">
|
||||||
|
<AppenderRef ref="Console" />
|
||||||
|
<AppenderRef ref="File" />
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
Reference in New Issue
Block a user