diff --git a/README.md b/README.md index 0d10a52..1e518c5 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,345 @@ INSERT INTO referral_levels (id, name, min_invitees, reward_percentage) VALUES ('gold', 'GOLD', 16, 22), ('platinum', 'PLATINUM', 50, 25); +----------------- +Скрипт,для нумерации для уже сохданных файлов +WITH max_id AS ( +SELECT COALESCE(MAX(support_id), 0) as max_val FROM file_entities +), +-- Нумеруем только NULL записи, начиная с max_val + 1 +numbered AS ( +SELECT +f.id, +(SELECT max_val FROM max_id) + ROW_NUMBER() OVER (ORDER BY f.created_at, f.id) as new_support_id +FROM file_entities f +WHERE f.support_id IS NULL +) +UPDATE file_entities +SET support_id = numbered.new_support_id +FROM numbered +WHERE file_entities.id = numbered.id; + +--------------------------------- +НАСТРОЙКА NGINX +-------------------------------- + +# admin.not-copy.com +server { +listen 80; +listen [::]:80; +server_name admin.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name admin.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/admin.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/admin.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2995; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# workspace.not-copy.com +server { +listen 80; +listen [::]:80; +server_name workspace.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name workspace.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/workspace.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/workspace.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2998; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# lp.not-copy.com +server { +listen 80; +listen [::]:80; +server_name lp.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name lp.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/lp.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/lp.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3003; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2993; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# dev-admin.not-copy.com +server { +listen 80; +listen [::]:80; +server_name dev-admin.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-admin.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-admin.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-admin.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2996; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# dev-workspace.not-copy.com +server { +listen 80; +listen [::]:80; +server_name dev-workspace.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-workspace.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-workspace.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-workspace.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:3002; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + +# dev-lp.not-copy.com +server { +listen 80; +listen [::]:80; +server_name dev-lp.not-copy.com; +return 301 https://$server_name$request_uri; +} + +server { +listen 443 ssl; +listen [::]:443 ssl; +server_name dev-lp.not-copy.com; + + ssl_certificate /etc/letsencrypt/live/dev-lp.not-copy.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/dev-lp.not-copy.com/privkey.pem; + + location /api/ { + proxy_pass http://localhost:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location / { + proxy_pass http://localhost:2994; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + + +# Проверяем конфиг +ls -la /etc/nginx/sites-available/ +ls -la /etc/nginx/sites-enabled/ + +# Создание конфига + +sudo nano /etc/nginx/sites-available/not-copy + +# Создание симлинк +sudo ln -s /etc/nginx/sites-available/not-copy /etc/nginx/sites-enabled/ + +# Чек +sudo nginx -t + +# Рестарт +sudo systemctl reload nginx + + +--------------------------------------------- + +# ПОЛНАЯ ИНСТРУКЦИЯ: НАСТРОЙКА ПРОКСИ ДЛЯ ОБХОДА БЛОКИРОВОК SEARCHAPI.IO + +## Данные для подключения к прокси-серверу +IP: 193.46.217.94 +Порт HTTP: 3128 +Порт SOCKS: 1080 +Логин SSH: root +Пароль SSH: u7nRtsD6 + +## 1. Настройка прокси-сервера +```bash +# Подключаемся к серверу +ssh root@193.46.217.94 +# Вводим пароль: u7nRtsD6 + +# Обновляем систему и устанавливаем пакеты +yum update -y +yum install -y epel-release net-tools nano +yum install -y 3proxy + +# Создаем конфигурацию +mkdir -p /etc/3proxy +cat > /etc/3proxy/3proxy.cfg << 'EOF' +nserver 8.8.8.8 +nserver 8.8.4.4 +timeouts 1 5 30 60 180 1800 15 60 +daemon +log /var/log/3proxy/3proxy.log +logformat "- +_L%t.%. %N.%p %E %U %C:%c %R:%r %O %I %h %T" +rotate 30 +auth none +allow * * * * * +proxy -p3128 +socks -p1080 +EOF + +# Создаем папку для логов и запускаем прокси +mkdir -p /var/log/3proxy +/usr/bin/3proxy /etc/3proxy/3proxy.cfg + +# Проверяем, что запустилось +ps aux | grep 3proxy +netstat -tulpn | grep -E '3128|1080' + +# Настраиваем файрвол +firewall-cmd --permanent --add-port=3128/tcp +firewall-cmd --permanent --add-port=1080/tcp +firewall-cmd --reload +firewall-cmd --list-ports + +# Проверяем локальную работу прокси +curl -x http://127.0.0.1:3128 https://api.ipify.org +# Должен вернуть: 193.46.217.94 + +# Настраиваем автозапуск +echo "/usr/bin/3proxy /etc/3proxy/3proxy.cfg" >> /etc/rc.local +chmod +x /etc/rc.local + + +# Проверяем доступность прокси +ping -c 4 193.46.217.94 +curl -x http://193.46.217.94:3128 https://api.ipify.org +# Должен вернуть: 193.46.217.94 + +# Проверяем работу с SearchAPI через прокси +curl -x http://193.46.217.94:3128 "https://www.searchapi.io/api/v1/search?engine=google_lens&api_key=5jyYZC8jSaxhZTwjMUhwtAXi&url=https://dev-workspace.not-copy.com/api/files/public/76d06557-6df1-4fcd-a273-050ec6a35faf&search_type=exact_matches" -o test.json +ls -la test.json +tail -20 test.json +# Файл должен быть полным (заканчиваться на '}') + + + +ps aux | grep 3proxy # Проверка статуса +netstat -tulpn | grep -E '3128|1080' # Проверка портов +tail -f /var/log/3proxy/3proxy.log # Просмотр логов +killall 3proxy # Остановка прокси +/usr/bin/3proxy /etc/3proxy/3proxy.cfg # Запуск прокси + + +firewall-cmd --permanent --remove-port=3128/tcp +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="IP_ТВОЕГО_СЕРВЕРА" port port="3128" protocol="tcp" accept' +firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="IP_ТВОЕГО_СЕРВЕРА" port port="1080" protocol="tcp" accept' +firewall-cmd --reload + + +На прокси-сервере: +bash +ps aux | grep 3proxy # Проверка статуса +netstat -tulpn | grep -E '3128|1080' # Проверка портов +tail -f /var/log/3proxy/3proxy.log # Просмотр логов +killall 3proxy # Остановка прокси +/usr/bin/3proxy /etc/3proxy/3proxy.cfg # Запуск прокси + +На основном сервере: +bash +curl -x http://193.46.217.94:3128 https://api.ipify.org # Проверка прокси +telnet 193.46.217.94 3128 # Проверка порта + ------ Настройка Яндекс Клауд diff --git a/build.gradle b/build.gradle index 745ea98..d3ccf62 100644 --- a/build.gradle +++ b/build.gradle @@ -59,7 +59,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.3.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' - implementation name: 'testlib-fat-0.3.0-all' + implementation name: 'testlib-fat-0.3.1-all' implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0' diff --git a/infrastructure/jenkins/deploy-backend.groovy b/infrastructure/jenkins/deploy-backend.groovy index 56cd560..78e2105 100644 --- a/infrastructure/jenkins/deploy-backend.groovy +++ b/infrastructure/jenkins/deploy-backend.groovy @@ -9,14 +9,29 @@ pipeline { ) string( name: 'PORT', - defaultValue: '3000', + defaultValue: '3001', description: 'Порт для запуска экземпляра' ) + string( + name: 'PORT_DB', + defaultValue: '', + description: 'Порт для запуска БД' + ) string( name: 'SERVER', defaultValue: '92.242.61.23', description: 'Сервер для деплоя' ) + string( + name: 'NETWORK', + defaultValue: 'app-network-dev', + description: 'Имя сети (будет создана если не существует)' + ) + choice( + name: 'SPRING_PROFILE', + choices: ['dev', 'prod'], + description: 'Профиль Spring для запуска' + ) } stages { @@ -36,10 +51,34 @@ pipeline { } } + stage('Copy to server') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + rm -rf /opt/deployments/${params.BRANCH}-${params.PORT}/* + mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT} + " + + sshpass -p '${SSH_PASS}' scp -r ./* ${SSH_USER}@${params.SERVER}:/opt/deployments/${params.BRANCH}-${params.PORT}/ + """ + } + } + } + } + stage('Deploy with docker-compose') { steps { script { - def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def springProfile = params.SPRING_PROFILE withCredentials([ usernamePassword( credentialsId: 'server-root-password', @@ -61,10 +100,16 @@ pipeline { # Основная команда sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + mkdir -p /opt/deployments/${params.BRANCH}-${params.PORT} cd /opt/deployments/${params.BRANCH}-${params.PORT} - echo '1. Создаем изолированную сеть для порта ${params.PORT}...' - docker network create app-network-${params.PORT} 2>/dev/null || echo 'Сеть уже существует' + echo '1. Проверяем/создаем сеть ${params.NETWORK}...' + if ! docker network ls --format '{{.Name}}' | grep -q '^${params.NETWORK}\$'; then + docker network create ${params.NETWORK} + echo 'Сеть ${params.NETWORK} создана' + else + echo 'Сеть ${params.NETWORK} уже существует' + fi echo '2. Останавливаем старые контейнеры порта ${params.PORT}...' docker stop app-backend-${params.PORT} 2>/dev/null || echo 'Контейнер не найден' @@ -77,12 +122,12 @@ pipeline { echo '3. Запускаем БД...' docker run -d \\ --name postgres-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ --network-alias no_copy_${params.PORT} \\ --network-alias database-${params.PORT} \\ --network-alias db-${params.PORT} \\ -p ${params.PORT}0:5432 \\ - -e POSTGRES_DB=no_copy_${params.PORT} \\ + -e POSTGRES_DB=no_copy${params.PORT_DB} \\ -e POSTGRES_USER=${DB_USER} \\ -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -v pgdata_${params.PORT}:/var/lib/postgresql/data \\ @@ -92,7 +137,7 @@ pipeline { echo '4. Запускаем storage...' docker run -d \\ --name file-storage-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ -v uploads_data_${params.PORT}:/storage:rw \\ alpine:latest tail -f /dev/null @@ -105,15 +150,18 @@ pipeline { echo '7. Запускаем приложение...' docker run -d \\ --name app-backend-${params.PORT} \\ - --network app-network-${params.PORT} \\ + --network ${params.NETWORK} \\ + --network-alias app \\ + --network-alias app-${params.PORT} \\ -p ${params.PORT}:8080 \\ -v uploads_data_${params.PORT}:/data/uploads:rw \\ - -e POSTGRES_DB=no_copy_${params.PORT} \\ + -e POSTGRES_DB=no_copy${params.PORT_DB} \\ -e POSTGRES_USER=${DB_USER} \\ -e POSTGRES_PASSWORD=${DB_PASSWORD} \\ -e POSTGRES_PORT=5432 \\ -e POSTGRES_HOST=no_copy_${params.PORT} \\ -e FILE_STORAGE_PATH=/data/uploads \\ + -e SPRING_PROFILES_ACTIVE=${springProfile} \\ app-backend-${params.PORT}:latest echo '8. Проверка...' @@ -131,12 +179,39 @@ pipeline { } } } + + stage('Verify network') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} " + echo 'Проверяем подключение к сети ${params.NETWORK}' + docker network inspect ${params.NETWORK} --format='{{.Name}}: {{range .Containers}}{{.Name}} {{end}}' + + echo 'Проверяем алиас app для контейнера app-backend-${params.PORT}' + docker inspect app-backend-${params.PORT} | grep -A 5 'Aliases' + " + """ + } + } + } + } } post { success { - echo "Deployment successful" - echo "Application URL: http://${params.SERVER}:${params.PORT}" + script { + def appUrl = params.SPRING_PROFILE == 'prod' ? 'https://workspace.not-copy.com' : 'https://dev-workspace.not-copy.com' + echo "Deployment successful" + echo "Application URL: ${appUrl}" + } } failure { echo "Deployment failed for branch ${params.BRANCH} on port ${params.PORT}" diff --git a/infrastructure/jenkins/deploy-front.groovy b/infrastructure/jenkins/deploy-front.groovy index 5758335..2f4f52b 100644 --- a/infrastructure/jenkins/deploy-front.groovy +++ b/infrastructure/jenkins/deploy-front.groovy @@ -12,6 +12,11 @@ pipeline { defaultValue: '2998', description: 'Порт для запуска экземпляра' ) + string( + name: 'NETWORK', + defaultValue: 'app-network-dev', + description: 'Docker сеть для подключения' + ) } stages { @@ -189,6 +194,70 @@ pipeline { } } } + +// stage('Connect to network') { +// steps { +// script { +// withCredentials([ +// usernamePassword( +// credentialsId: 'server-root-password', +// usernameVariable: 'SSH_USER', +// passwordVariable: 'SSH_PASS' +// ) +// ]) { +// sh """ +// sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " +// echo 'Отключаем от всех сетей и подключаем к ${params.NETWORK}' +// +// # Отключаем от всех сетей (кроме bridge, host, none) +// current_networks=\$(docker inspect no-copy-frontend-${params.PORT} --format='{{range \$net, \$v := .NetworkSettings.Networks}}{{\$net}}{{\"\\n\"}}{{end}}' | grep -v -E '^(bridge|host|none)\$' || true) +// +// for net in \$current_networks; do +// echo \"Отключаем от сети: \$net\" +// docker network disconnect \$net no-copy-frontend-${params.PORT} 2>/dev/null || true +// done +// +// # Подключаем к нужной сети +// docker network connect ${params.NETWORK} no-copy-frontend-${params.PORT} +// +// echo 'Готово! Контейнер теперь в сети:' +// docker inspect no-copy-frontend-${params.PORT} --format='{{range \$net, \$v := .NetworkSettings.Networks}}{{\$net}}{{\"\\n\"}}{{end}}' +// " +// """ +// } +// } +// } +// } + + stage('Connect to network') { + steps { + script { + withCredentials([ + usernamePassword( + credentialsId: 'server-root-password', + usernameVariable: 'SSH_USER', + passwordVariable: 'SSH_PASS' + ) + ]) { + sh """ + sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " + echo 'Подключаем контейнер к сети ${params.NETWORK}' + + if ! docker network ls --format '{{.Name}}' | grep -q '^${params.NETWORK}\$'; then + docker network create ${params.NETWORK} + fi + + if docker ps -a --format '{{.Names}}' | grep -q '^no-copy-frontend-${params.PORT}\$'; then + docker network disconnect ${params.NETWORK} no-copy-frontend-${params.PORT} 2>/dev/null || true + docker network connect ${params.NETWORK} no-copy-frontend-${params.PORT} + echo 'Контейнер подключен к ${params.NETWORK}' + fi + " + """ + } + } + } + } } post { diff --git a/infrastructure/jenkins/deploy.groovy b/infrastructure/jenkins/deploy.groovy index fbfb736..6600988 100644 --- a/infrastructure/jenkins/deploy.groovy +++ b/infrastructure/jenkins/deploy.groovy @@ -34,7 +34,7 @@ pipeline { stage('Deploy with docker-compose') { steps { script { - def buildTime = sh(script: "date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() + def buildTime = sh(script: "TZ='Asia/Novosibirsk' date '+%d-%m-%Y %H:%M:%S'", returnStdout: true).trim() withCredentials([ usernamePassword( credentialsId: 'server-root-password', diff --git a/libs/testlib-fat-0.3.0-all.jar b/libs/testlib-fat-0.3.1-all.jar similarity index 95% rename from libs/testlib-fat-0.3.0-all.jar rename to libs/testlib-fat-0.3.1-all.jar index c9f3df5..9b0a93c 100644 Binary files a/libs/testlib-fat-0.3.0-all.jar and b/libs/testlib-fat-0.3.1-all.jar differ diff --git a/referral/src/main/kotlin/Referral.kt b/referral/src/main/kotlin/Referral.kt index 4b65d8a..cb0e89f 100644 --- a/referral/src/main/kotlin/Referral.kt +++ b/referral/src/main/kotlin/Referral.kt @@ -9,5 +9,6 @@ interface Referral { val totalIncome: Int val availableIncome: Int val holdBalance: Int + val spentBalance: Int val active: Boolean } \ No newline at end of file diff --git a/referral/src/main/kotlin/ReferralService.kt b/referral/src/main/kotlin/ReferralService.kt index 489e149..d36534e 100644 --- a/referral/src/main/kotlin/ReferralService.kt +++ b/referral/src/main/kotlin/ReferralService.kt @@ -18,6 +18,7 @@ class ReferralService( override val totalIncome = 0 override val availableIncome = 0 override val holdBalance: Int = 0 + override val spentBalance: Int = 0 override val active: Boolean = false } @@ -59,7 +60,8 @@ class ReferralService( totalIncome = referral.totalIncome, availableIncome = referral.availableIncome, holdBalance = referral.holdBalance, - referralLink = referral.referralLink + referralLink = referral.referralLink, + spentBalance = referral.spentBalance, ) } diff --git a/referral/src/main/kotlin/ReferralStat.kt b/referral/src/main/kotlin/ReferralStat.kt index d9e6ae5..fa05122 100644 --- a/referral/src/main/kotlin/ReferralStat.kt +++ b/referral/src/main/kotlin/ReferralStat.kt @@ -7,4 +7,5 @@ data class ReferralStat( val availableIncome: Int, val holdBalance: Int, val referralLink: String, + val spentBalance: Int, ) \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/client/YooKassaClient.java b/src/main/java/ru/soune/nocopy/client/YooKassaClient.java new file mode 100644 index 0000000..63307ce --- /dev/null +++ b/src/main/java/ru/soune/nocopy/client/YooKassaClient.java @@ -0,0 +1,69 @@ +package ru.soune.nocopy.client; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; +import com.fasterxml.jackson.databind.ObjectMapper; + +@Component +@Slf4j +public class YooKassaClient { + + @Value("${yookassa.shop-id}") + private String shopId; + + @Value("${yookassa.secret-key}") + private String secretKey; + + private final HttpClient httpClient = HttpClient.newHttpClient(); + + private final ObjectMapper mapper = new ObjectMapper(); + + public boolean createAutoPayment(String paymentMethodId, double amount, String description, String userId) { + try { + String url = "https://api.yookassa.ru/v3/payments"; + String auth = shopId + ":" + secretKey; + String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); + + Map body = new HashMap<>(); + body.put("amount", Map.of("value", String.format("%.2f", amount), + "currency", "RUB")); + body.put("payment_method_id", paymentMethodId); + body.put("description", description); + body.put("capture", true); + body.put("metadata", Map.of("user_id", userId, "auto_payment", "true")); + + String requestBody = mapper.writeValueAsString(body); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .header("Content-Type", "application/json") + .header("Authorization", "Basic " + encodedAuth) + .header("Idempotence-Key", UUID.randomUUID().toString()) + .POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .build(); + + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200 || response.statusCode() == 201) { + log.info("Auto payment created: {}", response.body()); + return true; + } else { + log.error("Auto payment failed: {} - {}", response.statusCode(), response.body()); + return false; + } + } catch (Exception e) { + log.error("Error creating auto payment", e); + return false; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java index df4b7e1..92598e7 100644 --- a/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/ApplicationConfig.java @@ -12,14 +12,22 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.file.ProtectionStatus; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.util.FileUtil; -import java.util.Collections; +import java.util.List; @Configuration @EnableAutoConfiguration @AllArgsConstructor public class ApplicationConfig { + private final FileEntityRepository fileEntityRepository; + + private final FileUtil fileUtil; + @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -36,8 +44,11 @@ public class ApplicationConfig { AudioFilePathProvider audioFilePathProvider, DocumentLocalSearch documentLocalSearch) { + List initialFiles = loadProcessingFile(); + + //TODO инициализация файлов при создании return new com.vrt.NoCopyFileService( - Collections.emptyList(), + initialFiles, fileProvider, processingListener, imageUniqueCheck, @@ -47,5 +58,13 @@ public class ApplicationConfig { documentLocalSearch ); } + + private List loadProcessingFile() { + List byProtectionStatus = + fileEntityRepository.findByProtectionStatus(ProtectionStatus.PROCESSING); + + return byProtectionStatus.stream().map(fileEntity -> + fileUtil.createFileInfo(fileEntity, null)).toList(); + } } diff --git a/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java b/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java new file mode 100644 index 0000000..da8a0f5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/CorsConfig.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.filter.CorsFilter; + +@Configuration +public class CorsConfig { + + @Bean + public CorsFilter corsFilter() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedOrigin("*"); + config.addAllowedMethod("*"); + config.addAllowedHeader("*"); + source.registerCorsConfiguration("/**", config); + + return new CorsFilter(source); + } +} diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index f4465ef..1bdd94a 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -24,7 +24,17 @@ public class HandlerConfig { TariffHandler tariffHandler, TariffInfoHandler tariffInfoHandler, ReferralHandler referralHandler, - DaDataHandler daDataHandler + ResetPasswordHandler resetPasswordHandler, + DaDataHandler daDataHandler, + PaymentHandler paymentHandler, + CostHandler costHandler, + MonitoringHandler monitoringHandler, + ViolationHandler violationHandler, + ViolationStatisticsHandler violationStatisticsHandler, + GlobalSearchHandler globalSearchHandler, + ViolationNotionHandler violationNotionHandler, + ComplaintEntityHandler complaintEntityHandler, + UserInfoHandler userInfoHandler ) { Map map = new HashMap<>(); map.put(20001, login); @@ -35,11 +45,21 @@ public class HandlerConfig { map.put(20007, imageFoundRequestHandler); map.put(20008, authRequestHandler); map.put(20009, verifyRegisterUser); + map.put(20010, resetPasswordHandler); map.put(30000, companyHandler); map.put(30001, tariffHandler); map.put(30002, tariffInfoHandler); map.put(30003, referralHandler); map.put(30004, daDataHandler); + map.put(30005, paymentHandler); + map.put(30007, monitoringHandler); + map.put(30008, costHandler); + map.put(30009, violationHandler); + map.put(30010, violationStatisticsHandler); + map.put(30011, globalSearchHandler); + map.put(30012, violationNotionHandler); + map.put(30013, complaintEntityHandler); + map.put(30014, userInfoHandler); return map; } diff --git a/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java new file mode 100644 index 0000000..0638556 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/ImageMigration.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.configuration; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.file.ImageResizeService; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@Component +@Slf4j +public class ImageMigration { + + @Autowired + private FileEntityRepository fileRepository; + + @Autowired + private ImageResizeService imageResizeService; + + @EventListener(ApplicationReadyEvent.class) + public void migrate() { + List images = fileRepository.findByThumbnailPathIsNull(); + + for (FileEntity file : images) { + try { + Path path = Paths.get(file.getProtectedFilePath()); + if (Files.exists(path)) { + byte[] data = Files.readAllBytes(path); + imageResizeService.generateSizes(file, data); + fileRepository.save(file); + } + } catch (Exception e) { + log.error("Error migrating {}: {}", file.getId(), e.getMessage()); + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java index 6882014..d90475e 100644 --- a/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/file/AsyncConfig.java @@ -21,4 +21,15 @@ public class AsyncConfig { executor.initialize(); return executor; } + + @Bean(name = "taskExecutor") + public Executor taskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(3); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(50); + executor.setThreadNamePrefix("global-search-"); + executor.initialize(); + return executor; + } } diff --git a/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java b/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java deleted file mode 100644 index c64ba5c..0000000 --- a/src/main/java/ru/soune/nocopy/configuration/file/FileStorageConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package ru.soune.nocopy.configuration.file; - -import jakarta.annotation.PostConstruct; -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; - -@Configuration -@ConfigurationProperties(prefix = "file.storage") -@Data -public class FileStorageConfig { - private String basePath; - private long maxFileSize; - private int chunkSize; - private int authTokenLifeHours; - - @PostConstruct - public void init() throws IOException { - Files.createDirectories(Paths.get(basePath)); - } -} diff --git a/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java b/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java index 39eef84..4129f19 100644 --- a/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java +++ b/src/main/java/ru/soune/nocopy/configuration/file/NoCopyInitializer.java @@ -4,16 +4,12 @@ import lombok.AllArgsConstructor; import org.springframework.boot.context.event.ApplicationReadyEvent; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; -import ru.soune.nocopy.service.file.FileProcessingOrchestrator; @Component @AllArgsConstructor public class NoCopyInitializer { - private final FileProcessingOrchestrator orchestrator; - @EventListener(ApplicationReadyEvent.class) public void initializeOnStartup() { - orchestrator.initializeProcessingQueue(); } } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java new file mode 100644 index 0000000..3ca91ff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/configuration/search/SearchProperties.java @@ -0,0 +1,26 @@ +package ru.soune.nocopy.configuration.search; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import java.util.Map; + +@Data +@Component +@ConfigurationProperties(prefix = "search") +public class SearchProperties { + private Map engines; + private ProxyConfig proxy; + + @Data + public static class EngineConfig { + private boolean enabled; + } + + @Data + public static class ProxyConfig { + private boolean enabled; + private String host; + private int port; + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 535302b..1851c60 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -3,6 +3,8 @@ package ru.soune.nocopy.controller; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; import com.vrt.fileprotection.NoCopyCheckResult; +import com.vrt.fileprotection.audio.AudioCheckResult; +import com.vrt.fileprotection.documents.DocumentCheckResult; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.core.io.FileSystemResource; @@ -21,16 +23,20 @@ import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; +import ru.soune.nocopy.entity.company.Company; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; +import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.ProtectionsLimitService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import ru.soune.nocopy.service.register.AuthService; @@ -74,6 +80,10 @@ public class ApiController { private final AuthTokenRepository authTokenRepository; + private final UserRepository userRepository; + + private final CheckCounterService checkCounterService; + private final CloudStorageService cloudStorageService; @PostMapping("/v{version}/data") @@ -156,46 +166,124 @@ public class ApiController { MessageCode.DUPLICATE_FILE_UPLOAD.getDescription(), duplicateData )); + } catch (FileFormatException e){ + return buildErrorResponse(uploadId, chunkNumber, e.getMessage()); } catch (Exception e) { log.error("Error uploading chunk", e); return buildErrorResponse(uploadId, chunkNumber, "Failed to upload chunk: " + e.getMessage()); } } + private final NoCopyFileService noCopyFileService; + @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, @PathVariable String fileId, - @RequestParam(value = "auth_token",required = false) String authToken, + @RequestParam(value = "auth_token", required = false) String authToken, @RequestParam(required = false) List similarityLevels, @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { - SimilarityFilter filter = SimilarityFilter.builder() - .similarityLevels(similarityLevels) - .build(); - Page similarFiles = fileSimilarityService.findSimilarFiles(fileId, filter, pageable, authToken); - String messageDesc; - MessageCode success; + try { + FileEntity fileEntity = fileEntityRepository.findByFileId(fileId); - if (similarFiles.isEmpty()) { - messageDesc = MessageCode.FILE_NOT_FOUND.getDescription(); - success = MessageCode.SUCCESS; - } else { - messageDesc = MessageCode.SIMILAR_FILES_FOUND.getDescription(); - success = MessageCode.SUCCESS; + if (fileEntity == null) { + return ResponseEntity.ok().body(new BaseResponse(0, MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), null)); + } + + BaseResponse response; + if ("image".equals(fileEntity.getMimeType())) { + Page similarFilesPage = fileSimilarityService.findSimilarFiles( + fileId, similarityLevels, pageable, authToken); + response = buildSuccessResponse(similarFilesPage, MessageCode.SIMILAR_FILES_FOUND); + } else { + List results = processNonImageFile(fileEntity, authToken); + log.info("results: {}", results); + response = buildSuccessResponse(results, MessageCode.SIMILAR_FILES_FOUND); + checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); + } + + return ResponseEntity.ok(response); + + } catch(IllegalArgumentException e) { + log.error("Error with file extensions : {}", e.getMessage(), e); + return ResponseEntity.ok().body(new BaseResponse(MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), + MessageCode.FILE_FOR_SEARCH_NOT_VALID.getCode(), + MessageCode.FILE_FOR_SEARCH_NOT_VALID.getDescription(), null)); + } catch (Exception e) { + log.error("Error finding similar files: {}", e.getMessage(), e); + return ResponseEntity.ok().body(new BaseResponse(MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getCode(), + MessageCode.INVALID_FIELD.getDescription(), null)); + } + } + + private List processNonImageFile(FileEntity fileEntity, String authToken) throws Exception { + List results = new ArrayList<>(); + + FileEntity duplicateByHash = fileSimilarityService.findDuplicateByHash(fileEntity.getFilePath(), + fileEntity.getMimeType(), fileEntity.getUserId()); + log.info("duplicateByHash: {}", duplicateByHash); + if (duplicateByHash != null) { + results.add(fileSimilarityService.buildDTO(duplicateByHash)); + + return results; } - Map responseData = new HashMap<>(); - responseData.put("content", similarFiles.getContent()); - responseData.put("page", similarFiles.getNumber()); - responseData.put("size", similarFiles.getSize()); - responseData.put("totalElements", similarFiles.getTotalElements()); - responseData.put("totalPages", similarFiles.getTotalPages()); - responseData.put("hasNext", similarFiles.hasNext()); - responseData.put("hasPrevious", similarFiles.hasPrevious()); + File file = new File(fileEntity.getFilePath()); + FileProtector.Type type = "document".equals(fileEntity.getMimeType()) ? + FileProtector.Type.DOC : FileProtector.Type.AUDIO; - return ResponseEntity.ok() - .body(new BaseResponse(20004, success.getCode(), messageDesc, responseData)); + NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); + log.info("checkResult: {}", checkResult); + + switch (checkResult) { + case DocumentCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + FileEntity entity = fileEntityRepository.findByFileId(info.getId()); + if (entity != null) { + results.add(fileSimilarityService.buildDTO(entity)); + } + } + case AudioCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + FileEntity entity = fileEntityRepository.findByFileId(info.getId()); + if (entity != null) { + results.add(fileSimilarityService.buildDTO(entity)); + } + } + case DocumentCheckResult.Failed failed -> + log.warn("Document check failed: {}", failed.getMessage()); + case AudioCheckResult.Failed failed -> + log.warn("Audio check failed: {}", failed.getMessage()); + default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); + } + + return results; + } + + private BaseResponse buildSuccessResponse(Page page, MessageCode messageCode) { + Map responseData = new HashMap<>(); + responseData.put("content", page.getContent()); + responseData.put("page", page.getNumber()); + responseData.put("size", page.getSize()); + responseData.put("totalElements", page.getTotalElements()); + responseData.put("totalPages", page.getTotalPages()); + responseData.put("hasNext", page.hasNext()); + responseData.put("hasPrevious", page.hasPrevious()); + + return new BaseResponse(20004, messageCode.getCode(), + messageCode.getDescription(), responseData); + } + + private BaseResponse buildSuccessResponse(List list, MessageCode messageCode) { + Map responseData = new HashMap<>(); + responseData.put("content", list); + responseData.put("totalElements", list.size()); + + return new BaseResponse(20004, messageCode.getCode(), + messageCode.getDescription(), responseData); } @GetMapping("/v{version}/files/progress/{uploadId}") @@ -300,28 +388,52 @@ public class ApiController { @PathVariable(required = false) Integer version, @RequestHeader(value = "Authorization", required = false) String tokenHeader) { try { - if (tokenHeader == null) { + if (tokenHeader == null || tokenHeader.isBlank()) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); - errorData.put("fileId", fileId); - errorData.put("version", version); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD.getDescription(), + MessageCode.TOKEN_IS_NULL.getCode(), + MessageCode.TOKEN_IS_NULL.getDescription(), errorData)); } Long userId = authService.useUserAuthToken(tokenHeader); FileEntityResponse entityResponse = fileEntityService.getById(fileId, version); + FileStatus status = entityResponse.getStatus(); - if (!entityResponse.getUserId().equals(userId)) { + if (status.equals(FileStatus.BLOCKED) || status.equals(FileStatus.DELETED) || + status.equals(FileStatus.REMOVED)) { + Map 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 user = userRepository.findById(userId).orElseThrow(); + Company companyUser = user.getCompany(); + + if (companyUser != null && !fileUser.getCompany().getId().equals(companyUser.getId())) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), + errorData)); + } + + if (companyUser == null && !entityResponse.getUserId().equals(userId)) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(new BaseResponse(20004, + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), errorData)); } @@ -330,8 +442,8 @@ public class ApiController { errorData.put("file_status", entityResponse.getStatus()); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.FILE_DELETE.getCode(), + MessageCode.FILE_DELETE.getDescription(), errorData)); } @@ -340,8 +452,8 @@ public class ApiController { errorData.put("onDisk", entityResponse.isExistsOnDisk()); return ResponseEntity.ok().body(new BaseResponse(20004, - MessageCode.FILE_DOWNLOAD_ERROR.getCode(), - MessageCode.FILE_DOWNLOAD_ERROR.getDescription(), + MessageCode.FILE_NOT_EXIST.getCode(), + MessageCode.FILE_NOT_EXIST.getDescription(), errorData)); } @@ -376,7 +488,15 @@ public class ApiController { MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), errorData)); - } catch (NotFoundAuthToken | IOException e) { + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(new BaseResponse(20004, + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + errorData)); + } catch (IOException e) { Map errorData = new HashMap<>(); errorData.put("token", tokenHeader); errorData.put("fileId", fileId); @@ -389,45 +509,6 @@ public class ApiController { } } - @GetMapping("/protect/{fileId}") - public ResponseEntity protect( @PathVariable(required = false) String fileId) { - Optional optionalFileEntity = fileEntityRepository.findById(fileId); - - if (!optionalFileEntity.isPresent()) { - return ResponseEntity.notFound().build(); - } - FileEntity fileEntity = optionalFileEntity.get(); - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - - fileEntity.setProtectionStatus(ProtectionStatus.PROCESSING); - fileEntityRepository.save(fileEntity); - - return ResponseEntity.ok().build(); - } - - @GetMapping("/check/{fileId}/{type}") - public ResponseEntity check(@PathVariable(required = false) String fileId, - @PathVariable(required = false) String type) { - Optional optionalFileEntity = fileEntityRepository.findById(fileId); - - if (!optionalFileEntity.isPresent()) { - return ResponseEntity.notFound().build(); - } - - FileEntity fileEntity = optionalFileEntity.get(); - - Path path = Paths.get(fileEntity.getProtectedFilePath()); - - File file = path.toFile(); - - NoCopyCheckResult noCopyCheckResult = noCopyFileService.checkFile(file, - FileProtector.Type.valueOf(type.toUpperCase())); - - return ResponseEntity.ok().body(noCopyCheckResult); - } - @GetMapping("/check/file_stats") public ResponseEntity checkFileProtectStats(@RequestHeader("Authorization") String tokenHeader) { String token = tokenHeader.replace("Bearer ", ""); diff --git a/src/main/java/ru/soune/nocopy/controller/FileController.java b/src/main/java/ru/soune/nocopy/controller/FileController.java index bec838c..6beca5a 100644 --- a/src/main/java/ru/soune/nocopy/controller/FileController.java +++ b/src/main/java/ru/soune/nocopy/controller/FileController.java @@ -1,11 +1,11 @@ package ru.soune.nocopy.controller; +import com.vrt.NoCopyFileService; +import com.vrt.fileprotection.FileProtector; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.dto.file.CheckStatus; import ru.soune.nocopy.entity.file.FileEntity; @@ -16,10 +16,17 @@ import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.file.FileStorageService; -import org.springframework.http.ContentDisposition; +import ru.soune.nocopy.service.file.ImageResizeService; +import ru.soune.nocopy.util.FileUtil; + +import java.io.FileNotFoundException; +import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; @RestController @RequestMapping("/api/files") @@ -38,43 +45,96 @@ public class FileController { @Autowired private UserRepository userRepository; + @Autowired + private ImageResizeService imageResizeService; + + @Autowired + private NoCopyFileService noCopyFileService; + + @Autowired + private FileUtil fileUtil; + @GetMapping("/public/{fileId}") - public ResponseEntity getPublicFile( - @PathVariable String fileId) throws IOException { + public ResponseEntity getPublicFile(@PathVariable String fileId) { + try { + FileEntity fileEntity = fileRepository.findById(fileId) + .orElseThrow(() -> new FileNotFoundException("Not found: " + fileId)); - FileEntity fileEntity = fileRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("File not found")); + Path filePath = Paths.get(fileEntity.getFilePath()); + if (!Files.exists(filePath)) { + log.error("Not on disk: {}", fileEntity.getFilePath()); + return ResponseEntity.notFound().build(); + } - Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); + Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath()); - ContentDisposition contentDisposition = ContentDisposition.inline() - .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) - .build(); + MediaType mediaType = getMediaType(fileEntity); - return ResponseEntity.ok() - .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) - .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) - .body(resource); + ContentDisposition contentDisposition = ContentDisposition.inline() + .filename(fileEntity.getOriginalFileName(), StandardCharsets.UTF_8) + .build(); + + return ResponseEntity.ok() + .contentType(mediaType) + .contentLength(Files.size(filePath)) + .header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition.toString()) + .header(HttpHeaders.CACHE_CONTROL, "public, max-age=3600") + .body(resource); + + } catch (FileNotFoundException e) { + log.warn("Not on disk: {}", fileId); + return ResponseEntity.notFound().build(); + } catch (IOException e) { + log.error("Read file exception: {}", fileId, e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } } - @GetMapping("/protected/{fileId}") + private MediaType getMediaType(FileEntity fileEntity) { + try { + return MediaType.parseMediaType(fileEntity.getMimeType()); + } catch (InvalidMediaTypeException e) { + String extension = fileEntity.getFileExtension().toLowerCase(); + switch (extension) { + case "jpg": + case "jpeg": + return MediaType.IMAGE_JPEG; + case "png": + return MediaType.IMAGE_PNG; + case "pdf": + return MediaType.APPLICATION_PDF; + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } + } + + @GetMapping("/protected/{fileId}/{type}") public ResponseEntity getProtectedFile( - @PathVariable String fileId) throws IOException { + @PathVariable String fileId, @PathVariable String type) throws IOException { FileEntity fileEntity = fileRepository.findById(fileId) .orElseThrow(() -> new RuntimeException("File not found")); - Resource resource = fileStorageService.loadFileAsResource(fileEntity.getProtectedFilePath()); + String path = imageResizeService.getPath(fileEntity, type); + + Resource resource = fileStorageService.loadFileAsResource(path); if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED || fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) { throw new RuntimeException("File is not protected"); } + String fileName = fileEntity.getOriginalFileName().replace("." + + fileEntity.getFileExtension(), "") + "_nocopy_protected" + + "." + fileEntity.getFileExtension(); + String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8) + .replace("+", "%20"); + return ResponseEntity.ok() .contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension())) .header(HttpHeaders.CONTENT_DISPOSITION, - "inline; filename=\"" + fileEntity.getOriginalFileName() + "\"") + "inline; filename*=UTF-8''" + encodedFileName) .body(resource); } @@ -94,4 +154,16 @@ public class FileController { .ok() .body(protectedFileCheck); } + + @PostMapping("protect/{fileId}/{convertTo}") + public ResponseEntity 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); + } } diff --git a/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java new file mode 100644 index 0000000..2765f28 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/GlobalSearchController.java @@ -0,0 +1,173 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; +import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; +import ru.soune.nocopy.dto.search.GlobalSearchStatusResponse; +import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.search.GlobalSearchTask; +import ru.soune.nocopy.entity.search.SearchStatus; +import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.search.GlobalSearchService; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.util.*; + +@Slf4j +@RestController +@RequestMapping("/api/v1/global-search") +@RequiredArgsConstructor +public class GlobalSearchController { + + private final AuthService authService; + + private final GlobalSearchService globalSearchService; + + private final GlobalSearchTaskRepository globalSearchTaskRepository; + + private final GlobalSearchTaskRepository searchTaskRepository; + + private final ViolationService violationService; + + @PostMapping("/start") + public ResponseEntity startSearch( + @RequestBody GlobalSearchStartRequest request, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + GlobalSearchStartResponse response = new GlobalSearchStartResponse(); + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + List filesToProcess = globalSearchService.getFilesToProcess(request, userId); + + if (filesToProcess.isEmpty()) return ResponseEntity.ok().body("Files for search not found"); + + String taskId = globalSearchService.startSearch(request, userId, filesToProcess); + + Optional taskOptional = globalSearchTaskRepository.findById(taskId); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + GlobalSearchTask task = taskOptional.orElseThrow(); + + response.setTaskId(taskId); + response.setStatus(SearchStatus.ACCEPTED.name()); + response.setTotalFiles(task.getTotalFiles()); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(response); + } + + @GetMapping("/actual-task") + public ResponseEntity getStatus(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + Optional taskOptional; + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + taskOptional = searchTaskRepository.findByUserIdAndStatus(userId, + "PROCESSING"); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(taskOptional.get()); + } + + @GetMapping("/violation-summary") + public ResponseEntity getSummary(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + List fileViolationsSummary; + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + fileViolationsSummary = + violationService.getFileViolationsSummary(userId); + + if (fileViolationsSummary.isEmpty()) return ResponseEntity.ok().body("Violations summary not found"); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(fileViolationsSummary); + } + + @GetMapping("/status/{taskId}") + public ResponseEntity getStatus(@PathVariable String taskId, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { + + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + GlobalSearchStatusResponse response; + + try { + Long userId = authService.useUserAuthToken(tokenHeader); + + Optional taskOptional = searchTaskRepository.findByTaskIdAndUserId(taskId, userId); + + if (taskOptional.isEmpty()) return ResponseEntity.ok().body("Task not found"); + + GlobalSearchTask task = taskOptional.orElseThrow(); + + int progress = task.getTotalFiles() > 0 ? task.getProcessedFiles() * 100 / task.getTotalFiles() : 0; + + response = new GlobalSearchStatusResponse(); + response.setTaskId(taskId); + response.setStatus(task.getStatus()); + response.setProgress(progress); + } catch (NotFoundAuthToken e) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(response); + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/PaymentController.java b/src/main/java/ru/soune/nocopy/controller/PaymentController.java new file mode 100644 index 0000000..47407b5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/PaymentController.java @@ -0,0 +1,119 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.search.GlobalSearchStartResponse; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.exception.PaymentNotFoundException; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.service.payment.PaymentService; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/payments") +@RequiredArgsConstructor +@Slf4j +public class PaymentController { + + private final PaymentService paymentService; + + private final AuthService authService; + + @PostMapping("/create") + public ResponseEntity createPayment(@RequestParam String email, @RequestParam Long tariffId, + @RequestParam String operationType, @RequestParam String operationUuid) { + try { + Payment payment = paymentService.createPayment(email, tariffId, operationType, operationUuid); + + return ResponseEntity.ok(payment); + } catch (TariffNotFoundException e) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "Tariff not found", "message", e.getMessage())); + } catch (UserNotFoundException e) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .body(Map.of("error", "User not found", "message", e.getMessage())); + } catch (Exception e) { + return ResponseEntity + .status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(Map.of("error", "Internal server error", "message", e.getMessage())); + } + } + + @PostMapping("webhook/yookassa") + public void handleYooKassaNotification(@RequestBody Map notification) { + try { + paymentService.handlePaymentNotification(notification); + log.info("Notification from yookassa: " + notification); + } catch (PaymentNotFoundException e) { + log.error("Error processing payment notification", e); + } + } + + @PostMapping("/auto-renewal") + public ResponseEntity disableAutoRenewal(@RequestParam(value = "renewal") Boolean renewal, + @RequestHeader(value = "Authorization", required = false) String tokenHeader) { + try { + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + paymentService.changeAutoRenewal(authService.useUserAuthToken(tokenHeader), renewal); + return ResponseEntity.ok(Map.of("message", "Auto-renewal disabled")); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } + } + + @DeleteMapping("/methods/{paymentMethodId}") + public ResponseEntity deletePaymentMethod(@RequestHeader(value = "Authorization", required = false) String tokenHeader, + @PathVariable String paymentMethodId) { + try { + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + paymentService.deletePaymentMethod(authService.useUserAuthToken(tokenHeader), paymentMethodId); + return ResponseEntity.ok(Map.of("message", "Payment method deleted")); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } catch (RuntimeException e) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Map.of("error", e.getMessage())); + } + } + + @GetMapping("/methods") + public ResponseEntity getPaymentMethods(@RequestHeader(value = "Authorization", required = false) String tokenHeader) { + try { + if (tokenHeader == null || tokenHeader.isBlank()) { + Map errorData = new HashMap<>(); + errorData.put("token", tokenHeader); + + return ResponseEntity.ok().body(Map.of("error", errorData)); + } + + return ResponseEntity.ok(paymentService.getUserPaymentMethods( + authService.useUserAuthToken(tokenHeader))); + } catch (UserNotFoundException e) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(Map.of("error", e.getMessage())); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/PayoutController.java b/src/main/java/ru/soune/nocopy/controller/PayoutController.java new file mode 100644 index 0000000..1b57c74 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/PayoutController.java @@ -0,0 +1,216 @@ +package ru.soune.nocopy.controller; + + +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.payout.*; +import ru.soune.nocopy.entity.payout.BankTransferPayoutMethod; +import ru.soune.nocopy.entity.payout.CardPayoutMethod; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutType; +import ru.soune.nocopy.entity.user.AuthToken; +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.PendingPayoutExistsException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.payout.PayoutMethodService; +import ru.soune.nocopy.service.payout.PayoutRequestService; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@RestController +@RequestMapping("/api/payouts") +@RequiredArgsConstructor +@Slf4j +public class PayoutController { + + private final PayoutRequestService payoutRequestService; + + private final PayoutMethodService payoutMethodService; + + private final AuthTokenRepository authTokenRepository; + + /* + Create payout + */ + @PostMapping("/create-request") + public ResponseEntity createPayoutRequest(@RequestHeader("Authorization") String tokenHeader, + @Valid @RequestBody PayoutRequestDTO request) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + try { + PayoutRequest payoutRequest = payoutRequestService.createPayoutRequest(user.getId(), request); + return ResponseEntity.status(HttpStatus.CREATED).body(payoutRequest); + } catch (InsufficientFundsException | PendingPayoutExistsException | InvalidPayoutMethodException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(e.getMessage()); + } + } + + /* + Completed payout + */ + @PostMapping("/requests/{requestId}/process") + public ResponseEntity processPayoutRequest(@PathVariable Long requestId) { + PayoutRequest processed = payoutRequestService.processRequest(requestId); + + return ResponseEntity.ok(processed); + } + + /* + Cancel payout + */ + @PostMapping("/requests/{requestId}/cancel") + public ResponseEntity cancelPayoutRequest(@PathVariable Long requestId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + PayoutRequest cancelled = payoutRequestService.cancelPayoutRequest(user.getId(), requestId); + + return ResponseEntity.ok(cancelled); + } + + /* + Get user payouts + */ + @GetMapping("/requests") + public ResponseEntity> getUserRequests(@RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutRequestService.getUserPayoutRequests(user.getId())); + } + + /* + Get payout by id + */ + @GetMapping("/requests/{requestId}") + public ResponseEntity getPayoutRequest(@PathVariable Long requestId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutRequestService.getPayoutRequest(user.getId(), requestId)); + } + + /* + Get payout by id + */ + @GetMapping("user/payout-methods") + public ResponseEntity> getUserMethods(@RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + return ResponseEntity.ok(payoutMethodService.getUserMethods(user.getId())); + } + + /* + Get payout-methods + */ + @GetMapping("/payout-methods") + public ResponseEntity> getAvailableMethodTypes() { + List types = Arrays.stream(PayoutType.values()) + .map(type -> PayoutMethodTypeDTO.builder() + .code(type.name()) + .name(type.getDisplayName()) + .build()) + .collect(Collectors.toList()); + + return ResponseEntity.ok(types); + } + /* + add card method by userId + */ + @PostMapping("/payout-method/card") + public ResponseEntity addCardMethod(@Valid @RequestBody AddCardRequest request, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + CardPayoutMethod method = payoutMethodService.addCardMethod(user.getId(), request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(payoutMethodService.convertToDto(method)); + } + + /* + add bank payout-method + */ + @PostMapping("/payout-method/bank-transfer") + public ResponseEntity addBankTransferMethod(@Valid @RequestBody AddBankTransferRequest request, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + BankTransferPayoutMethod method = payoutMethodService.addBankTransferMethod(user.getId(), request); + + return ResponseEntity.status(HttpStatus.CREATED) + .body(payoutMethodService.convertToDto(method)); + } + + + /* + set default bank payout-method + */ + @PutMapping("/payout-method/{methodId}/default") + public ResponseEntity setDefaultMethod(@PathVariable Long methodId, + @RequestHeader("Authorization") String tokenHeader) { + String token = tokenHeader.replace("Bearer ", ""); + + Optional tokenOptional = authTokenRepository.findByToken(token); + + AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); + + User user = authToken.getUser(); + + payoutMethodService.setDefaultMethod(user.getId(), methodId); + + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java new file mode 100644 index 0000000..d18596a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/controller/SearchSettingsController.java @@ -0,0 +1,58 @@ +package ru.soune.nocopy.controller; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.configuration.search.SearchProperties; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.config.SearchSettingsDto; + +import java.util.Map; + + +@Slf4j +@RestController +@RequestMapping("/api/search/settings") +@RequiredArgsConstructor +public class SearchSettingsController { + + private final SearchProperties searchProperties; + + @GetMapping + public ResponseEntity getSettings() { + SearchSettingsDto dto = new SearchSettingsDto(); + + dto.setEngines(Map.of( + "yandex", searchProperties.getEngines().get("yandex").isEnabled(), + "google", searchProperties.getEngines().get("google").isEnabled())); + + dto.setProxyEnabled(searchProperties.getProxy().isEnabled()); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Success") + .messageBody(dto) + .build()); + } + + @PutMapping + public ResponseEntity updateSettings(@RequestBody SearchSettingsDto settings) { + if (settings.getEngines() != null) { + settings.getEngines().forEach((key, value) -> { + if (searchProperties.getEngines().containsKey(key)) { + searchProperties.getEngines().get(key).setEnabled(value); + } + }); + } + + if (settings.getProxyEnabled() != null) { + searchProperties.getProxy().setEnabled(settings.getProxyEnabled()); + } + + log.info("Settings updated: {}", settings); + + return ResponseEntity.ok().body(BaseResponse.builder() + .messageDesc("Settings updated") + .build()); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/BaseResponse.java b/src/main/java/ru/soune/nocopy/dto/BaseResponse.java index 75f1543..6a4e3b0 100644 --- a/src/main/java/ru/soune/nocopy/dto/BaseResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/BaseResponse.java @@ -3,10 +3,12 @@ package ru.soune.nocopy.dto; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; @Data @AllArgsConstructor +@Builder @JsonFormat(with = JsonFormat.Feature.ACCEPT_CASE_INSENSITIVE_PROPERTIES) public class BaseResponse { @JsonProperty("msg_id") diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index b4c1c01..34e903d 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -2,20 +2,27 @@ package ru.soune.nocopy.dto; public enum MessageCode { SUCCESS(0, "Operation successful"), + SUCCESS_MODERATION(0, "File moderated successfully"), REG_EMAIL_EXISTS(1, "Email already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), 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_TOKEN(2, "Invalid token"), + MAIL_VERIFIED_NULL(2, "Mail verified null"), + INVALID_TOKEN(2, "Token not found or time expired"), TOKEN_IS_ALIVE(2, "Token is alive"), INVALID_ACTION(2, "Invalid action"), FILE_UPLOAD_ERROR(2, "File upload error"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), FILE_DOWNLOAD_ERROR(2, "File download error"), + FILE_NOT_EXIST(2, "File not exist on disk"), + FILE_DELETE(2, "File was deleted"), + USER_NOT_HAD_PERMISSION(2, "User not have permission for file"), USER_NOT_VERIFIED(2, "User not verified"), PERMISSION_NOT_FOUND(2, "Permission not found"), USER_NOT_FOUND(2, "User not found"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), + TOKEN_IS_NULL(2, "Token field is null"), IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INCOMPLETE_UPLOAD(2, "Not load all chunks"), @@ -40,9 +47,19 @@ public enum MessageCode { RESOURCE_NOT_FOUND(4, "Resource not found"), INTERNAL_ERROR(4, "Internal server error"), COMPANY_NOT_FOUND(4, "Company not found"), + PAYMENT_NOT_FOUND(4, "Payment not found"), COMPANY_ALREADY_EXISTS(2, "Company already exists"), USER_LIMIT_IS_OVER(2, "Over user limits"), - ERROR_TARIFF_INFO(2, "Erorr with tariff info"); + MONITORING_TYPE_NOT_FOUND(4, "Monitoring type not found"), + ERROR_TARIFF_INFO(2, "Erorr with tariff info"), + 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" + + "more than max valid"), + USER_NOT_ACTIVE(2, "User not active"), + NOTION_NOT_FOUND(4, "Notion not found"), + NOT_FOUND(4, "Notion not found"), + MESSAGE_IS_REQUIRED_FOR_NOTION(4, "Message is required for notion"); private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java new file mode 100644 index 0000000..0de3cce --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintRequest.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java new file mode 100644 index 0000000..1fe00ac --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/complaint/ComplaintResponse.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java b/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java new file mode 100644 index 0000000..66a3b17 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/cost/CostRequest.java @@ -0,0 +1,18 @@ +package ru.soune.nocopy.dto.cost; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class CostRequest { + String action; + + @JsonProperty("file_type") + String fileType; + + @JsonProperty("count_files") + Integer countFilesForProtect; + + @JsonProperty("auth_token") + String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java new file mode 100644 index 0000000..7285870 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/cost/CostResponse.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.cost; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class CostResponse { + private int cost; + + private Boolean success; + + @JsonProperty("max_files_for_check") + private Integer maxFilesForCheck; + + @JsonProperty("count_file") + private Integer countFile; + + @JsonProperty("tokens_count") + private int needTokensCount; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java b/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java new file mode 100644 index 0000000..6c2bc7c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealInfo.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java b/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java new file mode 100644 index 0000000..1590820 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealRequest.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java b/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java new file mode 100644 index 0000000..60739b6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/AppealResponse.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java index 560fec3..68df125 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityRequest.java @@ -5,6 +5,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.file.FileStatus; @Data @Builder @@ -34,4 +35,40 @@ public class FileEntityRequest { @JsonProperty("token") private String token; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_order") + private String sortOrder; + + @JsonProperty("type") + private String type; + + @JsonProperty("file_status") + private FileStatus fileStatus; + + @JsonProperty("date_filter") + 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; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java index e57164c..60c65b6 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileEntityResponse.java @@ -20,6 +20,7 @@ public class FileEntityResponse { private String storedFileName; private String filePath; private String protectedFilePath; + private String thumbnailFileUrl; private Long fileSize; private String mimeType; private String fileExtension; @@ -40,4 +41,5 @@ public class FileEntityResponse { private String fileFormat; private Integer checksCount; private LocalDateTime fileUploadDate; + private String monitoring; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java b/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java index bbc5a5f..d8c3a0d 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileListResponse.java @@ -30,4 +30,10 @@ public class FileListResponse { @JsonProperty("page_size") private Integer pageSize; + + @JsonProperty("sort_by") + private String sortBy; + + @JsonProperty("sort_order") + private String sortOrder; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java new file mode 100644 index 0000000..8ffc794 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/FileModerationInfo.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/FileUploadRequest.java b/src/main/java/ru/soune/nocopy/dto/file/FileUploadRequest.java index 3b72bb4..030ef24 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/FileUploadRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/file/FileUploadRequest.java @@ -28,4 +28,7 @@ public class FileUploadRequest { @JsonProperty("token") private String token; + + @JsonProperty("convertTo") + private String convertTo; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java new file mode 100644 index 0000000..58a227c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/ModerationActionRequest.java @@ -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; +} + + + + + + + diff --git a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java index 44d8495..d94b0c2 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/file/SimilarFileDTO.java @@ -19,4 +19,6 @@ public class SimilarFileDTO { ProtectionStatus status; LocalDateTime uploadDate; String url; + Boolean owner; + String fileStatus; } diff --git a/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java b/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java new file mode 100644 index 0000000..81ba77f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/file/SortOrder.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.dto.file; + +import lombok.Getter; + +@Getter +public enum SortOrder { + ASC("asc"), + DESC("desc"); + + private final String value; + + SortOrder(String value) { + this.value = value; + } + + public static SortOrder fromString(String value) { + if (value == null) return ASC; + for (SortOrder order : SortOrder.values()) { + if (order.value.equalsIgnoreCase(value)) { + return order; + } + } + return ASC; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java index f064ad3..3542068 100644 --- a/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java +++ b/src/main/java/ru/soune/nocopy/dto/file/YandexSearchResponse.java @@ -12,11 +12,6 @@ public class YandexSearchResponse { @JsonProperty("images") private List images; -// @JsonProperty("images") -// public void setImages(List images) { -// this.images = images; -// } - @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class ImageResult { diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java new file mode 100644 index 0000000..9c9dda1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/GetViolationsRequest.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GetViolationsRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("page") + private Integer page; + + @JsonProperty("page_size") + private Integer pageSize; + + @JsonProperty("mark_as_read") + private boolean markAsRead; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java new file mode 100644 index 0000000..818849d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/MonitoringStatusResponse.java @@ -0,0 +1,40 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; + +/** + * Response for status monitoring + */ +@Data +@Builder +public class MonitoringStatusResponse { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("file_name") + private String fileName; + + @JsonProperty("monitoring_type") + private MonitoringType monitoringType; + + @JsonProperty("next_run") + private LocalDateTime nextRun; + + @JsonProperty("last_run") + private LocalDateTime lastRun; + + @JsonProperty("last_run_status") + private String lastRunStatus; + + @JsonProperty("is_active") + private boolean isActive; + + @JsonProperty("need_add_tokens") + private boolean needAddTokens; +} diff --git a/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java new file mode 100644 index 0000000..bf49b47 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/monitoring/SetMonitoringRequest.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.monitoring; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +/** + Set monitoring request + */ +@Data +public class SetMonitoringRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("monitoring_type") + private String monitoringType; + + @JsonProperty("auth_token") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java new file mode 100644 index 0000000..9c25f1f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentMethodDTO.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PaymentMethodDTO { + private String paymentMethodId; + private String lastFour; + private String cardType; + private String expiryMonth; + private String expiryYear; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java new file mode 100644 index 0000000..7755dcf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentRequest.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PaymentRequest { + private String email; + private String paymentUuid; + private String action; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java b/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java new file mode 100644 index 0000000..86544e3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payment/PaymentResponse.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.payment; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.payment.Payment; + +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class PaymentResponse { + List payments; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java b/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java new file mode 100644 index 0000000..39a5db3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/AddBankTransferRequest.java @@ -0,0 +1,44 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AddBankTransferRequest { + + @NotBlank(message = "Bank name is required") + @Size(min = 2, max = 200, message = "Bank name must be between 2 and 200 characters") + private String bankName; + + @NotBlank(message = "BIC is required") + @Pattern(regexp = "^[0-9]{9}$", message = "BIC must contain 9 digits") + private String bic; + + @Pattern(regexp = "^[0-9]{20}$", message = "Correspondent account must contain 20 digits") + private String correspondentAccount; + + @NotBlank(message = "Account number is required") + @Pattern(regexp = "^[0-9]{20}$", message = "Account number must contain 20 digits") + private String accountNumber; + + @NotBlank(message = "Account holder name is required") + @Size(min = 3, max = 200, message = "Account holder name must be between 3 and 200 characters") + private String accountHolder; + + @Pattern(regexp = "^[0-9]{10}$|^[0-9]{12}$", message = "INN must contain 10 or 12 digits") + private String inn; + + @Pattern(regexp = "^[0-9]{9}$", message = "KPP must contain 9 digits") + private String kpp; + + @Builder.Default + private boolean saveAsDefault = false; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java b/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java new file mode 100644 index 0000000..0f9f355 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/AddCardRequest.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AddCardRequest { + + @NotBlank(message = "Card number required") + @Pattern(regexp = "^[0-9]{16}$", message = "Card number 16 cipher") + private String cardNumber; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java new file mode 100644 index 0000000..51bde8c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodDTO.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.dto.payout; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodDTO { + private Long id; + private String type; + private String displayName; + private String maskedDetails; + private String cardNumber; + private boolean isDefault; + private boolean isVerified; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java new file mode 100644 index 0000000..df72994 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodResponse.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.payout; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodResponse { + private Long id; + private String type; + private String displayName; + private String maskedDetails; + private boolean isDefault; + private boolean isVerified; + private LocalDateTime createdAt; + private String bankName; + private String cardBrand; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java new file mode 100644 index 0000000..d1f0884 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutMethodTypeDTO.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.payout; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethodTypeDTO { + private String code; + private String name; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java new file mode 100644 index 0000000..609a3ae --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutRequestDTO.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.dto.payout; + +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import java.math.BigDecimal; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PayoutRequestDTO { + + @NotNull(message = "Sum of pay is required") + @DecimalMin(value = "1000.00", message = "Min sum for pay 1000") + private BigDecimal amount; + + @NotNull(message = "Способ выплаты обязателен") + private Long payoutMethodId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java b/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java new file mode 100644 index 0000000..243db58 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/payout/PayoutResponse.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.dto.payout; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutRequestStatus; +import ru.soune.nocopy.entity.payout.PayoutType; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor + +public class PayoutResponse { + private Long id; + private BigDecimal amount; + private String formattedAmount; + private PayoutRequestStatus status; + private PayoutType payoutType; + private String payoutMethodDisplay; + private LocalDateTime createdAt; + private String formattedCreatedAt; + private LocalDateTime processedAt; + private LocalDateTime completedAt; + private String transactionId; + private String rejectionReason; + private boolean canCancel; + + public static PayoutResponse fromEntity(PayoutRequest request) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + + return PayoutResponse.builder() + .id(request.getId()) + .amount(request.getAmount()) + .formattedAmount(String.format("%.2f ₽", request.getAmount())) + .status(request.getStatus()) + .payoutType(request.getPayoutType()) + .payoutMethodDisplay(request.getPayoutMethod() != null ? + request.getPayoutMethod().getDisplayName() : null) + .createdAt(request.getCreatedAt()) + .formattedCreatedAt(request.getCreatedAt() != null ? + request.getCreatedAt().format(formatter) : null) + .processedAt(request.getProcessAt()) + .completedAt(request.getCompletedAt()) + .rejectionReason(request.getRejectionReason()) + .canCancel(request.getStatus() == PayoutRequestStatus.PENDING) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index 15d1c47..70f7d2c 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.dto.register; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; import jakarta.validation.constraints.*; import lombok.Data; @@ -13,6 +14,8 @@ public class RegRequest { private String companyName; + private String inn; + @NotEmpty(message = "Need account type") private String accountType; @@ -31,4 +34,13 @@ public class RegRequest { private String authToken; private String referralLink; + + @JsonProperty("mail_verified") + private String mailVerified; + + @JsonProperty("ip") + private String ip; + + @JsonProperty("user_agent") + private String userAgent; } diff --git a/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java new file mode 100644 index 0000000..60ab799 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordRequest.java @@ -0,0 +1,24 @@ +package ru.soune.nocopy.dto.register; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ResetPasswordRequest { + + @JsonProperty("email") + private String email; + + @JsonProperty("verifyToken") + private String verifyToken; + + @JsonProperty("action") + private String action; + + @JsonProperty("password") + private String password; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java new file mode 100644 index 0000000..b3d7122 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/ResetPasswordResponse.java @@ -0,0 +1,13 @@ +package ru.soune.nocopy.dto.register; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ResetPasswordResponse { + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("authToken") + private String authToken; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java new file mode 100644 index 0000000..31d1358 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchFileResult.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; +import ru.soune.nocopy.dto.file.YandexSearchResponse; + +import java.util.List; + +@Data +public class GlobalSearchFileResult { + private String fileId; + private String fileName; + private String thumbnail; + private String fileStatus; + private List images; + private int page; + private int pageSize; + private int totalResults; + private int totalPages; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java new file mode 100644 index 0000000..9e1021a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartRequest.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class GlobalSearchStartRequest { + private String searchType; + + private List fileIds; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java new file mode 100644 index 0000000..9b55562 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStartResponse.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; + +@Data +public class GlobalSearchStartResponse { + private String taskId; + + private String status; + + private Integer totalFiles; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java new file mode 100644 index 0000000..ba11c41 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsRequest.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class GlobalSearchStatisticsRequest { + + @JsonProperty("token") + private String token; + + @JsonProperty("limit") + private Integer limit = 10; + + @JsonProperty("days") + private Integer days; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java new file mode 100644 index 0000000..0a199b1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatisticsResponse.java @@ -0,0 +1,87 @@ +package ru.soune.nocopy.dto.search; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.search.GlobalSearchTask; + +import java.time.format.DateTimeFormatter; +import java.util.List; + +@Data +@Builder +public class GlobalSearchStatisticsResponse { + + @JsonProperty("total_searches") + private long totalSearches; + + @JsonProperty("searches_by_status") + private StatusCountDto searchesByStatus; + + @JsonProperty("recent_searches") + private List recentSearches; + + @Data + @Builder + public static class StatusCountDto { + @JsonProperty("processing") + private long processing; + + @JsonProperty("completed") + private long completed; + + @JsonProperty("failed") + private long failed; + + @JsonProperty("total") + private long total; + } + + @Data + @Builder + public static class SearchStatDto { + @JsonProperty("task_id") + private String taskId; + + @JsonProperty("search_type") + private String searchType; + + @JsonProperty("status") + private String status; + + @JsonProperty("total_files") + private Integer totalFiles; + + @JsonProperty("violations_found") + private long violationsFound; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("duration_seconds") + private Long durationSeconds; + + @JsonProperty("file_ids") + private List fileIds; + + public static SearchStatDto fromEntity(GlobalSearchTask task, long violationsCount, List fileIds ) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + Long duration = null; + if (task.getUpdatedAt() != null && task.getCreatedAt() != null) { + duration = java.time.Duration.between(task.getCreatedAt(), task.getUpdatedAt()).getSeconds(); + } + + return SearchStatDto.builder() + .taskId(task.getTaskId()) + .searchType(task.getSearchType()) + .status(task.getStatus()) + .totalFiles(task.getTotalFiles()) + .violationsFound(violationsCount) + .fileIds(fileIds) + .createdAt(task.getCreatedAt() != null ? task.getCreatedAt().format(formatter) : null) + .durationSeconds(duration) + .build(); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java new file mode 100644 index 0000000..0634aa1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/GlobalSearchStatusResponse.java @@ -0,0 +1,18 @@ +package ru.soune.nocopy.dto.search; + +import lombok.Data; + +import java.util.List; + +@Data +public class GlobalSearchStatusResponse { + private String taskId; + + private String status; + + private String searchType; + + private Integer progress; + + private List results; +} diff --git a/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java new file mode 100644 index 0000000..ffacfa0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/search/config/SearchSettingsDto.java @@ -0,0 +1,11 @@ +package ru.soune.nocopy.dto.search.config; + + +import lombok.Data; +import java.util.Map; + +@Data +public class SearchSettingsDto { + private Map engines; + private Boolean proxyEnabled; +} diff --git a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java index 0748a9f..3c7627a 100644 --- a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffDTO.java @@ -16,4 +16,5 @@ public class TariffDTO { private int maxFilesCount; private Long diskSize; private Long maxUsers; + private String tariffTerm; } diff --git a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java index b623e6a..524cccf 100644 --- a/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/tarriff/TariffRequest.java @@ -49,4 +49,7 @@ public class TariffRequest { @JsonProperty("user_token") private String userToken; + + @JsonProperty("tariff_term") + private String tariffTerm; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java new file mode 100644 index 0000000..12fa859 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/user/UserAdditionalInfoBody.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class UserAdditionalInfoBody { + + @JsonProperty("action") + private String action; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java new file mode 100644 index 0000000..c3c06fb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/DeleteNotionResponse.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class DeleteNotionResponse { + private Long notionId; + private String message; + private boolean deleted; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java new file mode 100644 index 0000000..e4a5129 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/FileViolationSummaryDTO.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.file.FileStatus; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +public class FileViolationSummaryDTO { + private String fileId; + private Integer supportId; + private String fileName; + private Long fileSize; + private String mimeType; + private FileStatus status; + private LocalDateTime createdAt; + private long violationCount; + private LocalDateTime latestViolationDate; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java new file mode 100644 index 0000000..fa6ee64 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationInfoDTO.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationInfoDTO { + private Long violationId; + private String url; + private String pageUrl; + private String host; + private LocalDateTime createdDate; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java new file mode 100644 index 0000000..e431730 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionRequest.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Data; + +@Data +public class ViolationNotionRequest { + private String token; + private String action; + private Long violationId; + private Long notionId; + private String message; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java new file mode 100644 index 0000000..eea7e86 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionResponse.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.dto.violation; +import lombok.Builder; +import lombok.Data; +import java.time.LocalDateTime; + +@Data +@Builder +public class ViolationNotionResponse { + private Long id; + private String message; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private UserInfo user; + private ViolationInfo violation; + + @Data + @Builder + public static class UserInfo { + private Long id; + private String fullName; + private String email; + } + + @Data + @Builder + public static class ViolationInfo { + private Long id; + private String url; + private String host; + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java new file mode 100644 index 0000000..eead976 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationNotionsListResponse.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.dto.violation; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Data +@Builder +public class ViolationNotionsListResponse { + private Long violationId; + private String url; + private String host; + private int totalCount; + private List notions; +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java new file mode 100644 index 0000000..1edf968 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationRequest.java @@ -0,0 +1,42 @@ +package ru.soune.nocopy.dto.violation; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ViolationRequest { + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("violation_id") + private Long violationId; + + @JsonProperty("page") + private Integer page = 0; + + @JsonProperty("size") + private Integer size = 10; + + @JsonProperty("sort_direction") + private String sortDirection = "desc"; + + @JsonProperty("status") + private String status; + + @JsonProperty("start_date") + private String startDate; + + @JsonProperty("end_date") + private String endDate; + + @JsonProperty("group_by") + private String groupBy; + + @JsonProperty("action") + private String action; + + @JsonProperty("token") + private String token; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java new file mode 100644 index 0000000..94f1321 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationResponse.java @@ -0,0 +1,77 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; +import java.util.List; + +@Data +@Builder +public class ViolationResponse { + + @JsonProperty("violations") + private List violations; + + @JsonProperty("total_elements") + private long totalElements; + + @JsonProperty("total_pages") + private int totalPages; + + @JsonProperty("current_page") + private int currentPage; + + @JsonProperty("page_size") + private int pageSize; + + @JsonProperty("has_next") + private boolean hasNext; + + @JsonProperty("has_previous") + private boolean hasPrevious; + + @Data + @Builder + public static class ViolationDto { + + @JsonProperty("id") + private Long id; + + @JsonProperty("url") + private String url; + + @JsonProperty("page_url") + private String pageUrl; + + @JsonProperty("page_title") + private String pageTitle; + + @JsonProperty("host") + private String host; + + @JsonProperty("status") + private String status; + + @JsonProperty("created_date") + private LocalDateTime createdDate; + + @JsonProperty("file_id") + private String fileId; + + public static ViolationDto fromEntity(Violation violation) { + return ViolationDto.builder() + .id(violation.getId()) + .url(violation.getUrl()) + .pageUrl(violation.getPageUrl()) + .pageTitle(violation.getPageTitle()) + .host(violation.getHost()) + .status(violation.getStatus()) + .createdDate(violation.getCreatedDate()) + .fileId(violation.getFileEntity() != null ? violation.getFileEntity().getId() : null) + .build(); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java new file mode 100644 index 0000000..04cbac6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsRequest.java @@ -0,0 +1,20 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class ViolationStatisticsRequest { + + @JsonProperty("token") + private String token; + + @JsonProperty("file_id") + private String fileId; + + @JsonProperty("start_date") + private String startDate; + + @JsonProperty("end_date") + private String endDate; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java new file mode 100644 index 0000000..64d143c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/violation/ViolationStatisticsResponse.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.dto.violation; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ViolationStatisticsResponse { + + @JsonProperty("total_violations") + private long totalViolations; + + @JsonProperty("new_violations") + private long newViolations; + + @JsonProperty("in_progress_violations") + private long inProgressViolations; + + @JsonProperty("resolved_violations") + private long resolvedViolations; +} diff --git a/src/main/java/ru/soune/nocopy/entity/company/Company.java b/src/main/java/ru/soune/nocopy/entity/company/Company.java index 09faf32..6135ed8 100644 --- a/src/main/java/ru/soune/nocopy/entity/company/Company.java +++ b/src/main/java/ru/soune/nocopy/entity/company/Company.java @@ -32,6 +32,9 @@ public class Company { @Column(name = "phone") private String phone; + @Column(name = "inn", unique = true) + private String inn; + @CreatedDate @Column(name = "register_date", updatable = false, nullable = false) private LocalDateTime registerDate = LocalDateTime.now(); diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java new file mode 100644 index 0000000..acb19e9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintEntity.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java new file mode 100644 index 0000000..7be6b2e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/ComplaintStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.entity.complaint; + +public enum ComplaintStatus { + CREATED, SHOWED, IN_WORK, COMPLETED, CANCELLED +} diff --git a/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java new file mode 100644 index 0000000..b9ca6e6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/complaint/LawCase.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java b/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java new file mode 100644 index 0000000..01018fb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/AppealStatus.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.entity.file; + +public enum AppealStatus { + PENDING, + IN_REVIEW, + APPROVED, + REJECTED, + EXPIRED +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java new file mode 100644 index 0000000..26b28a6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/FileAppeal.java @@ -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(); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java index 2d8d781..5872a3a 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileEntity.java @@ -8,6 +8,7 @@ import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; +import java.util.concurrent.atomic.AtomicInteger; @Data @Builder @@ -17,6 +18,9 @@ import java.time.LocalDateTime; @Table(name = "file_entities") @EntityListeners(AuditingEntityListener.class) public class FileEntity { + + private static AtomicInteger supportIdCounter = new AtomicInteger(0); + @Id @GeneratedValue(strategy = GenerationType.UUID) private String id; @@ -77,14 +81,27 @@ public class FileEntity { @JsonIgnore private String signature; + @Column(name = "thumbnail_path") + private String thumbnailPath; + + @Column(name = "medium_path") + private String mediumPath; + + @Column + private String newExtension; + @OneToOne(mappedBy = "file", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ImageHashEntity imageHash; @PrePersist public void prePersist() { + if (this.supportId == null) { + this.supportId = supportIdCounter.incrementAndGet(); + } + if (this.status == null) { - this.status = FileStatus.ACTIVE; + this.status = FileStatus.MODERATION; } if (this.protectionStatus == null) { @@ -101,4 +118,8 @@ public class FileEntity { public void preUpdate() { this.updatedAt = LocalDateTime.now(); } + + public static void initializeCounter(int maxId) { + supportIdCounter = new AtomicInteger(maxId); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java b/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java index cdd4fc1..a23ead9 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileStatus.java @@ -7,5 +7,8 @@ public enum FileStatus { VIOLATION, CHECKED, ERROR, - TEMP + TEMP, + REMOVED, + MODERATION, + BLOCKED } diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileType.java b/src/main/java/ru/soune/nocopy/entity/file/FileType.java index c79f61d..987deec 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileType.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileType.java @@ -7,7 +7,7 @@ import java.util.List; @Getter public enum FileType { - IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "gif", "bmp")), + IMAGE("image", Arrays.asList("jpg", "jpeg", "png", "bmp")), VIDEO("video", Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv", "webm", "m4v", "mpg", "mpeg", "3gp", "3g2", "f4v", "m2ts", "mts", "vob", "ogv", "divx")), AUDIO("audio", List.of("wav")), diff --git a/src/main/java/ru/soune/nocopy/entity/file/FileUploadSession.java b/src/main/java/ru/soune/nocopy/entity/file/FileUploadSession.java index 80dc12f..e2e93c8 100644 --- a/src/main/java/ru/soune/nocopy/entity/file/FileUploadSession.java +++ b/src/main/java/ru/soune/nocopy/entity/file/FileUploadSession.java @@ -75,6 +75,9 @@ public class FileUploadSession { @Column(name = "completed_at") private LocalDateTime completedAt; + @Column(name = "convert_to") + String convertTo; + @ElementCollection @CollectionTable( name = "uploaded_chunks", diff --git a/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java b/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java new file mode 100644 index 0000000..8e8d8d7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/SupportIdInitializer.java @@ -0,0 +1,21 @@ +package ru.soune.nocopy.entity.file; + +import jakarta.annotation.PostConstruct; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.repository.FileEntityRepository; + +@Component +public class SupportIdInitializer { + + @Autowired + private FileEntityRepository fileRepository; + + @PostConstruct + public void init() { + Integer maxId = fileRepository.findMaxSupportId(); + if (maxId != null) { + FileEntity.initializeCounter(maxId); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/file/Violation.java b/src/main/java/ru/soune/nocopy/entity/file/Violation.java deleted file mode 100644 index c5317bc..0000000 --- a/src/main/java/ru/soune/nocopy/entity/file/Violation.java +++ /dev/null @@ -1,57 +0,0 @@ -package ru.soune.nocopy.entity.file; - -import jakarta.persistence.*; -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.user.User; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "violations") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@ToString -@EntityListeners(AuditingEntityListener.class) -public class Violation { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long violationId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - @ToString.Exclude - private User user; - - @Column(name = "violation_type", nullable = false, length = 50) - private String violationType; - - @Column(name = "description", columnDefinition = "TEXT") - private String description; - - @Column(name = "content_id") - private Long contentId; - - @Column(name = "status", nullable = false, length = 20) - private String status = "new"; - - @Column(name = "severity", length = 20) - private String severity; - - @Column(name = "resolved_at") - private LocalDateTime resolvedAt; - - @Column(name = "resolved_by") - private Long resolvedBy; - - @Column(name = "resolution_notes", columnDefinition = "TEXT") - private String resolutionNotes; - - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java new file mode 100644 index 0000000..5196134 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/file/moderation/ModerationLog.java @@ -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; +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java new file mode 100644 index 0000000..1c94abf --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/FileMonitoringEntity.java @@ -0,0 +1,60 @@ +package ru.soune.nocopy.entity.monitoring; + +import jakarta.persistence.*; +import lombok.*; +import ru.soune.nocopy.entity.file.FileEntity; + +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Entity +@Table(name = "file_monitoring") +public class FileMonitoringEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file_id", nullable = false) + private FileEntity file; + + @Column(name = "user_id", nullable = false) + private Long userId; + + @Enumerated(EnumType.STRING) + @Column(name = "monitoring_type", nullable = false) + private MonitoringType monitoringType; + + @Column(name = "next_run") + private LocalDateTime nextRun; + + @Column(name = "last_run") + private LocalDateTime lastRun; + + @Column(name = "last_run_status") + private String lastRunStatus; + + @Column(name = "is_active", nullable = false) + private boolean isActive = true; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @PrePersist + public void prePersist() { + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java new file mode 100644 index 0000000..21a8b84 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/MonitoringType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum MonitoringType { + MONITORING_DAILY, MONITORING_WEEKLY, MONITORING_MONTHLY, NONE +} diff --git a/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java new file mode 100644 index 0000000..97a663c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/monitoring/SearchEngine.java @@ -0,0 +1,8 @@ +package ru.soune.nocopy.entity.monitoring; + + +public enum SearchEngine { + YANDEX, + GOOGLE, + BOTH +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/Payment.java b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java new file mode 100644 index 0000000..3646fbc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/Payment.java @@ -0,0 +1,64 @@ +package ru.soune.nocopy.entity.payment; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "payment") +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +public class Payment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Enumerated(EnumType.STRING) + @Column(name = "status", nullable = false) + private PaymentStatus status; + + @Column(name = "payment_uuid", unique = true) + private String paymentUuid; + + @Column(name = "amount") + private Double amount; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Enumerated(EnumType.STRING) + @Column(name = "operation_type", nullable = false) + private PaymentOperationType operationType; + + @Column(name = "cancellation_reason") + private String cancellationReason; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private User user; + + @ManyToOne + @JoinColumn(name = "tariff_id") + @JsonIgnore + private Tariff tariff; +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java new file mode 100644 index 0000000..174d9a3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentMethod.java @@ -0,0 +1,45 @@ +package ru.soune.nocopy.entity.payment; + + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.soune.nocopy.entity.user.User; +import java.time.LocalDateTime; + +@Entity +@Table(name = "payment_methods") +@Getter @Setter @NoArgsConstructor @AllArgsConstructor +public class PaymentMethod { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "payment_method_id", nullable = false, unique = true) + private String paymentMethodId; + + @Column(name = "last_four") + private String lastFour; + + @Column(name = "card_type") + private String cardType; + + @Column(name = "expiry_month") + private String expiryMonth; + + @Column(name = "expiry_year") + private String expiryYear; + + @Column(name = "active") + private boolean active = true; + + @Column(name = "created_at") + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java new file mode 100644 index 0000000..69c1b00 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentOperationType.java @@ -0,0 +1,6 @@ +package ru.soune.nocopy.entity.payment; + +public enum PaymentOperationType { + TARIFF, + TOKEN +} diff --git a/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java new file mode 100644 index 0000000..0e9478e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payment/PaymentStatus.java @@ -0,0 +1,10 @@ +package ru.soune.nocopy.entity.payment; + + +public enum PaymentStatus { + PENDING, + WAITING, + SUCCEEDED, + CANCELED, + FAILED +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java new file mode 100644 index 0000000..fca64ee --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/BankTransferPayoutMethod.java @@ -0,0 +1,50 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.*; + +@Entity +@DiscriminatorValue("BANK_TRANSFER") +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +public class BankTransferPayoutMethod extends PayoutMethod { + + @Column(name = "bank_name") + private String bankName; + + @Column(name = "bic") + private String bic; + + @Column(name = "correspondent_account") + private String correspondentAccount; + + @Column(name = "account_number") + private String accountNumber; + + @Column(name = "account_holder") + private String accountHolder; + + @Column(name = "inn") + private String inn; + + @Column(name = "kpp") + private String kpp; + + @Override + public String getDisplayName() { + return String.format("Tax %s in %s", getMaskedAccount(), bankName); + } + + @Override + public String getMaskedDetails() { + return getMaskedAccount(); + } + + private String getMaskedAccount() { + if (accountNumber == null || accountNumber.length() < 4) return ""; + return "..." + accountNumber.substring(accountNumber.length() - 4); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java new file mode 100644 index 0000000..053fc91 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/CardPayoutMethod.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import lombok.*; +import ru.soune.nocopy.entity.payout.convertor.CardNumberEncryptor; + +@Entity +@DiscriminatorValue("CARD") +@NoArgsConstructor +@AllArgsConstructor +@Setter @Getter +public class CardPayoutMethod extends PayoutMethod { + @Convert(converter = CardNumberEncryptor.class) + @Column(name = "card_number", nullable = false) + private String cardNumber; + + @Column(name = "card_hash", unique = true) + private String cardHash; + + @Override + public String getDisplayName() { + return "Card: " + getMaskedCardNumber(); + } + + @Override + public String getMaskedDetails() { + return getMaskedCardNumber(); + } + + private String getMaskedCardNumber() { + if (cardNumber == null || cardNumber.length() < 4) return ""; + return "****" + cardNumber.substring(cardNumber.length() - 4); + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java new file mode 100644 index 0000000..9b3c8ff --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/InternalPayoutMethod.java @@ -0,0 +1,19 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; + +@Entity +@DiscriminatorValue("INTERNAL") +public class InternalPayoutMethod extends PayoutMethod { + + @Override + public String getDisplayName() { + return "Internal balance"; + } + + @Override + public String getMaskedDetails() { + return "Account balance"; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java new file mode 100644 index 0000000..77c91d3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutMethod.java @@ -0,0 +1,48 @@ +package ru.soune.nocopy.entity.payout; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "payout_methods") +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "method_type") +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PayoutMethod { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "method_type", insertable = false, updatable = false) + private String methodType; + + @Column(name = "is_default") + private boolean isDefault; + + @CreationTimestamp + private LocalDateTime createdAt; + + @UpdateTimestamp + private LocalDateTime updatedAt; + + + public String getDisplayName() { + return null; + } + + public String getMaskedDetails() { + return null; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java new file mode 100644 index 0000000..231f8d0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequest.java @@ -0,0 +1,64 @@ +package ru.soune.nocopy.entity.payout; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import ru.soune.nocopy.entity.user.User; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + + +@Entity +@Table(name = "payout_requests") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Getter +public class PayoutRequest { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "payout_method_id") + @JsonIgnore + private PayoutMethod payoutMethod; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + @JsonIgnore + private User user; + + @Column(nullable = false) + private BigDecimal amount; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private PayoutRequestStatus status; + + @Enumerated(EnumType.STRING) + @Column(nullable = false, name = "payout_type") + @JsonIgnore + private PayoutType payoutType; + + @Column(name = "process_at") + private LocalDateTime processAt; + + @Column(name = "completed_at") + private LocalDateTime completedAt; + + @Column(name = "rejection_reason") + private String rejectionReason; + + @CreationTimestamp + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @UpdateTimestamp + @Column(name = "updated_at") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java new file mode 100644 index 0000000..bf5f013 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutRequestStatus.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.entity.payout; + +public enum PayoutRequestStatus { + PENDING, + PROCESSING, + COMPLETED, + REJECTED, + CANCELLED +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java b/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java new file mode 100644 index 0000000..b7095bc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/PayoutType.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.entity.payout; + +public enum PayoutType { + CARD("card"), + BANK_TRANSFER("bank_transfer"), + INTERNAL("internal"); + + private final String displayName; + + PayoutType(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } +} diff --git a/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java b/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java new file mode 100644 index 0000000..46b95b1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/payout/convertor/CardNumberEncryptor.java @@ -0,0 +1,74 @@ +package ru.soune.nocopy.entity.payout.convertor; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import javax.crypto.Cipher; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.Base64; + +@Converter +public class CardNumberEncryptor implements AttributeConverter { + + private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final int GCM_TAG_LENGTH = 128; + private static final int GCM_IV_LENGTH = 12; + + private static final byte[] FIXED_KEY = "12345678901234567890123456789012".getBytes(); + + @Override + public String convertToDatabaseColumn(String cardNumber) { + if (cardNumber == null) return null; + + try { + SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES"); + + byte[] iv = new byte[GCM_IV_LENGTH]; + new SecureRandom().nextBytes(iv); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.ENCRYPT_MODE, key, parameterSpec); + + byte[] encrypted = cipher.doFinal(cardNumber.getBytes(StandardCharsets.UTF_8)); + + byte[] result = new byte[iv.length + encrypted.length]; + System.arraycopy(iv, 0, result, 0, iv.length); + System.arraycopy(encrypted, 0, result, iv.length, encrypted.length); + + return Base64.getEncoder().encodeToString(result); + + } catch (Exception e) { + throw new RuntimeException("Failed to encrypt card number", e); + } + } + + @Override + public String convertToEntityAttribute(String dbData) { + if (dbData == null) return null; + + try { + byte[] decoded = Base64.getDecoder().decode(dbData); + + byte[] iv = new byte[GCM_IV_LENGTH]; + byte[] encrypted = new byte[decoded.length - GCM_IV_LENGTH]; + System.arraycopy(decoded, 0, iv, 0, iv.length); + System.arraycopy(decoded, iv.length, encrypted, 0, encrypted.length); + + SecretKeySpec key = new SecretKeySpec(FIXED_KEY, "AES"); + + Cipher cipher = Cipher.getInstance(ALGORITHM); + GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv); + cipher.init(Cipher.DECRYPT_MODE, key, parameterSpec); + + byte[] decrypted = cipher.doFinal(encrypted); + return new String(decrypted, StandardCharsets.UTF_8); + + } catch (Exception e) { + throw new RuntimeException("Failed to decrypt card number", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/referral/Referral.java b/src/main/java/ru/soune/nocopy/entity/referral/Referral.java index b03dc4a..a978652 100644 --- a/src/main/java/ru/soune/nocopy/entity/referral/Referral.java +++ b/src/main/java/ru/soune/nocopy/entity/referral/Referral.java @@ -25,6 +25,9 @@ public class Referral { @Column(name = "total_income") private int totalIncome = 0; + @Column(name = "available_income") + private int availableIncome = 0; + @Column(name = "is_active") private boolean isActive = false; diff --git a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java new file mode 100644 index 0000000..bd0af95 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchResult.java @@ -0,0 +1,27 @@ +package ru.soune.nocopy.entity.search; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "global_search_results") +@Data +@Getter @Setter +@RequiredArgsConstructor +public class GlobalSearchResult { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String taskId; + + private String fileId; + + private String fileName; + + private String fileStatus; +} diff --git a/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java new file mode 100644 index 0000000..2a7b4c7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/GlobalSearchTask.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.search; + +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "global_search_tasks") +@Data +@Getter @Setter +@RequiredArgsConstructor +public class GlobalSearchTask { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private String taskId; + + private Long userId; + + private String searchType; + + private String status; + + private Integer totalFiles; + + private Integer processedFiles; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java b/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java new file mode 100644 index 0000000..867abb8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/search/SearchStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.entity.search; + +public enum SearchStatus { + PROCESSING, COMPLETED, ACCEPTED, FAILED, SUCCESS, TIMEOUT +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java index abf1395..3e6d3f6 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/Tariff.java @@ -40,4 +40,8 @@ public class Tariff { @Column(name = "tariffAccountType") private String tariffAccountType; + + @Column(name = "tariff_term") + @Enumerated(EnumType.STRING) + private TariffTimeTerm tariffTerm; } diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java index c8f647e..9c71022 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffInfo.java @@ -37,4 +37,10 @@ public class TariffInfo { @ManyToOne @JoinColumn(name = "tariff_id") private Tariff tariff; + + @Column(name = "auto_renewal") + private boolean autoRenewal = false; + + @Column(name = "payment_method_id") + private String paymentMethodId; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java new file mode 100644 index 0000000..b3a9441 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffTimeTerm.java @@ -0,0 +1,15 @@ +package ru.soune.nocopy.entity.tarif; + +import lombok.Getter; + +@Getter +public enum TariffTimeTerm { + MONTHLY(30),YEAR(365); + + private final Integer days; + + TariffTimeTerm(Integer days) { + this.days = days; + } + +} diff --git a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java index 43ae949..f48cbb9 100644 --- a/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java +++ b/src/main/java/ru/soune/nocopy/entity/tarif/TariffType.java @@ -2,5 +2,7 @@ package ru.soune.nocopy.entity.tarif; public enum TariffType { - START, BASIC, PRO, ENTERPRISE, DEMO, BUISNES, CORPORAT, STUDIO, FREE; + 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; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index b3bd024..0c58638 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -4,10 +4,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; import jakarta.validation.constraints.Size; import lombok.*; +import org.hibernate.annotations.ColumnDefault; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import ru.soune.nocopy.entity.company.Company; -import ru.soune.nocopy.entity.file.Violation; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffStatus; @@ -80,11 +80,6 @@ public class User { @ToString.Exclude private List userContents = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnore - @ToString.Exclude - private List violations = new ArrayList<>(); - @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore private ProtectedFileCheck protectedFileCheck; @@ -102,6 +97,15 @@ public class User { @ToString.Exclude private TariffInfo personalTariffInfo; + @Column(name = "failed_login_attempts") + @ColumnDefault("0") + private Integer failedLoginAttempts = 0; + + @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonIgnore + @ToString.Exclude + private UserNotActive userNotActive; + public TariffInfo getActiveTariffInfo() { if (company != null && company.getTariffInfo() != null) { return company.getTariffInfo(); diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java new file mode 100644 index 0000000..ea1c040 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfo.java @@ -0,0 +1,34 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.CreationTimestamp; + +import java.sql.Timestamp; + +@Entity +@NoArgsConstructor +@AllArgsConstructor +@Getter @Setter +@Builder +@Table(name = "user_additional_info") +public class UserAdditionalInfo { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, name = "register_date") + @CreationTimestamp + private Timestamp registrationDate; + + @Column(nullable = false, name = "ip") + private String ipAddress; + + @Column(nullable = false, name = "user_agent") + private String userAgent; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", unique = true, nullable = false) + private User user; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java new file mode 100644 index 0000000..3adf2e5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoAnswer.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoAnswer { + + @JsonProperty("id") + private Long id; + + @JsonProperty("user_id") + private Long userId; + + @JsonProperty("ip_address") + private String ipAddress; + + @JsonProperty("user_agent") + private String userAgent; + + @JsonProperty("registration_date") + private Timestamp registrationDate; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java new file mode 100644 index 0000000..89917a9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAdditionalInfoListAnswer.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.entity.user; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserAdditionalInfoListAnswer { + + @JsonProperty("items") + private List items; + + @JsonProperty("count") + private int count; +} diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java b/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java new file mode 100644 index 0000000..e586ade --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/UserNotActive.java @@ -0,0 +1,32 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "user_not_active") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@ToString +@Builder +public class UserNotActive { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + private User user; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdAt; + + @Column(name = "blocked_until") + private LocalDateTime blockedUntil; +} diff --git a/src/main/java/ru/soune/nocopy/entity/violation/Violation.java b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java new file mode 100644 index 0000000..ad0e4d2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/violation/Violation.java @@ -0,0 +1,57 @@ +package ru.soune.nocopy.entity.violation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.search.GlobalSearchResult; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "violation") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class Violation { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "url", nullable = false, columnDefinition = "TEXT") + private String url; + + @Column(name = "url_hash", nullable = false, unique = true, columnDefinition = "TEXT") + private String urlHash; + + @Column(name = "page_url", columnDefinition = "TEXT") + private String pageUrl; + + @Column(name = "title", columnDefinition = "TEXT") + private String pageTitle; + + @Column(name = "host", columnDefinition = "TEXT") + private String host; + + @Column(name = "status") + private String status; + + @CreatedDate + @Column(name = "created_at", updatable = false) + private LocalDateTime createdDate = LocalDateTime.now(); + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "file") + @ToString.Exclude + @JsonIgnore + private FileEntity fileEntity; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "global_search_result") + @ToString.Exclude + @JsonIgnore + private GlobalSearchResult globalSearchResult; +} diff --git a/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java new file mode 100644 index 0000000..0c361bc --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/violation/ViolationNotion.java @@ -0,0 +1,46 @@ +package ru.soune.nocopy.entity.violation; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.user.User; + +import java.time.LocalDateTime; + +@Entity +@Getter @Setter +@Table(name = "violation_notions") +@NoArgsConstructor +@EntityListeners(AuditingEntityListener.class) +public class ViolationNotion { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @JsonIgnore + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "violation_id", nullable = false) + @JsonIgnore + private Violation violation; + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", nullable = false) + private LocalDateTime updatedAt; + + @Column(name = "message") + private String message; +} diff --git a/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java b/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java new file mode 100644 index 0000000..80f3775 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/CompanyAlreadyExist.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class CompanyAlreadyExist extends RuntimeException { + public CompanyAlreadyExist(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java new file mode 100644 index 0000000..fce3a9f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ComplaintNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ComplaintNotFoundException extends RuntimeException { + public ComplaintNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java new file mode 100644 index 0000000..9ce0be5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/DuplicateComplaintException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class DuplicateComplaintException extends RuntimeException { + public DuplicateComplaintException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java b/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java new file mode 100644 index 0000000..3897426 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/DuplicateMethodException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class DuplicateMethodException extends RuntimeException { + public DuplicateMethodException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/FileFormatException.java b/src/main/java/ru/soune/nocopy/exception/FileFormatException.java new file mode 100644 index 0000000..6480320 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/FileFormatException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class FileFormatException extends RuntimeException { + public FileFormatException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java b/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java new file mode 100644 index 0000000..6d43734 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InsufficientFundsException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InsufficientFundsException extends RuntimeException { + public InsufficientFundsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java b/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java new file mode 100644 index 0000000..1b6dcb4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InsufficientTokensException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InsufficientTokensException extends RuntimeException { + public InsufficientTokensException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java b/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java new file mode 100644 index 0000000..b2ba888 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InvalidAppealException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InvalidAppealException extends RuntimeException { + public InvalidAppealException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java b/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java new file mode 100644 index 0000000..8be2b6d --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/InvalidPayoutMethodException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class InvalidPayoutMethodException extends RuntimeException { + public InvalidPayoutMethodException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java new file mode 100644 index 0000000..3d7f072 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/PaymentNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class PaymentNotFoundException extends RuntimeException { + public PaymentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java b/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java new file mode 100644 index 0000000..cba5885 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/PendingPayoutExistsException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class PendingPayoutExistsException extends RuntimeException { + public PendingPayoutExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java new file mode 100644 index 0000000..61f5cda --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/RegistrationAdditionalInfoNullException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class RegistrationAdditionalInfoNullException extends RuntimeException { + public RegistrationAdditionalInfoNullException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java b/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java new file mode 100644 index 0000000..9b505dd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/SearchFailedException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class SearchFailedException extends RuntimeException { + public SearchFailedException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java new file mode 100644 index 0000000..ce16c63 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserAdditionalInfoNotFound.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserAdditionalInfoNotFound extends RuntimeException { + public UserAdditionalInfoNotFound(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java new file mode 100644 index 0000000..7704209 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/UserNotHavePermission.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class UserNotHavePermission extends RuntimeException { + public UserNotHavePermission(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java b/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java new file mode 100644 index 0000000..b088ed1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/exception/ViolationNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.nocopy.exception; + +public class ViolationNotFoundException extends RuntimeException { + public ViolationNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java new file mode 100644 index 0000000..c0ebc7b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ComplaintEntityHandler.java @@ -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 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 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<>()); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/CostHandler.java b/src/main/java/ru/soune/nocopy/handler/CostHandler.java new file mode 100644 index 0000000..80234c6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/CostHandler.java @@ -0,0 +1,107 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.cost.CostRequest; +import ru.soune.nocopy.dto.cost.CostResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.cost.CostService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CostHandler implements RequestHandler { + + private final CostService costService; + + private final AuthTokenRepository authTokenRepository; + + private final ObjectMapper objectMapper; + + private final Integer MAX_FILES_FOR_CHECK = 1000; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + CostRequest costRequest = objectMapper.convertValue(request.getMessageBody(), CostRequest.class); + String action = costRequest.getAction(); + + return switch (action) { + case "costFileProtect" -> handleFileProtectCost(costRequest, request); + case "checkTokensForProtect" -> handleCheckTokensForFilesProtect(costRequest, request); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + null); + }; + } + + private BaseResponse handleFileProtectCost(CostRequest costRequest, BaseRequest request) { + int cost = costService.protectFileCost(costRequest.getFileType()); + + if (cost == 0) { + return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(), + MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(), + CostResponse.builder() + .cost(cost) + .build()); + } + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + CostResponse.builder() + .cost(cost) + .build()); + } + + private BaseResponse handleCheckTokensForFilesProtect(CostRequest costRequest, BaseRequest request) { + String authToken = costRequest.getAuthToken(); + Optional token = authTokenRepository.findByToken(authToken); + + if (token.isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + null); + } + + User user = token.get().getUser(); + Integer tokens = user.getActiveTariffInfo().getTokens(); + + int cost = costService.protectFileCost(costRequest.getFileType()); + int count = costRequest.getCountFilesForProtect(); + + if (cost == 0 || count <= 0 || count > MAX_FILES_FOR_CHECK) { + return new BaseResponse(request.getMsgId(), MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getCode(), + MessageCode.NOT_VALID_FILE_TYPE_OR_COUNT_FILE.getDescription(), + CostResponse.builder() + .cost(cost) + .countFile(count) + .maxFilesForCheck(MAX_FILES_FOR_CHECK) + .build()); + } + + int totalCost = count * cost; + boolean success = totalCost <= tokens; + int needToken = success ? 0: tokens - totalCost; + + CostResponse costResponse = CostResponse.builder() + .cost(totalCost) + .success(totalCost <= tokens) + .needTokensCount(Math.abs(needToken)) + .build(); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), costResponse); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 3953ab9..61d77a5 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -3,23 +3,28 @@ package ru.soune.nocopy.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; +import ru.soune.nocopy.entity.file.FileAppeal; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; -import ru.soune.nocopy.exception.FileEntityNotFoundException; -import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.entity.file.moderation.ModerationLog; +import ru.soune.nocopy.exception.*; +import ru.soune.nocopy.repository.FileAppealRepository; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.cloud.CloudStorageService; +import ru.soune.nocopy.repository.ModerationLogRepository; +import ru.soune.nocopy.service.file.moderation.ModerationService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileStatsService; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.time.LocalDate; +import java.util.*; +import java.util.stream.Collectors; @Slf4j @Component @@ -38,6 +43,12 @@ public class FileEntityHandler implements RequestHandler { private final CloudStorageService cloudStorageService; + private final ModerationService moderationService; + + private final FileAppealRepository fileAppealRepository; + + private final ModerationLogRepository moderationLogRepository; + @Override public BaseResponse handle(BaseRequest request) { try { @@ -63,6 +74,18 @@ public class FileEntityHandler implements RequestHandler { return handleProtectFile(request, fileRequest); case "delete_file": return handleDeleteFile(request, fileRequest); + case "change_file_status": + return handleChangeStatus(request, fileRequest); + case "moderate_file": + return handleModerateFile(request, fileRequest); + case "submit_appeal": + return handleSubmitAppeal(request, fileRequest); + case "review_appeal": + return handleReviewAppeal(request, fileRequest); + case "user_appeals": + return handleGetUserAppeals(request, fileRequest); + case "files_for_moderation": + return handleGetFilesForModeration(request, fileRequest); default: ActionResponse response = ActionResponse.builder() .action(action) @@ -186,53 +209,306 @@ public class FileEntityHandler implements RequestHandler { } } + private BaseResponse handleChangeStatus(BaseRequest request, FileEntityRequest fileRequest) { + FileStatus newStatus = fileRequest.getFileStatus(); + String fileId = fileRequest.getFileId(); + + try { + fileEntityService.changeFileStatus(newStatus ,fileRequest.getFileId()); + } catch (FileEntityNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), + FileEntityResponse.builder().id(fileId).build()); + } + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + FileEntityResponse.builder().status(newStatus).build()); + } + private BaseResponse handleSearchFiles(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; - if (fileRequest.getQuery() == null || fileRequest.getQuery().isBlank()) { - return handleGetUserFiles(request, fileRequest); - } + String sortBy = fileRequest.getSortBy() != null ? fileRequest.getSortBy() : "fileName"; + SortOrder sortOrder = SortOrder.fromString(fileRequest.getSortOrder()); FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); - var filteredFiles = allFiles.getFiles().stream() - .filter(f -> (f.getOriginalFileName() + f.getFileExtension()).toLowerCase() - .contains(fileRequest.getQuery().toLowerCase())) - .toList(); + String searchQuery = fileRequest.getQuery() != null ? fileRequest.getQuery().toLowerCase().trim() : ""; + String[] searchTerms = searchQuery.split("\\s+"); + String filterType = fileRequest.getType(); + String dateFilter = fileRequest.getDateFilter(); + + List filteredFiles = allFiles.getFiles().stream() + .filter(f -> matchesSearch(f, searchTerms, searchQuery, filterType)) + .filter(f -> matchesDateFilter(f, dateFilter)) + .collect(Collectors.toList()); + + Comparator comparator = getComparator(sortBy, sortOrder); + filteredFiles.sort(comparator); int start = (page - 1) * pageSize; int end = Math.min(start + pageSize, filteredFiles.size()); - List fileEntityResponses = filteredFiles.subList(start, Math.min(end, filteredFiles.size())); - long sumSize = fileEntityResponses.stream() + + if (start >= filteredFiles.size()) { + return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue()); + } + + List paginatedFiles = filteredFiles.subList(start, end); + long totalSize = paginatedFiles.stream() .mapToLong(FileEntityResponse::getFileSize) .sum(); FileListResponse response = FileListResponse.builder() - .files(fileEntityResponses) + .files(paginatedFiles) .totalCount(filteredFiles.size()) - .totalSize(sumSize) - .formattedTotalSize(fileEntityService.formatFileSize(sumSize)) + .totalSize(totalSize) + .formattedTotalSize(fileEntityService.formatFileSize(totalSize)) .page(page) .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder.getValue()) .build(); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), response); + } catch (NotFoundAuthToken e) { - return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), "Authentication required", null); } catch (Exception e) { log.error("Error searching files", e); - return new BaseResponse(request.getMsgId(), MessageCode.FILE_UPLOAD_ERROR.getCode(), + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_UPLOAD_ERROR.getCode(), "Failed to search files: " + e.getMessage(), null); } } + private boolean matchesDateFilter(FileEntityResponse file, String dateFilter) { + if (dateFilter == null || dateFilter.isEmpty()) { + return true; + } + + LocalDate fileDate = file.getCreatedAt().toLocalDate(); + LocalDate now = LocalDate.now(); + + switch (dateFilter.toLowerCase()) { + case "today": + return fileDate.equals(now); + case "week": + return fileDate.isAfter(now.minusWeeks(1)) && !fileDate.isAfter(now); + case "month": + return fileDate.isAfter(now.minusMonths(1)) && !fileDate.isAfter(now); + default: + return true; + } + } + + private boolean matchesSearch(FileEntityResponse file, String[] searchTerms, String fullQuery, String type) { + String fileName = getSafeString(file.getFileName()); + String originalFileName = getSafeString(file.getOriginalFileName()); + String mimeType = getSafeString(file.getMimeType()); + String fileFormat = getSafeString(file.getFileFormat()); + String fullFileName = fileName + " " + originalFileName; + + boolean matchesType = true; + if (type != null && !type.isEmpty()) { + matchesType = matchesMimeType(mimeType, type); + } + + if (!matchesType) { + return false; + } + + if (fullQuery != null && !fullQuery.isEmpty()) { + if (fullFileName.toLowerCase().contains(fullQuery)) { + return true; + } + + for (String term : searchTerms) { + if (term.length() < 2) continue; + if (fullFileName.toLowerCase().contains(term)) { + return true; + } + } + + if (fileFormat.toLowerCase().contains(fullQuery)) { + return true; + } + + if (mimeType.toLowerCase().contains(fullQuery)) { + return true; + } + + if (!fileFormat.isEmpty() && !fileName.toLowerCase().endsWith("." + fileFormat.toLowerCase())) { + String fileNameWithExtension = fileName + "." + fileFormat; + if (fileNameWithExtension.toLowerCase().contains(fullQuery)) { + return true; + } + } + + return false; + } + + return true; + } + + private boolean matchesMimeType(String mimeType, String filterType) { + if (filterType == null || filterType.isEmpty()) return true; + + String mimeTypeLower = mimeType.toLowerCase(); + String filterTypeLower = filterType.toLowerCase().trim(); + + return mimeTypeLower.equals(filterTypeLower); + } + + private String getSafeString(String value) { + return value != null ? value : ""; + } + + private Comparator getComparator(String sortBy, SortOrder sortOrder) { + Comparator comparator; + + switch (sortBy) { + case "fileSize": + comparator = Comparator.comparingLong( + FileEntityResponse::getFileSize + ); + break; + + case "fileType": + comparator = Comparator.comparing( + FileEntityResponse::getMimeType); + break; + + case "createdAt": + comparator = Comparator.comparing( + FileEntityResponse::getCreatedAt, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "updatedAt": + comparator = Comparator.comparing( + FileEntityResponse::getUpdatedAt, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "fileName": + comparator = Comparator.comparing( + f -> getSafeString(f.getFileName()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "protectStatus": + comparator = Comparator.comparing( + f -> getSafeString(f.getProtectStatus()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + case "supportId": + comparator = Comparator.comparing( + FileEntityResponse::getSupportId, + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + + default: + comparator = Comparator.comparing( + f -> getSafeString(f.getFileName()).toLowerCase(), + Comparator.nullsLast(Comparator.naturalOrder()) + ); + break; + } + + return sortOrder == SortOrder.DESC ? comparator.reversed() : comparator; + } + + private BaseResponse createEmptyResponse(BaseRequest request, int page, int pageSize, + String sortBy, String sortOrder) { + FileListResponse emptyResponse = FileListResponse.builder() + .files(Collections.emptyList()) + .totalCount(0) + .totalSize(0L) + .formattedTotalSize("0 B") + .page(page) + .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder) + .build(); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + emptyResponse); + } + + private BaseResponse handleGetUserFilesWithSort(BaseRequest request, FileEntityRequest fileRequest, + String sortBy, SortOrder sortOrder) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + + FileResponse allFiles = fileEntityService.getUserFiles(userId, 1, 1000, request.getVersion()); + + List files = new ArrayList<>(allFiles.getFiles()); + + Comparator comparator = getComparator(sortBy, sortOrder); + files.sort(comparator); + + int start = (page - 1) * pageSize; + int end = Math.min(start + pageSize, files.size()); + + if (start >= files.size()) { + return createEmptyResponse(request, page, pageSize, sortBy, sortOrder.getValue()); + } + + List paginatedFiles = files.subList(start, end); + long totalSize = paginatedFiles.stream() + .mapToLong(FileEntityResponse::getFileSize) + .sum(); + + FileListResponse response = FileListResponse.builder() + .files(paginatedFiles) + .totalCount(files.size()) + .totalSize(totalSize) + .formattedTotalSize(fileEntityService.formatFileSize(totalSize)) + .page(page) + .pageSize(pageSize) + .sortBy(sortBy) + .sortOrder(sortOrder.getValue()) + .build(); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + response); + + } catch (NotFoundAuthToken e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Authentication required", null); + } catch (Exception e) { + log.error("Error getting user files with sort", e); + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_UPLOAD_ERROR.getCode(), + "Failed to get files: " + e.getMessage(), + null); + } + } + private BaseResponse handleGetStorageUsage(BaseRequest request, FileEntityRequest fileRequest) { try { Long userId = authService.useUserAuthToken(fileRequest.getToken()); @@ -337,6 +613,326 @@ public class FileEntityHandler implements RequestHandler { } } + private BaseResponse handleModerateFile(BaseRequest request, FileEntityRequest fileRequest) { + try { + //TODO добавить проверку на админа +// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken()); + + ModerationActionRequest moderationRequest = ModerationActionRequest.builder() + .fileId(fileRequest.getFileId()) + .newStatus(fileRequest.getFileStatus()) + .comment(fileRequest.getComment()) + .build(); + + //TODO set moderator ID + moderationService.moderateFile(moderationRequest, 0L); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS_MODERATION.getCode(), + MessageCode.SUCCESS_MODERATION.getDescription(), + FileEntityResponse.builder().id(fileRequest.getFileId()).build()); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + MessageCode.ACCESS_DENIED.getDescription(), + FileEntityResponse.builder().id(fileRequest.getFileId()).build()); + } catch (FileEntityNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), + FileEntityResponse.builder() + .id(fileRequest.getFileId()).build()); + } catch (IllegalStateException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ILLEGAL_STATE.getCode(), + MessageCode.ILLEGAL_STATE.getDescription(), + FileEntityResponse.builder() + .status(fileRequest.getFileStatus()).build()); + } + } + + private BaseResponse handleSubmitAppeal(BaseRequest request, FileEntityRequest fileRequest) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + + AppealRequest appealRequest = AppealRequest.builder() + .fileId(fileRequest.getFileId()) + .appealReason(fileRequest.getAppealReason()) + .additionalInfo(fileRequest.getAdditionalInfo()) + .build(); + + AppealResponse response = moderationService.submitAppeal(appealRequest, userId); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Appeal submitted successfully", + response); + } catch (InvalidAppealException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to submit appeal: " + e.getMessage(), + null); + } + } + + private BaseResponse handleReviewAppeal(BaseRequest request, FileEntityRequest fileRequest) { + try { + // TODO +// Long moderatorId = authService.useAdminAuthToken(fileRequest.getToken()); + + String appealId = fileRequest.getAppealId(); + Boolean approve = fileRequest.getApprove(); + String comment = fileRequest.getComment(); + + if (appealId == null || approve == null) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "appealId and approve are required", + null); + } + //TODO + moderationService.reviewAppeal(appealId, approve, comment, 0L); + + return new BaseResponse(request.getMsgId(), + MessageCode.SUCCESS.getCode(), + "Appeal reviewed successfully", + Map.of( + "appealId", appealId, + "approved", approve, + "comment", comment + )); + + } catch (SecurityException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + "Admin access required", + null); + } catch (IllegalStateException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Error reviewing appeal", e); + return new BaseResponse(request.getMsgId(), + MessageCode.INVALID_FIELD.getCode(), + "Failed to review appeal: " + e.getMessage(), + null); + } + } + + private BaseResponse handleGetUserAppeals(BaseRequest request, FileEntityRequest fileRequest) { + try { + Long userId = authService.useUserAuthToken(fileRequest.getToken()); + + int page = fileRequest.getPage() != null ? fileRequest.getPage() : 1; + int pageSize = fileRequest.getPageSize() != null ? fileRequest.getPageSize() : 20; + + Page appeals = moderationService.getUserAppeals(userId, page - 1, pageSize); + + Map 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 files = moderationService.getFilesForModeration(page - 1, pageSize); + + List> filesWithInfo = files.getContent().stream() + .map(file -> { + FileModerationInfo info = moderationService.getFileModerationInfo(file.getId()); + + Map 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 moderationInfoMap = new HashMap<>(); + moderationInfoMap.put("hasActiveAppeal", info != null && info.getHasActiveAppeal()); + + if (info != null && info.getActiveAppeal() != null) { + Map 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 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 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 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> getModerationHistory(String fileId) { + List logs = moderationLogRepository.findByFileIdOrderByCreatedAtDesc(fileId); + if (logs == null || logs.isEmpty()) { + return Collections.emptyList(); + } + + return logs.stream() + .map(log -> { + Map 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> getFileAppeals(String fileId) { + List appeals = fileAppealRepository.findByFileId(fileId); + if (appeals == null || appeals.isEmpty()) { + return Collections.emptyList(); + } + + return appeals.stream() + .map(appeal -> { + Map appealMap = new HashMap<>(); + appealMap.put("id", appeal.getId()); + appealMap.put("userId", appeal.getUserId()); + appealMap.put("appealReason", appeal.getAppealReason()); + appealMap.put("additionalInfo", appeal.getAdditionalInfo()); + appealMap.put("status", appeal.getStatus() != null ? appeal.getStatus().name() : null); + appealMap.put("adminComment", appeal.getAdminComment()); + appealMap.put("moderatorId", appeal.getModeratorId()); + appealMap.put("createdAt", appeal.getCreatedAt()); + appealMap.put("resolvedAt", appeal.getResolvedAt()); + return appealMap; + }) + .collect(Collectors.toList()); + } + private String formatFileSize(long size) { if (size < 1024) return size + " B"; int exp = (int) (Math.log(size) / Math.log(1024)); diff --git a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 67755c0..8bc41ff 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -111,6 +111,7 @@ public class FileUploadHandler implements RequestHandler { fileRequest.getFileName(), fileRequest.getFileType(), fileRequest.getExtension(), + fileRequest.getConvertTo(), fileRequest.getFileSize()); InitFileResponse fileResponse = InitFileResponse.builder() diff --git a/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java new file mode 100644 index 0000000..a6e7820 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/GlobalSearchHandler.java @@ -0,0 +1,54 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsRequest; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.search.GlobalSearchStatisticsService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalSearchHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final GlobalSearchStatisticsService statisticsService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + GlobalSearchStatisticsRequest statRequest = objectMapper.convertValue( + request.getMessageBody(), GlobalSearchStatisticsRequest.class); + + if (statRequest.getToken() == null) { + throw new IllegalArgumentException("User token is required"); + } + + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isEmpty()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + + GlobalSearchStatisticsResponse statistics = statisticsService.getStatistics( + userId, statRequest.getLimit(), statRequest.getDays()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(statistics) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 2d1f5f0..51bb0fa 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -1,10 +1,10 @@ package ru.soune.nocopy.handler; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; @@ -15,7 +15,6 @@ import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.search.SearchImageService; -import ru.soune.nocopy.service.search.GoogleVisionSearchService; import ru.soune.nocopy.service.tariff.TariffConstants; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -32,14 +31,14 @@ public class ImageFoundRequestHandler implements RequestHandler { private final SearchImageService searchImageService; - private final GoogleVisionSearchService googleVisionSearchService; - private final CheckCounterService checkCounterService; private final FileEntityRepository fileEntityRepository; private final TariffInfoService tariffInfoService; + private final SearchProperties searchProperties; + @Override public BaseResponse handle(BaseRequest request) throws Exception { ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), @@ -51,48 +50,76 @@ public class ImageFoundRequestHandler implements RequestHandler { .orElseThrow(() -> { throw new NotValidFieldException("File not found", new BaseResponse(20007, MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), - Map.of("fileId",fileId))); - }); + Map.of("fileId", fileId)));}); -// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity); + List allYandexImages = new ArrayList<>(); + List allGoogleImages = new ArrayList<>(); + boolean hasTimeout = false; - //TODO uncommited when add billing -// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); + + log.info("Search settings: useGoogle={}, useYandex={}", useGoogle, useYandex); + + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", + "exact_matches"); + log.info("searchResponseGoogle OK"); + allGoogleImages = searchImageService.getAllImagesWithoutPagination( + searchResponseGoogle, "exact_matches"); + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", fileId, e); + } + } else { + log.info("Google search is disabled by settings"); + } + + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", + "visual_matches"); + log.info("searchResponseYandex OK"); + allYandexImages = searchImageService.getAllImagesWithoutPagination( + searchResponseYandex, "visual_matches"); + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", fileId); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", fileId, e); + } + } else { + log.info("Yandex search is disabled by settings"); + } + + if (allYandexImages.isEmpty() && allGoogleImages.isEmpty()) { + log.warn("No results from any search engine"); + String errorMessage = useGoogle || useYandex ? "No results found" : "All search engines are disabled"; - String searchResponseYandex; - String searchResponseGoogle; - try { - searchResponseYandex = searchImageService.searchReverseByPublicUrl(fileEntity, "yandex_reverse_image", - "visual_matches"); - searchResponseGoogle = searchImageService.searchReverseByPublicUrl(fileEntity, "google_lens", - "exact_matches"); - } catch (TimeoutException e) { - log.warn("Search timeout for file {}, returning empty results", fileId); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - "Search completed (partial)", Map.of( - "results", List.of(), - "total", 0, - "searchStatus", "timeout" - )); - } catch (IOException e) { - log.error("Search failed for file {}", fileId, e); - return new BaseResponse(request.getMsgId(), 20007, - "Search service temporarily unavailable", - Map.of("fileId", fileId)); + errorMessage, new YandexSearchResponse()); + } + + List allUniqueImages; + if (useGoogle && useYandex) { + allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, allGoogleImages); + log.info("Merged results from both engines: {} unique images", allUniqueImages.size()); + } else if (useGoogle) { + allUniqueImages = allGoogleImages; + log.info("Results only from Google: {} images", allUniqueImages.size()); + } else { + allUniqueImages = allYandexImages; + log.info("Results only from Yandex: {} images", allUniqueImages.size()); } tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH); - checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType()); - List allYandexImages = searchImageService.getAllImagesWithoutPagination( - searchResponseYandex, "visual_matches"); - List allGoogleImages = searchImageService.getAllImagesWithoutPagination( - searchResponseGoogle, "exact_matches"); - - List allUniqueImages = searchImageService.removeDuplicateUrls(allYandexImages, - allGoogleImages); - int page = imageSearchRequest.getPage() != null ? imageSearchRequest.getPage() : 1; int pageSize = 5; @@ -106,7 +133,9 @@ public class ImageFoundRequestHandler implements RequestHandler { finalResponse.setTotalResults(allUniqueImages.size()); finalResponse.setTotalPages((int) Math.ceil((double) allUniqueImages.size() / (pageSize * 2))); + String message = hasTimeout ? "Search completed (partial)" : MessageCode.SUCCESS.getDescription(); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), finalResponse); + message, finalResponse); } } diff --git a/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java new file mode 100644 index 0000000..876c71e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/MonitoringHandler.java @@ -0,0 +1,79 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.dto.monitoring.SetMonitoringRequest; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.UserNotHavePermission; +import ru.soune.nocopy.service.monitoring.FileMonitoringService; +import ru.soune.nocopy.service.register.AuthService; + +import java.io.FileNotFoundException; +import java.util.Arrays; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringHandler implements RequestHandler { + + private final ObjectMapper objectMapper; + + private final FileMonitoringService monitoringService; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + SetMonitoringRequest setRequest = objectMapper.convertValue( + request.getMessageBody(), SetMonitoringRequest.class); + + String authToken = setRequest.getAuthToken(); + AuthToken userAuthToken = authService.getAuthToken(authToken); + + if (authToken == null || userAuthToken == null) { + return new BaseResponse(30007, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), + MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), + Map.of("token", authToken)); + } + + User user = userAuthToken.getUser(); + Long userId = user.getId(); + String typeStr = setRequest.getMonitoringType(); + MonitoringType monitoringType; + + try { + monitoringType = MonitoringType.valueOf(typeStr); + } catch (IllegalArgumentException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.MONITORING_TYPE_NOT_FOUND.getCode(), + MessageCode.MONITORING_TYPE_NOT_FOUND.getDescription(), + Map.of("monitoring", Arrays.asList(MonitoringType.values()))); + } + + try { + monitoringService.setMonitoring(userId, setRequest.getFileId(), monitoringType); + } catch (UserNotHavePermission e) { + return new BaseResponse(request.getMsgId(), + MessageCode.USER_NOT_HAD_PERMISSION.getCode(), + MessageCode.USER_NOT_HAD_PERMISSION.getDescription(), Map.of("token", authToken)); + } catch (FileNotFoundException e) { + return new BaseResponse(request.getMsgId(), + MessageCode.FILE_NOT_FOUND.getCode(), + MessageCode.FILE_NOT_FOUND.getDescription(), Map.of("fileId", setRequest.getFileId())); + } + + MonitoringStatusResponse status = monitoringService.getMonitoringStatus(setRequest.getFileId(), userId); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + "Monitoring settings updated", status); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java new file mode 100644 index 0000000..57167af --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/PaymentHandler.java @@ -0,0 +1,51 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.payment.PaymentRequest; +import ru.soune.nocopy.dto.payment.PaymentResponse; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.exception.PaymentNotFoundException; +import ru.soune.nocopy.service.payment.PaymentService; + +import java.util.List; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class PaymentHandler implements RequestHandler{ + + private final PaymentService paymentService; + + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + PaymentRequest paymentRequest = objectMapper.convertValue(request.getMessageBody(), PaymentRequest.class); + String action = paymentRequest.getAction(); + + switch (action) { + case "user_payments": + List payments = paymentService.userPayments(paymentRequest.getEmail()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), new PaymentResponse(payments)); + case "payment_info": + try { + Payment payment = paymentService.paymentInfo(paymentRequest.getPaymentUuid()); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), new PaymentResponse(List.of(payment))); + } catch (PaymentNotFoundException e) { + return new BaseResponse(request.getMsgId(), MessageCode.PAYMENT_NOT_FOUND.getCode(), + MessageCode.PAYMENT_NOT_FOUND.getDescription(), Map.of("paymentUuid", + paymentRequest.getPaymentUuid())); + } + } + + return null; + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 6f69ac2..d9a54bd 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -12,6 +12,8 @@ import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.entity.referral.Referral; import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.exception.CompanyAlreadyExist; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; @@ -19,6 +21,7 @@ import ru.soune.nocopy.repository.ReferralJpaRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.user.AdditionalInfoService; import java.util.Arrays; import java.util.Map; @@ -41,71 +44,97 @@ public class RegRequestHandler implements RequestHandler { private final ReferralJpaRepository referralJpaRepository; + private final AdditionalInfoService additionalInfoService; + @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); + try { + if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) { + RegAnswer regAnswer = new RegAnswer(); + regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail()))); + regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone()))); - if (userRepository.existsByEmail(regRequest.getEmail()) || userRepository.existsByPhone(regRequest.getPhone())) { - RegAnswer regAnswer = new RegAnswer(); - regAnswer.setFieldErrors(Arrays.asList(Map.of("email", regRequest.getEmail()))); - regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", regRequest.getPhone()))); - - throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " + - regRequest.getPhone(), new BaseResponse(request.getMsgId(), - MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(), - MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); - } - - String referralLink = regRequest.getReferralLink(); - - if (referralLink != null) { - Referral referral = referralJpaRepository.findByReferralLink(referralLink); - if (referral == null) { - throw new NotValidFieldException("Referral link is not found:" + referralLink + - referralLink, new BaseResponse(request.getMsgId(), - MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(), - MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of( - "referralLink", referralLink))); + throw new NotValidFieldException("User already exists with email:" + regRequest.getEmail() + " or phone: " + + regRequest.getPhone(), new BaseResponse(request.getMsgId(), + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(), + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); } + + String referralLink = regRequest.getReferralLink(); + String mailVerified = regRequest.getMailVerified(); + + if (referralLink != null) { + Referral referral = referralJpaRepository.findByReferralLink(referralLink); + if (referral == null) { + throw new NotValidFieldException("Referral link is not found:" + referralLink + + referralLink, new BaseResponse(request.getMsgId(), + MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getCode(), + MessageCode.REFERRAL_LINK_IS_NOT_EXIST.getDescription(), Map.of( + "referralLink", referralLink))); + } + + if (mailVerified == null) { + throw new NotValidFieldException("Mailverified is null:" + referralLink + + referralLink, new BaseResponse(request.getMsgId(), + MessageCode.MAIL_VERIFIED_NULL.getCode(), + MessageCode.MAIL_VERIFIED_NULL.getDescription(), Map.of( + "mailVerified", mailVerified))); + } + } + + BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); + + regRequestValidator.validate(regRequest, bindingResult); + + if (bindingResult.hasErrors()) { + throw new ValidationException(bindingResult, request.getMsgId()); + } + + boolean verified = mailVerified.equals("true"); + + AuthToken authToken = authService.register(regRequest, verified); + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + RegAnswer regAnswer = new RegAnswer(); + + if (verified) { + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + "message", "not send", + "email", regRequest.getEmail() + )); + } + + regAnswer.setVerified(false); + regAnswer.setActive(false); + } else { + regAnswer.setUserId(authToken.getUser().getId()); + + regAnswer.setVerified(true); + regAnswer.setActive(true); + } + + String token = authToken.getToken(); + authService.useUserAuthToken(token); + + regAnswer.setToken(token); + regAnswer.setUserId(authToken.getUser().getId()); + + if (regRequest.getAccountType().equals("b2c")) { + referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); + } + + additionalInfoService.additionalInfo(regRequest.getIp(), regRequest.getUserAgent(), authToken.getUser()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), regAnswer); + } catch (CompanyAlreadyExist e) { + return new BaseResponse(request.getMsgId(), MessageCode.COMPANY_ALREADY_EXISTS.getCode(), + MessageCode.COMPANY_ALREADY_EXISTS.getDescription(), Map.of( + "inn", regRequest.getInn())); } - - BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); - - regRequestValidator.validate(regRequest, bindingResult); - - if (bindingResult.hasErrors()) { - throw new ValidationException(bindingResult, request.getMsgId()); - } - - AuthToken authToken = authService.register(regRequest); -// EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); - -// try { -// emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); -// } catch (Exception e) { -// return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), -// MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( -// "message", "not send", -// "email", regRequest.getEmail() -// )); -// } - - - String token = authToken.getToken(); - authService.useUserAuthToken(token); - - RegAnswer regAnswer = new RegAnswer(); - - regAnswer.setUserId(authToken.getUser().getId()); - regAnswer.setToken(token); - regAnswer.setVerified(true); - regAnswer.setActive(true); - - if (regRequest.getAccountType().equals("b2c")) { - referralService.onRegister(authToken.getUser().getId(), regRequest.getReferralLink()); - } - - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), regAnswer); } } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java new file mode 100644 index 0000000..e51a806 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ResetPasswordHandler.java @@ -0,0 +1,112 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.file.ActionResponse; +import ru.soune.nocopy.dto.register.ResetPasswordRequest; +import ru.soune.nocopy.dto.register.ResetPasswordResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.register.AuthService; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ResetPasswordHandler implements RequestHandler{ + + private final ObjectMapper objectMapper; + + private final EmailService emailService; + + private final AuthService authService; + + private final UserRepository userRepository; + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + + private final AuthTokenRepository authTokenRepository; + + private final PasswordEncoder passwordEncoder; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ResetPasswordRequest resetPasswordRequest = objectMapper.convertValue(request.getMessageBody(), + ResetPasswordRequest.class); + String action = resetPasswordRequest.getAction(); + User user = userRepository.findByEmail(resetPasswordRequest.getEmail()); + AuthToken authToken; + + if (user == null) { + throw new UserNotFoundException("User not found with email: " + resetPasswordRequest.getEmail()); + } + + switch (action) { + case "resetPassword": + authToken = authService.generateAuthToken(user); + + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + + emailService.sendResetPasswordEmail(authToken.getUser(), emailToken.getToken()); + + return successResponse(request, authToken, user, MessageCode.SUCCESS.getDescription()); + case "confirmVerification": + String verifyToken = resetPasswordRequest.getVerifyToken(); + + EmailVerificationToken verificationToken = + emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), verifyToken); + + if (verificationToken == null) { + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken)); + } + + List authTokens = authTokenRepository.findByUserId(user.getId()); + + authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst(); + + user.setPassword(passwordEncoder.encode(resetPasswordRequest.getPassword())); + user.setActive(true); + + emailVerificationTokenRepository.delete(verificationToken); + + User saveUser = userRepository.save(user); + + return successResponse(request, authToken, saveUser, MessageCode.SUCCESS.getDescription()); + default: + ActionResponse response = ActionResponse.builder() + .action(action) + .availableActions(Arrays.asList("confirmVerification", "resetPassword")) + .build(); + + return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, response); + + } + } + + private BaseResponse successResponse(BaseRequest request, AuthToken authToken, User user, String message) { + ResetPasswordResponse resetPasswordResponse = new ResetPasswordResponse(); + resetPasswordResponse.setAuthToken(authToken.getToken()); + resetPasswordResponse.setUserId(user.getId()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, resetPasswordResponse); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java index 50ea527..036bc2a 100644 --- a/src/main/java/ru/soune/nocopy/handler/TariffHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/TariffHandler.java @@ -10,6 +10,7 @@ import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.tarriff.TariffDTO; import ru.soune.nocopy.dto.tarriff.TariffRequest; import ru.soune.nocopy.dto.tarriff.TariffResponse; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.repository.AuthTokenRepository; @@ -44,6 +45,8 @@ public class TariffHandler implements RequestHandler { return handleUpdateTariff(request, tariffRequest); case "get" : return handleGetTariffs(request, tariffRequest); + case "tariffs_by_type" : + return handleGetTariffsByType(request, tariffRequest); } } catch (Exception e) { @@ -62,7 +65,7 @@ public class TariffHandler implements RequestHandler { private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) { tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(), tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(), - tariffRequest.getMaxFilesCount()); + tariffRequest.getMaxFilesCount(), tariffRequest.getTariffTerm()); TariffResponse tariffResponse = TariffResponse.builder() .tariffName(tariffRequest.getName()) @@ -120,10 +123,19 @@ public class TariffHandler implements RequestHandler { private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) { AuthToken authToken = authTokenRepository.findByToken(tariffRequest.getUserToken()).orElseThrow(); User user = authToken.getUser(); - List allTariffs = tariffService.getTariffByAccountType(user); + List allTariffs = tariffService.getTariffByAccountTypeAndTariffTerm(user, + TariffTimeTerm.valueOf(tariffRequest.getTariffTerm().toUpperCase())); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), Map.of("tariffs", allTariffs)); } + + private BaseResponse handleGetTariffsByType(BaseRequest request, TariffRequest tariffRequest) { + List allTariffs = tariffService.getTariffByType(tariffRequest.getType()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("token_tariffs", allTariffs)); + } } diff --git a/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java b/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java index 7c53c9f..c56dd12 100644 --- a/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/TariffInfoHandler.java @@ -12,6 +12,7 @@ import ru.soune.nocopy.dto.tarriff.TariffInfoRequest; import ru.soune.nocopy.dto.tarriff.TariffInfoResponse; import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.TariffStatus; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.repository.TariffRepository; import ru.soune.nocopy.service.tariff.TariffInfoService; @@ -75,13 +76,14 @@ public class TariffInfoHandler implements RequestHandler { try { Long tariffId = tariffInfoRequest.getTariffId(); Tariff tariff= tariffRepository.findById(tariffId).orElseThrow(); + TariffTimeTerm tariffTerm = tariff.getTariffTerm(); + LocalDateTime endDateTime = LocalDateTime.now().plusDays(tariffTerm.getDays()); TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(null, tariffInfoRequest.getStatus() != null ? tariffInfoRequest.getStatus() : TariffStatus.ACTIVE.name(), tariffInfoRequest.getStartTariff() != null ? tariffInfoRequest.getStartTariff() : LocalDateTime.now(), - tariffInfoRequest.getEndTariff() != null ? tariffInfoRequest.getEndTariff() : LocalDateTime.now().plusDays(30), - tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), tariff.getDiskSize(), 0L, - 0, tariff.getMaxFilesCount()); + endDateTime, tariffInfoRequest.getTariffId(), tariff.getName(), tariff.getTokens(), + tariff.getDiskSize(), 0L, 0, tariff.getMaxFilesCount()); TariffInfoDTO createdTariffInfo = tariffInfoService.createTariffInfo(tariffInfoDTO); diff --git a/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java new file mode 100644 index 0000000..07edf10 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/UserInfoHandler.java @@ -0,0 +1,214 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; + +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.user.UserAdditionalInfoBody; + +import ru.soune.nocopy.entity.user.UserAdditionalInfo; +import ru.soune.nocopy.entity.user.UserAdditionalInfoAnswer; +import ru.soune.nocopy.entity.user.UserAdditionalInfoListAnswer; +import ru.soune.nocopy.exception.UserAdditionalInfoNotFound; +import ru.soune.nocopy.service.user.AdditionalInfoService; + + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +@Slf4j +@RequiredArgsConstructor +public class UserInfoHandler implements RequestHandler { + + private final AdditionalInfoService additionalInfoService; + private final ObjectMapper objectMapper; + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + try { + UserAdditionalInfoBody body = objectMapper.convertValue(request.getMessageBody(), + UserAdditionalInfoBody.class); + + log.info("Processing UserInfoHandler with action: {}", body.getAction()); + + return switch (body.getAction()) { + case "getAll" -> getAllAdditionalInfos(request); + case "getById" -> getAdditionalInfoById(request, body.getUserId()); + case "update" -> updateAdditionalInfo(request, body); + case "delete" -> deleteAdditionalInfo(request, body.getUserId()); + default -> new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + body.getAction(), + null + ); + }; + } catch (IllegalArgumentException e) { + log.error("Validation error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (UserAdditionalInfoNotFound e) { + log.error("User additional info not found", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error in UserInfoHandler", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error: " + e.getMessage(), + null); + } + } + + private BaseResponse getAllAdditionalInfos(BaseRequest request) { + log.info("Getting all additional infos"); + + List infos = additionalInfoService.getAllAdditionalInfos(); + + List answerItems = infos.stream() + .map(this::convertToAnswer) + .collect(Collectors.toList()); + + UserAdditionalInfoListAnswer listAnswer = UserAdditionalInfoListAnswer.builder() + .items(answerItems) + .count(answerItems.size()) + .build(); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + listAnswer + ); + } + + private BaseResponse getAdditionalInfoById(BaseRequest request, Long userId) { + log.info("Getting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for getById action"); + } + + UserAdditionalInfo info = additionalInfoService.getAdditionalInfoByUser(userId); + + if (info == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(info); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse updateAdditionalInfo(BaseRequest request, UserAdditionalInfoBody body) { + log.info("Updating additional info for user: {}", body.getUserId()); + + if (body.getUserId() == null) { + throw new IllegalArgumentException("User ID is required for update action"); + } + + validateIpAddress(body.getIpAddress()); + validateUserAgent(body.getUserAgent()); + + Optional updatedInfo = additionalInfoService.updateAdditionalInfo( + body.getUserId(), + body.getIpAddress(), + body.getUserAgent() + ); + + if (updatedInfo.isEmpty()) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", body.getUserId()) + ); + } + + UserAdditionalInfoAnswer answer = convertToAnswer(updatedInfo.get()); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + answer + ); + } + + private BaseResponse deleteAdditionalInfo(BaseRequest request, Long userId) { + log.info("Deleting additional info for user: {}", userId); + + if (userId == null) { + throw new IllegalArgumentException("User ID is required for delete action"); + } + + UserAdditionalInfo existingInfo = additionalInfoService.getAdditionalInfoByUser(userId); + + if (existingInfo == null) { + throw new UserAdditionalInfoNotFound( + String.format("User additional info not found for userId: %d", userId) + ); + } + + additionalInfoService.deleteAdditionalInfo(userId); + + return new BaseResponse( + request.getMsgId(), + MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), + Map.of("deleted", true, "user_id", userId) + ); + } + + private UserAdditionalInfoAnswer convertToAnswer(UserAdditionalInfo info) { + return UserAdditionalInfoAnswer.builder() + .id(info.getId()) + .userId(info.getUser() != null ? info.getUser().getId() : null) + .ipAddress(info.getIpAddress()) + .userAgent(info.getUserAgent()) + .registrationDate(info.getRegistrationDate()) + .build(); + } + + private void validateIpAddress(String ip) { + if (ip == null || ip.trim().isEmpty()) { + throw new IllegalArgumentException("IP address is required for update action"); + } + + String ipv4Pattern = + "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"; + + if (!ip.trim().matches(ipv4Pattern)) { + log.warn("Invalid IP address format: {}", ip); + throw new IllegalArgumentException("Invalid IP address format"); + } + } + + private void validateUserAgent(String userAgent) { + if (userAgent == null || userAgent.trim().isEmpty()) { + throw new IllegalArgumentException("User agent is required for update action"); + } + + if (userAgent.length() > 500) { + throw new IllegalArgumentException("User agent must not exceed 500 characters"); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index 373b953..5dac630 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -23,6 +23,7 @@ import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.UserService; import java.io.IOException; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -53,9 +54,9 @@ public class VerifyRegisterUserHandler implements RequestHandler { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); - Optional userAuthTokens = authTokenRepository.findByUserId(userId); + List userAuthTokens = authTokenRepository.findByUserId(userId); - AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get(); + AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.getFirst(); if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) { return handleResend(request, user, authToken); @@ -68,13 +69,13 @@ public class VerifyRegisterUserHandler implements RequestHandler { EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId()); - if (token != null && !token.isExpired()) { - emailService.sendVerificationEmail(user, token.getToken()); - - log.info("Verification token resent for user {}", user.getEmail()); - return successResponse(request, authToken, "Verification code has been resent", false, - false, user.getEmail()); - } +// if (token != null && !token.isExpired()) { +// emailService.sendVerificationEmail(user, token.getToken()); +// +// log.info("Verification token resent for user {}", user.getEmail()); +// return successResponse(request, authToken, "Verification code has been resent", false, +// false, user.getEmail()); +// } if (token != null) { emailVerificationTokenRepository.delete(token); diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java new file mode 100644 index 0000000..8b658b4 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationHandler.java @@ -0,0 +1,160 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.violation.ViolationRequest; +import ru.soune.nocopy.dto.violation.ViolationResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.file.FileEntityService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.violation.ViolationService; +import ru.soune.nocopy.service.violation.ViolationStatus; + +import java.io.FileNotFoundException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Map; + +@Component +@Slf4j +@RequiredArgsConstructor +public class ViolationHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationService violationService; + + private final FileEntityRepository fileRepository; + + private final AuthService authService; + + private final FileEntityService fileEntityService; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ViolationRequest violationRequest = objectMapper.convertValue(request.getMessageBody(), ViolationRequest.class); + + validateRequest(violationRequest); + + String token = violationRequest.getToken(); + Long userId = authService.useUserAuthToken(token); + + List targetFiles; + + if (violationRequest.getFileId() != null && !violationRequest.getFileId().isEmpty()) { + FileEntity file = fileRepository.findById(violationRequest.getFileId()) + .orElseThrow(() -> new FileNotFoundException("File not found with id: " + violationRequest.getFileId())); + targetFiles = List.of(file); + } else { + targetFiles = fileEntityService.getAllUserFiles(userId); + } + + LocalDateTime startDate = null; + LocalDateTime endDate = null; + + if (violationRequest.getStartDate() != null && violationRequest.getEndDate() != null) { + startDate = parseDate(violationRequest.getStartDate()); + endDate = parseDate(violationRequest.getEndDate()); + } + + if ("updateStatus".equals(violationRequest.getAction())) { + String status = violationRequest.getStatus(); + Long violationId = violationRequest.getViolationId(); + + violationService.changeStatus(ViolationStatus.valueOf(status), violationId); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(Map.of("status", status)) + .build(); + } + + if ("group".equalsIgnoreCase(violationRequest.getAction())) { + Map groupedData = violationService.getGroupedViolations( + targetFiles, + violationRequest.getGroupBy(), + violationRequest.getStatus(), + startDate, + endDate, + violationRequest.getSortDirection()); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(groupedData) + .build(); + } else { + Page violationPage; + + if (startDate != null && endDate != null) { + violationPage = violationService.getViolationsByFilesAndDateRange( + targetFiles, startDate, endDate, + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } else if (violationRequest.getStatus() != null && !violationRequest.getStatus().isEmpty()) { + violationPage = violationService.getViolationsByFilesAndStatus( + targetFiles, violationRequest.getStatus(), + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } else { + violationPage = violationService.getViolationsByFiles( + targetFiles, + violationRequest.getPage(), violationRequest.getSize(), violationRequest.getSortDirection() + ); + } + + List violationDtos = violationPage.getContent() + .stream() + .map(ViolationResponse.ViolationDto::fromEntity) + .toList(); + + ViolationResponse response = ViolationResponse.builder() + .violations(violationDtos) + .totalElements(violationPage.getTotalElements()) + .totalPages(violationPage.getTotalPages()) + .currentPage(violationPage.getNumber()) + .pageSize(violationPage.getSize()) + .hasNext(violationPage.hasNext()) + .hasPrevious(violationPage.hasPrevious()) + .build(); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(response) + .build(); + } + } + + private void validateRequest(ViolationRequest request) { + if (request.getPage() < 0) { + request.setPage(0); + } + + if (request.getSize() <= 0 || request.getSize() > 100) { + request.setSize(10); + } + + if (!request.getSortDirection().equalsIgnoreCase("asc") && + !request.getSortDirection().equalsIgnoreCase("desc")) { + request.setSortDirection("desc"); + } + } + + private LocalDateTime parseDate(String dateStr) { + try { + return LocalDateTime.parse(dateStr, DATE_FORMATTER); + } catch (DateTimeParseException e) { + log.error(e.getMessage()); + return null; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java new file mode 100644 index 0000000..9c0a8f9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationNotionHandler.java @@ -0,0 +1,260 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.violation.*; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.violation.ViolationNotionService; + +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ViolationNotionHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationNotionService notionService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) { + try { + ViolationNotionRequest notionRequest = objectMapper.convertValue( + request.getMessageBody(), ViolationNotionRequest.class); + + if (notionRequest.getToken() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Token is required", + null); + } + + Optional tokenOptional = authTokenRepository.findByToken(notionRequest.getToken()); + if (tokenOptional.isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_TOKEN.getCode(), + "Invalid token", + null); + } + + Long userId = tokenOptional.get().getUser().getId(); + String action = notionRequest.getAction(); + + if (action == null || action.trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Action is required", + null); + } + + switch (action) { + case "get_notions": + return handleGetNotions(request, notionRequest, userId); + case "get_notion": + return handleGetNotion(request, notionRequest, userId); + case "add_notion": + return handleAddNotion(request, notionRequest, userId); + case "update_notion": + return handleUpdateNotion(request, notionRequest, userId); + case "delete_notion": + return handleDeleteNotion(request, notionRequest, userId); + default: + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_ACTION.getCode(), + "Invalid action: " + action, + null); + } + + } catch (IllegalArgumentException e) { + log.error("Validation error: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.VALIDATION_ERROR.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + log.error("Security error: {}", e.getMessage()); + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } catch (Exception e) { + log.error("Unexpected error", e); + return new BaseResponse( + request.getMsgId(), + MessageCode.INTERNAL_ERROR.getCode(), + "Internal server error", + null); + } + } + + private BaseResponse handleGetNotions(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getViolationId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "ViolationId is required for get_notions action", + null); + } + + try { + ViolationNotionsListResponse notions = notionService.getNotionsByViolationId( + notionRequest.getViolationId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notions) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleGetNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "NotionId is required for get_notion action", + null); + } + + try { + ViolationNotionResponse notion = notionService.getNotionById( + notionRequest.getNotionId(), userId); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(notion) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleAddNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getViolationId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "ViolationId is required for add_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "Message is required for add_notion action", + null); + } + + try { + ViolationNotionResponse created = notionService.createNotion( + notionRequest.getViolationId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(created) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleUpdateNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "NotionId is required for update_notion action", + null); + } + + if (notionRequest.getMessage() == null || notionRequest.getMessage().trim().isEmpty()) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "Message is required for update_notion action", + null); + } + + try { + ViolationNotionResponse updated = notionService.updateNotion( + notionRequest.getNotionId(), userId, notionRequest.getMessage()); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(updated) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } + } + + private BaseResponse handleDeleteNotion(BaseRequest request, ViolationNotionRequest notionRequest, Long userId) { + if (notionRequest.getNotionId() == null) { + return new BaseResponse( + request.getMsgId(), + MessageCode.INVALID_JSON_BODY.getCode(), + "NotionId is required for delete_notion action", + null); + } + + try { + notionService.deleteNotion(notionRequest.getNotionId(), userId); + DeleteNotionResponse deleteResponse = DeleteNotionResponse.builder() + .notionId(notionRequest.getNotionId()) + .message("Notion deleted successfully") + .deleted(true) + .build(); + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(deleteResponse) + .build(); + } catch (IllegalArgumentException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.NOTION_NOT_FOUND.getCode(), + e.getMessage(), + null); + } catch (SecurityException e) { + return new BaseResponse( + request.getMsgId(), + MessageCode.ACCESS_DENIED.getCode(), + e.getMessage(), + null); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java new file mode 100644 index 0000000..6f2de2c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/ViolationStatisticsHandler.java @@ -0,0 +1,90 @@ +package ru.soune.nocopy.handler; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.dto.BaseRequest; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.violation.ViolationStatisticsRequest; +import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ViolationStatisticsHandler implements RequestHandler { + private final ObjectMapper objectMapper; + + private final ViolationService violationService; + + private final AuthTokenRepository authTokenRepository; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + @Override + public BaseResponse handle(BaseRequest request) throws Exception { + ViolationStatisticsRequest statRequest = objectMapper.convertValue( + request.getMessageBody(), ViolationStatisticsRequest.class); + + validateRequest(statRequest); + + LocalDateTime startDate = null; + LocalDateTime endDate = null; + + if (statRequest.getStartDate() != null && statRequest.getEndDate() != null) { + startDate = parseDate(statRequest.getStartDate()); + endDate = parseDate(statRequest.getEndDate()); + + if (endDate != null && !statRequest.getEndDate().contains(" ")) { + endDate = endDate.withHour(23).withMinute(59).withSecond(59); + } + } + + Optional tokenOptional = + authTokenRepository.findByToken(statRequest.getToken()); + + if (tokenOptional.isEmpty()) { + throw new IllegalArgumentException("User token is required"); + } + + Long userId = tokenOptional.orElseThrow().getUser().getId(); + + ViolationStatisticsResponse statistics = violationService.getViolationStatistics( + userId, + statRequest.getFileId(), + startDate, + endDate); + + return BaseResponse.builder() + .msgId(request.getMsgId()) + .messageBody(statistics) + .build(); + } + + private void validateRequest(ViolationStatisticsRequest request) { + if (request.getToken() == null) { + throw new IllegalArgumentException("Token is required"); + } + } + + private LocalDateTime parseDate(String dateStr) { + try { + return LocalDateTime.parse(dateStr, DATE_FORMATTER); + } catch (DateTimeParseException e) { + try { + return LocalDateTime.parse(dateStr + " 00:00:00", DATE_FORMATTER); + } catch (DateTimeParseException ex) { + log.error("Error parsing date: {}", dateStr, ex); + return null; + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java index b57c7fb..84b708a 100644 --- a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java +++ b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.handler.validator; import org.apache.commons.validator.routines.EmailValidator; +import org.apache.commons.validator.routines.InetAddressValidator; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; @@ -28,6 +29,7 @@ public class RegRequestValidator implements Validator { validateEmail(request.getEmail(), errors); validateCompanyName(request.getCompanyName(), errors); validateFullName(request.getFullName(), errors); + validateAdditionalInfo(request.getIp(), request.getUserAgent(), errors); } private void validateFullName(String fullName, Errors errors) { @@ -73,6 +75,21 @@ public class RegRequestValidator implements Validator { } } + public void validateAdditionalInfo(String ip, String userAgent, Errors errors) { + if (ip == null || ip.trim().isEmpty()) { + errors.rejectValue("ip", "ip.required", "IP address is required"); + } else if (!InetAddressValidator.getInstance().isValid(ip.trim())) { + errors.rejectValue("ip", "ip.invalid", "Invalid IP address format"); + } + + if (userAgent == null || userAgent.trim().isEmpty()) { + errors.rejectValue("userAgent", "userAgent.required", "User agent is required"); + } else if (userAgent.length() > 500) { + errors.rejectValue("userAgent", "userAgent.tooLong", "User agent must not exceed " + + "500 characters"); + } + } + public void validateEmail(String email, Errors errors) { if (email == null || email.trim().isEmpty()) { errors.rejectValue("email", "email.is.empty", "Email must not be empty"); diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index c1b65e2..48efb76 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -20,5 +20,5 @@ public interface AuthTokenRepository extends JpaRepository { nativeQuery = true) Long findUserIdByToken(String token); Optional findByUserIdAndIsActive(Long userId, boolean isActive); - Optional findByUserId(Long userId); + List findByUserId(Long userId); } diff --git a/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java b/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java index b075868..258481f 100644 --- a/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/CompanyRepository.java @@ -13,6 +13,8 @@ import java.util.Optional; public interface CompanyRepository extends JpaRepository { Optional findByCompanyName(String companyName); + Optional findByInn(String companyName); + boolean existsByCompanyName(String companyName); List findByCompanyNameContainingIgnoreCase(String name); diff --git a/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java new file mode 100644 index 0000000..159d388 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ComplaintEntityRepository.java @@ -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 { + Optional findById(Long id); + Page findAll(Pageable pageable); + Page findByStatus(ComplaintStatus status, Pageable pageable); + List 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); +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java b/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java new file mode 100644 index 0000000..c645496 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FileAppealRepository.java @@ -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 { + + Optional findByFileIdAndStatus(String fileId, AppealStatus status); + + List findByFileId(String fileId); + + Page findByStatus(AppealStatus status, Pageable pageable); + + Page findByUserId(Long userId, Pageable pageable); + + @Query("SELECT fa FROM FileAppeal fa WHERE fa.status = :status ORDER BY fa.createdAt ASC") + Page findPendingAppeals(@Param("status") AppealStatus status, Pageable pageable); + + boolean existsByFileIdAndStatusIn(String fileId, List statuses); +} + diff --git a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java index 2fd8655..e54e23b 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileEntityRepository.java @@ -1,5 +1,7 @@ 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; @@ -8,6 +10,7 @@ import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -21,6 +24,8 @@ public interface FileEntityRepository extends JpaRepository List findByUserIdAndStatus(Long userId, FileStatus status); + List findByUserIdAndStatusIn(Long userId, Collection statuses); + Optional findByFilePath(String filePath); Optional findByUploadSessionId(String uploadSessionId); @@ -52,4 +57,25 @@ public interface FileEntityRepository extends JpaRepository String findFileIdByFilePath(@Param("filePath") String filePath); List findFileByUserIdAndStatus(Long userId, FileStatus status); + + @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.mimeType = :mimeType") + List findByUserIdAndMimeType(Long userId, String mimeType); + + @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType") + List findByMimeType(String mimeType); + + @Query("SELECT f FROM FileEntity f WHERE f.mimeType = :mimeType AND f.userId IN :userIds") + List findByMimeTypeAndUserIds(String mimeType, List userIds); + + @Query("SELECT MAX(f.supportId) FROM FileEntity f") + Integer findMaxSupportId(); + + List findByThumbnailPathIsNull(); + + Page findByStatusIn(List statuses, Pageable pageable); + + List findByUserIdAndStatusIn(Long userId, List statuses); + + @Query("SELECT f FROM FileEntity f WHERE f.userId = :userId AND f.status IN :statuses") + List findByStatuses(Long userId, @Param("statuses") List statuses); } diff --git a/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java new file mode 100644 index 0000000..126aa7a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/FileMonitoringRepository.java @@ -0,0 +1,25 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface FileMonitoringRepository extends JpaRepository { + + Optional findByFileId(String fileId); + + List findByUserIdAndIsActiveTrue(Long userId); + + @Query("SELECT fm FROM FileMonitoringEntity fm WHERE fm.isActive = true AND fm.monitoringType != 'NONE' AND fm.nextRun <= :now") + List findReadyForRun(@Param("now") LocalDateTime now); + + List findByMonitoringTypeAndIsActiveTrue(MonitoringType type); +} diff --git a/src/main/java/ru/soune/nocopy/repository/FileUploadSessionRepository.java b/src/main/java/ru/soune/nocopy/repository/FileUploadSessionRepository.java index 2b222a3..4bb6f6a 100644 --- a/src/main/java/ru/soune/nocopy/repository/FileUploadSessionRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/FileUploadSessionRepository.java @@ -27,4 +27,11 @@ public interface FileUploadSessionRepository extends JpaRepository findExpiredSessions( @Param("now") LocalDateTime now, @Param("statuses") Set statuses); + + + @Query("SELECT s FROM FileUploadSession s WHERE s.filePath = :filePath AND s.status = :status AND s.userId = :userId") + FileUploadSession findByFilePathAndStatus( + @Param("filePath") String filePath, + @Param("status") String status, + @Param("userId") Long userId); } diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java new file mode 100644 index 0000000..42eec8a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchResultRepository.java @@ -0,0 +1,23 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.search.GlobalSearchResult; + +import java.util.List; + +@Repository +public interface GlobalSearchResultRepository extends JpaRepository { + List findByTaskIdOrderByIdAsc(String taskId); + + void deleteByTaskId(String taskId); + + List findByTaskId(String taskId); + + @Query("SELECT g.fileId FROM GlobalSearchResult g WHERE g.taskId = :taskId") + List findFileIdsByTaskId(@Param("taskId") String taskId); + + long countByTaskId(String taskId); +} diff --git a/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java new file mode 100644 index 0000000..d95ff45 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/GlobalSearchTaskRepository.java @@ -0,0 +1,33 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.search.GlobalSearchTask; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface GlobalSearchTaskRepository extends JpaRepository { + Optional findByTaskIdAndUserId(String taskId, Long userId); + + Optional findByUserIdAndStatus(Long userId, String status); + + List findByUserIdOrderByCreatedAtDesc(Long userId); + + List findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(Long userId, LocalDateTime date); + + @Query("SELECT COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds") + long countByUserId(@Param("userIds") List userIds); + + @Query("SELECT t.status, COUNT(t) FROM GlobalSearchTask t WHERE t.userId IN :userIds GROUP BY t.status") + List countByStatusAndUserId(@Param("userIds") List userIds); + + @Query("SELECT t FROM GlobalSearchTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC") + List findTopByUserIdOrderByCreatedAtDesc(@Param("userId") Long userId, + org.springframework.data.domain.Pageable pageable); +} + diff --git a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java index add9315..0a68a2a 100644 --- a/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ImageSimilarityRepository.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.file.FileEntity; import java.util.List; +import java.util.Set; @Repository public interface ImageSimilarityRepository @@ -20,6 +21,8 @@ public interface ImageSimilarityRepository f.support_id AS supportId, f.created_at AS uploadDate, f.protection_status AS protectionStatus, + f.file_extension AS extension, + f.status AS fileStatus, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref @@ -28,6 +31,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId + AND f.status IN ('ACTIVE', 'MODERATION') """, nativeQuery = true) List findCandidates( @@ -43,6 +47,7 @@ public interface ImageSimilarityRepository f.support_id AS supportId, f.created_at AS uploadDate, f.protection_status AS protectionStatus, + f.file_extension AS extension, h.hash64_hi AS hash64Hi, h.hash64_lo AS hash64Lo FROM image_hashes ref @@ -51,6 +56,7 @@ public interface ImageSimilarityRepository JOIN file_entities f ON f.id = h.file_id WHERE ref.file_id = :fileId AND f.user_id = :userId + AND f.status = 'ACTIVE' """, nativeQuery = true) List findCandidatesFromUserFiles( @@ -58,6 +64,32 @@ public interface ImageSimilarityRepository @Param("userId") Long userId ); + @Query(value = """ + SELECT + f.id AS id, + f.original_file_name AS originalFileName, + f.file_size AS fileSize, + f.user_id AS userId, + f.support_id AS supportId, + f.created_at AS uploadDate, + f.protection_status AS protectionStatus, + f.file_extension AS extension, + h.hash64_hi AS hash64Hi, + h.hash64_lo AS hash64Lo + FROM image_hashes ref + JOIN image_hashes h + ON ref.file_id <> h.file_id + JOIN file_entities f + ON f.id = h.file_id + WHERE ref.file_id = :fileId AND f.user_id IN :userIds + AND f.status = 'ACTIVE' + """, + nativeQuery = true) + List findCandidatesFromUserFiles( + @Param("fileId") String fileId, + @Param("userIds") Set userIds + ); + @Query(value = """ SELECT @@ -70,11 +102,13 @@ public interface ImageSimilarityRepository f.stored_file_name AS similarFileId, f.support_id AS supportId, f.created_at AS uploadDate, + f.file_extension AS extension, f.protection_status AS protectionStatus FROM image_hashes h JOIN file_entities f ON f.id = h.file_id WHERE h.hash64_hi = :hash64Hi AND h.hash64_lo = :hash64Lo + AND f.status = 'ACTIVE' """, nativeQuery = true) List findExactDuplicates( diff --git a/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java b/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java new file mode 100644 index 0000000..f407060 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ModerationLogRepository.java @@ -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 { + + List findByFileIdOrderByCreatedAtDesc(String fileId); + + List findByModeratorId(Long moderatorId); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java new file mode 100644 index 0000000..6f772c5 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PaymentMethodRepository.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.payment.PaymentMethod; +import ru.soune.nocopy.entity.user.User; +import java.util.List; +import java.util.Optional; + +public interface PaymentMethodRepository extends JpaRepository { + List findByUserAndActiveTrue(User user); + Optional findByPaymentMethodId(String paymentMethodId); +} diff --git a/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java new file mode 100644 index 0000000..d599c4b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PaymentRepository.java @@ -0,0 +1,14 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payment.Payment; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PaymentRepository extends JpaRepository { + Optional findByPaymentUuid(String paymentUuid); + List findByUserId(Long userId); +} diff --git a/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java b/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java new file mode 100644 index 0000000..1b8effd --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PayoutMethodRepository.java @@ -0,0 +1,37 @@ +package ru.soune.nocopy.repository; + +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 org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payout.PayoutMethod; +import ru.soune.nocopy.entity.user.User; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface PayoutMethodRepository extends JpaRepository { + + Optional findByIdAndUserId(Long id, Long userId); + + @Query("SELECT p FROM PayoutMethod p WHERE p.user.Id = :userId AND TYPE(p) = :type") + Optional findByUserAndType(@Param("userId") Long userId, + @Param("type") Class type); + + List findByUserId(Long userId); + + Optional findByUserIdAndIsDefaultTrue(Long userId); + + long countByUserId(Long userId); + + @Query("SELECT COUNT(c) > 0 FROM CardPayoutMethod c WHERE c.cardHash = :cardHash") + boolean existsByCardHash(@Param("cardHash") String cardHash); + + @Modifying + @Query("UPDATE PayoutMethod p SET p.isDefault = false WHERE p.user.id = :userId AND p.id != :methodId") + void clearOtherDefault(@Param("userId") Long userId, @Param("methodId") Long methodId); + + Long user(User user); +} diff --git a/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java b/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java new file mode 100644 index 0000000..e6d90b0 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/PayoutRequestRepository.java @@ -0,0 +1,39 @@ +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 org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutRequestStatus; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface PayoutRequestRepository extends JpaRepository { + + List findByUserIdOrderByCreatedAtDesc(Long userId); + + Page findByUserId(Long userId, Pageable pageable); + + Optional findByIdAndUserId(Long id, Long userId); + + @Query("SELECT COALESCE(SUM(pr.amount), 0) FROM PayoutRequest pr " + + "WHERE pr.user.Id = :userId AND pr.status = 'COMPLETED'") + BigDecimal getTotalPayoutAmountByUser(@Param("userId") Long userId); + + boolean existsByUserIdAndStatusIn(Long userId, List statuses); + + List findByStatusOrderByCreatedAtAsc(PayoutRequestStatus status); + + Page findByStatus(PayoutRequestStatus status, Pageable pageable); + + @Query("SELECT pr FROM PayoutRequest pr WHERE pr.status = 'PENDING' " + + "AND pr.createdAt < :threshold") + List findPendingOlderThan(@Param("threshold") LocalDateTime threshold); +} diff --git a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java index edbdd48..c90eba9 100644 --- a/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/ReferralJpaRepository.java @@ -24,7 +24,10 @@ public interface ReferralJpaRepository extends JpaRepository { List findByInviterId(Long inviterId, Pageable pageable); @Modifying - @Query("UPDATE Referral r SET r.totalIncome = r.totalIncome + :amount WHERE r.userId = :userId") + @Query("UPDATE Referral r SET " + + "r.availableIncome = r.availableIncome + :amount ," + + "r.totalIncome = r.totalIncome + :amount " + + "WHERE r.userId = :userId") void increaseIncome(@Param("userId") Long userId, @Param("amount") int amount); @Modifying diff --git a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java index 9b9c8e7..cf3fe4b 100644 --- a/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java +++ b/src/main/java/ru/soune/nocopy/repository/SimilarImageProjection.java @@ -14,4 +14,6 @@ public interface SimilarImageProjection { Long getSupportId(); LocalDateTime getUploadDate(); ProtectionStatus getProtectionStatus(); + String getExtension(); + String getFileStatus(); } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java index a74bd87..6982080 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffInfoRepository.java @@ -1,9 +1,16 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.tarif.TariffInfo; +import java.time.LocalDateTime; +import java.util.List; + @Repository public interface TariffInfoRepository extends JpaRepository { + @Query("SELECT t FROM TariffInfo t WHERE t.autoRenewal = true AND t.endTariff <= :date") + List findForAutoRenewal(@Param("date") LocalDateTime date); } diff --git a/src/main/java/ru/soune/nocopy/repository/TariffRepository.java b/src/main/java/ru/soune/nocopy/repository/TariffRepository.java index a0e3bc9..b39e48a 100644 --- a/src/main/java/ru/soune/nocopy/repository/TariffRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/TariffRepository.java @@ -3,6 +3,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffTimeTerm; import ru.soune.nocopy.entity.tarif.TariffType; import java.util.List; @@ -13,5 +14,6 @@ public interface TariffRepository extends JpaRepository { Optional findByType(String type); List findAllByOrderByPriceAsc(); List findByTariffAccountType(String typeAccount); + List findByTariffAccountTypeAndTariffTerm(String typeAccount, TariffTimeTerm tariffTerm); Tariff findByName(String name); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java new file mode 100644 index 0000000..b45d0ca --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserAdditionalInfoRepository.java @@ -0,0 +1,9 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.user.UserAdditionalInfo; + +@Repository +public interface UserAdditionalInfoRepository extends JpaRepository { +} diff --git a/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java b/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java new file mode 100644 index 0000000..e3fad73 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/UserNotActiveRepository.java @@ -0,0 +1,16 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.user.UserNotActive; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface UserNotActiveRepository extends JpaRepository { + Optional findByUserId(Long userId); + List findByBlockedUntilBefore(LocalDateTime now); + +} diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index 88c3bce..8c7e31c 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,6 +2,7 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.user.User; import java.util.List; @@ -12,4 +13,5 @@ public interface UserRepository extends JpaRepository { boolean existsByPhone(@Size(min = 11, max = 14) String phone); List findByCompanyId(String companyId); long countByCompanyId(String companyId); + User findByPersonalTariffInfo(TariffInfo tariffInfo); } diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java new file mode 100644 index 0000000..ce6b74b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ViolationNotionRepository.java @@ -0,0 +1,22 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.violation.ViolationNotion; + +import java.util.List; + +@Repository +public interface ViolationNotionRepository extends JpaRepository { + @Query("SELECT vn FROM ViolationNotion vn " + + "JOIN FETCH vn.user " + + "WHERE vn.violation.id = :violationId " + + "ORDER BY vn.createdAt DESC") + List findByViolationId(@Param("violationId") Long violationId); + + @Query("SELECT COUNT(vn) FROM ViolationNotion vn WHERE vn.violation.id = :violationId") + long countByViolationId(@Param("violationId") Long violationId); + +} diff --git a/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java new file mode 100644 index 0000000..0834914 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/ViolationRepository.java @@ -0,0 +1,77 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.violation.Violation; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +@Repository +public interface ViolationRepository extends JpaRepository { + + Page findByFileEntity(FileEntity file, Pageable pageable); + + Page findByFileEntityAndStatus(FileEntity file, String status, Pageable pageable); + + Page findByFileEntityAndCreatedDateBetween(FileEntity file, LocalDateTime startDate, + LocalDateTime endDate, Pageable pageable); + + List findByFileEntity(FileEntity file); + + List findByFileEntityAndStatus(FileEntity file, String status); + + List findByFileEntityAndCreatedDateBetween(FileEntity file, LocalDateTime startDate, + LocalDateTime endDate); + + List findByFileEntityAndStatusAndCreatedDateBetween(FileEntity file, String status, + LocalDateTime startDate, LocalDateTime endDate); + + boolean existsByUrlHash(String urlHash); + + Optional findById(Long id); + + void deleteByFileEntity(FileEntity file); + + List findByFileEntityIn(List files); + + Page findByFileEntityIn(List files, Pageable pageable); + + Page findByFileEntityInAndStatus(List files, String status, Pageable pageable); + + List findByFileEntityInAndStatus(List files, String status); + + Page findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, + LocalDateTime endDate, Pageable pageable); + + List findByFileEntityInAndStatusAndCreatedDateBetween(List files, String status, + LocalDateTime startDate, LocalDateTime endDate); + + List findByFileEntityInAndCreatedDateBetween(List files, + LocalDateTime startDate, + LocalDateTime endDate); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.fileEntity.id IN :fileIds") + long countByFileEntityIdIn(@Param("fileIds") List fileIds); + + @Query("SELECT COUNT(v) FROM Violation v WHERE v.globalSearchResult.taskId = :taskId") + long countByGlobalSearchResultTaskId(@Param("taskId") String taskId); + + List findByFileEntityIn(List files, Sort sort); + + List findByFileEntityInAndStatus(List files, String status, Sort sort); + + List findByFileEntityInAndCreatedDateBetween(List files, LocalDateTime startDate, + LocalDateTime endDate, Sort sort); + + List findByFileEntityInAndStatusAndCreatedDateBetween(List files, String status, + LocalDateTime startDate, LocalDateTime endDate, + Sort sort); +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java new file mode 100644 index 0000000..442bcbe --- /dev/null +++ b/src/main/java/ru/soune/nocopy/scheduler/MonitoringScheduler.java @@ -0,0 +1,47 @@ +package ru.soune.nocopy.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.service.monitoring.MonitoringSearchService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class MonitoringScheduler { + + private final FileMonitoringRepository monitoringRepository; + + private final MonitoringSearchService monitoringSearchService; + + @Scheduled(cron = "0 1 2 * * *", zone = "Europe/Moscow") + @Transactional + public void processScheduledMonitoring() { + log.info("Starting scheduled monitoring check at 02:01 MSK"); + + LocalDateTime now = LocalDateTime.now(); + List readyForRun = monitoringRepository.findReadyForRun(now); + + log.info("Found {} files ready for monitoring", readyForRun.size()); + + //TODO check tokens for apply + for (FileMonitoringEntity monitoring : readyForRun) { + try { + monitoringSearchService.processFileSearch(monitoring); + log.info("Successfully queued search for file: {}", monitoring.getFile().getId()); + } catch (Exception e) { + log.error("Failed to process monitoring for file: {}", monitoring.getFile().getId(), e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + monitoringRepository.save(monitoring); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java b/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java new file mode 100644 index 0000000..112aacb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/scheduler/PaymentScheduler.java @@ -0,0 +1,70 @@ +package ru.soune.nocopy.scheduler; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.client.YooKassaClient; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.TariffInfoRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class PaymentScheduler { + + private final TariffInfoRepository tariffInfoRepository; + + private final EmailService emailService; + + private final YooKassaClient yooKassaClient; + + private final UserRepository userRepository; + + @Scheduled(cron = "0 0 1 * * *") + @Transactional + public void processAutoRenewals() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime renewalDate = now.plusDays(1); + List forRenewal = tariffInfoRepository.findForAutoRenewal(renewalDate); + + for (TariffInfo tariffInfo : forRenewal) { + try { + if (tariffInfo.getPaymentMethodId() == null) { + tariffInfo.setAutoRenewal(false); + tariffInfoRepository.save(tariffInfo); + continue; + } + + User user = userRepository.findByPersonalTariffInfo(tariffInfo); + boolean success = yooKassaClient.createAutoPayment( + tariffInfo.getPaymentMethodId(), + tariffInfo.getTariff().getPrice(), + "Auto-renewal " + tariffInfo.getTariff().getName(), + String.valueOf(user.getId())); + + Tariff tariff = tariffInfo.getTariff(); + if (success) { + tariffInfo.setStartTariff(now); + tariffInfo.setEndTariff(now.plusMonths(1)); + + tariffInfoRepository.save(tariffInfo); + + emailService.sendAutoRenewalSuccessEmail(user, tariff.getName(), tariff.getPrice(), renewalDate); + } else { + emailService.sendAutoRenewalFailedEmail(user, tariff.getName(), tariff.getPrice()); + } + } catch (Exception e) { + log.error("Error processing auto-renewal for tariffInfo: " + tariffInfo.getId(), e); + } + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java index b4be9e9..cb7334c 100644 --- a/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java +++ b/src/main/java/ru/soune/nocopy/service/FileSimilarityService.java @@ -10,14 +10,19 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import ru.soune.nocopy.dto.file.SimilarFileDTO; -import ru.soune.nocopy.dto.file.SimilarityFilter; +import ru.soune.nocopy.entity.company.Company; +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.repository.AuthTokenRepository; -import ru.soune.nocopy.repository.ImageHashRepository; -import ru.soune.nocopy.repository.ImageSimilarityRepository; -import ru.soune.nocopy.repository.SimilarImageProjection; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.DuplicateImageException; +import ru.soune.nocopy.repository.*; import ru.soune.nocopy.util.FileUtil; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.MessageDigest; import java.util.*; import java.util.stream.Collectors; @@ -34,48 +39,11 @@ public class FileSimilarityService { private final AuthTokenRepository authTokenRepository; + private final FileEntityRepository fileEntityRepository; + @Value("${server.baseurl}") private String baseUrl; - public List findSimilarFiles(String fileId) { - var imageHashEntity = hashRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("Hash not found")); - - Long hash64Hi = imageHashEntity.getHash64Hi(); - Long hash64Lo = imageHashEntity.getHash64Lo(); - - List candidates = repository.findCandidates(fileId); - - return candidates.stream() - .map(c -> { - Long cHi = c.getHash64Hi(); - Long cLo = c.getHash64Lo(); - - int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, cHi, cLo); - - String level; - if (hamming <= 5) { - level = "DUPLICATE"; - } else if (hamming <= 12) { - level = "SIMILAR"; - } else { - level = "DIFFERENT"; - } - - return SimilarFileDTO.builder() - .fileId(c.getId()) - .ownerId(c.getUserId()) - .originalFileName(c.getOriginalFileName()) - .fileSize(c.getFileSize()) - .hammingDistance(hamming) - .similarityLevel(level) - .build(); - }) - .sorted((a, b) -> - Integer.compare(a.getHammingDistance(), b.getHammingDistance())) - .toList(); - } - public List findDuplicateByHammingDistance(String fileId, int hammingDistance, int duplicate, int similar) { List candidates = repository.findCandidates(fileId); @@ -137,40 +105,114 @@ public class FileSimilarityService { return duplicates; } - public Page findSimilarFiles(String fileId, SimilarityFilter filter, Pageable pageable, String authToken) { - var imageHashEntity = hashRepository.findById(fileId) - .orElseThrow(() -> new RuntimeException("Hash not found")); + public void hasDuplicatesByHash(String path, String mimeType, Long userId) throws Exception { + FileEntity duplicateByHash = findDuplicateByHash(path, mimeType, userId); + if (duplicateByHash != null) { + throw new DuplicateImageException("Duplicate", duplicateByHash.getId(), + duplicateByHash.getUserId()); + } + } - Long hash64Hi = imageHashEntity.getHash64Hi(); - Long hash64Lo = imageHashEntity.getHash64Lo(); + public FileEntity findDuplicateByHash(String path, String mimeType, Long userId) throws Exception { + String hash = calculateFileHash(path); - List candidates = authToken.equals("all") ? repository.findCandidates(fileId): - repository.findCandidatesFromUserFiles(fileId, authTokenRepository.findUserIdByToken(authToken)); + User user = userRepository.findById(userId).orElseThrow(); + Company company = user.getCompany(); + List userIds = new ArrayList<>(); - List similarityLevels = (filter != null && filter.getSimilarityLevels() != null) - ? filter.getSimilarityLevels() - : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); + if (company != null) { + userIds.addAll(company.getUsers().stream().map(User::getId).toList()); + } else { + userIds.add(userId); + } - List allResults = candidates.stream() - .map(c -> createSimilarFileResponse(c, hash64Hi, hash64Lo)) - .filter(response -> similarityLevels.contains(response.getSimilarityLevel())) - .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) - .collect(Collectors.toList()); + List fileEntityList = fileEntityRepository.findByMimeTypeAndUserIds(mimeType, userIds); - int total = allResults.size(); - int page = (pageable != null) ? pageable.getPageNumber() : 0; - int size = (pageable != null) ? pageable.getPageSize() : 20; + for (FileEntity file : fileEntityList) { + if (file.getProtectedFilePath() == null) continue; + File existingFile = new File(file.getProtectedFilePath()); + File newFile = new File(path); - int fromIndex = Math.min(page * size, total); - int toIndex = Math.min(fromIndex + size, total); + if (existingFile.length() == newFile.length()) { + if (calculateFileHash(file.getProtectedFilePath()).equals(hash)) { + return file; + } + } + } - List pageContent = allResults.subList(fromIndex, toIndex); + return null; + } - return new PageImpl<>(pageContent, pageable, total); + private final UserRepository userRepository; + + public Page findSimilarFiles( + String fileId, + List similarityLevels, + Pageable pageable, + String authToken) { + + try { + var imageHashEntity = hashRepository.findById(fileId) + .orElseThrow(() -> new RuntimeException("Hash not found for file: " + fileId)); + + + List userIds; + List candidates = repository.findCandidates(fileId); + + Long userId = authTokenRepository.findUserIdByToken(authToken); + User user = userRepository.findById(userId).orElseThrow(() -> new RuntimeException("User not found")); + Company company = user.getCompany(); + + if (company != null) { + userIds = company.getUsers().stream().map(User::getId).collect(Collectors.toList()); + } else { + userIds = List.of(userId); + } + + List levels = (similarityLevels != null && !similarityLevels.isEmpty()) + ? similarityLevels + : List.of("DUPLICATE", "SIMILAR", "DIFFERENT"); + + List allResults = candidates.stream() + .map(c -> createSimilarFileResponse(c, + imageHashEntity.getHash64Hi(), + imageHashEntity.getHash64Lo(), userIds)) + .filter(dto -> levels.contains(dto.getSimilarityLevel())) + .filter(dto -> !(dto.getFileStatus().equals(FileStatus.MODERATION.name()) && !dto.getOwner())) + .sorted(Comparator.comparingInt(SimilarFileDTO::getHammingDistance)) + .collect(Collectors.toList()); + + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), allResults.size()); + List pageContent = start < allResults.size() ? + allResults.subList(start, end) : Collections.emptyList(); + + return new PageImpl<>(pageContent, pageable, allResults.size()); + + } catch (Exception e) { + log.error("Error finding similar files for fileId: {}", fileId, e); + return new PageImpl<>(Collections.emptyList(), pageable, 0); + } + } + + + private String calculateFileHash(String path) throws Exception { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + try (InputStream is = new FileInputStream(path)) { + byte[] buffer = new byte[8192]; + int read; + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + } + + byte[] hashBytes = digest.digest(); + return Base64.getEncoder().encodeToString(hashBytes); } private SimilarFileDTO createSimilarFileResponse(SimilarImageProjection similarImageProjection, - Long hash64Hi, Long hash64Lo) { + Long hash64Hi, Long hash64Lo, List userIds) { Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); int hamming = PerceptualHashHelper.INSTANCE.hammingDistance(new PHash(hash64Hi, hash64Lo), @@ -185,17 +227,40 @@ public class FileSimilarityService { level = "DIFFERENT"; } + boolean owner = userIds.contains(similarImageProjection.getUserId()); + + return SimilarFileDTO.builder() .fileId(similarImageProjection.getId()) .ownerId(similarImageProjection.getUserId()) - .originalFileName(similarImageProjection.getOriginalFileName()) + .originalFileName(similarImageProjection.getOriginalFileName().replace("." + + similarImageProjection.getExtension(), "") + "_nocopy_protected" + + "." + similarImageProjection.getExtension()) .fileSize(similarImageProjection.getFileSize()) .hammingDistance(hamming) .similarityLevel(level) .supportId(similarImageProjection.getSupportId()) .uploadDate(similarImageProjection.getUploadDate()) .status(similarImageProjection.getProtectionStatus()) - .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) + .owner(owner) +// .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId()) + .url(baseUrl + "/api/files/protected/" + similarImageProjection.getId() + "/thumbnail") + .fileStatus(similarImageProjection.getFileStatus()) + .build(); + } + + public SimilarFileDTO buildDTO(FileEntity fileEntity) { + return SimilarFileDTO.builder() + .fileId(fileEntity.getId()) + .ownerId(fileEntity.getUserId()) + .originalFileName(fileEntity.getOriginalFileName().replace("." + + fileEntity.getFileExtension(), "") + "_nocopy_protected" + + "." + fileEntity.getFileExtension()) + .fileSize(fileEntity.getFileSize()) + .uploadDate(fileEntity.getCreatedAt()) + .fileStatus(fileEntity.getStatus().name()) + .status(fileEntity.getProtectionStatus()) + .supportId(Long.valueOf(fileEntity.getSupportId())) .build(); } } diff --git a/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java b/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java new file mode 100644 index 0000000..6ea818e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/auth/CleanUpNotActiveUser.java @@ -0,0 +1,48 @@ +package ru.soune.nocopy.service.auth; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.user.UserNotActive; +import ru.soune.nocopy.repository.UserNotActiveRepository; +import ru.soune.nocopy.repository.UserRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CleanUpNotActiveUser { + + @Autowired + private UserNotActiveRepository userNotActiveRepository; + + @Autowired + private UserRepository userRepository; + + @Transactional + @Scheduled(fixedDelay = 600000) + public void cleanupNotActiveUsers() { + LocalDateTime now = LocalDateTime.now(); + + List expiredTokens = userNotActiveRepository.findByBlockedUntilBefore(now); + + for (UserNotActive userNotActive : expiredTokens) { + User user = userNotActive.getUser(); + user.setActive(true); + user.setFailedLoginAttempts(0); + + userRepository.save(user); + } + + if (!expiredTokens.isEmpty()) { + userNotActiveRepository.deleteAll(expiredTokens); + + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java new file mode 100644 index 0000000..3287a0b --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/complaint/ComplaintEntityService.java @@ -0,0 +1,129 @@ +package ru.soune.nocopy.service.complaint; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +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.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.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 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 getAllComplaints(Pageable pageable) { + return complaintRepository.findAll(pageable).map(this::mapToResponse); + } + + @Transactional(readOnly = true) + public List 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())); + + 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(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/cost/CostService.java b/src/main/java/ru/soune/nocopy/service/cost/CostService.java new file mode 100644 index 0000000..536aef9 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/cost/CostService.java @@ -0,0 +1,29 @@ +package ru.soune.nocopy.service.cost; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import ru.soune.nocopy.service.tariff.TariffConstants; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CostService { + + public int protectFileCost(String fileType) { + int cost = 0; + + switch (fileType) { + case "video": + return TariffConstants.VIDEO_TOKEN_VALUE; + case "image": + return TariffConstants.IMAGE_TOKEN_VALUE; + case "audio": + return TariffConstants.AUDIO_TOKEN_VALUE; + case "document": + return TariffConstants.PDF_TOKEN_VALUE; + default: + return cost; + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java b/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java index a1a6113..6d000af 100644 --- a/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java +++ b/src/main/java/ru/soune/nocopy/service/dadata/DaDataService.java @@ -22,7 +22,7 @@ public class DaDataService { private String apiKey ; @Value("${dadata.url}") - private String url ; + private String dadataUrl; private final OkHttpClient httpClient; @@ -44,7 +44,7 @@ public class DaDataService { String jsonBody = String.format("{\"query\": \"%s\"}", inn); Request request = new Request.Builder() - .url("https://suggestions.dadata.ru/suggestions/api/4_1/rs/findById/party") + .url(dadataUrl) .post(RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonBody)) .header("Accept", "application/json") .header("Authorization", "Token " + apiKey) @@ -52,6 +52,7 @@ public class DaDataService { try (Response response = httpClient.newCall(request).execute()) { ResponseBody body = response.body(); + if (body == null) { throw new IOException("Empty response body"); } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java index 3ed37c7..10b12a9 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileCleanupService.java @@ -38,7 +38,6 @@ public class FileCleanupService { public void cleanupExpiredFiles() { log.info("Starting cleanup of expired temporary files"); - cleanupOldPeriods(); cleanupTempFilesNightly(); Path tempDir = Paths.get(basePath, "temp"); @@ -91,19 +90,6 @@ public class FileCleanupService { } } - private void cleanupOldPeriods() { - LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); - - List checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo); - - for (ProtectedFileCheck check : checks) { - check.cleanupOldData(); - } - - protectedFileCheckRepository.saveAll(checks); - log.info("Cleaned up {} old protection usage records", checks.size()); - } - private void cleanupTempFilesNightly() { log.info("Starting nightly cleanup of temporary files"); diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index bf076a5..e801dac 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -2,19 +2,20 @@ package ru.soune.nocopy.service.file; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.FileResponse; import ru.soune.nocopy.entity.file.FileEntity; 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.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; -import ru.soune.nocopy.repository.SimilarImageProjection; +import ru.soune.nocopy.repository.FileMonitoringRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.ImageHashService; @@ -27,7 +28,7 @@ import java.nio.file.Paths; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; @Slf4j @@ -37,70 +38,14 @@ public class FileEntityService { private final FileEntityRepository fileEntityRepository; - private final ImageHashService imageHashService; - - private final FileSimilarityService fileSimilarityService; - private final UserRepository userRepository; + private final ImageResizeService imageResizeService; + private final CloudStorageService cloudStorageService; - @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 imageHash = Map.of(); - - if (!session.getFileType().startsWith("video")) { - imageHash = imageHashService.calculateHash(filePath); - List 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); - } - } + @Value("${server.baseurl}") + private String baseUrl; @Transactional(readOnly = true) public FileEntityResponse getById(String fileId, int version) { @@ -119,6 +64,34 @@ public class FileEntityService { return convertToResponse(fileEntity, version); } + @Transactional(readOnly = true) + public FileEntityResponse getByFilePath(String filePath, int version) { + FileEntity fileEntity = fileEntityRepository.findByFilePath(filePath) + .orElseThrow(() -> new FileEntityNotFoundException("Path: " + filePath)); + + return convertToResponse(fileEntity, version); + } + + @Transactional(readOnly = true) + public List getAllUserFiles(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); + List allFiles = new ArrayList<>(); + + if (user.getCompany() != null) { + for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() + .map(User::getId) + .toList()) { + allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED))); + } + } else { + allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)); + } + + return allFiles; + } + @Transactional(readOnly = true) public FileResponse getUserFiles(Long userId, int page, int pageSize, int version) { User user = userRepository.findById(userId).orElseThrow(); @@ -128,12 +101,13 @@ public class FileEntityService { for (Long uId : userRepository.findByCompanyId(user.getCompany().getId()).stream() .map(User::getId) .toList()) { - allFiles.addAll(fileEntityRepository.findByUserIdAndStatus(uId, FileStatus.ACTIVE)); + allFiles.addAll(fileEntityRepository.findByUserIdAndStatusIn(uId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED))); } } else { - allFiles = fileEntityRepository.findByUserIdAndStatus( - userId, FileStatus.ACTIVE); + allFiles = fileEntityRepository.findByUserIdAndStatusIn(userId, + List.of(FileStatus.ACTIVE, FileStatus.BLOCKED)); } int start = (page - 1) * pageSize; @@ -175,13 +149,13 @@ public class FileEntityService { fileEntity.setImageHash(null); } - fileEntity.setStatus(FileStatus.DELETED); + fileEntity.setStatus(FileStatus.REMOVED); fileEntity.setUpdatedAt(LocalDateTime.now()); - fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); +// fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); - Path path = Paths.get(fileEntity.getProtectedFilePath()); - Files.deleteIfExists(path); - fileEntity.setProtectedFilePath(""); +// Path path = Paths.get(fileEntity.getProtectedFilePath()); +// Files.deleteIfExists(path); +// fileEntity.setProtectedFilePath(""); fileEntityRepository.save(fileEntity); } @@ -196,7 +170,7 @@ public class FileEntityService { return totalSize != null ? totalSize : 0L; } - public void changeStatus(ProtectionStatus newStatus, String fileId) { + public void changeProtectionStatus(ProtectionStatus newStatus, String fileId) { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); fileEntity.setProtectionStatus(newStatus); @@ -204,6 +178,14 @@ public class FileEntityService { 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 public FileEntity writeProtectedFile(String id, byte[] data, String fileExt) throws IOException { FileEntity fileEntity = fileEntityRepository.findById(id) @@ -218,6 +200,10 @@ public class FileEntityService { Files.write(protectedFilePath, data); + if (imageResizeService.isImage(extension)) { + imageResizeService.generateSizes(fileEntity, data); + } + fileEntity.setProtectedFilePath(protectedFilePath.toString()); fileEntity.setProtectedAt(LocalDateTime.now()); fileEntity.setUpdatedAt(LocalDateTime.now()); @@ -308,14 +294,29 @@ public class FileEntityService { } } + private final FileMonitoringRepository fileMonitoringRepository; + private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); User user = userRepository.findById(fileEntity.getUserId()).get(); + String fileName = fileEntity.getOriginalFileName(); + String name = fileName; + Optional fileMonitoringRepositoryByFileId = + fileMonitoringRepository.findByFileId(fileEntity.getId()); + + String monitoring = fileMonitoringRepositoryByFileId.map(fileMonitoringEntity -> + fileMonitoringEntity.getMonitoringType().name()) + .orElseGet(MonitoringType.NONE::name); + + int lastDotIndex = fileName.lastIndexOf("."); + if (lastDotIndex > 0) { + name = fileName.substring(0, lastDotIndex); + } return FileEntityResponse.builder() .id(fileEntity.getId()) .userId(fileEntity.getUserId()) - .originalFileName(fileEntity.getOriginalFileName()) + .originalFileName(name + "." + fileEntity.getFileExtension()) .storedFileName(fileEntity.getStoredFileName()) .filePath(fileEntity.getProtectedFilePath()) .fileSize(fileEntity.getFileSize()) @@ -331,16 +332,16 @@ public class FileEntityService { .existsOnDisk(existsOnDisk) .supportId(fileEntity.getSupportId()) .protectStatus(fileEntity.getProtectionStatus().toString()) - .fileName(fileEntity.getOriginalFileName().replace("." + - fileEntity.getFileExtension(), "") + "_nocopy_protected" + - "." + fileEntity.getFileExtension()) + .fileName(name + "_nocopy_protected" + "." + fileEntity.getFileExtension()) .ownerName(user.getFullName()) .ownerEmail(user.getEmail()) .ownerCompany(user.getCompanyName()) + .monitoring(monitoring) //TODO fix after add logic for check for protect file .checksCount(0) .fileUploadDate(fileEntity.getCreatedAt()) .protectedFilePath(fileEntity.getProtectedFilePath()) + .thumbnailFileUrl(baseUrl + "/api/files/protected/" + fileEntity.getId() + "/thumbnail") .build(); } } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java b/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java deleted file mode 100644 index 5c4df44..0000000 --- a/src/main/java/ru/soune/nocopy/service/file/FileProcessingOrchestrator.java +++ /dev/null @@ -1,53 +0,0 @@ -package ru.soune.nocopy.service.file; - -import com.vrt.NoCopyFileService; -import com.vrt.fileprotection.FileProtector; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.stereotype.Service; -import ru.soune.nocopy.entity.file.FileEntity; -import ru.soune.nocopy.entity.file.ProtectionStatus; -import ru.soune.nocopy.repository.FileEntityRepository; -import ru.soune.nocopy.util.FileUtil; - -import java.util.List; - -@Slf4j -@Service -@RequiredArgsConstructor -public class FileProcessingOrchestrator { - - private final NoCopyFileService noCopyFileService; - - private final FileEntityRepository fileRepository; - - private final FileUtil fileUtil; - - public void initializeProcessingQueue() { - List filesToProtect = fileRepository.findByProtectionStatus(ProtectionStatus.NOT_PROTECTED); - - for (FileEntity fileEntity : filesToProtect) { - try { - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - - log.info("Add to query: {}", fileEntity.getOriginalFileName()); - } catch (Exception e) { - log.error("Fail add to query: {}", fileEntity.getId(), e); - } - } - } - - @Scheduled(fixedDelay = 120000) - public void checkNewFilesForProtection() { - List newFiles = fileRepository.findAllActiveFilesAndNotProtected(); - - for (FileEntity fileEntity : newFiles) { - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - } - } -} diff --git a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java index a7d0603..1a7a2fb 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileStorageService.java @@ -1,11 +1,10 @@ package ru.soune.nocopy.service.file; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.stereotype.Service; -import ru.soune.nocopy.configuration.file.FileStorageConfig; import java.io.FileNotFoundException; import java.io.IOException; @@ -16,11 +15,11 @@ import java.nio.file.Paths; @Slf4j public class FileStorageService { - @Autowired - private FileStorageConfig storageConfig; + @Value("${file.storage.base-path}") + private String basePath; public Resource loadFileAsResource(String filePath) throws IOException { - Path path = Paths.get(storageConfig.getBasePath()).resolve(filePath).normalize(); + Path path = Paths.get(basePath).resolve(filePath).normalize(); Resource resource = new UrlResource(path.toUri()); if (!resource.exists()) { diff --git a/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java b/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java index cab2d8e..d76c320 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileUploadService.java @@ -10,7 +10,7 @@ import java.io.IOException; public interface FileUploadService { FileUploadSession initUpload(Long userId, String fileName, - String fileType, String extension, long fileSize); + String fileType, String extension, String convertTo, long fileSize); void handleExpiredSession(FileUploadSession session); diff --git a/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java new file mode 100644 index 0000000..57d6216 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/ImageResizeService.java @@ -0,0 +1,92 @@ +package ru.soune.nocopy.service.file; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.file.FileEntity; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@Service +@Slf4j +public class ImageResizeService { + + private static final int THUMBNAIL_SIZE = 150; + private static final int MEDIUM_SIZE = 600; + + @Value("${file.storage.base-path}") + private String storagePath; + + public boolean isImage(String extension) { + return extension != null && List.of("jpg", "jpeg", "png", "webp") + .contains(extension.toLowerCase()); + } + + @Transactional + public void generateSizes(FileEntity file, byte[] data) throws IOException { + if (!isImage(file.getFileExtension())) { + return; + } + + try (ByteArrayInputStream bis = new ByteArrayInputStream(data)) { + BufferedImage original = ImageIO.read(bis); + + if (original == null) { + log.warn("Cannot read image: {}", file.getId()); + return; + } + + String baseName = file.getId().replace(".", "_"); + + if (original.getWidth() > THUMBNAIL_SIZE) { + String thumbPath = resizeAndSave(original, baseName + "_thumb", THUMBNAIL_SIZE); + file.setThumbnailPath(thumbPath); + } + + if (original.getWidth() > MEDIUM_SIZE) { + String mediumPath = resizeAndSave(original, baseName + "_medium", MEDIUM_SIZE); + file.setMediumPath(mediumPath); + } + } + } + + private String resizeAndSave(BufferedImage original, String fileName, int targetWidth) throws IOException { + int targetHeight = (int) ((double) original.getHeight() / original.getWidth() * targetWidth); + + BufferedImage resized = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D g = resized.createGraphics(); + g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g.drawImage(original, 0, 0, targetWidth, targetHeight, null); + g.dispose(); + + Path dirPath = Paths.get(storagePath, "resized"); + Files.createDirectories(dirPath); + + Path filePath = dirPath.resolve(fileName + ".jpg"); + ImageIO.write(resized, "jpg", filePath.toFile()); + + return filePath.toString(); + } + + public String getPath(FileEntity file, String size) { + + if ("thumbnail".equals(size) && file.getThumbnailPath() != null) { + return file.getThumbnailPath(); + } + + if ("medium".equals(size) && file.getMediumPath() != null) { + return file.getMediumPath(); + } + + return file.getProtectedFilePath(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java index 6ac96bb..7d2e137 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/AudioLocalSearchImpl.java @@ -26,7 +26,9 @@ public class AudioLocalSearchImpl implements AudioLocalSearch { public @Nullable FileProtector.FileInfo findBySignature(@NotNull String signature) { FileEntity fileEntity = fileEntityService.findBySignature(signature); - return fileUtil.createFileInfo(fileEntity); + if (fileEntity == null) return null; + + return fileUtil.createFileInfo(fileEntity, null); } @Override diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java index dea41ef..b7151d1 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/DocumentLocalSearchImpl.java @@ -23,7 +23,7 @@ public class DocumentLocalSearchImpl implements DocumentLocalSearch { @Override public @Nullable FileProtector.FileInfo findByIds(@NotNull String ownerId, @NotNull String fileId) { FileEntity fileEntity = fileEntityRepository.findByIdAndUserId(fileId, Long.valueOf(ownerId)); - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, null); log.info("fileInfo: {}", fileInfo); return fileInfo; diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java index c1e3948..bcf88b0 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/FileUploadServiceImpl.java @@ -2,27 +2,29 @@ package ru.soune.nocopy.service.file.impl; import com.vrt.NoCopyFileService; import com.vrt.fileprotection.FileProtector; +import com.vrt.fileprotection.NoCopyCheckResult; +import com.vrt.fileprotection.audio.AudioCheckResult; +import com.vrt.fileprotection.documents.DocumentCheckResult; import jakarta.annotation.PostConstruct; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.entity.file.*; -import ru.soune.nocopy.exception.ChunkSizeExceededException; -import ru.soune.nocopy.exception.DuplicateImageException; -import ru.soune.nocopy.exception.FileUploadException; -import ru.soune.nocopy.exception.UploadSessionNotFoundException; +import ru.soune.nocopy.exception.*; import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository; import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.ImageHashService; +import ru.soune.nocopy.service.cost.CostService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; @@ -59,9 +61,6 @@ public class FileUploadServiceImpl implements FileUploadService { @Value("${file.storage.max-retry-attempts:3}") private int maxRetryAttempts; - @Value("${file.storage.chunk-timeout-ms:300000}") // 5 минут - private long chunkTimeoutMs; - @Value("${file.storage.session-expiry-hours:24}") private int sessionExpiryHours; @@ -79,6 +78,9 @@ public class FileUploadServiceImpl implements FileUploadService { @Autowired private FileEntityService fileEntityService; + private final CostService costService; + + @Lazy private final NoCopyFileService noCopyFileService; private final FileUtil fileUtil; @@ -112,7 +114,7 @@ public class FileUploadServiceImpl implements FileUploadService { @Override @Transactional public FileUploadSession initUpload(Long userId, String fileName, - String fileType, String extension, long fileSize) { + String fileType, String extension, String convertTo, long fileSize) { log.info("Initializing upload for user {}: {} ({} bytes, type: {})", userId, fileName, fileSize, fileType); @@ -143,6 +145,7 @@ public class FileUploadServiceImpl implements FileUploadService { .status(UploadStatus.INITIATED) .expiresAt(LocalDateTime.now().plusHours(sessionExpiryHours)) .retryCount(0) + .convertTo(convertTo) .build(); FileUploadSession savedSession = sessionRepository.save(session); @@ -252,6 +255,12 @@ public class FileUploadServiceImpl implements FileUploadService { cleanupSessionFiles(session); + if (status != FileStatus.TEMP) { + FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity, session.getConvertTo()); + + noCopyFileService.addFile(fileInfo); + } + log.info("File processing completed for session: {}", session.getUploadId()); } catch (Exception e) { log.error("Failed to complete file processing for session {}: {}", @@ -313,21 +322,58 @@ public class FileUploadServiceImpl implements FileUploadService { chunkNumber, session.getChunksUploaded(), session.getTotalChunks()); boolean isLastChunk = session.getChunksUploaded().equals(session.getTotalChunks()); - + //TODO CHECK if (isLastChunk) { - log.info("All chunks uploaded for session {}. Starting assembly...", - session.getUploadId()); - String finalFilePath = assembleFileSynchronously(session); - if (session.getFileType().startsWith("image") && findSimilar == 0) { - checkForDuplicatesSynchronously(finalFilePath); + if (findSimilar == 0) { + if (session.getFileType().startsWith("image")) { + checkForDuplicatesSynchronously(finalFilePath); + } else if (session.getFileType().startsWith("audio") || session.getFileType().startsWith("document")) { + fileSimilarityService.hasDuplicatesByHash(finalFilePath ,session.getFileType(), + session.getUserId()); + + File file = new File(finalFilePath); + + if (session.getFileType().startsWith("audio") && !isWavFile(file)) { + session.setStatus(UploadStatus.FAILED); + session.setLastError("wav file not have RIFF header"); + + sessionRepository.save(session); + + throw new FileFormatException("Failed to upload chunk: .wav file not have RIFF header or " + + "another problems with .wav file"); + } + + FileProtector.Type type = "document".equals(session.getFileType()) ? + FileProtector.Type.DOC : FileProtector.Type.valueOf(session.getFileType().toUpperCase()); + + NoCopyCheckResult checkResult = noCopyFileService.checkFile(file, type); + + switch (checkResult) { + case DocumentCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + throw new DuplicateImageException("Duplicate", info.getId(), + Long.valueOf(info.getOwnerId())); + } + case AudioCheckResult.Success success -> { + FileProtector.FileInfo info = success.getInfo(); + throw new DuplicateImageException("Duplicate", info.getId(), + Long.valueOf(info.getOwnerId())); + } + case DocumentCheckResult.Failed failed -> + log.warn("Document check failed: {}", failed.getMessage()); + case AudioCheckResult.Failed failed -> + log.warn("Audio check failed: {}", failed.getMessage()); + default -> log.warn("Unexpected result type: {}", checkResult.getClass().getSimpleName()); + } + } } session.setStatus(UploadStatus.COMPLETED); session.setFilePath(finalFilePath); - FileStatus status = findSimilar == 0 ? FileStatus.ACTIVE: FileStatus.TEMP; + FileStatus status = findSimilar == 0 ? FileStatus.MODERATION: FileStatus.TEMP; if (status == FileStatus.TEMP) { fileEntityService.clearTempFiles(session.getUserId()); @@ -351,7 +397,7 @@ public class FileUploadServiceImpl implements FileUploadService { if (status != FileStatus.TEMP) { String fileType = session.getFileType(); - tariffInfoService.writeOffTokens(session.getUserId(), protectCost(fileType)); + tariffInfoService.writeOffTokens(session.getUserId(), costService.protectFileCost(fileType)); } } else { sessionRepository.save(session); @@ -388,21 +434,87 @@ public class FileUploadServiceImpl implements FileUploadService { } } - private int protectCost(String fileType) { - int cost = 0; - - switch (fileType) { - case "video": - return TariffConstants.VIDEO_TOKEN_VALUE; - case "image": - return TariffConstants.IMAGE_TOKEN_VALUE; - case "audio": - return TariffConstants.AUDIO_TOKEN_VALUE; - case "document": - return TariffConstants.PDF_TOKEN_VALUE; + //TODO убрать когда научимся защищать другие типы + public boolean isWavFile(File file) { + if (file == null || !file.exists() || file.length() < 12) { + return false; } - return cost; + try (FileInputStream fis = new FileInputStream(file)) { + byte[] header = new byte[4]; + int bytesRead = fis.read(header); + + if (bytesRead < 4) { + return false; + } + + return header[0] == 82 && header[1] == 73 && + header[2] == 70 && header[3] == 70; + + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + public static boolean isValidWav(File file) { + if (file == null || !file.exists() || file.length() < 44) { + return false; + } + + try (FileInputStream fis = new FileInputStream(file)) { + byte[] header = new byte[44]; + if (fis.read(header) < 44) { + return false; + } + + if (header[0] != 'R' || header[1] != 'I' || + header[2] != 'F' || header[3] != 'F') { + return false; + } + + if (header[8] != 'W' || header[9] != 'A' || + header[10] != 'V' || header[11] != 'E') { + return false; + } + + if (header[12] != 'f' || header[13] != 'm' || + header[14] != 't' || header[15] != ' ') { + return false; + } + + if (header[36] != 'd' || header[37] != 'a' || + header[38] != 't' || header[39] != 'a') { + return false; + } + + int audioFormat = (header[20] & 0xFF) | (header[21] & 0xFF) << 8; + if (audioFormat != 1 && audioFormat != 3) { + return false; + } + + int numChannels = (header[22] & 0xFF) | (header[23] & 0xFF) << 8; + if (numChannels < 1 || numChannels > 8) { + return false; + } + + int sampleRate = (header[24] & 0xFF) | (header[25] & 0xFF) << 8 | + (header[26] & 0xFF) << 16 | (header[27] & 0xFF) << 24; + if (sampleRate < 8000 || sampleRate > 192000) { + return false; + } + + int bitsPerSample = (header[34] & 0xFF) | (header[35] & 0xFF) << 8; + if (bitsPerSample < 8 || bitsPerSample > 32) { + return false; + } + + return true; + + } catch (IOException e) { + e.printStackTrace(); + return false; + } } private String assembleFileSynchronously(FileUploadSession session) throws IOException { @@ -510,76 +622,6 @@ public class FileUploadServiceImpl implements FileUploadService { } } - private void assembleFile(FileUploadSession session) throws IOException { - log.info("Starting file assembly for session: {} ({})", - session.getUploadId(), session.getFileName()); - - Path finalFilePath = null; - String checksum; - - try { - finalFilePath = prepareFinalFile(session); - log.info("Final file path: {}", finalFilePath); - - validateAllChunksExist(session); - - mergeChunksToFile(session, finalFilePath); - - validateFinalFile(session, finalFilePath); - - checksum = calculateChecksum(finalFilePath); - log.debug("File checksum calculated: {}", checksum); - - session.setFilePath(finalFilePath.toString()); - session.setChecksum(checksum); - session.setStatus(UploadStatus.COMPLETED); - session.setCompletedAt(LocalDateTime.now()); - - sessionRepository.save(session); - log.info("Upload session updated to COMPLETED: {}", session.getUploadId()); - - try { - FileEntity fileEntity = fileEntityService.createFromUploadSession(session, checksum); - - FileProtector.FileInfo fileInfo = fileUtil.createFileInfo(fileEntity); - - noCopyFileService.addFile(fileInfo); - - log.info("FileEntity successfully created for session: {}", - session.getUploadId()); - } catch (DuplicateImageException e) { - throw new DuplicateImageException("Duplicate", e.duplicateFileId(), e.userId()); - } catch (Exception e) { - log.error("Failed to create FileEntity for session {} (file uploaded, but metadata not saved): {}", - session.getUploadId(), e.getMessage()); - } - - cleanupSessionFiles(session); - - log.info("File assembly completed successfully: {} -> {} ({} bytes)", - session.getFileName(), finalFilePath, session.getFileSize()); - - } catch (Exception e) { - log.error("File assembly failed for session {}: {}", - session.getUploadId(), e.getMessage(), e); - - if (finalFilePath != null) { - try { - Files.deleteIfExists(finalFilePath); - log.debug("Cleaned up partial file: {}", finalFilePath); - } catch (IOException ioException) { - log.warn("Failed to cleanup partial file: {}", finalFilePath, ioException); - } - } - - session.setStatus(UploadStatus.FAILED); - session.setLastError(e.getMessage()); - sessionRepository.save(session); - - throw new IOException("File assembly failed: " + e.getMessage(), e); - } - } - private Path prepareFinalFile(FileUploadSession session) throws IOException { Path userUploadsDir = storageRoot.resolve("uploads") .resolve(String.valueOf(session.getUserId())) diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java index 79b8963..ea65c88 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ImageLocalSearchImpl.java @@ -44,11 +44,13 @@ public class ImageLocalSearchImpl implements ImageLocalSearch { @Override public @Nullable FileProtector.FileInfo findByPHash(@NotNull PHash pHash) { ImageHashEntity imageHashEntity = - imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(), - pHash.getSecondPart()); + imageHashRepository.findByHash64HiAndHash64Lo(pHash.getFirstPart(), pHash.getSecondPart()); + + if (imageHashEntity == null) return null; + FileEntity file = fileEntityRepository.findByFileId(imageHashEntity.getFileId()); return new FileProtector.FileInfo(FileProtector.Type.IMAGE, imageHashEntity.getFileId(), - String.valueOf(file.getUserId())); + String.valueOf(file.getUserId()), null); } } diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java index 257cf65..0a7e9cf 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/NoCopyProcessingListenerImpl.java @@ -16,21 +16,21 @@ public class NoCopyProcessingListenerImpl implements FileProtector.ProcessingLis @Override public void onStartProcessing(FileProtector.FileInfo fileInfo) { - fileEntityService.changeStatus(ProtectionStatus.PROCESSING, fileInfo.getId()); + fileEntityService.changeProtectionStatus(ProtectionStatus.PROCESSING, fileInfo.getId()); } @Override public void onProcessingFailed(FileProtector.FileInfo fileInfo) { - fileEntityService.changeStatus(ProtectionStatus.FAILED, fileInfo.getId()); + fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED, fileInfo.getId()); } @Override public void onSavingFailed(FileProtector.FileInfo fileInfo) { - fileEntityService.changeStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId()); + fileEntityService.changeProtectionStatus(ProtectionStatus.FAILED_SAVE, fileInfo.getId()); } @Override public void onFinish(FileProtector.FileInfo fileInfo) { - fileEntityService.changeStatus(ProtectionStatus.PROTECTED, fileInfo.getId());; + fileEntityService.changeProtectionStatus(ProtectionStatus.PROTECTED, fileInfo.getId());; } } diff --git a/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java b/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java index 382301d..3da08f1 100644 --- a/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/impl/ProtectionFileProviderImpl.java @@ -9,7 +9,11 @@ import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Component; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.repository.FileEntityRepository; +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.FileUploadService; import ru.soune.nocopy.service.file.cloud.CloudStorageService; import java.io.File; @@ -27,6 +31,10 @@ public class ProtectionFileProviderImpl implements FileProtector.FileProvider { private final FileEntityService fileEntityService; + private final FileUploadSessionRepository fileUploadSessionRepository; + + private final FileEntityRepository fileEntityRepository; + private final CloudStorageService cloudStorageService; private final FileEntityRepository fileEntityRepository; diff --git a/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java new file mode 100644 index 0000000..4e8b01f --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/file/moderation/ModerationService.java @@ -0,0 +1,238 @@ +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.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 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 static final List 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); + } + } + } + + /** + * Подача апелляции пользователем + */ + @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 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 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(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index b610f3e..fabb47d 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -19,6 +19,9 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.SecureRandom; import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; @Service @RequiredArgsConstructor @@ -38,22 +41,78 @@ public class EmailService { @Value("${app.email.verification.user}") private String user; - public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { + @Value("${app.email.verification.drop-password-subject}") + private String dropPasswordSubject; + + @Value("${server.baseurl}") + private String baseUrl; + + public void sendEmail(String from, String sendTo, String subject, String body) throws MessagingException { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setFrom(standartAdressFrom); - helper.setTo(user.getEmail()); - helper.setSubject(standartSubject); - - String htmlContent = loadAndProcessTemplate(user.getFullName(), code); - helper.setText(htmlContent, true); + helper.setFrom(from); + helper.setTo(sendTo); + helper.setSubject(subject); + helper.setText(body, true); mailSender.send(message); } + public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { + String htmlContent = loadAndProcessTemplate(Map.of("username", user.getFullName(), "code", code), + "templates/email-verification.html"); + sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); + } + + public void sendTokensNotFoundEmail(User user, Integer currentTokens, Integer missingTokens) + throws MessagingException, IOException { + String htmlContent = loadAndProcessTemplate(Map.of("username", user.getFullName(), + "current_tokens", String.valueOf(currentTokens), "missing_tokens", + String.valueOf(missingTokens), "link_to_token_board", baseUrl + "/pages/payment"), + "templates/token-not-found.html"); + + sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); + } + + public void sendAutoRenewalFailedEmail(User user, String tariffName, Double price) + throws MessagingException, IOException { + + Map templateData = new HashMap<>(); + templateData.put("username", user.getFullName()); + templateData.put("tariff_name", tariffName); + templateData.put("price", String.format("%.2f", price)); + templateData.put("support_email", "support@no-copy.ru"); + + String htmlContent = loadAndProcessTemplate(templateData, "templates/failed-template.html"); + + sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); + } + + public void sendAutoRenewalSuccessEmail(User user, String tariffName, Double price, LocalDateTime nextPaymentDate) + throws MessagingException, IOException { + + Map templateData = new HashMap<>(); + templateData.put("username", user.getFullName()); + templateData.put("tariff_name", tariffName); + templateData.put("price", String.format("%.2f", price)); + templateData.put("renewal_date", LocalDateTime.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))); + templateData.put("next_payment_date", nextPaymentDate.format(DateTimeFormatter.ofPattern("dd.MM.yyyy"))); + + String htmlContent = loadAndProcessTemplate(templateData, "templates/auto-renewal-success.html"); + + sendEmail("noreply@no-copy.ru", user.getEmail(), standartAdressFrom, htmlContent); + } + @Transactional public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) { + EmailVerificationToken existingToken = emailVerificationTokenRepository + .findByUserId(authToken.getUser().getId()); + + if (existingToken != null) { + emailVerificationTokenRepository.deleteById(existingToken.getId()); + } + EmailVerificationToken emailToken = new EmailVerificationToken(); emailToken.setUser(authToken.getUser()); emailToken.setToken(generateVerificationCode()); @@ -69,8 +128,8 @@ public class EmailService { return String.valueOf(code); } - private String loadAndProcessTemplate(String username, String code) throws IOException { - ClassPathResource resource = new ClassPathResource("templates/email-verification.html"); + private String loadAndProcessTemplate(String username, String code, String templatePath) throws IOException { + ClassPathResource resource = new ClassPathResource(templatePath); String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); String safeUsername = escapeHtml(username != null ? username : user); String safeCode = escapeHtml(code); @@ -80,6 +139,18 @@ public class EmailService { .replace("{{code}}", safeCode); } + private String loadAndProcessTemplate(Map fieldsForTemplate, String templatePath) throws IOException { + ClassPathResource resource = new ClassPathResource(templatePath); + String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + + for (Map.Entry entry : fieldsForTemplate.entrySet()) { + template = template.replace("{{" + entry.getKey() + "}}", escapeHtml(entry.getValue())); + } + + return template; + } + + private String escapeHtml(String text) { if (text == null) return ""; return text @@ -89,4 +160,29 @@ public class EmailService { .replace("\"", """) .replace("'", "'"); } + + public void sendResetPasswordEmail(User user, String code) { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom(standartAdressFrom); + helper.setTo(user.getEmail()); + helper.setSubject(dropPasswordSubject); + + String htmlContent = loadAndProcessTemplate(user.getFullName(), code, + "templates/reset-password.html"); + //TODO frontPort убрать после настройки редиректа nginx + String resetLink = escapeHtml(baseUrl + "/reset-password?email=" + user.getEmail()); + htmlContent = htmlContent.replace("{{resetLink}}", resetLink); + + helper.setText(htmlContent, true); + + mailSender.send(message); + + log.info("Email sent to: {}", user.getEmail()); + } catch (Exception e) { + log.error("Failed to send email: {}", e.getMessage()); + } + } } diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java new file mode 100644 index 0000000..a098f03 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/FileMonitoringService.java @@ -0,0 +1,117 @@ +package ru.soune.nocopy.service.monitoring; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.monitoring.MonitoringStatusResponse; +import ru.soune.nocopy.dto.tarriff.TariffDTO; +import ru.soune.nocopy.entity.file.*; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.tarif.TariffType; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.UserNotHavePermission; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.tariff.TariffService; + +import java.io.FileNotFoundException; +import java.time.LocalDateTime; +import java.time.temporal.TemporalAdjusters; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FileMonitoringService { + private final FileMonitoringRepository monitoringRepository; + + private final FileEntityRepository fileRepository; + + private final TariffService tariffService; + + private final UserRepository userRepository; + + @Transactional + public FileMonitoringEntity setMonitoring(Long userId, String fileId, MonitoringType type) throws FileNotFoundException { + FileEntity file = fileRepository.findById(fileId) + .orElseThrow(() -> new FileNotFoundException(fileId)); + + if (!file.getUserId().equals(userId)) { + throw new UserNotHavePermission("User not Have permission for file"); + } + + FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId) + .orElse(FileMonitoringEntity.builder() + .file(file) + .userId(userId) + .build()); + + monitoring.setMonitoringType(type); + monitoring.setActive(type != MonitoringType.NONE); + monitoring.setNextRun(calculateNextRun(type, LocalDateTime.now())); + monitoring.setUpdatedAt(LocalDateTime.now()); + + if (monitoring.getCreatedAt() == null) { + monitoring.setCreatedAt(LocalDateTime.now()); + } + + return monitoringRepository.save(monitoring); + } + + @Transactional + public void updateNextRun(FileMonitoringEntity monitoring) { + if (monitoring.isActive() && monitoring.getMonitoringType() != MonitoringType.NONE) { + LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType(), LocalDateTime.now()); + monitoring.setNextRun(nextRun); + monitoringRepository.save(monitoring); + } + } + + + private LocalDateTime calculateNextRun(MonitoringType type, LocalDateTime from) { + LocalDateTime nextRun = from.withHour(2).withMinute(0).withSecond(0).withNano(0); + + if (from.isAfter(nextRun)) { + nextRun = nextRun.plusDays(1); + } + + return switch (type) { + case MONITORING_DAILY -> nextRun; + case MONITORING_WEEKLY -> nextRun.with(TemporalAdjusters.nextOrSame(java.time.DayOfWeek.MONDAY)); + case MONITORING_MONTHLY -> nextRun.with(TemporalAdjusters.firstDayOfNextMonth()); + case NONE -> null; + }; + } + + public MonitoringStatusResponse getMonitoringStatus(String fileId, Long userId) { + FileMonitoringEntity monitoring = monitoringRepository.findByFileId(fileId).orElse(null); + FileEntity file = fileRepository.findById(fileId).orElseThrow(); + User user = userRepository.findById(userId).orElseThrow(); + + return MonitoringStatusResponse.builder() + .fileId(fileId) + .fileName(file.getOriginalFileName()) + .monitoringType(monitoring != null ? monitoring.getMonitoringType() : MonitoringType.NONE) + .nextRun(monitoring != null ? monitoring.getNextRun() : null) + .lastRun(monitoring != null ? monitoring.getLastRun() : null) + .lastRunStatus(monitoring != null ? monitoring.getLastRunStatus() : null) + .isActive(monitoring != null && monitoring.isActive()) + .needAddTokens(checkTokensForMonitoring(user, monitoring.getMonitoringType())) + .build(); + } + + public boolean checkTokensForMonitoring(User user, MonitoringType monitoringType) { + if (monitoringType == MonitoringType.NONE) { + return false; + } + + TariffDTO tariff = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + int currentUserCountTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); + + return currentUserCountTokens > tariff.getTokens(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java new file mode 100644 index 0000000..bcfdabb --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/monitoring/MonitoringSearchService.java @@ -0,0 +1,169 @@ +package ru.soune.nocopy.service.monitoring; + +import jakarta.mail.MessagingException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.configuration.search.SearchProperties; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.dto.tarriff.TariffDTO; +import ru.soune.nocopy.entity.monitoring.FileMonitoringEntity; +import ru.soune.nocopy.entity.monitoring.MonitoringType; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.tarif.TariffType; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.search.SearchImageService; +import ru.soune.nocopy.service.tariff.TariffInfoService; +import ru.soune.nocopy.service.tariff.TariffService; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MonitoringSearchService { + private final SearchImageService searchImageService; + + private final FileMonitoringRepository monitoringRepository; + + private final TariffInfoService tariffInfoService; + + private final TariffService tariffService; + + private final EmailService emailService; + + private final UserRepository userRepository; + + private final ViolationService violationService; + + private final SearchProperties searchProperties; + + @Transactional(noRollbackFor = TariffNotFoundException.class) + public void processFileSearch(FileMonitoringEntity monitoring) throws MessagingException, IOException { + monitoring.setLastRun(LocalDateTime.now()); + monitoring.setLastRunStatus("IN_PROGRESS"); + monitoringRepository.save(monitoring); + + MonitoringType monitoringType = monitoring.getMonitoringType(); + TariffDTO tariffMonitoring = tariffService.getTariffByType(TariffType.valueOf(monitoringType.name())); + + try { + tariffInfoService.writeOffTokens(monitoring.getUserId(), tariffMonitoring.getTokens()); + + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); + + log.info("Monitoring search settings: useYandex={}, useGoogle={}", useYandex, useGoogle); + + List allResults = new ArrayList<>(); + + if (useYandex) { + try { + String yandexResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "yandex_reverse_image", "visual_matches"); + + List yandexImages = + searchImageService.getAllImagesWithoutPagination(yandexResponse, "visual_matches"); + + allResults.addAll(yandexImages); + log.info("Yandex search found {} results", yandexImages.size()); + + } catch (TimeoutException e) { + log.warn("Yandex search timeout for monitoring file {}", monitoring.getFile().getId()); + } catch (IOException e) { + log.error("Yandex search failed for monitoring file {}", monitoring.getFile().getId(), e); + } + } else { + log.info("Yandex search is disabled by settings"); + } + + if (useGoogle) { + try { + String googleResponse = searchImageService.searchReverseByPublicUrl( + monitoring.getFile(), "google_lens", "exact_matches"); + + List googleImages = + searchImageService.getAllImagesWithoutPagination(googleResponse, "exact_matches"); + + allResults.addAll(googleImages); + log.info("Google search found {} results", googleImages.size()); + + } catch (TimeoutException e) { + log.warn("Google search timeout for monitoring file {}", monitoring.getFile().getId()); + } catch (IOException e) { + log.error("Google search failed for monitoring file {}", monitoring.getFile().getId(), e); + } + } else { + log.info("Google search is disabled by settings"); + } + + List uniqueResults = allResults; + if (useYandex && useGoogle) { + uniqueResults = searchImageService.removeDuplicateUrls( + useYandex ? allResults : new ArrayList<>(), + useGoogle ? allResults : new ArrayList<>() + ); + log.info("After deduplication: {} unique results", uniqueResults.size()); + } + + for (YandexSearchResponse.ImageResult imageResult : uniqueResults) { + violationService.processViolation(imageResult, monitoring.getFile(), null); + } + + monitoring.setLastRunStatus("SUCCESS"); + + if (uniqueResults.isEmpty()) { + log.info("No results found for monitoring file {}", monitoring.getFile().getId()); + } + + } catch (TariffNotFoundException e) { + User user = userRepository.findById(monitoring.getUserId()).orElseThrow(); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + int currentTokens = activeTariffInfo.getTokens() + activeTariffInfo.getBoughtTokens(); + + emailService.sendTokensNotFoundEmail(user, currentTokens, tariffMonitoring.getTokens()); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + updateNextRun(monitoring); + monitoringRepository.save(monitoring); + + } catch (Exception e) { + log.error("Error processing monitoring search", e); + monitoring.setLastRunStatus("ERROR: " + e.getMessage()); + updateNextRun(monitoring); + monitoringRepository.save(monitoring); + } + } + + private void updateNextRun(FileMonitoringEntity monitoring) { + LocalDateTime nextRun = calculateNextRun(monitoring.getMonitoringType()); + monitoring.setNextRun(nextRun); + } + + private LocalDateTime calculateNextRun(MonitoringType type) { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime nextRun = now.withHour(2).withMinute(0).withSecond(0).withNano(0); + + if (now.isAfter(nextRun)) { + nextRun = nextRun.plusDays(1); + } + + return switch (type) { + case MONITORING_DAILY -> nextRun; + case MONITORING_WEEKLY -> nextRun.plusWeeks(1); + case MONITORING_MONTHLY -> nextRun.plusMonths(1); + default -> null; + }; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java new file mode 100644 index 0000000..5bb807a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/notification/NotificationService.java @@ -0,0 +1,12 @@ +package ru.soune.nocopy.service.notification; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class NotificationService { + +} diff --git a/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java new file mode 100644 index 0000000..96c9f74 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payment/PaymentService.java @@ -0,0 +1,225 @@ +package ru.soune.nocopy.service.payment; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.ReferralService; +import ru.soune.nocopy.client.YooKassaClient; +import ru.soune.nocopy.dto.payment.PaymentMethodDTO; +import ru.soune.nocopy.entity.payment.Payment; +import ru.soune.nocopy.entity.payment.PaymentMethod; +import ru.soune.nocopy.entity.payment.PaymentOperationType; +import ru.soune.nocopy.entity.payment.PaymentStatus; +import ru.soune.nocopy.entity.tarif.Tariff; +import ru.soune.nocopy.entity.tarif.TariffInfo; +import ru.soune.nocopy.entity.tarif.TariffStatus; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.PaymentNotFoundException; +import ru.soune.nocopy.exception.TariffNotFoundException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.repository.*; +import ru.soune.nocopy.service.tariff.TariffInfoService; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +@Slf4j +public class PaymentService { + private final PaymentRepository paymentRepository; + + private final UserRepository userRepository; + + private final TariffRepository tariffRepository; + + private final TariffInfoRepository tariffInfoRepository; + + private final TariffInfoService tariffInfoService; + + private final ReferralService referralService; + + private final PaymentMethodRepository paymentMethodRepository; + + private final YooKassaClient yooKassaClient; + + @Transactional + public Payment createPayment(String email, Long tariffId, String operationType, String paymentUuid) { + User user = userRepository.findByEmail(email); + + if (user == null) { + throw new UserNotFoundException(email); + } + + Tariff tariff = tariffRepository.findById(tariffId).orElseThrow(() -> + new TariffNotFoundException("Tariff not found")); + + Payment payment = new Payment(); + payment.setStatus(PaymentStatus.PENDING); + payment.setAmount(tariff.getPrice()); + payment.setOperationType(PaymentOperationType.valueOf(operationType)); + payment.setUser(user); + payment.setTariff(tariff); + payment.setPaymentUuid(paymentUuid); + + return paymentRepository.save(payment); + } + + private void savePaymentMethod(User user, Map paymentMethodData) { + if (paymentMethodData == null) return; + + String paymentMethodId = (String) paymentMethodData.get("id"); + if (paymentMethodRepository.findByPaymentMethodId(paymentMethodId).isPresent()) return; + + Map card = (Map) paymentMethodData.get("card"); + if (card == null) return; + + PaymentMethod pm = new PaymentMethod(); + pm.setUser(user); + pm.setPaymentMethodId(paymentMethodId); + pm.setLastFour((String) card.get("last4")); + pm.setCardType((String) card.get("card_type")); + pm.setExpiryMonth((String) card.get("expiry_month")); + pm.setExpiryYear((String) card.get("expiry_year")); + pm.setCreatedAt(LocalDateTime.now()); + + paymentMethodRepository.save(pm); + } + + @Transactional(readOnly = true) + public List getUserPaymentMethods(Long userId) { + User user = userRepository.findById(userId).orElseThrow(); + + return paymentMethodRepository.findByUserAndActiveTrue(user) + .stream() + .map(pm -> new PaymentMethodDTO( + pm.getPaymentMethodId(), + pm.getLastFour(), + pm.getCardType(), + pm.getExpiryMonth(), + pm.getExpiryYear() + )) + .collect(Collectors.toList()); + } + + @Transactional + public void deletePaymentMethod(Long userId, String paymentMethodId) { + User user = userRepository.findById(userId).orElseThrow(); + PaymentMethod pm = paymentMethodRepository.findByPaymentMethodId(paymentMethodId) + .orElseThrow(() -> new RuntimeException("Payment method not found")); + + if (!pm.getUser().getId().equals(user.getId())) { + throw new RuntimeException("Access denied"); + } + + pm.setActive(false); + paymentMethodRepository.save(pm); + } + + @Transactional + public void changeAutoRenewal(Long userId, boolean autoRenewal) { + User user = userRepository.findById(userId).orElseThrow(); + + TariffInfo tariffInfo = user.getActiveTariffInfo(); + if (tariffInfo != null) { + tariffInfo.setAutoRenewal(autoRenewal); + tariffInfoRepository.save(tariffInfo); + } + } + + @Transactional + public void handlePaymentNotification(Map notification) { + String eventType = (String) notification.get("event"); + Map object = (Map) notification.get("object"); + String paymentUuid = (String) object.get("id"); + + Payment payment = paymentRepository.findByPaymentUuid(paymentUuid) + .orElseThrow(() -> new PaymentNotFoundException("Payment not found")); + + if ("payment.succeeded".equals(eventType)) { + Tariff tariff = payment.getTariff(); + double price = tariff.getPrice(); + int intPrice = (int) Math.round(price); + + payment.setStatus(PaymentStatus.SUCCEEDED); + payment.setCancellationReason(null); + + Map metadata = (Map) object.get("metadata"); + Map paymentMethod = (Map) object.get("payment_method"); + String paymentMethodId = paymentMethod != null ? (String) paymentMethod.get("id") : null; + + boolean autoRenewal = metadata != null && Boolean.parseBoolean(metadata.get("auto_renewal")); + + activateTariffForUser(payment.getUser(), tariff, autoRenewal, paymentMethodId); + + savePaymentMethod(payment.getUser(), paymentMethod); + + referralService.onUserAccountRefill(payment.getUser().getId(), intPrice); + } else if ("payment.canceled".equals(eventType)) { + payment.setStatus(PaymentStatus.CANCELED); + + Map cancellationDetails = (Map) object.get("cancellation_details"); + if (cancellationDetails != null) { + String reason = (String) cancellationDetails.get("reason"); + payment.setCancellationReason(reason); + } + + } else if ("payment.waiting_for_capture".equals(eventType)) { + payment.setStatus(PaymentStatus.WAITING); + payment.setCancellationReason(null); + + } else { + payment.setStatus(PaymentStatus.FAILED); + + Map cancellationDetails = (Map) object.get("cancellation_details"); + if (cancellationDetails != null) { + String reason = (String) cancellationDetails.get("reason"); + payment.setCancellationReason(reason); + } + } + + paymentRepository.save(payment); + } + + public List userPayments(String email) { + User user = userRepository.findByEmail(email); + + if (user == null) { + throw new UserNotFoundException("User not found"); + } + + return paymentRepository.findByUserId(user.getId()); + } + + public Payment paymentInfo(String uuid) { + return paymentRepository.findByPaymentUuid(uuid).orElseThrow(() -> + new PaymentNotFoundException("Payment not found")); + } + + private void activateTariffForUser(User user, Tariff tariff, boolean autoRenewal, String paymentMethodId) { + String type = tariff.getTariffAccountType(); + TariffInfo activeTariffInfo = user.getActiveTariffInfo(); + + if (type.equals("token")) { + Integer tokens = activeTariffInfo.getBoughtTokens(); + activeTariffInfo.setBoughtTokens(tokens + tariff.getTokens()); + + tariffInfoRepository.save(activeTariffInfo); + } else { + TariffInfo tariffInfo = new TariffInfo(); + tariffInfo.setStatus(TariffStatus.ACTIVE); + tariffInfo.setStartTariff(LocalDateTime.now()); + tariffInfo.setEndTariff(LocalDateTime.now().plusMonths(1)); + tariffInfo.setTokens(tariff.getTokens()); + tariffInfo.setBoughtTokens(activeTariffInfo.getBoughtTokens()); + tariffInfo.setTariff(tariff); + activeTariffInfo.setAutoRenewal(autoRenewal); + tariffInfo.setPaymentMethodId(paymentMethodId); + + tariffInfoService.linkUserTariffInfo(user, tariffInfoService.addTariffInfo(tariffInfo)); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java b/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java new file mode 100644 index 0000000..62b357c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/BalanceService.java @@ -0,0 +1,94 @@ +package ru.soune.nocopy.service.payout; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.referral.Referral; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.InsufficientFundsException; +import ru.soune.nocopy.repository.ReferralJpaRepository; + +import java.math.BigDecimal; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class BalanceService { + + private final ReferralJpaRepository referralJpaRepository; + + public boolean hasSufficientFunds(User user, BigDecimal amount) { + Referral referral = referralJpaRepository.findByUserId(user.getId()); + BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome()); + + return available.compareTo(amount) >= 0; + } + + public BigDecimal getBalance(User user) { + Referral referral = referralJpaRepository.findByUserId(user.getId()); + return BigDecimal.valueOf(referral.getAvailableIncome()); + } + + public BigDecimal getFrozenBalance(User user) { + Referral referral = referralJpaRepository.findByUserId(user.getId()); + return BigDecimal.valueOf(referral.getHoldBalance()); + } + + @Transactional + public void freezeAmount(User user, BigDecimal amount) { + Referral referral = referralJpaRepository.findByUserId(user.getId()); + BigDecimal available = BigDecimal.valueOf(referral.getAvailableIncome()); + + if (available.compareTo(amount) < 0) { + throw new InsufficientFundsException( + String.format("Not have amount. Amount have: %.2f", available) + ); + } + + BigDecimal newAvailable = available.subtract(amount); + BigDecimal newHold = BigDecimal.valueOf(referral.getHoldBalance()).add(amount); + + referral.setAvailableIncome(newAvailable.intValue()); + referral.setHoldBalance(newHold.intValue()); + + referralJpaRepository.save(referral); + + log.info("Amount freeze. Have: {}, Frozen: {}", newAvailable, newHold); + } + + @Transactional + public void unfreezeAmount(Long userId, BigDecimal amount) { + log.info("Unfreeze {} for user {}", amount, userId); + + Referral referral = referralJpaRepository.findByUserId(userId); + BigDecimal hold = BigDecimal.valueOf(referral.getHoldBalance()); + + if (hold.compareTo(amount) < 0) { + throw new IllegalStateException( + String.format("Not have freeze amount. Frozen: %.2f", hold) + ); + } + + BigDecimal newAvailable = BigDecimal.valueOf(referral.getAvailableIncome()).add(amount); + BigDecimal newHold = hold.subtract(amount); + + referral.setAvailableIncome(newAvailable.intValue()); + referral.setHoldBalance(newHold.intValue()); + + referralJpaRepository.save(referral); + + log.info("Amount frozen. Have: {}, Frozen: {}", newAvailable, newHold); + } + + public void withdrawAmount(Long userId, BigDecimal amount) { + Referral referral = referralJpaRepository.findByUserId(userId); + + if (referral.getHoldBalance() < amount.intValue()) { + throw new IllegalStateException("Not have amount"); + } + + referral.setHoldBalance(referral.getHoldBalance() - amount.intValue()); + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java new file mode 100644 index 0000000..60dfd25 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutMethodService.java @@ -0,0 +1,156 @@ +package ru.soune.nocopy.service.payout; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.payout.AddBankTransferRequest; +import ru.soune.nocopy.dto.payout.AddCardRequest; +import ru.soune.nocopy.dto.payout.PayoutMethodDTO; +import ru.soune.nocopy.entity.payout.*; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.DuplicateMethodException; +import ru.soune.nocopy.repository.PayoutMethodRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.user.UserService; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PayoutMethodService { + + private final PayoutMethodRepository payoutMethodRepository; + + private final UserRepository userRepository; + + @Transactional + public void initiatedStandartPayoutMethods(User user) { + createInternalMethod(user); + } + + @Transactional + public InternalPayoutMethod createInternalMethod(User user) { + if (payoutMethodRepository.findByUserAndType(user.getId(), InternalPayoutMethod.class).isPresent()) { + return (InternalPayoutMethod) payoutMethodRepository.findByUserAndType( + user.getId(), InternalPayoutMethod.class).get(); + } + + InternalPayoutMethod internalMethod = new InternalPayoutMethod(); + internalMethod.setUser(user); + internalMethod.setDefault(true); + + return payoutMethodRepository.save(internalMethod); + } + + public List getUserMethods(long userId) { + return payoutMethodRepository.findByUserId(userId) + .stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + } + + @Transactional + public void setDefaultMethod(Long userId, Long methodId) { + payoutMethodRepository.clearOtherDefault(userId, methodId); + + PayoutMethod method = payoutMethodRepository.findByIdAndUserId(methodId, userId) + .orElseThrow(() -> new IllegalArgumentException("Not found method")); + method.setDefault(true); + + payoutMethodRepository.save(method); + } + + @Transactional + public BankTransferPayoutMethod addBankTransferMethod(Long userId, AddBankTransferRequest request) { + User user = userRepository.findById(userId).orElseThrow(); + + BankTransferPayoutMethod bankMethod = new BankTransferPayoutMethod(); + bankMethod.setUser(user); + bankMethod.setBankName(request.getBankName()); + bankMethod.setBic(request.getBic()); + bankMethod.setCorrespondentAccount(request.getCorrespondentAccount()); + bankMethod.setAccountNumber(request.getAccountNumber()); + bankMethod.setAccountHolder(request.getAccountHolder()); + bankMethod.setInn(request.getInn()); + bankMethod.setKpp(request.getKpp()); + bankMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0); + + return payoutMethodRepository.save(bankMethod); + } + + @Transactional + public CardPayoutMethod addCardMethod(Long userId, AddCardRequest request) { + List byUserId = payoutMethodRepository.findByUserId(userId); + + if (!byUserId.isEmpty()) { + for (PayoutMethod payoutMethod : byUserId) { + if (payoutMethod.getMethodType().equals(PayoutType.CARD.getDisplayName().toUpperCase())) { + payoutMethodRepository.delete(payoutMethod); + } + } + } + + String cardHash = generateCardHash(request.getCardNumber()); + + if (payoutMethodRepository.existsByCardHash(cardHash)) { + throw new DuplicateMethodException("Card was added"); + } + + User user = userRepository.findById(userId).orElseThrow(); + + CardPayoutMethod cardMethod = new CardPayoutMethod(); + cardMethod.setUser(user); + cardMethod.setCardNumber(request.getCardNumber()); + cardMethod.setCardHash(cardHash); + cardMethod.setDefault(payoutMethodRepository.countByUserId(user.getId()) == 0); + + CardPayoutMethod saved = payoutMethodRepository.save(cardMethod); + log.info("Added user card: {}", user.getId()); + + return saved; + } + + public PayoutMethod getDefaultMethod(User user) { + return payoutMethodRepository.findByUserIdAndIsDefaultTrue(user.getId()) + .orElseGet(() -> { + List methods = payoutMethodRepository.findByUserId(user.getId()); + if (methods.isEmpty()) { + return createInternalMethod(user); + } + return methods.get(0); + }); + } + + public PayoutMethodDTO convertToDto(PayoutMethod method) { + PayoutMethodDTO payoutMethodDTO = PayoutMethodDTO.builder() + .id(method.getId()) + .type(method.getClass().getSimpleName().replace("PayoutMethod", "")) + .displayName(method.getDisplayName()) + .maskedDetails(method.getMaskedDetails()) + .isDefault(method.isDefault()) + .build(); + + if (method instanceof CardPayoutMethod cardMethod) { + payoutMethodDTO.setCardNumber(cardMethod.getCardNumber()); + } + + return payoutMethodDTO; + } + + private String generateCardHash(String cardNumber) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + byte[] hash = md.digest(cardNumber.getBytes()); + return Base64.getEncoder().encodeToString(hash); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed generate hash", e); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java new file mode 100644 index 0000000..f8bc0be --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/payout/PayoutRequestService.java @@ -0,0 +1,157 @@ +package ru.soune.nocopy.service.payout; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.ReferralService; +import ru.soune.nocopy.dto.payout.PayoutRequestDTO; +import ru.soune.nocopy.entity.payout.PayoutMethod; +import ru.soune.nocopy.entity.payout.PayoutRequest; +import ru.soune.nocopy.entity.payout.PayoutRequestStatus; +import ru.soune.nocopy.entity.payout.PayoutType; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.InsufficientFundsException; +import ru.soune.nocopy.exception.InvalidPayoutMethodException; +import ru.soune.nocopy.exception.PendingPayoutExistsException; +import ru.soune.nocopy.repository.PayoutMethodRepository; +import ru.soune.nocopy.repository.PayoutRequestRepository; +import ru.soune.nocopy.repository.UserRepository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PayoutRequestService { + private final PayoutRequestRepository payoutRequestRepository; + + private final PayoutMethodRepository payoutMethodRepository; + + private final BalanceService balanceService; + + private final UserRepository userRepository; + + private static final BigDecimal MIN_PAYOUT_AMOUNT = new BigDecimal("1000.00"); + + private static final List BLOCKING_STATUSES = Arrays.asList(PayoutRequestStatus.PENDING, + PayoutRequestStatus.PROCESSING); + + @Transactional + public PayoutRequest createPayoutRequest(Long userId, PayoutRequestDTO request) { + log.info("Create request for money {}: sum {}", userId, request.getAmount()); + + validatePayoutAmount(request.getAmount()); + + User user = userRepository.findById(userId).orElseThrow(); + + if (!balanceService.hasSufficientFunds(user, request.getAmount())) { + throw new InsufficientFundsException(String.format("Not have money for payout. Have: %.2f", + balanceService.getBalance(user))); + } + + if (hasActivePayoutRequests(user)) { + throw new PendingPayoutExistsException("Have active payout. Wait for end."); + } + + PayoutMethod payoutMethod = payoutMethodRepository + .findByIdAndUserId(request.getPayoutMethodId(), user.getId()) + .orElseThrow(() -> new InvalidPayoutMethodException("Not found payout method")); + + PayoutType payoutType = determinePayoutType(payoutMethod); + + PayoutRequest payoutRequest = PayoutRequest.builder() + .user(user) + .payoutMethod(payoutMethod) + .amount(request.getAmount()) + .status(PayoutRequestStatus.PENDING) + .payoutType(payoutType) + .build(); + + PayoutRequest savedRequest = payoutRequestRepository.save(payoutRequest); + + balanceService.freezeAmount(user, request.getAmount()); + + return savedRequest; + } + + private void validatePayoutAmount(BigDecimal amount) { + if (amount == null) { + throw new IllegalArgumentException("Sum is null"); + } + + if (amount.compareTo(MIN_PAYOUT_AMOUNT) < 0) { + throw new IllegalArgumentException( + String.format("Min sum for payout: %.2f rub.", MIN_PAYOUT_AMOUNT) + ); + } + + } + + private boolean hasActivePayoutRequests(User user) { + return payoutRequestRepository.existsByUserIdAndStatusIn(user.getId(), BLOCKING_STATUSES); + } + + private PayoutType determinePayoutType(PayoutMethod payoutMethod) { + String className = payoutMethod.getClass().getSimpleName(); + + switch (className) { + case "CardPayoutMethod": + return PayoutType.CARD; + case "BankTransferPayoutMethod": + return PayoutType.BANK_TRANSFER; + case "InternalPayoutMethod": + return PayoutType.INTERNAL; + default: + throw new InvalidPayoutMethodException("Unknown method"); + } + } + + @Transactional(readOnly = true) + public List getUserPayoutRequests(Long userId) { + return payoutRequestRepository.findByUserIdOrderByCreatedAtDesc(userId); + } + + @Transactional(readOnly = true) + public PayoutRequest getPayoutRequest(Long userId, Long requestId) { + return payoutRequestRepository.findByIdAndUserId(requestId, userId) + .orElseThrow(() -> new IllegalArgumentException("Payout not found")); + } + + @Transactional + public PayoutRequest cancelPayoutRequest(Long userId, Long requestId) { + PayoutRequest request = getPayoutRequest(userId, requestId); + + if (request.getStatus() != PayoutRequestStatus.PENDING) { + throw new IllegalStateException( + "Can't cnacel: " + request.getStatus() + ); + } + + request.setStatus(PayoutRequestStatus.CANCELLED); + + balanceService.unfreezeAmount(userId, request.getAmount()); + + return payoutRequestRepository.save(request); + } + + @Transactional + public PayoutRequest processRequest(Long requestId) { + PayoutRequest request = payoutRequestRepository.findById(requestId) + .orElseThrow(); + + if (request.getStatus() != PayoutRequestStatus.PENDING) { + throw new IllegalStateException("Can't proceed in status: " + request.getStatus()); + } + + request.setStatus(PayoutRequestStatus.COMPLETED); + request.setProcessAt(LocalDateTime.now()); + + balanceService.withdrawAmount(request.getUser().getId(), request.getAmount()); + + return payoutRequestRepository.save(request); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java index 59f67db..b02d0b4 100644 --- a/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java +++ b/src/main/java/ru/soune/nocopy/service/referral/ReferralRepoImpl.java @@ -99,8 +99,7 @@ public class ReferralRepoImpl implements ReferralRepo { @Override public int getAvailableIncome() { - Integer holdBalance = entity.getHoldBalance(); - return entity.getTotalIncome() - (holdBalance != null ? holdBalance : 0); + return entity.getAvailableIncome(); } @Override @@ -109,6 +108,11 @@ public class ReferralRepoImpl implements ReferralRepo { return holdBalance != null ? holdBalance : 0; } + @Override + public int getSpentBalance() { + return entity.getTotalIncome() - entity.getAvailableIncome() - entity.getHoldBalance(); + } + @Override public boolean getActive() { return entity.isActive(); } }; @@ -151,6 +155,7 @@ public class ReferralRepoImpl implements ReferralRepo { referral.setInviterId(entity.getInviter()); referral.setLevelId(entity.getCurrentLevel()); referral.setTotalIncome(entity.getTotalIncome()); + referral.setAvailableIncome(entity.getAvailableIncome()); referral.setHoldBalance(entity.getHoldBalance()); referral.setActive(entity.getActive()); diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index f90a75c..652d45b 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -11,22 +11,18 @@ import ru.soune.nocopy.dto.register.LoginAnswer; import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.entity.company.Company; -import ru.soune.nocopy.entity.tarif.Tariff; import ru.soune.nocopy.entity.tarif.TariffInfo; import ru.soune.nocopy.entity.tarif.TariffType; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.Permission; import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.exception.MaxUserCountIsOver; +import ru.soune.nocopy.entity.user.UserNotActive; +import ru.soune.nocopy.exception.CompanyAlreadyExist; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotValidFieldException; -import ru.soune.nocopy.repository.AuthTokenRepository; -import ru.soune.nocopy.repository.CompanyRepository; -import ru.soune.nocopy.repository.TariffInfoRepository; -import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.*; import ru.soune.nocopy.service.file.CheckCounterService; import ru.soune.nocopy.service.tariff.TariffInfoService; -import ru.soune.nocopy.service.tariff.TariffService; import java.security.SecureRandom; import java.time.LocalDateTime; @@ -50,21 +46,27 @@ public class AuthService { private final TariffInfoService tariffInfoService; + private final UserNotActiveRepository userNotActiveRepository; + private final SecureRandom secureRandom = new SecureRandom(); @Transactional - public AuthToken register(RegRequest registerRequest) { + public AuthToken register(RegRequest registerRequest, boolean verified) { User user = new User(); user.setFullName(registerRequest.getFullName()); user.setEmail(registerRequest.getEmail()); user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); - user.setActive(true); - user.setEmailVerified(true); + user.setActive(!verified); + user.setEmailVerified(!verified); AccountType accountType = AccountType.valueOf(registerRequest.getAccountType().toUpperCase()); if (accountType == AccountType.B2B && registerRequest.getCompanyName() != null) { - createCompanyAccount(registerRequest, user); + try { + createCompanyAccount(registerRequest, user); + } catch (CompanyAlreadyExist exist) { + throw new CompanyAlreadyExist("Company already exist"); + } } else if (accountType == AccountType.B2C && registerRequest.getCompanyName() == null) { createIndividualAccount(user); } else { @@ -104,14 +106,30 @@ public class AuthService { return authToken.getUser().getId(); } - - @Transactional public AuthToken login(LoginRequest request) { User user = userRepository.findByEmail(request.getEmail()); + if (!user.isActive()) { + throw new NotValidFieldException("User not active", new BaseResponse(20003, + MessageCode.USER_NOT_ACTIVE.getCode(), + MessageCode.USER_NOT_ACTIVE.getDescription(), Map.of("userId", user.getId()))); + } + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { LoginAnswer loginAnswer = new LoginAnswer(); loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword()))); + loginAnswer.setVerified(user.isEmailVerified()); + loginAnswer.setActive(user.isActive()); + + int attempts = user.getFailedLoginAttempts() + 1; + user.setFailedLoginAttempts(attempts); + + if (attempts >= 5) { + user.setActive(false); + blockUser(user); + } + + userRepository.save(user); throw new NotValidFieldException("Invalid password", new BaseResponse(20003, MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(), @@ -119,6 +137,9 @@ public class AuthService { } user.setLastLoginAt(LocalDateTime.now()); + user.setFailedLoginAttempts(0); + user.setActive(true); + User savedUser = userRepository.save(user); AuthToken authToken = new AuthToken(); @@ -134,10 +155,15 @@ public class AuthService { .ifPresent(authTokenRepository::delete); } + @Transactional public AuthToken generateAuthToken(User user) { return authTokenRepository.save(genereateAuthToken(user)); } + public AuthToken getAuthToken(String token) { + return authTokenRepository.findByToken(token).get(); + } + private AuthToken genereateAuthToken(User user) { AuthToken authToken = new AuthToken(); authToken.setToken(generateAuthToken()); @@ -153,8 +179,15 @@ public class AuthService { } private void createCompanyAccount(RegRequest request, User user) { + Optional byInn = companyRepository.findByInn(request.getInn()); + + if (byInn.isPresent()) { + throw new CompanyAlreadyExist("Company was already created"); + } + Company company = new Company(); company.setCompanyName(request.getCompanyName()); + company.setInn(request.getInn()); TariffInfo companyTariff = tariffInfoService.createTariffInfo(TariffType.DEMO); company.setTariffInfo(companyTariff); @@ -192,18 +225,33 @@ public class AuthService { throw new IllegalStateException("Company not have tariff"); } - Tariff tariff = tariffInfo.getTariff(); - Long maxUsers = tariff.getMaxUsers(); - long currentCountUsers = userRepository.countByCompanyId(company.getId()); - - if (currentCountUsers >= maxUsers) { - throw new NotValidFieldException("Max user is over.", new BaseResponse(20002, - MessageCode.USER_LIMIT_IS_OVER.getCode(), - MessageCode.USER_LIMIT_IS_OVER.getDescription(), Map.of( - "limit", maxUsers, "countUser", currentCountUsers))); - } +// Tariff tariff = tariffInfo.getTariff(); +// Long maxUsers = tariff.getMaxUsers(); +// long currentCountUsers = userRepository.countByCompanyId(company.getId()); +// +// if (currentCountUsers >= maxUsers) { +// throw new NotValidFieldException("Max user is over.", new BaseResponse(20002, +// MessageCode.USER_LIMIT_IS_OVER.getCode(), +// MessageCode.USER_LIMIT_IS_OVER.getDescription(), Map.of( +// "limit", maxUsers, "countUser", currentCountUsers))); +// } user.setCompany(company); user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS); } + + public void blockUser(User user) { + Optional existing = userNotActiveRepository.findByUserId(user.getId()); + if (existing.isEmpty()) { + LocalDateTime now = LocalDateTime.now(); + + UserNotActive notActive = UserNotActive.builder() + .user(user) + .createdAt(now) + .blockedUntil(now.plusMinutes(15)) + .build(); + + userNotActiveRepository.save(notActive); + } + } } diff --git a/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java index 455d8e8..2a16142 100644 --- a/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java +++ b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.service.register; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -12,6 +13,7 @@ import java.util.List; @Service @RequiredArgsConstructor +@Slf4j public class CleanUpVerifyTokens { private final EmailVerificationTokenRepository emailVerificationTokenRepository; @@ -20,8 +22,7 @@ public class CleanUpVerifyTokens { @Scheduled(fixedDelay = 30000) public void cleanupExpiredTokens() { LocalDateTime now = LocalDateTime.now(); - - + log.info("Check time for tokens"); List expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now); if (!expiredTokens.isEmpty()) { diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java new file mode 100644 index 0000000..c155366 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchAsyncProcessor.java @@ -0,0 +1,178 @@ +package ru.soune.nocopy.service.search; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.configuration.search.SearchProperties; +import ru.soune.nocopy.dto.file.YandexSearchResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.search.GlobalSearchResult; +import ru.soune.nocopy.entity.search.GlobalSearchTask; +import ru.soune.nocopy.entity.search.SearchStatus; +import ru.soune.nocopy.repository.GlobalSearchResultRepository; +import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.service.tariff.TariffConstants; +import ru.soune.nocopy.service.tariff.TariffInfoService; +import ru.soune.nocopy.service.violation.ViolationService; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class GlobalSearchAsyncProcessor { + + private final GlobalSearchResultRepository globalSearchResultRepository; + + private final SearchImageService searchImageService; + + private final TariffInfoService tariffInfoService; + + private final GlobalSearchTaskRepository globalSearchTaskRepository; + + private final ViolationService violationService; + + private final SearchProperties searchProperties; + + @Async + @Transactional + public void processFilesAsync(String taskId, List filesToProcess, Long userId) { + try { + for (int i = 0; i < filesToProcess.size(); i++) { + FileEntity file = filesToProcess.get(i); + tariffInfoService.writeOffTokens(userId, TariffConstants.TOKEN_VALUE_FOR_SEARCH); + GlobalSearchResult result = processFile(file, taskId, userId); + globalSearchResultRepository.save(result); + + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setProcessedFiles(i + 1); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + } + + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setStatus(SearchStatus.COMPLETED.name()); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + + } catch (Exception e) { + log.error("Global search failed for task: {}", taskId, e); + + GlobalSearchTask task = globalSearchTaskRepository.findById(taskId).orElseThrow(); + task.setStatus(SearchStatus.FAILED.name()); + task.setUpdatedAt(LocalDateTime.now()); + globalSearchTaskRepository.save(task); + } + } + + private GlobalSearchResult processFile(FileEntity file, String taskId, Long userId) { + GlobalSearchResult result = new GlobalSearchResult(); + result.setTaskId(taskId); + result.setFileId(file.getId()); + result.setFileName(file.getOriginalFileName()); + + result = globalSearchResultRepository.save(result); + + boolean useGoogle = searchProperties.getEngines().getOrDefault("google", + new SearchProperties.EngineConfig()).isEnabled(); + boolean useYandex = searchProperties.getEngines().getOrDefault("yandex", + new SearchProperties.EngineConfig()).isEnabled(); + + log.info("Global search settings for file {}: useGoogle={}, useYandex={}", + file.getId(), useGoogle, useYandex); + + List googleImages = new ArrayList<>(); + List yandexImages = new ArrayList<>(); + List allResults = new ArrayList<>(); + boolean hasTimeout = false; + + if (useGoogle) { + try { + String searchResponseGoogle = searchImageService.searchReverseByPublicUrl( + file, "google_lens", "exact_matches"); + + googleImages = searchImageService.getAllImagesWithoutPagination(searchResponseGoogle, + "exact_matches"); + + allResults.addAll(googleImages); + log.info("Google search OK for file {}, found {} results", file.getId(), googleImages.size()); + + } catch (TimeoutException e) { + log.warn("Google search timeout for file {}", file.getId()); + hasTimeout = true; + } catch (IOException e) { + log.error("Google search failed for file {}", file.getId(), e); + } + } else { + log.info("Google search is disabled by settings for file {}", file.getId()); + } + + if (useYandex) { + try { + String searchResponseYandex = searchImageService.searchReverseByPublicUrl( + file, "yandex_reverse_image", "visual_matches"); + + yandexImages = searchImageService.getAllImagesWithoutPagination(searchResponseYandex, "visual_matches"); + + allResults.addAll(yandexImages); + log.info("Yandex search OK for file {}, found {} results", file.getId(), yandexImages.size()); + + } catch (TimeoutException e) { + log.warn("Yandex search timeout for file {}", file.getId()); + hasTimeout = true; + } catch (IOException e) { + log.error("Yandex search failed for file {}", file.getId(), e); + } + } else { + log.info("Yandex search is disabled by settings for file {}", file.getId()); + } + + List allUniqueImages; + if (useGoogle && useYandex) { + allUniqueImages = searchImageService.removeDuplicateUrls( + yandexImages.stream() + .filter(img -> img.getUrl() != null) + .collect(Collectors.toList()), + googleImages.stream() + .filter(img -> img.getUrl() != null) + .collect(Collectors.toList()) + ); + log.info("Merged results from both engines: {} unique images", allUniqueImages.size()); + } else if (useGoogle) { + allUniqueImages = googleImages.stream() + .filter(img -> img.getUrl() != null) + .collect(Collectors.toList()); + log.info("Results only from Google: {} images", allUniqueImages.size()); + } else if (useYandex) { + allUniqueImages = yandexImages.stream() + .filter(img -> img.getUrl() != null) + .collect(Collectors.toList()); + log.info("Results only from Yandex: {} images", allUniqueImages.size()); + } else { + allUniqueImages = new ArrayList<>(); + log.warn("All search engines are disabled for file {}", file.getId()); + } + + for (YandexSearchResponse.ImageResult imageResult : allUniqueImages) { + violationService.processViolation(imageResult, file, result); + } + + if (allUniqueImages.isEmpty() && hasTimeout) { + result.setFileStatus(SearchStatus.TIMEOUT.name()); + } else if (allUniqueImages.isEmpty() && (!useGoogle && !useYandex)) { + result.setFileStatus(SearchStatus.FAILED.name()); + log.warn("No results for file {} because all engines are disabled", file.getId()); + } else { + result.setFileStatus(SearchStatus.SUCCESS.name()); + } + + return result; + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java new file mode 100644 index 0000000..e5c29a6 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchService.java @@ -0,0 +1,72 @@ +package ru.soune.nocopy.service.search; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.search.GlobalSearchStartRequest; +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.search.GlobalSearchTask; +import ru.soune.nocopy.entity.search.SearchStatus; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.FileMonitoringRepository; +import ru.soune.nocopy.repository.GlobalSearchTaskRepository; +import ru.soune.nocopy.service.file.FileEntityService; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GlobalSearchService { + + private final FileEntityRepository fileEntityRepository; + + private final FileMonitoringRepository fileMonitoringRepository; + + private final GlobalSearchTaskRepository globalSearchTaskRepository; + + private final GlobalSearchAsyncProcessor asyncProcessor; + + private final FileEntityService fileEntityService; + + @Transactional + public String startSearch(GlobalSearchStartRequest request, Long userId, List filesToProcess) { + GlobalSearchTask task = new GlobalSearchTask(); + task.setUserId(userId); + task.setSearchType(request.getSearchType()); + task.setStatus(SearchStatus.PROCESSING.name()); + task.setTotalFiles(filesToProcess.size()); + task.setProcessedFiles(0); + task.setCreatedAt(LocalDateTime.now()); + + globalSearchTaskRepository.save(task); + + asyncProcessor.processFilesAsync(task.getTaskId(), filesToProcess, userId); + + return task.getTaskId(); + } + + public List getFilesToProcess(GlobalSearchStartRequest request, Long userId) { + if ("monitoring".equals(request.getSearchType())) { + List monitoringFiles = + fileMonitoringRepository.findByUserIdAndIsActiveTrue(userId); + + return monitoringFiles.isEmpty() ? new ArrayList<>(): monitoringFiles.stream() + .map(FileMonitoringEntity::getFile) + .toList(); + } else { + List fileIds = request.getFileIds().isEmpty() ? fileEntityService.getAllUserFiles(userId) + .stream() + .filter(f -> f.getMimeType().equals(FileType.IMAGE.getDisplayName())) + .map(FileEntity::getId) + .toList(): request.getFileIds(); + + return fileEntityRepository.findAllById(fileIds); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java new file mode 100644 index 0000000..1fc8c5c --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/search/GlobalSearchStatisticsService.java @@ -0,0 +1,101 @@ +package ru.soune.nocopy.service.search; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.search.GlobalSearchStatisticsResponse; +import ru.soune.nocopy.entity.company.Company; +import ru.soune.nocopy.entity.search.GlobalSearchResult; +import ru.soune.nocopy.entity.search.GlobalSearchTask; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.*; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class GlobalSearchStatisticsService { + + private final GlobalSearchTaskRepository taskRepository; + + private final GlobalSearchResultRepository resultRepository; + + private final ViolationRepository violationRepository; + + private final UserRepository userRepository; + + @Transactional(readOnly = true) + public GlobalSearchStatisticsResponse getStatistics(Long userId, Integer limit, Integer days) { + User user = userRepository.findById(userId).orElseThrow(); + Company company = user.getCompany(); + List userIds = company == null ? List.of(userId) : + user.getCompany() + .getUsers() + .stream() + .map(User::getId).toList(); + + long totalSearches = taskRepository.countByUserId(userIds); + List statusCounts = taskRepository.countByStatusAndUserId(userIds); + + GlobalSearchStatisticsResponse.StatusCountDto statusCountDto = + GlobalSearchStatisticsResponse.StatusCountDto.builder() + .processing(0) + .completed(0) + .failed(0) + .total(totalSearches) + .build(); + + for (Object[] row : statusCounts) { + String status = (String) row[0]; + Long count = (Long) row[1]; + + if ("PROCESSING".equalsIgnoreCase(status)) { + statusCountDto.setProcessing(count); + } else if ("COMPLETED".equalsIgnoreCase(status) || "SUCCESS".equalsIgnoreCase(status)) { + statusCountDto.setCompleted(statusCountDto.getCompleted() + count); + } else if ("FAILED".equalsIgnoreCase(status) || "TIMEOUT".equalsIgnoreCase(status)) { + statusCountDto.setFailed(statusCountDto.getFailed() + count); + } + } + + List recentTasks; + + if (days != null && days > 0) { + LocalDateTime fromDate = LocalDateTime.now().minusDays(days); + recentTasks = taskRepository.findByUserIdAndCreatedAtAfterOrderByCreatedAtDesc(userId, fromDate); + + if (limit != null && limit > 0 && recentTasks.size() > limit) { + recentTasks = recentTasks.subList(0, limit); + } + } else { + org.springframework.data.domain.Pageable pageable = + org.springframework.data.domain.PageRequest.of(0, limit != null ? limit : 10); + recentTasks = taskRepository.findTopByUserIdOrderByCreatedAtDesc(userId, pageable); + } + + List recentSearchDtos = new ArrayList<>(); + + for (GlobalSearchTask task : recentTasks) { + List globalSearchResults = resultRepository.findByTaskId(task.getTaskId()); + List fileIds = globalSearchResults.stream() + .map(GlobalSearchResult::getFileId) + .toList(); + + GlobalSearchStatisticsResponse.SearchStatDto searchStatDto = + GlobalSearchStatisticsResponse.SearchStatDto.fromEntity(task, + violationRepository.countByFileEntityIdIn(fileIds), fileIds); + + recentSearchDtos.add(searchStatDto); + } + + return GlobalSearchStatisticsResponse.builder() + .totalSearches(totalSearches) + .searchesByStatus(statusCountDto) + .recentSearches(recentSearchDtos) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java index ba6fa2c..32ac438 100644 --- a/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java +++ b/src/main/java/ru/soune/nocopy/service/search/SearchImageService.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import okhttp3.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; +import ru.soune.nocopy.configuration.search.SearchProperties; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.YandexSearchResponse; @@ -15,30 +16,23 @@ import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.exception.NotValidFieldException; import java.io.*; +import java.net.InetSocketAddress; +import java.net.Proxy; import java.net.SocketTimeoutException; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.*; @Slf4j @Service + public class SearchImageService { private final ObjectMapper objectMapper; - private final OkHttpClient httpClient; - - public SearchImageService() { - this.httpClient = new OkHttpClient.Builder() - .connectTimeout(5, TimeUnit.SECONDS) - .writeTimeout(5, TimeUnit.SECONDS) - .readTimeout(15, TimeUnit.SECONDS) - .callTimeout(20, TimeUnit.SECONDS) - .retryOnConnectionFailure(true) - .build(); + private final SearchProperties searchProperties; + public SearchImageService(SearchProperties searchProperties) { + this.searchProperties = searchProperties; this.objectMapper = new ObjectMapper(); } @@ -95,19 +89,41 @@ public class SearchImageService { return mimeType != null && mimeType.startsWith("image"); } - private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { + private OkHttpClient createHttpClient() { + boolean useProxy = searchProperties.getProxy().isEnabled(); + OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectTimeout(35, TimeUnit.SECONDS) + .writeTimeout(35, TimeUnit.SECONDS) + .readTimeout(35, TimeUnit.SECONDS) + .callTimeout(35, TimeUnit.SECONDS) + .followRedirects(true) + .followSslRedirects(true) + .retryOnConnectionFailure(true); + if (useProxy && searchProperties.getProxy().isEnabled()) { + Proxy proxy = new Proxy(Proxy.Type.HTTP, + new InetSocketAddress(searchProperties.getProxy().getHost(), + searchProperties.getProxy().getPort())); + builder.proxy(proxy); + } + + return builder.build(); + } + + private String callReverseImageApiByUrl(String imageUrl, String engine, String searchType) throws IOException { if (searchApiKey == null || searchApiKey.isBlank()) { throw new IllegalStateException("SearchAPI key not configured"); } - HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search") + OkHttpClient client = createHttpClient(); + + HttpUrl url = HttpUrl.parse("https://www.searchapi.io/api/v1/search") .newBuilder() .addQueryParameter("engine", engine) .addQueryParameter("api_key", searchApiKey) .addQueryParameter("url", imageUrl) .addQueryParameter("search_type", searchType) - .addQueryParameter("wait", "true") + .addQueryParameter("t_", String.valueOf(System.currentTimeMillis())) .build(); Request request = new Request.Builder() @@ -118,16 +134,17 @@ public class SearchImageService { long start = System.currentTimeMillis(); - try (Response response = httpClient.newCall(request).execute()) { - + try (Response response = client.newCall(request).execute()) { + ResponseBody body = response.body(); long duration = System.currentTimeMillis() - start; - log.info("Yandex response code={}, duration={}ms", response.code(), duration); + log.info("SearchAPI response code={}, duration={}ms, engine={}", + response.code(), duration, engine); if (!response.isSuccessful()) { - throw new IOException("API error: " + response.code()); + String errorBody = response.body() != null ? response.body().string() : "null"; + throw new IOException("API error " + response.code() + ": " + errorBody); } - ResponseBody body = response.body(); if (body == null) { throw new IOException("Empty response body"); } @@ -149,8 +166,13 @@ public class SearchImageService { if ("exact_matches".equals(findType)) { JsonNode thumbnail = match.path("thumbnail"); - if (!thumbnail.isMissingNode() && thumbnail.asText().startsWith("data:image")) { - result.setUrl(thumbnail.asText()); + if (!thumbnail.isMissingNode()) { + String thumbnailUrl = thumbnail.asText(); + if (thumbnailUrl.startsWith("data:image")) { + result.setUrl(thumbnailUrl); + } else if (!thumbnailUrl.isBlank()) { + result.setUrl(thumbnailUrl); + } } JsonNode imageNode = match.path("image"); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java index 951f7c3..50fba93 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInfoService.java @@ -1,6 +1,8 @@ package ru.soune.nocopy.service.tariff; +import jakarta.mail.MessagingException; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; 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.TariffRepository; import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import java.io.IOException; import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @Service @AllArgsConstructor +@Slf4j public class TariffInfoService { private TariffInfoRepository tariffInfoRepository; @@ -28,6 +33,8 @@ public class TariffInfoService { private UserRepository userRepository; + private EmailService emailService; + public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus, Integer tokens) { TariffInfo tariffInfo = new TariffInfo(); @@ -39,9 +46,9 @@ public class TariffInfoService { return tariffInfoRepository.save(tariffInfo); } - - @Transactional - public void writeOffTokens(long userId, Integer value) { + +// @Transactional + public void writeOffTokens(long userId, Integer value) throws MessagingException, IOException { User user = userRepository.findById(userId).orElseThrow(); TariffInfo activeTariffInfo; @@ -52,6 +59,10 @@ public class TariffInfoService { activeTariffInfo = user.getCompany().getTariffInfo(); } + writeOffTokensForTariff(activeTariffInfo, value); + } + + public void writeOffTokensForTariff(TariffInfo activeTariffInfo, Integer value) throws MessagingException, IOException { Integer tokens = activeTariffInfo.getTokens(); Integer boughtTokens = activeTariffInfo.getBoughtTokens(); @@ -64,6 +75,12 @@ public class TariffInfoService { tariffInfoRepository.save(activeTariffInfo); } 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"); } } @@ -71,15 +88,15 @@ public class TariffInfoService { public TariffInfo createTariffInfo(TariffType tariffType) { Tariff tariff = tariffRepository.findByType(tariffType.toString()).orElseThrow(); - TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(), LocalDateTime.now().plusDays(30), - TariffStatus.ACTIVE, tariff.getTokens()); + TariffInfo tariffInfo = createTariffInfo(LocalDateTime.now(), + LocalDateTime.now().plusDays(tariff.getTariffTerm().getDays()), TariffStatus.ACTIVE, tariff.getTokens()); tariffInfo.setTariff(tariff); return tariffInfoRepository.save(tariffInfo); } - public void addTariffInfo(TariffInfo tariffInfo) { - tariffInfoRepository.save(tariffInfo); + public TariffInfo addTariffInfo(TariffInfo tariffInfo) { + return tariffInfoRepository.save(tariffInfo); } public List getAllTariffInfos() { @@ -130,6 +147,13 @@ public class TariffInfoService { tariffInfo.getTariff().getMaxFilesCount()); } + @Transactional + public void linkUserTariffInfo(User user, TariffInfo tariffInfo) { + user.setPersonalTariffInfo(tariffInfo); + + userRepository.save(user); + } + private void updateTariffInfoFromDTO(TariffInfo tariffInfo, TariffInfoDTO dto) { tariffInfo.setStatus(TariffStatus.valueOf(dto.getStatus())); tariffInfo.setStartTariff(dto.getStartTariff()); diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java index 0e037fa..f5ae48e 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffInitializationService.java @@ -7,13 +7,12 @@ import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; 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.repository.TariffRepository; import java.util.Arrays; -import java.util.HashMap; import java.util.List; -import java.util.Map; @Service @RequiredArgsConstructor @@ -109,6 +108,18 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 100); tariff.setMaxUsers(1L); 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; case START: @@ -119,6 +130,18 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 500); tariff.setMaxUsers(1L); 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; case PRO: @@ -129,8 +152,21 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 10L); tariff.setMaxUsers(1L); tariff.setTariffAccountType("b2c"); + tariff.setTariffTerm(TariffTimeTerm.MONTHLY); 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: tariff.setName("studio"); tariff.setPrice(9750); @@ -139,6 +175,18 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setMaxUsers(2L); 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; case BASIC: @@ -149,6 +197,18 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setMaxUsers(basicUserCount); 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; case ENTERPRISE: @@ -159,6 +219,18 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 10L); tariff.setMaxUsers(10L); 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; case BUISNES: @@ -169,8 +241,21 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setMaxUsers(5L); tariff.setTariffAccountType("b2b"); + tariff.setTariffTerm(TariffTimeTerm.MONTHLY); 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: tariff.setName("corporat"); tariff.setPrice(99000); @@ -179,6 +264,90 @@ public class TariffInitializationService { tariff.setDiskSize(1024L * 1024 * 1024 * 50L); tariff.setMaxUsers(10L); tariff.setTariffAccountType("b2b"); + tariff.setTariffTerm(TariffTimeTerm.MONTHLY); + 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: + tariff.setName("token_100"); + tariff.setPrice(299); + tariff.setTokens(100); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("token"); + break; + + case TOKEN_500: + tariff.setName("token_500"); + tariff.setPrice(1299); + tariff.setTokens(500); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("token"); + break; + + case TOKEN_1000: + tariff.setName("token_1000"); + tariff.setPrice(2399); + tariff.setTokens(1000); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("token"); + break; + + case TOKEN_5000: + tariff.setName("token_5000"); + tariff.setPrice(9999); + tariff.setTokens(5000); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("token"); + break; + + + case MONITORING_DAILY: + tariff.setName("monitoring_daily"); + tariff.setPrice(0); + tariff.setTokens(10); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); + break; + + case MONITORING_WEEKLY: + tariff.setName("monitoring_weekly"); + tariff.setPrice(0); + tariff.setTokens(25); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); + break; + + case MONITORING_MONTHLY: + tariff.setName("monitoring_monthly"); + tariff.setPrice(0); + tariff.setTokens(50); + tariff.setMaxFilesCount(0); + tariff.setDiskSize(0L); + tariff.setMaxUsers(0L); + tariff.setTariffAccountType("monitoring"); break; } diff --git a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java index 5c6561e..c8309a3 100644 --- a/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java +++ b/src/main/java/ru/soune/nocopy/service/tariff/TariffService.java @@ -5,6 +5,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.tarriff.TariffDTO; 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.user.User; import ru.soune.nocopy.exception.TariffNotFoundException; @@ -20,7 +21,7 @@ public class TariffService { private final TariffRepository tariffRepository; 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.setName(tariffName); tariff.setPrice(price); @@ -29,6 +30,7 @@ public class TariffService { tariff.setDiskSize(diskSize); tariff.setMaxUsers(maxUsers); tariff.setMaxFilesCount(maxFilesCount); + tariff.setTariffTerm(TariffTimeTerm.valueOf(tariffTerm.toUpperCase())); tariffRepository.save(tariff); } @@ -42,6 +44,22 @@ public class TariffService { .collect(Collectors.toList()); } + public List 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 getTariffByType(String type) { + return tariffRepository.findByTariffAccountType(type) + .stream() + .map(this::convertToDTO) + .collect(Collectors.toList()); + } + public List getAllTariffs() { return tariffRepository.findAllByOrderByPriceAsc() .stream() @@ -84,6 +102,8 @@ public class TariffService { } private TariffDTO convertToDTO(Tariff tariff) { + TariffTimeTerm tariffTerm = tariff.getTariffTerm(); + return new TariffDTO( tariff.getId(), tariff.getType(), @@ -92,7 +112,8 @@ public class TariffService { tariff.getTokens(), tariff.getMaxFilesCount(), tariff.getDiskSize(), - tariff.getMaxUsers() + tariff.getMaxUsers(), + tariffTerm == null ? "" : tariffTerm.name() ); } @@ -104,5 +125,6 @@ public class TariffService { tariff.setMaxFilesCount(dto.getMaxFilesCount()); tariff.setDiskSize(dto.getDiskSize()); tariff.setMaxUsers(dto.getMaxUsers()); + tariff.setTariffTerm(TariffTimeTerm.valueOf(dto.getTariffTerm().toUpperCase())); } } diff --git a/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java new file mode 100644 index 0000000..3ca53d2 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/user/AdditionalInfoService.java @@ -0,0 +1,56 @@ +package ru.soune.nocopy.service.user; + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.user.UserAdditionalInfo; +import ru.soune.nocopy.exception.RegistrationAdditionalInfoNullException; +import ru.soune.nocopy.repository.UserAdditionalInfoRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AdditionalInfoService { + + private final UserAdditionalInfoRepository additionalInfoRepository; + + @Transactional + public void additionalInfo(String ip, String userAgent, User user) { + if (ip == null || userAgent == null) { + throw new RegistrationAdditionalInfoNullException("Ip address or user agent is null"); + } + + UserAdditionalInfo userAdditionalInfo = UserAdditionalInfo.builder() + .ipAddress(ip) + .userAgent(userAgent) + .user(user) + .build(); + + additionalInfoRepository.save(userAdditionalInfo); + } + + public UserAdditionalInfo getAdditionalInfoByUser(Long userId) { + return additionalInfoRepository.findById(userId).orElse(null); + } + + public List getAllAdditionalInfos() { + return additionalInfoRepository.findAll(); + } + + public void deleteAdditionalInfo(Long userId) { + additionalInfoRepository.deleteById(userId); + } + + @Transactional + public Optional updateAdditionalInfo(Long userId, String ip, String userAgent) { + return additionalInfoRepository.findById(userId) + .map(info -> { + info.setIpAddress(ip); + info.setUserAgent(userAgent); + return additionalInfoRepository.save(info); + }); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java new file mode 100644 index 0000000..235e1ce --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationNotionService.java @@ -0,0 +1,127 @@ +package ru.soune.nocopy.service.violation; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.violation.ViolationNotionResponse; +import ru.soune.nocopy.dto.violation.ViolationNotionsListResponse; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.entity.violation.ViolationNotion; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.repository.ViolationNotionRepository; +import ru.soune.nocopy.repository.ViolationRepository; + +import java.util.List; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ViolationNotionService { + + private final ViolationNotionRepository notionRepository; + private final UserRepository userRepository; + private final ViolationRepository violationRepository; + + @Transactional(readOnly = true) + public ViolationNotionsListResponse getNotionsByViolationId(Long violationId, Long userId) { + Violation violation = violationRepository.findById(violationId) + .orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId)); + + List notions = notionRepository.findByViolationId(violationId); + long totalCount = notionRepository.countByViolationId(violationId); + + List notionResponses = notions.stream() + .map(this::mapToResponse) + .collect(Collectors.toList()); + + return ViolationNotionsListResponse.builder() + .violationId(violationId) + .url(violation.getUrl()) + .host(violation.getHost()) + .totalCount((int) totalCount) + .notions(notionResponses) + .build(); + } + + @Transactional(readOnly = true) + public ViolationNotionResponse getNotionById(Long notionId, Long userId) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + if (!notion.getUser().getId().equals(userId)) { + throw new SecurityException("User does not have access to this notion"); + } + + return mapToResponse(notion); + } + + @Transactional + public ViolationNotionResponse createNotion(Long violationId, Long userId, String message) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new IllegalArgumentException("User not found: " + userId)); + + Violation violation = violationRepository.findById(violationId) + .orElseThrow(() -> new IllegalArgumentException("Violation not found: " + violationId)); + + ViolationNotion notion = new ViolationNotion(); + notion.setUser(user); + notion.setViolation(violation); + notion.setMessage(message); + + ViolationNotion saved = notionRepository.save(notion); + log.info("Created notion for violation: {} by user: {}", violationId, userId); + + return mapToResponse(saved); + } + + @Transactional + public ViolationNotionResponse updateNotion(Long notionId, Long userId, String message) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + if (!notion.getUser().getId().equals(userId)) { + throw new SecurityException("Only author can update this notion"); + } + + notion.setMessage(message); + ViolationNotion updated = notionRepository.save(notion); + log.info("Updated notion: {} by user: {}", notionId, userId); + + return mapToResponse(updated); + } + + @Transactional + public void deleteNotion(Long notionId, Long userId) { + ViolationNotion notion = notionRepository.findById(notionId) + .orElseThrow(() -> new IllegalArgumentException("Notion not found: " + notionId)); + + if (!notion.getUser().getId().equals(userId)) { + throw new SecurityException("Only author can delete this notion"); + } + + notionRepository.delete(notion); + log.info("Deleted notion: {} by user: {}", notionId, userId); + } + + private ViolationNotionResponse mapToResponse(ViolationNotion notion) { + return ViolationNotionResponse.builder() + .id(notion.getId()) + .message(notion.getMessage()) + .createdAt(notion.getCreatedAt()) + .updatedAt(notion.getUpdatedAt()) + .user(ViolationNotionResponse.UserInfo.builder() + .id(notion.getUser().getId()) + .fullName(notion.getUser().getFullName()) + .email(notion.getUser().getEmail()) + .build()) + .violation(ViolationNotionResponse.ViolationInfo.builder() + .id(notion.getViolation().getId()) + .url(notion.getViolation().getPageUrl()) + .host(notion.getViolation().getHost()) + .build()) + .build(); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java new file mode 100644 index 0000000..8f8fad3 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationService.java @@ -0,0 +1,232 @@ +package ru.soune.nocopy.service.violation; + +import lombok.AllArgsConstructor; +import org.apache.commons.codec.digest.DigestUtils; +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.YandexSearchResponse; +import ru.soune.nocopy.dto.violation.FileViolationSummaryDTO; +import ru.soune.nocopy.dto.violation.ViolationStatisticsResponse; +import ru.soune.nocopy.entity.file.FileEntity; +import ru.soune.nocopy.entity.search.GlobalSearchResult; +import ru.soune.nocopy.entity.violation.Violation; +import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.ViolationRepository; +import ru.soune.nocopy.service.file.FileEntityService; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class ViolationService { + + private final ViolationRepository violationRepository; + + private final FileEntityRepository fileEntityRepository; + + private final FileEntityService fileEntityService; + + public void processViolation(YandexSearchResponse.ImageResult imageResult, FileEntity file, + GlobalSearchResult result) { + String url = imageResult.getUrl(); + String urlHash = DigestUtils.sha256Hex(url); + + if (!violationRepository.existsByUrlHash(urlHash)) { + Violation violation = new Violation(); + + if (result != null) { + violation.setGlobalSearchResult(result); + } + + violation.setHost(imageResult.getHost()); + violation.setUrl(url); + violation.setUrlHash(urlHash); + violation.setPageUrl(imageResult.getPageUrl()); + violation.setPageTitle(imageResult.getPageTitle()); + violation.setFileEntity(file); + violation.setStatus(ViolationStatus.NEW.name()); + + violationRepository.save(violation); + } + } + + public List getFileViolationsSummary(Long userId) { + List userFiles = fileEntityService.getAllUserFiles(userId); + List violations = violationRepository.findByFileEntityIn(userFiles); + + Map> violationsByFileId = violations.stream() + .collect(Collectors.groupingBy(v -> v.getFileEntity().getId())); + + return userFiles.stream() + .map(file -> { + List fileViolations = violationsByFileId.getOrDefault(file.getId(), + Collections.emptyList()); + + if (fileViolations.isEmpty()) { + return null; + } + + LocalDateTime latestDate = fileViolations.stream() + .map(Violation::getCreatedDate) + .max(LocalDateTime::compareTo) + .orElse(null); + + return FileViolationSummaryDTO.builder() + .fileId(file.getId()) + .supportId(file.getSupportId()) + .fileName(file.getOriginalFileName()) + .fileSize(file.getFileSize()) + .mimeType(file.getMimeType()) + .status(file.getStatus()) + .createdAt(file.getCreatedAt()) + .violationCount(fileViolations.size()) + .latestViolationDate(latestDate) + .build(); + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + @Transactional + public void changeStatus(ViolationStatus status, Long violationId) { + Violation violation = violationRepository.findById(violationId).orElseThrow(); + violation.setStatus(status.name()); + + violationRepository.save(violation); + } + + public Page getViolationsByFiles(List files, int page, int size, String sortDirection) { + Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate"); + Pageable pageable = PageRequest.of(page, size, sort); + return violationRepository.findByFileEntityIn(files, pageable); + } + + public Page getViolationsByFilesAndStatus(List files, String status, int page, int size, String sortDirection) { + Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate"); + Pageable pageable = PageRequest.of(page, size, sort); + return violationRepository.findByFileEntityInAndStatus(files, status, pageable); + } + + public Page getViolationsByFilesAndDateRange(List files, LocalDateTime startDate, LocalDateTime endDate, + int page, int size, String sortDirection) { + Sort sort = Sort.by(sortDirection.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC, "createdDate"); + Pageable pageable = PageRequest.of(page, size, sort); + return violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate, pageable); + } + + public Map getGroupedViolations(List files, String groupBy, String status, + LocalDateTime startDate, LocalDateTime endDate, + String sortDirection) { + List violations; + + if (status != null && !status.isEmpty() && startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndStatusAndCreatedDateBetween(files, status, startDate, endDate); + } else if (status != null && !status.isEmpty()) { + violations = violationRepository.findByFileEntityInAndStatus(files, status); + } else if (startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndCreatedDateBetween(files, startDate, endDate); + } else { + violations = violationRepository.findByFileEntityIn(files); + } + + Map grouped = violations.stream() + .collect(Collectors.groupingBy( + v -> extractGroupKey(v.getPageUrl(), groupBy), + Collectors.counting())); + + return grouped.entrySet().stream() + .sorted((e1, e2) -> { + if (sortDirection.equalsIgnoreCase("desc")) { + return e2.getValue().compareTo(e1.getValue()); + } else { + return e1.getValue().compareTo(e2.getValue()); + } + }) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (e1, e2) -> e1, + LinkedHashMap::new)); + } + + public ViolationStatisticsResponse getViolationStatistics(Long userId, String fileId, + LocalDateTime startDate, LocalDateTime endDate) { + + List userFiles; + + if (fileId != null && !fileId.isEmpty()) { + FileEntity file = fileEntityRepository.findById(fileId) + .orElseThrow(() -> new IllegalArgumentException("File not found with id: " + fileId)); + + if (!file.getUserId().equals(userId)) { + throw new IllegalArgumentException("File does not belong to user"); + } + + userFiles = List.of(file); + } else { + userFiles = fileEntityService.getAllUserFiles(userId); + } + + if (userFiles.isEmpty()) { + return ViolationStatisticsResponse.builder() + .totalViolations(0) + .newViolations(0) + .inProgressViolations(0) + .resolvedViolations(0) + .build(); + } + + List violations; + + if (startDate != null && endDate != null) { + violations = violationRepository.findByFileEntityInAndCreatedDateBetween(userFiles, startDate, endDate); + } else { + violations = violationRepository.findByFileEntityIn(userFiles); + } + + Set inProgressStatuses = Set.of( + ViolationStatus.SHOWED.name(), + ViolationStatus.LEGAL_IN_WORK.name(), + ViolationStatus.COMPLAINT_IN_WORK.name(), + ViolationStatus.COMPLAINT_AND_LEGAL_IN_WORK.name() + ); + + long newViolations = violations.stream() + .filter(v -> v.getStatus().equals(ViolationStatus.NEW.name())) + .count(); + + long inProgressViolations = violations.stream() + .filter(v -> inProgressStatuses.contains(v.getStatus())) + .count(); + + long resolvedViolations = violations.stream() + .filter(v -> v.getStatus().equals(ViolationStatus.AUTHORIZED_USE.name())) + .count(); + + return ViolationStatisticsResponse.builder() + .totalViolations(violations.size()) + .newViolations(newViolations) + .inProgressViolations(inProgressViolations) + .resolvedViolations(resolvedViolations) + .build(); + } + + private String extractGroupKey(String url, String groupBy) { + try { + String domain = url.replaceAll("https?://(www\\.)?", "").split("/")[0]; + if ("tld".equalsIgnoreCase(groupBy)) { + int lastDot = domain.lastIndexOf('.'); + return lastDot > 0 ? domain.substring(lastDot + 1) : domain; + } + return domain; + } catch (Exception e) { + return "unknown"; + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java b/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java new file mode 100644 index 0000000..6deac03 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/violation/ViolationStatus.java @@ -0,0 +1,5 @@ +package ru.soune.nocopy.service.violation; + +public enum ViolationStatus { + NEW , SHOWED, LEGAL_IN_WORK, COMPLAINT_IN_WORK, COMPLAINT_AND_LEGAL_IN_WORK , AUTHORIZED_USE +} diff --git a/src/main/java/ru/soune/nocopy/util/FileUtil.java b/src/main/java/ru/soune/nocopy/util/FileUtil.java index 7bfb07b..f53642b 100644 --- a/src/main/java/ru/soune/nocopy/util/FileUtil.java +++ b/src/main/java/ru/soune/nocopy/util/FileUtil.java @@ -9,20 +9,21 @@ import ru.soune.nocopy.entity.file.FileEntity; @Service public class FileUtil { + public ImageLocalSearch.Result convertToResult(SimilarFileDTO response) { FileProtector.FileInfo fileInfo = new FileProtector.FileInfo(FileProtector.Type.IMAGE, response.getFileId(), - String.valueOf(response.getOwnerId())); + String.valueOf(response.getOwnerId()), null); ImageScore imageScore = new ImageScore(ImageScore.Rate.valueOf(response.getSimilarityLevel()), response.getHammingDistance()); return new ImageLocalSearch.Result(fileInfo, imageScore); } - public FileProtector.FileInfo createFileInfo(FileEntity fileEntity) { + public FileProtector.FileInfo createFileInfo(FileEntity fileEntity, String convertTo) { FileProtector.Type type = determineFileType(fileEntity.getMimeType()); - return new FileProtector.FileInfo(type, fileEntity.getId(), String.valueOf(fileEntity.getUserId())); + return new FileProtector.FileInfo(type, fileEntity.getId(), String.valueOf(fileEntity.getUserId()), convertTo); } public int hamming64(long aHi, long aLo, long bHi, long bLo) { diff --git a/src/main/resources/application-dev.yaml b/src/main/resources/application-dev.yaml deleted file mode 100644 index bd1102b..0000000 --- a/src/main/resources/application-dev.yaml +++ /dev/null @@ -1,2 +0,0 @@ -server: - baseurl: "http://localhost" \ No newline at end of file diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml deleted file mode 100644 index 2a35858..0000000 --- a/src/main/resources/application-prod.yaml +++ /dev/null @@ -1,2 +0,0 @@ -server: - baseurl: "http://92.242.61.23" \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 7f8823c..6bc410a 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -75,7 +75,9 @@ security: server: port: ${SERVER_PORT:8080} - baseurl: "http://92.242.61.23" + baseurl: ${BASE_URL:https://dev-workspace.not-copy.com} + front: + port: ":2998" tomcat: uri-encoding: UTF-8 servlet: @@ -109,14 +111,31 @@ yandex: searchapi: - api-key: ${SEARCHAPI_API_KEY:rdwNzCr2eybYVbU1KrGTJAgm} + api-key: ${SEARCHAPI_API_KEY:5jyYZC8jSaxhZTwjMUhwtAXi} reverse-image-url: "https://searchapi.io/api/v1/search" +search: + engines: + yandex: + enabled: true + google: + enabled: false + proxy: + enabled: false + host: "193.46.217.94" + port: 3128 + +yookassa: + secret-key: test_0Yns_0NHV5GJf6ypJ5HC4NSfnLO8SJkw-1PwrVWsDl4 + shop-id: 1276731 + + app: email: verification: standart-subject: NO COPY - Подтверждение email адреса standart-address-from: vlad.popovtsev@gmail.com + drop-password-subject: NO COPY - сброс пароля user: Пользователь tarif: @@ -150,3 +169,26 @@ tarif: enterprice-disk-size: 500 enterprice-user-count: 80 +--- +spring: + config: + activate: + on-profile: dev +server: + baseurl: https://dev-workspace.not-copy.com + +--- +spring: + config: + activate: + on-profile: prod +server: + baseurl: https://workspace.not-copy.com + +--- +spring: + config: + activate: + on-profile: local +server: + baseurl: http://localhost:8080 \ No newline at end of file diff --git a/src/main/resources/templates/auto-renewal-success.html b/src/main/resources/templates/auto-renewal-success.html new file mode 100644 index 0000000..959fa36 --- /dev/null +++ b/src/main/resources/templates/auto-renewal-success.html @@ -0,0 +1,169 @@ + + + + + + Подписка успешно продлена + + + +
+
+ +

Подписка успешно продлена!

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

Ваша подписка была автоматически продлена. Спасибо, что остаётесь с нами!

+ +
+
+ Тариф: + {{tariff_name}} +
+
+ Сумма списания: + {{price}} ₽ +
+
+ Дата продления: + {{renewal_date}} +
+
+ Следующее списание: + {{next_payment_date}} +
+
+ +
+ 💡 Детали подписки: +
    +
  • Доступ к сервису активен до {{next_payment_date}}
  • +
  • Вы всегда можете управлять подпиской в личном кабинете
  • +
+
+ +

+ Если у вас есть вопросы, наша служба поддержки всегда готова помочь. +

+
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/failed-template.html b/src/main/resources/templates/failed-template.html new file mode 100644 index 0000000..33c671f --- /dev/null +++ b/src/main/resources/templates/failed-template.html @@ -0,0 +1,170 @@ + + + + + + Автопродление подписки не удалось + + + +
+
+ +

Автопродление не удалось

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

К сожалению, автоматическое продление подписки не удалось выполнить.

+ +
+
+ Тариф: + {{tariff_name}} +
+
+ Сумма: + {{price}} ₽ +
+
+ Дата попытки: + {{attempt_date}} +
+
+ +
+ Что делать? +
    +
  • Проверьте достаточно ли средств на карте
  • +
  • Убедитесь что карта не заблокирована
  • +
  • Если проблема сохраняется, обратитесь в поддержку
  • +
