78 Commits
Author SHA1 Message Date
vladp 31140e810b dev delete async
Test Workflow / test (push) Successful in 3s
2026-04-09 11:03:31 +07:00
vladp aef2c1dfe0 dev add verified notifications
Test Workflow / test (push) Successful in 1m29s
2026-04-08 21:58:28 +07:00
vladp be1ad7f066 dev add notification entity
Test Workflow / test (push) Has been cancelled
2026-04-04 11:26:35 +07:00
vladp 7cd2aa7d32 Merge branch 'dev' into NCBACK-35
# Conflicts:
#	src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java
2026-04-04 11:23:05 +07:00
vladp 6bffd5b385 dev add notification entity
Test Workflow / test (push) Waiting to run
2026-04-04 00:52:01 +07:00
vladp 283f0b9e6f dev add notification entity
Test Workflow / test (push) Waiting to run
2026-04-03 15:42:45 +07:00
vladp a9eda37aad dev add notification entity
Test Workflow / test (push) Successful in 5s
2026-04-03 12:29:09 +07:00
vladp ecd42ce263 dev add notification entity
Test Workflow / test (push) Successful in 4s
2026-04-02 12:43:11 +07:00
vladp 9d33c7532a dev add notification entity
Test Workflow / test (push) Successful in 3s
2026-04-02 12:18:47 +07:00
vladp 0f2908b037 dev add notification entity
Test Workflow / test (push) Successful in 4s
2026-04-02 11:57:34 +07:00
vladp 37abdeca9a dev add notification entity
Test Workflow / test (push) Successful in 3s
2026-04-02 11:26:59 +07:00
vladp e87c14198b dev add notification entity
Test Workflow / test (push) Successful in 3s
2026-04-02 11:24:26 +07:00
vladp 8bc95efa2b dev add notification entity
Test Workflow / test (push) Successful in 4s
2026-04-02 11:01:09 +07:00
vladp 051f1ef5d4 dev add notification entity
Test Workflow / test (push) Successful in 2s
2026-04-01 15:32:36 +07:00
vladp 549d248cb7 dev add notification entity
Test Workflow / test (push) Successful in 14s
2026-04-01 11:43:06 +07:00
vladp 9a3a8bde05 dev add notification entity
Test Workflow / test (push) Waiting to run
2026-03-31 15:31:59 +07:00
vladp 69e758a710 dev email send when tokens not found
Test Workflow / test (push) Successful in 3s
2026-03-31 14:43:34 +07:00
vladp d7874aa37a 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
2026-03-31 14:24:03 +07:00
vladp ad4850414a dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 13:31:26 +07:00
vladp 3d2c7ecb05 dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 13:06:27 +07:00
vladp 51170891ce dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 11:52:39 +07:00
vladp d775802a99 dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 11:35:19 +07:00
vladp dfb1fca8cd dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 11:34:41 +07:00
vladp 3f375d3b12 dev email send when tokens not found
Test Workflow / test (push) Waiting to run
2026-03-31 11:24:43 +07:00
vladp a7b9f22939 dev email send when tokens not found
Test Workflow / test (push) Successful in 3s
2026-03-30 14:42:51 +07:00
vladp e78f882417 dev email send when tokens not found
Test Workflow / test (push) Successful in 2s
2026-03-30 14:28:56 +07:00
vladp 2445b70c7a dev email send when tokens not found
Test Workflow / test (push) Successful in 2s
2026-03-30 13:48:10 +07:00
vladp 41ac8933e9 dev email send when tokens not found
Test Workflow / test (push) Successful in 2s
2026-03-30 13:36:57 +07:00
vladp 7af909c4f2 dev email send when tokens not found
Test Workflow / test (push) Successful in 2s
2026-03-30 13:21:11 +07:00
vladp 976d1f751e dev email send when tokens not found
Test Workflow / test (push) Successful in 6s
2026-03-30 12:48:11 +07:00
vladp dbef82d896 dev email send when tokens not found
Test Workflow / test (push) Successful in 2s
2026-03-30 12:29:53 +07:00
vladp 7e1877ffc3 dev email send when tokens not found
Test Workflow / test (push) Successful in 6s
2026-03-30 12:14:06 +07:00
vladp 8ce07c6b81 dev add reason entity
Test Workflow / test (push) Successful in 5s
2026-03-30 11:31:31 +07:00
vladp fa1892585a dev add reason entity
Test Workflow / test (push) Successful in 4s
2026-03-27 22:26:22 +07:00
vladp 6d6721fb0a dev add reason entity
Test Workflow / test (push) Waiting to run
2026-03-27 21:54:36 +07:00
vladp 535be69840 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 19:35:55 +07:00
vladp fdea57d947 dev add reason entity
Test Workflow / test (push) Successful in 1s
2026-03-27 19:29:54 +07:00
vladp 0aa7f5c865 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 19:08:15 +07:00
vladp 067b967a17 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 17:54:04 +07:00
vladp 9a8a4a72f4 dev add reason entity
Test Workflow / test (push) Successful in 3s
2026-03-27 17:31:06 +07:00
vladp 964910ec69 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 16:37:20 +07:00
vladp 547e70ecdd dev add reason entity
Test Workflow / test (push) Successful in 3s
2026-03-27 15:31:45 +07:00
vladp 78b97b48c9 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 14:31:49 +07:00
vladp 4e0579c952 dev add reason entity
Test Workflow / test (push) Successful in 2s
2026-03-27 14:20:09 +07:00
vladp 57dcc12cf2 dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-27 13:37:37 +07:00
vladp a01adfc848 dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-26 14:42:38 +07:00
vladp 8965ffdd1f dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-26 14:34:34 +07:00
vladp a21eb899b3 dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-26 11:40:32 +07:00
vladp 95ebcf0241 dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-25 16:31:21 +07:00
vladp a3e6e0091a dev add new status fo file
Test Workflow / test (push) Successful in 4s
2026-03-25 16:24:19 +07:00
vladp 2f38a7c455 dev add new status fo file
Test Workflow / test (push) Successful in 3s
2026-03-25 16:12:26 +07:00
vladp 17aa52d4ac dev add new status fo file
Test Workflow / test (push) Successful in 4s
2026-03-25 15:55:16 +07:00
vladp 8eb0772196 dev add additional info user
Test Workflow / test (push) Successful in 3s
2026-03-25 13:08:10 +07:00
vladp c1782cf59b dev add additional info user
Test Workflow / test (push) Successful in 4s
2026-03-25 12:46:33 +07:00
vladp 46cf252699 dev add additional info user
Test Workflow / test (push) Successful in 4s
2026-03-25 12:11:25 +07:00
vladp ebcb68135b dev add additional info user
Test Workflow / test (push) Successful in 4s
2026-03-25 12:00:04 +07:00
vladp e68ad3d3d3 dev add additional info user
Test Workflow / test (push) Waiting to run
2026-03-25 11:52:01 +07:00
vladp 12e001211b dev add additional info user
Test Workflow / test (push) Successful in 11s
2026-03-24 16:16:16 +07:00
vladp 40f8effb31 dev add additional info user
Test Workflow / test (push) Successful in 11s
2026-03-24 11:37:25 +07:00
vladp 69c3d876ae dev add complaint entity
Test Workflow / test (push) Successful in 3s
2026-03-23 15:17:42 +07:00
vladp 610415951d dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 19:27:13 +07:00
vladp 9ca116856c dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 18:44:03 +07:00
vladp 86db70caf4 dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 18:36:54 +07:00
vladp 0b2ef108ca dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 18:23:53 +07:00
vladp 0fbe725eb9 dev add violation notion
Test Workflow / test (push) Successful in 7s
2026-03-20 17:37:01 +07:00
vladp 29cdbb887f dev add violation notion 2026-03-20 17:37:00 +07:00
vladp 2bf5e99fbb dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 17:33:09 +07:00
vladp 655e8f0844 dev add violation notion
Test Workflow / test (push) Successful in 2s
2026-03-20 17:13:22 +07:00
vladp 31aa1c0545 dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 16:43:11 +07:00
vladp 97d6c8317e dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 16:35:35 +07:00
vladp a42f3e8ed8 dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 16:28:23 +07:00
vladp 46c75deacb dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 16:21:47 +07:00
vladp 4930a73762 dev add violation notion
Test Workflow / test (push) Successful in 3s
2026-03-20 16:08:32 +07:00
vladp 726da25ddf Revert "dev add violation notion"
Test Workflow / test (push) Successful in 3s
This reverts commit 4ff5c5ec13.
2026-03-20 16:02:11 +07:00
vladp a9b7b7ca6e Revert "dev add violation notion"
This reverts commit b6656961de.
2026-03-20 16:02:10 +07:00
vladp daef30ec11 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 16:05:15 +07:00
vladp a25b33fe13 NCBACK-35 add worked logic for save protected files
Test Workflow / test (push) Successful in 4s
2026-02-16 15:04:58 +07:00
vladp 2985d84b04 NCBACK-35 add cloud for save when download last chunk and after protected
Test Workflow / test (push) Successful in 3s
2026-02-13 21:11:30 +07:00
106 changed files with 3198 additions and 301 deletions
+11
View File
@@ -323,6 +323,7 @@ server_name dev-workspace.not-copy.com;
ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem;
location /api/ { location /api/ {
proxy_pass http://localhost:3001; proxy_pass http://localhost:3001;
proxy_set_header Host $host; proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
@@ -490,3 +491,13 @@ killall 3proxy # Остановка прокси
bash bash
curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси
telnet 193.46.217.94 3128 # Проверка порта telnet 193.46.217.94 3128 # Проверка порта
------
Настройка Яндекс Клауд
1. YCAJEpWAtaVkVGX0sH6_EupEg - индетификатор ключа
2. YCMmVykfXrZ_nfU13Vo4yoCVGa70DnTlBgF1pUzO - секретный ключ
private static final String KEY_ID = "YCAJEmHZcWxzf4oGWvETG3mms";
private static final String SECRET_KEY = "YCPAPZ32XC-LWb8xE0b_xTcBD8NZUCHpSOfu8LUG";
+6
View File
@@ -65,6 +65,12 @@ dependencies {
implementation project(':referral') implementation project(':referral')
//cloud
implementation("software.amazon.awssdk:aws-sdk-java:2.29.33")
implementation("software.amazon.awssdk:apache-client:2.29.33")
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0")
implementation("org.springframework.boot:spring-boot-starter-web")
} }
tasks.named('test') { tasks.named('test') {
+1
View File
@@ -9,5 +9,6 @@ interface Referral {
val totalIncome: Int val totalIncome: Int
val availableIncome: Int val availableIncome: Int
val holdBalance: Int val holdBalance: Int
val spentBalance: Int
val active: Boolean val active: Boolean
} }
+3 -1
View File
@@ -18,6 +18,7 @@ class ReferralService(
override val totalIncome = 0 override val totalIncome = 0
override val availableIncome = 0 override val availableIncome = 0
override val holdBalance: Int = 0 override val holdBalance: Int = 0
override val spentBalance: Int = 0
override val active: Boolean = false override val active: Boolean = false
} }
@@ -59,7 +60,8 @@ class ReferralService(
totalIncome = referral.totalIncome, totalIncome = referral.totalIncome,
availableIncome = referral.availableIncome, availableIncome = referral.availableIncome,
holdBalance = referral.holdBalance, holdBalance = referral.holdBalance,
referralLink = referral.referralLink referralLink = referral.referralLink,
spentBalance = referral.spentBalance,
) )
} }
+1
View File
@@ -7,4 +7,5 @@ data class ReferralStat(
val availableIncome: Int, val availableIncome: Int,
val holdBalance: Int, val holdBalance: Int,
val referralLink: String, val referralLink: String,
val spentBalance: Int,
) )
@@ -20,6 +20,10 @@ import java.util.Collections;
@AllArgsConstructor @AllArgsConstructor
public class ApplicationConfig { public class ApplicationConfig {
// private final FileEntityRepository fileEntityRepository;
// private final FileUtil fileUtil;
@Bean @Bean
PasswordEncoder passwordEncoder() { PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); return new BCryptPasswordEncoder();
@@ -36,6 +40,9 @@ public class ApplicationConfig {
AudioFilePathProvider audioFilePathProvider, AudioFilePathProvider audioFilePathProvider,
DocumentLocalSearch documentLocalSearch) { DocumentLocalSearch documentLocalSearch) {
// List<FileProtector.FileInfo> initialFiles = loadProcessingFile();
//TODO инициализация файлов при создании
return new com.vrt.NoCopyFileService( return new com.vrt.NoCopyFileService(
Collections.emptyList(), Collections.emptyList(),
fileProvider, fileProvider,
@@ -47,5 +54,13 @@ public class ApplicationConfig {
documentLocalSearch documentLocalSearch
); );
} }
// private List<FileProtector.FileInfo> loadProcessingFile() {
// List<FileEntity> byProtectionStatus =
// fileEntityRepository.findByProtectionStatus(ProtectionStatus.PROCESSING);
//
// return byProtectionStatus.stream().map(fileEntity ->
// fileUtil.createFileInfo(fileEntity, null)).toList();
// }
} }
@@ -32,7 +32,10 @@ public class HandlerConfig {
ViolationHandler violationHandler, ViolationHandler violationHandler,
ViolationStatisticsHandler violationStatisticsHandler, ViolationStatisticsHandler violationStatisticsHandler,
GlobalSearchHandler globalSearchHandler, GlobalSearchHandler globalSearchHandler,
ViolationNotionHandler violationNotionHandler ViolationNotionHandler violationNotionHandler,
ComplaintEntityHandler complaintEntityHandler,
UserInfoHandler userInfoHandler,
NotificationHandler notificationHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -56,6 +59,9 @@ public class HandlerConfig {
map.put(30010, violationStatisticsHandler); map.put(30010, violationStatisticsHandler);
map.put(30011, globalSearchHandler); map.put(30011, globalSearchHandler);
map.put(30012, violationNotionHandler); map.put(30012, violationNotionHandler);
map.put(30013, complaintEntityHandler);
map.put(30014, userInfoHandler);
map.put(30015, notificationHandler);
return map; return map;
} }
@@ -7,7 +7,6 @@ import com.vrt.fileprotection.audio.AudioCheckResult;
import com.vrt.fileprotection.documents.DocumentCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.FileSystemResource;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PageableDefault;
@@ -37,6 +36,7 @@ import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.file.ProtectionsLimitService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
@@ -67,6 +67,8 @@ public class ApiController {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final NoCopyFileService noCopyFileService;
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final ProtectionsLimitService protectionsLimitService; private final ProtectionsLimitService protectionsLimitService;
@@ -77,6 +79,8 @@ public class ApiController {
private final CheckCounterService checkCounterService; private final CheckCounterService checkCounterService;
private final CloudStorageService cloudStorageService;
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@PathVariable("version") int version) { @PathVariable("version") int version) {
@@ -157,7 +161,7 @@ public class ApiController {
MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(),
duplicateData duplicateData
)); ));
}catch (FileFormatException e){ } catch (FileFormatException e){
return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); return buildErrorResponse(uploadId, chunkNumber, e.getMessage());
} catch (Exception e) { } catch (Exception e) {
log.error("Error uploading chunk", e); log.error("Error uploading chunk", e);
@@ -165,8 +169,6 @@ public class ApiController {
} }
} }
private final NoCopyFileService noCopyFileService;
@GetMapping("/v{version}/files/{fileId}/similar") @GetMapping("/v{version}/files/{fileId}/similar")
public ResponseEntity<BaseResponse> findSimilarFiles( public ResponseEntity<BaseResponse> findSimilarFiles(
@PathVariable("version") int version, @PathVariable("version") int version,
@@ -391,6 +393,18 @@ public class ApiController {
Long userId = authService.useUserAuthToken(tokenHeader); Long userId = authService.useUserAuthToken(tokenHeader);
FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version);
FileStatus status = entityResponse.getStatus();
if (status.equals(FileStatus.BLOCKED) || status.equals(FileStatus.DELETED) ||
status.equals(FileStatus.REMOVED)) {
Map<String, Object> errorData = new HashMap<>();
errorData.put("fileId", entityResponse.getId());
return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_IS_BLOCKED.getCode(),
MessageCode.FILE_IS_BLOCKED.getDescription(),
errorData));
}
User fileUser = userRepository.findById(entityResponse.getUserId()).orElseThrow(); User fileUser = userRepository.findById(entityResponse.getUserId()).orElseThrow();
User user = userRepository.findById(userId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow();
@@ -436,20 +450,21 @@ public class ApiController {
errorData)); errorData));
} }
Path filePath = Paths.get(entityResponse.getProtectedFilePath()); Path filePath = Paths.get(entityResponse.getProtectedFilePath());
FileSystemResource resource = new FileSystemResource(filePath); File file = cloudStorageService.readFileFromStorageByPath(
entityResponse.getProtectedFilePath());
long fileSize = Files.size(filePath); long fileSize = Files.size(filePath);
if (!resource.exists()) { if (!file.exists()) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
errorData.put("resource", resource.exists()); errorData.put("file", file.exists());
return ResponseEntity.ok().body(new BaseResponse(20004, return ResponseEntity.ok().body(new BaseResponse(20004,
MessageCode.FILE_DOWNLOAD_ERROR.getCode(), MessageCode.FILE_DOWNLOAD_ERROR.getCode(),
MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), MessageCode.FILE_DOWNLOAD_ERROR.getDescription(),
errorData)); errorData));
} }
String contentType = determineContentType(filePath); String contentType = determineContentType(filePath);
return ResponseEntity.ok() return ResponseEntity.ok()
@@ -457,7 +472,7 @@ public class ApiController {
.contentType(MediaType.parseMediaType(contentType)) .contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + entityResponse.getFileName() + "\"") "attachment; filename=\"" + entityResponse.getFileName() + "\"")
.body(resource); .body(file);
} catch (FileEntityNotFoundException e) { } catch (FileEntityNotFoundException e) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
errorData.put("fileId", fileId); errorData.put("fileId", fileId);
@@ -500,33 +515,6 @@ public class ApiController {
return ResponseEntity.ok().body(fileTypeStats); return ResponseEntity.ok().body(fileTypeStats);
} }
private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
return similarFiles.stream().anyMatch(f -> f.getHammingDistance() <= 5);
}
private ResponseEntity<BaseResponse> handleDuplicate(FileEntity fileEntity, List<SimilarFileDTO> similarFiles)
throws IOException {
fileEntityService.deleteFromDisk(fileEntity);
fileEntityService.softDeleteFileWithHash(fileEntity);
Optional<FileEntity> originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId());
if (originalFile.isPresent()) {
Map<String, String> duplicateInfo = Map.of(
"duplicate_file_id", originalFile.get().getId(),
"owner_user_id", String.valueOf(originalFile.get().getUserId()));
return ResponseEntity.ok().body(new BaseResponse(
20004,
MessageCode.DUPLICATE_FILE_UPLOAD.getCode(),
"Failed to upload chunk, duplicate",
duplicateInfo));
}
return null;
}
private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk, private ResponseEntity<BaseResponse> buildSuccessResponse(String uploadId, Integer chunkNumber, MultipartFile chunk,
String fileId) { String fileId) {
ChunkUploadResponse responseBody = ChunkUploadResponse.builder() ChunkUploadResponse responseBody = ChunkUploadResponse.builder()
@@ -1,5 +1,7 @@
package ru.soune.nocopy.controller; package ru.soune.nocopy.controller;
import com.vrt.NoCopyFileService;
import com.vrt.fileprotection.FileProtector;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
@@ -15,6 +17,7 @@ import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.CheckCounterService;
import ru.soune.nocopy.service.file.FileStorageService; import ru.soune.nocopy.service.file.FileStorageService;
import ru.soune.nocopy.service.file.ImageResizeService; import ru.soune.nocopy.service.file.ImageResizeService;
import ru.soune.nocopy.util.FileUtil;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.net.URLEncoder; import java.net.URLEncoder;
@@ -45,6 +48,12 @@ public class FileController {
@Autowired @Autowired
private ImageResizeService imageResizeService; private ImageResizeService imageResizeService;
@Autowired
private NoCopyFileService noCopyFileService;
@Autowired
private FileUtil fileUtil;
@GetMapping("/public/{fileId}") @GetMapping("/public/{fileId}")
public ResponseEntity<Resource> getPublicFile(@PathVariable String fileId) { public ResponseEntity<Resource> getPublicFile(@PathVariable String fileId) {
try { try {
@@ -145,4 +154,16 @@ public class FileController {
.ok() .ok()
.body(protectedFileCheck); .body(protectedFileCheck);
} }
@PostMapping("protect/{fileId}/{convertTo}")
public ResponseEntity<FileProtector.FileInfo> convert(@PathVariable String fileId, @PathVariable String convertTo) {
FileEntity file = fileRepository.findByFileId(fileId);
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(file, convertTo);
noCopyFileService.addFile(fileInfo);
return ResponseEntity
.ok()
.body(fileInfo);
}
} }
@@ -14,13 +14,17 @@ import ru.soune.nocopy.entity.payout.PayoutRequest;
import ru.soune.nocopy.entity.payout.PayoutType; import ru.soune.nocopy.entity.payout.PayoutType;
import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InsufficientFundsException;
import ru.soune.nocopy.exception.InvalidPayoutMethodException;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.PendingPayoutExistsException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.service.payout.PayoutMethodService; import ru.soune.nocopy.service.payout.PayoutMethodService;
import ru.soune.nocopy.service.payout.PayoutRequestService; import ru.soune.nocopy.service.payout.PayoutRequestService;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -40,7 +44,7 @@ public class PayoutController {
Create payout Create payout
*/ */
@PostMapping("/create-request") @PostMapping("/create-request")
public ResponseEntity<PayoutRequest> createPayoutRequest(@RequestHeader("Authorization") String tokenHeader, public ResponseEntity<?> createPayoutRequest(@RequestHeader("Authorization") String tokenHeader,
@Valid @RequestBody PayoutRequestDTO request) { @Valid @RequestBody PayoutRequestDTO request) {
String token = tokenHeader.replace("Bearer ", ""); String token = tokenHeader.replace("Bearer ", "");
@@ -50,9 +54,12 @@ public class PayoutController {
User user = authToken.getUser(); User user = authToken.getUser();
try {
PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request);
return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest);
} catch (InsufficientFundsException | PendingPayoutExistsException | InvalidPayoutMethodException e) {
return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage());
}
} }
/* /*
@@ -2,9 +2,11 @@ package ru.soune.nocopy.dto;
public enum MessageCode { public enum MessageCode {
SUCCESS(0, "Operation successful"), SUCCESS(0, "Operation successful"),
SUCCESS_MODERATION(0, "File moderated successfully"),
REG_EMAIL_EXISTS(1, "Email already registered"), REG_EMAIL_EXISTS(1, "Email already registered"),
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"), REFERRAL_LINK_IS_NOT_EXIST(1, "Refferal link is not exist"),
FILE_IS_BLOCKED(1, "File is blocked,deleted or removed"),
INVALID_FIELD(2, "Invalid field"), INVALID_FIELD(2, "Invalid field"),
MAIL_VERIFIED_NULL(2, "Mail verified null"), MAIL_VERIFIED_NULL(2, "Mail verified null"),
INVALID_TOKEN(2, "Token not found or time expired"), INVALID_TOKEN(2, "Token not found or time expired"),
@@ -51,10 +53,12 @@ public enum MessageCode {
MONITORING_TYPE_NOT_FOUND(4, "Monitoring type not found"), MONITORING_TYPE_NOT_FOUND(4, "Monitoring type not found"),
ERROR_TARIFF_INFO(2, "Erorr with tariff info"), ERROR_TARIFF_INFO(2, "Erorr with tariff info"),
FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"), FILE_FOR_SEARCH_NOT_VALID(2, "File for search unsupported"),
ILLEGAL_STATE(2, "File is not in moderation state"),
NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" + NOT_VALID_FILE_TYPE_OR_COUNT_FILE(2, "Cost for file type not found, count is negative or files count" +
"more than max valid"), "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"), NOTION_NOT_FOUND(4, "Notion not found"),
NOT_FOUND(4, "Notion not found"),
MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion");
private final Integer code; private final Integer code;
@@ -0,0 +1,38 @@
package ru.soune.nocopy.dto.complaint;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
@Data
public class ComplaintRequest {
@JsonProperty("action")
private String action;
@JsonProperty("id")
private Long id;
@JsonProperty("violation_id")
private Long violationId;
@JsonProperty("text")
private String complaintText;
@JsonProperty("email")
private String email;
@JsonProperty("status")
private String status;
@JsonProperty("page")
private Integer page;
@JsonProperty("size")
private Integer size;
@JsonProperty("sort_by")
private String sortBy;
@JsonProperty("sort_direction")
private String sortDirection;
}
@@ -0,0 +1,39 @@
package ru.soune.nocopy.dto.complaint;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ComplaintResponse {
@JsonProperty("id")
private Long id;
@JsonProperty("status")
private String status;
@JsonProperty("complaint_text")
private String complaintText;
@JsonProperty("violation_id")
private Long violationId;
@JsonProperty("email")
private String email;
@JsonProperty("created_at")
private String createdAt;
@JsonProperty("updated_at")
private String updatedAt;
@JsonProperty("not_moderated")
private Boolean notModerated;
}
@@ -0,0 +1,14 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class AppealInfo {
private String appealId;
private String status;
private LocalDateTime createdAt;
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class AppealRequest {
private String fileId;
private String appealReason;
private String additionalInfo;
private String token;
}
@@ -0,0 +1,19 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class AppealResponse {
private String appealId;
private String fileId;
private String appealReason;
private String additionalInfo;
private String status;
private String adminComment;
private LocalDateTime createdAt;
private LocalDateTime resolvedAt;
}
@@ -5,6 +5,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.file.FileStatus;
@Data @Data
@Builder @Builder
@@ -44,6 +45,30 @@ public class FileEntityRequest {
@JsonProperty("type") @JsonProperty("type")
private String type; private String type;
@JsonProperty("file_status")
private FileStatus fileStatus;
@JsonProperty("date_filter") @JsonProperty("date_filter")
private String dateFilter; private String dateFilter;
@JsonProperty("comment")
private String comment;
@JsonProperty("appeal_reason")
private String appealReason;
@JsonProperty("additional_info")
private String additionalInfo;
@JsonProperty("appeal_id")
private String appealId;
@JsonProperty("approve")
private Boolean approve;
@JsonProperty("reason_text")
private String reasonText;
@JsonProperty("search_query")
private String searchQuery;
} }
@@ -0,0 +1,20 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.file.FileStatus;
import java.time.LocalDateTime;
@Data
@Builder
public class FileModerationInfo {
private String fileId;
private String fileName;
private String ownerName;
private String ownerEmail;
private FileStatus currentStatus;
private LocalDateTime uploadDate;
private Boolean hasActiveAppeal;
private AppealInfo activeAppeal;
}
@@ -0,0 +1,21 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.file.FileStatus;
@Data
@Builder
public class ModerationActionRequest {
private String fileId;
private FileStatus newStatus;
private String comment;
private String token;
}
@@ -20,4 +20,5 @@ public class SimilarFileDTO {
LocalDateTime uploadDate; LocalDateTime uploadDate;
String url; String url;
Boolean owner; Boolean owner;
String fileStatus;
} }
@@ -0,0 +1,8 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
@Data
public class ActivateNotificationRequest {
private String authToken;
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class ActiveNotificationsResponse {
private List<NotificationDto> notifications;
private long unreadCount;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
import java.util.List;
@Data
public class MarkAsReadRequest {
private List<Long> notificationIds;
private String authToken;
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class MarkAsReadResponse {
private boolean success;
private int updatedCount;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import java.time.LocalDateTime;
@Data
@Builder
public class NotificationDto {
private Long id;
private String message;
private NotificationType type;
private NotificationStatus status;
private LocalDateTime createdAt;
}
@@ -0,0 +1,18 @@
package ru.soune.nocopy.dto.notification;
import lombok.Data;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import java.util.List;
@Data
public class NotificationListRequest {
private int page = 0;
private int size = 10;
private String sortBy = "createdAt";
private String sortDirection = "DESC";
private List<NotificationType> types;
private List<NotificationStatus> statuses;
private String authToken;
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.dto.notification;
import lombok.Builder;
import lombok.Data;
import java.util.List;
@Data
@Builder
public class NotificationListResponse {
private List<NotificationDto> notifications;
private int currentPage;
private int totalPages;
private long totalElements;
}
@@ -37,4 +37,10 @@ public class RegRequest {
@JsonProperty("mail_verified") @JsonProperty("mail_verified")
private String mailVerified; private String mailVerified;
@JsonProperty("ip")
private String ip;
@JsonProperty("user_agent")
private String userAgent;
} }
@@ -16,4 +16,5 @@ public class TariffDTO {
private int maxFilesCount; private int maxFilesCount;
private Long diskSize; private Long diskSize;
private Long maxUsers; private Long maxUsers;
private String tariffTerm;
} }
@@ -49,4 +49,7 @@ public class TariffRequest {
@JsonProperty("user_token") @JsonProperty("user_token")
private String userToken; private String userToken;
@JsonProperty("tariff_term")
private String tariffTerm;
} }
@@ -0,0 +1,20 @@
package ru.soune.nocopy.dto.user;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class UserAdditionalInfoBody {
@JsonProperty("action")
private String action;
@JsonProperty("user_id")
private Long userId;
@JsonProperty("ip_address")
private String ipAddress;
@JsonProperty("user_agent")
private String userAgent;
}
@@ -10,6 +10,9 @@ public class ViolationRequest {
@JsonProperty("file_id") @JsonProperty("file_id")
private String fileId; private String fileId;
@JsonProperty("violation_id")
private Long violationId;
@JsonProperty("page") @JsonProperty("page")
private Integer page = 0; private Integer page = 0;
@@ -0,0 +1,58 @@
package ru.soune.nocopy.entity.complaint;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
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.violation.Violation;
import java.time.LocalDateTime;
@Entity
@Table(name = "complaint")
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@EntityListeners(AuditingEntityListener.class)
public class ComplaintEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "status")
@Enumerated(EnumType.STRING)
private ComplaintStatus status;
@Column(name = "complaint_text", columnDefinition = "TEXT")
private String complaintText;
@Column(name = "email")
private String email;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "violation_id")
@ToString.Exclude
@JsonIgnore
private Violation violation;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "law_case_id")
@ToString.Exclude
@JsonIgnore
private LawCase lawCase;
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
@Column(name = "updated_at")
@LastModifiedDate
private LocalDateTime updatedAt;
@Column(name = "not_moderated_file")
private Boolean not_moderated_file;
}
@@ -0,0 +1,5 @@
package ru.soune.nocopy.entity.complaint;
public enum ComplaintStatus {
CREATED, SHOWED, IN_WORK, COMPLETED, CANCELLED
}
@@ -0,0 +1,17 @@
package ru.soune.nocopy.entity.complaint;
import jakarta.persistence.*;
import lombok.*;
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter @Setter
@Table(name = "law_case")
public class LawCase {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
@@ -0,0 +1,9 @@
package ru.soune.nocopy.entity.file;
public enum AppealStatus {
PENDING,
IN_REVIEW,
APPROVED,
REJECTED,
EXPIRED
}
@@ -0,0 +1,81 @@
package ru.soune.nocopy.entity.file;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "file_appeals")
@EntityListeners(AuditingEntityListener.class)
public class FileAppeal {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "file_id", nullable = false)
private String fileId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "file_id", insertable = false, updatable = false)
@ToString.Exclude
private FileEntity file;
@Column(name = "user_id", nullable = false)
private Long userId;
@Column(name = "appeal_reason", nullable = false, columnDefinition = "TEXT")
private String appealReason;
@Column(name = "additional_info", columnDefinition = "TEXT")
private String additionalInfo;
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private AppealStatus status;
@Column(name = "admin_comment", columnDefinition = "TEXT")
private String adminComment;
@Column(name = "moderator_id")
private Long moderatorId;
@Column(name = "moderation_before_status")
@Enumerated(EnumType.STRING)
private FileStatus moderationBeforeStatus;
@Column(name = "resolved_at")
private LocalDateTime resolvedAt;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@PrePersist
public void prePersist() {
if (this.status == null) {
this.status = AppealStatus.PENDING;
}
if (this.createdAt == null) {
this.createdAt = LocalDateTime.now();
}
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
}
@@ -87,6 +87,9 @@ public class FileEntity {
@Column(name = "medium_path") @Column(name = "medium_path")
private String mediumPath; private String mediumPath;
@Column
private String newExtension;
@OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
private ImageHashEntity imageHash; private ImageHashEntity imageHash;
@@ -98,7 +101,7 @@ public class FileEntity {
} }
if (this.status == null) { if (this.status == null) {
this.status = FileStatus.ACTIVE; this.status = FileStatus.MODERATION;
} }
if (this.protectionStatus == null) { if (this.protectionStatus == null) {
@@ -7,5 +7,8 @@ public enum FileStatus {
VIOLATION, VIOLATION,
CHECKED, CHECKED,
ERROR, ERROR,
TEMP TEMP,
REMOVED,
MODERATION,
BLOCKED
} }
@@ -0,0 +1,53 @@
package ru.soune.nocopy.entity.file.moderation;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "moderation_logs")
@EntityListeners(AuditingEntityListener.class)
public class ModerationLog {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "file_id", nullable = false)
private String fileId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "file_id", insertable = false, updatable = false)
@ToString.Exclude
private FileEntity file;
@Column(name = "moderator_id", nullable = false)
private Long moderatorId;
@Enumerated(EnumType.STRING)
@Column(name = "old_status", nullable = false)
private FileStatus oldStatus;
@Enumerated(EnumType.STRING)
@Column(name = "new_status", nullable = false)
private FileStatus newStatus;
@Column(name = "reason", columnDefinition = "TEXT")
private String reason;
@Column(name = "comment", columnDefinition = "TEXT")
private String comment;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
}
@@ -1,6 +1,14 @@
package ru.soune.nocopy.entity.monitoring; package ru.soune.nocopy.entity.monitoring;
import lombok.Getter;
@Getter
public enum MonitoringType { public enum MonitoringType {
MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY, NONE MONITORING_DAILY(1), MONITORING_WEEKLY(7), MONITORING_MONTHLY(30), NONE(0);
private final int days;
MonitoringType(int days) {
this.days = days;
}
} }
@@ -0,0 +1,49 @@
package ru.soune.nocopy.entity.notification;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@Builder
@Table(name = "notifications", indexes = {
@Index(name = "idx_user_status", columnList = "user_id, status"),
@Index(name = "idx_user_created", columnList = "user_id, created_at DESC")
})
public class Notification {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "created_at", updatable = false)
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at")
private LocalDateTime updatedAt;
@Column(name = "message")
private String message;
@Enumerated(EnumType.STRING)
@Column(name = "notification_type")
private NotificationType notificationType;
@Column(name = "status")
private NotificationStatus status = NotificationStatus.NEW;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
@JsonIgnore
private User user;
}
@@ -0,0 +1,14 @@
package ru.soune.nocopy.entity.notification;
import lombok.Getter;
@Getter
public enum NotificationMessage {
FILE_SEARCH("notification.file.search");
private final String messageKey;
NotificationMessage(String messageKey) {
this.messageKey = messageKey;
}
}
@@ -0,0 +1,5 @@
package ru.soune.nocopy.entity.notification;
public enum NotificationStatus {
NEW, READIED
}
@@ -0,0 +1,31 @@
package ru.soune.nocopy.entity.notification;
import lombok.Getter;
@Getter
public enum NotificationType {
SEARCH_RESULT("notification.search.result"),
MONITORING_RESULT("notification.monitoring.result"),
OPERATION_IMPOSSIBLE("notification.operation.impossible"),
START_FAILED_NOW("notification.start.failed.now"),
START_FAILED_NEXT("notification.start.failed.next"),
PAYOUT_RESULT("notification.payout.result"),
PAYMENT_RESULT("notification.payment.result"),
REFERRAL_DEPOSIT("notification.referral.deposit"),
REFERRAL_REGISTERED("notification.referral.registered"),
REFERRAL_ACTIVATED("notification.referral.activated"),
COMPLAINT_STATUS_CHANGED("notification.complaint.status"),
CASE_STATUS_CHANGED("notification.case.status"),
INCOMING_MESSAGE("notification.incoming.message"),
TARIFF_EXPIRING("notification.tariff.expiring"),
TOKEN_NOT_FOUND("notification.token.not.found"),
FILE_MODERATION_EVENT("notification.file.moderation"),
FILE_ADDED_TO_SYSTEM("notification.file.added"),
SEARCH_OPERATION_FAILED("notification.search.operation.filed");
private final String messageKey;
NotificationType(String messageKey) {
this.messageKey = messageKey;
}
}
@@ -40,4 +40,8 @@ public class Tariff {
@Column(name = "tariffAccountType") @Column(name = "tariffAccountType")
private String tariffAccountType; private String tariffAccountType;
@Column(name = "tariff_term")
@Enumerated(EnumType.STRING)
private TariffTimeTerm tariffTerm;
} }
@@ -0,0 +1,22 @@
package ru.soune.nocopy.entity.tarif;
import lombok.Getter;
import java.util.List;
@Getter
public enum TariffTimeTerm {
MONTHLY(30, "MONTHLY"),YEAR(365, "YEAR");
private final Integer days;
private final String name;
TariffTimeTerm(Integer days, String name) {
this.days = days;
this.name = name;
}
public static List<String> validTerms() {
return List.of(MONTHLY.name(), YEAR.name());
}
}
@@ -2,6 +2,7 @@ package ru.soune.nocopy.entity.tarif;
public enum TariffType { public enum TariffType {
START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000, START, START_YEAR, BASIC, BASIC_YEAR, PRO, PRO_YEAR, ENTERPRISE, ENTERPRISE_YEAR, DEMO, BUISNES, BUISNES_YEAR,
CORPORAT, CORPORAT_YEAR, STUDIO, STUDIO_YEAR, FREE, TOKEN_100, TOKEN_500, TOKEN_1000, TOKEN_5000,
MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY; MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY;
} }
@@ -0,0 +1,34 @@
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;
import java.sql.Timestamp;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@Builder
@Table(name = "user_additional_info")
public class UserAdditionalInfo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, name = "register_date")
@CreationTimestamp
private Timestamp registrationDate;
@Column(nullable = false, name = "ip")
private String ipAddress;
@Column(nullable = false, name = "user_agent")
private String userAgent;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", unique = true, nullable = false)
private User user;
}
@@ -0,0 +1,31 @@
package ru.soune.nocopy.entity.user;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.sql.Timestamp;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserAdditionalInfoAnswer {
@JsonProperty("id")
private Long id;
@JsonProperty("user_id")
private Long userId;
@JsonProperty("ip_address")
private String ipAddress;
@JsonProperty("user_agent")
private String userAgent;
@JsonProperty("registration_date")
private Timestamp registrationDate;
}
@@ -0,0 +1,22 @@
package ru.soune.nocopy.entity.user;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserAdditionalInfoListAnswer {
@JsonProperty("items")
private List<UserAdditionalInfoAnswer> items;
@JsonProperty("count")
private int count;
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class ComplaintNotFoundException extends RuntimeException {
public ComplaintNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class DuplicateComplaintException extends RuntimeException {
public DuplicateComplaintException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class InsufficientTokensException extends RuntimeException {
public InsufficientTokensException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class InvalidAppealException extends RuntimeException {
public InvalidAppealException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class RegistrationAdditionalInfoNullException extends RuntimeException {
public RegistrationAdditionalInfoNullException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class SearchFailedException extends RuntimeException {
public SearchFailedException(String message) {
super(message);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class UserAdditionalInfoNotFound extends RuntimeException {
public UserAdditionalInfoNotFound(String message) {
super(message);
}
}
@@ -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<>());
}
}
@@ -3,15 +3,21 @@ package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; 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.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository; 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.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService; import ru.soune.nocopy.service.file.FileStatsService;
@@ -35,6 +41,14 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
private final ModerationService moderationService;
private final FileAppealRepository fileAppealRepository;
private final ModerationLogRepository moderationLogRepository;
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
try { try {
@@ -60,6 +74,18 @@ public class FileEntityHandler implements RequestHandler {
return handleProtectFile(request, fileRequest); return handleProtectFile(request, fileRequest);
case "delete_file": case "delete_file":
return handleDeleteFile(request, fileRequest); 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: default:
ActionResponse response = ActionResponse.builder() ActionResponse response = ActionResponse.builder()
.action(action) .action(action)
@@ -183,6 +209,25 @@ 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) { private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = authService.useUserAuthToken(fileRequest.getToken());
@@ -536,7 +581,7 @@ public class FileEntityHandler implements RequestHandler {
int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete(); int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete();
if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) { if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) {
fileEntityService.deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
.message("File deleted from disk") .message("File deleted from disk")
@@ -568,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) { private String formatFileSize(long size) {
if (size < 1024) return size + " B"; if (size < 1024) return size + " B";
int exp = (int) (Math.log(size) / Math.log(1024)); int exp = (int) (Math.log(size) / Math.log(1024));
@@ -0,0 +1,147 @@
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.MessageCode;
import ru.soune.nocopy.dto.file.ActionResponse;
import ru.soune.nocopy.service.notification.NotificationService;
import com.fasterxml.jackson.databind.type.TypeFactory;
import ru.soune.nocopy.dto.notification.*;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class NotificationHandler implements RequestHandler {
private final NotificationService notificationService;
private final ObjectMapper objectMapper;
private final AuthService authService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
Map<String, Object> bodyMap = objectMapper.convertValue(request.getMessageBody(),
TypeFactory.defaultInstance().constructMapType(Map.class, String.class, Object.class));
String action = (String) bodyMap.get("action");
if (action == null) {
ActionResponse response = ActionResponse.builder()
.action(action)
.availableActions(Arrays.asList("list", "active", "markRead"))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_FIELD.getCode(),
"Action is required",
response);
}
switch (action) {
case "list": {
NotificationListRequest listRequest = objectMapper.convertValue(request.getMessageBody(),
NotificationListRequest.class);
User user = authService.getUser(listRequest.getAuthToken());
Page<Notification> page = notificationService.getAllNotifications(
user,
listRequest.getPage(),
listRequest.getSize(),
listRequest.getSortBy(),
listRequest.getSortDirection(),
listRequest.getTypes(),
listRequest.getStatuses());
List<NotificationDto> dtos = page.getContent().stream()
.map(this::toDto)
.collect(Collectors.toList());
NotificationListResponse response = NotificationListResponse.builder()
.notifications(dtos)
.currentPage(page.getNumber())
.totalPages(page.getTotalPages())
.totalElements(page.getTotalElements())
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
case "active": {
ActivateNotificationRequest activateRequest = objectMapper.convertValue(request.getMessageBody(),
ActivateNotificationRequest.class);
User user = authService.getUser(activateRequest.getAuthToken());
List<Notification> recent = notificationService.getActiveNotifications(user);
long unreadCount = notificationService.getUnreadCount(user);
ActiveNotificationsResponse response = ActiveNotificationsResponse.builder()
.notifications(recent.stream().map(this::toDto).collect(Collectors.toList()))
.unreadCount(unreadCount)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
case "markRead": {
MarkAsReadRequest markRequest = objectMapper.convertValue(request.getMessageBody(),
MarkAsReadRequest.class);
User user = authService.getUser(markRequest.getAuthToken());
int updated = notificationService.markAsRead(user, markRequest.getNotificationIds());
MarkAsReadResponse response = MarkAsReadResponse.builder()
.success(true)
.updatedCount(updated)
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
response);
}
default:
ActionResponse response = ActionResponse.builder()
.action(action)
.availableActions(Arrays.asList("list", "active", "markRead"))
.build();
return new BaseResponse(request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action,
response);
}
}
private NotificationDto toDto(Notification notification) {
return NotificationDto.builder()
.id(notification.getId())
.message(notification.getMessage())
.type(notification.getNotificationType())
.status(notification.getStatus())
.createdAt(notification.getCreatedAt())
.build();
}
}
@@ -21,6 +21,7 @@ import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.user.AdditionalInfoService;
import java.util.Arrays; import java.util.Arrays;
import java.util.Map; import java.util.Map;
@@ -43,6 +44,8 @@ public class RegRequestHandler implements RequestHandler {
private final ReferralJpaRepository referralJpaRepository; private final ReferralJpaRepository referralJpaRepository;
private final AdditionalInfoService additionalInfoService;
@Override @Override
public BaseResponse handle(BaseRequest request) throws ValidationException { public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -124,9 +127,10 @@ public class RegRequestHandler implements RequestHandler {
referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink());
} }
additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent(), authToken.getUser());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); MessageCode.SUCCESS.getDescription(), regAnswer);
} catch (CompanyAlreadyExist e) { } catch (CompanyAlreadyExist e) {
return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(),
MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of( MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of(
@@ -10,6 +10,7 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffRequest; import ru.soune.nocopy.dto.tarriff.TariffRequest;
import ru.soune.nocopy.dto.tarriff.TariffResponse; 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.AuthToken;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
@@ -64,7 +65,7 @@ public class TariffHandler implements RequestHandler {
private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) { private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) {
tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(), tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(),
tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(), tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(),
tariffRequest.getMaxFilesCount()); tariffRequest.getMaxFilesCount(), tariffRequest.getTariffTerm());
TariffResponse tariffResponse = TariffResponse.builder() TariffResponse tariffResponse = TariffResponse.builder()
.tariffName(tariffRequest.getName()) .tariffName(tariffRequest.getName())
@@ -122,7 +123,18 @@ public class TariffHandler implements RequestHandler {
private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) { private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) {
AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow(); AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow();
User user = authToken.getUser(); User user = authToken.getUser();
List<TariffDTO> allTariffs = tariffService.getTariffByAccountType(user);
if (tariffRequest.getTariffTerm() == null || !TariffTimeTerm.validTerms().contains(
tariffRequest.getTariffTerm())) {
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Not valid tariff_term",
Map.of("Valid term", TariffTimeTerm.validTerms()));
}
List<TariffDTO> allTariffs = tariffService.getTariffByAccountTypeAndTariffTerm(user,
TariffTimeTerm.valueOf(tariffRequest.getTariffTerm().toUpperCase()));
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), MessageCode.SUCCESS.getDescription(),
@@ -12,6 +12,7 @@ import ru.soune.nocopy.dto.tarriff.TariffInfoRequest;
import ru.soune.nocopy.dto.tarriff.TariffInfoResponse; import ru.soune.nocopy.dto.tarriff.TariffInfoResponse;
import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffStatus; 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.entity.tarif.TariffType;
import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.TariffRepository;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
@@ -75,13 +76,14 @@ public class TariffInfoHandler implements RequestHandler {
try { try {
Long tariffId = tariffInfoRequest.getTariffId(); Long tariffId = tariffInfoRequest.getTariffId();
Tariff tariff= tariffRepository.findById(tariffId).orElseThrow(); Tariff tariff= tariffRepository.findById(tariffId).orElseThrow();
TariffTimeTerm tariffTerm = tariff.getTariffTerm();
LocalDateTime endDateTime = LocalDateTime.now().plusDays(tariffTerm.getDays());
TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(null, TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(null,
tariffInfoRequest.getStatus() != null ? tariffInfoRequest.getStatus() : TariffStatus.ACTIVE.name(), tariffInfoRequest.getStatus() != null ? tariffInfoRequest.getStatus() : TariffStatus.ACTIVE.name(),
tariffInfoRequest.getStartTariff() != null ? tariffInfoRequest.getStartTariff() : LocalDateTime.now(), tariffInfoRequest.getStartTariff() != null ? tariffInfoRequest.getStartTariff() : LocalDateTime.now(),
tariffInfoRequest.getEndTariff() != null ? tariffInfoRequest.getEndTariff() : LocalDateTime.now().plusDays(30), endDateTime, tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(),
tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), tariff.getDiskSize(), 0L, tariff.getDiskSize(), 0L, 0, tariff.getMaxFilesCount());
0, tariff.getMaxFilesCount());
TariffInfoDTO createdTariffInfo = tariffInfoService.createTariffInfo(tariffInfoDTO); 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");
}
}
}
@@ -15,6 +15,7 @@ import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.violation.ViolationService; import ru.soune.nocopy.service.violation.ViolationService;
import ru.soune.nocopy.service.violation.ViolationStatus;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -66,6 +67,18 @@ public class ViolationHandler implements RequestHandler {
endDate = parseDate(violationRequest.getEndDate()); 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())) { if ("group".equalsIgnoreCase(violationRequest.getAction())) {
Map<String, Long> groupedData = violationService.getGroupedViolations( Map<String, Long> groupedData = violationService.getGroupedViolations(
targetFiles, targetFiles,
@@ -1,6 +1,7 @@
package ru.soune.nocopy.handler.validator; package ru.soune.nocopy.handler.validator;
import org.apache.commons.validator.routines.EmailValidator; import org.apache.commons.validator.routines.EmailValidator;
import org.apache.commons.validator.routines.InetAddressValidator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
@@ -28,6 +29,7 @@ public class RegRequestValidator implements Validator {
validateEmail(request.getEmail(), errors); validateEmail(request.getEmail(), errors);
validateCompanyName(request.getCompanyName(), errors); validateCompanyName(request.getCompanyName(), errors);
validateFullName(request.getFullName(), errors); validateFullName(request.getFullName(), errors);
validateAdditionalInfo(request.getIp(), request.getUserAgent(), errors);
} }
private void validateFullName(String fullName, Errors 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) { public void validateEmail(String email, Errors errors) {
if (email == null || email.trim().isEmpty()) { if (email == null || email.trim().isEmpty()) {
errors.rejectValue("email", "email.is.empty", "Email must not be empty"); errors.rejectValue("email", "email.is.empty", "Email must not be empty");
@@ -0,0 +1,28 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.complaint.ComplaintEntity;
import ru.soune.nocopy.entity.complaint.ComplaintStatus;
import java.util.List;
import java.util.Optional;
@Repository
public interface ComplaintEntityRepository extends JpaRepository<ComplaintEntity, String> {
Optional<ComplaintEntity> findById(Long id);
Page<ComplaintEntity> findAll(Pageable pageable);
Page<ComplaintEntity> findByStatus(ComplaintStatus status, Pageable pageable);
List<ComplaintEntity> findByViolationId(Long violationId);
boolean existsByViolationId(Long violationId);
@Modifying
@Transactional
@Query("UPDATE ComplaintEntity c SET c.status = :status WHERE c.id = :id")
int updateStatus(Long id, ComplaintStatus status);
}
@@ -0,0 +1,28 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.file.FileAppeal;
import ru.soune.nocopy.entity.file.AppealStatus;
import java.util.List;
import java.util.Optional;
public interface FileAppealRepository extends JpaRepository<FileAppeal, String> {
Optional<FileAppeal> findByFileIdAndStatus(String fileId, AppealStatus status);
List<FileAppeal> findByFileId(String fileId);
Page<FileAppeal> findByStatus(AppealStatus status, Pageable pageable);
Page<FileAppeal> findByUserId(Long userId, Pageable pageable);
@Query("SELECT fa FROM FileAppeal fa WHERE fa.status = :status ORDER BY fa.createdAt ASC")
Page<FileAppeal> findPendingAppeals(@Param("status") AppealStatus status, Pageable pageable);
boolean existsByFileIdAndStatusIn(String fileId, List<AppealStatus> statuses);
}
@@ -1,5 +1,7 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
@@ -8,6 +10,7 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -21,6 +24,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findByUserIdAndStatus(Long userId, FileStatus status);
List<FileEntity> findByUserIdAndStatusIn(Long userId, Collection<FileStatus> statuses);
Optional<FileEntity> findByFilePath(String filePath); Optional<FileEntity> findByFilePath(String filePath);
Optional<FileEntity> findByUploadSessionId(String uploadSessionId); Optional<FileEntity> findByUploadSessionId(String uploadSessionId);
@@ -53,12 +58,6 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
List<FileEntity> findFileByUserIdAndStatus(Long userId, FileStatus status); List<FileEntity> findFileByUserIdAndStatus(Long userId, FileStatus status);
@Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.mimeType = :mimeType")
List<FileEntity> findByUserIdAndMimeType(Long userId, String mimeType);
@Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType")
List<FileEntity> findByMimeType(String mimeType);
@Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds") @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds")
List<FileEntity> findByMimeTypeAndUserIds(String mimeType, List<Long> userIds); List<FileEntity> findByMimeTypeAndUserIds(String mimeType, List<Long> userIds);
@@ -66,4 +65,8 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
Integer findMaxSupportId(); Integer findMaxSupportId();
List<FileEntity> findByThumbnailPathIsNull(); List<FileEntity> findByThumbnailPathIsNull();
Page<FileEntity> findByStatusIn(List<FileStatus> statuses, Pageable pageable);
List<FileEntity> findByUserIdAndStatusIn(Long userId, List<FileStatus> statuses);
} }
@@ -22,6 +22,7 @@ public interface ImageSimilarityRepository
f.created_at AS uploadDate, f.created_at AS uploadDate,
f.protection_status AS protectionStatus, f.protection_status AS protectionStatus,
f.file_extension AS extension, f.file_extension AS extension,
f.status AS fileStatus,
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo h.hash64_lo AS hash64Lo
FROM image_hashes ref FROM image_hashes ref
@@ -30,7 +31,7 @@ public interface ImageSimilarityRepository
JOIN file_entities f JOIN file_entities f
ON f.id = h.file_id ON f.id = h.file_id
WHERE ref.file_id = :fileId WHERE ref.file_id = :fileId
AND f.status = 'ACTIVE' AND f.status IN ('ACTIVE', 'MODERATION')
""", """,
nativeQuery = true) nativeQuery = true)
List<SimilarImageProjection> findCandidates( List<SimilarImageProjection> findCandidates(
@@ -0,0 +1,13 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import java.util.List;
public interface ModerationLogRepository extends JpaRepository<ModerationLog, String> {
List<ModerationLog> findByFileIdOrderByCreatedAtDesc(String fileId);
List<ModerationLog> findByModeratorId(Long moderatorId);
}
@@ -0,0 +1,40 @@
package ru.soune.nocopy.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User;
import java.util.List;
public interface NotificationRepository extends JpaRepository<Notification, Long> {
Page<Notification> findByUser(User user, Pageable pageable);
Page<Notification> findByUserAndNotificationTypeIn(User user, List<NotificationType> types, Pageable pageable);
Page<Notification> findByUserAndStatusIn(User user, List<NotificationStatus> statuses, Pageable pageable);
Page<Notification> findByUserAndNotificationTypeInAndStatusIn(User user, List<NotificationType> types,
List<NotificationStatus> statuses,
Pageable pageable);
List<Notification> findTop5ByUserAndStatusOrderByCreatedAtDesc(User user, NotificationStatus status);
long countByUserAndStatus(User user, NotificationStatus status);
@Modifying
@Query("UPDATE Notification n SET n.status = :newStatus WHERE n.id IN :ids AND n.user = :user AND " +
"n.status = :oldStatus")
int updateStatus(@Param("ids") List<Long> ids,
@Param("user") User user,
@Param("oldStatus") NotificationStatus oldStatus,
@Param("newStatus") NotificationStatus newStatus);
Page<Notification> getNotificationsByUser(User user, Pageable pageable);
}
@@ -15,4 +15,5 @@ public interface SimilarImageProjection {
LocalDateTime getUploadDate(); LocalDateTime getUploadDate();
ProtectionStatus getProtectionStatus(); ProtectionStatus getProtectionStatus();
String getExtension(); String getExtension();
String getFileStatus();
} }
@@ -3,6 +3,7 @@ package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import java.util.List; import java.util.List;
@@ -13,5 +14,6 @@ public interface TariffRepository extends JpaRepository<Tariff, Long> {
Optional<Tariff> findByType(String type); Optional<Tariff> findByType(String type);
List<Tariff> findAllByOrderByPriceAsc(); List<Tariff> findAllByOrderByPriceAsc();
List<Tariff> findByTariffAccountType(String typeAccount); List<Tariff> findByTariffAccountType(String typeAccount);
List<Tariff> findByTariffAccountTypeAndTariffTerm(String typeAccount, TariffTimeTerm tariffTerm);
Tariff findByName(String name); Tariff findByName(String name);
} }
@@ -0,0 +1,9 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.user.UserAdditionalInfo;
@Repository
public interface UserAdditionalInfoRepository extends JpaRepository<UserAdditionalInfo, Long> {
}
@@ -7,12 +7,16 @@ import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.service.monitoring.MonitoringSearchService; import ru.soune.nocopy.service.monitoring.MonitoringSearchService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.notification.NotificationService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@Component @Component
@@ -23,6 +27,8 @@ public class MonitoringScheduler {
private final MonitoringSearchService monitoringSearchService; private final MonitoringSearchService monitoringSearchService;
private final NotificationService notificationService;
@Scheduled(cron = "0 1 2 * * *", zone = "Europe/Moscow") @Scheduled(cron = "0 1 2 * * *", zone = "Europe/Moscow")
@Transactional @Transactional
public void processScheduledMonitoring() { public void processScheduledMonitoring() {
@@ -30,6 +36,8 @@ public class MonitoringScheduler {
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
List<FileMonitoringEntity> readyForRun = monitoringRepository.findReadyForRun(now); List<FileMonitoringEntity> readyForRun = monitoringRepository.findReadyForRun(now);
Map<Long, List<FileMonitoringEntity>> groupingFilesMonitoring = readyForRun.stream()
.collect(Collectors.groupingBy(FileMonitoringEntity::getUserId));
log.info("Found {} files ready for monitoring", readyForRun.size()); log.info("Found {} files ready for monitoring", readyForRun.size());
@@ -44,5 +52,10 @@ public class MonitoringScheduler {
monitoringRepository.save(monitoring); monitoringRepository.save(monitoring);
} }
} }
Set<Long> usersForNotifications = groupingFilesMonitoring.keySet();
for (Long userId : usersForNotifications) {
notificationService.addNotification(NotificationType.MONITORING_RESULT, userId);
}
} }
} }
@@ -12,10 +12,12 @@ import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.file.SimilarFileDTO; import ru.soune.nocopy.dto.file.SimilarFileDTO;
import ru.soune.nocopy.entity.company.Company; import ru.soune.nocopy.entity.company.Company;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.repository.*; import ru.soune.nocopy.repository.*;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import java.io.File; import java.io.File;
@@ -40,6 +42,8 @@ public class FileSimilarityService {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Value("${server.baseurl}") @Value("${server.baseurl}")
private String baseUrl; private String baseUrl;
@@ -154,22 +158,19 @@ public class FileSimilarityService {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId)); .orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId));
List<SimilarImageProjection> candidates;
List<Long> userIds; List<Long> userIds;
// if (authToken == null || "all".equals(authToken)) { List<SimilarImageProjection> candidates = repository.findCandidates(fileId);
candidates = repository.findCandidates(fileId);
// } else {
Long userId = authTokenRepository.findUserIdByToken(authToken); Long userId = authTokenRepository.findUserIdByToken(authToken);
User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found")); User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found"));
Company company = user.getCompany(); Company company = user.getCompany();
if (company != null) { if (company != null) {
userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toList()); userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toList());
// candidates = repository.findCandidatesFromUserFiles(fileId, userIds);
} else { } else {
userIds = List.of(userId); userIds = List.of(userId);
// candidates = repository.findCandidatesFromUserFiles(fileId, userId);
} }
// }
List<String> levels = (similarityLevels != null && !similarityLevels.isEmpty()) List<String> levels = (similarityLevels != null && !similarityLevels.isEmpty())
? similarityLevels ? similarityLevels
@@ -180,6 +181,7 @@ public class FileSimilarityService {
imageHashEntity.getHash64Hi(), imageHashEntity.getHash64Hi(),
imageHashEntity.getHash64Lo(), userIds)) imageHashEntity.getHash64Lo(), userIds))
.filter(dto -> levels.contains(dto.getSimilarityLevel())) .filter(dto -> levels.contains(dto.getSimilarityLevel()))
.filter(dto -> !(dto.getFileStatus().equals(FileStatus.MODERATION.name()) && !dto.getOwner()))
.sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance))
.collect(Collectors.toList()); .collect(Collectors.toList());
@@ -199,8 +201,9 @@ public class FileSimilarityService {
private String calculateFileHash(String path) throws Exception { private String calculateFileHash(String path) throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256"); MessageDigest digest = MessageDigest.getInstance("SHA-256");
File file = cloudStorageService.readFileFromStorageByPath(path);
try (InputStream is = new FileInputStream(path)) { try (InputStream is = new FileInputStream(file)) {
byte[] buffer = new byte[8192]; byte[] buffer = new byte[8192];
int read; int read;
while ((read = is.read(buffer)) > 0) { while ((read = is.read(buffer)) > 0) {
@@ -228,6 +231,9 @@ public class FileSimilarityService {
level = "DIFFERENT"; level = "DIFFERENT";
} }
boolean owner = userIds.contains(similarImageProjection.getUserId());
return SimilarFileDTO.builder() return SimilarFileDTO.builder()
.fileId(similarImageProjection.getId()) .fileId(similarImageProjection.getId())
.ownerId(similarImageProjection.getUserId()) .ownerId(similarImageProjection.getUserId())
@@ -240,9 +246,10 @@ public class FileSimilarityService {
.supportId(similarImageProjection.getSupportId()) .supportId(similarImageProjection.getSupportId())
.uploadDate(similarImageProjection.getUploadDate()) .uploadDate(similarImageProjection.getUploadDate())
.status(similarImageProjection.getProtectionStatus()) .status(similarImageProjection.getProtectionStatus())
.owner(userIds.contains(similarImageProjection.getUserId())) .owner(owner)
// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) // .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail")
.fileStatus(similarImageProjection.getFileStatus())
.build(); .build();
} }
@@ -255,6 +262,7 @@ public class FileSimilarityService {
"." + fileEntity.getFileExtension()) "." + fileEntity.getFileExtension())
.fileSize(fileEntity.getFileSize()) .fileSize(fileEntity.getFileSize())
.uploadDate(fileEntity.getCreatedAt()) .uploadDate(fileEntity.getCreatedAt())
.fileStatus(fileEntity.getStatus().name())
.status(fileEntity.getProtectionStatus()) .status(fileEntity.getProtectionStatus())
.supportId(Long.valueOf(fileEntity.getSupportId())) .supportId(Long.valueOf(fileEntity.getSupportId()))
.build(); .build();
@@ -8,6 +8,7 @@ import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ImageHashEntity; import ru.soune.nocopy.entity.file.ImageHashEntity;
import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -22,9 +23,13 @@ public class ImageHashService {
private final ImageHashRepository repository; private final ImageHashRepository repository;
private final CloudStorageService cloudStorageService;
public Map<String, Long> calculateHash(Path imagePath) throws IOException { public Map<String, Long> calculateHash(Path imagePath) throws IOException {
// File file = cloudStorageService.readFileFromStorageByPath(imagePath.toString());
File file = imagePath.toFile(); File file = imagePath.toFile();
PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file); PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file);
Long firstPart = pHash.getFirstPart(); Long firstPart = pHash.getFirstPart();
@@ -0,0 +1,136 @@
package ru.soune.nocopy.service.complaint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.complaint.ComplaintRequest;
import ru.soune.nocopy.dto.complaint.ComplaintResponse;
import ru.soune.nocopy.entity.complaint.ComplaintEntity;
import ru.soune.nocopy.entity.complaint.ComplaintStatus;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.violation.Violation;
import ru.soune.nocopy.exception.ComplaintNotFoundException;
import ru.soune.nocopy.exception.DuplicateComplaintException;
import ru.soune.nocopy.exception.ViolationNotFoundException;
import ru.soune.nocopy.repository.ComplaintEntityRepository;
import ru.soune.nocopy.repository.ViolationRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.violation.ViolationService;
import ru.soune.nocopy.service.violation.ViolationStatus;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class ComplaintEntityService {
private final ComplaintEntityRepository complaintRepository;
private final ViolationRepository violationRepository;
private final ViolationService violationService;
private final NotificationService notificationService;
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public ComplaintResponse createComplaint(ComplaintRequest request) {
Violation violation = violationRepository.findById(request.getViolationId())
.orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId()));
if (complaintRepository.existsByViolationId(request.getViolationId())) {
throw new DuplicateComplaintException("Complaint already exists for violation: " + request.getViolationId());
}
ComplaintEntity complaint = ComplaintEntity.builder()
.status(ComplaintStatus.CREATED)
.complaintText(request.getComplaintText())
.email(request.getEmail())
.violation(violation)
.not_moderated_file(violation.getFileEntity().getStatus().equals(FileStatus.MODERATION))
.build();
violationService.changeStatus(ViolationStatus.COMPLAINT_IN_WORK, request.getViolationId());
return mapToResponse(complaintRepository.save(complaint));
}
@Transactional(readOnly = true)
public ComplaintResponse getComplaintById(Long id) {
return complaintRepository.findById(id)
.map(this::mapToResponse)
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
}
@Transactional(readOnly = true)
public Page<ComplaintResponse> getAllComplaints(Pageable pageable) {
return complaintRepository.findAll(pageable).map(this::mapToResponse);
}
@Transactional(readOnly = true)
public List<ComplaintResponse> getComplaintsByViolationId(Long violationId) {
if (!violationRepository.existsById(violationId)) {
throw new ViolationNotFoundException("Violation not found: " + violationId);
}
return complaintRepository.findByViolationId(violationId)
.stream()
.map(this::mapToResponse)
.collect(Collectors.toList());
}
public ComplaintResponse updateComplaintStatus(Long id, ComplaintStatus status) {
ComplaintEntity complaint = complaintRepository.findById(id)
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
complaint.setStatus(status);
return mapToResponse(complaintRepository.save(complaint));
}
public ComplaintResponse updateComplaint(Long id, ComplaintRequest request) {
ComplaintEntity complaint = complaintRepository.findById(id)
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
if (request.getViolationId() != null) {
Violation violation = violationRepository.findById(request.getViolationId())
.orElseThrow(() -> new ViolationNotFoundException("Violation not found: " + request.getViolationId()));
complaint.setViolation(violation);
}
if (request.getComplaintText() != null) complaint.setComplaintText(request.getComplaintText());
if (request.getEmail() != null) complaint.setEmail(request.getEmail());
if (request.getStatus() != null) {
complaint.setStatus(ComplaintStatus.valueOf(request.getStatus()));
notificationService.addNotification(NotificationType.COMPLAINT_STATUS_CHANGED, complaint.getViolation()
.getFileEntity().getUserId(), request.getStatus());
}
return mapToResponse(complaintRepository.save(complaint));
}
public ComplaintResponse deleteComplaint(Long id) {
ComplaintEntity complaint = complaintRepository.findById(id)
.orElseThrow(() -> new ComplaintNotFoundException("Complaint not found: " + id));
complaint.setStatus(ComplaintStatus.CANCELLED);
return mapToResponse(complaintRepository.save(complaint));
}
private ComplaintResponse mapToResponse(ComplaintEntity entity) {
return ComplaintResponse.builder()
.id(entity.getId())
.status(entity.getStatus() != null ? entity.getStatus().name() : null)
.complaintText(entity.getComplaintText())
.email(entity.getEmail())
.violationId(entity.getViolation() != null ? entity.getViolation().getId() : null)
.createdAt(entity.getCreatedAt() != null ? entity.getCreatedAt().format(DATE_FORMATTER) : null)
.updatedAt(entity.getUpdatedAt() != null ? entity.getUpdatedAt().format(DATE_FORMATTER) : null)
.notModerated(entity.getNot_moderated_file() != null ? entity.getNot_moderated_file() : null)
.build();
}
}
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
import ru.soune.nocopy.entity.user.ProtectedFileCheck; import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository; import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
@@ -15,7 +16,10 @@ import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j @Slf4j
@Component @Component
@@ -34,6 +38,8 @@ public class FileCleanupService {
public void cleanupExpiredFiles() { public void cleanupExpiredFiles() {
log.info("Starting cleanup of expired temporary files"); log.info("Starting cleanup of expired temporary files");
cleanupTempFilesNightly();
Path tempDir = Paths.get(basePath, "temp"); Path tempDir = Paths.get(basePath, "temp");
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
return; return;
@@ -83,4 +89,50 @@ public class FileCleanupService {
log.error("Error during cleanup", e); log.error("Error during cleanup", e);
} }
} }
private void cleanupTempFilesNightly() {
log.info("Starting nightly cleanup of temporary files");
String tempDir = System.getProperty("java.io.tmpdir");
Path tempPath = Paths.get(tempDir);
if (!Files.exists(tempPath)) {
return;
}
Instant cutoffTime = Instant.now().minus(24, ChronoUnit.HOURS); // 24 часа
int deletedCount = 0;
try (Stream<Path> files = Files.list(tempPath)) {
List<Path> tempFiles = files
.filter(path -> {
String filename = path.getFileName().toString();
return filename.contains("_") &&
!filename.endsWith(".tmp") &&
Files.isRegularFile(path);
})
.collect(Collectors.toList());
for (Path filePath : tempFiles) {
try {
File file = filePath.toFile();
Instant lastModified = Instant.ofEpochMilli(file.lastModified());
if (lastModified.isBefore(cutoffTime)) {
boolean deleted = Files.deleteIfExists(filePath);
if (deleted) {
deletedCount++;
}
}
} catch (IOException e) {
log.error("Error deleting file: {}", filePath, e);
}
}
log.info("Nightly cleanup deleted {} temporary files", deletedCount);
} catch (IOException e) {
log.error("Failed to list temp directory", e);
}
}
} }
@@ -9,22 +9,18 @@ import ru.soune.nocopy.dto.file.FileEntityResponse;
import ru.soune.nocopy.dto.file.FileResponse; import ru.soune.nocopy.dto.file.FileResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.monitoring.MonitoringType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.handler.MonitoringHandler;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
@@ -32,7 +28,6 @@ import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -43,74 +38,15 @@ public class FileEntityService {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final ImageHashService imageHashService;
private final FileSimilarityService fileSimilarityService;
private final UserRepository userRepository; private final UserRepository userRepository;
private final ImageResizeService imageResizeService; private final ImageResizeService imageResizeService;
private final CloudStorageService cloudStorageService;
@Value("${server.baseurl}") @Value("${server.baseurl}")
private String baseUrl; private String baseUrl;
@Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
log.info("Creating FileEntity for upload session: {}", session.getUploadId());
try {
Path filePath = Paths.get(session.getFilePath());
if (!Files.exists(filePath)) {
throw new IOException("File not found on disk: " + filePath);
}
Map<String, Long> imageHash = Map.of();
if (session.getFileType().startsWith("image")) {
imageHash = imageHashService.calculateHash(filePath);
List<SimilarImageProjection> duplicatedByHash = fileSimilarityService.findDuplicatedByHash(
imageHash.get("hi"), imageHash.get("low"));
if (!duplicatedByHash.isEmpty()) {
SimilarImageProjection similarImageProjection = duplicatedByHash.get(0);
throw new DuplicateImageException("Duplicate", similarImageProjection.getId(),
similarImageProjection.getUserId());
}
}
long fileSize = Files.size(filePath);
String originalName = session.getFileName();
String storedName = filePath.getFileName().toString();
FileEntity fileEntity = FileEntity.builder()
.userId(session.getUserId())
.originalFileName(originalName)
.storedFileName(storedName)
.filePath(session.getFilePath())
.fileSize(fileSize)
.mimeType(session.getFileType())
.fileExtension(session.getExtension())
.checksum(checksum)
.uploadSessionId(session.getUploadId())
.status(FileStatus.ACTIVE)
.build();
FileEntity saved = fileEntityRepository.save(fileEntity);
if (!imageHash.isEmpty()) {
imageHashService.create(saved, imageHash);
}
return saved;
} catch (IOException e) {
log.error("Failed to create FileEntity for session {}: {}",
session.getUploadId(), e.getMessage(), e);
throw new RuntimeException("Failed to create file entity: " + e.getMessage(), e);
}
}
@Transactional(readOnly = true) @Transactional(readOnly = true)
public FileEntityResponse getById(String fileId, int version) { public FileEntityResponse getById(String fileId, int version) {
FileEntity fileEntity = fileEntityRepository.findById(fileId) FileEntity fileEntity = fileEntityRepository.findById(fileId)
@@ -145,10 +81,12 @@ public class FileEntityService {
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
.map(User::getId) .map(User::getId)
.toList()) { .toList()) {
allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE)); allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)));
} }
} else { } else {
allFiles = fileEntityRepository.findByUserIdAndStatus(userId, FileStatus.ACTIVE); allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
} }
return allFiles; return allFiles;
@@ -163,12 +101,13 @@ public class FileEntityService {
for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream()
.map(User::getId) .map(User::getId)
.toList()) { .toList()) {
allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE)); allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId,
List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION)));
} }
} else { } else {
allFiles = fileEntityRepository.findByUserIdAndStatus( allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId,
userId, FileStatus.ACTIVE); List.of(FileStatus.ACTIVE, FileStatus.BLOCKED, FileStatus.MODERATION));
} }
int start = (page - 1) * pageSize; int start = (page - 1) * pageSize;
@@ -205,45 +144,24 @@ public class FileEntityService {
.build(); .build();
} }
@Transactional
public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException {
fileEntity.setStatus(FileStatus.DELETED);
fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
Path path = Paths.get(fileEntity.getProtectedFilePath());
Files.deleteIfExists(path);
fileEntity.setProtectedFilePath("");
return fileEntityRepository.save(fileEntity);
}
public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException {
if (fileEntity.getImageHash() != null) { if (fileEntity.getImageHash() != null) {
fileEntity.setImageHash(null); fileEntity.setImageHash(null);
} }
fileEntity.setStatus(FileStatus.DELETED); fileEntity.setStatus(FileStatus.REMOVED);
fileEntity.setUpdatedAt(LocalDateTime.now()); fileEntity.setUpdatedAt(LocalDateTime.now());
fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); // fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED);
Path path = Paths.get(fileEntity.getProtectedFilePath()); // Path path = Paths.get(fileEntity.getProtectedFilePath());
Files.deleteIfExists(path); // Files.deleteIfExists(path);
fileEntity.setProtectedFilePath(""); // fileEntity.setProtectedFilePath("");
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public void deleteFromDisk(FileEntity fileEntity) throws IOException { public void deleteFromDisk(String deleteFilePath) throws IOException {
Path path = Paths.get(fileEntity.getFilePath()); Files.deleteIfExists(Paths.get(deleteFilePath));
if (!Files.exists(path)) {
return;
}
Files.delete(path);
fileEntityRepository.delete(fileEntity);
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -252,7 +170,7 @@ public class FileEntityService {
return totalSize != null ? totalSize : 0L; return totalSize != null ? totalSize : 0L;
} }
public void changeStatus(ProtectionStatus newStatus, String fileId) { public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) {
FileEntity fileEntity = fileEntityRepository.findById(fileId) FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId)); .orElseThrow(() -> new FileEntityNotFoundException(fileId));
fileEntity.setProtectionStatus(newStatus); fileEntity.setProtectionStatus(newStatus);
@@ -260,8 +178,16 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public void changeFileStatus(FileStatus newStatus, String fileId) {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
fileEntity.setStatus(newStatus);
fileEntityRepository.save(fileEntity);
}
@Transactional @Transactional
public void writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { public FileEntity writeProtectedFile(String id, byte[] data, String fileExt) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(id) FileEntity fileEntity = fileEntityRepository.findById(id)
.orElseThrow(() -> new RuntimeException("File not found: " + id)); .orElseThrow(() -> new RuntimeException("File not found: " + id));
@@ -284,14 +210,14 @@ public class FileEntityService {
fileEntity.setFileExtension(extension); fileEntity.setFileExtension(extension);
fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED); fileEntity.setProtectionStatus(ProtectionStatus.PROTECTED);
fileEntityRepository.save(fileEntity); return fileEntityRepository.save(fileEntity);
} }
public void clearTempFiles(long userId) throws IOException { public void clearTempFiles(long userId) {
List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP); List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP);
for (FileEntity fileEntity : fileByUserIdAndStatus) { for (FileEntity fileEntity : fileByUserIdAndStatus) {
deleteFromDisk(fileEntity); cloudStorageService.deleteFromStorageDisk(fileEntity);
} }
} }
@@ -303,24 +229,6 @@ public class FileEntityService {
return fileEntityRepository.findFileIdByFilePath(filePath); return fileEntityRepository.findFileIdByFilePath(filePath);
} }
public File getFileById(String id) {
try {
FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
File file = new File(fileEntity.getFilePath());
if (!file.exists()) {
throw new RuntimeException("File not found on disk: " + fileEntity.getFilePath());
}
return file;
} catch (Exception e) {
log.error("Error getting file: {}", id, e);
return null;
}
}
public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException { public Path prepareProtectedPath(FileEntity fileEntity, String extension) throws IOException {
Path originalPath = Paths.get(fileEntity.getFilePath()); Path originalPath = Paths.get(fileEntity.getFilePath());
@@ -357,6 +265,18 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
private boolean checkFileExistsOnDisk(String filePath) { private boolean checkFileExistsOnDisk(String filePath) {
try { try {
return Files.exists(Paths.get(filePath)); return Files.exists(Paths.get(filePath));
@@ -424,16 +344,4 @@ public class FileEntityService {
.thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail") .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail")
.build(); .build();
} }
public String formatFileSize(long size) {
if (size < 1024) {
return size + " B";
} else if (size < 1024 * 1024) {
return String.format("%.1f KB", size / 1024.0);
} else if (size < 1024 * 1024 * 1024) {
return String.format("%.1f MB", size / (1024.0 * 1024.0));
} else {
return String.format("%.1f GB", size / (1024.0 * 1024.0 * 1024.0));
}
}
} }
@@ -2,11 +2,10 @@ package ru.soune.nocopy.service.file;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import java.io.IOException;
public interface FileUploadService { public interface FileUploadService {
FileUploadSession initUpload(Long userId, String fileName, FileUploadSession initUpload(Long userId, String fileName,
String fileType, String extension, String convertTo, long fileSize); String fileType, String extension, String convertTo, long fileSize);
@@ -19,5 +18,5 @@ public interface FileUploadService {
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
void completeFileProcessingAsync(FileUploadSession session, FileStatus status); FileEntity completeFileProcessing(FileUploadSession session, FileStatus status);
} }
@@ -0,0 +1,143 @@
package ru.soune.nocopy.service.file.cloud;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.AwsCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.http.apache.ApacheHttpClient;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
@Service
@Slf4j
@RequiredArgsConstructor
public class CloudStorageService {
private final FileEntityRepository fileEntityRepository;
@Value("${yandex.cloud.key}")
private String key;
@Value("${yandex.cloud.secret-key}")
private String secretKey;
@Value("${yandex.cloud.region}")
private String region;
@Value("${yandex.cloud.s3-endpoint}")
private String s3endpoint;
@Value("${yandex.cloud.bucket}")
private String bucket;
public void saveFilesToStorage(String mimeType, String extension, String sendToCloudFilePath) {
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String mimeTypeFromFile = mimeType.equals("document") ? "application" : mimeType;
String contentType = mimeTypeFromFile + "/" + extension;
String filePath = "files" + sendToCloudFilePath;
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucket)
.key(filePath)
.contentType(contentType)
.build();
try {
s3Client.putObject(putObjectRequest,
RequestBody.fromBytes(Files.readAllBytes(Paths.get(sendToCloudFilePath))));
Files.deleteIfExists(Paths.get(sendToCloudFilePath));
} catch (IOException e) {
log.error("Failed to read file: {}", filePath, e);
}
}
public File readFileFromStorageByPath(String filePath) throws IOException {
GetObjectRequest objectRequest = GetObjectRequest.builder()
.bucket(bucket)
.key("files" + filePath)
.build();
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String tempDir = System.getProperty("java.io.tmpdir");
String fileName = filePath.replace("/", "_");
File tempFile = new File(tempDir, fileName);
s3Client.getObject(objectRequest, tempFile.toPath());
return tempFile;
}
public void deleteFileFromStorage(String filePath) {
if (filePath == null || filePath.isEmpty()) {
log.warn("Attempted to delete file with empty path");
return;
}
AwsCredentials credentials = AwsBasicCredentials.create(key, secretKey);
S3Client s3Client = S3Client.builder()
.httpClient(ApacheHttpClient.create())
.region(Region.of(region))
.endpointOverride(URI.create(s3endpoint))
.credentialsProvider(StaticCredentialsProvider.create(credentials))
.build();
String cloudKey = "files" + filePath;
try {
try {
s3Client.headObject(builder -> builder.bucket(bucket).key(cloudKey).build());
} catch (Exception e) {
log.warn("File does not exist in cloud storage: {}", cloudKey);
return;
}
s3Client.deleteObject(builder -> builder.bucket(bucket).key(cloudKey).build());
log.info("File successfully deleted from cloud storage: {}", cloudKey);
} catch (Exception e) {
log.error("Failed to delete file from cloud storage: {}", cloudKey, e);
throw new RuntimeException("Failed to delete file from cloud storage: " + cloudKey, e);
}
}
public void deleteFromStorageDisk(FileEntity fileEntity) {
deleteFileFromStorage(fileEntity.getFilePath());
if (fileEntity.getProtectedFilePath() != null) {
deleteFileFromStorage(fileEntity.getProtectedFilePath());
}
fileEntityRepository.delete(fileEntity);
}
}
@@ -6,6 +6,7 @@ import com.vrt.fileprotection.NoCopyCheckResult;
import com.vrt.fileprotection.audio.AudioCheckResult; import com.vrt.fileprotection.audio.AudioCheckResult;
import com.vrt.fileprotection.documents.DocumentCheckResult; import com.vrt.fileprotection.documents.DocumentCheckResult;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -17,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.*; import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
@@ -26,6 +28,9 @@ import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.cost.CostService; import ru.soune.nocopy.service.cost.CostService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
@@ -84,6 +89,10 @@ public class FileUploadServiceImpl implements FileUploadService {
private final TariffInfoService tariffInfoService; private final TariffInfoService tariffInfoService;
private final NotificationService notificationService;
private final CloudStorageService cloudStorageService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -179,7 +188,6 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
@Override @Override
@Transactional
public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber, public UploadProgressResponse uploadChunk(String uploadId, Integer chunkNumber,
MultipartFile chunkFile, Integer findSimilar) { MultipartFile chunkFile, Integer findSimilar) {
FileUploadSession session = sessionRepository.findById(uploadId) FileUploadSession session = sessionRepository.findById(uploadId)
@@ -218,10 +226,9 @@ public class FileUploadServiceImpl implements FileUploadService {
return UploadProgressResponse.fromSession(session); return UploadProgressResponse.fromSession(session);
} }
@Async("fileUploadTaskExecutor")
@Transactional @Transactional
@Override @Override
public void completeFileProcessingAsync(FileUploadSession session, FileStatus status) { public FileEntity completeFileProcessing(FileUploadSession session, FileStatus status) {
try { try {
Path filePath = Paths.get(session.getFilePath()); Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath); String checksum = calculateChecksum(filePath);
@@ -244,7 +251,6 @@ public class FileUploadServiceImpl implements FileUploadService {
if (session.getFileType().equals("image")) { if (session.getFileType().equals("image")) {
Map<String, Long> hash = imageHashService.calculateHash(filePath); Map<String, Long> hash = imageHashService.calculateHash(filePath);
imageHashService.create(saved, hash); imageHashService.create(saved, hash);
} }
@@ -252,15 +258,16 @@ public class FileUploadServiceImpl implements FileUploadService {
if (status != FileStatus.TEMP) { if (status != FileStatus.TEMP) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo()); FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
noCopyFileService.addFile(fileInfo); noCopyFileService.addFile(fileInfo);
} }
log.info("File processing completed for session: {}", session.getUploadId()); log.info("File processing completed for session: {}", session.getUploadId());
return saved;
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to complete file processing for session {}: {}", log.error("Failed to complete file processing for session {}: {}",
session.getUploadId(), e.getMessage(), e); session.getUploadId(), e.getMessage(), e);
throw new FileUploadException("Failed to complete file processing", e);
} }
} }
@@ -293,6 +300,9 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
@Autowired
private EntityManager entityManager;
private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile, private UploadProgressResponse processChunk(FileUploadSession session, Integer chunkNumber, MultipartFile chunkFile,
Integer findSimilar) { Integer findSimilar) {
String chunkPath = null; String chunkPath = null;
@@ -313,7 +323,7 @@ public class FileUploadServiceImpl implements FileUploadService {
chunkNumber, session.getChunksUploaded(), session.getTotalChunks()); chunkNumber, session.getChunksUploaded(), session.getTotalChunks());
boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks()); boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks());
//TODO CHECK
if (isLastChunk) { if (isLastChunk) {
String finalFilePath = assembleFileSynchronously(session); String finalFilePath = assembleFileSynchronously(session);
@@ -324,7 +334,8 @@ public class FileUploadServiceImpl implements FileUploadService {
fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(), fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(),
session.getUserId()); session.getUserId());
File file = new File(finalFilePath); // File file = new File(finalFilePath);
File file = cloudStorageService.readFileFromStorageByPath(finalFilePath);
if (session.getFileType().startsWith("audio") && !isWavFile(file)) { if (session.getFileType().startsWith("audio") && !isWavFile(file)) {
session.setStatus(UploadStatus.FAILED); session.setStatus(UploadStatus.FAILED);
@@ -364,14 +375,24 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath); session.setFilePath(finalFilePath);
FileStatus status = findSimilar == 0 ? FileStatus.ACTIVE: FileStatus.TEMP; notificationService.addNotification(NotificationType.FILE_ADDED_TO_SYSTEM, session.getUserId());
FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP;
if (status == FileStatus.TEMP) { if (status == FileStatus.TEMP) {
fileEntityService.clearTempFiles(session.getUserId()); fileEntityService.clearTempFiles(session.getUserId());
} }
completeFileProcessingAsync(session, status); FileEntity fileEntity = completeFileProcessing(session, status);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getFilePath());
if (status != FileStatus.TEMP) {
FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo());
noCopyFileService.addFile(fileInfo);
}
if (status != FileStatus.TEMP) { if (status != FileStatus.TEMP) {
String fileType = session.getFileType(); String fileType = session.getFileType();
@@ -511,6 +532,7 @@ public class FileUploadServiceImpl implements FileUploadService {
private void checkForDuplicatesSynchronously(String filePath) private void checkForDuplicatesSynchronously(String filePath)
throws IOException , DuplicateImageException { throws IOException , DuplicateImageException {
Path path = Paths.get(filePath); Path path = Paths.get(filePath);
Map<String, Long> hash = imageHashService.calculateHash(path); Map<String, Long> hash = imageHashService.calculateHash(path);
List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash( List<SimilarImageProjection> duplicates = fileSimilarityService.findDuplicatedByHash(
@@ -16,21 +16,21 @@ public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingLis
@Override @Override
public void onStartProcessing(FileProtector.FileInfo fileInfo) { public void onStartProcessing(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId()); fileEntityService.changeProtectionStatus(ProtectionStatus.PROCESSING, fileInfo.getId());
} }
@Override @Override
public void onProcessingFailed(FileProtector.FileInfo fileInfo) { public void onProcessingFailed(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId()); fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED, fileInfo.getId());
} }
@Override @Override
public void onSavingFailed(FileProtector.FileInfo fileInfo) { public void onSavingFailed(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId()); fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId());
} }
@Override @Override
public void onFinish(FileProtector.FileInfo fileInfo) { public void onFinish(FileProtector.FileInfo fileInfo) {
fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());; fileEntityService.changeProtectionStatus(ProtectionStatus.PROTECTED, fileInfo.getId());;
} }
} }
@@ -10,8 +10,11 @@ import org.springframework.stereotype.Component;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.file.cloud.CloudStorageService;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -32,27 +35,53 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CloudStorageService cloudStorageService;
@Nullable @Nullable
@Override @Override
public File getImageFile(@NotNull String id) { public File getImageFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getVideoFile(@NotNull String id) { public File getVideoFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Nullable @Nullable
@Override @Override
public File getAudioFile(@NotNull String id) { public File getAudioFile(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
public @Nullable File getDocument(@NotNull String id) { public @Nullable File getDocument(@NotNull String id) {
return fileEntityService.getFileById(id); FileEntity fileEntity = fileEntityRepository.findById(id).orElseThrow(() ->
new RuntimeException("File not found: " + id));
try {
return cloudStorageService.readFileFromStorageByPath(fileEntity.getFilePath());
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
@@ -87,7 +116,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeAudioFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -99,7 +132,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeVideoFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -111,7 +148,11 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) { public com.vrt.fileprotection.OperationResult writeImageFile(@NotNull String id, @NotNull byte[] data, @Nullable String fileExt) {
try { try {
fileEntityService.writeProtectedFile(id, data, fileExt); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, fileExt);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -122,7 +163,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider {
@Override @Override
public @NotNull OperationResult writeDocumentFile(@NotNull String id, @NotNull byte[] data) { public @NotNull OperationResult writeDocumentFile(@NotNull String id, @NotNull byte[] data) {
try { try {
fileEntityService.writeProtectedFile(id, data, null); FileEntity fileEntity = fileEntityService.writeProtectedFile(id, data, null);
cloudStorageService.saveFilesToStorage(fileEntity.getMimeType(), fileEntity.getFileExtension(),
fileEntity.getProtectedFilePath());
return OperationResult.Companion.success(); return OperationResult.Companion.success();
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to create protected file: {}", id, e); log.error("Failed to create protected file: {}", id, e);
@@ -0,0 +1,244 @@
package ru.soune.nocopy.service.file.moderation;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.file.moderation.ModerationLog;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.InvalidAppealException;
import ru.soune.nocopy.repository.FileAppealRepository;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ModerationLogRepository;
import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.notification.NotificationService;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
@Service
@RequiredArgsConstructor
public class ModerationService {
private final FileEntityRepository fileEntityRepository;
private final FileAppealRepository fileAppealRepository;
private final ModerationLogRepository moderationLogRepository;
private final FileEntityService fileEntityService;
private final NotificationService notificationService;
private static final List<FileStatus> MODERATION_NEEDED_STATUSES = Arrays.asList(
FileStatus.MODERATION,
FileStatus.BLOCKED
);
/**
* Модерация файла администратором
*/
@Transactional
public void moderateFile(ModerationActionRequest request, Long moderatorId) {
FileEntity file = fileEntityRepository.findById(request.getFileId())
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
if (!MODERATION_NEEDED_STATUSES.contains(file.getStatus())) {
throw new IllegalStateException("File is not in moderation state. Current status: " + file.getStatus());
}
ModerationLog log = ModerationLog.builder()
.fileId(file.getId())
.moderatorId(moderatorId)
.oldStatus(file.getStatus())
.newStatus(request.getNewStatus())
.reason(request.getComment())
.comment(request.getComment())
.build();
moderationLogRepository.save(log);
file.setStatus(request.getNewStatus());
fileEntityRepository.save(file);
if (request.getNewStatus() == FileStatus.BLOCKED ||
request.getNewStatus() == FileStatus.VIOLATION) {
fileAppealRepository.findByFileIdAndStatus(file.getId(), AppealStatus.PENDING)
.ifPresent(appeal -> {
appeal.setStatus(AppealStatus.REJECTED);
appeal.setAdminComment(request.getComment());
appeal.setResolvedAt(LocalDateTime.now());
fileAppealRepository.save(appeal);
});
} else if (request.getNewStatus() == FileStatus.REMOVED) {
try {
fileEntityService.softDeleteFileWithHash(file);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
notificationService.addNotification(NotificationType.FILE_MODERATION_EVENT, file.getUserId());
}
/**
* Подача апелляции пользователем
*/
@Transactional
public AppealResponse submitAppeal(AppealRequest request, Long userId) {
FileEntity file = fileEntityRepository.findById(request.getFileId())
.orElseThrow(() -> new FileEntityNotFoundException(request.getFileId()));
if (!file.getUserId().equals(userId)) {
throw new SecurityException("User is not the owner of this file");
}
if (!canAppeal(file)) {
throw new InvalidAppealException("Cannot appeal this file. Current status: " + file.getStatus());
}
if (hasActiveAppeal(file.getId())) {
throw new InvalidAppealException("Active appeal already exists for this file");
}
FileAppeal appeal = FileAppeal.builder()
.fileId(file.getId())
.userId(userId)
.appealReason(request.getAppealReason())
.additionalInfo(request.getAdditionalInfo())
.status(AppealStatus.PENDING)
.moderationBeforeStatus(file.getStatus())
.build();
FileAppeal saved = fileAppealRepository.save(appeal);
return convertToResponse(saved);
}
/**
* Рассмотрение апелляции администратором
*/
@Transactional
public void reviewAppeal(String appealId, boolean approve, String comment, Long moderatorId) {
FileAppeal appeal = fileAppealRepository.findById(appealId)
.orElseThrow(() -> new RuntimeException("Appeal not found: " + appealId));
if (appeal.getStatus() != AppealStatus.PENDING) {
throw new IllegalStateException("Appeal already processed");
}
FileEntity file = fileEntityRepository.findById(appeal.getFileId())
.orElseThrow(() -> new FileEntityNotFoundException(appeal.getFileId()));
appeal.setStatus(approve ? AppealStatus.APPROVED : AppealStatus.REJECTED);
appeal.setAdminComment(comment);
appeal.setModeratorId(moderatorId);
appeal.setResolvedAt(LocalDateTime.now());
if (approve) {
file.setStatus(FileStatus.ACTIVE);
}
ModerationLog log = ModerationLog.builder()
.fileId(file.getId())
.moderatorId(moderatorId)
.oldStatus(file.getStatus())
.newStatus(file.getStatus())
.comment("Appeal review: " + comment)
.build();
moderationLogRepository.save(log);
fileEntityRepository.save(file);
fileAppealRepository.save(appeal);
}
/**
* Проверка, можно ли подать апелляцию на файл
*/
public boolean canAppeal(FileEntity file) {
return file.getStatus() == FileStatus.BLOCKED;
}
/**
* Проверка наличия активной апелляции
*/
public boolean hasActiveAppeal(String fileId) {
return fileAppealRepository.existsByFileIdAndStatusIn(
fileId,
Arrays.asList(AppealStatus.PENDING, AppealStatus.IN_REVIEW)
);
}
/**
* Получение информации о файле для модерации
*/
@Transactional(readOnly = true)
public FileModerationInfo getFileModerationInfo(String fileId) {
FileEntity file = fileEntityRepository.findById(fileId)
.orElseThrow(() -> new FileEntityNotFoundException(fileId));
FileAppeal activeAppeal = fileAppealRepository.findByFileIdAndStatus(
fileId, AppealStatus.PENDING).orElse(null);
return FileModerationInfo.builder()
.fileId(file.getId())
.fileName(file.getOriginalFileName())
.currentStatus(file.getStatus())
.uploadDate(file.getCreatedAt())
.hasActiveAppeal(activeAppeal != null)
.activeAppeal(activeAppeal != null ?
AppealInfo.builder()
.appealId(activeAppeal.getId())
.status(activeAppeal.getStatus().name())
.createdAt(activeAppeal.getCreatedAt())
.build() : null)
.build();
}
/**
* Получение списка файлов на модерации
*/
@Transactional(readOnly = true)
public Page<FileEntity> getFilesForModeration(int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return fileEntityRepository.findByStatusIn(
Arrays.asList(FileStatus.MODERATION, FileStatus.BLOCKED),
pageable
);
}
/**
* Получение апелляций пользователя
*/
@Transactional(readOnly = true)
public Page<AppealResponse> getUserAppeals(Long userId, int page, int size) {
Pageable pageable = PageRequest.of(page, size, Sort.by("createdAt").descending());
return fileAppealRepository.findByUserId(userId, pageable)
.map(this::convertToResponse);
}
private AppealResponse convertToResponse(FileAppeal appeal) {
return AppealResponse.builder()
.appealId(appeal.getId())
.fileId(appeal.getFileId())
.appealReason(appeal.getAppealReason())
.additionalInfo(appeal.getAdditionalInfo())
.status(appeal.getStatus().name())
.adminComment(appeal.getAdminComment())
.createdAt(appeal.getCreatedAt())
.resolvedAt(appeal.getResolvedAt())
.build();
}
}
@@ -3,6 +3,7 @@ package ru.soune.nocopy.service.monitoring;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.configuration.search.SearchProperties;
@@ -10,6 +11,8 @@ import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.monitoring.MonitoringType;
import ru.soune.nocopy.entity.notification.NotificationMessage;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
@@ -17,6 +20,7 @@ import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.FileMonitoringRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.search.SearchImageService; import ru.soune.nocopy.service.search.SearchImageService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.service.tariff.TariffService; import ru.soune.nocopy.service.tariff.TariffService;
@@ -48,7 +52,11 @@ public class MonitoringSearchService {
private final SearchProperties searchProperties; private final SearchProperties searchProperties;
@Transactional private final NotificationService notificationService;
private final MessageSource messageSource;
@Transactional(noRollbackFor = TariffNotFoundException.class)
public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException {
monitoring.setLastRun(LocalDateTime.now()); monitoring.setLastRun(LocalDateTime.now());
monitoring.setLastRunStatus("IN_PROGRESS"); monitoring.setLastRunStatus("IN_PROGRESS");
@@ -123,7 +131,8 @@ public class MonitoringSearchService {
} }
monitoring.setLastRunStatus("SUCCESS"); monitoring.setLastRunStatus("SUCCESS");
updateNextRun(monitoring);
monitoringRepository.save(monitoring);
if (uniqueResults.isEmpty()) { if (uniqueResults.isEmpty()) {
log.info("No results found for monitoring file {}", monitoring.getFile().getId()); log.info("No results found for monitoring file {}", monitoring.getFile().getId());
} }
@@ -134,17 +143,24 @@ public class MonitoringSearchService {
int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens();
emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens()); emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens());
monitoring.setLastRunStatus("ERROR: " + e.getMessage()); monitoring.setLastRunStatus("ERROR: " + e.getMessage());
updateNextRun(monitoring); updateNextRun(monitoring);
monitoringRepository.save(monitoring); monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.TOKEN_NOT_FOUND, user.getId(),
NotificationMessage.FILE_SEARCH.getMessageKey());
} catch (Exception e) { } catch (Exception e) {
User user = userRepository.findById(monitoring.getUserId()).orElseThrow();
log.error("Error processing monitoring search", e); log.error("Error processing monitoring search", e);
monitoring.setLastRunStatus("ERROR: " + e.getMessage()); monitoring.setLastRunStatus("ERROR: " + e.getMessage());
} finally {
updateNextRun(monitoring); updateNextRun(monitoring);
monitoringRepository.save(monitoring); monitoringRepository.save(monitoring);
notificationService.addNotification(NotificationType.SEARCH_OPERATION_FAILED, user.getId());
} }
} }
@@ -2,11 +2,115 @@ package ru.soune.nocopy.service.notification;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.context.MessageSource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.notification.Notification;
import ru.soune.nocopy.entity.notification.NotificationStatus;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.NotificationRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.user.UserService;
import java.util.List;
import java.util.Locale;
@Service @Service
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
public class NotificationService { public class NotificationService {
private final NotificationRepository notificationRepository;
private final UserRepository userRepository;
private final MessageSource messageSource;
@Transactional
public void addNotification(NotificationType notificationType, long userId, Object... args) {
Locale locale = Locale.forLanguageTag("ru");
String message = messageSource.getMessage(notificationType.getMessageKey(), args, locale);
Notification notification = Notification.builder()
.notificationType(notificationType)
.user(userRepository.findById(userId).orElseThrow())
.status(NotificationStatus.NEW)
.message(message)
.build();
notificationRepository.save(notification);
}
@Transactional
public void deleteNotification(Notification notification) {
notificationRepository.delete(notification);
}
/**
Получение всех нотификаций с сортировкой
*/
@Transactional
public Page<Notification> allUserNotifications(User user, int page, int size, String propertyToSort) {
Pageable pageable = PageRequest.of(page, size, Sort.by(propertyToSort).descending());
return notificationRepository.getNotificationsByUser(user, pageable);
}
public Page<Notification> getAllNotifications(User user,
int page,
int size,
String sortBy,
String sortDirection,
List<NotificationType> types,
List<NotificationStatus> statuses) {
Sort sort = Sort.by(Sort.Direction.fromString(sortDirection), sortBy);
PageRequest pageable = PageRequest.of(page, size, sort);
boolean hasTypes = types != null && !types.isEmpty();
boolean hasStatuses = statuses != null && !statuses.isEmpty();
if (hasTypes && hasStatuses) {
return notificationRepository.findByUserAndNotificationTypeInAndStatusIn(user, types, statuses, pageable);
} else if (hasTypes) {
return notificationRepository.findByUserAndNotificationTypeIn(user, types, pageable);
} else if (hasStatuses) {
return notificationRepository.findByUserAndStatusIn(user, statuses, pageable);
} else {
return notificationRepository.findByUser(user, pageable);
}
}
/**
* 5 последних непрочитанных уведомлений.
*/
public List<Notification> getActiveNotifications(User user) {
return notificationRepository.findTop5ByUserAndStatusOrderByCreatedAtDesc(user, NotificationStatus.NEW);
}
/**
* Общее количество непрочитанных уведомлений.
*/
public long getUnreadCount(User user) {
return notificationRepository.countByUserAndStatus(user, NotificationStatus.NEW);
}
/**
* Пометить выбранные уведомления как прочитанные.
* @return количество обновлённых записей
*/
@Transactional
public int markAsRead(User user, List<Long> notificationIds) {
if (notificationIds == null || notificationIds.isEmpty()) {
return 0;
}
return notificationRepository.updateStatus(notificationIds, user, NotificationStatus.NEW,
NotificationStatus.READIED);
}
} }
@@ -4,9 +4,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.ReferralRepo;
import ru.soune.ReferralService; import ru.soune.ReferralService;
import ru.soune.nocopy.client.YooKassaClient; import ru.soune.nocopy.client.YooKassaClient;
import ru.soune.nocopy.dto.payment.PaymentMethodDTO; import ru.soune.nocopy.dto.payment.PaymentMethodDTO;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.payment.Payment; import ru.soune.nocopy.entity.payment.Payment;
import ru.soune.nocopy.entity.payment.PaymentMethod; import ru.soune.nocopy.entity.payment.PaymentMethod;
import ru.soune.nocopy.entity.payment.PaymentOperationType; import ru.soune.nocopy.entity.payment.PaymentOperationType;
@@ -19,6 +21,7 @@ import ru.soune.nocopy.exception.PaymentNotFoundException;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.*; import ru.soune.nocopy.repository.*;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -46,6 +49,10 @@ public class PaymentService {
private final YooKassaClient yooKassaClient; private final YooKassaClient yooKassaClient;
private final ReferralRepo referralRepo;
private final NotificationService notificationService;
@Transactional @Transactional
public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) {
User user = userRepository.findByEmail(email); User user = userRepository.findByEmail(email);
@@ -158,15 +165,19 @@ public class PaymentService {
savePaymentMethod(payment.getUser(), paymentMethod); savePaymentMethod(payment.getUser(), paymentMethod);
referralService.onUserAccountRefill(payment.getUser().getId(), intPrice); referralService.onUserAccountRefill(payment.getUser().getId(), intPrice);
// long inviterIdForUser = referralRepo.getInviterIdForUser(payment.getUser().getId());
// notificationService.addNotification(NotificationType.REFERRAL_ACTIVATED, inviterIdForUser);
} else if ("payment.canceled".equals(eventType)) { } else if ("payment.canceled".equals(eventType)) {
payment.setStatus(PaymentStatus.CANCELED); payment.setStatus(PaymentStatus.CANCELED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details"); Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason"); String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason); payment.setCancellationReason(reason);
}
notificationService.addNotification(NotificationType.PAYMENT_RESULT, payment.getUser().getId(), reason);
} else if ("payment.waiting_for_capture".equals(eventType)) { } else if ("payment.waiting_for_capture".equals(eventType)) {
payment.setStatus(PaymentStatus.WAITING); payment.setStatus(PaymentStatus.WAITING);
payment.setCancellationReason(null); payment.setCancellationReason(null);
@@ -175,10 +186,11 @@ public class PaymentService {
payment.setStatus(PaymentStatus.FAILED); payment.setStatus(PaymentStatus.FAILED);
Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details"); Map<String, Object> cancellationDetails = (Map<String, Object>) object.get("cancellation_details");
if (cancellationDetails != null) {
String reason = (String) cancellationDetails.get("reason"); String reason = (String) cancellationDetails.get("reason");
payment.setCancellationReason(reason); payment.setCancellationReason(reason);
}
notificationService.addNotification(NotificationType.PAYMENT_RESULT, payment.getUser().getId(), reason);
} }
paymentRepository.save(payment); paymentRepository.save(payment);
@@ -75,9 +75,6 @@ public class PayoutRequestService {
balanceService.freezeAmount(user, request.getAmount()); balanceService.freezeAmount(user, request.getAmount());
log.info("Payout requset created: ID={}, user={}, sum={}, status={}",
savedRequest.getId(), user.getId(), request.getAmount(), savedRequest.getStatus());
return savedRequest; return savedRequest;
} }
@@ -11,9 +11,11 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.Referral; import ru.soune.Referral;
import ru.soune.ReferralInvitee; import ru.soune.ReferralInvitee;
import ru.soune.ReferralRepo; import ru.soune.ReferralRepo;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.ReferralJpaRepository; import ru.soune.nocopy.repository.ReferralJpaRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
@@ -31,6 +33,8 @@ public class ReferralRepoImpl implements ReferralRepo {
private final UserRepository userRepository; private final UserRepository userRepository;
private final NotificationService notificationService;
@Override @Override
public @NotNull String getUserReferralLink(long userId) { public @NotNull String getUserReferralLink(long userId) {
ru.soune.nocopy.entity.referral.Referral referral = ru.soune.nocopy.entity.referral.Referral referral =
@@ -99,8 +103,7 @@ public class ReferralRepoImpl implements ReferralRepo {
@Override @Override
public int getAvailableIncome() { public int getAvailableIncome() {
Integer holdBalance = entity.getHoldBalance(); return entity.getAvailableIncome();
return entity.getTotalIncome() - (holdBalance != null ? holdBalance : 0);
} }
@Override @Override
@@ -109,6 +112,11 @@ public class ReferralRepoImpl implements ReferralRepo {
return holdBalance != null ? holdBalance : 0; return holdBalance != null ? holdBalance : 0;
} }
@Override
public int getSpentBalance() {
return entity.getTotalIncome() - entity.getAvailableIncome() - entity.getHoldBalance();
}
@Override @Override
public boolean getActive() { return entity.isActive(); } public boolean getActive() { return entity.isActive(); }
}; };
@@ -146,6 +154,8 @@ public class ReferralRepoImpl implements ReferralRepo {
@Override @Override
public void createReferralEntity(@NotNull Referral entity) { public void createReferralEntity(@NotNull Referral entity) {
ru.soune.nocopy.entity.referral.Referral referral = new ru.soune.nocopy.entity.referral.Referral(); ru.soune.nocopy.entity.referral.Referral referral = new ru.soune.nocopy.entity.referral.Referral();
Long inviter = entity.getInviter();
referral.setUserId(entity.getUserId()); referral.setUserId(entity.getUserId());
referral.setReferralLink(entity.getReferralLink()); referral.setReferralLink(entity.getReferralLink());
referral.setInviterId(entity.getInviter()); referral.setInviterId(entity.getInviter());
@@ -156,6 +166,10 @@ public class ReferralRepoImpl implements ReferralRepo {
referral.setActive(entity.getActive()); referral.setActive(entity.getActive());
referralRepository.save(referral); referralRepository.save(referral);
if (inviter != null){
notificationService.addNotification(NotificationType.REFERRAL_REGISTERED, inviter);
}
} }
@Override @Override
@@ -106,6 +106,26 @@ public class AuthService {
return authToken.getUser().getId(); return authToken.getUser().getId();
} }
@Transactional
public User getUser(String token) {
if (token == null || token.isBlank()) {
throw new NotFoundAuthToken("Token is required");
}
if (token.startsWith("Bearer ")) {
token = token.replace("Bearer ", "");
}
AuthToken authToken = authTokenRepository.findByToken(token)
.orElseThrow(() -> new NotFoundAuthToken("Token not found"));
authToken.setLastUsedAt(LocalDateTime.now());
authToken.setExpiresAt(LocalDateTime.now().plusHours(1));
authTokenRepository.save(authToken);
return authToken.getUser();
}
public AuthToken login(LoginRequest request) { public AuthToken login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail()); User user = userRepository.findByEmail(request.getEmail());
@@ -161,7 +181,7 @@ public class AuthService {
} }
public AuthToken getAuthToken(String token) { public AuthToken getAuthToken(String token) {
return authTokenRepository.findByToken(token).get(); return authTokenRepository.findByToken(token).orElse(null);
} }
private AuthToken genereateAuthToken(User user) { private AuthToken genereateAuthToken(User user) {
@@ -8,11 +8,14 @@ import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.configuration.search.SearchProperties;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.notification.NotificationType;
import ru.soune.nocopy.entity.search.GlobalSearchResult; import ru.soune.nocopy.entity.search.GlobalSearchResult;
import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.GlobalSearchTask;
import ru.soune.nocopy.entity.search.SearchStatus; import ru.soune.nocopy.entity.search.SearchStatus;
import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.GlobalSearchResultRepository; import ru.soune.nocopy.repository.GlobalSearchResultRepository;
import ru.soune.nocopy.repository.GlobalSearchTaskRepository; import ru.soune.nocopy.repository.GlobalSearchTaskRepository;
import ru.soune.nocopy.service.notification.NotificationService;
import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService; import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.service.violation.ViolationService; import ru.soune.nocopy.service.violation.ViolationService;
@@ -41,31 +44,38 @@ public class GlobalSearchAsyncProcessor {
private final SearchProperties searchProperties; private final SearchProperties searchProperties;
private final NotificationService notificationService;
private final FileEntityRepository fileEntityRepository;
@Async @Async
@Transactional @Transactional
public void processFilesAsync(String taskId, List<FileEntity> filesToProcess, Long userId) { public void processFilesAsync(String taskId, List<String> filesToProcess, Long userId) {
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
try { try {
for (int i = 0; i < filesToProcess.size(); i++) { for (int i = 0; i < filesToProcess.size(); i++) {
FileEntity file = filesToProcess.get(i); String uuid = filesToProcess.get(i);
FileEntity file = fileEntityRepository.findById(uuid)
.orElseThrow(() -> new RuntimeException("File not found: " + uuid));
tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH);
GlobalSearchResult result = processFile(file, taskId, userId); GlobalSearchResult result = processFile(file, taskId, userId);
globalSearchResultRepository.save(result); globalSearchResultRepository.save(result);
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setProcessedFiles(i + 1); task.setProcessedFiles(i + 1);
task.setUpdatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task); globalSearchTaskRepository.save(task);
} }
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setStatus(SearchStatus.COMPLETED.name()); task.setStatus(SearchStatus.COMPLETED.name());
task.setUpdatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task); globalSearchTaskRepository.save(task);
notificationService.addNotification(NotificationType.SEARCH_RESULT, userId);
} catch (Exception e) { } catch (Exception e) {
log.error("Global search failed for task: {}", taskId, e); log.error("Global search failed for task: {}", taskId, e);
GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow();
task.setStatus(SearchStatus.FAILED.name()); task.setStatus(SearchStatus.FAILED.name());
task.setUpdatedAt(LocalDateTime.now()); task.setUpdatedAt(LocalDateTime.now());
globalSearchTaskRepository.save(task); globalSearchTaskRepository.save(task);
@@ -171,10 +181,6 @@ public class GlobalSearchAsyncProcessor {
log.warn("No results for file {} because all engines are disabled", file.getId()); log.warn("No results for file {} because all engines are disabled", file.getId());
} else { } else {
result.setFileStatus(SearchStatus.SUCCESS.name()); result.setFileStatus(SearchStatus.SUCCESS.name());
if (!allUniqueImages.isEmpty()) {
tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH);
}
} }
return result; return result;
@@ -6,6 +6,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; import ru.soune.nocopy.dto.search.GlobalSearchStartRequest;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity;
import ru.soune.nocopy.entity.search.GlobalSearchTask; import ru.soune.nocopy.entity.search.GlobalSearchTask;
import ru.soune.nocopy.entity.search.SearchStatus; import ru.soune.nocopy.entity.search.SearchStatus;
@@ -45,7 +46,9 @@ public class GlobalSearchService {
globalSearchTaskRepository.save(task); globalSearchTaskRepository.save(task);
asyncProcessor.processFilesAsync(task.getTaskId(), filesToProcess, userId); List<String> fileIds = filesToProcess.stream().map(FileEntity::getId).toList();
asyncProcessor.processFilesAsync(task.getTaskId(), fileIds, userId);
return task.getTaskId(); return task.getTaskId();
} }
@@ -61,6 +64,7 @@ public class GlobalSearchService {
} else { } else {
List<String> fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId) List<String> fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId)
.stream() .stream()
.filter(f -> f.getMimeType().equals(FileType.IMAGE.getDisplayName()))
.map(FileEntity::getId) .map(FileEntity::getId)
.toList(): request.getFileIds(); .toList(): request.getFileIds();
@@ -90,6 +90,7 @@ public class SearchImageService {
} }
private OkHttpClient createHttpClient() { private OkHttpClient createHttpClient() {
boolean useProxy = searchProperties.getProxy().isEnabled();
OkHttpClient.Builder builder = new OkHttpClient.Builder() OkHttpClient.Builder builder = new OkHttpClient.Builder()
.connectTimeout(35, TimeUnit.SECONDS) .connectTimeout(35, TimeUnit.SECONDS)
.writeTimeout(35, TimeUnit.SECONDS) .writeTimeout(35, TimeUnit.SECONDS)
@@ -99,12 +100,12 @@ public class SearchImageService {
.followSslRedirects(true) .followSslRedirects(true)
.retryOnConnectionFailure(true); .retryOnConnectionFailure(true);
// if (useProxy && searchProperties.getProxy().isEnabled()) { if (useProxy && searchProperties.getProxy().isEnabled()) {
// Proxy proxy = new Proxy(Proxy.Type.HTTP, Proxy proxy = new Proxy(Proxy.Type.HTTP,
// new InetSocketAddress(searchProperties.getProxy().getHost(), new InetSocketAddress(searchProperties.getProxy().getHost(),
// searchProperties.getProxy().getPort())); searchProperties.getProxy().getPort()));
// builder.proxy(proxy); builder.proxy(proxy);
// } }
return builder.build(); return builder.build();
} }
@@ -114,9 +115,6 @@ public class SearchImageService {
throw new IllegalStateException("SearchAPI key not configured"); throw new IllegalStateException("SearchAPI key not configured");
} }
// boolean useProxy = searchProperties.getProxy().isEnabled();
// OkHttpClient client = createHttpClient(useProxy);
OkHttpClient client = createHttpClient(); OkHttpClient client = createHttpClient();
HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search")
@@ -137,6 +135,7 @@ public class SearchImageService {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
try (Response response = client.newCall(request).execute()) { try (Response response = client.newCall(request).execute()) {
ResponseBody body = response.body();
long duration = System.currentTimeMillis() - start; long duration = System.currentTimeMillis() - start;
log.info("SearchAPI response code={}, duration={}ms, engine={}", log.info("SearchAPI response code={}, duration={}ms, engine={}",
response.code(), duration, engine); response.code(), duration, engine);
@@ -146,7 +145,6 @@ public class SearchImageService {
throw new IOException("API error " + response.code() + ": " + errorBody); throw new IOException("API error " + response.code() + ": " + errorBody);
} }
ResponseBody body = response.body();
if (body == null) { if (body == null) {
throw new IOException("Empty response body"); throw new IOException("Empty response body");
} }
@@ -1,6 +1,8 @@
package ru.soune.nocopy.service.tariff; package ru.soune.nocopy.service.tariff;
import jakarta.mail.MessagingException;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO; import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
@@ -13,13 +15,16 @@ import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.TariffInfoRepository; import ru.soune.nocopy.repository.TariffInfoRepository;
import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.TariffRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService;
import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
@Slf4j
public class TariffInfoService { public class TariffInfoService {
private TariffInfoRepository tariffInfoRepository; private TariffInfoRepository tariffInfoRepository;
@@ -28,6 +33,8 @@ public class TariffInfoService {
private UserRepository userRepository; private UserRepository userRepository;
private EmailService emailService;
public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus, public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus,
Integer tokens) { Integer tokens) {
TariffInfo tariffInfo = new TariffInfo(); TariffInfo tariffInfo = new TariffInfo();
@@ -40,8 +47,8 @@ public class TariffInfoService {
return tariffInfoRepository.save(tariffInfo); return tariffInfoRepository.save(tariffInfo);
} }
@Transactional // @Transactional
public void writeOffTokens(long userId, Integer value) { public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException {
User user = userRepository.findById(userId).orElseThrow(); User user = userRepository.findById(userId).orElseThrow();
TariffInfo activeTariffInfo; TariffInfo activeTariffInfo;
@@ -55,7 +62,7 @@ public class TariffInfoService {
writeOffTokensForTariff(activeTariffInfo, value); writeOffTokensForTariff(activeTariffInfo, value);
} }
public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) { public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException {
Integer tokens = activeTariffInfo.getTokens(); Integer tokens = activeTariffInfo.getTokens();
Integer boughtTokens = activeTariffInfo.getBoughtTokens(); Integer boughtTokens = activeTariffInfo.getBoughtTokens();
@@ -68,6 +75,12 @@ public class TariffInfoService {
tariffInfoRepository.save(activeTariffInfo); tariffInfoRepository.save(activeTariffInfo);
} else { } else {
int needTokens = boughtTokens - value;
User user = userRepository.findByPersonalTariffInfo(activeTariffInfo);
emailService.sendTokensNotFoundEmail(user, tokens, Math.abs(needTokens));
log.error("User:" + user.getId() + " not have tokens for protect");
throw new TariffNotFoundException("User not have tokens for protect"); throw new TariffNotFoundException("User not have tokens for protect");
} }
} }
@@ -75,8 +88,8 @@ public class TariffInfoService {
public TariffInfo createTariffInfo(TariffType tariffType) { public TariffInfo createTariffInfo(TariffType tariffType) {
Tariff tariff = tariffRepository.findByType(tariffType.toString()).orElseThrow(); Tariff tariff = tariffRepository.findByType(tariffType.toString()).orElseThrow();
TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(), LocalDateTime.now().plusDays(30), TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(),
TariffStatus.ACTIVE, tariff.getTokens()); LocalDateTime.now().plusDays(tariff.getTariffTerm().getDays()), TariffStatus.ACTIVE, tariff.getTokens());
tariffInfo.setTariff(tariff); tariffInfo.setTariff(tariff);
return tariffInfoRepository.save(tariffInfo); return tariffInfoRepository.save(tariffInfo);
@@ -7,13 +7,12 @@ import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.repository.TariffRepository;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -109,6 +108,18 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 100); tariff.setDiskSize(1024L * 1024 * 100);
tariff.setMaxUsers(1L); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c"); tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break;
case DEMO:
tariff.setName("demo");
tariff.setPrice(0);
tariff.setTokens(30);
tariff.setMaxFilesCount(10);
tariff.setDiskSize(1024L * 1024 * 100);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break; break;
case START: case START:
@@ -119,6 +130,18 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 500); tariff.setDiskSize(1024L * 1024 * 500);
tariff.setMaxUsers(1L); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c"); tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break;
case START_YEAR:
tariff.setName("start_year");
tariff.setPrice(1100);
tariff.setTokens(300);
tariff.setMaxFilesCount(80);
tariff.setDiskSize(1024L * 1024 * 500);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break; break;
case PRO: case PRO:
@@ -129,8 +152,21 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 10L); tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(1L); tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c"); tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break; break;
case PRO_YEAR:
tariff.setName("pro_year");
tariff.setPrice(4100);
tariff.setTokens(900);
tariff.setMaxFilesCount(300);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(1L);
tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break;
case STUDIO: case STUDIO:
tariff.setName("studio"); tariff.setName("studio");
tariff.setPrice(9750); tariff.setPrice(9750);
@@ -139,6 +175,18 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(2L); tariff.setMaxUsers(2L);
tariff.setTariffAccountType("b2c"); tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break;
case STUDIO_YEAR:
tariff.setName("studio_year");
tariff.setPrice(9500);
tariff.setTokens(3000);
tariff.setMaxFilesCount(1000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(2L);
tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break; break;
case BASIC: case BASIC:
@@ -149,6 +197,18 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(basicUserCount); tariff.setMaxUsers(basicUserCount);
tariff.setTariffAccountType("b2c"); tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break;
case BASIC_YEAR:
tariff.setName("basic_year");
tariff.setPrice(basicPrice);
tariff.setTokens(basicTokens);
tariff.setMaxFilesCount(enterpriceMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(basicUserCount);
tariff.setTariffAccountType("b2c");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break; break;
case ENTERPRISE: case ENTERPRISE:
@@ -159,6 +219,18 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 10L); tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(10L); tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b"); tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break;
case ENTERPRISE_YEAR:
tariff.setName("enterprise_year");
tariff.setPrice(200000);
tariff.setTokens(30000);
tariff.setMaxFilesCount(100000);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break; break;
case BUISNES: case BUISNES:
@@ -169,8 +241,21 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(5L); tariff.setMaxUsers(5L);
tariff.setTariffAccountType("b2b"); tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break; break;
case BUISNES_YEAR:
tariff.setName("buisnes_year");
tariff.setPrice(29000);
tariff.setTokens(5000);
tariff.setMaxFilesCount(5000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(5L);
tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break;
case CORPORAT: case CORPORAT:
tariff.setName("corporat"); tariff.setName("corporat");
tariff.setPrice(99000); tariff.setPrice(99000);
@@ -179,8 +264,21 @@ public class TariffInitializationService {
tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(10L); tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b"); tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.MONTHLY);
break; break;
case CORPORAT_YEAR:
tariff.setName("corporat_year");
tariff.setPrice(99000);
tariff.setTokens(20000);
tariff.setMaxFilesCount(50000);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(10L);
tariff.setTariffAccountType("b2b");
tariff.setTariffTerm(TariffTimeTerm.YEAR);
break;
case TOKEN_100: case TOKEN_100:
tariff.setName("token_100"); tariff.setName("token_100");
tariff.setPrice(299); tariff.setPrice(299);
@@ -5,6 +5,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffTimeTerm;
import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.tarif.TariffType;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.TariffNotFoundException; import ru.soune.nocopy.exception.TariffNotFoundException;
@@ -20,7 +21,7 @@ public class TariffService {
private final TariffRepository tariffRepository; private final TariffRepository tariffRepository;
public void addTariff(String tariffName, double price, String type, int tokens, long diskSize, long maxUsers, public void addTariff(String tariffName, double price, String type, int tokens, long diskSize, long maxUsers,
int maxFilesCount) { int maxFilesCount, String tariffTerm) {
Tariff tariff = new Tariff(); Tariff tariff = new Tariff();
tariff.setName(tariffName); tariff.setName(tariffName);
tariff.setPrice(price); tariff.setPrice(price);
@@ -29,6 +30,7 @@ public class TariffService {
tariff.setDiskSize(diskSize); tariff.setDiskSize(diskSize);
tariff.setMaxUsers(maxUsers); tariff.setMaxUsers(maxUsers);
tariff.setMaxFilesCount(maxFilesCount); tariff.setMaxFilesCount(maxFilesCount);
tariff.setTariffTerm(TariffTimeTerm.valueOf(tariffTerm.toUpperCase()));
tariffRepository.save(tariff); tariffRepository.save(tariff);
} }
@@ -42,6 +44,15 @@ public class TariffService {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public List<TariffDTO> getTariffByAccountTypeAndTariffTerm(User user, TariffTimeTerm tariffTerm) {
String typeAccount = user.canManageCompanySettings() ? "b2b": "b2c";
return tariffRepository.findByTariffAccountTypeAndTariffTerm(typeAccount, tariffTerm)
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
public List<TariffDTO> getTariffByType(String type) { public List<TariffDTO> getTariffByType(String type) {
return tariffRepository.findByTariffAccountType(type) return tariffRepository.findByTariffAccountType(type)
.stream() .stream()
@@ -91,6 +102,8 @@ public class TariffService {
} }
private TariffDTO convertToDTO(Tariff tariff) { private TariffDTO convertToDTO(Tariff tariff) {
TariffTimeTerm tariffTerm = tariff.getTariffTerm();
return new TariffDTO( return new TariffDTO(
tariff.getId(), tariff.getId(),
tariff.getType(), tariff.getType(),
@@ -99,7 +112,8 @@ public class TariffService {
tariff.getTokens(), tariff.getTokens(),
tariff.getMaxFilesCount(), tariff.getMaxFilesCount(),
tariff.getDiskSize(), tariff.getDiskSize(),
tariff.getMaxUsers() tariff.getMaxUsers(),
tariffTerm == null ? "" : tariffTerm.name()
); );
} }
@@ -111,5 +125,6 @@ public class TariffService {
tariff.setMaxFilesCount(dto.getMaxFilesCount()); tariff.setMaxFilesCount(dto.getMaxFilesCount());
tariff.setDiskSize(dto.getDiskSize()); tariff.setDiskSize(dto.getDiskSize());
tariff.setMaxUsers(dto.getMaxUsers()); tariff.setMaxUsers(dto.getMaxUsers());
tariff.setTariffTerm(TariffTimeTerm.valueOf(dto.getTariffTerm().toUpperCase()));
} }
} }

Some files were not shown because too many files have changed in this diff Show More