1. 限制记录 api 日志的字段长度
2. 完整记录所有的 databus api 的请求日志 3. 新增 iwork 同步可以按 id 维度进行 4. 新增自动扫描 BusinessBaseDO 的 公司部门数据权限模式
This commit is contained in:
@@ -36,6 +36,11 @@
|
|||||||
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
|
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zt.plat</groupId>
|
||||||
|
<artifactId>zt-spring-boot-starter-mybatis</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
<!-- Test 测试相关 -->
|
<!-- Test 测试相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.zt.plat</groupId>
|
<groupId>com.zt.plat</groupId>
|
||||||
|
|||||||
@@ -2,27 +2,54 @@ package com.zt.plat.framework.business.framework;
|
|||||||
|
|
||||||
import com.zt.plat.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer;
|
import com.zt.plat.framework.datapermission.core.rule.company.CompanyDataPermissionRuleCustomizer;
|
||||||
import com.zt.plat.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
|
import com.zt.plat.framework.datapermission.core.rule.dept.DeptDataPermissionRuleCustomizer;
|
||||||
|
import org.springframework.beans.factory.BeanFactory;
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author chenbowen
|
* 自动为继承 BusinessBaseDO 的实体注册公司/部门数据权限字段。
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
public class BusinessDataPermissionConfiguration {
|
public class BusinessDataPermissionConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public CompanyDataPermissionRuleCustomizer sysCompanyDataPermissionRuleCustomizer() {
|
public BusinessDataPermissionEntityScanner businessDataPermissionEntityScanner(BeanFactory beanFactory, ApplicationContext applicationContext) {
|
||||||
return rule -> {
|
Set<String> basePackages = new LinkedHashSet<>();
|
||||||
// companyId
|
if (AutoConfigurationPackages.has(beanFactory)) {
|
||||||
rule.addCompanyColumn("demo_contract", "company_id");
|
basePackages.addAll(AutoConfigurationPackages.get(beanFactory));
|
||||||
};
|
}
|
||||||
|
if (basePackages.isEmpty()) {
|
||||||
|
basePackages.add("com.zt");
|
||||||
|
}
|
||||||
|
ClassLoader classLoader = applicationContext != null
|
||||||
|
? applicationContext.getClassLoader()
|
||||||
|
: Thread.currentThread().getContextClassLoader();
|
||||||
|
if (classLoader == null) {
|
||||||
|
classLoader = BusinessDataPermissionConfiguration.class.getClassLoader();
|
||||||
|
}
|
||||||
|
return new BusinessDataPermissionEntityScanner(basePackages, classLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() {
|
public CompanyDataPermissionRuleCustomizer autoCompanyDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) {
|
||||||
return rule -> {
|
return rule -> scanner.getEntityMetadata().forEach(metadata -> {
|
||||||
// dept
|
if (metadata.hasCompanyColumn()) {
|
||||||
rule.addDeptColumn("demo_contract", "dept_id");
|
rule.addCompanyColumn(metadata.getTableName(), metadata.getCompanyColumn());
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DeptDataPermissionRuleCustomizer autoDeptDataPermissionRuleCustomizer(BusinessDataPermissionEntityScanner scanner) {
|
||||||
|
return rule -> scanner.getEntityMetadata().forEach(metadata -> {
|
||||||
|
if (metadata.hasDeptColumn()) {
|
||||||
|
rule.addDeptColumn(metadata.getTableName(), metadata.getDeptColumn());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
|||||||
|
package com.zt.plat.framework.mybatis.core.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记业务实体对应表中的公司字段名称,默认 company_id。
|
||||||
|
*
|
||||||
|
* @author chenbow
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
public @interface CompanyColumn {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表中公司字段名称
|
||||||
|
*/
|
||||||
|
String value() default "company_id";
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.zt.plat.framework.mybatis.core.annotation;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Inherited;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记业务实体对应表中的部门字段名称,默认 dept_id。
|
||||||
|
*
|
||||||
|
* @author chenbow
|
||||||
|
*/
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
public @interface DeptColumn {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 表中部门字段名称
|
||||||
|
*/
|
||||||
|
String value() default "dept_id";
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ package com.zt.plat.framework.mybatis.core.dataobject;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.zt.plat.framework.mybatis.core.annotation.CompanyColumn;
|
||||||
|
import com.zt.plat.framework.mybatis.core.annotation.DeptColumn;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import org.apache.ibatis.type.JdbcType;
|
import org.apache.ibatis.type.JdbcType;
|
||||||
@@ -13,6 +15,8 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@EqualsAndHashCode(callSuper = true)
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@CompanyColumn
|
||||||
|
@DeptColumn
|
||||||
public class BusinessBaseDO extends BaseDO {
|
public class BusinessBaseDO extends BaseDO {
|
||||||
|
|
||||||
/** 公司编号 */
|
/** 公司编号 */
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@ package com.zt.plat.module.databus.framework.integration.gateway.core;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiInvocationContext;
|
||||||
|
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
@@ -90,6 +91,7 @@ public class ApiGatewayRequestMapper {
|
|||||||
});
|
});
|
||||||
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
context.setUserAgent(GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), HttpHeaders.USER_AGENT));
|
||||||
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
|
context.setClientIp(resolveClientIp(headers, context.getRequestHeaders()));
|
||||||
|
captureAccessLogId(context);
|
||||||
populateQueryParams(headers, context, originalRequestUri);
|
populateQueryParams(headers, context, originalRequestUri);
|
||||||
if (properties.isEnableTenantHeader()) {
|
if (properties.isEnableTenantHeader()) {
|
||||||
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
|
Object tenantHeaderValue = context.getRequestHeaders().get(properties.getTenantHeader());
|
||||||
@@ -114,6 +116,21 @@ public class ApiGatewayRequestMapper {
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void captureAccessLogId(ApiInvocationContext context) {
|
||||||
|
String headerValue = GatewayHeaderUtils.findFirstHeaderValue(context.getRequestHeaders(), ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
|
||||||
|
if (!StringUtils.hasText(headerValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Long logId = Long.valueOf(headerValue);
|
||||||
|
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, logId);
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
// 忽略格式问题,仅在属性中保留原文以便排查
|
||||||
|
context.getAttributes().put(ApiGatewayAccessLogger.ATTR_LOG_ID, headerValue);
|
||||||
|
}
|
||||||
|
context.getRequestHeaders().remove(ApiGatewayAccessLogger.HEADER_ACCESS_LOG_ID);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isInternalHeader(String headerName) {
|
private boolean isInternalHeader(String headerName) {
|
||||||
if (!StringUtils.hasText(headerName)) {
|
if (!StringUtils.hasText(headerName)) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.zt.plat.framework.tenant.core.context.TenantContextHolder;
|
|||||||
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
|
import com.zt.plat.framework.web.core.util.WebFrameworkUtils;
|
||||||
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
|
import com.zt.plat.module.databus.dal.dataobject.gateway.ApiClientCredentialDO;
|
||||||
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
import com.zt.plat.module.databus.framework.integration.config.ApiGatewayProperties;
|
||||||
|
import com.zt.plat.module.databus.framework.integration.gateway.core.ApiGatewayAccessLogger;
|
||||||
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
|
import com.zt.plat.module.databus.framework.integration.gateway.model.ApiGatewayResponse;
|
||||||
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
|
import com.zt.plat.module.databus.service.gateway.ApiAnonymousUserService;
|
||||||
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
|
import com.zt.plat.module.databus.service.gateway.ApiClientCredentialService;
|
||||||
@@ -56,6 +57,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
|||||||
private final ApiClientCredentialService credentialService;
|
private final ApiClientCredentialService credentialService;
|
||||||
private final ApiAnonymousUserService anonymousUserService;
|
private final ApiAnonymousUserService anonymousUserService;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
|
private final ApiGatewayAccessLogger accessLogger;
|
||||||
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
private final AntPathMatcher pathMatcher = new AntPathMatcher();
|
||||||
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
private static final TypeReference<Map<String, Object>> MAP_TYPE = new TypeReference<>() {};
|
||||||
|
|
||||||
@@ -72,18 +74,24 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
|||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Long accessLogId = accessLogger.logEntrance(request);
|
||||||
// 校验访问 IP 是否落在允许范围内
|
// 校验访问 IP 是否落在允许范围内
|
||||||
if (!isIpAllowed(request)) {
|
if (!isIpAllowed(request)) {
|
||||||
log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication);
|
log.warn("[API-PORTAL] 拦截来自 IP {} 访问 {} 的请求", request.getRemoteAddr(), pathWithinApplication);
|
||||||
response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
response.sendError(HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
||||||
|
accessLogger.finalizeEarly(request, HttpStatus.FORBIDDEN.value(), "IP 禁止访问");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ApiGatewayProperties.Security security = properties.getSecurity();
|
ApiGatewayProperties.Security security = properties.getSecurity();
|
||||||
ApiClientCredentialDO credential = null;
|
ApiClientCredentialDO credential = null;
|
||||||
if (!security.isEnabled()) {
|
if (!security.isEnabled()) {
|
||||||
filterChain.doFilter(request, response);
|
byte[] originalBody = StreamUtils.copyToByteArray(request.getInputStream());
|
||||||
|
CachedBodyHttpServletRequest passthroughRequest = new CachedBodyHttpServletRequest(request, originalBody);
|
||||||
|
ApiGatewayAccessLogger.propagateLogIdHeader(passthroughRequest, accessLogId);
|
||||||
|
filterChain.doFilter(passthroughRequest, response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
boolean dispatchedToGateway = false;
|
||||||
try {
|
try {
|
||||||
Long tenantId = resolveTenantId(request);
|
Long tenantId = resolveTenantId(request);
|
||||||
// 从请求头解析 appId 并加载客户端凭证,包含匿名访问配置
|
// 从请求头解析 appId 并加载客户端凭证,包含匿名访问配置
|
||||||
@@ -118,6 +126,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
// 使用可重复读取的请求包装,供后续过滤器继续消费
|
// 使用可重复读取的请求包装,供后续过滤器继续消费
|
||||||
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody);
|
CachedBodyHttpServletRequest securedRequest = new CachedBodyHttpServletRequest(request, decryptedBody);
|
||||||
|
ApiGatewayAccessLogger.propagateLogIdHeader(securedRequest, accessLogId);
|
||||||
if (StringUtils.hasText(request.getCharacterEncoding())) {
|
if (StringUtils.hasText(request.getCharacterEncoding())) {
|
||||||
securedRequest.setCharacterEncoding(request.getCharacterEncoding());
|
securedRequest.setCharacterEncoding(request.getCharacterEncoding());
|
||||||
}
|
}
|
||||||
@@ -129,6 +138,7 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
|||||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||||
try {
|
try {
|
||||||
filterChain.doFilter(securedRequest, responseWrapper);
|
filterChain.doFilter(securedRequest, responseWrapper);
|
||||||
|
dispatchedToGateway = true;
|
||||||
encryptResponse(responseWrapper, credential, security);
|
encryptResponse(responseWrapper, credential, security);
|
||||||
} finally {
|
} finally {
|
||||||
responseWrapper.copyBodyToResponse();
|
responseWrapper.copyBodyToResponse();
|
||||||
@@ -136,9 +146,15 @@ public class GatewaySecurityFilter extends OncePerRequestFilter {
|
|||||||
} catch (SecurityValidationException ex) {
|
} catch (SecurityValidationException ex) {
|
||||||
log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage());
|
log.warn("[API-PORTAL] 安全校验失败: {}", ex.getMessage());
|
||||||
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage());
|
writeErrorResponse(response, security, credential, ex.status(), ex.getMessage());
|
||||||
|
if (!dispatchedToGateway) {
|
||||||
|
accessLogger.finalizeEarly(request, ex.status().value(), ex.getMessage());
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("[API-PORTAL] 处理安全校验时出现异常", ex);
|
log.error("[API-PORTAL] 处理安全校验时出现异常", ex);
|
||||||
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败");
|
writeErrorResponse(response, security, credential, HttpStatus.INTERNAL_SERVER_ERROR, "网关安全校验失败");
|
||||||
|
if (!dispatchedToGateway) {
|
||||||
|
accessLogger.finalizeEarly(request, HttpStatus.INTERNAL_SERVER_ERROR.value(), "网关安全校验失败");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,11 +26,76 @@ import java.time.LocalDateTime;
|
|||||||
@KeySequence(value = "infra_api_error_log_seq")
|
@KeySequence(value = "infra_api_error_log_seq")
|
||||||
public class ApiErrorLogDO extends BaseDO {
|
public class ApiErrorLogDO extends BaseDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #traceId} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer TRACE_ID_MAX_LENGTH = 64;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #applicationName} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer APPLICATION_NAME_MAX_LENGTH = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #requestMethod} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer REQUEST_METHOD_MAX_LENGTH = 16;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #requestUrl} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer REQUEST_URL_MAX_LENGTH = 255;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link #requestParams} 的最大长度
|
* {@link #requestParams} 的最大长度
|
||||||
*/
|
*/
|
||||||
public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;
|
public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #userIp} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer USER_IP_MAX_LENGTH = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #userAgent} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer USER_AGENT_MAX_LENGTH = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionName} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_NAME_MAX_LENGTH = 128;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionClassName} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_CLASS_NAME_MAX_LENGTH = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionFileName} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_FILE_NAME_MAX_LENGTH = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionMethodName} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_METHOD_NAME_MAX_LENGTH = 512;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionMessage} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_MESSAGE_MAX_LENGTH = 4000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionRootCauseMessage} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_ROOT_CAUSE_MESSAGE_MAX_LENGTH = 4000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link #exceptionStackTrace} 的最大长度
|
||||||
|
*/
|
||||||
|
public static final Integer EXCEPTION_STACK_TRACE_MAX_LENGTH = 8000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 编号
|
* 编号
|
||||||
*/
|
*/
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user