+
+ +

+ После успешной оплаты доступ к сервису будет восстановлен автоматически. +

+
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/reset-password.html b/src/main/resources/templates/reset-password.html new file mode 100644 index 0000000..abc80ec --- /dev/null +++ b/src/main/resources/templates/reset-password.html @@ -0,0 +1,201 @@ + + + + + + Восстановление пароля + + + +
+
+ +

Восстановление пароля

+

Безопасный доступ к вашему аккаунту

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

Мы получили запрос на восстановление пароля для вашего аккаунта NO COPY.

+

Для продолжения восстановления доступа, пожалуйста, используйте следующий код подтверждения:

+ +
+

{{code}}

+

Код для восстановления пароля

+
+ +
+

Код действителен в течение 15 минут

+
+ +

Как использовать этот код:

+
    +
  1. Вернитесь на страницу восстановления пароля
  2. +
  3. Введите полученный код в поле подтверждения
  4. +
  5. Создайте новый надежный пароль
  6. +
  7. Войдите в аккаунт с новым паролем
  8. +
+ + + + + +
+ Важная информация:
+ 1. Если вы не запрашивали восстановление пароля, проигнорируйте это письмо.
+ 2. Никому не сообщайте этот код, включая сотрудников поддержки.
+ 3. После успешного восстановления старый пароль будет недействителен. +
+ +

+ Совет по безопасности: Используйте пароль длиной не менее 8 символов, + содержащий буквы (заглавные и строчные), цифры и специальные символы. +

+
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/token-not-found.html b/src/main/resources/templates/token-not-found.html new file mode 100644 index 0000000..8cb9674 --- /dev/null +++ b/src/main/resources/templates/token-not-found.html @@ -0,0 +1,171 @@ + + + + + + Недостаточно токенов + + + +
+
+ +

Недостаточно токенов

+
+ +
+

+ Здравствуйте, {{username}}! +

+ +

При попытке провести мониторинг файлов, токенов для автоматического мониторинга не достаточно.

+ +
+

Ваш текущий баланс:

+

{{current_tokens}} токенов

+

Не хватает: {{missing_tokens}} токенов

+
+ + +
+ + +
+ + \ No newline at end of file