Commit 0b25a0d3 authored by fangxinjiang's avatar fangxinjiang

init

parent 2775a7d5
...@@ -54,11 +54,23 @@ import java.util.Map; ...@@ -54,11 +54,23 @@ import java.util.Map;
@RequiredArgsConstructor @RequiredArgsConstructor
@EnableAuthorizationServer @EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
/**
* @author fxj
* @date 2022/5/26 20:12
* @description 数据源 保持的时候需要默认为spring 中的配置的DataSource
*/
private final DataSource dataSource; private final DataSource dataSource;
/**
* @author fxj
* @date 2022/5/26 20:12
* @description 权限控制器
*/
private final AuthenticationManager authenticationManager; private final AuthenticationManager authenticationManager;
/**
* @author fxj
* @date 2022/5/26 20:12
* @description 保持token 的方式,这里为redis
*/
private final TokenStore redisTokenStore; private final TokenStore redisTokenStore;
@Override @Override
......
package com.yifu.cloud.plus.v1.yifu.auth.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import java.util.List;
/**
* @author lengleng
* @date 2020/10/4
* <p>
* 网关配置文件
*/
@Data
@RefreshScope
@ConfigurationProperties("gateway")
public class GatewayConfigProperties {
/**
* 网关解密登录前端密码 秘钥 {@link com.yifu.cloud.plus.v1.yifu.auth.filter.PasswordDecoderFilter}
*/
private String encodeKey;
}
...@@ -16,12 +16,14 @@ ...@@ -16,12 +16,14 @@
package com.yifu.cloud.plus.v1.yifu.auth.config; package com.yifu.cloud.plus.v1.yifu.auth.config;
import com.yifu.cloud.plus.v1.yifu.auth.filter.PasswordDecoderFilter;
import com.yifu.cloud.plus.v1.yifu.common.security.component.YifuDaoAuthenticationProvider; import com.yifu.cloud.plus.v1.yifu.common.security.component.YifuDaoAuthenticationProvider;
import com.yifu.cloud.plus.v1.yifu.common.security.grant.CustomAppAuthenticationProvider; import com.yifu.cloud.plus.v1.yifu.common.security.grant.CustomAppAuthenticationProvider;
import com.yifu.cloud.plus.v1.yifu.common.security.handler.FormAuthenticationFailureHandler; import com.yifu.cloud.plus.v1.yifu.common.security.handler.FormAuthenticationFailureHandler;
import com.yifu.cloud.plus.v1.yifu.common.security.handler.SsoLogoutSuccessHandler; import com.yifu.cloud.plus.v1.yifu.common.security.handler.SsoLogoutSuccessHandler;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
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 org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
...@@ -40,6 +42,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl ...@@ -40,6 +42,7 @@ import org.springframework.security.web.authentication.logout.LogoutSuccessHandl
* @author lengleng * @author lengleng
* @date 2022/1/12 认证相关配置 * @date 2022/1/12 认证相关配置
*/ */
@Log4j2
@Primary @Primary
@Order(90) @Order(90)
@Configuration @Configuration
...@@ -49,10 +52,11 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { ...@@ -49,10 +52,11 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override @Override
@SneakyThrows @SneakyThrows
protected void configure(HttpSecurity http) { protected void configure(HttpSecurity http) {
http.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form") http.addFilterBefore(getPasswordDecoderFilter(),PasswordDecoderFilter.class)
.formLogin().loginPage("/token/login").loginProcessingUrl("/token/form")
.failureHandler(authenticationFailureHandler()).and().logout() .failureHandler(authenticationFailureHandler()).and().logout()
.logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true) .logoutSuccessHandler(logoutSuccessHandler()).deleteCookies("JSESSIONID").invalidateHttpSession(true)
.and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**").permitAll() .and().authorizeRequests().antMatchers("/token/**", "/actuator/**", "/mobile/**", "/oauth/token").permitAll()
.anyRequest().authenticated().and().csrf().disable(); .anyRequest().authenticated().and().csrf().disable();
} }
...@@ -114,4 +118,18 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter { ...@@ -114,4 +118,18 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
return PasswordEncoderFactories.createDelegatingPasswordEncoder(); return PasswordEncoderFactories.createDelegatingPasswordEncoder();
} }
/**
* @author fxj
* @date 2022/5/27 17:22
*/
public PasswordDecoderFilter getPasswordDecoderFilter(){
PasswordDecoderFilter filter = new PasswordDecoderFilter();
try {
filter.setAuthenticationManager(this.authenticationManager());
}catch (Exception e){
log.error("WebSecurityConfigure>>>>>",e);
}
filter.setAuthenticationFailureHandler(authenticationFailureHandler());
return filter;
}
} }
package com.yifu.cloud.plus.v1.yifu.auth.filter;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import com.yifu.cloud.plus.v1.yifu.auth.config.GatewayConfigProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author fxj *
* @date 2022年05月27日 11:14
* @description
*/
@Log4j2
@EnableConfigurationProperties(GatewayConfigProperties.class)
@RequiredArgsConstructor
public class PasswordDecoderFilter extends UsernamePasswordAuthenticationFilter {
private static final String KEY_ALGORITHM = "AES";
private GatewayConfigProperties gatewayConfig;
private boolean postOnly = true;
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String username = this.obtainUsername(request);
username = username != null ? username : "";
username = username.trim();
String password = this.obtainPassword(request);
password = password != null ? decryptAES(password) : "";
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 原文解密
* @return
*/
private String decryptAES(String password) {
// 构建前端对应解密AES 因子
AES aes = new AES(Mode.CFB, Padding.NoPadding,
new SecretKeySpec(gatewayConfig.getEncodeKey().getBytes(), KEY_ALGORITHM),
new IvParameterSpec(gatewayConfig.getEncodeKey().getBytes()));
// 解密
return aes.decryptStr(password);
}
}
package com.yifu.cloud.plus.v1.yifu.auth.util;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.Charsets;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @author fxj *
* @date 2022年05月27日 11:01
* @description
*/
public class MyHttpRequest extends HttpServletRequestWrapper {
private String body;
public MyHttpRequest(HttpServletRequest request) throws IOException {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream,"UTF-8"));
char[] charBuffer = new char[128];
int bytesRead = -1;
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
stringBuilder.append(charBuffer, 0, bytesRead);
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
throw ex;
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
throw ex;
}
}
}
body = stringBuilder.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes("UTF-8"));
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
return servletInputStream;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream(), Charsets.UTF_8));
}
public String getBody() {
return this.body;
}
@Override
public String getParameter(String name) {
return super.getParameter(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return super.getParameterMap();
}
@Override
public Enumeration<String> getParameterNames() {
return super.getParameterNames();
}
@Override
public String[] getParameterValues(String name) {
return super.getParameterValues(name);
}
/**
* 设置自定义post参数 //
*
* @param paramMaps
* @return
*/
public void setParamsMaps(Map paramMaps) {
Map paramBodyMap = new HashMap();
if (!StringUtils.isEmpty(body)) {
paramBodyMap = JSONObject.parseObject(body, Map.class);
}
paramBodyMap.putAll(paramMaps);
body = JSONObject.toJSONString(paramBodyMap);
}
}
...@@ -4,6 +4,9 @@ server: ...@@ -4,6 +4,9 @@ server:
jasypt: jasypt:
encryptor: encryptor:
password: pig #根密码 password: pig #根密码
# 前端界面秘钥
gateway:
encode-key: 'thanks,pig4cloud'
# 暴露监控端点 # 暴露监控端点
management: management:
endpoints: endpoints:
......
/*
* Copyright (c) 2020 yifu4cloud Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.yifu.cloud.plus.v1.yifu.common.dapr.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.PropertySource;
import org.springframework.stereotype.Component;
/*
*
*
* @author fxj
* @date 2022-05-23 14:28
**/
@Data
@Component
@PropertySource("classpath:daprConfig.properties")
@ConfigurationProperties(value = "dapr.provider", ignoreInvalidFields = false)
public class DaprProviderProperties {
/*
* @author fxj
* @date 14:34
* @Description dapr sidercar url 如:http://localhost:3005/v1.0/invoke/
**/
String appUrl;
/*
* @author fxj
* @date 14:35
* @decription app_id 如:"yifu_upms_sider"
**/
String appId;
String appPort;
String httpPort;
String grpcPort;
String metricsPort;
}
...@@ -4,9 +4,13 @@ import com.yifu.cloud.plus.v1.yifu.common.core.constant.CommonConstants; ...@@ -4,9 +4,13 @@ import com.yifu.cloud.plus.v1.yifu.common.core.constant.CommonConstants;
import com.yifu.cloud.plus.v1.yifu.common.core.util.R; import com.yifu.cloud.plus.v1.yifu.common.core.util.R;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.net.URI;
import java.net.URL;
/** /**
* @author fxj * @author fxj
* @date 2022/5/23 18:13 * @date 2022/5/23 18:13
...@@ -28,15 +32,17 @@ public class HttpDaprUtil { ...@@ -28,15 +32,17 @@ public class HttpDaprUtil {
* @date 2022-05-23 17:56 * @date 2022-05-23 17:56
* @description get请求获取指定对象信息 * @description get请求获取指定对象信息
**/ **/
public static <T> R<T> invokeMothod(String appUrl, String appId, String method, String param, Class cs) throws Exception { public static <T> R<T> invokeMethodGet(String appUrl, String appId, String method, String param, Class cs) throws Exception {
// 实例化 RestTemplate // 实例化 RestTemplate
if (null == restTemplate) { initResTemplate();
restTemplate = new RestTemplate();
}
// 初始化请求URL // 初始化请求URL
initUrl(appUrl, appId, method, param); initUrl(appUrl, appId, method, param);
ResponseEntity<T> res; ResponseEntity<T> res;
try { try {
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
res = restTemplate.getForEntity(stringBuffer.toString(), cs); res = restTemplate.getForEntity(stringBuffer.toString(), cs);
} catch (Exception e) { } catch (Exception e) {
return R.failed("获取信息失败:" + e.getMessage()); return R.failed("获取信息失败:" + e.getMessage());
...@@ -48,6 +54,35 @@ public class HttpDaprUtil { ...@@ -48,6 +54,35 @@ public class HttpDaprUtil {
} }
} }
public static <T> R<T> invokeMethodPost(String appUrl, String appId, String method, Object param, Class cs,String token) throws Exception {
// 实例化 RestTemplate
initResTemplate();
// 初始化请求URL
initUrl(appUrl, appId, method, "");
ResponseEntity<T> res;
try {
HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
headers.add("Accept", MediaType.APPLICATION_JSON.toString());
headers.add("access_token",token);
res = restTemplate.postForEntity(URI.create(stringBuffer.toString()),param, cs);
} catch (Exception e) {
return R.failed("获取信息失败:" + e.getMessage());
}
if (null != res && CommonConstants.SUCCESS_CODE == res.getStatusCodeValue()) {
return R.ok(res.getBody());
} else {
return R.failed("获取信息失败!");
}
}
private static void initResTemplate() {
if (null == restTemplate) {
restTemplate = new RestTemplate();
}
}
/** /**
* @param appUrl * @param appUrl
......
...@@ -4,3 +4,10 @@ dapr.upms.appPort=4000 ...@@ -4,3 +4,10 @@ dapr.upms.appPort=4000
dapr.upms.httpPort=3500 dapr.upms.httpPort=3500
dapr.upms.grpcPort=52000 dapr.upms.grpcPort=52000
dapr.upms.metricsPort=9094 dapr.upms.metricsPort=9094
dapr.provider.appUrl=http://localhost:3500/v1.0/invoke/
dapr.provider.appId=provider-sider
dapr.provider.appPort=7001
dapr.provider.httpPort=3500
dapr.provider.grpcPort=52000
dapr.provider.metricsPort=9094
...@@ -86,6 +86,9 @@ public class CustomAppAuthenticationProvider extends AbstractUserDetailsAuthenti ...@@ -86,6 +86,9 @@ public class CustomAppAuthenticationProvider extends AbstractUserDetailsAuthenti
return token; return token;
} }
/****
* 手动加载用户信息
*/
@Override @Override
protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication) protected UserDetails retrieveUser(String phone, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException { throws AuthenticationException {
......
...@@ -56,12 +56,13 @@ public class YifuUserDetailsServiceImpl implements YifuUserDetailsService { ...@@ -56,12 +56,13 @@ public class YifuUserDetailsServiceImpl implements YifuUserDetailsService {
@SneakyThrows @SneakyThrows
public UserDetails loadUserByUsername(String username) { public UserDetails loadUserByUsername(String username) {
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS); Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
Object o = cache.get(username);
if (cache != null && cache.get(username) != null) { if (cache != null && cache.get(username) != null) {
return (YifuUser) cache.get(username).get(); return (YifuUser) cache.get(username).get();
} }
R<UserInfo> result;// = remoteUserService.info(username, SecurityConstants.FROM_IN); R<UserInfo> result;// = remoteUserService.info(username, SecurityConstants.FROM_IN);
result = HttpDaprUtil.invokeMothod(daprUpmsProperties.getAppUrl(),daprUpmsProperties.getAppId(), "/user/getInfoByUsername", "?username="+username, UserInfo.class); result = HttpDaprUtil.invokeMethodGet(daprUpmsProperties.getAppUrl(),daprUpmsProperties.getAppId(), "/user/getInfoByUsername", "?username="+username, UserInfo.class);
UserDetails userDetails = getUserDetails(result); UserDetails userDetails = getUserDetails(result);
if (cache != null) { if (cache != null) {
cache.put(username, userDetails); cache.put(username, userDetails);
......
...@@ -16,13 +16,22 @@ ...@@ -16,13 +16,22 @@
*/ */
package com.yifu.cloud.plus.v1.consumer.service.impl; package com.yifu.cloud.plus.v1.consumer.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yifu.cloud.plus.v1.consumer.entity.Consumer; import com.yifu.cloud.plus.v1.consumer.entity.Consumer;
import com.yifu.cloud.plus.v1.consumer.mapper.ConsumerMapper; import com.yifu.cloud.plus.v1.consumer.mapper.ConsumerMapper;
import com.yifu.cloud.plus.v1.consumer.service.ConsumerService; import com.yifu.cloud.plus.v1.consumer.service.ConsumerService;
import com.yifu.cloud.plus.v1.provider.entity.Provider; import com.yifu.cloud.plus.v1.provider.entity.Provider;
import com.yifu.cloud.plus.v1.yifu.common.core.util.R;
import com.yifu.cloud.plus.v1.yifu.common.dapr.config.DaprProviderProperties;
import com.yifu.cloud.plus.v1.yifu.common.dapr.util.HttpDaprUtil;
import com.yifu.cloud.plus.v1.yifu.common.security.util.SecurityUtils;
import io.seata.spring.annotation.GlobalTransactional; import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.redis.support.collections.RedisStore;
import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
...@@ -32,12 +41,14 @@ import org.springframework.transaction.annotation.Transactional; ...@@ -32,12 +41,14 @@ import org.springframework.transaction.annotation.Transactional;
* @author fxj * @author fxj
* @date 2022-05-13 23:28:12 * @date 2022-05-13 23:28:12
*/ */
@EnableConfigurationProperties(DaprProviderProperties.class)
@RequiredArgsConstructor @RequiredArgsConstructor
@Service @Service
public class ConsumerServiceImpl extends ServiceImpl<ConsumerMapper, Consumer> implements ConsumerService { public class ConsumerServiceImpl extends ServiceImpl<ConsumerMapper, Consumer> implements ConsumerService {
private final DaprProviderProperties daprProperties;
@SneakyThrows
@GlobalTransactional // 分布式seata事务 @GlobalTransactional // 分布式seata事务
@Transactional @Transactional
@Override @Override
...@@ -49,7 +60,8 @@ public class ConsumerServiceImpl extends ServiceImpl<ConsumerMapper, Consumer> i ...@@ -49,7 +60,8 @@ public class ConsumerServiceImpl extends ServiceImpl<ConsumerMapper, Consumer> i
provider.setCreateTime(consumer.getCreateTime()); provider.setCreateTime(consumer.getCreateTime());
provider.setUpdateBy(consumer.getUpdateBy()); provider.setUpdateBy(consumer.getUpdateBy());
provider.setUpdateTime(consumer.getUpdateTime()); provider.setUpdateTime(consumer.getUpdateTime());
//remoteProviderService.testSeata(provider, SecurityConstants.FROM_IN);
R result = HttpDaprUtil.invokeMethodPost(daprProperties.getAppUrl(),daprProperties.getAppId(), "/provider", provider, Provider.class,null);
baseMapper.insert(consumer); baseMapper.insert(consumer);
return true; return true;
} }
......
...@@ -24,6 +24,7 @@ import com.yifu.cloud.plus.v1.yifu.common.log.annotation.SysLog; ...@@ -24,6 +24,7 @@ import com.yifu.cloud.plus.v1.yifu.common.log.annotation.SysLog;
import com.yifu.cloud.plus.v1.provider.entity.Provider; import com.yifu.cloud.plus.v1.provider.entity.Provider;
import com.yifu.cloud.plus.v1.provider.service.ProviderService; import com.yifu.cloud.plus.v1.provider.service.ProviderService;
import com.yifu.cloud.plus.v1.yifu.common.security.annotation.Inner; import com.yifu.cloud.plus.v1.yifu.common.security.annotation.Inner;
import com.yifu.cloud.plus.v1.yifu.common.security.util.SecurityUtils;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.security.SecurityRequirement;
...@@ -84,6 +85,7 @@ public class ProviderController { ...@@ -84,6 +85,7 @@ public class ProviderController {
@PostMapping @PostMapping
@PreAuthorize("@pms.hasPermission('provider_provider_add')" ) @PreAuthorize("@pms.hasPermission('provider_provider_add')" )
public R save(@RequestBody Provider provider) { public R save(@RequestBody Provider provider) {
SecurityUtils.getAuthentication();
return R.ok(providerService.save(provider)); return R.ok(providerService.save(provider));
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment