63 Commits
Author SHA1 Message Date
vladp 4ee7e439c0 NC_YA
Test Workflow / test (push) Successful in 3s
2026-02-06 04:24:08 +07:00
vladp 503463e1d0 NC_YA
Test Workflow / test (push) Successful in 3s
2026-02-06 04:19:43 +07:00
vladp 5f61c82dee NC_YA
Test Workflow / test (push) Successful in 3s
2026-02-06 04:14:17 +07:00
vladp 2139ca0e8c NC_YA
Test Workflow / test (push) Successful in 7s
2026-02-06 03:50:26 +07:00
vladp 01332c7db5 NC_YA
Test Workflow / test (push) Successful in 4s
2026-02-06 03:31:41 +07:00
vladp 80c53515fb dev add stats api
Test Workflow / test (push) Successful in 3s
2026-02-05 20:23:33 +07:00
vladp 511da8321b dev add stats api
Test Workflow / test (push) Successful in 3s
2026-02-05 20:21:27 +07:00
vladp df1fd89c29 dev add stats api
Test Workflow / test (push) Successful in 3s
2026-02-05 19:47:32 +07:00
vladp 90e3134e98 dev add stats api
Test Workflow / test (push) Successful in 3s
2026-02-05 19:33:37 +07:00
vladp 130fd184b9 dev add stats api
Test Workflow / test (push) Successful in 3s
2026-02-05 19:02:17 +07:00
vladp f90b8a0bce dev add stats for search check
Test Workflow / test (push) Successful in 3s
2026-02-05 18:55:54 +07:00
vladp adc978bea8 dev write off for search
Test Workflow / test (push) Successful in 3s
2026-02-05 17:25:50 +07:00
vladp 7bb48f3944 dev add test find by searchapo
Test Workflow / test (push) Successful in 3s
2026-02-05 17:16:16 +07:00
milidev 27b812b0da NCBACK-36: [Referral] Referral Service
Test Workflow / test (push) Successful in 3s
2026-02-05 15:43:42 +07:00
vladp 6643070999 dev add test find by searchapo
Test Workflow / test (push) Successful in 8s
2026-02-05 12:44:27 +07:00
vladp 4395d276ed dev add test find by searchapo
Test Workflow / test (push) Successful in 4s
2026-02-05 10:47:40 +07:00
vladp e49a944b05 dev add test find by searchapo
Test Workflow / test (push) Successful in 3s
2026-02-05 03:30:15 +07:00
vladp 8e716f7d20 dev add test find by searchapo
Test Workflow / test (push) Waiting to run
2026-02-05 03:27:16 +07:00
vladp 2e8080183c dev add test find by searchapo
Test Workflow / test (push) Waiting to run
2026-02-05 00:46:17 +07:00
vladp dc9215523b dev add test find by searchapo
Test Workflow / test (push) Waiting to run
2026-02-05 00:37:59 +07:00
vladp 72691511fa dev add
Test Workflow / test (push) Successful in 3s
2026-02-04 14:56:19 +07:00
vladp 135d94e0ef dev add
Test Workflow / test (push) Successful in 4s
2026-02-04 13:41:09 +07:00
vladp d1bacb4ac7 dev add
Test Workflow / test (push) Successful in 9s
2026-02-04 12:50:35 +07:00
vladp 7a809e6b8c dev add
Test Workflow / test (push) Successful in 4s
2026-02-04 11:47:43 +07:00
vladp 06a6196a26 dev add
Test Workflow / test (push) Successful in 10s
2026-02-03 16:01:32 +07:00
vladp 13324d4628 dev add tarifinfo by permission for user info
Test Workflow / test (push) Successful in 4s
2026-02-03 15:05:03 +07:00
vladp c38b86e1b9 dev add tariffinfo endpoints
Test Workflow / test (push) Successful in 4s
2026-02-02 15:47:58 +07:00
vladp 3ce8edd7e1 dev add tariff endpoints
Test Workflow / test (push) Successful in 3s
2026-02-02 15:15:43 +07:00
vladp 22b487da22 dev add tariff endpoints
Test Workflow / test (push) Successful in 4s
2026-02-02 15:00:18 +07:00
vladp 4395654b0d dev add apllication tariff fields
Test Workflow / test (push) Successful in 3s
2026-02-02 12:19:34 +07:00
vladp 7dd057603d dev
Test Workflow / test (push) Successful in 2s
2026-01-30 22:36:20 +07:00
vladp a4858cf6c5 dev
Test Workflow / test (push) Successful in 3s
2026-01-30 22:30:27 +07:00
vladp 7d52276b31 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 3s
2026-01-30 19:27:01 +07:00
vladp 8fe8c566c0 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 5s
2026-01-30 18:04:08 +07:00
vladp 36d40a0b8d dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 4s
2026-01-30 18:03:51 +07:00
vladp 32f0a07e32 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 3s
2026-01-30 18:02:34 +07:00
vladp 51ef63e8cd dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 17:55:14 +07:00
vladp b0237fd812 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 17:51:59 +07:00
vladp de8c53784b dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 17:39:19 +07:00
vladp 3cd6b5396c dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 3s
2026-01-30 17:36:57 +07:00
vladp 615924ea9e dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 17:29:25 +07:00
vladp 9a8aa498c0 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 15:09:39 +07:00
vladp 66633d4964 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 14:24:23 +07:00
vladp ce8c63475f dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 14:00:21 +07:00
vladp 30c05b6b24 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 13:02:30 +07:00
vladp 49dc19cd04 dev add tariff and temp flag for files
Test Workflow / test (push) Successful in 2s
2026-01-30 12:52:16 +07:00
vladp fb5642db9e dev check null company
Test Workflow / test (push) Successful in 2s
2026-01-29 21:46:37 +07:00
vladp c539b1fbc2 dev check null company
Test Workflow / test (push) Successful in 2s
2026-01-29 21:31:28 +07:00
vladp 61b8ea902b dev limit for yasearch result
Test Workflow / test (push) Successful in 1s
2026-01-29 20:13:10 +07:00
vladp 6ee7ab4d04 dev add rest for company, add link for users , update reg users
Test Workflow / test (push) Successful in 2s
2026-01-29 19:24:36 +07:00
vladp 96acf796b2 dev change status on Processing for prePresist
Test Workflow / test (push) Successful in 3s
2026-01-29 16:25:34 +07:00
vladp ff7836900b dev fix show image
Test Workflow / test (push) Successful in 2s
2026-01-29 13:08:43 +07:00
vladp ec20d68261 dev add check limits logic
Test Workflow / test (push) Successful in 3s
2026-01-28 23:25:16 +07:00
vladp 31fac0ab93 dev check to protect
Test Workflow / test (push) Successful in 3s
2026-01-28 21:18:42 +07:00
vladp a32f089989 dev add info for files
Test Workflow / test (push) Successful in 2s
2026-01-28 19:26:40 +07:00
vladp c019f23777 dev download only protected files
Test Workflow / test (push) Successful in 3s
2026-01-28 18:38:43 +07:00
vladp 02bf7868ee dev add split network and add new ports with apps
Test Workflow / test (push) Successful in 9s
2026-01-28 18:19:20 +07:00
vladp 2215adc37f dev
Test Workflow / test (push) Successful in 3s
2026-01-27 20:55:33 +07:00
vladp 6fcec8d67e dev
Test Workflow / test (push) Successful in 2s
2026-01-27 20:41:08 +07:00
vladp 4a185e6f9a dev
Test Workflow / test (push) Successful in 3s
2026-01-27 20:27:39 +07:00
vladp cf2f73955b dev
Test Workflow / test (push) Successful in 3s
2026-01-27 08:50:52 +07:00
vladp f67e172365 dev
Test Workflow / test (push) Successful in 3s
2026-01-27 08:25:38 +07:00
backdev d32e8dcd75 Merge pull request 'NCBACK-33' (#16) from NCBACK-33 into dev
Test Workflow / test (push) Successful in 3s
Reviewed-on: #16
2026-01-27 09:11:31 +08:00
102 changed files with 4136 additions and 389 deletions
+46
View File
@@ -76,3 +76,49 @@ ALTER TABLE file_entities
ALTER COLUMN support_id ALTER COLUMN support_id
SET DEFAULT nextval('file_support_id_seq'); SET DEFAULT nextval('file_support_id_seq');
----------
Раздать всем лимиты,у кого их нет
INSERT INTO protect_check (user_id, check_limit, count_checked, last_check_at, version)
SELECT
id as user_id,
10 as check_limit,
0 as count_checked,
NULL as last_check_at,
0 as version
FROM users
WHERE id NOT IN (SELECT user_id FROM protect_check);
--------
Обновлять констрейнты для file_entities
ALTER TABLE file_entities DROP CONSTRAINT file_entities_status_check;
ALTER TABLE file_entities
ADD CONSTRAINT file_entities_status_check
CHECK (status IN (
'ACTIVE',
'DELETED',
'PROCESSING',
'VIOLATION',
'CHECKED',
'ERROR',
'TEMP'
));
-------
Записи для лимитов поиска
INSERT INTO protect_check (user_id, limit_check, count_checked, last_check_at, version)
SELECT
u.id,
100, -- limit_check = 100
0, -- count_checked = 0
NULL, -- last_check_at = NULL
0 -- version = 0
FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM protect_check pc
WHERE pc.user_id = u.id
);
+2
View File
@@ -61,6 +61,8 @@ dependencies {
implementation name: 'testlib-fat-0.2.1-all' implementation name: 'testlib-fat-0.2.1-all'
implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.12.0'
} }
tasks.named('test') { tasks.named('test') {
+73
View File
@@ -0,0 +1,73 @@
version: '3.9'
services:
storage:
image: alpine:latest
container_name: file-storage-local
networks:
- app-network-local
volumes:
- uploads_data_local:/storage:rw
command: tail -f /dev/null
restart: unless-stopped
db:
image: postgres:17.7
restart: always
environment:
POSTGRES_DB: no_copy_
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_SHARED_BUFFERS: 512MB
POSTGRES_EFFECTIVE_CACHE_SIZE: 1536MB
ports:
- "54320:5432"
volumes:
- pgdata_local:/var/lib/postgresql/data
- ./init-scripts:/docker-entrypoint-initdb.d:ro
container_name: postgres-local
networks:
app-network-local:
aliases:
- database
app:
build: .
container_name: app-backend-local
environment:
JAVA_TOOL_OPTIONS: "-Xmx2g -Xms1g"
FILE_STORAGE_PATH: /data/uploads
POSTGRES_DB: no_copy_
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_PORT: 5432
POSTGRES_HOST: db
# Используем local профиль
SPRING_PROFILES_ACTIVE: local
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
depends_on:
- db
ports:
- "8080:8080"
networks:
app-network-local:
aliases:
- app
- backend
volumes:
- uploads_data_local:/data/uploads:rw
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/actuator/health" ]
interval: 30s
timeout: 10s
retries: 3
volumes:
pgdata_local:
uploads_data_local:
networks:
app-network-local:
driver: bridge
+49
View File
@@ -0,0 +1,49 @@
version: '3.9'
services:
storage:
image: alpine:latest
container_name: file-storage
networks:
- app-network
volumes:
- uploads_data:/storage:rw
command: tail -f /dev/null
restart: unless-stopped
app:
build: .
container_name: app-backend
environment:
JAVA_TOOL_OPTIONS: "-Xmx2g -Xms1g"
FILE_STORAGE_PATH: /data/uploads
# Данные для Yandex Cloud БД
YC_DB_USER: ${YC_DB_USER}
YC_DB_PASSWORD: ${YC_DB_PASSWORD}
# Используем prod профиль
SPRING_PROFILES_ACTIVE: prod
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
ports:
- "80:8080"
networks:
app-network:
aliases:
- app
- backend
volumes:
- uploads_data:/data/uploads:rw
healthcheck:
test: [ "CMD", "curl", "-f", "http://localhost:8080/actuator/health" ]
interval: 30s
timeout: 10s
retries: 3
volumes:
uploads_data:
networks:
app-network:
external: true
driver: bridge
+4 -3
View File
@@ -88,17 +88,18 @@ services:
# FILE_CHUNK_SIZE: 1048576 # FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000 FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
POSTGRES_HOST: db POSTGRES_HOST: db
STORAGE_SERVICE_URL: http://storage:8081 STORAGE_SERVICE_URL: http://storage:8081
SPRING_PROFILES_ACTIVE: docker SPRING_PROFILES_ACTIVE: prod
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
# MAIL_HOST: postfix # MAIL_HOST: postfix
# MAIL_PORT: 25 # MAIL_PORT: 25
# MAIL_USERNAME: noreply@no-copy.ru # MAIL_USERNAME: noreply@no-copy.ru
+2
View File
@@ -0,0 +1,2 @@
YC_DB_USER=test
YC_DB_PASSWORD=testtest
+24
View File
@@ -0,0 +1,24 @@
plugins {
kotlin("jvm") version "2.1.10"
}
group = "ru.soune"
version = "1.0.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.10.2")
implementation("io.insert-koin:koin-core:4.1.1")
implementation("io.insert-koin:koin-core-jvm:4.1.1")
}
tasks.test {
useJUnitPlatform()
}
kotlin {
jvmToolchain(21)
}
+5
View File
@@ -0,0 +1,5 @@
package ru.soune
fun main() {
println("Hello World!")
}
@@ -0,0 +1,3 @@
package ru.soune.actions
abstract class NoCopyAction
@@ -0,0 +1,5 @@
package ru.soune.actions.free
import ru.soune.actions.NoCopyAction
sealed class NoCopyFreeAction : NoCopyAction()
@@ -0,0 +1,8 @@
package ru.soune.actions.paid
import ru.soune.actions.NoCopyAction
sealed class NoCopyPaidAction : NoCopyAction() {
abstract val cost: Double
}
+26
View File
@@ -0,0 +1,26 @@
## Команды для помещения в сеть
Сеть для работы бэка и фронта : Наименование == app-network-{port}
### Проверить состояние:
docker inspect {container-name} --format='{{range $net, $_ := .NetworkSettings.Networks}}{{$net}} {{end}}'
### Создать сеть ,если нет
docker network create {name-network} 2>/dev/null || echo "Сеть уже существует"
### Переместить контейнер:
# Отключить от старой сети
docker network disconnect {network-name-old} {container-name}
# Подключить к новой сети
docker network connect {network-name-new} {container-name}
# Добавить алиас (опционально)
docker network connect --alias {alias} {network-name-new} {container-name}
+156
View File
@@ -0,0 +1,156 @@
pipeline {
agent any
parameters {
string(
name: 'BRANCH',
defaultValue: 'main',
description: 'Ветка для деплоя админ-панели'
)
string(
name: 'PORT',
defaultValue: '2996',
description: 'Порт для запуска экземпляра'
)
}
stages {
stage('Git pull') {
steps {
script {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'https://code.3err0.ru/frontdev/no-copy-admin-panel-frontend.git',
credentialsId: 'nx-jen'
]]
])
}
}
}
stage('Stop old') {
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 "
docker stop no-copy-admin-panel-${params.PORT} 2>/dev/null || true
docker rm no-copy-admin-panel-${params.PORT} 2>/dev/null || true
mkdir -p /opt/deployments/admin-panel/${params.BRANCH}-${params.PORT}
rm -rf /opt/deployments/admin-panel/${params.BRANCH}-${params.PORT}/*
"
"""
}
}
}
}
stage('Copy to server') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/admin-panel/${params.BRANCH}-${params.PORT}/
"""
}
}
}
}
stage('Build on server') {
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 "
cd /opt/deployments/admin-panel/${params.BRANCH}-${params.PORT}
docker build -t no-copy-admin-panel:${params.BRANCH}-${params.PORT} .
"
"""
}
}
}
}
stage('Start on server') {
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 "
docker run -d \\
--name no-copy-admin-panel-${params.PORT} \\
--restart unless-stopped \\
--network app-network \\
-p ${params.PORT}:2996 \\
no-copy-admin-panel:${params.BRANCH}-${params.PORT}
sleep 5
docker ps --filter name=no-copy-admin-panel-${params.PORT}
"
"""
}
}
}
}
stage('Check status') {
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 "
if docker ps --format '{{.Names}}' | grep -q 'no-copy-admin-panel-${params.PORT}'; then
HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:${params.PORT} 2>/dev/null || echo '000')
echo 'Admin panel available on http://92.242.61.23:${params.PORT}'
else
exit 1
fi
"
"""
}
}
}
}
}
post {
success {
echo "Admin panel ${params.BRANCH} deployed"
}
failure {
echo "Deployment failed"
}
}
}
@@ -0,0 +1,146 @@
pipeline {
agent any
parameters {
string(
name: 'BRANCH',
defaultValue: 'dev',
description: 'Ветка для деплоя'
)
string(
name: 'PORT',
defaultValue: '3000',
description: 'Порт для запуска экземпляра'
)
string(
name: 'SERVER',
defaultValue: '92.242.61.23',
description: 'Сервер для деплоя'
)
}
stages {
stage('Git pull') {
steps {
cleanWs()
script {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'https://code.3err0.ru/backdev/no-copy.git',
credentialsId: 'nx-jen'
]]
])
}
}
}
stage('Deploy with docker-compose') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
),
string(credentialsId: 'DB_USER', variable: 'DB_USER'),
string(credentialsId: 'DB_PASSWORD', variable: 'DB_PASSWORD')
]) {
sh """
# Тестируем подключение сначала
echo "Testing SSH connection..."
if sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} 'echo "SSH connection successful"'; then
echo "SSH connection OK"
else
echo "SSH connection failed"
exit 1
fi
# Основная команда
sshpass -p '${SSH_PASS}' ssh -o StrictHostKeyChecking=no ${SSH_USER}@${params.SERVER} "
cd /opt/deployments/${params.BRANCH}-${params.PORT}
echo '1. Создаем изолированную сеть для порта ${params.PORT}...'
docker network create app-network-${params.PORT} 2>/dev/null || echo 'Сеть уже существует'
echo '2. Останавливаем старые контейнеры порта ${params.PORT}...'
docker stop app-backend-${params.PORT} 2>/dev/null || echo 'Контейнер не найден'
docker rm app-backend-${params.PORT} 2>/dev/null || echo 'Контейнер не найден'
docker stop postgres-${params.PORT} 2>/dev/null || echo 'Контейнер БД не найден'
docker rm postgres-${params.PORT} 2>/dev/null || echo 'Контейнер БД не найден'
docker stop file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден'
docker rm file-storage-${params.PORT} 2>/dev/null || echo 'Контейнер storage не найден'
echo '3. Запускаем БД...'
docker run -d \\
--name postgres-${params.PORT} \\
--network app-network-${params.PORT} \\
--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_USER=${DB_USER} \\
-e POSTGRES_PASSWORD=${DB_PASSWORD} \\
-v pgdata_${params.PORT}:/var/lib/postgresql/data \\
postgres:17.7
echo '4. Запускаем storage...'
docker run -d \\
--name file-storage-${params.PORT} \\
--network app-network-${params.PORT} \\
-v uploads_data_${params.PORT}:/storage:rw \\
alpine:latest tail -f /dev/null
echo '5. Ждем БД...'
sleep 15
echo '6. Собираем приложение...'
docker build --no-cache -t app-backend-${params.PORT}:latest .
echo '7. Запускаем приложение...'
docker run -d \\
--name app-backend-${params.PORT} \\
--network app-network-${params.PORT} \\
-p ${params.PORT}:8080 \\
-v uploads_data_${params.PORT}:/data/uploads:rw \\
-e POSTGRES_DB=no_copy_${params.PORT} \\
-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 \\
app-backend-${params.PORT}:latest
echo '8. Проверка...'
sleep 10
if curl -s -f http://localhost:${params.PORT}/health > /dev/null 2>&1; then
echo 'Приложение работает на порту ${params.PORT}'
else
echo 'Проверка health не удалась'
docker logs app-backend-${params.PORT} --tail=20
fi
"
"""
}
}
}
}
}
post {
success {
echo "Deployment successful"
echo "Application URL: http://${params.SERVER}:${params.PORT}"
}
failure {
echo "Deployment failed for branch ${params.BRANCH} on port ${params.PORT}"
}
always {
echo "Deployment process finished"
}
}
}
+52 -33
View File
@@ -7,6 +7,11 @@ pipeline {
defaultValue: 'main', defaultValue: 'main',
description: 'Ветка для деплоя' description: 'Ветка для деплоя'
) )
string(
name: 'PORT',
defaultValue: '2998',
description: 'Порт для запуска экземпляра'
)
} }
stages { stages {
@@ -37,15 +42,15 @@ pipeline {
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
docker stop no-copy-frontend 2>/dev/null || true docker stop no-copy-frontend-${params.PORT} 2>/dev/null || true
docker rm no-copy-frontend 2>/dev/null || true docker rm no-copy-frontend-${params.PORT} 2>/dev/null || true
cd /opt/deployments/frontend/${params.BRANCH} 2>/dev/null || mkdir -p /opt/deployments/frontend/${params.BRANCH} cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT} 2>/dev/null || mkdir -p /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
if [ -f 'docker-compose.yaml' ]; then if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml down 2>/dev/null || true docker-compose -f docker-compose.yaml -p frontend-${params.PORT} down 2>/dev/null || true
elif [ -f 'docker-compose.yml' ]; then elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml down 2>/dev/null || true docker-compose -f docker-compose.yml -p frontend-${params.PORT} down 2>/dev/null || true
fi fi
" "
""" """
@@ -66,11 +71,11 @@ pipeline {
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
rm -rf /opt/deployments/frontend/${params.BRANCH}/* rm -rf /opt/deployments/frontend/${params.BRANCH}-${params.PORT}/*
mkdir -p /opt/deployments/frontend/${params.BRANCH} mkdir -p /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
" "
sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/frontend/${params.BRANCH}/ sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/frontend/${params.BRANCH}-${params.PORT}/
""" """
} }
} }
@@ -89,16 +94,21 @@ pipeline {
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
cd /opt/deployments/frontend/${params.BRANCH} cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
if [ -f 'docker-compose.yaml' ]; then echo '=== Редактируем docker-compose.yml ==='
docker-compose -f docker-compose.yaml build
elif [ -f 'docker-compose.yml' ]; then # Меняем имя контейнера
docker-compose -f docker-compose.yml build sed -i 's/container_name: no-copy-frontend\$/container_name: no-copy-frontend-${params.PORT}/' docker-compose.yml
else
echo 'Exception: docker-compose file not found' # Меняем порт (2998 на нужный порт)
exit 1 sed -i 's/\\\"2998:2999\\\"/\\\"${params.PORT}:2999\\\"/' docker-compose.yml
fi
echo 'Проверяем изменения:'
grep -n 'container_name\\|ports' docker-compose.yml
echo '=== Строим образ ==='
docker-compose -f docker-compose.yml -p frontend-${params.PORT} build
" "
""" """
} }
@@ -118,21 +128,23 @@ pipeline {
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
cd /opt/deployments/frontend/${params.BRANCH} cd /opt/deployments/frontend/${params.BRANCH}-${params.PORT}
# Проверяем и подключаем к сети app-network если нужно echo '=== Запускаем контейнер ==='
docker network inspect app-network 2>/dev/null || echo 'Warning: app-network not found' docker-compose -f docker-compose.yml -p frontend-${params.PORT} up -d
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml up -d
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml up -d
fi
sleep 10 sleep 10
echo 'Status container:' echo '=== Проверяем запуск ==='
docker ps --filter name=no-copy-frontend if docker ps --format '{{.Names}}' | grep -q 'no-copy-frontend-${params.PORT}'; then
echo '✓ Контейнер запущен: no-copy-frontend-${params.PORT}'
echo '✓ Порт: ${params.PORT}:2999'
else
echo '✗ Контейнер не запустился'
echo 'Логи:'
docker-compose -f docker-compose.yml -p frontend-${params.PORT} logs
exit 1
fi
" "
""" """
} }
@@ -152,15 +164,21 @@ pipeline {
]) { ]) {
sh """ sh """
sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 " sshpass -p '$SSH_PASS' ssh $SSH_USER@92.242.61.23 "
if docker ps --format '{{.Names}}' | grep -q 'no-copy-frontend'; then # Ищем контейнер с правильным именем (с портом)
echo 'Container started' if docker ps --format '{{.Names}}' | grep -q 'no-copy-frontend-${params.PORT}'; then
echo 'Container started: no-copy-frontend-${params.PORT}'
sleep 5 sleep 5
HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2998 || echo '000') HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:${params.PORT} || echo '000')
echo 'HTTP code: \$HTTP_CODE' echo 'HTTP code: \$HTTP_CODE'
echo 'Frontend available on http://92.242.61.23:2998' echo 'Frontend available on http://92.242.61.23:${params.PORT}'
# Показываем все фронтенды
echo ''
echo 'All frontend containers:'
docker ps --filter name=no-copy-frontend --format 'table {{.Names}}\\t{{.Ports}}\\t{{.Status}}'
else else
echo 'Exception: container did not start' echo 'Exception: container did not start'
docker ps -a | grep frontend docker ps -a | grep frontend
@@ -177,7 +195,8 @@ pipeline {
post { post {
success { success {
echo "Front branch ${params.BRANCH} deployment completed" echo "Front branch ${params.BRANCH} deployment completed"
echo "Frontend available on http://92.242.61.23:2998" echo "Frontend available on http://92.242.61.23:${params.PORT}"
echo "Container: no-copy-frontend-${params.PORT}"
} }
failure { failure {
echo "Deployment failed" echo "Deployment failed"
@@ -1,183 +0,0 @@
pipeline {
agent any
parameters {
string(
name: 'BRANCH',
defaultValue: 'main',
description: 'Ветка для деплоя'
)
}
stages {
stage('Git pull') {
steps {
script {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'https://code.3err0.ru/frontdev/no-copy-frontend.git',
credentialsId: 'nx-jen'
]]
])
}
}
}
stage('Stop old') {
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 "
docker stop no-copy-frontend 2>/dev/null || true
docker rm no-copy-frontend 2>/dev/null || true
cd /opt/deployments/frontend/${params.BRANCH} 2>/dev/null || mkdir -p /opt/deployments/frontend/${params.BRANCH}
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml down 2>/dev/null || true
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml down 2>/dev/null || true
fi
"
"""
}
}
}
}
stage('Copy to server') {
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 "
rm -rf /opt/deployments/frontend/${params.BRANCH}/*
mkdir -p /opt/deployments/frontend/${params.BRANCH}
"
sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/frontend/${params.BRANCH}/
"""
}
}
}
}
stage('Build on server') {
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 "
cd /opt/deployments/frontend/${params.BRANCH}
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml build
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml build
else
echo 'Exception:don't found docker-compose'
exit 1
fi
"
"""
}
}
}
}
stage('Start on server') {
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 "
cd /opt/deployments/frontend/${params.BRANCH}
if [ -f 'docker-compose.yaml' ]; then
docker-compose -f docker-compose.yaml up -d
elif [ -f 'docker-compose.yml' ]; then
docker-compose -f docker-compose.yml up -d
fi
sleep 10
echo 'Status container:'
docker ps --filter name=no-copy-frontend
"
"""
}
}
}
}
stage('Check status') {
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 "
if docker ps --format '{{.Names}}' | grep -q 'no-copy-frontend'; then
echo 'Container start'
sleep 5
HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:2998 || echo '000')
echo 'HTTP код: ' \$HTTP_CODE
echo 'Frong start on http://92.242.61.23:2998'
else
echo 'Exception: container don't start'
docker ps -a | grep frontend
exit 1
fi
"
"""
}
}
}
}
}
post {
success {
echo "Front branch ${params.BRANCH} completed"
echo "Use on http://92.242.61.23:2998"
}
failure {
echo "Failed"
}
}
}
+156
View File
@@ -0,0 +1,156 @@
pipeline {
agent any
parameters {
string(
name: 'BRANCH',
defaultValue: 'main',
description: 'Ветка для деплоя легальной панели'
)
string(
name: 'PORT',
defaultValue: '2997',
description: 'Порт для запуска экземпляра'
)
}
stages {
stage('Git pull') {
steps {
script {
checkout([
$class: 'GitSCM',
branches: [[name: params.BRANCH]],
userRemoteConfigs: [[
url: 'https://code.3err0.ru/frontdev/no-copy-legal-panel-frontend.git',
credentialsId: 'nx-jen'
]]
])
}
}
}
stage('Stop old') {
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 "
docker stop no-copy-legal-panel-${params.PORT} 2>/dev/null || true
docker rm no-copy-legal-panel-${params.PORT} 2>/dev/null || true
mkdir -p /opt/deployments/legal-panel/${params.BRANCH}-${params.PORT}
rm -rf /opt/deployments/legal-panel/${params.BRANCH}-${params.PORT}/*
"
"""
}
}
}
}
stage('Copy to server') {
steps {
script {
withCredentials([
usernamePassword(
credentialsId: 'server-root-password',
usernameVariable: 'SSH_USER',
passwordVariable: 'SSH_PASS'
)
]) {
sh """
sshpass -p '$SSH_PASS' scp -r ./* $SSH_USER@92.242.61.23:/opt/deployments/legal-panel/${params.BRANCH}-${params.PORT}/
"""
}
}
}
}
stage('Build on server') {
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 "
cd /opt/deployments/legal-panel/${params.BRANCH}-${params.PORT}
docker build -t no-copy-legal-panel:${params.BRANCH}-${params.PORT} .
"
"""
}
}
}
}
stage('Start on server') {
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 "
docker run -d \\
--name no-copy-legal-panel-${params.PORT} \\
--restart unless-stopped \\
--network app-network \\
-p ${params.PORT}:2997 \\
no-copy-legal-panel:${params.BRANCH}-${params.PORT}
sleep 5
docker ps --filter name=no-copy-legal-panel-${params.PORT}
"
"""
}
}
}
}
stage('Check status') {
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 "
if docker ps --format '{{.Names}}' | grep -q 'no-copy-legal-panel-${params.PORT}'; then
HTTP_CODE=\$(curl -s -o /dev/null -w '%{http_code}' http://localhost:${params.PORT} 2>/dev/null || echo '000')
echo 'Legal panel available on http://92.242.61.23:${params.PORT}'
else
exit 1
fi
"
"""
}
}
}
}
}
post {
success {
echo "Legal panel ${params.BRANCH} deployed on port ${params.PORT}"
}
failure {
echo "Deployment failed"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
package ru.soune
interface Referral {
val userId: String
val referralLink: String
val inviter: String?
val currentLevel: String
val totalIncome: Int
val availableIncome: Int
val holdBalance: Int
val active: Boolean
}
@@ -0,0 +1,7 @@
package ru.soune
data class ReferralInvitee(
val email: String,
val isActive: Boolean,
val regDate: String,
)
+10
View File
@@ -0,0 +1,10 @@
package ru.soune
interface ReferralLevel {
val id: String
val name: String
val share: Int
val minInvitee: Int
val maxInvitee: Int
val next: String?
}
@@ -0,0 +1,11 @@
package ru.soune
interface ReferralLevelProvider {
fun getAvailableReferralLevels(): List<ReferralLevel>
fun getInitialReferralLevelId(): String
fun getReferralLevelById(levelId: String): ReferralLevel
}
+33
View File
@@ -0,0 +1,33 @@
package ru.soune
interface ReferralRepo {
fun getUserReferralLink(userId: String): String
fun getUserIdByLink(link: String): String
fun getInviterIdForUser(userId: String): String?
fun getReferralLevelForUser(userId: String): String
fun getReferralByUserId(userId: String): Referral
fun getReferralInvitees(userId: String, pageSize: Int, pageNumber: Int): List<ReferralInvitee>
// создаем новую запись в БД
fun createReferralEntity(entity: Referral)
// прибавить к totalIncome и availableIncome значение transferAmount
fun increaseIncome(userId: String, transferAmount: Int)
// установить active в true
fun activateUser(userId: String): Boolean
// посчитать записи, у которых inviter == userId и active == true
fun getActiveInviteeForUser(userId: String): Int
// посчитать записи, у которых inviter == userId
fun getTotalInviteeForUser(userId: String): Int
fun upgradeReferralLevelForUser(userId: String, newLevelId: String)
}
@@ -0,0 +1,67 @@
package ru.soune
class ReferralService(
private val referralLevelProvider: ReferralLevelProvider,
private val referralRepo: ReferralRepo,
) {
fun onRegister(userId: String, inviterReferralLink: String?) {
val initialReferralLevel = referralLevelProvider.getInitialReferralLevelId()
val referralLink = referralRepo.getUserReferralLink(userId)
val inviter = inviterReferralLink?.let(referralRepo::getUserIdByLink)
val referral = object : Referral {
override val userId = userId
override val referralLink = referralLink
override val inviter = inviter
override val currentLevel = initialReferralLevel
override val totalIncome = 0
override val availableIncome = 0
override val holdBalance: Int = 0
override val active: Boolean = false
}
referralRepo.createReferralEntity(referral)
}
fun onUserAccountRefill(userId: String, transferAmount: Int) {
val wasActivated = referralRepo.activateUser(userId)
val inviterId = referralRepo.getInviterIdForUser(userId) ?: return
val currentInviterLevel = referralRepo.getReferralLevelForUser(inviterId)
.let { referralLevelProvider.getReferralLevelById(it) }
val reward = transferAmount * currentInviterLevel.share / 100
referralRepo.increaseIncome(inviterId, reward)
if (!wasActivated) return
val currentCount = referralRepo.getActiveInviteeForUser(inviterId)
val nextLevel = currentInviterLevel.next
if (currentCount + 1 > currentInviterLevel.maxInvitee && nextLevel != null) {
referralRepo.upgradeReferralLevelForUser(inviterId, nextLevel)
}
}
fun getReferralLevels(): List<ReferralLevel> =
referralLevelProvider.getAvailableReferralLevels()
fun getReferralLevelForUser(userId: String): ReferralLevel =
referralRepo.getReferralLevelForUser(userId)
.let { referralLevelProvider.getReferralLevelById(it) }
fun getReferralStatForUser(userId: String): ReferralStat {
var referral = referralRepo.getReferralByUserId(userId)
return ReferralStat(
totalInvitee = referralRepo.getTotalInviteeForUser(userId),
activeInvitee = referralRepo.getActiveInviteeForUser(userId),
totalIncome = referral.totalIncome,
availableIncome = referral.availableIncome,
holdBalance = referral.holdBalance,
)
}
fun getInviteeForUser(userId: String, pageSize: Int, pageNumber: Int): List<ReferralInvitee> =
referralRepo.getReferralInvitees(userId, pageSize, pageNumber)
}
+9
View File
@@ -0,0 +1,9 @@
package ru.soune
data class ReferralStat(
val totalInvitee: Int,
val activeInvitee: Int,
val totalIncome: Int,
val availableIncome: Int,
val holdBalance: Int,
)
+1
View File
@@ -3,4 +3,5 @@ plugins {
} }
rootProject.name = 'no-copy' rootProject.name = 'no-copy'
include 'referral' include 'referral'
include 'finance'
@@ -19,7 +19,10 @@ public class HandlerConfig {
LogoutRequestHandler logoutHandler, LogoutRequestHandler logoutHandler,
ImageFoundRequestHandler imageFoundRequestHandler, ImageFoundRequestHandler imageFoundRequestHandler,
VerifyRegisterUserHandler verifyRegisterUser, VerifyRegisterUserHandler verifyRegisterUser,
AuthRequestHandler authRequestHandler AuthRequestHandler authRequestHandler,
CompanyHandler companyHandler,
TariffHandler tariffHandler,
TariffInfoHandler tariffInfoHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -30,6 +33,9 @@ public class HandlerConfig {
map.put(20007, imageFoundRequestHandler); map.put(20007, imageFoundRequestHandler);
map.put(20008, authRequestHandler); map.put(20008, authRequestHandler);
map.put(20009, verifyRegisterUser); map.put(20009, verifyRegisterUser);
map.put(30000, companyHandler);
map.put(30001, tariffHandler);
map.put(30002, tariffInfoHandler);
return map; return map;
} }
@@ -0,0 +1,25 @@
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));
}
}
@@ -5,8 +5,7 @@ import com.vrt.fileprotection.FileProtector;
import com.vrt.fileprotection.NoCopyCheckResult; import com.vrt.fileprotection.NoCopyCheckResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.Resource; import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.UrlResource;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault; import org.springframework.data.web.PageableDefault;
@@ -26,10 +25,13 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.*; import ru.soune.nocopy.handler.*;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.file.ProtectionsLimitService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
@@ -67,6 +69,10 @@ public class ApiController {
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final ProtectionsLimitService protectionsLimitService;
private final AuthTokenRepository authTokenRepository;
@PostMapping("/v{version}/data") @PostMapping("/v{version}/data")
public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request, public ResponseEntity<?> handlePostRequest(@RequestBody BaseRequest request,
@PathVariable("version") int version) { @PathVariable("version") int version) {
@@ -285,7 +291,6 @@ public class ApiController {
} }
} }
@GetMapping("/v{version}/files/download/{fileId}") @GetMapping("/v{version}/files/download/{fileId}")
public ResponseEntity<?> downloadFile( public ResponseEntity<?> downloadFile(
@PathVariable(required = false) String fileId, @PathVariable(required = false) String fileId,
@@ -337,8 +342,11 @@ public class ApiController {
errorData)); errorData));
} }
Path filePath = Paths.get(entityResponse.getFilePath());
Resource resource = new UrlResource(filePath.toUri()); Path filePath = Paths.get(entityResponse.getProtectedFilePath());
// Resource resource = new UrlResource(filePath.toUri());
FileSystemResource resource = new FileSystemResource(filePath);
long fileSize = Files.size(filePath);
if (!resource.exists()) { if (!resource.exists()) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
@@ -353,10 +361,10 @@ public class ApiController {
String contentType = determineContentType(filePath); String contentType = determineContentType(filePath);
return ResponseEntity.ok() return ResponseEntity.ok()
.contentLength(fileSize)
.contentType(MediaType.parseMediaType(contentType)) .contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, .header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"") "attachment; filename=\"" + entityResponse.getOriginalFileName() + "\"")
.header(HttpHeaders.CONTENT_LENGTH, String.valueOf(entityResponse.getFileSize()))
.body(resource); .body(resource);
} catch (FileEntityNotFoundException e) { } catch (FileEntityNotFoundException e) {
Map<String, Object> errorData = new HashMap<>(); Map<String, Object> errorData = new HashMap<>();
@@ -401,14 +409,14 @@ public class ApiController {
public ResponseEntity<?> check(@PathVariable(required = false) String fileId, public ResponseEntity<?> check(@PathVariable(required = false) String fileId,
@PathVariable(required = false) String type) { @PathVariable(required = false) String type) {
Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileId); Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileId);
if (!optionalFileEntity.isPresent()) { if (!optionalFileEntity.isPresent()) {
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
} }
FileEntity fileEntity = optionalFileEntity.get(); FileEntity fileEntity = optionalFileEntity.get();
Path path = type.toLowerCase().equals("image") ? Paths.get(fileEntity.getFilePath()) : Path path = Paths.get(fileEntity.getProtectedFilePath());
Paths.get(fileEntity.getProtectedFilePath());
File file = path.toFile(); File file = path.toFile();
@@ -418,21 +426,17 @@ public class ApiController {
return ResponseEntity.ok().body(noCopyCheckResult); return ResponseEntity.ok().body(noCopyCheckResult);
} }
private ResponseEntity<BaseResponse> checkForDuplicates(String uploadId) throws IOException { @GetMapping("/check/file_stats")
Optional<FileEntity> uploadedFile = fileEntityRepository.findByUploadSessionId(uploadId); public ResponseEntity<?> checkFileProtectStats(@RequestHeader("Authorization") String tokenHeader) {
String token = tokenHeader.replace("Bearer ", "");
if (uploadedFile.isEmpty() || !uploadedFile.get().getMimeType().equals("image")) { Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
return null;
}
FileEntity fileEntity = uploadedFile.get(); AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found"));
List<SimilarFileDTO> similarFiles = fileSimilarityService.findSimilarFiles(fileEntity.getId());
if (hasDuplicate(similarFiles)) { Map<String, Object> fileTypeStats = protectionsLimitService.getFileTypeStats(authToken.getUser().getId());
return handleDuplicate(fileEntity, similarFiles);
}
return null; return ResponseEntity.ok().body(fileTypeStats);
} }
private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) { private boolean hasDuplicate(List<SimilarFileDTO> similarFiles) {
@@ -0,0 +1,92 @@
package ru.soune.nocopy.controller;
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.web.bind.annotation.*;
import ru.soune.nocopy.dto.file.CheckStatus;
import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.entity.user.User;
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 java.io.IOException;
@RestController
@RequestMapping("/api/files")
@Slf4j
public class FileController {
@Autowired
private FileStorageService fileStorageService;
@Autowired
private FileEntityRepository fileRepository;
@Autowired
private CheckCounterService checkCounterService;
@Autowired
private UserRepository userRepository;
@GetMapping("/public/{fileId}")
public ResponseEntity<Resource> getPublicFile(
@PathVariable String fileId) throws IOException {
FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found"));
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getFilePath());
return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.body(resource);
}
@GetMapping("/protected/{fileId}")
public ResponseEntity<Resource> getProtectedFile(
@PathVariable String fileId) throws IOException {
FileEntity fileEntity = fileRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("File not found"));
Resource resource = fileStorageService.loadFileAsResource(fileEntity.getProtectedFilePath());
if (fileEntity.getProtectionStatus() == ProtectionStatus.NOT_PROTECTED ||
fileEntity.getProtectionStatus() == ProtectionStatus.PROCESSING) {
throw new RuntimeException("File is not protected");
}
return ResponseEntity.ok()
.contentType(new MediaType(fileEntity.getMimeType(), fileEntity.getFileExtension()))
.header(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + fileEntity.getOriginalFileName() + "\"")
.body(resource);
}
@GetMapping("check-file/status/{userId}")
public ResponseEntity<CheckStatus> getStatus(@PathVariable Long userId) {
CheckStatus status = checkCounterService.getCurrentStatus(userId);
return ResponseEntity.ok(status);
}
@PostMapping("check-file/update-limit/{userId}/{limit}")
public ResponseEntity<ProtectedFileCheck> getStatus(@PathVariable Long userId, @PathVariable Integer limit) {
User user = userRepository.findById(userId).orElseThrow();
ProtectedFileCheck protectedFileCheck = checkCounterService.updateLimit(user, limit);
return ResponseEntity
.ok()
.body(protectedFileCheck);
}
}
@@ -9,8 +9,11 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.register.ChangePasswordRequest; import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
import ru.soune.nocopy.dto.user.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.user.UserRequest; import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.InvalidUserEmail;
@@ -19,7 +22,9 @@ import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.mapper.UserMapper; import ru.soune.nocopy.mapper.UserMapper;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.file.FileStatsService;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.tariff.TariffService;
import ru.soune.nocopy.service.user.UserService; import ru.soune.nocopy.service.user.UserService;
import java.util.Arrays; import java.util.Arrays;
@@ -44,12 +49,17 @@ public class UserController {
private final AuthService authService; private final AuthService authService;
private final TariffService tariffService;
private final FileStatsService fileStatsService;
@GetMapping("/all") @GetMapping("/all")
public ResponseEntity<List<UserDTO>> getAllUsers() { public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> allUsers = userRepository.findAll().stream() List<UserDTO> allUsers = userRepository.findAll().stream()
.map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.isActive(), .map(u -> new UserDTO(u.getFullName(), u.getCompanyName(), u.getEmail(), u.isActive(),
u.getPhone(), u.getGenderType(), u.getPhone(), u.getGenderType(),
u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType(), tariffService.getAllTariffs(),
null, null))
.toList(); .toList();
return ResponseEntity.ok(allUsers); return ResponseEntity.ok(allUsers);
@@ -62,26 +72,68 @@ public class UserController {
Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token); Optional<AuthToken> tokenOptional = authTokenRepository.findByToken(token);
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); if (tokenOptional.isPresent()) {
if (authToken != null) {
User user = userRepository.findByEmail(email); User user = userRepository.findByEmail(email);
//TODO add mapper
if (user != null) { if (user != null) {
UserDTO userDTO = userMapper.toDTO(user); UserDTO userDTO = userMapper.toDTO(user);
userDTO.setEmail(email); userDTO.setEmail(email);
userDTO.setFullName(user.getFullName()); userDTO.setFullName(user.getFullName());
userDTO.setCompany(user.getCompany());
if (user.getCompany() != null) {
userDTO.setCompany(user.getCompany().getCompanyName());
}
if (user.canManageCompanySettings() || user.canLogin() && user.getCompany() == null) {
userDTO.setTariffs(tariffService.getAllTariffs());
Long fileOnDisk = user.canManageCompanySettings() ?
fileStatsService.calculateCompanyFileSize(user.getId()):
fileStatsService.calculateUserFileSize(user.getId());
Integer filesCount = user.canManageCompanySettings() ?
fileStatsService.filesCompanyCount(user.getId()):
fileStatsService.filesUserCount(user.getId());
TariffInfo personalTariffInfo;
if (user.getCompany() == null) {
personalTariffInfo = user.getPersonalTariffInfo();
} else {
personalTariffInfo = user.getCompany().getTariffInfo();
}
Tariff tariff = personalTariffInfo.getTariff();
TariffInfoDTO infoDTO = TariffInfoDTO.builder().tokens(personalTariffInfo.getTokens())
.id(personalTariffInfo.getId())
.tariffName(tariff.getName())
.status(personalTariffInfo.getStatus().name())
.tariffId(tariff.getId())
.currentFileCounts(filesCount)
.maxFileCounts(tariff.getMaxFilesCount())
.currentFileOnDisk(fileOnDisk)
.maxFileOnDisk(tariff.getDiskSize())
.startTariff(personalTariffInfo.getStartTariff())
.endTariff(personalTariffInfo.getEndTariff())
.build();
userDTO.setTariffInfo(infoDTO);
}
userDTO.setPhone(user.getPhone()); userDTO.setPhone(user.getPhone());
userDTO.setGenderType(user.getGenderType()); userDTO.setGenderType(user.getGenderType());
userDTO.setBirthday(user.getBirthday()); userDTO.setBirthday(user.getBirthday());
userDTO.setCreatedAt(user.getCreatedAt()); userDTO.setCreatedAt(user.getCreatedAt());
userDTO.setSubscriptionType(user.getSubscriptionType()); userDTO.setSubscriptionType(user.getSubscriptionType());
userDTO.setActive(user.isActive()); userDTO.setActive(user.isActive());
userDTO.setPermission(user.getUserPermissions());
return ResponseEntity.ok(userDTO); return ResponseEntity.ok(userDTO);
} }
} else {
return ResponseEntity.notFound().build();
} }
return ResponseEntity.notFound().build(); return ResponseEntity.notFound().build();
@@ -144,10 +196,6 @@ public class UserController {
user.setActive(true); user.setActive(true);
user.setEmailVerified(true); user.setEmailVerified(true);
if (registerRequest.getCompanyName() != null) {
user.setCompany(registerRequest.getCompanyName());
}
if (registerRequest.getPhone() != null) { if (registerRequest.getPhone() != null) {
user.setPhone(registerRequest.getPhone()); user.setPhone(registerRequest.getPhone());
} }
@@ -12,6 +12,7 @@ public enum MessageCode {
DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"),
FILE_DOWNLOAD_ERROR(2, "File download error"), FILE_DOWNLOAD_ERROR(2, "File download error"),
USER_NOT_VERIFIED(2, "User not verified"), USER_NOT_VERIFIED(2, "User not verified"),
PERMISSION_NOT_FOUND(2, "Permission not found"),
USER_NOT_FOUND(2, "User not found"), USER_NOT_FOUND(2, "User not found"),
FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
IMAGE_FOUND_ERROR(2, "Image found error"), IMAGE_FOUND_ERROR(2, "Image found error"),
@@ -27,7 +28,19 @@ public enum MessageCode {
FILE_NOT_FOUND(4, "File not found"), FILE_NOT_FOUND(4, "File not found"),
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"), AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"),
SEND_EMAIL_EXCEPTION(2, "Send email exception"), SEND_EMAIL_EXCEPTION(2, "Send email exception"),
SIMILAR_FILES_FOUND(0, "Similar files found"); SIMILAR_FILES_FOUND(0, "Similar files found"),
FILE_IS_PROTECTED(0, "File is protected"),
FILE_IS_NOT_PROTECTED(0, "File is not protected"),
TARIFF_IS_ADD(0, "Tariff is added"),
TARIFF_IS_DELETED(0, "Tariff is deleted"),
TARIFF_IS_UPDATED(0, "Tariff is updated"),
TARIFF_IS_NOT_FOUND(0, "Tariff is not found"),
VALIDATION_ERROR(2, "Validation error"),
RESOURCE_NOT_FOUND(4, "Resource not found"),
INTERNAL_ERROR(4, "Internal server error"),
COMPANY_NOT_FOUND(4, "Company not found"),
COMPANY_ALREADY_EXISTS(2, "Company already exists"),
ERROR_TARIFF_INFO(2, "Erorr with tariff info");
private final Integer code; private final Integer code;
@@ -0,0 +1,23 @@
package ru.soune.nocopy.dto.company;
import lombok.Data;
import java.util.Map;
@Data
public class CompanyActionRequestDto {
private String action;
private String companyId;
private String companyName;
private Map<String, Object> companyData;
private Long userId;
private Map<String, Object> searchParams;
private PageableParams pageable;
@Data
public static class PageableParams {
private int page;
private int size;
private String sort;
private String direction;
}
}
@@ -0,0 +1,16 @@
package ru.soune.nocopy.dto.company;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class CompanyResponseDto {
private String id;
private String companyName;
private String phone;
private String address;
private String email;
private LocalDateTime registerDate;
private LocalDateTime updateDate;
private long userCount;
}
@@ -0,0 +1,25 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class CheckIncrementResult {
private boolean success;
private Integer countChecked;
private Integer remainingLimit;
private String message;
public static CheckIncrementResult success(Integer countChecked) {
return CheckIncrementResult.builder()
.success(true)
.countChecked(countChecked)
.message("Check count incremented successfully")
.build();
}
}
@@ -0,0 +1,15 @@
package ru.soune.nocopy.dto.file;
import lombok.Builder;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@Builder
public class CheckStatus {
private Long userId;
private Integer countChecked;
private Integer remainingLimit;
private LocalDateTime lastCheckAt;
}
@@ -19,6 +19,7 @@ public class FileEntityResponse {
private String originalFileName; private String originalFileName;
private String storedFileName; private String storedFileName;
private String filePath; private String filePath;
private String protectedFilePath;
private Long fileSize; private Long fileSize;
private String mimeType; private String mimeType;
private String fileExtension; private String fileExtension;
@@ -32,4 +33,11 @@ public class FileEntityResponse {
private boolean existsOnDisk; private boolean existsOnDisk;
private Integer supportId; private Integer supportId;
private String protectStatus; private String protectStatus;
private String ownerName;
private String ownerEmail;
private String ownerCompany;
private String fileName;
private String fileFormat;
private Integer checksCount;
private LocalDateTime fileUploadDate;
} }
@@ -0,0 +1,16 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileTypeStatsDto {
private String fileType;
private Integer count;
}
@@ -0,0 +1,20 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
class PeriodHistoryDto {
private LocalDate periodStart;
private LocalDate periodEnd;
private Integer totalChecks;
private Map<String, Integer> checksByType;
}
@@ -0,0 +1,29 @@
package ru.soune.nocopy.dto.file;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Map;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ProtectionLimitsDto {
private Long userId;
private String userName;
private String userEmail;
private LocalDate lastResetDate;
private Integer totalChecksAllTime;
private Integer periodChecksCount;
private Map<String, Integer> checksByFileType;
private LocalDate periodStartDate;
private LocalDate periodEndDate;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime lastCheckAt;
}
@@ -2,6 +2,9 @@ package ru.soune.nocopy.dto.file;
import lombok.Builder; import lombok.Builder;
import lombok.Value; import lombok.Value;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.time.LocalDateTime;
@Value @Value
@Builder @Builder
@@ -12,4 +15,8 @@ public class SimilarFileDTO {
Integer hammingDistance; Integer hammingDistance;
String similarityLevel; String similarityLevel;
Long ownerId; Long ownerId;
Long supportId;
ProtectionStatus status;
LocalDateTime uploadDate;
String url;
} }
@@ -12,6 +12,15 @@ public class YandexSearchResponse {
@JsonProperty("images") @JsonProperty("images")
private List<ImageResult> images; private List<ImageResult> images;
@JsonProperty("images")
public void setImages(List<ImageResult> images) {
if (images != null && images.size() > 5) {
this.images = images.subList(0, 5);
} else {
this.images = images;
}
}
@Data @Data
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
public static class ImageResult { public static class ImageResult {
@@ -0,0 +1,5 @@
package ru.soune.nocopy.dto.register;
public enum AccountType {
B2B, B2C
}
@@ -13,6 +13,9 @@ public class RegRequest {
private String companyName; private String companyName;
@NotEmpty(message = "Need account type")
private String accountType;
@Size(min = 11, max = 14, message = "Phone must be 11-14 digits") @Size(min = 11, max = 14, message = "Phone must be 11-14 digits")
private String phone; private String phone;
@@ -24,4 +27,6 @@ public class RegRequest {
@NotBlank(message = "Password is required") @NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters") @Size(min = 8, message = "Password must be at least 8 characters")
private String password; private String password;
private String authToken;
} }
@@ -0,0 +1,19 @@
package ru.soune.nocopy.dto.tarriff;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TariffDTO {
private Long id;
private String type;
private String name;
private double price;
private int tokens;
private int maxFilesCount;
private Long diskSize;
private Long maxUsers;
}
@@ -0,0 +1,26 @@
package ru.soune.nocopy.dto.tarriff;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TariffInfoDTO {
private String id;
private String status;
private LocalDateTime startTariff;
private LocalDateTime endTariff;
private Long tariffId;
private String tariffName;
private Integer tokens;
private Long maxFileOnDisk;
private Long currentFileOnDisk;
private Integer currentFileCounts;
private Integer maxFileCounts;
}
@@ -0,0 +1,35 @@
package ru.soune.nocopy.dto.tarriff;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TariffInfoRequest {
private String id;
@NotNull(message = "Action is required")
@JsonProperty("action")
private String action;
@JsonProperty("status")
private String status;
@JsonProperty("start_tariff")
private LocalDateTime startTariff;
@JsonProperty("end_tariff")
private LocalDateTime endTariff;
@JsonProperty("tariff_id")
private Long tariffId;
@JsonProperty("tariff_type")
private String tariffType;
}
@@ -0,0 +1,33 @@
package ru.soune.nocopy.dto.tarriff;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TariffInfoResponse {
@JsonProperty("id")
private String id;
@JsonProperty("status")
private String status;
@JsonProperty("start_tariff")
private LocalDateTime startTariff;
@JsonProperty("end_tariff")
private LocalDateTime endTariff;
@JsonProperty("tariff_id")
private Long tariffId;
@JsonProperty("tariff_name")
private String tariffName;
}
@@ -0,0 +1,49 @@
package ru.soune.nocopy.dto.tarriff;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TariffRequest {
private Long id;
@NotNull
@NotBlank
@JsonProperty("type")
private String type;
@NotNull
@NotBlank
@JsonProperty("tariff_name")
private String name;
@Min(value = 0, message = "Price cannot be negative")
@JsonProperty("tariff_price")
private double price;
@Min(value = 0, message = "Tokens must be at least 0")
@JsonProperty("tariff_tokens")
private int tokens;
@Min(value = 0, message = "Max files count must be at least 0")
@JsonProperty("max_files")
private int maxFilesCount;
@Min(value = 0, message = "Disk size must be at least 0")
@JsonProperty("disk_size")
private int diskSize;
@Min(value = 0, message = "Max users must be at least 0")
@JsonProperty("max_users")
private int maxUsers;
@JsonProperty("action")
private String action;
}
@@ -0,0 +1,12 @@
package ru.soune.nocopy.dto.tarriff;
import lombok.Builder;
import lombok.Data;
import ru.soune.nocopy.entity.tarif.Tariff;
@Data
@Builder
public class TariffResponse {
private String tariffName;
private String tariffType;
}
@@ -4,11 +4,14 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import ru.soune.nocopy.dto.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
import ru.soune.nocopy.entity.user.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.user.SubscriptionType; import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List;
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@@ -24,4 +27,7 @@ public class UserDTO {
private LocalDate birthday; private LocalDate birthday;
private LocalDateTime createdAt; private LocalDateTime createdAt;
private SubscriptionType subscriptionType; private SubscriptionType subscriptionType;
private List<TariffDTO> tariffs;
private TariffInfoDTO tariffInfo;
private Long permission;
} }
@@ -0,0 +1,54 @@
package ru.soune.nocopy.entity.company;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import jakarta.validation.constraints.Size;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
@Table(name = "company")
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "company_name")
private String companyName;
@Column(name = "address")
private String address;
@Column(name = "phone")
private String phone;
@CreatedDate
@Column(name = "register_date", updatable = false, nullable = false)
private LocalDateTime registerDate = LocalDateTime.now();
@Column(name = "update_date")
private LocalDateTime updateDate;
@Size(max = 1024)
@Column(name = "email", length = 1024)
private String email;
@OneToMany(mappedBy = "company")
@JsonIgnore
@ToString.Exclude
private List<User> users = new ArrayList<>();
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "tariff_info_id")
private TariffInfo tariffInfo;
}
@@ -3,13 +3,10 @@ package ru.soune.nocopy.entity.file;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.hibernate.annotations.GenerationTime;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Data @Data
@@ -6,5 +6,6 @@ public enum FileStatus {
PROCESSING, PROCESSING,
VIOLATION, VIOLATION,
CHECKED, CHECKED,
ERROR ERROR,
TEMP
} }
@@ -0,0 +1,40 @@
package ru.soune.nocopy.entity.tarif;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@Table(name = "tariff")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Tariff {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "type", unique = true, nullable = false)
private String type;
@Column(name = "tariff_name")
private String name;
@Column(name = "price")
private double price;
@Column(name = "tokens")
private int tokens;
@Column(name = "max_files_count")
private int maxFilesCount;
@Column(name = "disk_size")
private Long diskSize;
@Column(name = "max_users")
private Long maxUsers;
}
@@ -0,0 +1,37 @@
package ru.soune.nocopy.entity.tarif;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Entity
@Table(name = "tariff_info")
@AllArgsConstructor
@NoArgsConstructor
@Getter @Setter
public class TariffInfo {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private String id;
@Column(name = "status", nullable = false)
private TariffStatus status;
@Column(name = "start_tariff")
private LocalDateTime startTariff;
@Column(name = "end_tariff")
private LocalDateTime endTariff;
@Column(name = "tokens")
private Integer tokens;
@ManyToOne
@JoinColumn(name = "tariff_id")
private Tariff tariff;
}
@@ -0,0 +1,5 @@
package ru.soune.nocopy.entity.tarif;
public enum TariffStatus {
ACTIVE, EXPIRED, INACTIVE
}
@@ -0,0 +1,6 @@
package ru.soune.nocopy.entity.tarif;
public enum TariffType {
START, BASIC, PRO, ENTERPRISE, DEMO;
}
@@ -0,0 +1,30 @@
package ru.soune.nocopy.entity.user;
public class Permission {
public static final long LOGIN_BIT = 1L;
public static final long COMPANY_SETTINGS_BIT = 1L << 1;
public static final long DEFAULT_USER_PERMISSIONS = LOGIN_BIT;
public static final long COMPANY_ADMIN_PERMISSIONS =
LOGIN_BIT | COMPANY_SETTINGS_BIT;
public static final long SUPER_ADMIN_PERMISSIONS = -1L;
public static boolean hasPermission(long userPermissions, long permissionBit) {
return (userPermissions & permissionBit) != 0;
}
public static long addPermission(long userPermissions, long permissionBit) {
return userPermissions | permissionBit;
}
public static long removePermission(long userPermissions, long permissionBit) {
return userPermissions & ~permissionBit;
}
public static boolean canLogin(long userPermissions) {
return hasPermission(userPermissions, LOGIN_BIT);
}
public static boolean canManageCompanySettings(long userPermissions) {
return hasPermission(userPermissions, COMPANY_SETTINGS_BIT);
}
}
@@ -0,0 +1,103 @@
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.HashMap;
import java.util.Map;
@Entity
@Table(name = "protect_check")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@EntityListeners(AuditingEntityListener.class)
public class ProtectedFileCheck {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", unique = true, nullable = false)
private User user;
@Column(nullable = false)
@Builder.Default
private Integer countChecked = 0;
@Column(name = "last_check_at")
private LocalDateTime lastCheckAt;
@Version
private Long version;
@ElementCollection
@CollectionTable(
name = "protection_usage_by_type",
joinColumns = @JoinColumn(name = "protection_usage_id"))
@MapKeyColumn(name = "file_type")
@Column(name = "count")
@Builder.Default
private Map<String, Integer> usageByType = new HashMap<>();
@Column(name = "period_start_date")
@Builder.Default
private LocalDate periodStartDate = LocalDate.now();
@Column(name = "period_count")
@Builder.Default
private Integer periodCount = 0;
public synchronized boolean writeCheck(String fileType) {
resetPeriodIfNeeded();
countChecked++;
periodCount++;
usageByType.merge(fileType, 1, Integer::sum);
lastCheckAt = LocalDateTime.now();
return true;
}
private void resetPeriodIfNeeded() {
LocalDate today = LocalDate.now();
if (periodStartDate == null ||
ChronoUnit.DAYS.between(periodStartDate, today) >= 30) {
cleanupOldData();
periodStartDate = today;
periodCount = 0;
}
}
public void cleanupOldData() {
if (usageByType != null && !usageByType.isEmpty()) {
usageByType.clear();
periodCount = 0;
}
lastCheckAt = LocalDateTime.now();
}
public int getPeriodCount() {
resetPeriodIfNeeded();
return periodCount;
}
public LocalDate getPeriodStartDate() {
resetPeriodIfNeeded();
return periodStartDate;
}
}
@@ -6,7 +6,10 @@ import jakarta.validation.constraints.Size;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; 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.file.Violation;
import ru.soune.nocopy.entity.tarif.TariffInfo;
import ru.soune.nocopy.entity.tarif.TariffStatus;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -37,9 +40,6 @@ public class User {
@Column(name = "email_verified") @Column(name = "email_verified")
private boolean emailVerified = false; private boolean emailVerified = false;
@Column(name = "company")
private String company;
@Column(nullable = false) @Column(nullable = false)
@Size(min = 6) @Size(min = 6)
@JsonIgnore @JsonIgnore
@@ -83,4 +83,67 @@ public class User {
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<Violation> violations = new ArrayList<>();} private List<Violation> violations = new ArrayList<>();
@OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore
private ProtectedFileCheck protectedFileCheck;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "company_id")
@ToString.Exclude
private Company company;
@Column(name = "permissions")
private Long userPermissions = Permission.DEFAULT_USER_PERMISSIONS;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "personal_tariff_info_id")
@ToString.Exclude
private TariffInfo personalTariffInfo;
public TariffInfo getActiveTariffInfo() {
if (company != null && company.getTariffInfo() != null) {
return company.getTariffInfo();
}
return personalTariffInfo;
}
public boolean hasTariffAccess() {
TariffInfo activeTariff = getActiveTariffInfo();
if (activeTariff == null) {
return false;
}
return activeTariff.getStatus() == TariffStatus.ACTIVE &&
LocalDateTime.now().isBefore(activeTariff.getEndTariff());
}
public boolean canLogin() {
return Permission.canLogin(this.userPermissions);
}
public boolean canManageCompanySettings() {
return Permission.canManageCompanySettings(this.userPermissions);
}
public void grantPermission(long permissionBit) {
this.userPermissions = Permission.addPermission(this.userPermissions, permissionBit);
}
public void removePermission(long permissionBit) {
this.userPermissions = Permission.removePermission(this.userPermissions, permissionBit);
}
public boolean hasPermission(long permissionBit) {
return Permission.hasPermission(this.userPermissions, permissionBit);
}
public String getCompanyName() {
if (company != null) {
return company.getCompanyName();
} else {
return null;
}
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,11 @@
package ru.soune.nocopy.exception;
public class RetryableException extends RuntimeException {
public RetryableException(String message) {
super(message);
}
public RetryableException(String message, Throwable cause) {
super(message, cause);
}
}
@@ -0,0 +1,7 @@
package ru.soune.nocopy.exception;
public class TariffNotFoundException extends RuntimeException {
public TariffNotFoundException(String message) {
super(message);
}
}
@@ -0,0 +1,411 @@
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.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.company.CompanyActionRequestDto;
import ru.soune.nocopy.dto.company.CompanyResponseDto;
import ru.soune.nocopy.entity.company.Company;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.ResourceNotFoundException;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.service.CompanyService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
@RequiredArgsConstructor
public class CompanyHandler implements RequestHandler {
private final CompanyService companyService;
private final RegRequestValidator regRequestValidator;
private final ObjectMapper objectMapper;
@Override
public BaseResponse handle(BaseRequest request) {
try {
CompanyActionRequestDto actionRequest = objectMapper.convertValue(
request.getMessageBody(),
CompanyActionRequestDto.class
);
String action = actionRequest.getAction();
return switch (action) {
case "create" -> handleCreate(actionRequest, request.getMsgId());
case "update" -> handleUpdate(actionRequest, request.getMsgId());
case "delete" -> handleDelete(actionRequest, request.getMsgId());
case "getCompany" -> handleGet(actionRequest, request.getMsgId());
case "getAll" -> handleGetAll(actionRequest, request.getMsgId());
case "addUser" -> handleAddUser(actionRequest, request.getMsgId());
case "removeUser" -> handleRemoveUser(actionRequest, request.getMsgId());
case "getUsers" -> handleGetUsers(actionRequest, request.getMsgId());
case "search" -> handleSearch(actionRequest, request.getMsgId());
case "countUser" -> handleCountUsers(actionRequest, request.getMsgId());
default -> new BaseResponse(
request.getMsgId(),
MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action,
null
);
};
} catch (ValidationException e) {
Map<String, String> errors = e.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(
error -> error.getField(),
error -> error.getDefaultMessage() != null ? error.getDefaultMessage() : "Validation error"
));
return new BaseResponse(
request.getMsgId(),
MessageCode.VALIDATION_ERROR.getCode(),
"Validation failed",
Map.of("errors", errors)
);
} catch (ResourceNotFoundException e) {
return new BaseResponse(
request.getMsgId(),
MessageCode.COMPANY_NOT_FOUND.getCode(),
e.getMessage(),
null
);
} catch (IllegalArgumentException e) {
return new BaseResponse(
request.getMsgId(),
MessageCode.VALIDATION_ERROR.getCode(),
e.getMessage(),
null
);
} catch (Exception e) {
log.error("Error handling company action", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.INTERNAL_ERROR.getCode(),
"Error processing request: " + e.getMessage(),
null
);
}
}
private BaseResponse handleCreate(CompanyActionRequestDto request, Integer msgId) {
if (companyService.companyExistsByName(request.getCompanyName())) {
return new BaseResponse(
msgId,
MessageCode.COMPANY_ALREADY_EXISTS.getCode(),
"Company with this name already exists",
null
);
}
validateCompanyCreateData(request);
Company company = companyService.addCompany(
request.getCompanyName(),
getStringParam(request, "phone"),
getStringParam(request, "address"),
getStringParam(request, "email")
);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Company created successfully",
mapToResponseDto(company)
);
}
private BaseResponse handleUpdate(CompanyActionRequestDto request, Integer msgId) {
if (request.getCompanyId() == null) {
throw new IllegalArgumentException("Company ID is required for update");
}
Map<String, Object> companyData = request.getCompanyData();
if (companyData == null) {
companyData = new HashMap<>();
}
Company company = companyService.updateCompany(
request.getCompanyId(),
(String) companyData.get("companyName"),
(String) companyData.get("phone"),
(String) companyData.get("address"),
(String) companyData.get("email")
);
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Company updated successfully",
mapToResponseDto(company)
);
}
private BaseResponse handleDelete(CompanyActionRequestDto request, Integer msgId) {
if (request.getCompanyId() == null) {
throw new IllegalArgumentException("Company ID is required for deletion");
}
companyService.deleteCompany(request.getCompanyId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Company deleted successfully",
Map.of("deletedCompanyId", request.getCompanyId())
);
}
private BaseResponse handleGet(CompanyActionRequestDto request, Integer msgId) {
Company company;
if (request.getCompanyId() != null) {
company = companyService.getCompanyById(request.getCompanyId());
} else if (request.getCompanyName() != null) {
company = companyService.getCompanyByName(request.getCompanyName());
} else {
throw new IllegalArgumentException("Either companyId or companyName is required");
}
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Company retrieved successfully",
mapToResponseDto(company)
);
}
private BaseResponse handleGetAll(CompanyActionRequestDto request, Integer msgId) {
List<Company> companies;
if (request.getPageable() != null) {
Pageable pageable = createPageable(request.getPageable());
Page<Company> companyPage = companyService.getAllCompanies(pageable);
List<CompanyResponseDto> companyDtos = companyPage.getContent().stream()
.map(this::mapToResponseDto)
.collect(Collectors.toList());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Companies retrieved successfully",
Map.of(
"companies", companyDtos,
"totalElements", companyPage.getTotalElements(),
"totalPages", companyPage.getTotalPages(),
"currentPage", companyPage.getNumber(),
"pageSize", companyPage.getSize()
)
);
} else {
companies = companyService.getAllCompanies();
List<CompanyResponseDto> companyDtos = companies.stream()
.map(this::mapToResponseDto)
.collect(Collectors.toList());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Companies retrieved successfully",
Map.of("companies", companyDtos)
);
}
}
private BaseResponse handleAddUser(CompanyActionRequestDto request, Integer msgId) {
if (request.getCompanyId() == null || request.getUserId() == null) {
throw new IllegalArgumentException("Company ID and User ID are required");
}
companyService.addUserToCompany(request.getCompanyId(), request.getUserId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"User added to company successfully",
Map.of(
"companyId", request.getCompanyId(),
"userId", request.getUserId()
)
);
}
private BaseResponse handleRemoveUser(CompanyActionRequestDto request, Integer msgId) {
if (request.getUserId() == null) {
throw new IllegalArgumentException("User ID is required");
}
companyService.deleteUserFromCompany(request.getUserId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"User removed from company successfully",
Map.of("userId", request.getUserId())
);
}
private BaseResponse handleGetUsers(CompanyActionRequestDto request, Integer msgId) {
if (request.getCompanyId() == null) {
throw new IllegalArgumentException("Company ID is required");
}
List<User> users = companyService.getCompanyUsers(request.getCompanyId());
List<Map<String, Object>> userDtos = users.stream()
.map(user -> {
Map<String, Object> map = new HashMap<>();
map.put("id", user.getId());
map.put("email", user.getEmail());
map.put("phone", user.getPhone());
map.put("fullName", user.getFullName());
return map;
}).toList();
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Company users retrieved successfully",
Map.of(
"companyId", request.getCompanyId(),
"users", userDtos,
"count", userDtos.size()
)
);
}
private BaseResponse handleSearch(CompanyActionRequestDto request, Integer msgId) {
String searchTerm = getStringParam(request, "name");
if (searchTerm == null) {
throw new IllegalArgumentException("Search term is required");
}
List<Company> companies = companyService.searchCompaniesByName(searchTerm);
List<CompanyResponseDto> companyDtos = companies.stream()
.map(this::mapToResponseDto)
.collect(Collectors.toList());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"Companies search completed",
Map.of(
"searchTerm", searchTerm,
"companies", companyDtos,
"count", companyDtos.size()
)
);
}
private BaseResponse handleCountUsers(CompanyActionRequestDto request, Integer msgId) {
if (request.getCompanyId() == null) {
throw new IllegalArgumentException("Company ID is required");
}
long count = companyService.countCompanyUsers(request.getCompanyId());
return new BaseResponse(
msgId,
MessageCode.SUCCESS.getCode(),
"User count retrieved",
Map.of(
"companyId", request.getCompanyId(),
"userCount", count
)
);
}
private void validateCompanyCreateData(CompanyActionRequestDto request) throws ValidationException {
BindingResult bindingResult = new BeanPropertyBindingResult(request, "companyRequest");
if (request.getCompanyName() != null) {
regRequestValidator.validateCompanyName(request.getCompanyName(), bindingResult);
}
String email = getStringParam(request, "email");
if (email != null) {
regRequestValidator.validateEmail(email, bindingResult);
}
String phone = getStringParam(request, "phone");
if (phone != null) {
regRequestValidator.validatePhone(phone, bindingResult);
}
if (bindingResult.hasErrors()) {
throw new ValidationException(bindingResult, null);
}
}
private String getStringParam(CompanyActionRequestDto request, String paramName) {
if (request.getCompanyData() != null) {
Object value = request.getCompanyData().get(paramName);
return value != null ? value.toString() : null;
}
Map<String, Object> searchParams = request.getSearchParams();
if (searchParams != null) {
Object value = searchParams.get(paramName);
return value != null ? value.toString() : null;
}
return null;
}
private CompanyResponseDto mapToResponseDto(Company company) {
CompanyResponseDto dto = new CompanyResponseDto();
dto.setId(company.getId());
dto.setCompanyName(company.getCompanyName());
dto.setPhone(company.getPhone());
dto.setAddress(company.getAddress());
dto.setEmail(company.getEmail());
dto.setRegisterDate(company.getRegisterDate());
dto.setUpdateDate(company.getUpdateDate());
try {
dto.setUserCount(companyService.countCompanyUsers(company.getId()));
} catch (Exception e) {
dto.setUserCount(0);
}
return dto;
}
private Pageable createPageable(CompanyActionRequestDto.PageableParams params) {
Sort sort = Sort.unsorted();
if (params.getSort() != null) {
Sort.Direction direction = params.getDirection() != null &&
"desc".equalsIgnoreCase(params.getDirection()) ?
Sort.Direction.DESC : Sort.Direction.ASC;
sort = Sort.by(direction, params.getSort());
}
return PageRequest.of(
params.getPage(),
params.getSize(),
sort
);
}
}
@@ -8,16 +8,17 @@ import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService; import ru.soune.nocopy.service.file.FileStatsService;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional;
@Slf4j @Slf4j
@Component @Component
@@ -34,8 +35,6 @@ public class FileEntityHandler implements RequestHandler {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final ImageHashRepository imageHashRepository;
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
try { try {
@@ -57,6 +56,8 @@ public class FileEntityHandler implements RequestHandler {
return handleSearchFiles(request, fileRequest); return handleSearchFiles(request, fileRequest);
case "storage_usage": case "storage_usage":
return handleGetStorageUsage(request, fileRequest); return handleGetStorageUsage(request, fileRequest);
case "check_protected":
return handleProtectFile(request, fileRequest);
case "delete_file": case "delete_file":
return handleDeleteFile(request, fileRequest); return handleDeleteFile(request, fileRequest);
default: default:
@@ -259,6 +260,29 @@ public class FileEntityHandler implements RequestHandler {
} }
} }
private BaseResponse handleProtectFile(BaseRequest request, FileEntityRequest fileRequest) {
String fileId = fileRequest.getFileId();
Optional<FileEntity> optionalFileEntity = fileEntityRepository.findById(fileId);
if (optionalFileEntity.isEmpty()) {
return new BaseResponse(request.getMsgId(),
MessageCode.FILE_NOT_FOUND.getCode(),
"File not found exception: " + fileRequest.getFileId(),
null);
}
FileEntity fileEntity = optionalFileEntity.get();
ProtectionStatus protectionStatus = fileEntity.getProtectionStatus();
Integer code = protectionStatus == ProtectionStatus.PROTECTED ? MessageCode.FILE_IS_PROTECTED.getCode():
MessageCode.FILE_IS_NOT_PROTECTED.getCode();
String message = protectionStatus == ProtectionStatus.PROTECTED ? MessageCode.FILE_IS_PROTECTED.getDescription():
MessageCode.FILE_IS_NOT_PROTECTED.getDescription();
return new BaseResponse(request.getMsgId(), code, message, fileEntityService.getById(fileId,
request.getVersion()));
}
private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) { private BaseResponse handleDeleteFile(BaseRequest request, FileEntityRequest fileRequest) {
try { try {
Long userId = authService.useUserAuthToken(fileRequest.getToken()); Long userId = authService.useUserAuthToken(fileRequest.getToken());
@@ -285,10 +309,6 @@ public class FileEntityHandler implements RequestHandler {
.build(); .build();
} else { } else {
fileEntityService.softDeleteFileWithHash(fileEntity); fileEntityService.softDeleteFileWithHash(fileEntity);
//
// imageHashRepository.deleteByFileId(fileEntity.getId());
//
// fileEntityService.markAsDeleted(fileEntity);
response = DeleteFileResponse.builder() response = DeleteFileResponse.builder()
.fileId(fileRequest.getFileId()) .fileId(fileRequest.getFileId())
@@ -8,9 +8,17 @@ import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity;
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.SearchApiToYandexResponseMapper;
import ru.soune.nocopy.service.search.YandexSearchService; import ru.soune.nocopy.service.search.YandexSearchService;
import ru.soune.nocopy.service.search.GoogleVisionSearchService; import ru.soune.nocopy.service.search.GoogleVisionSearchService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.util.Map;
@Slf4j @Slf4j
@Component @Component
@@ -23,6 +31,14 @@ public class ImageFoundRequestHandler implements RequestHandler {
private final GoogleVisionSearchService googleVisionSearchService; private final GoogleVisionSearchService googleVisionSearchService;
private final CheckCounterService checkCounterService;
private final FileEntityRepository fileEntityRepository;
private final TariffInfoService tariffInfoService;
private final SearchApiToYandexResponseMapper mapper = new SearchApiToYandexResponseMapper();
@Override @Override
public BaseResponse handle(BaseRequest request) throws Exception { public BaseResponse handle(BaseRequest request) throws Exception {
ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(), ImageSearchRequest imageSearchRequest = objectMapper.convertValue(request.getMessageBody(),
@@ -30,12 +46,25 @@ public class ImageFoundRequestHandler implements RequestHandler {
String fileId = imageSearchRequest.getFileId(); String fileId = imageSearchRequest.getFileId();
YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileId); FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> {
throw new NotValidFieldException("File not found", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId",fileId)));
});
// YandexSearchResponse response = yandexSearchService.searchByFileEntity(fileEntity);
//TODO uncommited when add billing //TODO uncommited when add billing
// GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId); // GoogleVisionSearchResponse googleVisionSearchResponse = googleVisionSearchService.searchByFileEntity(fileId);
String yandexReverseSearchResponse = yandexSearchService.searchReverseByPublicUrl(fileEntity);
tariffInfoService.writeOffTokens(fileEntity.getUserId(), TariffConstants.TOKEN_VALUE_FOR_SEARCH);
checkCounterService.incrementCheckCount(fileEntity.getUserId(), fileEntity.getMimeType());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), response); MessageCode.SUCCESS.getDescription(),mapper.mapToYandexResponse(yandexReverseSearchResponse));
} }
} }
@@ -50,6 +50,16 @@ public class LoginRequestHandler implements RequestHandler {
MessageCode.USER_NOT_VERIFIED.getDescription(), loginAnswer); MessageCode.USER_NOT_VERIFIED.getDescription(), loginAnswer);
} }
if (!user.canLogin()) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of(
"login_permission", "not found")));
return new BaseResponse(request.getMsgId(), MessageCode.PERMISSION_NOT_FOUND.getCode(),
MessageCode.PERMISSION_NOT_FOUND.getDescription(), loginAnswer);
}
AuthToken authToken = authService.login(loginRequest); AuthToken authToken = authService.login(loginRequest);
String token = authToken.getToken(); String token = authToken.getToken();
@@ -10,7 +10,6 @@ import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator; import ru.soune.nocopy.handler.validator.RegRequestValidator;
@@ -59,26 +58,29 @@ public class RegRequestHandler implements RequestHandler {
} }
AuthToken authToken = authService.register(regRequest); AuthToken authToken = authService.register(regRequest);
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); // EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
try { // try {
emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); // emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken());
} catch (Exception e) { // } catch (Exception e) {
return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), // return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(),
MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( // MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of(
"message", "not send", // "message", "not send",
"email", regRequest.getEmail() // "email", regRequest.getEmail()
)); // ));
} // }
String token = authToken.getToken(); String token = authToken.getToken();
authService.useUserAuthToken(token); authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer(); RegAnswer regAnswer = new RegAnswer();
regAnswer.setVerified(false);
regAnswer.setActive(false);
regAnswer.setUserId(authToken.getUser().getId()); regAnswer.setUserId(authToken.getUser().getId());
regAnswer.setToken(token);
regAnswer.setVerified(true);
regAnswer.setActive(true);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); MessageCode.SUCCESS.getDescription(), regAnswer);
@@ -0,0 +1,122 @@
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.tarriff.TariffDTO;
import ru.soune.nocopy.dto.tarriff.TariffRequest;
import ru.soune.nocopy.dto.tarriff.TariffResponse;
import ru.soune.nocopy.service.tariff.TariffService;
import java.util.List;
import java.util.Map;
@Component
@Slf4j
@RequiredArgsConstructor
public class TariffHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final TariffService tariffService;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
try {
TariffRequest tariffRequest = objectMapper.convertValue(request.getMessageBody(), TariffRequest.class);
String action = tariffRequest.getAction();
switch (action) {
case "add":
return handleAddTariff(request, tariffRequest);
case "remove":
return handleRemoveTariff(request, tariffRequest);
case "update":
return handleUpdateTariff(request, tariffRequest);
case "get" :
return handleGetTariffs(request, tariffRequest);
}
} catch (Exception e) {
log.error("Error add tariff", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Error processing request: " + e.getMessage(),
null);
}
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), null);
}
private BaseResponse handleAddTariff(BaseRequest request, TariffRequest tariffRequest) {
tariffService.addTariff(tariffRequest.getName(), tariffRequest.getPrice(), tariffRequest.getType(),
tariffRequest.getTokens(), tariffRequest.getDiskSize(), tariffRequest.getMaxUsers(),
tariffRequest.getMaxFilesCount());
TariffResponse tariffResponse = TariffResponse.builder()
.tariffName(tariffRequest.getName())
.tariffType(tariffRequest.getType())
.build();
return new BaseResponse(request.getMsgId(), MessageCode.TARIFF_IS_ADD.getCode(),
MessageCode.TARIFF_IS_ADD.getDescription(),
tariffResponse);
}
private BaseResponse handleRemoveTariff(BaseRequest request, TariffRequest tariffRequest) {
TariffDTO tariff = tariffService.getTariffById(tariffRequest.getId());
if (tariff == null) {
return new BaseResponse(request.getMsgId(), MessageCode.TARIFF_IS_NOT_FOUND.getCode(),
MessageCode.TARIFF_IS_NOT_FOUND.getDescription(),
Map.of("id", tariffRequest.getId()));
}
TariffResponse tariffResponse = TariffResponse.builder()
.tariffName(tariff.getName())
.tariffType(tariff.getType())
.build();
tariffService.deleteTariff(tariffRequest.getId());
return new BaseResponse(request.getMsgId(), MessageCode.TARIFF_IS_DELETED.getCode(),
MessageCode.TARIFF_IS_DELETED.getDescription(),
tariffResponse);
}
private BaseResponse handleUpdateTariff(BaseRequest request, TariffRequest tariffRequest) {
TariffDTO tariff = tariffService.getTariffById(tariffRequest.getId());
if (tariff == null) {
return new BaseResponse(request.getMsgId(), MessageCode.TARIFF_IS_NOT_FOUND.getCode(),
MessageCode.TARIFF_IS_NOT_FOUND.getDescription(),
Map.of("id", tariffRequest.getId()));
}
TariffDTO tariffDTO = tariffService.updateTariff(tariffRequest.getId(), tariff);
TariffResponse tariffResponse = TariffResponse.builder()
.tariffName(tariffDTO.getName())
.tariffType(tariffDTO.getType())
.build();
return new BaseResponse(request.getMsgId(), MessageCode.TARIFF_IS_UPDATED.getCode(),
MessageCode.TARIFF_IS_UPDATED.getDescription(),
tariffResponse);
}
private BaseResponse handleGetTariffs(BaseRequest request, TariffRequest tariffRequest) {
List<TariffDTO> allTariffs = tariffService.getAllTariffs();
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(),
Map.of("tariffs", allTariffs));
}
}
@@ -0,0 +1,317 @@
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.tarriff.TariffInfoDTO;
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.TariffType;
import ru.soune.nocopy.repository.TariffRepository;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@Slf4j
@RequiredArgsConstructor
public class TariffInfoHandler implements RequestHandler {
private final ObjectMapper objectMapper;
private final TariffInfoService tariffInfoService;
private final TariffRepository tariffRepository;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
try {
TariffInfoRequest tariffInfoRequest = objectMapper.convertValue(request.getMessageBody(), TariffInfoRequest.class);
String action = tariffInfoRequest.getAction();
switch (action) {
case "create":
return handleCreateTariffInfo(request, tariffInfoRequest);
case "create_by_type":
return handleCreateTariffInfoByType(request, tariffInfoRequest);
case "update":
return handleUpdateTariffInfo(request, tariffInfoRequest);
case "delete":
return handleDeleteTariffInfo(request, tariffInfoRequest);
case "get_by_id":
return handleGetTariffInfoById(request, tariffInfoRequest);
case "get_all":
return handleGetAllTariffInfos(request, tariffInfoRequest);
default:
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Unknown action: " + action,
null
);
}
} catch (Exception e) {
log.error("Error processing tariff info request", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Error processing request: " + e.getMessage(),
null
);
}
}
private BaseResponse handleCreateTariffInfo(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
Long tariffId = tariffInfoRequest.getTariffId();
Tariff tariff= tariffRepository.findById(tariffId).orElseThrow();
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());
TariffInfoDTO createdTariffInfo = tariffInfoService.createTariffInfo(tariffInfoDTO);
TariffInfoResponse response = TariffInfoResponse.builder()
.id(createdTariffInfo.getId())
.status(createdTariffInfo.getStatus())
.startTariff(createdTariffInfo.getStartTariff())
.endTariff(createdTariffInfo.getEndTariff())
.tariffId(createdTariffInfo.getTariffId())
.tariffName(createdTariffInfo.getTariffName())
.build();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff info created successfully",
response
);
} catch (Exception e) {
log.error("Error creating tariff info", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to create tariff info: " + e.getMessage(),
null
);
}
}
private BaseResponse handleCreateTariffInfoByType(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
if (tariffInfoRequest.getTariffType() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Tariff type is required for create_by_type action",
null
);
}
tariffInfoService.createTariffInfo(
TariffType.valueOf(tariffInfoRequest.getTariffType().toUpperCase())
);
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff info created successfully by type: " + tariffInfoRequest.getTariffType(),
null
);
} catch (IllegalArgumentException e) {
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Invalid tariff type: " + tariffInfoRequest.getTariffType(),
null
);
} catch (Exception e) {
log.error("Error creating tariff info by type", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to create tariff info by type: " + e.getMessage(),
null
);
}
}
private BaseResponse handleUpdateTariffInfo(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
if (tariffInfoRequest.getId() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Tariff info ID is required for update",
null
);
}
Long tariffId = tariffInfoRequest.getTariffId();
Tariff tariff = tariffRepository.findById(tariffId).orElseThrow();
TariffInfoDTO tariffInfoDTO = new TariffInfoDTO(
tariffInfoRequest.getId(),
tariffInfoRequest.getStatus(),
tariffInfoRequest.getStartTariff(),
tariffInfoRequest.getEndTariff(),
tariffInfoRequest.getTariffId(),
tariff.getName(), null, tariff.getDiskSize(), null, null, tariff.getMaxFilesCount()
);
TariffInfoDTO updatedTariffInfo = tariffInfoService.updateTariffInfo(
tariffInfoRequest.getId(),
tariffInfoDTO
);
TariffInfoResponse response = TariffInfoResponse.builder()
.id(updatedTariffInfo.getId())
.status(updatedTariffInfo.getStatus())
.startTariff(updatedTariffInfo.getStartTariff())
.endTariff(updatedTariffInfo.getEndTariff())
.tariffId(updatedTariffInfo.getTariffId())
.tariffName(updatedTariffInfo.getTariffName())
.build();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff info updated successfully",
response
);
} catch (Exception e) {
log.error("Error updating tariff info", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to update tariff info: " + e.getMessage(),
null
);
}
}
private BaseResponse handleDeleteTariffInfo(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
if (tariffInfoRequest.getId() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Tariff info ID is required for delete",
null
);
}
TariffInfoDTO tariffInfo = tariffInfoService.getTariffInfoById(tariffInfoRequest.getId());
tariffInfoService.deleteTariffInfo(tariffInfoRequest.getId());
TariffInfoResponse response = TariffInfoResponse.builder()
.id(tariffInfo.getId())
.status(tariffInfo.getStatus())
.startTariff(tariffInfo.getStartTariff())
.endTariff(tariffInfo.getEndTariff())
.tariffId(tariffInfo.getTariffId())
.tariffName(tariffInfo.getTariffName())
.build();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff info deleted successfully",
response
);
} catch (Exception e) {
log.error("Error deleting tariff info", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to delete tariff info: " + e.getMessage(),
null
);
}
}
private BaseResponse handleGetTariffInfoById(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
if (tariffInfoRequest.getId() == null) {
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Tariff info ID is required",
null
);
}
TariffInfoDTO tariffInfo = tariffInfoService.getTariffInfoById(tariffInfoRequest.getId());
TariffInfoResponse response = TariffInfoResponse.builder()
.id(tariffInfo.getId())
.status(tariffInfo.getStatus())
.startTariff(tariffInfo.getStartTariff())
.endTariff(tariffInfo.getEndTariff())
.tariffId(tariffInfo.getTariffId())
.tariffName(tariffInfo.getTariffName())
.build();
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff info retrieved successfully",
response
);
} catch (Exception e) {
log.error("Error getting tariff info by ID", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to get tariff info: " + e.getMessage(),
null
);
}
}
private BaseResponse handleGetAllTariffInfos(BaseRequest request, TariffInfoRequest tariffInfoRequest) {
try {
List<TariffInfoDTO> allTariffInfos = tariffInfoService.getAllTariffInfos();
List<TariffInfoResponse> responses = allTariffInfos.stream()
.map(info -> TariffInfoResponse.builder()
.id(info.getId())
.status(info.getStatus())
.startTariff(info.getStartTariff())
.endTariff(info.getEndTariff())
.tariffId(info.getTariffId())
.tariffName(info.getTariffName())
.build())
.collect(Collectors.toList());
return new BaseResponse(
request.getMsgId(),
MessageCode.SUCCESS.getCode(),
"Tariff infos retrieved successfully",
Map.of("tariff_infos", responses, "count", responses.size())
);
} catch (Exception e) {
log.error("Error getting all tariff infos", e);
return new BaseResponse(
request.getMsgId(),
MessageCode.ERROR_TARIFF_INFO.getCode(),
"Failed to get tariff infos: " + e.getMessage(),
null
);
}
}
}
@@ -55,7 +55,7 @@ public class RegRequestValidator implements Validator {
} }
} }
private void validateCompanyName(String companyName, Errors errors) { public void validateCompanyName(String companyName, Errors errors) {
if (companyName == null || companyName.trim().isEmpty()) { if (companyName == null || companyName.trim().isEmpty()) {
return; return;
} }
@@ -73,7 +73,7 @@ public class RegRequestValidator implements Validator {
} }
} }
private void validateEmail(String email, Errors errors) { public void validateEmail(String email, Errors errors) {
if (email == null || email.trim().isEmpty()) { if (email == null || email.trim().isEmpty()) {
errors.rejectValue("email", "email.is.empty", "Email must not be empty"); errors.rejectValue("email", "email.is.empty", "Email must not be empty");
return; return;
@@ -94,7 +94,7 @@ public class RegRequestValidator implements Validator {
} }
private void validatePhone(String phone, Errors errors) { public void validatePhone(String phone, Errors errors) {
if (phone == null || phone.trim().isEmpty()) { if (phone == null || phone.trim().isEmpty()) {
errors.rejectValue("phone", "phone.empty", errors.rejectValue("phone", "phone.empty",
"Test phone numbers are not allowed"); "Test phone numbers are not allowed");
@@ -0,0 +1,23 @@
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.stereotype.Repository;
import ru.soune.nocopy.entity.company.Company;
import java.util.List;
import java.util.Optional;
@Repository
public interface CompanyRepository extends JpaRepository<Company, String> {
Optional<Company> findByCompanyName(String companyName);
boolean existsByCompanyName(String companyName);
List<Company> findByCompanyNameContainingIgnoreCase(String name);
Page<Company> findAll(Pageable pageable);
Page<Company> findByCompanyNameContainingIgnoreCase(String name, Pageable pageable);
}
@@ -49,5 +49,5 @@ public interface FileEntityRepository extends JpaRepository<FileEntity, String>
@Query("SELECT f.id FROM FileEntity f WHERE f.filePath = :filePath") @Query("SELECT f.id FROM FileEntity f WHERE f.filePath = :filePath")
String findFileIdByFilePath(@Param("filePath") String filePath); String findFileIdByFilePath(@Param("filePath") String filePath);
long countByUserId(Long userId); List<FileEntity> findFileByUserIdAndStatus(Long userId, FileStatus status);
} }
@@ -17,6 +17,9 @@ public interface ImageSimilarityRepository
f.original_file_name AS originalFileName, f.original_file_name AS originalFileName,
f.file_size AS fileSize, f.file_size AS fileSize,
f.user_id AS userId, f.user_id AS userId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus,
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo h.hash64_lo AS hash64Lo
FROM image_hashes ref FROM image_hashes ref
@@ -37,6 +40,9 @@ public interface ImageSimilarityRepository
f.original_file_name AS originalFileName, f.original_file_name AS originalFileName,
f.file_size AS fileSize, f.file_size AS fileSize,
f.user_id AS userId, f.user_id AS userId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus,
h.hash64_hi AS hash64Hi, h.hash64_hi AS hash64Hi,
h.hash64_lo AS hash64Lo h.hash64_lo AS hash64Lo
FROM image_hashes ref FROM image_hashes ref
@@ -61,7 +67,10 @@ public interface ImageSimilarityRepository
f.user_id AS userId, f.user_id AS userId,
f.original_file_name AS originalFileName, f.original_file_name AS originalFileName,
f.file_size AS fileSize, f.file_size AS fileSize,
f.stored_file_name AS similarFileId f.stored_file_name AS similarFileId,
f.support_id AS supportId,
f.created_at AS uploadDate,
f.protection_status AS protectionStatus
FROM image_hashes h FROM image_hashes h
JOIN file_entities f ON f.id = h.file_id JOIN file_entities f ON f.id = h.file_id
WHERE h.hash64_hi = :hash64Hi WHERE h.hash64_hi = :hash64Hi
@@ -0,0 +1,21 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface ProtectedFileCheckRepository extends JpaRepository<ProtectedFileCheck, Long> {
Optional<ProtectedFileCheck> findByUser_Id(Long userId);
ProtectedFileCheck findByUserId(Long userId);
boolean existsByUser_Id(Long userId);
List<ProtectedFileCheck> findByPeriodStartDateBefore(LocalDate thirtyDaysAgo);
}
@@ -1,5 +1,9 @@
package ru.soune.nocopy.repository; package ru.soune.nocopy.repository;
import ru.soune.nocopy.entity.file.ProtectionStatus;
import java.time.LocalDateTime;
public interface SimilarImageProjection { public interface SimilarImageProjection {
Long getHash64Hi(); Long getHash64Hi();
Long getHash64Lo(); Long getHash64Lo();
@@ -7,4 +11,7 @@ public interface SimilarImageProjection {
Long getUserId(); Long getUserId();
String getOriginalFileName(); String getOriginalFileName();
Long getFileSize(); Long getFileSize();
Long getSupportId();
LocalDateTime getUploadDate();
ProtectionStatus getProtectionStatus();
} }
@@ -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.tarif.TariffInfo;
@Repository
public interface TariffInfoRepository extends JpaRepository<TariffInfo, String> {
}
@@ -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.tarif.Tariff;
import ru.soune.nocopy.entity.tarif.TariffType;
import java.util.List;
import java.util.Optional;
@Repository
public interface TariffRepository extends JpaRepository<Tariff, Long> {
Optional<Tariff> findByType(String type);
List<Tariff> findAllByOrderByPriceAsc();
Tariff findByName(String name);
}
@@ -4,10 +4,12 @@ import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import java.util.Optional; import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email); User findByEmail(String email);
boolean existsByEmail(String email); boolean existsByEmail(String email);
boolean existsByPhone(@Size(min = 11, max = 14) String phone); boolean existsByPhone(@Size(min = 11, max = 14) String phone);
List<User> findByCompanyId(String companyId);
long countByCompanyId(String companyId);
} }
@@ -0,0 +1,198 @@
package ru.soune.nocopy.service;
import lombok.AllArgsConstructor;
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 org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import ru.soune.nocopy.entity.company.Company;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.exception.ResourceNotFoundException;
import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.CompanyRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
@Slf4j
public class CompanyService {
private final CompanyRepository companyRepository;
private final UserRepository userRepository;
private final RegRequestValidator regRequestValidator;
@Transactional(readOnly = true)
public Company getCompanyById(String id) {
return companyRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Company not found with id: " + id));
}
@Transactional(readOnly = true)
public Company getCompanyByName(String companyName) {
return companyRepository.findByCompanyName(companyName)
.orElseThrow(() -> new ResourceNotFoundException("Company not found with name: " + companyName));
}
@Transactional(readOnly = true)
public List<Company> getAllCompanies() {
return companyRepository.findAll();
}
@Transactional(readOnly = true)
public Page<Company> getAllCompanies(Pageable pageable) {
return companyRepository.findAll(pageable);
}
@Transactional(readOnly = true)
public List<Company> searchCompaniesByName(String name) {
return companyRepository.findByCompanyNameContainingIgnoreCase(name);
}
@Transactional
public Company updateCompany(String id, String companyName, String phone, String address, String email) {
Company company = getCompanyById(id);
if (companyName != null) {
company.setCompanyName(companyName);
}
if (phone != null) {
company.setPhone(phone);
}
if (address != null) {
company.setAddress(address);
}
if (email != null) {
company.setEmail(email);
}
company.setUpdateDate(LocalDateTime.now());
validateCompanyData(company);
return companyRepository.save(company);
}
@Transactional
public Company addCompany(String companyName, String phone, String address, String email) {
Company company = new Company();
company.setCompanyName(companyName);
company.setPhone(phone);
company.setAddress(address);
company.setEmail(email);
company.setRegisterDate(LocalDateTime.now());
validateCompanyData(company);
return companyRepository.save(company);
}
@Transactional
public void deleteCompany(String id) {
List<User> users = userRepository.findByCompanyId(id);
users.forEach(user -> {
user.setCompany(null);
userRepository.save(user);
});
companyRepository.deleteById(id);
log.info("Company deleted: {}", id);
}
@Transactional(readOnly = true)
public List<User> getCompanyUsers(String companyId) {
return userRepository.findByCompanyId(companyId);
}
@Transactional(readOnly = true)
public long countCompanyUsers(String companyId) {
return userRepository.countByCompanyId(companyId);
}
@Transactional
public void addUserToCompany(String companyId, Long userId) {
Company company = getCompanyById(companyId);
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));
if (user.getCompany() != null) {
throw new IllegalArgumentException("User already belongs to another company");
}
user.setCompany(company);
userRepository.save(user);
log.info("User {} added to company {}", userId, companyId);
}
@Transactional
public void addUserToCompany(Company company, User user) {
if (user.getCompany() != null) {
throw new IllegalArgumentException("User already belongs to another company");
}
user.setCompany(company);
userRepository.save(user);
log.info("User {} added to company {}", user.getId(), company.getId());
}
@Transactional
public void deleteUserFromCompany(User user) {
user.setCompany(null);
userRepository.save(user);
log.info("User {} removed from company", user.getId());
}
@Transactional
public void deleteUserFromCompany(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId));
user.setCompany(null);
userRepository.save(user);
log.info("User {} removed from company", userId);
}
@Transactional(readOnly = true)
public boolean companyExistsByName(String companyName) {
return companyRepository.existsByCompanyName(companyName);
}
private void validateCompanyData(Company company) {
BindingResult bindingResult = new BeanPropertyBindingResult(company, "company");
if (company.getCompanyName() != null) {
regRequestValidator.validateCompanyName(company.getCompanyName(), bindingResult);
}
if (company.getEmail() != null) {
regRequestValidator.validateEmail(company.getEmail(), bindingResult);
}
if (company.getPhone() != null) {
regRequestValidator.validatePhone(company.getPhone(), bindingResult);
}
if (bindingResult.hasErrors()) {
Map<String, String> fieldErrors = bindingResult.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
fieldError -> fieldError.getDefaultMessage() != null
? fieldError.getDefaultMessage()
: "Validation error"));
throw new ValidationException(bindingResult, null);
}
}
}
@@ -1,7 +1,10 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service;
import com.vrt.fileprotection.image.phash.PHash;
import com.vrt.fileprotection.image.phash.PerceptualHashHelper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
@@ -31,6 +34,9 @@ public class FileSimilarityService {
private final AuthTokenRepository authTokenRepository; private final AuthTokenRepository authTokenRepository;
@Value("${server.baseurl}")
private String baseUrl;
public List<SimilarFileDTO> findSimilarFiles(String fileId) { public List<SimilarFileDTO> findSimilarFiles(String fileId) {
var imageHashEntity = hashRepository.findById(fileId) var imageHashEntity = hashRepository.findById(fileId)
.orElseThrow(() -> new RuntimeException("Hash not found")); .orElseThrow(() -> new RuntimeException("Hash not found"));
@@ -167,11 +173,11 @@ public class FileSimilarityService {
Long hash64Hi, Long hash64Lo) { Long hash64Hi, Long hash64Lo) {
Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi(); Long imageProjectionHash64Hi = similarImageProjection.getHash64Hi();
Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo(); Long similarImageProjectionHash64Lo = similarImageProjection.getHash64Lo();
int hamming = PerceptualHashHelper.INSTANCE.hammingDistance(new PHash(hash64Hi, hash64Lo),
int hamming = fileUtil.hamming64(hash64Hi, hash64Lo, imageProjectionHash64Hi, similarImageProjectionHash64Lo); new PHash(imageProjectionHash64Hi, similarImageProjectionHash64Lo));
String level; String level;
if (hamming <= 5) { if (hamming <= 6) {
level = "DUPLICATE"; level = "DUPLICATE";
} else if (hamming <= 12) { } else if (hamming <= 12) {
level = "SIMILAR"; level = "SIMILAR";
@@ -186,6 +192,10 @@ public class FileSimilarityService {
.fileSize(similarImageProjection.getFileSize()) .fileSize(similarImageProjection.getFileSize())
.hammingDistance(hamming) .hammingDistance(hamming)
.similarityLevel(level) .similarityLevel(level)
.supportId(similarImageProjection.getSupportId())
.uploadDate(similarImageProjection.getUploadDate())
.status(similarImageProjection.getProtectionStatus())
.url(baseUrl + "/api/files/protected/" + similarImageProjection.getId())
.build(); .build();
} }
} }
@@ -24,7 +24,9 @@ public class ImageHashService {
public Map<String, Long> calculateHash(Path imagePath) throws IOException { public Map<String, Long> calculateHash(Path imagePath) throws IOException {
File file = imagePath.toFile(); File file = imagePath.toFile();
PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file); PHash pHash = PerceptualHashHelper.INSTANCE.generateDCTPerceptualHash(file);
Long firstPart = pHash.getFirstPart(); Long firstPart = pHash.getFirstPart();
Long secondPart = pHash.getSecondPart(); Long secondPart = pHash.getSecondPart();
Map<String, Long> hash = Map.of( Map<String, Long> hash = Map.of(
@@ -0,0 +1,79 @@
package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.CheckIncrementResult;
import ru.soune.nocopy.dto.file.CheckStatus;
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.RetryableException;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import ru.soune.nocopy.repository.UserRepository;
@Service
@RequiredArgsConstructor
@Slf4j
public class CheckCounterService {
private final ProtectedFileCheckRepository protectedFileCheckRepository;
private final UserRepository userRepository;
@Transactional
public void incrementCheckCount(Long userId, String mimeType) {
try {
ProtectedFileCheck check = protectedFileCheckRepository.findByUser_Id(userId).orElse(null);
User user = userRepository.findById(userId).orElse(null);
if (check == null) {
check = initProtectCheckLimit(user,0);
}
boolean success = check.writeCheck(mimeType);
if (success) {
protectedFileCheckRepository.save(check);
log.info("Check count incremented for user {}: count={}",
userId, check.getCountChecked());
CheckIncrementResult.success(check.getCountChecked());
}
} catch (ObjectOptimisticLockingFailureException e) {
log.warn("Optimistic lock failure for user {}, retrying...", userId);
throw new RetryableException("Please try again");
}
}
@Transactional(readOnly = true)
public CheckStatus getCurrentStatus(Long userId) {
ProtectedFileCheck check = protectedFileCheckRepository.findByUser_Id(userId)
.orElseThrow(NullPointerException::new);
return CheckStatus.builder()
.userId(userId)
.countChecked(check.getCountChecked())
.lastCheckAt(check.getLastCheckAt())
.build();
}
public ProtectedFileCheck initProtectCheckLimit(User user, Integer checked) {
ProtectedFileCheck check = ProtectedFileCheck.builder()
.user(user)
.countChecked(checked)
.build();
return protectedFileCheckRepository.save(check);
}
@Transactional
public ProtectedFileCheck updateLimit(User user, Integer limit) {
ProtectedFileCheck check = ProtectedFileCheck.builder()
.user(user)
.build();
return protectedFileCheckRepository.save(check);
}
}
@@ -5,13 +5,17 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import java.io.IOException; import java.io.IOException;
import java.nio.file.*; import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant; import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.List;
@Slf4j @Slf4j
@Component @Component
@@ -24,10 +28,14 @@ public class FileCleanupService {
@Value("${file.storage.temp-ttl-hours}") @Value("${file.storage.temp-ttl-hours}")
private int tempTtlHours; private int tempTtlHours;
private final ProtectedFileCheckRepository protectedFileCheckRepository;
@Scheduled(cron = "0 0 3 * * *") @Scheduled(cron = "0 0 3 * * *")
public void cleanupExpiredFiles() { public void cleanupExpiredFiles() {
log.info("Starting cleanup of expired temporary files"); log.info("Starting cleanup of expired temporary files");
cleanupOldPeriods();
Path tempDir = Paths.get(basePath, "temp"); Path tempDir = Paths.get(basePath, "temp");
if (!Files.exists(tempDir)) { if (!Files.exists(tempDir)) {
return; return;
@@ -77,4 +85,17 @@ public class FileCleanupService {
log.error("Error during cleanup", e); log.error("Error during cleanup", e);
} }
} }
private void cleanupOldPeriods() {
LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30);
List<ProtectedFileCheck> checks = protectedFileCheckRepository.findByPeriodStartDateBefore(thirtyDaysAgo);
for (ProtectedFileCheck check : checks) {
check.cleanupOldData();
}
protectedFileCheckRepository.saveAll(checks);
log.info("Cleaned up {} old protection usage records", checks.size());
}
} }
@@ -10,11 +10,12 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.ImageHashRepository;
import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.repository.SimilarImageProjection;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
@@ -39,7 +40,7 @@ public class FileEntityService {
private final FileSimilarityService fileSimilarityService; private final FileSimilarityService fileSimilarityService;
private final ImageHashRepository imageHashRepository; private final UserRepository userRepository;
@Transactional(noRollbackFor = DuplicateImageException.class) @Transactional(noRollbackFor = DuplicateImageException.class)
public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { public FileEntity createFromUploadSession(FileUploadSession session, String checksum) {
@@ -216,7 +217,6 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
@Transactional
public void deleteFromDisk(FileEntity fileEntity) throws IOException { public void deleteFromDisk(FileEntity fileEntity) throws IOException {
Path path = Paths.get(fileEntity.getFilePath()); Path path = Paths.get(fileEntity.getFilePath());
@@ -266,6 +266,14 @@ public class FileEntityService {
fileEntityRepository.save(fileEntity); fileEntityRepository.save(fileEntity);
} }
public void clearTempFiles(long userId) throws IOException {
List<FileEntity> fileByUserIdAndStatus = fileEntityRepository.findFileByUserIdAndStatus(userId, FileStatus.TEMP);
for (FileEntity fileEntity : fileByUserIdAndStatus) {
deleteFromDisk(fileEntity);
}
}
public FileEntity findBySignature(String signature) { public FileEntity findBySignature(String signature) {
return fileEntityRepository.findBySignature(signature); return fileEntityRepository.findBySignature(signature);
} }
@@ -347,13 +355,14 @@ public class FileEntityService {
private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) { private FileEntityResponse convertToResponse(FileEntity fileEntity, int version) {
boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath()); boolean existsOnDisk = checkFileExistsOnDisk(fileEntity.getFilePath());
User user = userRepository.findById(fileEntity.getUserId()).get();
return FileEntityResponse.builder() return FileEntityResponse.builder()
.id(fileEntity.getId()) .id(fileEntity.getId())
.userId(fileEntity.getUserId()) .userId(fileEntity.getUserId())
.originalFileName(fileEntity.getOriginalFileName()) .originalFileName(fileEntity.getOriginalFileName())
.storedFileName(fileEntity.getStoredFileName()) .storedFileName(fileEntity.getStoredFileName())
.filePath(fileEntity.getFilePath()) .filePath(fileEntity.getProtectedFilePath())
.fileSize(fileEntity.getFileSize()) .fileSize(fileEntity.getFileSize())
.mimeType(fileEntity.getMimeType()) .mimeType(fileEntity.getMimeType())
.fileExtension(fileEntity.getFileExtension()) .fileExtension(fileEntity.getFileExtension())
@@ -367,6 +376,14 @@ public class FileEntityService {
.existsOnDisk(existsOnDisk) .existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId()) .supportId(fileEntity.getSupportId())
.protectStatus(fileEntity.getProtectionStatus().toString()) .protectStatus(fileEntity.getProtectionStatus().toString())
.fileName(fileEntity.getOriginalFileName())
.ownerName(user.getFullName())
.ownerEmail(user.getEmail())
.ownerCompany(user.getCompanyName())
//TODO fix after add logic for check for protect file
.checksCount(0)
.fileUploadDate(fileEntity.getCreatedAt())
.protectedFilePath(fileEntity.getProtectedFilePath())
.build(); .build();
} }
@@ -7,15 +7,24 @@ import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.ProtectionStatus;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.CompanyService;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class FileStatsService { public class FileStatsService {
private final FileEntityRepository fileEntityRepository; private final FileEntityRepository fileEntityRepository;
private final CompanyService companyService;
private final UserRepository userRepository;
public FileInfoUserResponse getUserFileStats(Long userId) { public FileInfoUserResponse getUserFileStats(Long userId) {
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId); List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId);
return calculateStats(userFiles); return calculateStats(userFiles);
@@ -49,25 +58,68 @@ public class FileStatsService {
.build(); .build();
} }
public Long calculateUserFileSize(Long userId) {
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId);
return calculateTotalSize(userFiles);
}
public Long calculateCompanyFileSize(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
List<User> companyUsers = companyService.getCompanyUsers(user.getCompany().getId());
List<FileEntity> userFiles = new ArrayList<>();
for (User companyUser : companyUsers) {
userFiles.addAll(fileEntityRepository.findByUserId(companyUser.getId()));
}
return calculateTotalSize(userFiles);
}
public Integer filesUserCount(Long userId) {
List<FileEntity> userFiles = fileEntityRepository.findByUserId(userId);
return calculateTotalCount(userFiles);
}
public Integer filesCompanyCount(Long userId) {
User user = userRepository.findById(userId).orElseThrow();
List<User> companyUsers = companyService.getCompanyUsers(user.getCompany().getId());
List<FileEntity> userFiles = new ArrayList<>();
for (User companyUser : companyUsers) {
userFiles.addAll(fileEntityRepository.findByUserId(companyUser.getId()));
}
return calculateTotalCount(userFiles);
}
private Long protectedUserFiles(List<FileEntity> files) { private Long protectedUserFiles(List<FileEntity> files) {
return files.stream().filter(file -> file.getProtectionStatus() == ProtectionStatus.PROTECTED).count(); return files.stream().filter(file -> file.getProtectionStatus() == ProtectionStatus.PROTECTED).count();
} }
private Long protectedUserFiles(List<FileEntity> files, FileType type) { private Long protectedUserFiles(List<FileEntity> files, FileType type) {
return files.stream().filter(file -> file.getProtectionStatus() == ProtectionStatus.PROTECTED && return files.stream().filter(file -> file.getStatus() != FileStatus.DELETED &&
file.getStatus() != FileStatus.TEMP &&
file.getProtectionStatus() == ProtectionStatus.PROTECTED &&
file.getMimeType().equals(type.getDisplayName())).count(); file.getMimeType().equals(type.getDisplayName())).count();
} }
private Long calculateTotalSize(List<FileEntity> files) { private Long calculateTotalSize(List<FileEntity> files) {
return files.stream() return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED) .filter(file -> file.getStatus() != FileStatus.DELETED && file.getStatus() != FileStatus.TEMP)
.mapToLong(FileEntity::getFileSize) .mapToLong(FileEntity::getFileSize)
.sum(); .sum();
} }
private Integer calculateTotalCount(List<FileEntity> files) { private Integer calculateTotalCount(List<FileEntity> files) {
return (int) files.stream() return (int) files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED) .filter(file -> file.getStatus() != FileStatus.DELETED && file.getStatus() != FileStatus.TEMP)
.count(); .count();
} }
@@ -79,7 +131,7 @@ public class FileStatsService {
private Long calculateMediaSize(List<FileEntity> files, FileType fileType) { private Long calculateMediaSize(List<FileEntity> files, FileType fileType) {
return files.stream() return files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED) .filter(file -> file.getStatus() != FileStatus.DELETED && file.getStatus() != FileStatus.TEMP)
.filter(file -> isFileType(file, fileType)) .filter(file -> isFileType(file, fileType))
.mapToLong(FileEntity::getFileSize) .mapToLong(FileEntity::getFileSize)
.sum(); .sum();
@@ -87,7 +139,7 @@ public class FileStatsService {
private Integer calculateMediaCount(List<FileEntity> files, FileType fileType) { private Integer calculateMediaCount(List<FileEntity> files, FileType fileType) {
return (int) files.stream() return (int) files.stream()
.filter(file -> file.getStatus() != FileStatus.DELETED) .filter(file -> file.getStatus() != FileStatus.DELETED && file.getStatus() != FileStatus.TEMP)
.filter(file -> isFileType(file, fileType)) .filter(file -> isFileType(file, fileType))
.count(); .count();
} }
@@ -0,0 +1,36 @@
package ru.soune.nocopy.service.file;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.nio.file.Path;
import java.nio.file.Paths;
@Service
@Slf4j
public class FileStorageService {
@Autowired
private FileStorageConfig storageConfig;
public Resource loadFileAsResource(String filePath) throws IOException {
Path path = Paths.get(storageConfig.getBasePath()).resolve(filePath).normalize();
Resource resource = new UrlResource(path.toUri());
if (!resource.exists()) {
throw new FileNotFoundException("File not found: " + filePath);
}
if (!resource.isReadable()) {
throw new IOException("File is not readable: " + filePath);
}
return resource;
}
}
@@ -2,6 +2,7 @@ package ru.soune.nocopy.service.file;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import java.io.IOException; import java.io.IOException;
@@ -18,5 +19,5 @@ public interface FileUploadService {
UploadProgressResponse getUploadProgress(String uploadId); UploadProgressResponse getUploadProgress(String uploadId);
void completeFileProcessingAsync(FileUploadSession session); void completeFileProcessingAsync(FileUploadSession session, FileStatus status);
} }
@@ -0,0 +1,46 @@
package ru.soune.nocopy.service.file;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.file.FileTypeStatsDto;
import ru.soune.nocopy.entity.user.ProtectedFileCheck;
import ru.soune.nocopy.repository.ProtectedFileCheckRepository;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
public class ProtectionsLimitService {
private final ProtectedFileCheckRepository protectedFileCheckRepository;
@Transactional(readOnly = true)
public Map<String, Object> getFileTypeStats(Long userId) {
ProtectedFileCheck protection = protectedFileCheckRepository
.findByUserId(userId);
Map<String, Integer> checksByType = protection.getUsageByType();
int totalChecks = protection.getPeriodCount();
if (totalChecks == 0) {
return Map.of();
}
Map<String, Object> collect = checksByType.entrySet().stream()
.collect(Collectors.toMap(
Map.Entry::getKey,
entry -> FileTypeStatsDto.builder()
.fileType(entry.getKey())
.count(entry.getValue())
.build()
));
collect.put("allCountForPeriod", totalChecks);
return collect;
}
}
@@ -12,10 +12,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.file.UploadProgressResponse; import ru.soune.nocopy.dto.file.UploadProgressResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.*;
import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.ChunkSizeExceededException; import ru.soune.nocopy.exception.ChunkSizeExceededException;
import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.DuplicateImageException;
import ru.soune.nocopy.exception.FileUploadException; import ru.soune.nocopy.exception.FileUploadException;
@@ -27,6 +24,8 @@ import ru.soune.nocopy.service.FileSimilarityService;
import ru.soune.nocopy.service.ImageHashService; import ru.soune.nocopy.service.ImageHashService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
import ru.soune.nocopy.service.tariff.TariffConstants;
import ru.soune.nocopy.service.tariff.TariffInfoService;
import ru.soune.nocopy.util.FileUtil; import ru.soune.nocopy.util.FileUtil;
import java.io.*; import java.io.*;
@@ -82,6 +81,8 @@ public class FileUploadServiceImpl implements FileUploadService {
private final FileUtil fileUtil; private final FileUtil fileUtil;
private final TariffInfoService tariffInfoService;
@PostConstruct @PostConstruct
public void init() { public void init() {
try { try {
@@ -218,7 +219,7 @@ public class FileUploadServiceImpl implements FileUploadService {
@Async("fileUploadTaskExecutor") @Async("fileUploadTaskExecutor")
@Transactional @Transactional
@Override @Override
public void completeFileProcessingAsync(FileUploadSession session) { public void completeFileProcessingAsync(FileUploadSession session, FileStatus status) {
try { try {
Path filePath = Paths.get(session.getFilePath()); Path filePath = Paths.get(session.getFilePath());
String checksum = calculateChecksum(filePath); String checksum = calculateChecksum(filePath);
@@ -233,7 +234,8 @@ public class FileUploadServiceImpl implements FileUploadService {
.fileExtension(session.getExtension()) .fileExtension(session.getExtension())
.checksum(checksum) .checksum(checksum)
.uploadSessionId(session.getUploadId()) .uploadSessionId(session.getUploadId())
.status(FileStatus.ACTIVE) .protectionStatus(ProtectionStatus.PROCESSING)
.status(status)
.build(); .build();
FileEntity saved = fileEntityRepository.save(fileEntity); FileEntity saved = fileEntityRepository.save(fileEntity);
@@ -246,7 +248,9 @@ public class FileUploadServiceImpl implements FileUploadService {
cleanupSessionFiles(session); cleanupSessionFiles(session);
if (status != FileStatus.TEMP) {
noCopyFileService.addFile(fileUtil.createFileInfo(fileEntity)); noCopyFileService.addFile(fileUtil.createFileInfo(fileEntity));
}
log.info("File processing completed for session: {}", session.getUploadId()); log.info("File processing completed for session: {}", session.getUploadId());
@@ -319,8 +323,20 @@ public class FileUploadServiceImpl implements FileUploadService {
session.setStatus(UploadStatus.COMPLETED); session.setStatus(UploadStatus.COMPLETED);
session.setFilePath(finalFilePath); session.setFilePath(finalFilePath);
completeFileProcessingAsync(session); FileStatus status = findSimilar == 0 ? FileStatus.ACTIVE: FileStatus.TEMP;
if (status == FileStatus.TEMP) {
fileEntityService.clearTempFiles(session.getUserId());
}
completeFileProcessingAsync(session, status);
if (status != FileStatus.TEMP) {
String fileType = session.getFileType();
tariffInfoService.writeOffTokens(session.getUserId(), protectCost(fileType));
}
} else { } else {
sessionRepository.save(session); sessionRepository.save(session);
} }
@@ -356,13 +372,32 @@ public class FileUploadServiceImpl implements FileUploadService {
} }
} }
private int protectCost(String fileType) {
int cost = 0;
switch (fileType) {
case "video":
cost = TariffConstants.VIDEO_TOKEN_VALUE;
case "image":
cost = TariffConstants.IMAGE_TOKEN_VALUE;
case "audio":
cost = TariffConstants.AUDIO_TOKEN_VALUE;
case "pdf":
cost = TariffConstants.PDF_TOKEN_VALUE;
}
return cost;
}
private String assembleFileSynchronously(FileUploadSession session) throws IOException { private String assembleFileSynchronously(FileUploadSession session) throws IOException {
Path finalFilePath = prepareFinalFile(session); Path finalFilePath = prepareFinalFile(session);
log.info("file path: " + finalFilePath);
validateAllChunksExist(session); validateAllChunksExist(session);
log.info("validateAllChunksExist : " + session);
mergeChunksToFile(session, finalFilePath); mergeChunksToFile(session, finalFilePath);
log.info("mergeChunksToFile: " + session);
validateFinalFile(session, finalFilePath); validateFinalFile(session, finalFilePath);
log.info("finalFilePath: " + finalFilePath);
return finalFilePath.toString(); return finalFilePath.toString();
} }
@@ -50,8 +50,6 @@ public class EmailService {
helper.setText(htmlContent, true); helper.setText(htmlContent, true);
mailSender.send(message); mailSender.send(message);
log.info("Email sent to: {}", user.getEmail());
} }
@Transactional @Transactional
@@ -1,20 +1,29 @@
package ru.soune.nocopy.service.register; package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.register.AccountType;
import ru.soune.nocopy.dto.register.LoginAnswer; import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.dto.register.LoginRequest;
import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import ru.soune.nocopy.entity.company.Company;
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.AuthToken;
import ru.soune.nocopy.entity.user.Permission;
import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; 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.UserRepository;
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.security.SecureRandom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -30,7 +39,13 @@ public class AuthService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final MessageSource messageSource; private final CompanyRepository companyRepository;
private final CheckCounterService checkCounterService;
private final TariffInfoRepository tariffInfoRepository;
private final TariffInfoService tariffInfoService;
private final SecureRandom secureRandom = new SecureRandom(); private final SecureRandom secureRandom = new SecureRandom();
@@ -40,9 +55,17 @@ public class AuthService {
user.setFullName(registerRequest.getFullName()); user.setFullName(registerRequest.getFullName());
user.setEmail(registerRequest.getEmail()); user.setEmail(registerRequest.getEmail());
user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setActive(true);
user.setEmailVerified(true);
if (registerRequest.getCompanyName() != null) { AccountType accountType = AccountType.valueOf(registerRequest.getAccountType().toUpperCase());
user.setCompany(registerRequest.getCompanyName());
if (accountType == AccountType.B2B && registerRequest.getCompanyName() != null) {
createCompanyAccount(registerRequest, user);
} else if (accountType == AccountType.B2C && registerRequest.getCompanyName() == null) {
createIndividualAccount(user);
} else {
createIndividualCompanyAccount(registerRequest, user);
} }
if (registerRequest.getPhone() != null) { if (registerRequest.getPhone() != null) {
@@ -53,6 +76,8 @@ public class AuthService {
AuthToken authToken = genereateAuthToken(savedUser); AuthToken authToken = genereateAuthToken(savedUser);
checkCounterService.initProtectCheckLimit(user, 0);
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
@@ -123,4 +148,48 @@ public class AuthService {
secureRandom.nextBytes(bytes); secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
} }
private void createCompanyAccount(RegRequest request, User user) {
Company company = new Company();
company.setCompanyName(request.getCompanyName());
TariffInfo companyTariff = tariffInfoService.createTariffInfo(TariffType.DEMO);
company.setTariffInfo(companyTariff);
companyRepository.save(company);
tariffInfoService.addTariffInfo(companyTariff);
user.setCompany(company);
user.grantPermission(Permission.COMPANY_ADMIN_PERMISSIONS);
}
private void createIndividualAccount(User user) {
TariffInfo personalTariff = tariffInfoService.createTariffInfo(TariffType.DEMO);
tariffInfoService.addTariffInfo(personalTariff);
user.setPersonalTariffInfo(personalTariff);
user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS);
}
private void createIndividualCompanyAccount(RegRequest request, User user) {
String authToken = request.getAuthToken();
Long userIdByToken = authTokenRepository.findUserIdByToken(authToken);
User companyUser = userRepository.findById(userIdByToken).orElseThrow();
Company company = companyUser.getCompany();
TariffInfo tariffInfo = company.getTariffInfo();
Long maxUsers = tariffInfo.getTariff().getMaxUsers();
long currentCountUsers = userRepository.countByCompanyId(company.getId());
//TODO add exception
if (currentCountUsers < maxUsers) {
throw new RuntimeException();
}
user.setCompany(companyUser.getCompany());
user.grantPermission(Permission.DEFAULT_USER_PERMISSIONS);
}
} }
@@ -0,0 +1,62 @@
package ru.soune.nocopy.service.search;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import ru.soune.nocopy.dto.file.YandexSearchResponse;
import java.util.ArrayList;
import java.util.List;
public class SearchApiToYandexResponseMapper {
private final ObjectMapper objectMapper = new ObjectMapper();
public YandexSearchResponse mapToYandexResponse(String searchApiJson) throws Exception {
JsonNode root = objectMapper.readTree(searchApiJson);
YandexSearchResponse response = new YandexSearchResponse();
List<YandexSearchResponse.ImageResult> imageResults = new ArrayList<>();
JsonNode visualMatches = root.path("visual_matches");
if (visualMatches.isArray()) {
for (JsonNode match : visualMatches) {
YandexSearchResponse.ImageResult result = new YandexSearchResponse.ImageResult();
JsonNode imageNode = match.path("image");
if (!imageNode.isMissingNode()) {
result.setUrl(imageNode.path("link").asText());
result.setWidth(imageNode.path("width").asInt());
result.setHeight(imageNode.path("height").asInt());
}
result.setPageUrl(match.path("link").asText());
result.setPageTitle(match.path("title").asText());
String source = match.path("source").asText();
result.setHost(extractHostFromSource(source));
if (result.getUrl() != null && !result.getUrl().isEmpty()) {
imageResults.add(result);
}
}
}
response.setImages(imageResults);
return response;
}
private String extractHostFromSource(String source) {
if (source == null || source.isEmpty()) {
return "";
}
source = source.replaceFirst("^(https?://)?(www\\.)?", "");
int slashIndex = source.indexOf('/');
if (slashIndex > 0) {
return source.substring(0, slashIndex);
}
return source;
}
}
@@ -4,8 +4,8 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.DeserializationFeature;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import okhttp3.*;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
@@ -13,7 +13,6 @@ import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository;
import java.io.*; import java.io.*;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
@@ -22,15 +21,28 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Base64; import java.util.Base64;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor
public class YandexSearchService { public class YandexSearchService {
private final FileEntityRepository fileEntityRepository;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final OkHttpClient httpClient;
public YandexSearchService() {
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(120, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.callTimeout(180, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.build();
this.objectMapper = new ObjectMapper();
}
@Value("${yandex.api-key}") @Value("${yandex.api-key}")
private String apiKey; private String apiKey;
@@ -40,19 +52,19 @@ public class YandexSearchService {
@Value("${yandex.search-url}") @Value("${yandex.search-url}")
private String searchUrl; private String searchUrl;
@Value("${searchapi.api-key:}")
private String searchApiKey;
@Value("${server.baseurl}")
private String appBaseUrl;
@PostConstruct @PostConstruct
public void init() { public void init() {
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
} }
public YandexSearchResponse searchByFileEntity(String fileId) throws IOException { public YandexSearchResponse searchByFileEntity(FileEntity fileEntity) throws IOException {
FileEntity fileEntity = fileEntityRepository.findById(fileId)
.orElseThrow(() -> {
throw new NotValidFieldException("File not found", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId",fileId)));
});
byte[] fileBytes; byte[] fileBytes;
if (!isImageFile(fileEntity)) { if (!isImageFile(fileEntity)) {
@@ -67,13 +79,70 @@ public class YandexSearchService {
} catch (IOException e) { } catch (IOException e) {
throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007, throw new NotValidFieldException("File not found or cannot read file", new BaseResponse(20007,
MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(), MessageCode.FILE_NOT_FOUND.getCode(), MessageCode.FILE_NOT_FOUND.getDescription(),
Map.of("fileId", fileId, Map.of("fileId", fileEntity.getId(),
"filePath", fileEntity.getFilePath()))); "filePath", fileEntity.getFilePath())));
} }
return callYandexApi(fileBytes); return callYandexApi(fileBytes);
} }
public String searchReverseByPublicUrl(FileEntity fileEntity) throws IOException {
if (!isImageFile(fileEntity)) {
log.error("File not image: {}", fileEntity.getMimeType());
throw new NotValidFieldException("File not image", new BaseResponse(20007,
MessageCode.INVALID_FIELD.getCode(), MessageCode.INVALID_FIELD.getDescription(),
Map.of("file_type", fileEntity.getMimeType())));
}
String publicUrl = String.format("%s/api/files/public/%s", appBaseUrl, fileEntity.getId());
log.info("Searching reverse for image: {}", publicUrl);
return callReverseImageApiByUrl(publicUrl);
}
private String callReverseImageApiByUrl(String imageUrl) throws IOException {
if (searchApiKey == null || searchApiKey.isEmpty()) {
throw new IllegalStateException("SearchAPI key not configured. Set searchapi.api-key property");
}
HttpUrl url = HttpUrl.parse("https://searchapi.io/api/v1/search")
.newBuilder()
.addQueryParameter("engine", "yandex_reverse_image")
.addQueryParameter("api_key", searchApiKey)
.addQueryParameter("url", imageUrl)
.addQueryParameter("wait", "true")
.addQueryParameter("timeout", "30000")
.build();
log.info("Calling Yandex Reverse Image API: {}", url);
Request request = new Request.Builder()
.url(url)
.header("Accept", "application/json")
.header("User-Agent", "Mozilla/5.0")
.build();
try (Response response = httpClient.newCall(request).execute()) {
log.info("Response code: {}", response.code());
log.info("Response headers: {}", response.headers());
if (!response.isSuccessful()) {
String errorBody = response.body() != null ? response.body().string() : "No error body";
log.error("API request failed: {} - {}", response.code(), errorBody);
throw new IOException("API request failed: " + response.code() + " - " + response.message());
}
String responseBody = response.body().string();
log.info("Response body length: {} characters", responseBody.length());
return responseBody;
}
}
private boolean isImageFile(FileEntity fileEntity) { private boolean isImageFile(FileEntity fileEntity) {
String mimeType = fileEntity.getMimeType(); String mimeType = fileEntity.getMimeType();
return mimeType != null && mimeType.startsWith("image"); return mimeType != null && mimeType.startsWith("image");
@@ -0,0 +1,21 @@
package ru.soune.nocopy.service.tariff;
public class TariffConstants {
public static final double DEMO_PRICE = 0;
public static final double BASIC_PRICE = 100;
public static final double START_PRICE = 200;
public static final double PRO_PRICE = 300;
public static final double ENTERPRISE_PRICE = 400;
public static final Integer DEMO_TOKENS = 10;
public static final Integer BASIC_TOKENS = 100;
public static final Integer START_TOKENS = 200;
public static final Integer PRO_TOKENS = 300;
public static final Integer ENTERPRISE_TOKENS = 400;
public static final Integer IMAGE_TOKEN_VALUE = 3;
public static final Integer VIDEO_TOKEN_VALUE = 16;
public static final Integer AUDIO_TOKEN_VALUE = 8;
public static final Integer PDF_TOKEN_VALUE = 6;
public static final Integer TOKEN_VALUE_FOR_SEARCH = 5;
}
@@ -0,0 +1,137 @@
package ru.soune.nocopy.service.tariff;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.tarriff.TariffInfoDTO;
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.tarif.TariffType;
import ru.soune.nocopy.entity.user.User;
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 java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class TariffInfoService {
private TariffInfoRepository tariffInfoRepository;
private TariffRepository tariffRepository;
private UserRepository userRepository;
public TariffInfo createTariffInfo(LocalDateTime startTariff, LocalDateTime endTariff, TariffStatus tariffStatus,
Integer tokens) {
TariffInfo tariffInfo = new TariffInfo();
tariffInfo.setStartTariff(startTariff);
tariffInfo.setStatus(tariffStatus);
tariffInfo.setEndTariff(endTariff);
tariffInfo.setTokens(tokens);
return tariffInfoRepository.save(tariffInfo);
}
@Transactional
public void writeOffTokens(long userId, Integer value) {
User user = userRepository.findById(userId).orElseThrow();
TariffInfo activeTariffInfo;
if (user.getCompany() == null) {
activeTariffInfo = user.getPersonalTariffInfo();
} else {
activeTariffInfo = user.getCompany().getTariffInfo();
}
Integer tokens = activeTariffInfo.getTokens();
if (tokens - value >= 0) {
activeTariffInfo.setTokens(activeTariffInfo.getTokens() - value);
tariffInfoRepository.save(activeTariffInfo);
} else {
throw new TariffNotFoundException("User not have tokens for protect");
}
}
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.setTariff(tariff);
return tariffInfoRepository.save(tariffInfo);
}
public void addTariffInfo(TariffInfo tariffInfo) {
tariffInfoRepository.save(tariffInfo);
}
public List<TariffInfoDTO> getAllTariffInfos() {
return tariffInfoRepository.findAll()
.stream()
.map(this::convertToInfoDTO)
.collect(Collectors.toList());
}
public TariffInfoDTO getTariffInfoById(String id) {
return tariffInfoRepository.findById(id)
.map(this::convertToInfoDTO)
.orElseThrow(() -> new TariffNotFoundException("Tariff info not found"));
}
@Transactional
public TariffInfoDTO createTariffInfo(TariffInfoDTO tariffInfoDTO) {
TariffInfo tariffInfo = new TariffInfo();
updateTariffInfoFromDTO(tariffInfo, tariffInfoDTO);
return convertToInfoDTO(tariffInfoRepository.save(tariffInfo));
}
@Transactional
public TariffInfoDTO updateTariffInfo(String id, TariffInfoDTO tariffInfoDTO) {
TariffInfo tariffInfo = tariffInfoRepository.findById(id)
.orElseThrow(() -> new TariffNotFoundException("Tariff not found"));
updateTariffInfoFromDTO(tariffInfo, tariffInfoDTO);
return convertToInfoDTO(tariffInfoRepository.save(tariffInfo));
}
@Transactional
public void deleteTariffInfo(String id) {
tariffInfoRepository.deleteById(id);
}
private TariffInfoDTO convertToInfoDTO(TariffInfo tariffInfo) {
return new TariffInfoDTO(
tariffInfo.getId(),
tariffInfo.getStatus().name(),
tariffInfo.getStartTariff(),
tariffInfo.getEndTariff(),
tariffInfo.getTariff() != null ? tariffInfo.getTariff().getId() : null,
tariffInfo.getTariff() != null ? tariffInfo.getTariff().getName() : null, tariffInfo.getTokens(),
tariffInfo.getTariff().getDiskSize(), 0L, 0,
tariffInfo.getTariff().getMaxFilesCount());
}
private void updateTariffInfoFromDTO(TariffInfo tariffInfo, TariffInfoDTO dto) {
tariffInfo.setStatus(TariffStatus.valueOf(dto.getStatus()));
tariffInfo.setStartTariff(dto.getStartTariff());
tariffInfo.setEndTariff(dto.getEndTariff());
if (dto.getTariffId() != null) {
Tariff tariff = tariffRepository.findById(dto.getTariffId())
.orElseThrow(() -> new TariffNotFoundException("Tariff not found"));
tariffInfo.setTariff(tariff);
}
}
}
@@ -0,0 +1,152 @@
package ru.soune.nocopy.service.tariff;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
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.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
public class TariffInitializationService {
@Value("${tarif.demo-price}")
private long demoPrice;
@Value("${tarif.demo-tokens}")
private int demoTokens;
@Value("${tarif.demo-user-count}")
private long demoUserCount;
@Value("${tarif.demo-max-files-count}")
private int demoMaxFileCount;
@Value("${tarif.start-price}")
private long startPrice;
@Value("${tarif.start-tokens}")
private int startTokens;
@Value("${tarif.start-max-files-count}")
private int startMaxFileCount;
@Value("${tarif.start-user-count}")
private long startUserCount;
@Value("${tarif.basic-price}")
private long basicPrice;
@Value("${tarif.basic-tokens}")
private int basicTokens;
@Value("${tarif.basic-max-files-count}")
private int basicMaxFileCount;
@Value("${tarif.basic-user-count}")
private long basicUserCount;
@Value("${tarif.pro-price}")
private long proPrice;
@Value("${tarif.pro-tokens}")
private int proTokens;
@Value("${tarif.pro-max-files-count}")
private int proMaxFileCount;
@Value("${tarif.pro-user-count}")
private long proUserCount;
@Value("${tarif.enterprice-price}")
private long enterpricePrice;
@Value("${tarif.enterprice-tokens}")
private int enterpriceTokens;
@Value("${tarif.enterprice-max-files-count}")
private int enterpriceMaxFileCount;
@Value("${tarif.enterprice-user-count}")
private long enterpriceUserCount;
private final TariffRepository tariffRepository;
@EventListener(ApplicationReadyEvent.class)
@Transactional
public void initializeDefaultTariffs() {
List<Tariff> existingTariffs = tariffRepository.findAll();
if (existingTariffs.isEmpty()) {
createDefaultTariffs();
}
}
private void createDefaultTariffs() {
Arrays.stream(TariffType.values())
.forEach(this::createDefaultTariff);
}
private void createDefaultTariff(TariffType type) {
Tariff tariff = new Tariff();
tariff.setType(type.name());
switch (type) {
case DEMO:
tariff.setName("demo");
tariff.setPrice(demoPrice);
tariff.setTokens(demoTokens);
tariff.setMaxFilesCount(demoMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 100);
tariff.setMaxUsers(demoUserCount);
break;
case START:
tariff.setName("start");
tariff.setPrice(startPrice);
tariff.setTokens(startTokens);
tariff.setMaxFilesCount(startMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 500);
tariff.setMaxUsers(startUserCount);
break;
case PRO:
tariff.setName("pro");
tariff.setPrice(proPrice);
tariff.setTokens(proTokens);
tariff.setMaxFilesCount(proMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 10L);
tariff.setMaxUsers(proUserCount);
break;
case ENTERPRISE:
tariff.setName("premium");
tariff.setPrice(enterpricePrice);
tariff.setTokens(enterpriceTokens);
tariff.setMaxFilesCount(enterpriceMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(enterpriceUserCount);
break;
case BASIC:
tariff.setName("basic");
tariff.setPrice(basicPrice);
tariff.setTokens(basicTokens);
tariff.setMaxFilesCount(enterpriceMaxFileCount);
tariff.setDiskSize(1024L * 1024 * 1024 * 50L);
tariff.setMaxUsers(basicUserCount);
break;
}
tariffRepository.save(tariff);
}
}
@@ -0,0 +1,98 @@
package ru.soune.nocopy.service.tariff;
import lombok.AllArgsConstructor;
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.TariffType;
import ru.soune.nocopy.exception.TariffNotFoundException;
import ru.soune.nocopy.repository.TariffRepository;
import java.util.List;
import java.util.stream.Collectors;
@Service
@AllArgsConstructor
public class TariffService {
private final TariffRepository tariffRepository;
public void addTariff(String tariffName, double price, String type, int tokens, long diskSize, long maxUsers,
int maxFilesCount) {
Tariff tariff = new Tariff();
tariff.setName(tariffName);
tariff.setPrice(price);
tariff.setType(type);
tariff.setTokens(tokens);
tariff.setDiskSize(diskSize);
tariff.setMaxUsers(maxUsers);
tariff.setMaxFilesCount(maxFilesCount);
tariffRepository.save(tariff);
}
public List<TariffDTO> getAllTariffs() {
return tariffRepository.findAllByOrderByPriceAsc()
.stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
}
public TariffDTO getTariffById(Long id) {
return tariffRepository.findById(id)
.map(this::convertToDTO)
.orElseThrow(() -> new TariffNotFoundException("Tariff not found"));
}
public TariffDTO getTariffByType(TariffType type) {
return tariffRepository.findByType(type.toString())
.map(this::convertToDTO)
.orElseThrow(() -> new TariffNotFoundException("Tariff not found"));
}
@Transactional
public TariffDTO createTariff(TariffDTO tariffDTO) {
Tariff tariff = new Tariff();
updateTariffFromDTO(tariff, tariffDTO);
return convertToDTO(tariffRepository.save(tariff));
}
@Transactional
public TariffDTO updateTariff(Long id, TariffDTO tariffDTO) {
Tariff tariff = tariffRepository.findById(id)
.orElseThrow(() -> new TariffNotFoundException("Tariff not found"));
updateTariffFromDTO(tariff, tariffDTO);
return convertToDTO(tariffRepository.save(tariff));
}
@Transactional
public void deleteTariff(Long id) {
tariffRepository.deleteById(id);
}
private TariffDTO convertToDTO(Tariff tariff) {
return new TariffDTO(
tariff.getId(),
tariff.getType(),
tariff.getName(),
tariff.getPrice(),
tariff.getTokens(),
tariff.getMaxFilesCount(),
tariff.getDiskSize(),
tariff.getMaxUsers()
);
}
private void updateTariffFromDTO(Tariff tariff, TariffDTO dto) {
tariff.setType(dto.getType());
tariff.setName(dto.getName());
tariff.setPrice(dto.getPrice());
tariff.setTokens(dto.getTokens());
tariff.setMaxFilesCount(dto.getMaxFilesCount());
tariff.setDiskSize(dto.getDiskSize());
tariff.setMaxUsers(dto.getMaxUsers());
}
}
@@ -61,10 +61,6 @@ public class UserService {
user.setFullName(request.getFullName()); user.setFullName(request.getFullName());
} }
if (request.getCompany() != null) {
user.setCompany(request.getCompany());
}
if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) { if (request.getEmail() != null && !request.getEmail().equals(user.getEmail())) {
if (userRepository.existsByEmail(request.getEmail())) { if (userRepository.existsByEmail(request.getEmail())) {
throw new InvalidUserEmail("Email already exists: " + request.getEmail()); throw new InvalidUserEmail("Email already exists: " + request.getEmail());
@@ -87,7 +83,7 @@ public class UserService {
return UserDTO.builder() return UserDTO.builder()
.fullName(user.getFullName()) .fullName(user.getFullName())
.email(user.getEmail()) .email(user.getEmail())
.company(user.getCompany()) .company(user.getCompanyName())
.phone(user.getPhone()) .phone(user.getPhone())
.genderType(user.getGenderType()) .genderType(user.getGenderType())
.birthday(user.getBirthday()) .birthday(user.getBirthday())

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