Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8555002415 | ||
|
|
4560925183 |
@@ -19,8 +19,7 @@ public class HandlerConfig {
|
|||||||
LogoutRequestHandler logoutHandler,
|
LogoutRequestHandler logoutHandler,
|
||||||
ImageFoundRequestHandler imageFoundRequestHandler,
|
ImageFoundRequestHandler imageFoundRequestHandler,
|
||||||
VerifyRegisterUserHandler verifyRegisterUser,
|
VerifyRegisterUserHandler verifyRegisterUser,
|
||||||
AuthRequestHandler authRequestHandler,
|
AuthRequestHandler authRequestHandler
|
||||||
ResetPasswordHandler resetPasswordHandler
|
|
||||||
) {
|
) {
|
||||||
Map<Integer, RequestHandler> map = new HashMap<>();
|
Map<Integer, RequestHandler> map = new HashMap<>();
|
||||||
map.put(20001, login);
|
map.put(20001, login);
|
||||||
@@ -31,7 +30,6 @@ public class HandlerConfig {
|
|||||||
map.put(20007, imageFoundRequestHandler);
|
map.put(20007, imageFoundRequestHandler);
|
||||||
map.put(20008, authRequestHandler);
|
map.put(20008, authRequestHandler);
|
||||||
map.put(20009, verifyRegisterUser);
|
map.put(20009, verifyRegisterUser);
|
||||||
map.put(20010, resetPasswordHandler);
|
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public enum MessageCode {
|
|||||||
AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
|
AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
|
||||||
FILE_NOT_FOUND(4, "File not found"),
|
FILE_NOT_FOUND(4, "File not found"),
|
||||||
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"),
|
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"),
|
||||||
USER_NOT_ACTIVE(2, "User not active");
|
SEND_EMAIL_EXCEPTION(2, "Send email exception"),;
|
||||||
|
|
||||||
private final Integer code;
|
private final Integer code;
|
||||||
|
|
||||||
|
|||||||
@@ -17,5 +17,9 @@ public class RegAnswer {
|
|||||||
|
|
||||||
private boolean isVerified;
|
private boolean isVerified;
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
private String email;
|
||||||
|
|
||||||
private List<Map<String, String>> fieldErrors;
|
private List<Map<String, String>> fieldErrors;
|
||||||
}
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
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,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
|||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import jakarta.validation.constraints.Size;
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.ColumnDefault;
|
|
||||||
import org.springframework.data.annotation.CreatedDate;
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
import ru.soune.nocopy.entity.file.ImageProtection;
|
import ru.soune.nocopy.entity.file.ImageProtection;
|
||||||
@@ -77,11 +76,6 @@ public class User {
|
|||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private List<AuthToken> tokens = new ArrayList<>();
|
private List<AuthToken> tokens = new ArrayList<>();
|
||||||
|
|
||||||
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
|
||||||
@JsonIgnore
|
|
||||||
@ToString.Exclude
|
|
||||||
private UserNotActive userNotActive;
|
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
@@ -96,8 +90,4 @@ public class User {
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@ToString.Exclude
|
@ToString.Exclude
|
||||||
private List<ImageProtection> imageProtections = new ArrayList<>();
|
private List<ImageProtection> imageProtections = new ArrayList<>();
|
||||||
|
|
||||||
@Column(name = "failed_login_attempts")
|
|
||||||
@ColumnDefault("0")
|
|
||||||
private Integer failedLoginAttempts = 0;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@@ -61,14 +61,24 @@ public class RegRequestHandler implements RequestHandler {
|
|||||||
AuthToken authToken = authService.register(regRequest);
|
AuthToken authToken = authService.register(regRequest);
|
||||||
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
|
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
|
||||||
|
|
||||||
|
try {
|
||||||
emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken());
|
emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken());
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(),
|
||||||
|
MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of(
|
||||||
|
"message", "not send",
|
||||||
|
"email", regRequest.getEmail()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
String token = authToken.getToken();
|
String token = authToken.getToken();
|
||||||
authService.useUserAuthToken(token);
|
authService.useUserAuthToken(token);
|
||||||
|
|
||||||
RegAnswer regAnswer = new RegAnswer();
|
RegAnswer regAnswer = new RegAnswer();
|
||||||
regAnswer.setToken(token);
|
|
||||||
regAnswer.setVerified(false);
|
regAnswer.setVerified(false);
|
||||||
regAnswer.setActive(false);
|
regAnswer.setActive(false);
|
||||||
|
regAnswer.setUserId(authToken.getUser().getId());
|
||||||
|
|
||||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
|
||||||
MessageCode.SUCCESS.getDescription(), regAnswer);
|
MessageCode.SUCCESS.getDescription(), regAnswer);
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package ru.soune.nocopy.handler;
|
package ru.soune.nocopy.handler;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -21,7 +22,7 @@ import ru.soune.nocopy.service.mail.EmailService;
|
|||||||
import ru.soune.nocopy.service.register.AuthService;
|
import ru.soune.nocopy.service.register.AuthService;
|
||||||
import ru.soune.nocopy.service.user.UserService;
|
import ru.soune.nocopy.service.user.UserService;
|
||||||
|
|
||||||
import java.util.List;
|
import java.io.IOException;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
private final AuthService authService;
|
private final AuthService authService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BaseResponse handle(BaseRequest request) {
|
public BaseResponse handle(BaseRequest request) throws MessagingException, IOException {
|
||||||
VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class);
|
VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class);
|
||||||
|
|
||||||
Long userId = verifyUserRequest.getUserId();
|
Long userId = verifyUserRequest.getUserId();
|
||||||
@@ -52,9 +53,9 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
User user = userRepository.findById(userId)
|
User user = userRepository.findById(userId)
|
||||||
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
|
.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId));
|
||||||
|
|
||||||
List<AuthToken> authTokens = authTokenRepository.findByUserId(user.getId());
|
Optional<AuthToken> userAuthTokens = authTokenRepository.findByUserId(userId);
|
||||||
|
|
||||||
AuthToken authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst();
|
AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get();
|
||||||
|
|
||||||
if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) {
|
if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) {
|
||||||
return handleResend(request, user, authToken);
|
return handleResend(request, user, authToken);
|
||||||
@@ -63,7 +64,7 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken());
|
return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken());
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) {
|
private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) throws MessagingException, IOException {
|
||||||
|
|
||||||
EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId());
|
EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId());
|
||||||
|
|
||||||
@@ -71,7 +72,8 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
emailService.sendVerificationEmail(user, token.getToken());
|
emailService.sendVerificationEmail(user, token.getToken());
|
||||||
|
|
||||||
log.info("Verification token resent for user {}", user.getEmail());
|
log.info("Verification token resent for user {}", user.getEmail());
|
||||||
return successResponse(request, authToken, "Verification code has been resent", false, false);
|
return successResponse(request, authToken, "Verification code has been resent", false,
|
||||||
|
false, user.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (token != null) {
|
if (token != null) {
|
||||||
@@ -85,7 +87,7 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
log.info("New verification token generated for user {}", user.getEmail());
|
log.info("New verification token generated for user {}", user.getEmail());
|
||||||
|
|
||||||
return successResponse(request, authToken, "New verification code has been sent",
|
return successResponse(request, authToken, "New verification code has been sent",
|
||||||
false, false);
|
false, false, user.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -100,19 +102,20 @@ public class VerifyRegisterUserHandler implements RequestHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
User updateUser = userService.activateAndVerifyUser(user.getId());
|
User updateUser = userService.activateAndVerifyUser(user.getId());
|
||||||
|
|
||||||
emailVerificationTokenRepository.delete(token);
|
emailVerificationTokenRepository.delete(token);
|
||||||
|
|
||||||
return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(),
|
return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(),
|
||||||
updateUser.isEmailVerified());
|
updateUser.isEmailVerified(), updateUser.getEmail());
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive,
|
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive,
|
||||||
boolean isVerified) {
|
boolean isVerified, String email) {
|
||||||
|
|
||||||
RegAnswer regAnswer = new RegAnswer();
|
RegAnswer regAnswer = new RegAnswer();
|
||||||
regAnswer.setToken(authToken.getToken());
|
regAnswer.setToken(authToken.getToken());
|
||||||
regAnswer.setVerified(isVerified);
|
regAnswer.setVerified(isVerified);
|
||||||
regAnswer.setActive(isActive);
|
regAnswer.setActive(isActive);
|
||||||
|
regAnswer.setEmail(email);
|
||||||
|
|
||||||
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer);
|
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,5 @@ public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
|
|||||||
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
|
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
|
||||||
Optional<AuthToken> findByToken(String token);
|
Optional<AuthToken> findByToken(String token);
|
||||||
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
|
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
|
||||||
List<AuthToken> findByUserId(Long userId);
|
Optional<AuthToken> findByUserId(Long userId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package ru.soune.nocopy.service.mail;
|
package ru.soune.nocopy.service.mail;
|
||||||
|
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
import jakarta.mail.internet.MimeMessage;
|
import jakarta.mail.internet.MimeMessage;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -31,17 +32,13 @@ public class EmailService {
|
|||||||
@Value("${app.email.verification.standart-subject}")
|
@Value("${app.email.verification.standart-subject}")
|
||||||
private String standartSubject;
|
private String standartSubject;
|
||||||
|
|
||||||
@Value("${app.email.verification.drop-password-subject}")
|
|
||||||
private String dropPasswordSubject;
|
|
||||||
|
|
||||||
@Value("${app.email.verification.standart-address-from}")
|
@Value("${app.email.verification.standart-address-from}")
|
||||||
private String standartAdressFrom;
|
private String standartAdressFrom;
|
||||||
|
|
||||||
@Value("${app.email.verification.user}")
|
@Value("${app.email.verification.user}")
|
||||||
private String user;
|
private String user;
|
||||||
|
|
||||||
public void sendVerificationEmail(User user, String code) {
|
public void sendVerificationEmail(User user, String code) throws MessagingException, IOException {
|
||||||
try {
|
|
||||||
MimeMessage message = mailSender.createMimeMessage();
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
|
||||||
|
|
||||||
@@ -49,38 +46,12 @@ public class EmailService {
|
|||||||
helper.setTo(user.getEmail());
|
helper.setTo(user.getEmail());
|
||||||
helper.setSubject(standartSubject);
|
helper.setSubject(standartSubject);
|
||||||
|
|
||||||
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
|
String htmlContent = loadAndProcessTemplate(user.getFullName(), code);
|
||||||
"templates/email-verification.html");
|
|
||||||
helper.setText(htmlContent, true);
|
helper.setText(htmlContent, true);
|
||||||
|
|
||||||
mailSender.send(message);
|
mailSender.send(message);
|
||||||
|
|
||||||
log.info("Email sent to: {}", user.getEmail());
|
log.info("Email sent to: {}", user.getEmail());
|
||||||
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to send email: {}", e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -100,8 +71,8 @@ public class EmailService {
|
|||||||
return String.valueOf(code);
|
return String.valueOf(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String loadAndProcessTemplate(String username, String code, String templatePath) throws IOException {
|
private String loadAndProcessTemplate(String username, String code) throws IOException {
|
||||||
ClassPathResource resource = new ClassPathResource(templatePath);
|
ClassPathResource resource = new ClassPathResource("templates/email-verification.html");
|
||||||
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||||
String safeUsername = escapeHtml(username != null ? username : user);
|
String safeUsername = escapeHtml(username != null ? username : user);
|
||||||
String safeCode = escapeHtml(code);
|
String safeCode = escapeHtml(code);
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ import ru.soune.nocopy.dto.register.LoginRequest;
|
|||||||
import ru.soune.nocopy.dto.register.RegRequest;
|
import ru.soune.nocopy.dto.register.RegRequest;
|
||||||
import ru.soune.nocopy.entity.user.AuthToken;
|
import ru.soune.nocopy.entity.user.AuthToken;
|
||||||
import ru.soune.nocopy.entity.user.User;
|
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.NotFoundAuthToken;
|
||||||
import ru.soune.nocopy.exception.NotValidFieldException;
|
import ru.soune.nocopy.exception.NotValidFieldException;
|
||||||
import ru.soune.nocopy.repository.AuthTokenRepository;
|
import ru.soune.nocopy.repository.AuthTokenRepository;
|
||||||
import ru.soune.nocopy.repository.UserNotActiveRepository;
|
|
||||||
import ru.soune.nocopy.repository.UserRepository;
|
import ru.soune.nocopy.repository.UserRepository;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@@ -32,7 +30,7 @@ public class AuthService {
|
|||||||
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
private final UserNotActiveRepository userNotActiveRepository;
|
private final MessageSource messageSource;
|
||||||
|
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
@@ -78,30 +76,14 @@ public class AuthService {
|
|||||||
return authToken.getUser().getId();
|
return authToken.getUser().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public AuthToken login(LoginRequest request) {
|
public AuthToken login(LoginRequest request) {
|
||||||
User user = userRepository.findByEmail(request.getEmail());
|
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())) {
|
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
|
||||||
LoginAnswer loginAnswer = new LoginAnswer();
|
LoginAnswer loginAnswer = new LoginAnswer();
|
||||||
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
|
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,
|
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
|
||||||
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
|
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
|
||||||
@@ -109,9 +91,6 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
user.setLastLoginAt(LocalDateTime.now());
|
user.setLastLoginAt(LocalDateTime.now());
|
||||||
user.setFailedLoginAttempts(0);
|
|
||||||
user.setActive(true);
|
|
||||||
|
|
||||||
User savedUser = userRepository.save(user);
|
User savedUser = userRepository.save(user);
|
||||||
|
|
||||||
AuthToken authToken = new AuthToken();
|
AuthToken authToken = new AuthToken();
|
||||||
@@ -127,7 +106,6 @@ public class AuthService {
|
|||||||
.ifPresent(authTokenRepository::delete);
|
.ifPresent(authTokenRepository::delete);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
|
||||||
public AuthToken generateAuthToken(User user) {
|
public AuthToken generateAuthToken(User user) {
|
||||||
return authTokenRepository.save(genereateAuthToken(user));
|
return authTokenRepository.save(genereateAuthToken(user));
|
||||||
}
|
}
|
||||||
@@ -145,19 +123,4 @@ public class AuthService {
|
|||||||
secureRandom.nextBytes(bytes);
|
secureRandom.nextBytes(bytes);
|
||||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,5 @@ app:
|
|||||||
email:
|
email:
|
||||||
verification:
|
verification:
|
||||||
standart-subject: NO COPY - Подтверждение email адреса
|
standart-subject: NO COPY - Подтверждение email адреса
|
||||||
drop-password-subject: NO COPY - сброс пароля
|
|
||||||
standart-address-from: NO COPY <noreply@nocopy.com>
|
standart-address-from: NO COPY <noreply@nocopy.com>
|
||||||
user: Пользователь
|
user: Пользователь
|
||||||
@@ -1,201 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Восстановление пароля</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 600px;
|
|
||||||
margin: 0 auto;
|
|
||||||
background: white;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
padding: 40px 30px;
|
|
||||||
}
|
|
||||||
.greeting {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
.username {
|
|
||||||
color: #ee5a52;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.code-container {
|
|
||||||
background: #f8f9fa;
|
|
||||||
border: 2px dashed #ff6b6b;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 25px;
|
|
||||||
text-align: center;
|
|
||||||
margin: 30px 0;
|
|
||||||
}
|
|
||||||
.verification-code {
|
|
||||||
font-size: 42px;
|
|
||||||
font-weight: 800;
|
|
||||||
letter-spacing: 8px;
|
|
||||||
color: #333;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.info-text {
|
|
||||||
color: #666;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
background: #fff3cd;
|
|
||||||
border: 1px solid #ffeaa7;
|
|
||||||
color: #856404;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 8px;
|
|
||||||
margin: 25px 0;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
.action-button {
|
|
||||||
display: inline-block;
|
|
||||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
|
|
||||||
color: white;
|
|
||||||
padding: 14px 32px;
|
|
||||||
text-decoration: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 20px 0;
|
|
||||||
text-align: center;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
}
|
|
||||||
.action-button:hover {
|
|
||||||
background: linear-gradient(135deg, #ee5a52 0%, #d64541 100%);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 12px rgba(238, 90, 82, 0.3);
|
|
||||||
}
|
|
||||||
.steps {
|
|
||||||
margin: 30px 0;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
.steps li {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
text-align: center;
|
|
||||||
padding: 25px;
|
|
||||||
color: #666;
|
|
||||||
font-size: 12px;
|
|
||||||
border-top: 1px solid #eee;
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
.highlight {
|
|
||||||
color: #ee5a52;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
.urgent {
|
|
||||||
color: #d63031;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
.link-fallback {
|
|
||||||
margin-top: 15px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
.timer {
|
|
||||||
background: #ffebee;
|
|
||||||
border-left: 4px solid #ff6b6b;
|
|
||||||
padding: 12px 15px;
|
|
||||||
margin: 20px 0;
|
|
||||||
border-radius: 0 8px 8px 0;
|
|
||||||
}
|
|
||||||
.timer-text {
|
|
||||||
color: #d32f2f;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<div class="header">
|
|
||||||
<div class="logo">NO COPY</div>
|
|
||||||
<h1>Восстановление пароля</h1>
|
|
||||||
<p>Безопасный доступ к вашему аккаунту</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="content">
|
|
||||||
<p class="greeting">
|
|
||||||
Здравствуйте, <span class="username">{{username}}</span>!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>Мы получили запрос на восстановление пароля для вашего аккаунта NO COPY.</p>
|
|
||||||
<p>Для продолжения восстановления доступа, пожалуйста, используйте следующий код подтверждения:</p>
|
|
||||||
|
|
||||||
<div class="code-container">
|
|
||||||
<p class="verification-code">{{code}}</p>
|
|
||||||
<p class="info-text">Код для восстановления пароля</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="timer">
|
|
||||||
<p class="timer-text">Код действителен в течение 15 минут</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3>Как использовать этот код:</h3>
|
|
||||||
<ol class="steps">
|
|
||||||
<li>Вернитесь на страницу восстановления пароля</li>
|
|
||||||
<li>Введите полученный код в поле подтверждения</li>
|
|
||||||
<li>Создайте новый надежный пароль</li>
|
|
||||||
<li>Войдите в аккаунт с новым паролем</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div style="text-align: center;">
|
|
||||||
<a href="{{resetLink}}" class="action-button">Перейти к восстановлению пароля</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="link-fallback">
|
|
||||||
Если кнопка не работает, скопируйте и вставьте эту ссылку в браузер:<br>
|
|
||||||
<span style="color: #3498db; word-break: break-all;">{{resetLink}}</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="warning">
|
|
||||||
<strong>Важная информация:</strong><br>
|
|
||||||
1. Если вы не запрашивали восстановление пароля, проигнорируйте это письмо.<br>
|
|
||||||
2. Никому не сообщайте этот код, включая сотрудников поддержки.<br>
|
|
||||||
3. После успешного восстановления старый пароль будет недействителен.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p style="font-size: 13px; color: #7f8c8d;">
|
|
||||||
<strong>Совет по безопасности:</strong> Используйте пароль длиной не менее 8 символов,
|
|
||||||
содержащий буквы (заглавные и строчные), цифры и специальные символы.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
<p>© 2024 NO COPY. Все права защищены.</p>
|
|
||||||
<p>Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.</p>
|
|
||||||
<p>Если у вас возникли вопросы или вы не запрашивали восстановление пароля,
|
|
||||||
<a href="mailto:support@nocopy.com" style="color: #ee5a52;">свяжитесь со службой поддержки</a>.</p>
|
|
||||||
<p style="font-size: 11px; margin-top: 10px;">
|
|
||||||
IP запроса: {{ipAddress}} • Время: {{timestamp}} • Устройство: {{deviceInfo}}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user