From 40f8effb319263069fb8a2b43f25719e1425643a Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 24 Mar 2026 11:37:25 +0700 Subject: [PATCH] dev add additional info user --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../soune/nocopy/dto/register/RegRequest.java | 6 + .../dto/user/UserAdditionalInfoBody.java | 20 ++ .../entity/user/UserAdditionalInfo.java | 34 +++ .../entity/user/UserAdditionalInfoAnswer.java | 31 +++ .../user/UserAdditionalInfoListAnswer.java | 22 ++ ...gistrationAdditionalInfoNullException.java | 7 + .../exception/UserAdditionalInfoNotFound.java | 7 + .../nocopy/handler/RegRequestHandler.java | 6 +- .../soune/nocopy/handler/UserInfoHandler.java | 214 ++++++++++++++++++ .../validator/RegRequestValidator.java | 17 ++ .../UserAdditionalInfoRepository.java | 9 + .../service/user/AdditionalInfoService.java | 54 +++++ 13 files changed, 429 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java create mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java create mode 100644 src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java create mode 100644 src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java create mode 100644 src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 1309a1e..1bdd94a 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -33,7 +33,8 @@ public class HandlerConfig { ViolationStatisticsHandler violationStatisticsHandler, GlobalSearchHandler globalSearchHandler, ViolationNotionHandler violationNotionHandler, - ComplaintEntityHandler complaintEntityHandler + ComplaintEntityHandler complaintEntityHandler, + UserInfoHandler userInfoHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -58,6 +59,7 @@ public class HandlerConfig { map.put(30011, globalSearchHandler); map.put(30012, violationNotionHandler); map.put(30013, complaintEntityHandler); + map.put(30014, userInfoHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index ef19795..70f7d2c 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -37,4 +37,10 @@ public class RegRequest { @JsonProperty("mail_verified") private String mailVerified; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("user_agent") + private String userAgent; } diff --git a/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java new file mode 100644 index 0000000..12fa859 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserAdditionalInfoBody { + + @JsonProperty("action") + private String action; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java new file mode 100644 index 0000000..ea1c040 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.sql.Timestamp; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +@Builder +@Table(name = "user_additional_info") +public class UserAdditionalInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, name = "register_date") + @CreationTimestamp + private Timestamp registrationDate; + + @Column(nullable = false, name = "ip") + private String ipAddress; + + @Column(nullable = false, name = "user_agent") + private String userAgent; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", unique = true, nullable = false) + private User user; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java new file mode 100644 index 0000000..3adf2e5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoAnswer { + + @JsonProperty("id") + private Long id; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; + + @JsonProperty("registration_date") + private Timestamp registrationDate; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java new file mode 100644 index 0000000..89917a9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoListAnswer { + + @JsonProperty("items") + private List items; + + @JsonProperty("count") + private int count; +} diff --git a/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java new file mode 100644 index 0000000..61f5cda --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class RegistrationAdditionalInfoNullException extends RuntimeException { + public RegistrationAdditionalInfoNullException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java new file mode 100644 index 0000000..ce16c63 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserAdditionalInfoNotFound extends RuntimeException { + public UserAdditionalInfoNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 6e60a96..77efad5 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -21,6 +21,7 @@ import ru.soune.nocopy.repository.ReferralJpaRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.user.AdditionalInfoService; import java.util.Arrays; import java.util.Map; @@ -43,6 +44,8 @@ public class RegRequestHandler implements RequestHandler { private final ReferralJpaRepository referralJpaRepository; + private final AdditionalInfoService additionalInfoService; + @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); @@ -124,9 +127,10 @@ public class RegRequestHandler implements RequestHandler { referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); } + additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent()); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); - } catch (CompanyAlreadyExist e) { return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(), MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of( diff --git a/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java new file mode 100644 index 0000000..07edf10 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java @@ -0,0 +1,214 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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.user.UserAdditionalInfoBody; + +import ru.soune.nocopy.entity.user.UserAdditionalInfo; +import ru.soune.nocopy.entity.user.UserAdditionalInfoAnswer; +import ru.soune.nocopy.entity.user.UserAdditionalInfoListAnswer; +import ru.soune.nocopy.exception.UserAdditionalInfoNotFound; +import ru.soune.nocopy.service.user.AdditionalInfoService; + + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@Slf4j +@RequiredArgsConstructor +public class UserInfoHandler implements RequestHandler { + + private final AdditionalInfoService additionalInfoService; + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + try { + UserAdditionalInfoBody body = objectMapper.convertValue(request.getMessageBody(), + UserAdditionalInfoBody.class); + + log.info("Processing UserInfoHandler with action: {}", body.getAction()); + + return switch (body.getAction()) { + case "getAll" -> getAllAdditionalInfos(request); + case "getById" -> getAdditionalInfoById(request, body.getUserId()); + case "update" -> updateAdditionalInfo(request, body); + case "delete" -> deleteAdditionalInfo(request, body.getUserId()); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + body.getAction(), + null + ); + }; + } catch (IllegalArgumentException e) { + log.error("Validation error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (UserAdditionalInfoNotFound e) { + log.error("User additional info not found", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error: " + e.getMessage(), + null); + } + } + + private BaseResponse getAllAdditionalInfos(BaseRequest request) { + log.info("Getting all additional infos"); + + List infos = additionalInfoService.getAllAdditionalInfos(); + + List answerItems = infos.stream() + .map(this::convertToAnswer) + .collect(Collectors.toList()); + + UserAdditionalInfoListAnswer listAnswer = UserAdditionalInfoListAnswer.builder() + .items(answerItems) + .count(answerItems.size()) + .build(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + listAnswer + ); + } + + private BaseResponse getAdditionalInfoById(BaseRequest request, Long userId) { + log.info("Getting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for getById action"); + } + + UserAdditionalInfo info = additionalInfoService.getAdditionalInfoByUser(userId); + + if (info == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(info); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse updateAdditionalInfo(BaseRequest request, UserAdditionalInfoBody body) { + log.info("Updating additional info for user: {}", body.getUserId()); + + if (body.getUserId() == null) { + throw new IllegalArgumentException("User ID is required for update action"); + } + + validateIpAddress(body.getIpAddress()); + validateUserAgent(body.getUserAgent()); + + Optional updatedInfo = additionalInfoService.updateAdditionalInfo( + body.getUserId(), + body.getIpAddress(), + body.getUserAgent() + ); + + if (updatedInfo.isEmpty()) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", body.getUserId()) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(updatedInfo.get()); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse deleteAdditionalInfo(BaseRequest request, Long userId) { + log.info("Deleting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for delete action"); + } + + UserAdditionalInfo existingInfo = additionalInfoService.getAdditionalInfoByUser(userId); + + if (existingInfo == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + additionalInfoService.deleteAdditionalInfo(userId); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("deleted", true, "user_id", userId) + ); + } + + private UserAdditionalInfoAnswer convertToAnswer(UserAdditionalInfo info) { + return UserAdditionalInfoAnswer.builder() + .id(info.getId()) + .userId(info.getUser() != null ? info.getUser().getId() : null) + .ipAddress(info.getIpAddress()) + .userAgent(info.getUserAgent()) + .registrationDate(info.getRegistrationDate()) + .build(); + } + + private void validateIpAddress(String ip) { + if (ip == null || ip.trim().isEmpty()) { + throw new IllegalArgumentException("IP address is required for update action"); + } + + String ipv4Pattern = + "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + if (!ip.trim().matches(ipv4Pattern)) { + log.warn("Invalid IP address format: {}", ip); + throw new IllegalArgumentException("Invalid IP address format"); + } + } + + private void validateUserAgent(String userAgent) { + if (userAgent == null || userAgent.trim().isEmpty()) { + throw new IllegalArgumentException("User agent is required for update action"); + } + + if (userAgent.length() > 500) { + throw new IllegalArgumentException("User agent must not exceed 500 characters"); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java index b57c7fb..84b708a 100644 --- a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java +++ b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.handler.validator; import org.apache.commons.validator.routines.EmailValidator; +import org.apache.commons.validator.routines.InetAddressValidator; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -28,6 +29,7 @@ public class RegRequestValidator implements Validator { validateEmail(request.getEmail(), errors); validateCompanyName(request.getCompanyName(), errors); validateFullName(request.getFullName(), errors); + validateAdditionalInfo(request.getIp(), request.getUserAgent(), errors); } private void validateFullName(String fullName, Errors errors) { @@ -73,6 +75,21 @@ public class RegRequestValidator implements Validator { } } + public void validateAdditionalInfo(String ip, String userAgent, Errors errors) { + if (ip == null || ip.trim().isEmpty()) { + errors.rejectValue("ip", "ip.required", "IP address is required"); + } else if (!InetAddressValidator.getInstance().isValid(ip.trim())) { + errors.rejectValue("ip", "ip.invalid", "Invalid IP address format"); + } + + if (userAgent == null || userAgent.trim().isEmpty()) { + errors.rejectValue("userAgent", "userAgent.required", "User agent is required"); + } else if (userAgent.length() > 500) { + errors.rejectValue("userAgent", "userAgent.tooLong", "User agent must not exceed " + + "500 characters"); + } + } + public void validateEmail(String email, Errors errors) { if (email == null || email.trim().isEmpty()) { errors.rejectValue("email", "email.is.empty", "Email must not be empty"); diff --git a/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java new file mode 100644 index 0000000..b45d0ca --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.user.UserAdditionalInfo; + +@Repository +public interface UserAdditionalInfoRepository extends JpaRepository { +} diff --git a/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java new file mode 100644 index 0000000..f0016cc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.service.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.user.UserAdditionalInfo; +import ru.soune.nocopy.exception.RegistrationAdditionalInfoNullException; +import ru.soune.nocopy.repository.UserAdditionalInfoRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AdditionalInfoService { + + private final UserAdditionalInfoRepository additionalInfoRepository; + + @Transactional + public void additionalInfo(String ip, String userAgent) { + if (ip == null || userAgent == null) { + throw new RegistrationAdditionalInfoNullException("Ip address or user agent is null"); + } + + UserAdditionalInfo userAdditionalInfo = UserAdditionalInfo.builder() + .ipAddress(ip) + .userAgent(userAgent) + .build(); + + additionalInfoRepository.save(userAdditionalInfo); + } + + public UserAdditionalInfo getAdditionalInfoByUser(Long userId) { + return additionalInfoRepository.findById(userId).orElse(null); + } + + public List getAllAdditionalInfos() { + return additionalInfoRepository.findAll(); + } + + public void deleteAdditionalInfo(Long userId) { + additionalInfoRepository.deleteById(userId); + } + + @Transactional + public Optional updateAdditionalInfo(Long userId, String ip, String userAgent) { + return additionalInfoRepository.findById(userId) + .map(info -> { + info.setIpAddress(ip); + info.setUserAgent(userAgent); + return additionalInfoRepository.save(info); + }); + } +}