feat(user): 新增邮箱注册与验证码发送功能
- 新增 UserRegisterDTO 及 /user/register 接口 - 集成 MailerSend,异步发送 6 位验证码邮件 - 添加 RedisUtil 缓存验证码 10 分钟 - 补充 SEND_MAIL_FAILED、CONFIRM_PASSWORD_NOT_MATCH 错误码 - 关闭 Spring Security CSRF 与表单登录,放行 /user/register - AppleService 移除 @AllArgsConstructor,改用 @Resource 注入
This commit is contained in:
7
pom.xml
7
pom.xml
@@ -226,6 +226,13 @@
|
|||||||
<artifactId>spring-boot-starter-security</artifactId>
|
<artifactId>spring-boot-starter-security</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- mailerSender 邮件服务 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mailersend</groupId>
|
||||||
|
<artifactId>java-sdk</artifactId>
|
||||||
|
<version>1.4.1</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 添加Hibernate Validator作为Bean Validation提供程序 -->
|
<!-- 添加Hibernate Validator作为Bean Validation提供程序 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.hibernate.validator</groupId>
|
<groupId>org.hibernate.validator</groupId>
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ public enum ErrorCode {
|
|||||||
FILE_NAME_ERROR(40002, "文件名错误"),
|
FILE_NAME_ERROR(40002, "文件名错误"),
|
||||||
USER_NOT_FOUND(40401, "用户不存在"),
|
USER_NOT_FOUND(40401, "用户不存在"),
|
||||||
USER_INFO_UPDATE_FAILED(50002, "用户信息更新失败"),
|
USER_INFO_UPDATE_FAILED(50002, "用户信息更新失败"),
|
||||||
PASSWORD_OR_MAIL_ERROR(50003,"密码或邮箱错误" );
|
PASSWORD_OR_MAIL_ERROR(50003,"密码或邮箱错误" ),
|
||||||
|
SEND_MAIL_FAILED(50004,"邮件发送失败" ),
|
||||||
|
CONFIRM_PASSWORD_NOT_MATCH(50005,"重复密码不匹配" );
|
||||||
/**
|
/**
|
||||||
* 状态码
|
* 状态码
|
||||||
*/
|
*/
|
||||||
|
|||||||
10
src/main/java/com/yolo/keyborad/config/AsyncConfig.java
Normal file
10
src/main/java/com/yolo/keyborad/config/AsyncConfig.java
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.annotation.EnableAsync;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableAsync
|
||||||
|
public class AsyncConfig {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MailTaskExecutorConfig {
|
||||||
|
|
||||||
|
@Bean(name = "mailTaskExecutor")
|
||||||
|
public Executor mailTaskExecutor() {
|
||||||
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
|
|
||||||
|
executor.setCorePoolSize(2);
|
||||||
|
executor.setMaxPoolSize(5);
|
||||||
|
executor.setQueueCapacity(100);
|
||||||
|
executor.setThreadNamePrefix("mail-sender-");
|
||||||
|
|
||||||
|
executor.initialize();
|
||||||
|
return executor;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/com/yolo/keyborad/config/RedisConfig.java
Normal file
40
src/main/java/com/yolo/keyborad/config/RedisConfig.java
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package com.yolo.keyborad.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redis配置类
|
||||||
|
*
|
||||||
|
* @author ziin
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置StringRedisTemplate
|
||||||
|
* @param connectionFactory Redis连接工厂
|
||||||
|
* @return StringRedisTemplate实例
|
||||||
|
*/
|
||||||
|
@Bean("redisTemplate")
|
||||||
|
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
|
||||||
|
StringRedisTemplate template = new StringRedisTemplate();
|
||||||
|
template.setConnectionFactory(connectionFactory);
|
||||||
|
|
||||||
|
// 设置key序列化方式
|
||||||
|
template.setKeySerializer(new StringRedisSerializer());
|
||||||
|
// 设置value序列化方式
|
||||||
|
template.setValueSerializer(new StringRedisSerializer());
|
||||||
|
// 设置hash key序列化方式
|
||||||
|
template.setHashKeySerializer(new StringRedisSerializer());
|
||||||
|
// 设置hash value序列化方式
|
||||||
|
template.setHashValueSerializer(new StringRedisSerializer());
|
||||||
|
|
||||||
|
template.afterPropertiesSet();
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,7 +74,8 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
|||||||
"/user/login",
|
"/user/login",
|
||||||
"/character/listByUser",
|
"/character/listByUser",
|
||||||
"/user/updateInfo",
|
"/user/updateInfo",
|
||||||
"/user/detail"
|
"/user/detail",
|
||||||
|
"/user/register"
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ package com.yolo.keyborad.config;
|
|||||||
|
|
||||||
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.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
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;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
@@ -13,4 +16,21 @@ public class SecurityConfig {
|
|||||||
// strength(cost)默认 10,够用了;可以视情况调高,比如 12
|
// strength(cost)默认 10,够用了;可以视情况调高,比如 12
|
||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http
|
||||||
|
// REST 接口通常会关掉 csrf(前后端分离的话)
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
// 配置请求权限
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.anyRequest().permitAll() // 所有请求都放行
|
||||||
|
)
|
||||||
|
// 关掉默认的登录页表单
|
||||||
|
.formLogin(AbstractHttpConfigurer::disable)
|
||||||
|
// 关掉 httpBasic 弹窗登录
|
||||||
|
.httpBasic(AbstractHttpConfigurer::disable);
|
||||||
|
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import com.yolo.keyborad.common.ResultUtils;
|
|||||||
import com.yolo.keyborad.model.dto.AppleLoginReq;
|
import com.yolo.keyborad.model.dto.AppleLoginReq;
|
||||||
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
||||||
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
||||||
|
import com.yolo.keyborad.model.dto.user.UserRegisterDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||||
import com.yolo.keyborad.model.vo.user.KeyboardUserInfoRespVO;
|
import com.yolo.keyborad.model.vo.user.KeyboardUserInfoRespVO;
|
||||||
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
||||||
@@ -78,4 +79,11 @@ public class UserController {
|
|||||||
KeyboardUser keyboardUser = userService.getById(loginId);
|
KeyboardUser keyboardUser = userService.getById(loginId);
|
||||||
return ResultUtils.success(BeanUtil.copyProperties(keyboardUser, KeyboardUserInfoRespVO.class));
|
return ResultUtils.success(BeanUtil.copyProperties(keyboardUser, KeyboardUserInfoRespVO.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
@Operation(summary = "用户注册",description = "用户注册接口")
|
||||||
|
public BaseResponse<Boolean> register(@RequestBody UserRegisterDTO userRegisterDTO) {
|
||||||
|
userService.userRegister(userRegisterDTO);
|
||||||
|
return ResultUtils.success(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.yolo.keyborad.model.dto.user;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/12/3 20:56
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class UserRegisterDTO {
|
||||||
|
|
||||||
|
private String mailAddress;
|
||||||
|
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
private String passwordConfirm;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.yolo.keyborad.service;
|
|||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
||||||
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
||||||
|
import com.yolo.keyborad.model.dto.user.UserRegisterDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||||
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
||||||
|
|
||||||
@@ -19,4 +20,7 @@ public interface UserService extends IService<KeyboardUser> {
|
|||||||
KeyboardUserRespVO login(UserLoginDTO userLoginDTO);
|
KeyboardUserRespVO login(UserLoginDTO userLoginDTO);
|
||||||
|
|
||||||
Boolean updateUserInfo(KeyboardUserReq keyboardUser);
|
Boolean updateUserInfo(KeyboardUserReq keyboardUser);
|
||||||
|
|
||||||
|
Boolean userRegister(UserRegisterDTO userRegisterDTO);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import java.util.Objects;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@AllArgsConstructor
|
|
||||||
public class AppleServiceImpl implements IAppleService {
|
public class AppleServiceImpl implements IAppleService {
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
|
|||||||
@@ -12,13 +12,21 @@ import com.yolo.keyborad.exception.BusinessException;
|
|||||||
import com.yolo.keyborad.mapper.KeyboardUserMapper;
|
import com.yolo.keyborad.mapper.KeyboardUserMapper;
|
||||||
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
import com.yolo.keyborad.model.dto.user.KeyboardUserReq;
|
||||||
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
import com.yolo.keyborad.model.dto.user.UserLoginDTO;
|
||||||
|
import com.yolo.keyborad.model.dto.user.UserRegisterDTO;
|
||||||
import com.yolo.keyborad.model.entity.KeyboardUser;
|
import com.yolo.keyborad.model.entity.KeyboardUser;
|
||||||
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
import com.yolo.keyborad.model.vo.user.KeyboardUserRespVO;
|
||||||
import com.yolo.keyborad.service.UserService;
|
import com.yolo.keyborad.service.UserService;
|
||||||
|
import com.yolo.keyborad.utils.RedisUtil;
|
||||||
|
import com.yolo.keyborad.utils.SendMailUtils;
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* @author: ziin
|
* @author: ziin
|
||||||
@@ -32,7 +40,14 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
|||||||
private KeyboardUserMapper keyboardUserMapper;
|
private KeyboardUserMapper keyboardUserMapper;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private BCryptPasswordEncoder bCryptPasswordEncoder;
|
private PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private SendMailUtils sendMailUtils;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public KeyboardUser selectUserWithSubjectId(String sub) {
|
public KeyboardUser selectUserWithSubjectId(String sub) {
|
||||||
@@ -47,7 +62,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
|||||||
KeyboardUser keyboardUser = new KeyboardUser();
|
KeyboardUser keyboardUser = new KeyboardUser();
|
||||||
keyboardUser.setSubjectId(sub);
|
keyboardUser.setSubjectId(sub);
|
||||||
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||||
keyboardUser.setNickName("User" + RandomUtil.randomString(6));
|
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||||
keyboardUserMapper.insert(keyboardUser);
|
keyboardUserMapper.insert(keyboardUser);
|
||||||
return keyboardUser;
|
return keyboardUser;
|
||||||
}
|
}
|
||||||
@@ -63,7 +78,7 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
|||||||
if (keyboardUser == null) {
|
if (keyboardUser == null) {
|
||||||
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
throw new BusinessException(ErrorCode.USER_NOT_FOUND);
|
||||||
}
|
}
|
||||||
if (!bCryptPasswordEncoder.matches(userLoginDTO.getPassword(), keyboardUser.getPassword())) {
|
if (!passwordEncoder.matches(userLoginDTO.getPassword(), keyboardUser.getPassword())) {
|
||||||
throw new BusinessException(ErrorCode.PASSWORD_OR_MAIL_ERROR);
|
throw new BusinessException(ErrorCode.PASSWORD_OR_MAIL_ERROR);
|
||||||
}
|
}
|
||||||
StpUtil.login(keyboardUser.getId());
|
StpUtil.login(keyboardUser.getId());
|
||||||
@@ -89,4 +104,22 @@ public class UserServiceImpl extends ServiceImpl<KeyboardUserMapper, KeyboardUse
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Boolean userRegister(UserRegisterDTO userRegisterDTO) {
|
||||||
|
if (!userRegisterDTO.getPassword().equals(userRegisterDTO.getPasswordConfirm())) {
|
||||||
|
throw new BusinessException(ErrorCode.CONFIRM_PASSWORD_NOT_MATCH);
|
||||||
|
}
|
||||||
|
KeyboardUser keyboardUser = new KeyboardUser();
|
||||||
|
keyboardUser.setUid(IdUtil.getSnowflake().nextId());
|
||||||
|
keyboardUser.setNickName("User_" + RandomUtil.randomString(6));
|
||||||
|
keyboardUser.setPassword(passwordEncoder.encode(userRegisterDTO.getPassword()));
|
||||||
|
keyboardUser.setEmail(userRegisterDTO.getMailAddress());
|
||||||
|
log.info(keyboardUser.toString());
|
||||||
|
int code = RandomUtil.randomInt(100000, 999999);
|
||||||
|
redisUtil.setEx("user:"+userRegisterDTO.getMailAddress(), String.valueOf(code),600, TimeUnit.SECONDS);
|
||||||
|
sendMailUtils.sendEmail(keyboardUser.getNickName(),userRegisterDTO.getMailAddress(),code);
|
||||||
|
return keyboardUserMapper.insert(keyboardUser) > 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1359
src/main/java/com/yolo/keyborad/utils/RedisUtil.java
Normal file
1359
src/main/java/com/yolo/keyborad/utils/RedisUtil.java
Normal file
File diff suppressed because it is too large
Load Diff
57
src/main/java/com/yolo/keyborad/utils/SendMailUtils.java
Normal file
57
src/main/java/com/yolo/keyborad/utils/SendMailUtils.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.yolo.keyborad.utils;
|
||||||
|
|
||||||
|
import com.mailersend.sdk.MailerSend;
|
||||||
|
import com.mailersend.sdk.MailerSendResponse;
|
||||||
|
import com.mailersend.sdk.Recipient;
|
||||||
|
import com.mailersend.sdk.emails.Email;
|
||||||
|
import com.mailersend.sdk.exceptions.MailerSendException;
|
||||||
|
import com.yolo.keyborad.common.ErrorCode;
|
||||||
|
import com.yolo.keyborad.exception.BusinessException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.scheduling.annotation.Async;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* @author: ziin
|
||||||
|
* @date: 2025/12/3 20:43
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class SendMailUtils {
|
||||||
|
|
||||||
|
@Value("${mail_access_token}")
|
||||||
|
private String accessToken;
|
||||||
|
|
||||||
|
@Async("mailTaskExecutor")
|
||||||
|
public void sendEmail(String userName,String userMail,Integer code ) {
|
||||||
|
|
||||||
|
Email email = new Email();
|
||||||
|
|
||||||
|
email.setFrom("verify code", "MS_JqR6MO@no-replay.loveamorkey.com");
|
||||||
|
|
||||||
|
Recipient recipient = new Recipient(userName, userMail);
|
||||||
|
|
||||||
|
email.addRecipient(userName,userMail);
|
||||||
|
|
||||||
|
email.setSubject("no-replay");
|
||||||
|
|
||||||
|
email.setTemplateId("k68zxl28q79lj905");
|
||||||
|
|
||||||
|
email.addPersonalization(recipient, "code", code);
|
||||||
|
|
||||||
|
MailerSend ms = new MailerSend();
|
||||||
|
|
||||||
|
ms.setToken(accessToken);
|
||||||
|
|
||||||
|
try {
|
||||||
|
MailerSendResponse response = ms.emails().send(email);
|
||||||
|
log.info("邮件发送成功 messageId={}", response.messageId);
|
||||||
|
} catch (MailerSendException e) {
|
||||||
|
log.error("邮件发送失败: {}", e.getMessage());
|
||||||
|
// 异步任务不能再抛业务异常,避免影响主业务流程
|
||||||
|
// 如果需要,你可以把失败记录写入 DB 或重试队列
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -48,4 +48,6 @@ mybatis-plus:
|
|||||||
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
|
||||||
|
|
||||||
appid: loveKeyboard
|
appid: loveKeyboard
|
||||||
appsecret: kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H
|
appsecret: kZJM39HYvhxwbJkG1fmquQRVkQiLAh2H
|
||||||
|
|
||||||
|
mail_access_token: mlsn.f2aafcaf4bccf01529f8636fa13a2c16c33a934d5e14be3adb0cc21c2fe40fe1
|
||||||
Reference in New Issue
Block a user