Merge branch 'dev' into NCBACK-35

# Conflicts:
#	README.md
#	src/main/java/ru/soune/nocopy/controller/ApiController.java
#	src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java
#	src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java
#	src/main/java/ru/soune/nocopy/service/file/FileEntityService.java
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
#	src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java
This commit is contained in:
vladp
2026-03-31 14:24:03 +07:00
229 changed files with 10416 additions and 757 deletions
@@ -0,0 +1,139 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
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.complaint.ComplaintRequest;
import ru.soune.nocopy.dto.complaint.ComplaintResponse;
import ru.soune.nocopy.entity.complaint.ComplaintStatus;
import ru.soune.nocopy.service.complaint.ComplaintEntityService;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class ComplaintEntityHandler implements RequestHandler {
private final ComplaintEntityService complaintService;
private final ObjectMapper objectMapper;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
Integer msgId = request.getMsgId();
ComplaintRequest complaintRequest = objectMapper.convertValue(request.getMessageBody(), ComplaintRequest.class);
String action = complaintRequest.getAction();
try {
switch (action.toLowerCase()) {
case "create":
return handleCreate(msgId, complaintRequest);
case "get":
return handleGet(msgId, complaintRequest);
case "get_all":
return handleGetAll(msgId, complaintRequest);
case "get_by_violation":
return handleGetByViolation(msgId, complaintRequest);
case "update_status":
return handleUpdateStatus(msgId, complaintRequest);
case "update":
return handleUpdate(msgId, complaintRequest);
case "delete":
return handleDelete(msgId, complaintRequest);
default:
return errorResponse(msgId, MessageCode.MSG_ID_NOT_FOUND.getCode(), "Unknown action: " + action);
}
} catch (Exception e) {
log.error("Error handling complaint request", e);
return errorResponse(msgId, MessageCode.INVALID_JSON_BODY.getCode(), e.getMessage());
}
}
private BaseResponse handleCreate(Integer msgId, ComplaintRequest req) {
if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(),
"violation_id is required");
if (req.getComplaintText() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(),
"text is required");
ComplaintResponse response = complaintService.createComplaint(req);
return successResponse(msgId, "Complaint created successfully", response);
}
private BaseResponse handleGet(Integer msgId, ComplaintRequest req) {
if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required");
ComplaintResponse response = complaintService.getComplaintById(req.getId());
return successResponse(msgId, "Complaint retrieved successfully", response);
}
private BaseResponse handleGetAll(Integer msgId, ComplaintRequest req) {
Pageable pageable = PageRequest.of(
req.getPage() != null ? req.getPage() : 0,
req.getSize() != null ? req.getSize() : 20,
Sort.by(Sort.Direction.fromString(req.getSortDirection() != null ? req.getSortDirection() : "desc"),
req.getSortBy() != null ? req.getSortBy() : "createdAt")
);
var page = complaintService.getAllComplaints(pageable);
Map<String, Object> data = new HashMap<>();
data.put("content", page.getContent());
data.put("page", page.getNumber());
data.put("size", page.getSize());
data.put("totalElements", page.getTotalElements());
data.put("totalPages", page.getTotalPages());
return successResponse(msgId, "Complaints retrieved successfully", data);
}
private BaseResponse handleGetByViolation(Integer msgId, ComplaintRequest req) {
if (req.getViolationId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(),
"violation_id is required");
var complaints = complaintService.getComplaintsByViolationId(req.getViolationId());
Map<String, Object> data = new HashMap<>();
data.put("content", complaints);
data.put("totalElements", complaints.size());
return successResponse(msgId, "Complaints retrieved successfully", data);
}
private BaseResponse handleUpdateStatus(Integer msgId, ComplaintRequest req) {
if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required");
if (req.getStatus() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "status is required");
ComplaintStatus status = ComplaintStatus.valueOf(req.getStatus().toUpperCase());
ComplaintResponse response = complaintService.updateComplaintStatus(req.getId(), status);
return successResponse(msgId, "Status updated successfully", response);
}
private BaseResponse handleUpdate(Integer msgId, ComplaintRequest req) {
if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required");
ComplaintResponse response = complaintService.updateComplaint(req.getId(), req);
return successResponse(msgId, "Complaint updated successfully", response);
}
private BaseResponse handleDelete(Integer msgId, ComplaintRequest req) {
if (req.getId() == null) return errorResponse(msgId, MessageCode.NOT_FOUND.getCode(), "id is required");
ComplaintResponse response = complaintService.deleteComplaint(req.getId());
return successResponse(msgId, "Complaint deleted successfully", response);
}
private BaseResponse successResponse(Integer msgId, String message, Object data) {
return new BaseResponse(msgId, MessageCode.SUCCESS.getCode(), message, data);
}
private BaseResponse errorResponse(Integer msgId, Integer code, String message) {
return new BaseResponse(msgId, code, message, new HashMap<>());
}
}
@@ -0,0 +1,107 @@
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.cost.CostRequest;
import ru.soune.nocopy.dto.cost.CostResponse;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.cost.CostService;
import java.util.Optional;
@Slf4j
@Component
@RequiredArgsConstructor
public class CostHandler implements RequestHandler {
private final CostService costService;
private final AuthTokenRepository authTokenRepository;
private final ObjectMapper objectMapper;
private final Integer MAX_FILES_FOR_CHECK = 1000;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
CostRequest costRequest = objectMapper.convertValue(request.getMessageBody(), CostRequest.class);
String action = costRequest.getAction();
return switch (action) {
case "costFileProtect" -> handleFileProtectCost(costRequest, request);
case "checkTokensForProtect" -> handleCheckTokensForFilesProtect(costRequest, request);
default -> new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action,
null);
};
}
private BaseResponse handleFileProtectCost(CostRequest costRequest, BaseRequest request) {
int cost = costService.protectFileCost(costRequest.getFileType());
if (cost == 0) {
return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(),
MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(),
CostResponse.builder()
.cost(cost)
.build());
}
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
CostResponse.builder()
.cost(cost)
.build());
}
private BaseResponse handleCheckTokensForFilesProtect(CostRequest costRequest, BaseRequest request) {
String authToken = costRequest.getAuthToken();
Optional<AuthToken> token = authTokenRepository.findByToken(authToken);
if (token.isEmpty()) {
return new BaseResponse(
request.getMsgId(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
null);
}
User user = token.get().getUser();
Integer tokens = user.getActiveTariffInfo().getTokens();
int cost = costService.protectFileCost(costRequest.getFileType());
int count = costRequest.getCountFilesForProtect();
if (cost == 0 || count <= 0 || count > MAX_FILES_FOR_CHECK) {
return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(),
MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(),
CostResponse.builder()
.cost(cost)
.countFile(count)
.maxFilesForCheck(MAX_FILES_FOR_CHECK)
.build());
}
int totalCost = count * cost;
boolean success = totalCost <= tokens;
int needToken = success ? 0: tokens - totalCost;
CostResponse costResponse = CostResponse.builder()
.cost(totalCost)
.success(totalCost <= tokens)
.needTokensCount(Math.abs(needToken))
.build();
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), costResponse);
}
}
@@ -3,23 +3,28 @@ package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.file.FileAppeal;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.moderation.ModerationService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.time.LocalDate;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Component
@@ -38,6 +43,12 @@ public class FileEntityHandler implements RequestHandler {
private final CloudStorageService cloudStorageService;
private final ModerationService moderationService;
private final FileAppealRepository fileAppealRepository;
private final ModerationLogRepository moderationLogRepository;
@Override
public BaseResponse handle(BaseRequest request) {
try {
@@ -63,6 +74,18 @@ public class FileEntityHandler implements RequestHandler {
return handleProtectFile(request, fileRequest);
case "delete_file":
return handleDeleteFile(request, fileRequest);
case "change_file_status":
return handleChangeStatus(request, fileRequest);
case "moderate_file":
return handleModerateFile(request, fileRequest);
case "submit_appeal":
return handleSubmitAppeal(request, fileRequest);
case "review_appeal":
return handleReviewAppeal(request, fileRequest);
case "user_appeals":
return handleGetUserAppeals(request, fileRequest);
case "files_for_moderation":
return handleGetFilesForModeration(request, fileRequest);
default:
ActionResponse response = ActionResponse.builder()
.action(action)
@@ -186,53 +209,306 @@ public class FileEntityHandler implements RequestHandler {
}
}
private BaseResponse handleChangeStatus(BaseRequest request, FileEntityRequest fileRequest) {
FileStatus newStatus = fileRequest.getFileStatus();
String fileId = fileRequest.getFileId();
try {
fileEntityService.changeFileStatus(newStatus ,fileRequest.getFileId());
} catch (FileEntityNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_NOT_FOUND.getCode(),
MessageCode.FILE_NOT_FOUND.getDescription(),
FileEntityResponse.builder().id(fileId).build());
}
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
FileEntityResponse.builder().status(newStatus).build());
}
private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = authService.useUserAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) {
return handleGetUserFiles(request, fileRequest);
}
String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName";
SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder());
FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion());
var filteredFiles = allFiles.getFiles().stream()
.filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase()
.contains(fileRequest.getQuery().toLowerCase()))
.toList();
String searchQuery = fileRequest.getQuery() != null ? fileRequest.getQuery().toLowerCase().trim() : "";
String[] searchTerms = searchQuery.split("\\s+");
String filterType = fileRequest.getType();
String dateFilter = fileRequest.getDateFilter();
List<FileEntityResponse> filteredFiles = allFiles.getFiles().stream()
.filter(f -> matchesSearch(f, searchTerms, searchQuery, filterType))
.filter(f -> matchesDateFilter(f, dateFilter))
.collect(Collectors.toList());
Comparator<FileEntityResponse> comparator = getComparator(sortBy, sortOrder);
filteredFiles.sort(comparator);
int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, filteredFiles.size());
List<FileEntityResponse> fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size()));
long sumSize = fileEntityResponses.stream()
if (start >= filteredFiles.size()) {
return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue());
}
List<FileEntityResponse> paginatedFiles = filteredFiles.subList(start, end);
long totalSize = paginatedFiles.stream()
.mapToLong(FileEntityResponse::getFileSize)
.sum();
FileListResponse response = FileListResponse.builder()
.files(fileEntityResponses)
.files(paginatedFiles)
.totalCount(filteredFiles.size())
.totalSize(sumSize)
.formattedTotalSize(fileEntityService.formatFileSize(sumSize))
.totalSize(totalSize)
.formattedTotalSize(fileEntityService.formatFileSize(totalSize))
.page(page)
.pageSize(pageSize)
.sortBy(sortBy)
.sortOrder(sortOrder.getValue())
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required", null);
} catch (Exception e) {
log.error("Error searching files", e);
return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(),
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to search files: " + e.getMessage(), null);
}
}
private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) {
if (dateFilter == null || dateFilter.isEmpty()) {
return true;
}
LocalDate fileDate = file.getCreatedAt().toLocalDate();
LocalDate now = LocalDate.now();
switch (dateFilter.toLowerCase()) {
case "today":
return fileDate.equals(now);
case "week":
return fileDate.isAfter(now.minusWeeks(1)) && !fileDate.isAfter(now);
case "month":
return fileDate.isAfter(now.minusMonths(1)) && !fileDate.isAfter(now);
default:
return true;
}
}
private boolean matchesSearch(FileEntityResponse file, String[] searchTerms, String fullQuery, String type) {
String fileName = getSafeString(file.getFileName());
String originalFileName = getSafeString(file.getOriginalFileName());
String mimeType = getSafeString(file.getMimeType());
String fileFormat = getSafeString(file.getFileFormat());
String fullFileName = fileName + " " + originalFileName;
boolean matchesType = true;
if (type != null && !type.isEmpty()) {
matchesType = matchesMimeType(mimeType, type);
}
if (!matchesType) {
return false;
}
if (fullQuery != null && !fullQuery.isEmpty()) {
if (fullFileName.toLowerCase().contains(fullQuery)) {
return true;
}
for (String term : searchTerms) {
if (term.length() < 2) continue;
if (fullFileName.toLowerCase().contains(term)) {
return true;
}
}
if (fileFormat.toLowerCase().contains(fullQuery)) {
return true;
}
if (mimeType.toLowerCase().contains(fullQuery)) {
return true;
}
if (!fileFormat.isEmpty() && !fileName.toLowerCase().endsWith("." + fileFormat.toLowerCase())) {
String fileNameWithExtension = fileName + "." + fileFormat;
if (fileNameWithExtension.toLowerCase().contains(fullQuery)) {
return true;
}
}
return false;
}
return true;
}
private boolean matchesMimeType(String mimeType, String filterType) {
if (filterType == null || filterType.isEmpty()) return true;
String mimeTypeLower = mimeType.toLowerCase();
String filterTypeLower = filterType.toLowerCase().trim();
return mimeTypeLower.equals(filterTypeLower);
}
private String getSafeString(String value) {
return value != null ? value : "";
}
private Comparator<FileEntityResponse> getComparator(String sortBy, SortOrder sortOrder) {
Comparator<FileEntityResponse> comparator;
switch (sortBy) {
case "fileSize":
comparator = Comparator.comparingLong(
FileEntityResponse::getFileSize
);
break;
case "fileType":
comparator = Comparator.comparing(
FileEntityResponse::getMimeType);
break;
case "createdAt":
comparator = Comparator.comparing(
FileEntityResponse::getCreatedAt,
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
case "updatedAt":
comparator = Comparator.comparing(
FileEntityResponse::getUpdatedAt,
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
case "fileName":
comparator = Comparator.comparing(
f -> getSafeString(f.getFileName()).toLowerCase(),
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
case "protectStatus":
comparator = Comparator.comparing(
f -> getSafeString(f.getProtectStatus()).toLowerCase(),
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
case "supportId":
comparator = Comparator.comparing(
FileEntityResponse::getSupportId,
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
default:
comparator = Comparator.comparing(
f -> getSafeString(f.getFileName()).toLowerCase(),
Comparator.nullsLast(Comparator.naturalOrder())
);
break;
}
return sortOrder == SortOrder.DESC ? comparator.reversed() : comparator;
}
private BaseResponse createEmptyResponse(BaseRequest request, int page, int pageSize,
String sortBy, String sortOrder) {
FileListResponse emptyResponse = FileListResponse.builder()
.files(Collections.emptyList())
.totalCount(0)
.totalSize(0L)
.formattedTotalSize("0 B")
.page(page)
.pageSize(pageSize)
.sortBy(sortBy)
.sortOrder(sortOrder)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
emptyResponse);
}
private BaseResponse handleGetUserFilesWithSort(BaseRequest request, FileEntityRequest fileRequest,
String sortBy, SortOrder sortOrder) {
try {
Long userId = authService.useUserAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion());
List<FileEntityResponse> files = new ArrayList<>(allFiles.getFiles());
Comparator<FileEntityResponse> comparator = getComparator(sortBy, sortOrder);
files.sort(comparator);
int start = (page - 1) * pageSize;
int end = Math.min(start + pageSize, files.size());
if (start >= files.size()) {
return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue());
}
List<FileEntityResponse> paginatedFiles = files.subList(start, end);
long totalSize = paginatedFiles.stream()
.mapToLong(FileEntityResponse::getFileSize)
.sum();
FileListResponse response = FileListResponse.builder()
.files(paginatedFiles)
.totalCount(files.size())
.totalSize(totalSize)
.formattedTotalSize(fileEntityService.formatFileSize(totalSize))
.page(page)
.pageSize(pageSize)
.sortBy(sortBy)
.sortOrder(sortOrder.getValue())
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (NotFoundAuthToken e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required", null);
} catch (Exception e) {
log.error("Error getting user files with sort", e);
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_UPLOAD_ERROR.getCode(),
"Failed to get files: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = authService.useUserAuthToken(fileRequest.getToken());
@@ -337,6 +613,326 @@ public class FileEntityHandler implements RequestHandler {
}
}
private BaseResponse handleModerateFile(BaseRequest request, FileEntityRequest fileRequest) {
try {
//TODO добавить проверку на админа
// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken());
ModerationActionRequest moderationRequest = ModerationActionRequest.builder()
.fileId(fileRequest.getFileId())
.newStatus(fileRequest.getFileStatus())
.comment(fileRequest.getComment())
.build();
//TODO set moderator ID
moderationService.moderateFile(moderationRequest, 0L);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS_MODERATION.getCode(),
MessageCode.SUCCESS_MODERATION.getDescription(),
FileEntityResponse.builder().id(fileRequest.getFileId()).build());
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
MessageCode.ACCESS_DENIED.getDescription(),
FileEntityResponse.builder().id(fileRequest.getFileId()).build());
} catch (FileEntityNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_NOT_FOUND.getCode(),
MessageCode.FILE_NOT_FOUND.getDescription(),
FileEntityResponse.builder()
.id(fileRequest.getFileId()).build());
} catch (IllegalStateException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ILLEGAL_STATE.getCode(),
MessageCode.ILLEGAL_STATE.getDescription(),
FileEntityResponse.builder()
.status(fileRequest.getFileStatus()).build());
}
}
private BaseResponse handleSubmitAppeal(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = authService.useUserAuthToken(fileRequest.getToken());
AppealRequest appealRequest = AppealRequest.builder()
.fileId(fileRequest.getFileId())
.appealReason(fileRequest.getAppealReason())
.additionalInfo(fileRequest.getAdditionalInfo())
.build();
AppealResponse response = moderationService.submitAppeal(appealRequest, userId);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Appeal submitted successfully",
response);
} catch (InvalidAppealException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to submit appeal: " + e.getMessage(),
null);
}
}
private BaseResponse handleReviewAppeal(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken());
String appealId = fileRequest.getAppealId();
Boolean approve = fileRequest.getApprove();
String comment = fileRequest.getComment();
if (appealId == null || approve == null) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"appealId and approve are required",
null);
}
//TODO
moderationService.reviewAppeal(appealId, approve, comment, 0L);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Appeal reviewed successfully",
Map.of(
"appealId", appealId,
"approved", approve,
"comment", comment
));
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Admin access required",
null);
} catch (IllegalStateException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Error reviewing appeal", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to review appeal: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetUserAppeals(BaseRequest request, FileEntityRequest fileRequest) {
try {
Long userId = authService.useUserAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
Page<AppealResponse> appeals = moderationService.getUserAppeals(userId, page - 1, pageSize);
Map<String, Object> response = Map.of(
"appeals", appeals.getContent(),
"totalCount", appeals.getTotalElements(),
"totalPages", appeals.getTotalPages(),
"currentPage", page,
"pageSize", pageSize);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_TOKEN.getCode(),
"Authentication required",
null);
} catch (Exception e) {
log.error("Error getting user appeals", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to get appeals: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetFilesForModeration(BaseRequest request, FileEntityRequest fileRequest) {
try {
//TODO
// authService.useAdminAuthToken(fileRequest.getToken());
int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1;
int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20;
Page<FileEntity> files = moderationService.getFilesForModeration(page - 1, pageSize);
List<Map<String, Object>> filesWithInfo = files.getContent().stream()
.map(file -> {
FileModerationInfo info = moderationService.getFileModerationInfo(file.getId());
Map<String, Object> fileMap = new HashMap<>();
fileMap.put("fileId", file.getId());
fileMap.put("fileName", file.getOriginalFileName());
fileMap.put("userId", file.getUserId());
fileMap.put("status", file.getStatus() != null ? file.getStatus().name() : null);
Map<String, Object> moderationInfoMap = new HashMap<>();
moderationInfoMap.put("hasActiveAppeal", info != null && info.getHasActiveAppeal());
if (info != null && info.getActiveAppeal() != null) {
Map<String, Object> appealInfoMap = new HashMap<>();
appealInfoMap.put("appealId", info.getActiveAppeal().getAppealId());
appealInfoMap.put("status", info.getActiveAppeal().getStatus());
appealInfoMap.put("createdAt", info.getActiveAppeal().getCreatedAt());
moderationInfoMap.put("appealInfo", appealInfoMap);
} else {
moderationInfoMap.put("appealInfo", null);
}
fileMap.put("moderationInfo", moderationInfoMap);
return fileMap;
})
.collect(Collectors.toList());
Map<String, Object> response = Map.of(
"files", filesWithInfo,
"totalCount", files.getTotalElements(),
"totalPages", files.getTotalPages(),
"currentPage", page,
"pageSize", pageSize
);
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Admin access required",
null);
} catch (Exception e) {
log.error("Error getting files for moderation", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to get files: " + e.getMessage(),
null);
}
}
private BaseResponse handleGetFileModerationInfo(BaseRequest request, FileEntityRequest fileRequest) {
try {
// TODO
// authService.useAdminAuthToken(fileRequest.getToken());
String fileId = fileRequest.getFileId();
if (fileId == null) {
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"fileId is required",
null);
}
FileModerationInfo moderationInfo = moderationService.getFileModerationInfo(fileId);
FileEntity file = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
Map<String, Object> fileInfoMap = new HashMap<>();
fileInfoMap.put("id", file.getId());
fileInfoMap.put("fileName", file.getOriginalFileName());
fileInfoMap.put("storedFileName", file.getStoredFileName());
fileInfoMap.put("fileSize", file.getFileSize());
fileInfoMap.put("formattedSize", fileEntityService.formatFileSize(file.getFileSize()));
fileInfoMap.put("mimeType", file.getMimeType());
fileInfoMap.put("fileExtension", file.getFileExtension());
fileInfoMap.put("checksum", file.getChecksum());
fileInfoMap.put("status", file.getStatus() != null ? file.getStatus().name() : null);
fileInfoMap.put("createdAt", file.getCreatedAt());
fileInfoMap.put("updatedAt", file.getUpdatedAt());
Map<String, Object> response = new HashMap<>();
response.put("fileInfo", fileInfoMap);
response.put("moderationInfo", moderationInfo != null ? moderationInfo : new HashMap<>());
response.put("moderationHistory", getModerationHistory(fileId));
response.put("appeals", getFileAppeals(fileId));
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
} catch (SecurityException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
"Admin access required",
null);
} catch (FileEntityNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_NOT_FOUND.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Error getting file moderation info", e);
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Failed to get file info: " + e.getMessage(),
null);
}
}
private List<Map<String, Object>> getModerationHistory(String fileId) {
List<ModerationLog> logs = moderationLogRepository.findByFileIdOrderByCreatedAtDesc(fileId);
if (logs == null || logs.isEmpty()) {
return Collections.emptyList();
}
return logs.stream()
.map(log -> {
Map<String, Object> logMap = new HashMap<>();
logMap.put("id", log.getId());
logMap.put("moderatorId", log.getModeratorId());
logMap.put("oldStatus", log.getOldStatus() != null ? log.getOldStatus().name() : null);
logMap.put("newStatus", log.getNewStatus() != null ? log.getNewStatus().name() : null);
logMap.put("reason", log.getReason());
logMap.put("comment", log.getComment());
logMap.put("createdAt", log.getCreatedAt());
return logMap;
})
.collect(Collectors.toList());
}
private List<Map<String, Object>> getFileAppeals(String fileId) {
List<FileAppeal> appeals = fileAppealRepository.findByFileId(fileId);
if (appeals == null || appeals.isEmpty()) {
return Collections.emptyList();
}
return appeals.stream()
.map(appeal -> {
Map<String, Object> appealMap = new HashMap<>();
appealMap.put("id", appeal.getId());
appealMap.put("userId", appeal.getUserId());
appealMap.put("appealReason", appeal.getAppealReason());
appealMap.put("additionalInfo", appeal.getAdditionalInfo());
appealMap.put("status", appeal.getStatus() != null ? appeal.getStatus().name() : null);
appealMap.put("adminComment", appeal.getAdminComment());
appealMap.put("moderatorId", appeal.getModeratorId());
appealMap.put("createdAt", appeal.getCreatedAt());
appealMap.put("resolvedAt", appeal.getResolvedAt());
return appealMap;
})
.collect(Collectors.toList());
}
private String formatFileSize(long size) {
if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024));
@@ -111,6 +111,7 @@ public class FileUploadHandler implements RequestHandler {
fileRequest.getFileName(),
fileRequest.getFileType(),
fileRequest.getExtension(),
fileRequest.getConvertTo(),
fileRequest.getFileSize());
InitFileResponse fileResponse = InitFileResponse.builder()
@@ -0,0 +1,54 @@
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.search.GlobalSearchStatisticsRequest;
import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.search.GlobalSearchStatisticsService;
import java.util.Optional;
@Slf4j
@Component
@RequiredArgsConstructor
public class GlobalSearchHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final GlobalSearchStatisticsService statisticsService;
private final AuthTokenRepository authTokenRepository;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
GlobalSearchStatisticsRequest statRequest = objectMapper.convertValue(
request.getMessageBody(), GlobalSearchStatisticsRequest.class);
if (statRequest.getToken() == null) {
throw new IllegalArgumentException("User token is required");
}
Optional<AuthToken> tokenOptional =
authTokenRepository.findByToken(statRequest.getToken());
if (tokenOptional.isEmpty()) {
throw new IllegalArgumentException("User token is required");
}
Long userId = tokenOptional.orElseThrow().getUser().getId();
GlobalSearchStatisticsResponse statistics = statisticsService.getStatistics(
userId, statRequest.getLimit(), statRequest.getDays());
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(statistics)
.build();
}
}
@@ -1,10 +1,10 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.configuration.search.SearchProperties;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
@@ -15,7 +15,6 @@ import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.search.SearchImageService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
@@ -32,14 +31,14 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final SearchImageService searchImageService;
private final GoogleVisionSearchService googleVisionSearchService;
private final CheckCounterService checkCounterService;
private final FileEntityRepository fileEntityRepository;
private final TariffInfoService tariffInfoService;
private final SearchProperties searchProperties;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -51,48 +50,76 @@ public class ImageFoundRequestHandler implements RequestHandler {
.orElseThrow(() -> {
throw new NotValidFieldException("File not found", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId",fileId)));
});
Map.of("fileId", fileId)));});
// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity);
List<YandexSearchResponse.ImageResult> allYandexImages = new ArrayList<>();
List<YandexSearchResponse.ImageResult> allGoogleImages = new ArrayList<>();
boolean hasTimeout = false;
//TODO uncommited when add billing
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
boolean useGoogle = searchProperties.getEngines().getOrDefault("google",
new SearchProperties.EngineConfig()).isEnabled();
boolean useYandex = searchProperties.getEngines().getOrDefault("yandex",
new SearchProperties.EngineConfig()).isEnabled();
log.info("Search settings: useGoogle={}, useYandex={}", useGoogle, useYandex);
if (useGoogle) {
try {
String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens",
"exact_matches");
log.info("searchResponseGoogle OK");
allGoogleImages = searchImageService.getAllImagesWithoutPagination(
searchResponseGoogle, "exact_matches");
} catch (TimeoutException e) {
log.warn("Google search timeout for file {}", fileId);
hasTimeout = true;
} catch (IOException e) {
log.error("Google search failed for file {}", fileId, e);
}
} else {
log.info("Google search is disabled by settings");
}
if (useYandex) {
try {
String searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image",
"visual_matches");
log.info("searchResponseYandex OK");
allYandexImages = searchImageService.getAllImagesWithoutPagination(
searchResponseYandex, "visual_matches");
} catch (TimeoutException e) {
log.warn("Yandex search timeout for file {}", fileId);
hasTimeout = true;
} catch (IOException e) {
log.error("Yandex search failed for file {}", fileId, e);
}
} else {
log.info("Yandex search is disabled by settings");
}
if (allYandexImages.isEmpty() && allGoogleImages.isEmpty()) {
log.warn("No results from any search engine");
String errorMessage = useGoogle || useYandex ? "No results found" : "All search engines are disabled";
String searchResponseYandex;
String searchResponseGoogle;
try {
searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image",
"visual_matches");
searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens",
"exact_matches");
} catch (TimeoutException e) {
log.warn("Search timeout for file {}, returning empty results", fileId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
"Search completed (partial)", Map.of(
"results", List.of(),
"total", 0,
"searchStatus", "timeout"
));
} catch (IOException e) {
log.error("Search failed for file {}", fileId, e);
return new BaseResponse(request.getMsgId(), 20007,
"Search service temporarily unavailable",
Map.of("fileId", fileId));
errorMessage, new YandexSearchResponse());
}
List<YandexSearchResponse.ImageResult> allUniqueImages;
if (useGoogle && useYandex) {
allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages);
log.info("Merged results from both engines: {} unique images", allUniqueImages.size());
} else if (useGoogle) {
allUniqueImages = allGoogleImages;
log.info("Results only from Google: {} images", allUniqueImages.size());
} else {
allUniqueImages = allYandexImages;
log.info("Results only from Yandex: {} images", allUniqueImages.size());
}
tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH);
checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType());
List<YandexSearchResponse.ImageResult> allYandexImages = searchImageService.getAllImagesWithoutPagination(
searchResponseYandex, "visual_matches");
List<YandexSearchResponse.ImageResult> allGoogleImages = searchImageService.getAllImagesWithoutPagination(
searchResponseGoogle, "exact_matches");
List<YandexSearchResponse.ImageResult> allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages,
allGoogleImages);
int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1;
int pageSize = 5;
@@ -106,7 +133,9 @@ public class ImageFoundRequestHandler implements RequestHandler {
finalResponse.setTotalResults(allUniqueImages.size());
finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2)));
String message = hasTimeout ? "Search completed (partial)" : MessageCode.SUCCESS.getDescription();
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), finalResponse);
message, finalResponse);
}
}
@@ -0,0 +1,79 @@
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.monitoring.MonitoringStatusResponse;
import ru.soune.nocopy.dto.monitoring.SetMonitoringRequest;
import ru.soune.nocopy.entity.monitoring.MonitoringType;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.UserNotHavePermission;
import ru.soune.nocopy.service.monitoring.FileMonitoringService;
import ru.soune.nocopy.service.register.AuthService;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.Map;
@Slf4j
@Component
@RequiredArgsConstructor
public class MonitoringHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final FileMonitoringService monitoringService;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
SetMonitoringRequest setRequest = objectMapper.convertValue(
request.getMessageBody(), SetMonitoringRequest.class);
String authToken = setRequest.getAuthToken();
AuthToken userAuthToken = authService.getAuthToken(authToken);
if (authToken == null || userAuthToken == null) {
return new BaseResponse(30007, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(),
Map.of("token", authToken));
}
User user = userAuthToken.getUser();
Long userId = user.getId();
String typeStr = setRequest.getMonitoringType();
MonitoringType monitoringType;
try {
monitoringType = MonitoringType.valueOf(typeStr);
} catch (IllegalArgumentException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.MONITORING_TYPE_NOT_FOUND.getCode(),
MessageCode.MONITORING_TYPE_NOT_FOUND.getDescription(),
Map.of("monitoring", Arrays.asList(MonitoringType.values())));
}
try {
monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType);
} catch (UserNotHavePermission e) {
return new BaseResponse(request.getMsgId(),
MessageCode.USER_NOT_HAD_PERMISSION.getCode(),
MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), Map.of("token", authToken));
} catch (FileNotFoundException e) {
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_NOT_FOUND.getCode(),
MessageCode.FILE_NOT_FOUND.getDescription(), Map.of("fileId", setRequest.getFileId()));
}
MonitoringStatusResponse status = monitoringService.getMonitoringStatus(setRequest.getFileId(), userId);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
"Monitoring settings updated", status);
}
}
@@ -0,0 +1,51 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
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.payment.PaymentRequest;
import ru.soune.nocopy.dto.payment.PaymentResponse;
import ru.soune.nocopy.entity.payment.Payment;
import ru.soune.nocopy.exception.PaymentNotFoundException;
import ru.soune.nocopy.service.payment.PaymentService;
import java.util.List;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class PaymentHandler implements RequestHandler{
private final PaymentService paymentService;
private final ObjectMapper objectMapper;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
PaymentRequest paymentRequest = objectMapper.convertValue(request.getMessageBody(), PaymentRequest.class);
String action = paymentRequest.getAction();
switch (action) {
case "user_payments":
List<Payment> payments = paymentService.userPayments(paymentRequest.getEmail());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), new PaymentResponse(payments));
case "payment_info":
try {
Payment payment = paymentService.paymentInfo(paymentRequest.getPaymentUuid());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), new PaymentResponse(List.of(payment)));
} catch (PaymentNotFoundException e) {
return new BaseResponse(request.getMsgId(), MessageCode.PAYMENT_NOT_FOUND.getCode(),
MessageCode.PAYMENT_NOT_FOUND.getDescription(), Map.of("paymentUuid",
paymentRequest.getPaymentUuid()));
}
}
return null;
}
}
@@ -12,6 +12,8 @@ import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.referral.Referral;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.exception.CompanyAlreadyExist;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
@@ -19,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;
@@ -41,71 +44,97 @@ 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);
try {
if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail())));
regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone())));
if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail())));
regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone())));
throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " +
regRequest.getPhone(), new BaseResponse(request.getMsgId(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
}
String referralLink = regRequest.getReferralLink();
if (referralLink != null) {
Referral referral = referralJpaRepository.findByReferralLink(referralLink);
if (referral == null) {
throw new NotValidFieldException("Referral link is not found:" + referralLink +
referralLink, new BaseResponse(request.getMsgId(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of(
"referralLink", referralLink)));
throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " +
regRequest.getPhone(), new BaseResponse(request.getMsgId(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(),
MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer));
}
String referralLink = regRequest.getReferralLink();
String mailVerified = regRequest.getMailVerified();
if (referralLink != null) {
Referral referral = referralJpaRepository.findByReferralLink(referralLink);
if (referral == null) {
throw new NotValidFieldException("Referral link is not found:" + referralLink +
referralLink, new BaseResponse(request.getMsgId(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(),
MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of(
"referralLink", referralLink)));
}
if (mailVerified == null) {
throw new NotValidFieldException("Mailverified is null:" + referralLink +
referralLink, new BaseResponse(request.getMsgId(),
MessageCode.MAIL_VERIFIED_NULL.getCode(),
MessageCode.MAIL_VERIFIED_NULL.getDescription(), Map.of(
"mailVerified", mailVerified)));
}
}
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult);
if (bindingResult.hasErrors()) {
throw new ValidationException(bindingResult, request.getMsgId());
}
boolean verified = mailVerified.equals("true");
AuthToken authToken = authService.register(regRequest, verified);
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
RegAnswer regAnswer = new RegAnswer();
if (verified) {
try {
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()
));
}
regAnswer.setVerified(false);
regAnswer.setActive(false);
} else {
regAnswer.setUserId(authToken.getUser().getId());
regAnswer.setVerified(true);
regAnswer.setActive(true);
}
String token = authToken.getToken();
authService.useUserAuthToken(token);
regAnswer.setToken(token);
regAnswer.setUserId(authToken.getUser().getId());
if (regRequest.getAccountType().equals("b2c")) {
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink());
}
additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent(), authToken.getUser());
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(
"inn", regRequest.getInn()));
}
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult);
if (bindingResult.hasErrors()) {
throw new ValidationException(bindingResult, request.getMsgId());
}
AuthToken authToken = authService.register(regRequest);
// EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
// try {
// 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();
authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer();
regAnswer.setUserId(authToken.getUser().getId());
regAnswer.setToken(token);
regAnswer.setVerified(true);
regAnswer.setActive(true);
if (regRequest.getAccountType().equals("b2c")) {
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink());
}
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer);
}
}
@@ -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) {
return 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);
}
}
@@ -10,6 +10,7 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffRequest;
import ru.soune.nocopy.dto.tarriff.TariffResponse;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.AuthTokenRepository;
@@ -44,6 +45,8 @@ public class TariffHandler implements RequestHandler {
return handleUpdateTariff(request, tariffRequest);
case "get" :
return handleGetTariffs(request, tariffRequest);
case "tariffs_by_type" :
return handleGetTariffsByType(request, tariffRequest);
}
} catch (Exception e) {
@@ -62,7 +65,7 @@ public class TariffHandler implements RequestHandler {
private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) {
tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(),
tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(),
tariffRequest.getMaxFilesCount());
tariffRequest.getMaxFilesCount(), tariffRequest.getTariffTerm());
TariffResponse tariffResponse = TariffResponse.builder()
.tariffName(tariffRequest.getName())
@@ -120,10 +123,19 @@ public class TariffHandler implements RequestHandler {
private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) {
AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow();
User user = authToken.getUser();
List<TariffDTO> allTariffs = tariffService.getTariffByAccountType(user);
List<TariffDTO> allTariffs = tariffService.getTariffByAccountTypeAndTariffTerm(user,
TariffTimeTerm.valueOf(tariffRequest.getTariffTerm().toUpperCase()));
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
Map.of("tariffs", allTariffs));
}
private BaseResponse handleGetTariffsByType(BaseRequest request, TariffRequest tariffRequest) {
List<TariffDTO> allTariffs = tariffService.getTariffByType(tariffRequest.getType());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
Map.of("token_tariffs", allTariffs));
}
}
@@ -12,6 +12,7 @@ import ru.soune.nocopy.dto.tarriff.TariffInfoRequest;
import ru.soune.nocopy.dto.tarriff.TariffInfoResponse;
import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffStatus;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.repository.TariffRepository;
import ru.soune.nocopy.service.tariff.TariffInfoService;
@@ -75,13 +76,14 @@ public class TariffInfoHandler implements RequestHandler {
try {
Long tariffId = tariffInfoRequest.getTariffId();
Tariff tariff= tariffRepository.findById(tariffId).orElseThrow();
TariffTimeTerm tariffTerm = tariff.getTariffTerm();
LocalDateTime endDateTime = LocalDateTime.now().plusDays(tariffTerm.getDays());
TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(null,
tariffInfoRequest.getStatus() != null ? tariffInfoRequest.getStatus() : TariffStatus.ACTIVE.name(),
tariffInfoRequest.getStartTariff() != null ? tariffInfoRequest.getStartTariff() : LocalDateTime.now(),
tariffInfoRequest.getEndTariff() != null ? tariffInfoRequest.getEndTariff() : LocalDateTime.now().plusDays(30),
tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), tariff.getDiskSize(), 0L,
0, tariff.getMaxFilesCount());
endDateTime, tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(),
tariff.getDiskSize(), 0L, 0, tariff.getMaxFilesCount());
TariffInfoDTO createdTariffInfo = tariffInfoService.createTariffInfo(tariffInfoDTO);
@@ -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<UserAdditionalInfo> infos = additionalInfoService.getAllAdditionalInfos();
List<UserAdditionalInfoAnswer> 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<UserAdditionalInfo> 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");
}
}
}
@@ -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);
@@ -68,13 +69,13 @@ public class VerifyRegisterUserHandler implements RequestHandler {
EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId());
if (token != null && !token.isExpired()) {
emailService.sendVerificationEmail(user, token.getToken());
log.info("Verification token resent for user {}", user.getEmail());
return successResponse(request, authToken, "Verification code has been resent", false,
false, user.getEmail());
}
// if (token != null && !token.isExpired()) {
// emailService.sendVerificationEmail(user, token.getToken());
//
// log.info("Verification token resent for user {}", user.getEmail());
// return successResponse(request, authToken, "Verification code has been resent", false,
// false, user.getEmail());
// }
if (token != null) {
emailVerificationTokenRepository.delete(token);
@@ -0,0 +1,160 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.violation.ViolationRequest;
import ru.soune.nocopy.dto.violation.ViolationResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.violation.Violation;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.violation.ViolationService;
import ru.soune.nocopy.service.violation.ViolationStatus;
import java.io.FileNotFoundException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
@RequiredArgsConstructor
public class ViolationHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final ViolationService violationService;
private final FileEntityRepository fileRepository;
private final AuthService authService;
private final FileEntityService fileEntityService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ViolationRequest violationRequest = objectMapper.convertValue(request.getMessageBody(), ViolationRequest.class);
validateRequest(violationRequest);
String token = violationRequest.getToken();
Long userId = authService.useUserAuthToken(token);
List<FileEntity> targetFiles;
if (violationRequest.getFileId() != null && !violationRequest.getFileId().isEmpty()) {
FileEntity file = fileRepository.findById(violationRequest.getFileId())
.orElseThrow(() -> new FileNotFoundException("File not found with id: " + violationRequest.getFileId()));
targetFiles = List.of(file);
} else {
targetFiles = fileEntityService.getAllUserFiles(userId);
}
LocalDateTime startDate = null;
LocalDateTime endDate = null;
if (violationRequest.getStartDate() != null && violationRequest.getEndDate() != null) {
startDate = parseDate(violationRequest.getStartDate());
endDate = parseDate(violationRequest.getEndDate());
}
if ("updateStatus".equals(violationRequest.getAction())) {
String status = violationRequest.getStatus();
Long violationId = violationRequest.getViolationId();
violationService.changeStatus(ViolationStatus.valueOf(status), violationId);
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(Map.of("status", status))
.build();
}
if ("group".equalsIgnoreCase(violationRequest.getAction())) {
Map<String, Long> groupedData = violationService.getGroupedViolations(
targetFiles,
violationRequest.getGroupBy(),
violationRequest.getStatus(),
startDate,
endDate,
violationRequest.getSortDirection());
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(groupedData)
.build();
} else {
Page<Violation> violationPage;
if (startDate != null && endDate != null) {
violationPage = violationService.getViolationsByFilesAndDateRange(
targetFiles, startDate, endDate,
violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection()
);
} else if (violationRequest.getStatus() != null && !violationRequest.getStatus().isEmpty()) {
violationPage = violationService.getViolationsByFilesAndStatus(
targetFiles, violationRequest.getStatus(),
violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection()
);
} else {
violationPage = violationService.getViolationsByFiles(
targetFiles,
violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection()
);
}
List<ViolationResponse.ViolationDto> violationDtos = violationPage.getContent()
.stream()
.map(ViolationResponse.ViolationDto::fromEntity)
.toList();
ViolationResponse response = ViolationResponse.builder()
.violations(violationDtos)
.totalElements(violationPage.getTotalElements())
.totalPages(violationPage.getTotalPages())
.currentPage(violationPage.getNumber())
.pageSize(violationPage.getSize())
.hasNext(violationPage.hasNext())
.hasPrevious(violationPage.hasPrevious())
.build();
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(response)
.build();
}
}
private void validateRequest(ViolationRequest request) {
if (request.getPage() < 0) {
request.setPage(0);
}
if (request.getSize() <= 0 || request.getSize() > 100) {
request.setSize(10);
}
if (!request.getSortDirection().equalsIgnoreCase("asc") &&
!request.getSortDirection().equalsIgnoreCase("desc")) {
request.setSortDirection("desc");
}
}
private LocalDateTime parseDate(String dateStr) {
try {
return LocalDateTime.parse(dateStr, DATE_FORMATTER);
} catch (DateTimeParseException e) {
log.error(e.getMessage());
return null;
}
}
}
@@ -0,0 +1,260 @@
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<AuthToken> 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: {}", e.getMessage());
return new BaseResponse(
request.getMsgId(),
MessageCode.VALIDATION_ERROR.getCode(),
e.getMessage(),
null);
} catch (SecurityException e) {
log.error("Security error: {}", e.getMessage());
return new BaseResponse(
request.getMsgId(),
MessageCode.ACCESS_DENIED.getCode(),
e.getMessage(),
null);
} catch (Exception e) {
log.error("Unexpected error", 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.getViolationId() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_JSON_BODY.getCode(),
"ViolationId is required for get_notions action",
null);
}
try {
ViolationNotionsListResponse notions = notionService.getNotionsByViolationId(
notionRequest.getViolationId(), userId);
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(notions)
.build();
} catch (IllegalArgumentException e) {
return new BaseResponse(
request.getMsgId(),
MessageCode.NOTION_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.INVALID_JSON_BODY.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.getViolationId() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_JSON_BODY.getCode(),
"ViolationId is required for add_notion action",
null);
}
if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) {
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_JSON_BODY.getCode(),
"Message is required for add_notion action",
null);
}
try {
ViolationNotionResponse created = notionService.createNotion(
notionRequest.getViolationId(), 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.INVALID_JSON_BODY.getCode(),
"NotionId is required for update_notion action",
null);
}
if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) {
return new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_JSON_BODY.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.INVALID_JSON_BODY.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(),
null);
}
}
}
@@ -0,0 +1,90 @@
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.violation.ViolationStatisticsRequest;
import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.violation.ViolationService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
@Slf4j
@Component
@RequiredArgsConstructor
public class ViolationStatisticsHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final ViolationService violationService;
private final AuthTokenRepository authTokenRepository;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ViolationStatisticsRequest statRequest = objectMapper.convertValue(
request.getMessageBody(), ViolationStatisticsRequest.class);
validateRequest(statRequest);
LocalDateTime startDate = null;
LocalDateTime endDate = null;
if (statRequest.getStartDate() != null && statRequest.getEndDate() != null) {
startDate = parseDate(statRequest.getStartDate());
endDate = parseDate(statRequest.getEndDate());
if (endDate != null && !statRequest.getEndDate().contains(" ")) {
endDate = endDate.withHour(23).withMinute(59).withSecond(59);
}
}
Optional<AuthToken> tokenOptional =
authTokenRepository.findByToken(statRequest.getToken());
if (tokenOptional.isEmpty()) {
throw new IllegalArgumentException("User token is required");
}
Long userId = tokenOptional.orElseThrow().getUser().getId();
ViolationStatisticsResponse statistics = violationService.getViolationStatistics(
userId,
statRequest.getFileId(),
startDate,
endDate);
return BaseResponse.builder()
.msgId(request.getMsgId())
.messageBody(statistics)
.build();
}
private void validateRequest(ViolationStatisticsRequest request) {
if (request.getToken() == null) {
throw new IllegalArgumentException("Token is required");
}
}
private LocalDateTime parseDate(String dateStr) {
try {
return LocalDateTime.parse(dateStr, DATE_FORMATTER);
} catch (DateTimeParseException e) {
try {
return LocalDateTime.parse(dateStr + " 00:00:00", DATE_FORMATTER);
} catch (DateTimeParseException ex) {
log.error("Error parsing date: {}", dateStr, ex);
return null;
}
}
}
}
@@ -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");