From f1e0f9594ed2661e812ddc84ecced64ce7ae7191 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 17 Mar 2026 16:35:27 +0700 Subject: [PATCH] dev add violation notion --- .../nocopy/configuration/HandlerConfig.java | 4 +- .../java/ru/soune/nocopy/dto/MessageCode.java | 4 +- .../dto/violation/DeleteNotionResponse.java | 12 + .../dto/violation/ViolationNotionRequest.java | 12 + .../violation/ViolationNotionResponse.java | 30 ++ .../ViolationNotionsListResponse.java | 15 + .../entity/violation/ViolationNotion.java | 47 ++++ .../handler/ViolationNotionHandler.java | 265 ++++++++++++++++++ .../repository/ViolationNotionRepository.java | 24 ++ .../violation/ViolationNotionService.java | 138 +++++++++ 10 files changed, 549 insertions(+), 2 deletions(-) create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java create mode 100644 src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java create mode 100644 src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java create mode 100644 src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java create mode 100644 src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 2b02251..4c3baff 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -31,7 +31,8 @@ public class HandlerConfig { MonitoringHandler monitoringHandler, ViolationHandler violationHandler, ViolationStatisticsHandler violationStatisticsHandler, - GlobalSearchHandler globalSearchHandler + GlobalSearchHandler globalSearchHandler, + ViolationNotionHandler violationNotionHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -54,6 +55,7 @@ public class HandlerConfig { map.put(30009, violationHandler); map.put(30010, violationStatisticsHandler); map.put(30011, globalSearchHandler); + map.put(30012, violationNotionHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 4384659..ec273ba 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -53,7 +53,9 @@ public enum MessageCode { FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + "more than max valid"), - USER_NOT_ACTIVE(2, "User not active"); + USER_NOT_ACTIVE(2, "User not active"), + NOTION_NOT_FOUND(4, "Notion not found"), + MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java new file mode 100644 index 0000000..c3c06fb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DeleteNotionResponse { + private Long notionId; + private String message; + private boolean deleted; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java new file mode 100644 index 0000000..fca54be --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Data; + +@Data +public class ViolationNotionRequest { + private String token; + private String fileId; + private String message; + private String action; + private Long notionId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java new file mode 100644 index 0000000..17da29a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java @@ -0,0 +1,30 @@ +package ru.soune.nocopy.dto.violation; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationNotionResponse { + private Long id; + private String message; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private UserInfo user; + private FileInfo file; + + @Data + @Builder + public static class UserInfo { + private Long id; + private String fullName; + private String email; + } + + @Data + @Builder + public static class FileInfo { + private String id; + private String originalFileName; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java new file mode 100644 index 0000000..884bb32 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ViolationNotionsListResponse { + private String fileId; + private String fileName; + private int totalCount; + private List notions; +} diff --git a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java new file mode 100644 index 0000000..aa93cbc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.entity.violation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Getter @Setter +@Table(name = "violation_notions") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class ViolationNotion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + @JsonIgnore + private FileEntity file; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "message") + private String message; +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java new file mode 100644 index 0000000..2c06a01 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java @@ -0,0 +1,265 @@ +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.violation.*; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.violation.ViolationNotionService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ViolationNotionHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationNotionService notionService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) { + try { + ViolationNotionRequest notionRequest = objectMapper.convertValue( + request.getMessageBody(), ViolationNotionRequest.class); + + if (notionRequest.getToken() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Token is required", + null); + } + + Optional tokenOptional = authTokenRepository.findByToken(notionRequest.getToken()); + if (tokenOptional.isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Invalid token", + null); + } + + Long userId = tokenOptional.get().getUser().getId(); + String action = notionRequest.getAction(); + + if (action == null || action.trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Action is required", + null); + } + + switch (action) { + case "get_notions": + return handleGetNotions(request, notionRequest, userId); + + case "get_notion": + return handleGetNotion(request, notionRequest, userId); + + case "add_notion": + return handleAddNotion(request, notionRequest, userId); + + case "update_notion": + return handleUpdateNotion(request, notionRequest, userId); + + case "delete_notion": + return handleDeleteNotion(request, notionRequest, userId); + + default: + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + null); + } + + } catch (IllegalArgumentException e) { + log.error("Validation error in ViolationNotionHandler: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + log.error("Security error in ViolationNotionHandler: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error in ViolationNotionHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error", + null); + } + } + + private BaseResponse handleGetNotions(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getFileId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "FileId is required for get_notions action", + null); + } + + try { + ViolationNotionsListResponse notions = notionService.getNotionsByFileId( + notionRequest.getFileId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notions) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleGetNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for get_notion action", + null); + } + + try { + ViolationNotionResponse notion = notionService.getNotionById( + notionRequest.getNotionId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notion) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleAddNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getFileId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + "FileId is required for add_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + "Message is required for add_notion action", + null); + } + + try { + ViolationNotionResponse created = notionService.createNotion( + notionRequest.getFileId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(created) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleUpdateNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for update_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.MESSAGE_IS_REQUIRED_FOR_NOTION.getCode(), + "Message is required for update_notion action", + null); + } + + try { + ViolationNotionResponse updated = notionService.updateNotion( + notionRequest.getNotionId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(updated) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleDeleteNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + "NotionId is required for delete_notion action", + null); + } + + try { + notionService.deleteNotion(notionRequest.getNotionId(), userId); + DeleteNotionResponse deleteResponse = DeleteNotionResponse.builder() + .notionId(notionRequest.getNotionId()) + .message("Notion deleted successfully") + .deleted(true) + .build(); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(deleteResponse) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + MessageCode.ACCESS_DENIED.getDescription()); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java new file mode 100644 index 0000000..129321d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java @@ -0,0 +1,24 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.violation.ViolationNotion; + +import java.util.List; + +@Repository +public interface ViolationNotionRepository extends JpaRepository { + + @Query("SELECT vn FROM ViolationNotion vn " + + "JOIN FETCH vn.user " + + "WHERE vn.file.id = :fileId " + + "ORDER BY vn.createdAt DESC") + List findByFileId(@Param("fileId") String fileId); + + @Query("SELECT COUNT(vn) FROM ViolationNotion vn WHERE vn.file.id = :fileId") + long countByFileId(@Param("fileId") String fileId); + + void deleteByFileId(String fileId); +} diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java new file mode 100644 index 0000000..e0e648b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -0,0 +1,138 @@ +package ru.soune.nocopy.service.violation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.violation.ViolationNotionResponse; +import ru.soune.nocopy.dto.violation.ViolationNotionsListResponse; +import ru.soune.nocopy.entity.company.Company; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.violation.ViolationNotion; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.ViolationNotionRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ViolationNotionService { + + private final ViolationNotionRepository notionRepository; + + private final UserRepository userRepository; + + private final FileEntityRepository fileRepository; + + @Transactional(readOnly = true) + public ViolationNotionsListResponse getNotionsByFileId(String fileId, Long userId) { + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); + checkPermission(userId, file); + + List notions = notionRepository.findByFileId(fileId); + long totalCount = notionRepository.countByFileId(fileId); + + List notionResponses = notions.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + return ViolationNotionsListResponse.builder() + .fileId(fileId) + .fileName(file.getOriginalFileName()) + .totalCount((int) totalCount) + .notions(notionResponses) + .build(); + } + + @Transactional(readOnly = true) + public ViolationNotionResponse getNotionById(Long notionId, Long userId) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + if (!notion.getFile().getUserId().equals(userId) && !notion.getUser().getId().equals(userId)) { + throw new SecurityException("User does not have access to this notion"); + } + + return mapToResponse(notion); + } + + @Transactional + public ViolationNotionResponse createNotion(String fileId, Long userId, String message) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); + + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new IllegalArgumentException("File not found: " + fileId)); + + ViolationNotion notion = new ViolationNotion(); + notion.setUser(user); + notion.setFile(file); + notion.setMessage(message); + + ViolationNotion saved = notionRepository.save(notion); + + return mapToResponse(saved); + } + + @Transactional + public void deleteNotion(Long notionId, Long userId) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + checkPermission(userId, notion.getFile()); + + notionRepository.delete(notion); + } + + @Transactional + public ViolationNotionResponse updateNotion(Long notionId, Long userId, String message) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + if (!notion.getUser().getId().equals(userId)) { + throw new SecurityException("Only author can update this notion"); + } + + notion.setMessage(message); + ViolationNotion updated = notionRepository.save(notion); + log.info("Updated notion with id: {} by user: {}", notionId, userId); + + return mapToResponse(updated); + } + + private ViolationNotionResponse mapToResponse(ViolationNotion notion) { + return ViolationNotionResponse.builder() + .id(notion.getId()) + .message(notion.getMessage()) + .createdAt(notion.getCreatedAt()) + .user(ViolationNotionResponse.UserInfo.builder() + .id(notion.getUser().getId()) + .fullName(notion.getUser().getFullName()) + .email(notion.getUser().getEmail()) + .build()) + .build(); + } + + private void checkPermission(long userId, FileEntity file) { + User user = userRepository.findById(userId).orElseThrow(() -> + new IllegalArgumentException("User not found: " + userId)); + Company company = user.getCompany(); + + if (company != null) { + List companyUsers = company.getUsers() + .stream() + .map(User::getId) + .toList(); + if (companyUsers.contains(userId)) { + throw new SecurityException("User does not have access to this file"); + } + } else if (!file.getUserId().equals(userId)) { + throw new SecurityException("User does not have access to this file"); + } + } +}