@@ -24,6 +24,7 @@ public class HandlerConfig {
|
||||
TariffHandler tariffHandler,
|
||||
TariffInfoHandler tariffInfoHandler,
|
||||
ReferralHandler referralHandler,
|
||||
ResetPasswordHandler resetPasswordHandler,
|
||||
DaDataHandler daDataHandler
|
||||
) {
|
||||
Map<Integer, RequestHandler> map = new HashMap<>();
|
||||
@@ -35,6 +36,7 @@ public class HandlerConfig {
|
||||
map.put(20007, imageFoundRequestHandler);
|
||||
map.put(20008, authRequestHandler);
|
||||
map.put(20009, verifyRegisterUser);
|
||||
map.put(20010, resetPasswordHandler);
|
||||
map.put(30000, companyHandler);
|
||||
map.put(30001, tariffHandler);
|
||||
map.put(30002, tariffInfoHandler);
|
||||
|
||||
@@ -44,7 +44,8 @@ public enum MessageCode {
|
||||
COMPANY_NOT_FOUND(4, "Company not found"),
|
||||
COMPANY_ALREADY_EXISTS(2, "Company already exists"),
|
||||
USER_LIMIT_IS_OVER(2, "Over user limits"),
|
||||
ERROR_TARIFF_INFO(2, "Erorr with tariff info");
|
||||
ERROR_TARIFF_INFO(2, "Erorr with tariff info"),
|
||||
USER_NOT_ACTIVE(2, "User not active");
|
||||
|
||||
private final Integer code;
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package ru.soune.nocopy.dto.register;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ResetPasswordRequest {
|
||||
|
||||
@JsonProperty("email")
|
||||
private String email;
|
||||
|
||||
@JsonProperty("verifyToken")
|
||||
private String verifyToken;
|
||||
|
||||
@JsonProperty("action")
|
||||
private String action;
|
||||
|
||||
@JsonProperty("password")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.soune.nocopy.dto.register;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ResetPasswordResponse {
|
||||
@JsonProperty("user_id")
|
||||
private Long userId;
|
||||
|
||||
@JsonProperty("authToken")
|
||||
private String authToken;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.*;
|
||||
import org.hibernate.annotations.ColumnDefault;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
import ru.soune.nocopy.entity.company.Company;
|
||||
@@ -102,6 +103,15 @@ public class User {
|
||||
@ToString.Exclude
|
||||
private TariffInfo personalTariffInfo;
|
||||
|
||||
@Column(name = "failed_login_attempts")
|
||||
@ColumnDefault("0")
|
||||
private Integer failedLoginAttempts = 0;
|
||||
|
||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
@JsonIgnore
|
||||
@ToString.Exclude
|
||||
private UserNotActive userNotActive;
|
||||
|
||||
public TariffInfo getActiveTariffInfo() {
|
||||
if (company != null && company.getTariffInfo() != null) {
|
||||
return company.getTariffInfo();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package ru.soune.nocopy.entity.user;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "user_not_active")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@ToString
|
||||
@Builder
|
||||
public class UserNotActive {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
private User user;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "blocked_until")
|
||||
private LocalDateTime blockedUntil;
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package ru.soune.nocopy.handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.soune.nocopy.dto.BaseRequest;
|
||||
import ru.soune.nocopy.dto.BaseResponse;
|
||||
import ru.soune.nocopy.dto.MessageCode;
|
||||
import ru.soune.nocopy.dto.file.ActionResponse;
|
||||
import ru.soune.nocopy.dto.register.ResetPasswordRequest;
|
||||
import ru.soune.nocopy.dto.register.ResetPasswordResponse;
|
||||
import ru.soune.nocopy.entity.user.AuthToken;
|
||||
import ru.soune.nocopy.entity.user.EmailVerificationToken;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
import ru.soune.nocopy.exception.UserNotFoundException;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.service.mail.EmailService;
|
||||
import ru.soune.nocopy.service.register.AuthService;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ResetPasswordHandler implements RequestHandler{
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final EmailService emailService;
|
||||
|
||||
private final AuthService authService;
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
|
||||
|
||||
private final AuthTokenRepository authTokenRepository;
|
||||
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Override
|
||||
public BaseResponse handle(BaseRequest request) throws Exception {
|
||||
ResetPasswordRequest resetPasswordRequest = objectMapper.convertValue(request.getMessageBody(),
|
||||
ResetPasswordRequest.class);
|
||||
String action = resetPasswordRequest.getAction();
|
||||
User user = userRepository.findByEmail(resetPasswordRequest.getEmail());
|
||||
AuthToken authToken;
|
||||
|
||||
if (user == null) {
|
||||
throw new UserNotFoundException("User not found with email: " + resetPasswordRequest.getEmail());
|
||||
}
|
||||
|
||||
switch (action) {
|
||||
case "resetPassword":
|
||||
authToken = authService.generateAuthToken(user);
|
||||
|
||||
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
|
||||
|
||||
emailService.sendResetPasswordEmail(authToken.getUser(), emailToken.getToken());
|
||||
|
||||
return successResponse(request, authToken, user, MessageCode.SUCCESS.getDescription());
|
||||
case "confirmVerification":
|
||||
String verifyToken = resetPasswordRequest.getVerifyToken();
|
||||
|
||||
EmailVerificationToken verificationToken =
|
||||
emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), verifyToken);
|
||||
|
||||
if (verificationToken == null) {
|
||||
throw new NotValidFieldException("Verification token not found",
|
||||
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
|
||||
MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken)));
|
||||
}
|
||||
List<AuthToken> authTokens = authTokenRepository.findByUserId(user.getId());
|
||||
|
||||
authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst();
|
||||
|
||||
user.setPassword(passwordEncoder.encode(resetPasswordRequest.getPassword()));
|
||||
user.setActive(true);
|
||||
|
||||
emailVerificationTokenRepository.delete(verificationToken);
|
||||
|
||||
User saveUser = userRepository.save(user);
|
||||
|
||||
return successResponse(request, authToken, saveUser, MessageCode.SUCCESS.getDescription());
|
||||
default:
|
||||
ActionResponse response = ActionResponse.builder()
|
||||
.action(action)
|
||||
.availableActions(Arrays.asList("confirmVerification", "resetPassword"))
|
||||
.build();
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(),
|
||||
"Invalid action: " + action, response);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, User user, String message) {
|
||||
ResetPasswordResponse resetPasswordResponse = new ResetPasswordResponse();
|
||||
resetPasswordResponse.setAuthToken(authToken.getToken());
|
||||
resetPasswordResponse.setUserId(user.getId());
|
||||
|
||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, resetPasswordResponse);
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import ru.soune.nocopy.service.register.AuthService;
|
||||
import ru.soune.nocopy.service.user.UserService;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -53,9 +54,9 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
||||
User user = userRepository.findById(userId)
|
||||
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
|
||||
|
||||
Optional<AuthToken> userAuthTokens = authTokenRepository.findByUserId(userId);
|
||||
List<AuthToken> userAuthTokens = authTokenRepository.findByUserId(userId);
|
||||
|
||||
AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get();
|
||||
AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.getFirst();
|
||||
|
||||
if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) {
|
||||
return handleResend(request, user, authToken);
|
||||
|
||||
@@ -20,5 +20,5 @@ public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
|
||||
nativeQuery = true)
|
||||
Long findUserIdByToken(String token);
|
||||
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
|
||||
Optional<AuthToken> findByUserId(Long userId);
|
||||
List<AuthToken> findByUserId(Long userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package ru.soune.nocopy.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.soune.nocopy.entity.user.UserNotActive;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface UserNotActiveRepository extends JpaRepository<UserNotActive, Long> {
|
||||
Optional<UserNotActive> findByUserId(Long userId);
|
||||
List<UserNotActive> findByBlockedUntilBefore(LocalDateTime now);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package ru.soune.nocopy.service.auth;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.entity.user.UserNotActive;
|
||||
import ru.soune.nocopy.repository.UserNotActiveRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CleanUpNotActiveUser {
|
||||
|
||||
@Autowired
|
||||
private UserNotActiveRepository userNotActiveRepository;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
@Scheduled(fixedDelay = 600000)
|
||||
public void cleanupNotActiveUsers() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<UserNotActive> expiredTokens = userNotActiveRepository.findByBlockedUntilBefore(now);
|
||||
|
||||
for (UserNotActive userNotActive : expiredTokens) {
|
||||
User user = userNotActive.getUser();
|
||||
user.setActive(true);
|
||||
user.setFailedLoginAttempts(0);
|
||||
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
if (!expiredTokens.isEmpty()) {
|
||||
userNotActiveRepository.deleteAll(expiredTokens);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,9 @@ public class EmailService {
|
||||
@Value("${app.email.verification.user}")
|
||||
private String user;
|
||||
|
||||
@Value("${app.email.verification.drop-password-subject}")
|
||||
private String dropPasswordSubject;
|
||||
|
||||
public void sendVerificationEmail(User user, String code) throws MessagingException, IOException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
@@ -45,8 +48,8 @@ public class EmailService {
|
||||
helper.setFrom("noreply@no-copy.ru");
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setSubject(standartSubject);
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
|
||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
|
||||
"templates/email-verification.html");
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
@@ -69,8 +72,8 @@ public class EmailService {
|
||||
return String.valueOf(code);
|
||||
}
|
||||
|
||||
private String loadAndProcessTemplate(String username, String code) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource("templates/email-verification.html");
|
||||
private String loadAndProcessTemplate(String username, String code, String templatePath) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource(templatePath);
|
||||
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
String safeUsername = escapeHtml(username != null ? username : user);
|
||||
String safeCode = escapeHtml(code);
|
||||
@@ -89,4 +92,25 @@ public class EmailService {
|
||||
.replace("\"", """)
|
||||
.replace("'", "'");
|
||||
}
|
||||
|
||||
public void sendResetPasswordEmail(User user, String code) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||
|
||||
helper.setFrom(standartAdressFrom);
|
||||
helper.setTo(user.getEmail());
|
||||
helper.setSubject(dropPasswordSubject);
|
||||
|
||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
|
||||
"templates/reset-password.html");
|
||||
helper.setText(htmlContent, true);
|
||||
|
||||
mailSender.send(message);
|
||||
|
||||
log.info("Email sent to: {}", user.getEmail());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send email: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,10 @@ import ru.soune.nocopy.entity.tarif.TariffType;
|
||||
import ru.soune.nocopy.entity.user.AuthToken;
|
||||
import ru.soune.nocopy.entity.user.Permission;
|
||||
import ru.soune.nocopy.entity.user.User;
|
||||
import ru.soune.nocopy.entity.user.UserNotActive;
|
||||
import ru.soune.nocopy.exception.NotFoundAuthToken;
|
||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||
import ru.soune.nocopy.repository.CompanyRepository;
|
||||
import ru.soune.nocopy.repository.TariffInfoRepository;
|
||||
import ru.soune.nocopy.repository.UserRepository;
|
||||
import ru.soune.nocopy.repository.*;
|
||||
import ru.soune.nocopy.service.file.CheckCounterService;
|
||||
import ru.soune.nocopy.service.tariff.TariffInfoService;
|
||||
|
||||
@@ -48,6 +46,8 @@ public class AuthService {
|
||||
|
||||
private final TariffInfoService tariffInfoService;
|
||||
|
||||
private final UserNotActiveRepository userNotActiveRepository;
|
||||
|
||||
private final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
@Transactional
|
||||
@@ -102,14 +102,30 @@ public class AuthService {
|
||||
return authToken.getUser().getId();
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
public AuthToken login(LoginRequest request) {
|
||||
User user = userRepository.findByEmail(request.getEmail());
|
||||
|
||||
if (!user.isActive()) {
|
||||
throw new NotValidFieldException("User not active", new BaseResponse(20003,
|
||||
MessageCode.USER_NOT_ACTIVE.getCode(),
|
||||
MessageCode.USER_NOT_ACTIVE.getDescription(), Map.of("userId", user.getId())));
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||
LoginAnswer loginAnswer = new LoginAnswer();
|
||||
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
|
||||
loginAnswer.setVerified(user.isEmailVerified());
|
||||
loginAnswer.setActive(user.isActive());
|
||||
|
||||
int attempts = user.getFailedLoginAttempts() + 1;
|
||||
user.setFailedLoginAttempts(attempts);
|
||||
|
||||
if (attempts >= 5) {
|
||||
user.setActive(false);
|
||||
blockUser(user);
|
||||
}
|
||||
|
||||
userRepository.save(user);
|
||||
|
||||
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
|
||||
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
|
||||
@@ -117,6 +133,9 @@ public class AuthService {
|
||||
}
|
||||
|
||||
user.setLastLoginAt(LocalDateTime.now());
|
||||
user.setFailedLoginAttempts(0);
|
||||
user.setActive(true);
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
AuthToken authToken = new AuthToken();
|
||||
@@ -132,6 +151,7 @@ public class AuthService {
|
||||
.ifPresent(authTokenRepository::delete);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public AuthToken generateAuthToken(User user) {
|
||||
return authTokenRepository.save(genereateAuthToken(user));
|
||||
}
|
||||
@@ -211,4 +231,19 @@ public class AuthService {
|
||||
user.setCompany(company);
|
||||
user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS);
|
||||
}
|
||||
|
||||
public void blockUser(User user) {
|
||||
Optional<UserNotActive> existing = userNotActiveRepository.findByUserId(user.getId());
|
||||
if (existing.isEmpty()) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
UserNotActive notActive = UserNotActive.builder()
|
||||
.user(user)
|
||||
.createdAt(now)
|
||||
.blockedUntil(now.plusMinutes(15))
|
||||
.build();
|
||||
|
||||
userNotActiveRepository.save(notActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user