From 4704f780e5d47391044d41bcd42340e608c7c905 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 16 Jan 2026 20:20:14 +0700 Subject: [PATCH 01/10] NCBACK-33 add start logic for verify by token --- build.gradle | 1 + .../nocopy/controller/ApiController.java | 2 +- .../nocopy/controller/UserController.java | 4 +- .../java/ru/soune/nocopy/dto/UserDTO.java | 2 +- .../java/ru/soune/nocopy/dto/UserRequest.java | 2 +- .../soune/nocopy/entity/ImageProtection.java | 2 + .../ru/soune/nocopy/entity/Violation.java | 1 + .../nocopy/entity/{ => user}/AuthToken.java | 2 +- .../entity/user/EmailVerificationToken.java | 31 +++++ .../nocopy/entity/{ => user}/GenderType.java | 2 +- .../soune/nocopy/entity/{ => user}/User.java | 10 +- .../nocopy/entity/{ => user}/UserAction.java | 2 +- .../nocopy/entity/{ => user}/UserContent.java | 3 +- .../nocopy/handler/FileEntityHandler.java | 2 +- .../nocopy/handler/FileUploadHandler.java | 2 +- .../nocopy/handler/LoginRequestHandler.java | 2 +- .../nocopy/handler/LogoutRequestHandler.java | 4 +- .../nocopy/handler/RegRequestHandler.java | 15 ++- .../nocopy/mapper/UserContentMapper.java | 2 +- .../ru/soune/nocopy/mapper/UserMapper.java | 2 +- .../repository/AuthTokenRepository.java | 2 +- .../EmailVerificationTokenRepository.java | 10 ++ .../repository/UserContentRepository.java | 2 +- .../nocopy/repository/UserRepository.java | 2 +- .../ru/soune/nocopy/service/AuthService.java | 9 +- .../ru/soune/nocopy/service/EmailService.java | 89 ++++++++++++ .../nocopy/service/UserContentService.java | 4 +- .../ru/soune/nocopy/service/UserService.java | 2 +- src/main/resources/application.yaml | 23 +++- .../templates/email-verification.html | 127 ++++++++++++++++++ .../soune/nocopy/service/AuthServiceTest.java | 4 +- 31 files changed, 333 insertions(+), 34 deletions(-) rename src/main/java/ru/soune/nocopy/entity/{ => user}/AuthToken.java (97%) create mode 100644 src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java rename src/main/java/ru/soune/nocopy/entity/{ => user}/GenderType.java (54%) rename src/main/java/ru/soune/nocopy/entity/{ => user}/User.java (89%) rename src/main/java/ru/soune/nocopy/entity/{ => user}/UserAction.java (96%) rename src/main/java/ru/soune/nocopy/entity/{ => user}/UserContent.java (96%) create mode 100644 src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java create mode 100644 src/main/java/ru/soune/nocopy/service/EmailService.java create mode 100644 src/main/resources/templates/email-verification.html diff --git a/build.gradle b/build.gradle index 111da7e..866c470 100644 --- a/build.gradle +++ b/build.gradle @@ -30,6 +30,7 @@ dependencies { implementation 'org.springframework.security:spring-security-crypto:6.5.3' implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'commons-validator:commons-validator:1.7' + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: '4.0.1' implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0' implementation 'tools.jackson.core:jackson-core:3.0.3' diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 4f9239d..8cd26af 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -19,7 +19,7 @@ import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.UploadProgress; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index 39efe8e..77df18a 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -6,8 +6,8 @@ import org.springframework.web.bind.annotation.*; import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.UserRequest; -import ru.soune.nocopy.entity.AuthToken; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.mapper.UserMapper; diff --git a/src/main/java/ru/soune/nocopy/dto/UserDTO.java b/src/main/java/ru/soune/nocopy/dto/UserDTO.java index 45c19ee..e5a6b63 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/UserDTO.java @@ -4,7 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; -import ru.soune.nocopy.entity.GenderType; +import ru.soune.nocopy.entity.user.GenderType; import ru.soune.nocopy.entity.SubscriptionType; import java.time.LocalDate; diff --git a/src/main/java/ru/soune/nocopy/dto/UserRequest.java b/src/main/java/ru/soune/nocopy/dto/UserRequest.java index 91f7379..ca89d88 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/UserRequest.java @@ -5,7 +5,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Size; import lombok.Data; -import ru.soune.nocopy.entity.GenderType; +import ru.soune.nocopy.entity.user.GenderType; import java.time.LocalDate; diff --git a/src/main/java/ru/soune/nocopy/entity/ImageProtection.java b/src/main/java/ru/soune/nocopy/entity/ImageProtection.java index eef9e95..92cdedc 100644 --- a/src/main/java/ru/soune/nocopy/entity/ImageProtection.java +++ b/src/main/java/ru/soune/nocopy/entity/ImageProtection.java @@ -4,6 +4,8 @@ import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.user.UserContent; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/entity/Violation.java b/src/main/java/ru/soune/nocopy/entity/Violation.java index dd504bd..b8a7a1a 100644 --- a/src/main/java/ru/soune/nocopy/entity/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/Violation.java @@ -4,6 +4,7 @@ import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.user.User; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/entity/AuthToken.java b/src/main/java/ru/soune/nocopy/entity/user/AuthToken.java similarity index 97% rename from src/main/java/ru/soune/nocopy/entity/AuthToken.java rename to src/main/java/ru/soune/nocopy/entity/user/AuthToken.java index 63cf34a..aa10036 100644 --- a/src/main/java/ru/soune/nocopy/entity/AuthToken.java +++ b/src/main/java/ru/soune/nocopy/entity/user/AuthToken.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java new file mode 100644 index 0000000..7b009e1 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.entity.user; + +import jakarta.persistence.*; +import lombok.*; +import java.time.LocalDateTime; + +@Entity +@Table(name = "email_verification_tokens") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class EmailVerificationToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(name = "token", nullable = false, length = 6) + private String token; + + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt; + + @Column(name = "created_at", nullable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/ru/soune/nocopy/entity/GenderType.java b/src/main/java/ru/soune/nocopy/entity/user/GenderType.java similarity index 54% rename from src/main/java/ru/soune/nocopy/entity/GenderType.java rename to src/main/java/ru/soune/nocopy/entity/user/GenderType.java index 580d825..6a06e95 100644 --- a/src/main/java/ru/soune/nocopy/entity/GenderType.java +++ b/src/main/java/ru/soune/nocopy/entity/user/GenderType.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; public enum GenderType { MALE, FEMALE diff --git a/src/main/java/ru/soune/nocopy/entity/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java similarity index 89% rename from src/main/java/ru/soune/nocopy/entity/User.java rename to src/main/java/ru/soune/nocopy/entity/user/User.java index 0841027..830bafd 100644 --- a/src/main/java/ru/soune/nocopy/entity/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; import com.fasterxml.jackson.annotation.JsonIgnore; import jakarta.persistence.*; @@ -6,6 +6,9 @@ import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.ImageProtection; +import ru.soune.nocopy.entity.SubscriptionType; +import ru.soune.nocopy.entity.Violation; import java.time.LocalDate; import java.time.LocalDateTime; @@ -33,6 +36,9 @@ public class User { @Column(name = "email", nullable = false, length = 1024, unique = true) private String email; + @Column(name = "email_verified", nullable = false) + private Boolean emailVerified = false; + @Column(name = "company") private String company; @@ -64,7 +70,7 @@ public class User { private LocalDateTime lastLoginAt; @Column(name = "is_active") - private Boolean isActive = true; + private Boolean isActive = false; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore diff --git a/src/main/java/ru/soune/nocopy/entity/UserAction.java b/src/main/java/ru/soune/nocopy/entity/user/UserAction.java similarity index 96% rename from src/main/java/ru/soune/nocopy/entity/UserAction.java rename to src/main/java/ru/soune/nocopy/entity/user/UserAction.java index 661ab3a..9a27e8e 100644 --- a/src/main/java/ru/soune/nocopy/entity/UserAction.java +++ b/src/main/java/ru/soune/nocopy/entity/user/UserAction.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/ru/soune/nocopy/entity/UserContent.java b/src/main/java/ru/soune/nocopy/entity/user/UserContent.java similarity index 96% rename from src/main/java/ru/soune/nocopy/entity/UserContent.java rename to src/main/java/ru/soune/nocopy/entity/user/UserContent.java index 1691926..0472791 100644 --- a/src/main/java/ru/soune/nocopy/entity/UserContent.java +++ b/src/main/java/ru/soune/nocopy/entity/user/UserContent.java @@ -1,9 +1,10 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.ContentStatus; import ru.soune.nocopy.entity.file.FileType; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index a7f9c73..61a1850 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -6,7 +6,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.exception.FileEntityNotFoundException; diff --git a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 4cb33b5..a2036a1 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -10,7 +10,7 @@ import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.exception.*; diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 95abf63..8cc115a 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.AuthService; diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index 44fa253..f4aa19f 100644 --- a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 349f0a7..847c19d 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -7,15 +7,20 @@ import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.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.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.EmailService; +import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; +import java.util.UUID; @Slf4j @Component @@ -29,6 +34,10 @@ public class RegRequestHandler implements RequestHandler { private final ObjectMapper objectMapper; + private final EmailService emailService; + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); @@ -45,6 +54,7 @@ public class RegRequestHandler implements RequestHandler { } BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); + regRequestValidator.validate(regRequest, bindingResult); if (bindingResult.hasErrors()) { @@ -52,6 +62,9 @@ public class RegRequestHandler implements RequestHandler { } AuthToken authToken = authService.register(regRequest); + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); RegAnswer regAnswer = new RegAnswer(); regAnswer.setToken(authToken.getToken()); diff --git a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java index 3959f8f..ed7c734 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java @@ -5,7 +5,7 @@ import org.mapstruct.MappingTarget; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; import ru.soune.nocopy.dto.UserContentUpdateRequest; -import ru.soune.nocopy.entity.UserContent; +import ru.soune.nocopy.entity.user.UserContent; @Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) diff --git a/src/main/java/ru/soune/nocopy/mapper/UserMapper.java b/src/main/java/ru/soune/nocopy/mapper/UserMapper.java index c151480..a6c4de6 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserMapper.java @@ -4,7 +4,7 @@ import org.mapstruct.Mapper; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; import ru.soune.nocopy.dto.UserDTO; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.User; @Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index 74826fd..b5b0d88 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -2,7 +2,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import java.time.LocalDate; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java new file mode 100644 index 0000000..f67573e --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java @@ -0,0 +1,10 @@ +package ru.soune.nocopy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.nocopy.entity.user.EmailVerificationToken; + +import java.util.Optional; + +public interface EmailVerificationTokenRepository extends JpaRepository { + Optional findByToken(String token); +} diff --git a/src/main/java/ru/soune/nocopy/repository/UserContentRepository.java b/src/main/java/ru/soune/nocopy/repository/UserContentRepository.java index 74fcd4b..e6a5de9 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserContentRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserContentRepository.java @@ -3,7 +3,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import ru.soune.nocopy.entity.file.FileType; -import ru.soune.nocopy.entity.UserContent; +import ru.soune.nocopy.entity.user.UserContent; import java.util.List; diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index f674629..af1a3d9 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,7 +2,7 @@ package ru.soune.nocopy.repository; import jakarta.validation.constraints.Size; import org.springframework.data.jpa.repository.JpaRepository; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.User; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/service/AuthService.java b/src/main/java/ru/soune/nocopy/service/AuthService.java index 2377b2b..ad58219 100644 --- a/src/main/java/ru/soune/nocopy/service/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/AuthService.java @@ -6,18 +6,15 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.entity.AuthToken; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; import java.security.SecureRandom; import java.time.LocalDateTime; -import java.util.Arrays; -import java.util.Base64; -import java.util.Map; -import java.util.Optional; +import java.util.*; @Service @RequiredArgsConstructor diff --git a/src/main/java/ru/soune/nocopy/service/EmailService.java b/src/main/java/ru/soune/nocopy/service/EmailService.java new file mode 100644 index 0000000..9a41952 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/EmailService.java @@ -0,0 +1,89 @@ +package ru.soune.nocopy.service; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.io.ClassPathResource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.time.LocalDateTime; + +@Service +@RequiredArgsConstructor +@Slf4j +public class EmailService { + + private final JavaMailSender mailSender; + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + + public void sendVerificationEmail(User user, String code) { + try { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + + helper.setFrom("NO COPY "); + helper.setTo(user.getEmail()); + helper.setSubject("NO COPY - Подтверждение email адреса"); + + String htmlContent = loadAndProcessTemplate(user.getFullName(), code); + + helper.setText(htmlContent, true); + + mailSender.send(message); + + } catch (MessagingException | IOException e) { + log.error("Fail send email: " + e.getMessage()); + } + } + + @Transactional + public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) { + EmailVerificationToken emailToken = new EmailVerificationToken(); + emailToken.setUser(authToken.getUser()); + emailToken.setToken(generateVerificationCode()); + emailToken.setExpiresAt(LocalDateTime.now().plusMinutes(15)); + emailToken.setCreatedAt(LocalDateTime.now()); + + return emailVerificationTokenRepository.save(emailToken); + } + + private String generateVerificationCode() { + SecureRandom random = new SecureRandom(); + int code = 100000 + random.nextInt(900000); + + return String.valueOf(code); + } + + private String loadAndProcessTemplate(String username, String code) throws IOException { + ClassPathResource resource = new ClassPathResource("templates/email-verification.html"); + String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); + String safeUsername = escapeHtml(username != null ? username : "Пользователь"); + String safeCode = escapeHtml(code); + + return template + .replace("{{username}}", safeUsername) + .replace("{{code}}", safeCode); + } + + private String escapeHtml(String text) { + if (text == null) return ""; + return text + .replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'"); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/UserContentService.java b/src/main/java/ru/soune/nocopy/service/UserContentService.java index e113752..3d033e2 100644 --- a/src/main/java/ru/soune/nocopy/service/UserContentService.java +++ b/src/main/java/ru/soune/nocopy/service/UserContentService.java @@ -6,8 +6,8 @@ import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.UserContentRequest; import ru.soune.nocopy.dto.UserContentUpdateRequest; import ru.soune.nocopy.entity.file.FileType; -import ru.soune.nocopy.entity.User; -import ru.soune.nocopy.entity.UserContent; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.entity.user.UserContent; import ru.soune.nocopy.exception.ContentNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.mapper.UserContentMapper; diff --git a/src/main/java/ru/soune/nocopy/service/UserService.java b/src/main/java/ru/soune/nocopy/service/UserService.java index 7c4b79f..c590bb9 100644 --- a/src/main/java/ru/soune/nocopy/service/UserService.java +++ b/src/main/java/ru/soune/nocopy/service/UserService.java @@ -7,7 +7,7 @@ import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.UserRequest; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 9a34782..493b9ce 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,4 +1,16 @@ spring: + mail: + host: smtp.gmail.com + port: 587 + username: vlad.popovtsev@gmail.com + password: aylgtjcxdtjvohrk + properties: + mail: + smtp: + auth: true + starttls: + enable: true + datasource: url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} username: ${POSTGRES_USER} @@ -56,4 +68,13 @@ logging: yandex: api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} - search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} \ No newline at end of file + search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} + +app: + email: + verification: + expiration-hours: ${EMAIL_VERIFICATION_EXPIRATION:24} + base-url: ${EMAIL_BASE_URL:http://localhost:80} + from-email: ${EMAIL_FROM:noreply@yourdomain.com} + templates: + verification: verification-email.html \ No newline at end of file diff --git a/src/main/resources/templates/email-verification.html b/src/main/resources/templates/email-verification.html new file mode 100644 index 0000000..e7450a6 --- /dev/null +++ b/src/main/resources/templates/email-verification.html @@ -0,0 +1,127 @@ + + + + + + Подтверждение email + + + +
+
+ +

Подтверждение email адреса

+
+ +
+

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

+ +

Спасибо за регистрацию на платформе NO COPY. + Для завершения регистрации, пожалуйста, введите код подтверждения:

+ +
+

{{code}}

+

Код подтверждения

+
+ +

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

+ +
+ Важно: Если вы не регистрировались на NO COPY, + просто проигнорируйте это письмо. +
+
+ + +
+ + \ No newline at end of file diff --git a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java index 499d694..2a3a013 100644 --- a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java @@ -9,8 +9,8 @@ import org.mockito.MockitoAnnotations; import org.springframework.context.MessageSource; import org.springframework.security.crypto.password.PasswordEncoder; import ru.soune.nocopy.dto.LoginRequest; -import ru.soune.nocopy.entity.AuthToken; -import ru.soune.nocopy.entity.User; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.AuthTokenRepository; -- 2.54.0 From 4a8e561258c640d702ec17ec3a673ed8e534000c Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 19 Jan 2026 19:26:59 +0700 Subject: [PATCH 02/10] NCBACK-33 add email send --- .env | 11 +- docker-compose.yaml | 219 +++--------------- .../ru/soune/nocopy/entity/user/User.java | 9 +- .../soune/nocopy/entity/user/UserAction.java | 44 ---- .../ru/soune/nocopy/service/EmailService.java | 10 +- src/main/resources/application.yaml | 27 +-- 6 files changed, 62 insertions(+), 258 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/entity/user/UserAction.java diff --git a/.env b/.env index e54b26f..0c81544 100644 --- a/.env +++ b/.env @@ -1,10 +1,19 @@ +# Postgres POSTGRES_DB=no_copy_ POSTGRES_USER=ncp_db POSTGRES_PASSWORD=ncpDbApp POSTGRES_PORT=5432 POSTGRES_HOST=postgres +# Redis REDIS_HOST=redis REDIS_PORT=6379 -SERVER_PORT=8080 \ No newline at end of file +# Backend server +SERVER_PORT=8080 + +# Postfix +MAIL_HOST_PROD=postfix-production +MAIL_PORT_PROD=25 +MAIL_USERNAME_PROD=noreply@nocopy.com +SMTP_PASSWORD_PROD=nocopy!nocopy! \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 6a69921..a6a02d4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,15 +1,32 @@ version: '3.9' services: + postfix: + image: boky/postfix:latest + container_name: postfix-production + hostname: ${DOMAIN:-no-copy.ru} + environment: + - DOMAIN=${DOMAIN:-no-copy.ru} + - ALLOWED_SENDER_DOMAINS=* + - RELAYHOST= + - DISABLE_SMTP_AUTH= + - SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru} + - SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!} + - RELAY_NETWORKS=172.16.0.0/12 + - POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12 + - POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination + - POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination + - DISABLE_SIGNING=true + ports: + - "25:25" + - "587:587" + networks: + - app-network + restart: unless-stopped + storage: image: alpine:latest container_name: file-storage - deploy: - resources: - limits: - memory: 128M - reservations: - memory: 64M networks: - app-network volumes: @@ -19,19 +36,9 @@ services: db: image: postgres:17.7 - restart: always - deploy: - resources: - limits: - cpus: '2.0' - memory: 2G - reservations: - cpus: '1.0' - memory: 1G + container_name: postgres environment: POSTGRES_DB: no_copy_ -# POSTGRES_USER: postgres -# POSTGRES_PASSWORD: postgres POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_SHARED_BUFFERS: 512MB @@ -41,42 +48,36 @@ services: volumes: - pgdata:/var/lib/postgresql/data - ./init-scripts:/docker-entrypoint-initdb.d:ro - container_name: postgres networks: app-network: aliases: - database + restart: unless-stopped app: build: . container_name: app-backend - deploy: - resources: - limits: - cpus: '1.5' - memory: 1G - reservations: - cpus: '0.5' - memory: 512M environment: FILE_STORAGE_PATH: /data/uploads MAX_FILE_SIZE: 10737418240 -# FILE_CHUNK_SIZE: 1048576 FILE_CHUNK_SIZE: 1000000 POSTGRES_DB: no_copy_ -# POSTGRES_USER: postgres -# POSTGRES_PASSWORD: postgres POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PORT: 5432 POSTGRES_HOST: db STORAGE_SERVICE_URL: http://storage:8081 SPRING_PROFILES_ACTIVE: docker - YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q - YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd - YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" + MAIL_HOST: postfix + MAIL_PORT: 25 + MAIL_USERNAME: noreply@no-copy.ru + SMTP_PASSWORD: nocopy!nocopy! + SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" + SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" + SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true" depends_on: - db + - postfix ports: - "80:8080" networks: @@ -88,163 +89,17 @@ services: volumes: - uploads_data:/data/uploads:rw healthcheck: - test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_" ] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_"] interval: 10s retries: 5 - - grafana: - image: grafana/grafana:10.3.1 - container_name: grafana - deploy: - resources: - limits: - cpus: '0.5' - memory: 512M - reservations: - memory: 256M - ports: - - "3000:3000" - depends_on: - prometheus: - condition: service_healthy - loki: - condition: service_healthy - tempo: - condition: service_started - alloy: - condition: service_started - environment: - GF_AUTH_ANONYMOUS_ENABLED: "true" - GF_SECURITY_ADMIN_USER: admin - GF_SECURITY_ADMIN_PASSWORD: admin - GF_METRICS_ENABLED: "true" - GF_DATABASE_MAX_IDLE_CONN: "2" - GF_DATABASE_MAX_OPEN_CONN: "10" - volumes: - - ./infrastructure/grafana/provisioning:/etc/grafana/provisioning - - ./infrastructure/grafana/dashboards:/var/lib/grafana/dashboards - healthcheck: - test: [ "CMD", "wget", "--spider", "http://localhost:3000/api/health" ] - interval: 10s - timeout: 5s - retries: 10 - networks: - - app-network - - prometheus: - image: prom/prometheus:latest - container_name: prometheus - deploy: - resources: - limits: - cpus: '0.5' - memory: 1G - reservations: - memory: 512M - ports: - - "9090:9090" - volumes: - - ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - - prometheus_data:/prometheus - command: - - '--config.file=/etc/prometheus/prometheus.yml' - - '--storage.tsdb.path=/prometheus' - - '--storage.tsdb.retention.time=15d' # Удерживать 15 дней - - '--web.enable-lifecycle' - healthcheck: - test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ] - interval: 10s - timeout: 5s - retries: 5 - networks: - - app-network - - loki: - image: grafana/loki:2.9.2 - container_name: loki - deploy: - resources: - limits: - cpus: '1.0' - memory: 2G - reservations: - memory: 1G - ports: - - "3100:3100" - volumes: - - ./infrastructure/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro - - loki_data:/loki - command: - - -config.file=/etc/loki/loki-config.yaml - healthcheck: - test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ] - interval: 10s - timeout: 5s - retries: 5 - networks: - - app-network - - tempo: - image: grafana/tempo:2.4.1 - container_name: tempo - deploy: - resources: - limits: - cpus: '0.5' - memory: 512M - reservations: - memory: 256M - command: [ "-config.file=/etc/tempo/tempo.yaml" ] - volumes: - - ./infrastructure/tempo/tempo.yaml:/etc/tempo/tempo.yaml - - tempo_data:/var/tempo - ports: - - "3200:3200" - networks: - - app-network - - alloy: - image: grafana/alloy:latest - container_name: alloy - deploy: - resources: - limits: - cpus: '0.5' - memory: 256M - reservations: - memory: 128M - user: root - ports: - - "9080:9080" - - "4317:4317" - - "4318:4318" - volumes: - - ./infrastructure/alloy/config.alloy:/etc/alloy/config.alloy:ro - - /var/lib/docker/containers:/var/lib/docker/containers:ro - - /var/run/docker.sock:/var/run/docker.sock - - /var/log:/var/log:ro - environment: - GRAFANA_LOKI_URL: http://loki:3100/loki/api/v1/push - command: - - run - - --server.http.listen-addr=0.0.0.0:9080 - - --storage.path=/var/lib/alloy/data - - /etc/alloy/config.alloy - networks: - - app-network + restart: unless-stopped volumes: pgdata: - artifacts: - tempo_data: - loki_data: - loki_chunks: - loki_index: - loki_rules: uploads_data: - prometheus_data: + postfix-data: networks: app-network: external: true - driver: bridge \ No newline at end of file + driver: bridge diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index 830bafd..5a146fe 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -36,8 +36,8 @@ public class User { @Column(name = "email", nullable = false, length = 1024, unique = true) private String email; - @Column(name = "email_verified", nullable = false) - private Boolean emailVerified = false; + @Column(name = "email_verified") + private boolean emailVerified = false; @Column(name = "company") private String company; @@ -82,11 +82,6 @@ public class User { @ToString.Exclude private List userContents = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnore - @ToString.Exclude - private List userActions = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore @ToString.Exclude diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserAction.java b/src/main/java/ru/soune/nocopy/entity/user/UserAction.java deleted file mode 100644 index 9a27e8e..0000000 --- a/src/main/java/ru/soune/nocopy/entity/user/UserAction.java +++ /dev/null @@ -1,44 +0,0 @@ -package ru.soune.nocopy.entity.user; - -import jakarta.persistence.*; -import lombok.*; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -@Entity -@Table(name = "user_actions") -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@ToString -@EntityListeners(AuditingEntityListener.class) -public class UserAction { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long actionId; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - @ToString.Exclude - private User user; - - @Column(name = "action", nullable = false, length = 100) - private String action; - - @Column(name = "details", columnDefinition = "TEXT") - private String details; - - @Column(name = "ip_address", length = 45) - private String ipAddress; - - @Column(name = "user_agent", columnDefinition = "TEXT") - private String userAgent; - - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; -} diff --git a/src/main/java/ru/soune/nocopy/service/EmailService.java b/src/main/java/ru/soune/nocopy/service/EmailService.java index 9a41952..ed18525 100644 --- a/src/main/java/ru/soune/nocopy/service/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/EmailService.java @@ -4,6 +4,7 @@ import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ClassPathResource; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; @@ -35,16 +36,17 @@ public class EmailService { helper.setFrom("NO COPY "); helper.setTo(user.getEmail()); - helper.setSubject("NO COPY - Подтверждение email адреса"); + helper.setSubject("NO COPY - Подтверждение email"); String htmlContent = loadAndProcessTemplate(user.getFullName(), code); - helper.setText(htmlContent, true); mailSender.send(message); - } catch (MessagingException | IOException e) { - log.error("Fail send email: " + e.getMessage()); + log.info("Email sent to: {}", user.getEmail()); + + } catch (Exception e) { + log.error("Failed to send email: {}", e.getMessage()); } } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 493b9ce..985d913 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,15 +1,13 @@ spring: mail: - host: smtp.gmail.com - port: 587 - username: vlad.popovtsev@gmail.com - password: aylgtjcxdtjvohrk + host: postfix + port: 25 + username: noreply@nocopy.com + password: nocopy!nocopy! properties: - mail: - smtp: - auth: true - starttls: - enable: true + mail.smtp.auth: false + mail.smtp.starttls.enable: false + mail.debug: true datasource: url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} @@ -36,8 +34,6 @@ spring: file: storage: base-path: ${FILE_STORAGE_PATH:/data/uploads} - # chunk-size: ${FILE_CHUNK_SIZE:5242880} # 5MB -# chunk-size: ${FILE_CHUNK_SIZE:1048576} # 1MB chunk-size: ${FILE_CHUNK_SIZE:1000000} # 1MB max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3} @@ -69,12 +65,3 @@ yandex: api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} - -app: - email: - verification: - expiration-hours: ${EMAIL_VERIFICATION_EXPIRATION:24} - base-url: ${EMAIL_BASE_URL:http://localhost:80} - from-email: ${EMAIL_FROM:noreply@yourdomain.com} - templates: - verification: verification-email.html \ No newline at end of file -- 2.54.0 From 55fa8b84e2ea9f6cd3cc76859d761ec8a0022d7b Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 20 Jan 2026 03:47:51 +0700 Subject: [PATCH 03/10] NCBACK-33 add email send --- docker-compose.yaml | 251 +++++++++++++++--- .../nocopy/configuration/HandlerConfig.java | 4 +- .../nocopy/controller/ApiController.java | 2 +- .../nocopy/controller/UserController.java | 8 +- .../ru/soune/nocopy/dto/AuthResponse.java | 10 - .../ru/soune/nocopy/dto/UserContentDTO.java | 14 - .../soune/nocopy/dto/UserContentResponse.java | 17 -- .../{ => register}/ChangePasswordRequest.java | 2 +- .../dto/{ => register}/LogOutAnswer.java | 2 +- .../dto/{ => register}/LoginAnswer.java | 2 +- .../dto/{ => register}/LoginRequest.java | 2 +- .../dto/{ => register}/LogoutRequest.java | 2 +- .../nocopy/dto/{ => register}/RegAnswer.java | 2 +- .../nocopy/dto/{ => register}/RegRequest.java | 2 +- .../dto/register/VerifyUserRequest.java | 17 ++ .../dto/{ => user}/UserContentRequest.java | 2 +- .../{ => user}/UserContentUpdateRequest.java | 2 +- .../soune/nocopy/dto/{ => user}/UserDTO.java | 4 +- .../nocopy/dto/{ => user}/UserRequest.java | 2 +- .../entity/{ => file}/ContentStatus.java | 2 +- .../entity/{ => file}/ImageProtection.java | 2 +- .../nocopy/entity/{ => file}/Violation.java | 2 +- .../entity/user/EmailVerificationToken.java | 2 +- .../entity/{ => user}/SubscriptionType.java | 2 +- .../ru/soune/nocopy/entity/user/User.java | 5 +- .../soune/nocopy/entity/user/UserContent.java | 2 +- .../handler/ImageFoundRequestHandler.java | 3 +- .../nocopy/handler/LoginRequestHandler.java | 4 +- .../nocopy/handler/LogoutRequestHandler.java | 4 +- .../nocopy/handler/RegRequestHandler.java | 11 +- .../handler/VerifyRegisterUserHandler.java | 68 +++++ .../validator/RegRequestValidator.java | 5 +- .../nocopy/mapper/UserContentMapper.java | 2 +- .../ru/soune/nocopy/mapper/UserMapper.java | 2 +- .../repository/AuthTokenRepository.java | 1 + .../EmailVerificationTokenRepository.java | 4 + .../service/{ => mail}/EmailService.java | 19 +- .../service/{ => register}/AuthService.java | 5 +- .../service/register/CleanUpVerifyTokens.java | 31 +++ .../{ => search}/YandexSearchService.java | 3 +- .../{ => user}/UserContentService.java | 6 +- .../service/{ => user}/UserService.java | 20 +- src/main/resources/application.yaml | 35 ++- .../soune/nocopy/service/AuthServiceTest.java | 3 +- 44 files changed, 443 insertions(+), 147 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/dto/AuthResponse.java delete mode 100644 src/main/java/ru/soune/nocopy/dto/UserContentDTO.java delete mode 100644 src/main/java/ru/soune/nocopy/dto/UserContentResponse.java rename src/main/java/ru/soune/nocopy/dto/{ => register}/ChangePasswordRequest.java (86%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/LogOutAnswer.java (83%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/LoginAnswer.java (88%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/LoginRequest.java (94%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/LogoutRequest.java (92%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/RegAnswer.java (88%) rename src/main/java/ru/soune/nocopy/dto/{ => register}/RegRequest.java (95%) create mode 100644 src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java rename src/main/java/ru/soune/nocopy/dto/{ => user}/UserContentRequest.java (92%) rename src/main/java/ru/soune/nocopy/dto/{ => user}/UserContentUpdateRequest.java (93%) rename src/main/java/ru/soune/nocopy/dto/{ => user}/UserDTO.java (87%) rename src/main/java/ru/soune/nocopy/dto/{ => user}/UserRequest.java (95%) rename src/main/java/ru/soune/nocopy/entity/{ => file}/ContentStatus.java (88%) rename src/main/java/ru/soune/nocopy/entity/{ => file}/ImageProtection.java (97%) rename src/main/java/ru/soune/nocopy/entity/{ => file}/Violation.java (97%) rename src/main/java/ru/soune/nocopy/entity/{ => user}/SubscriptionType.java (74%) create mode 100644 src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java rename src/main/java/ru/soune/nocopy/service/{ => mail}/EmailService.java (88%) rename src/main/java/ru/soune/nocopy/service/{ => register}/AuthService.java (94%) create mode 100644 src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java rename src/main/java/ru/soune/nocopy/service/{ => search}/YandexSearchService.java (98%) rename src/main/java/ru/soune/nocopy/service/{ => user}/UserContentService.java (94%) rename src/main/java/ru/soune/nocopy/service/{ => user}/UserService.java (83%) diff --git a/docker-compose.yaml b/docker-compose.yaml index a6a02d4..a7824c4 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,32 +1,15 @@ version: '3.9' services: - postfix: - image: boky/postfix:latest - container_name: postfix-production - hostname: ${DOMAIN:-no-copy.ru} - environment: - - DOMAIN=${DOMAIN:-no-copy.ru} - - ALLOWED_SENDER_DOMAINS=* - - RELAYHOST= - - DISABLE_SMTP_AUTH= - - SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru} - - SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!} - - RELAY_NETWORKS=172.16.0.0/12 - - POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12 - - POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination - - POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination - - DISABLE_SIGNING=true - ports: - - "25:25" - - "587:587" - networks: - - app-network - restart: unless-stopped - storage: image: alpine:latest container_name: file-storage + deploy: + resources: + limits: + memory: 128M + reservations: + memory: 64M networks: - app-network volumes: @@ -34,11 +17,44 @@ services: command: tail -f /dev/null restart: unless-stopped +# postfix: +# image: boky/postfix:latest +# container_name: postfix-production +# hostname: ${DOMAIN:-no-copy.ru} +# environment: +# - DOMAIN=${DOMAIN:-no-copy.ru} +# - ALLOWED_SENDER_DOMAINS=* +# - RELAYHOST= +# - DISABLE_SMTP_AUTH= +# - SMTP_USERNAME=${MAIL_USERNAME:-noreply@no-copy.ru} +# - SMTP_PASSWORD=${SMTP_PASSWORD:-nocopy!nocopy!} +# - RELAY_NETWORKS=172.16.0.0/12 +# - POSTFIX_mynetworks=127.0.0.0/8 172.17.0.0/16 172.16.0.0/12 +# - POSTFIX_smtpd_relay_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination +# - POSTFIX_smtpd_recipient_restrictions=permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination +# - DISABLE_SIGNING=true +# ports: +# - "25:25" +# - "587:587" +# networks: +# - app-network +# restart: unless-stopped + db: image: postgres:17.7 - container_name: postgres + restart: always + deploy: + resources: + limits: + cpus: '2.0' + memory: 2G + reservations: + cpus: '1.0' + memory: 1G environment: POSTGRES_DB: no_copy_ + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_SHARED_BUFFERS: 512MB @@ -48,36 +64,49 @@ services: volumes: - pgdata:/var/lib/postgresql/data - ./init-scripts:/docker-entrypoint-initdb.d:ro + container_name: postgres networks: app-network: aliases: - database - restart: unless-stopped app: build: . container_name: app-backend + deploy: + resources: + limits: + cpus: '1.5' + memory: 1G + reservations: + cpus: '0.5' + memory: 512M environment: FILE_STORAGE_PATH: /data/uploads MAX_FILE_SIZE: 10737418240 + # FILE_CHUNK_SIZE: 1048576 FILE_CHUNK_SIZE: 1000000 POSTGRES_DB: no_copy_ + # POSTGRES_USER: postgres + # POSTGRES_PASSWORD: postgres POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PORT: 5432 POSTGRES_HOST: db STORAGE_SERVICE_URL: http://storage:8081 SPRING_PROFILES_ACTIVE: docker - MAIL_HOST: postfix - MAIL_PORT: 25 - MAIL_USERNAME: noreply@no-copy.ru - SMTP_PASSWORD: nocopy!nocopy! - SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" - SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" - SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true" + YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q + YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd + YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" +# MAIL_HOST: postfix +# MAIL_PORT: 25 +# MAIL_USERNAME: noreply@no-copy.ru +# SMTP_PASSWORD: nocopy!nocopy! +# SPRING_MAIL_PROPERTIES_MAIL_SMTP_STARTTLS_ENABLE: "false" +# SPRING_MAIL_PROPERTIES_MAIL_SMTP_AUTH: "false" +# SPRING_MAIL_PROPERTIES_MAIL_DEBUG: "true" depends_on: - db - - postfix ports: - "80:8080" networks: @@ -89,17 +118,163 @@ services: volumes: - uploads_data:/data/uploads:rw healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_"] + test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d no_copy_" ] interval: 10s retries: 5 - restart: unless-stopped + + grafana: + image: grafana/grafana:10.3.1 + container_name: grafana + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + memory: 256M + ports: + - "3000:3000" + depends_on: + prometheus: + condition: service_healthy + loki: + condition: service_healthy + tempo: + condition: service_started + alloy: + condition: service_started + environment: + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + GF_METRICS_ENABLED: "true" + GF_DATABASE_MAX_IDLE_CONN: "2" + GF_DATABASE_MAX_OPEN_CONN: "10" + volumes: + - ./infrastructure/grafana/provisioning:/etc/grafana/provisioning + - ./infrastructure/grafana/dashboards:/var/lib/grafana/dashboards + healthcheck: + test: [ "CMD", "wget", "--spider", "http://localhost:3000/api/health" ] + interval: 10s + timeout: 5s + retries: 10 + networks: + - app-network + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + deploy: + resources: + limits: + cpus: '0.5' + memory: 1G + reservations: + memory: 512M + ports: + - "9090:9090" + volumes: + - ./infrastructure/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--storage.tsdb.retention.time=15d' # Удерживать 15 дней + - '--web.enable-lifecycle' + healthcheck: + test: [ "CMD", "wget", "--spider", "http://localhost:9090/-/healthy" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-network + + loki: + image: grafana/loki:2.9.2 + container_name: loki + deploy: + resources: + limits: + cpus: '1.0' + memory: 2G + reservations: + memory: 1G + ports: + - "3100:3100" + volumes: + - ./infrastructure/loki/loki-config.yaml:/etc/loki/loki-config.yaml:ro + - loki_data:/loki + command: + - -config.file=/etc/loki/loki-config.yaml + healthcheck: + test: [ "CMD", "wget", "--spider", "-q", "http://localhost:3100/ready" ] + interval: 10s + timeout: 5s + retries: 5 + networks: + - app-network + + tempo: + image: grafana/tempo:2.4.1 + container_name: tempo + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + memory: 256M + command: [ "-config.file=/etc/tempo/tempo.yaml" ] + volumes: + - ./infrastructure/tempo/tempo.yaml:/etc/tempo/tempo.yaml + - tempo_data:/var/tempo + ports: + - "3200:3200" + networks: + - app-network + + alloy: + image: grafana/alloy:latest + container_name: alloy + deploy: + resources: + limits: + cpus: '0.5' + memory: 256M + reservations: + memory: 128M + user: root + ports: + - "9080:9080" + - "4317:4317" + - "4318:4318" + volumes: + - ./infrastructure/alloy/config.alloy:/etc/alloy/config.alloy:ro + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + - /var/log:/var/log:ro + environment: + GRAFANA_LOKI_URL: http://loki:3100/loki/api/v1/push + command: + - run + - --server.http.listen-addr=0.0.0.0:9080 + - --storage.path=/var/lib/alloy/data + - /etc/alloy/config.alloy + networks: + - app-network volumes: pgdata: + artifacts: + tempo_data: + loki_data: + loki_chunks: + loki_index: + loki_rules: uploads_data: - postfix-data: - + prometheus_data: +# postfix-data: networks: app-network: external: true - driver: bridge + driver: bridge \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index 674b27c..6c435d6 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -17,7 +17,8 @@ public class HandlerConfig { FileUploadHandler upload, FileEntityHandler file, LogoutRequestHandler logoutHandler, - ImageFoundRequestHandler imageFoundRequestHandler + ImageFoundRequestHandler imageFoundRequestHandler, + VerifyRegisterUserHandler verifyRegisterUser ) { Map map = new HashMap<>(); map.put(20001, login); @@ -26,6 +27,7 @@ public class HandlerConfig { map.put(20005, file); map.put(20006, logoutHandler); map.put(20007, imageFoundRequestHandler); + map.put(20009, verifyRegisterUser); return map; } diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 8cd26af..92557a1 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -14,7 +14,7 @@ import org.springframework.web.multipart.MultipartFile; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; -import ru.soune.nocopy.dto.RegAnswer; +import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.FileEntityResponse; diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index 77df18a..cdb376c 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -3,9 +3,9 @@ package ru.soune.nocopy.controller; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import ru.soune.nocopy.dto.ChangePasswordRequest; -import ru.soune.nocopy.dto.UserDTO; -import ru.soune.nocopy.dto.UserRequest; +import ru.soune.nocopy.dto.register.ChangePasswordRequest; +import ru.soune.nocopy.dto.user.UserDTO; +import ru.soune.nocopy.dto.user.UserRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InvalidUserEmail; @@ -13,7 +13,7 @@ import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.mapper.UserMapper; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.UserService; +import ru.soune.nocopy.service.user.UserService; import java.util.List; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/dto/AuthResponse.java b/src/main/java/ru/soune/nocopy/dto/AuthResponse.java deleted file mode 100644 index 168e09a..0000000 --- a/src/main/java/ru/soune/nocopy/dto/AuthResponse.java +++ /dev/null @@ -1,10 +0,0 @@ -package ru.soune.nocopy.dto; - -import java.time.LocalDateTime; - -public record AuthResponse ( - boolean success, - String message, - String token, - LocalDateTime expiresAt -) {} diff --git a/src/main/java/ru/soune/nocopy/dto/UserContentDTO.java b/src/main/java/ru/soune/nocopy/dto/UserContentDTO.java deleted file mode 100644 index 934bfda..0000000 --- a/src/main/java/ru/soune/nocopy/dto/UserContentDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package ru.soune.nocopy.dto; - -import lombok.Data; - -@Data -public class UserContentDTO { - String fileName; - String originalFilename; - String fileType; - String fileExtension; - Long fileSize; - String filePath; - Integer protectionLevel; -} diff --git a/src/main/java/ru/soune/nocopy/dto/UserContentResponse.java b/src/main/java/ru/soune/nocopy/dto/UserContentResponse.java deleted file mode 100644 index 8c14615..0000000 --- a/src/main/java/ru/soune/nocopy/dto/UserContentResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package ru.soune.nocopy.dto; - -import ru.soune.nocopy.entity.ContentStatus; -import ru.soune.nocopy.entity.file.FileType; - -import java.time.LocalDateTime; - -public record UserContentResponse( - Long id, - String filename, - String originalFilename, - FileType fileType, - long fileSize, - String filePath, - ContentStatus status, - LocalDateTime uploadDate -) {} diff --git a/src/main/java/ru/soune/nocopy/dto/ChangePasswordRequest.java b/src/main/java/ru/soune/nocopy/dto/register/ChangePasswordRequest.java similarity index 86% rename from src/main/java/ru/soune/nocopy/dto/ChangePasswordRequest.java rename to src/main/java/ru/soune/nocopy/dto/register/ChangePasswordRequest.java index 204db58..2e33460 100644 --- a/src/main/java/ru/soune/nocopy/dto/ChangePasswordRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/ChangePasswordRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/ru/soune/nocopy/dto/LogOutAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/LogOutAnswer.java similarity index 83% rename from src/main/java/ru/soune/nocopy/dto/LogOutAnswer.java rename to src/main/java/ru/soune/nocopy/dto/register/LogOutAnswer.java index 009f5f5..04478d5 100644 --- a/src/main/java/ru/soune/nocopy/dto/LogOutAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/LogOutAnswer.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/ru/soune/nocopy/dto/LoginAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java similarity index 88% rename from src/main/java/ru/soune/nocopy/dto/LoginAnswer.java rename to src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java index e791d8d..b244fd8 100644 --- a/src/main/java/ru/soune/nocopy/dto/LoginAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/ru/soune/nocopy/dto/LoginRequest.java b/src/main/java/ru/soune/nocopy/dto/register/LoginRequest.java similarity index 94% rename from src/main/java/ru/soune/nocopy/dto/LoginRequest.java rename to src/main/java/ru/soune/nocopy/dto/register/LoginRequest.java index b0548bc..6e99b07 100644 --- a/src/main/java/ru/soune/nocopy/dto/LoginRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/LoginRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ru/soune/nocopy/dto/LogoutRequest.java b/src/main/java/ru/soune/nocopy/dto/register/LogoutRequest.java similarity index 92% rename from src/main/java/ru/soune/nocopy/dto/LogoutRequest.java rename to src/main/java/ru/soune/nocopy/dto/register/LogoutRequest.java index 33a43e4..d0c30eb 100644 --- a/src/main/java/ru/soune/nocopy/dto/LogoutRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/LogoutRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/ru/soune/nocopy/dto/RegAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java similarity index 88% rename from src/main/java/ru/soune/nocopy/dto/RegAnswer.java rename to src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java index b51b4cc..5b265ad 100644 --- a/src/main/java/ru/soune/nocopy/dto/RegAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/src/main/java/ru/soune/nocopy/dto/RegRequest.java b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java similarity index 95% rename from src/main/java/ru/soune/nocopy/dto/RegRequest.java rename to src/main/java/ru/soune/nocopy/dto/register/RegRequest.java index d7adfea..4ae695f 100644 --- a/src/main/java/ru/soune/nocopy/dto/RegRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.register; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.*; diff --git a/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java b/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java new file mode 100644 index 0000000..a0e3472 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java @@ -0,0 +1,17 @@ +package ru.soune.nocopy.dto.register; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class VerifyUserRequest { + @JsonProperty("verify_token") + private String verifyToken; + + @JsonProperty("user_id") + private Long userId; +} diff --git a/src/main/java/ru/soune/nocopy/dto/UserContentRequest.java b/src/main/java/ru/soune/nocopy/dto/user/UserContentRequest.java similarity index 92% rename from src/main/java/ru/soune/nocopy/dto/UserContentRequest.java rename to src/main/java/ru/soune/nocopy/dto/user/UserContentRequest.java index bd28100..2de4791 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserContentRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserContentRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.user; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/ru/soune/nocopy/dto/UserContentUpdateRequest.java b/src/main/java/ru/soune/nocopy/dto/user/UserContentUpdateRequest.java similarity index 93% rename from src/main/java/ru/soune/nocopy/dto/UserContentUpdateRequest.java rename to src/main/java/ru/soune/nocopy/dto/user/UserContentUpdateRequest.java index 7595b21..1b4f3dc 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserContentUpdateRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserContentUpdateRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.user; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/ru/soune/nocopy/dto/UserDTO.java b/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java similarity index 87% rename from src/main/java/ru/soune/nocopy/dto/UserDTO.java rename to src/main/java/ru/soune/nocopy/dto/user/UserDTO.java index e5a6b63..45f9cbe 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserDTO.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserDTO.java @@ -1,11 +1,11 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.user; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import ru.soune.nocopy.entity.user.GenderType; -import ru.soune.nocopy.entity.SubscriptionType; +import ru.soune.nocopy.entity.user.SubscriptionType; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/dto/UserRequest.java b/src/main/java/ru/soune/nocopy/dto/user/UserRequest.java similarity index 95% rename from src/main/java/ru/soune/nocopy/dto/UserRequest.java rename to src/main/java/ru/soune/nocopy/dto/user/UserRequest.java index ca89d88..9a5e48e 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserRequest.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.user; import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.validation.constraints.Email; diff --git a/src/main/java/ru/soune/nocopy/entity/ContentStatus.java b/src/main/java/ru/soune/nocopy/entity/file/ContentStatus.java similarity index 88% rename from src/main/java/ru/soune/nocopy/entity/ContentStatus.java rename to src/main/java/ru/soune/nocopy/entity/file/ContentStatus.java index c2dfa41..2bb47f6 100644 --- a/src/main/java/ru/soune/nocopy/entity/ContentStatus.java +++ b/src/main/java/ru/soune/nocopy/entity/file/ContentStatus.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.file; import lombok.Getter; diff --git a/src/main/java/ru/soune/nocopy/entity/ImageProtection.java b/src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java similarity index 97% rename from src/main/java/ru/soune/nocopy/entity/ImageProtection.java rename to src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java index 92cdedc..52d7c8c 100644 --- a/src/main/java/ru/soune/nocopy/entity/ImageProtection.java +++ b/src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.file; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/ru/soune/nocopy/entity/Violation.java b/src/main/java/ru/soune/nocopy/entity/file/Violation.java similarity index 97% rename from src/main/java/ru/soune/nocopy/entity/Violation.java rename to src/main/java/ru/soune/nocopy/entity/file/Violation.java index b8a7a1a..c5317bc 100644 --- a/src/main/java/ru/soune/nocopy/entity/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/file/Violation.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.file; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java index 7b009e1..1b02bcf 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java +++ b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java @@ -24,7 +24,7 @@ public class EmailVerificationToken { private String token; @Column(name = "expires_at", nullable = false) - private LocalDateTime expiresAt; + private LocalDateTime expiresAt = LocalDateTime.now().plusMinutes(15); @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; diff --git a/src/main/java/ru/soune/nocopy/entity/SubscriptionType.java b/src/main/java/ru/soune/nocopy/entity/user/SubscriptionType.java similarity index 74% rename from src/main/java/ru/soune/nocopy/entity/SubscriptionType.java rename to src/main/java/ru/soune/nocopy/entity/user/SubscriptionType.java index 952722e..2648e98 100644 --- a/src/main/java/ru/soune/nocopy/entity/SubscriptionType.java +++ b/src/main/java/ru/soune/nocopy/entity/user/SubscriptionType.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.user; import lombok.Getter; diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index 5a146fe..0aa4990 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -6,9 +6,8 @@ import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.ImageProtection; -import ru.soune.nocopy.entity.SubscriptionType; -import ru.soune.nocopy.entity.Violation; +import ru.soune.nocopy.entity.file.ImageProtection; +import ru.soune.nocopy.entity.file.Violation; import java.time.LocalDate; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/entity/user/UserContent.java b/src/main/java/ru/soune/nocopy/entity/user/UserContent.java index 0472791..4dcf724 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/UserContent.java +++ b/src/main/java/ru/soune/nocopy/entity/user/UserContent.java @@ -4,7 +4,7 @@ import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.ContentStatus; +import ru.soune.nocopy.entity.file.ContentStatus; import ru.soune.nocopy.entity.file.FileType; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 3d13d4a..13e005e 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -3,14 +3,13 @@ package ru.soune.nocopy.handler; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.YandexSearchResponse; -import ru.soune.nocopy.service.YandexSearchService; +import ru.soune.nocopy.service.search.YandexSearchService; @Slf4j @Component diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 8cc115a..5c023ce 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -4,10 +4,12 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; +import ru.soune.nocopy.dto.register.LoginAnswer; +import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.register.AuthService; import java.util.Arrays; import java.util.Map; diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index f4aa19f..4fa77b1 100644 --- a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java @@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; +import ru.soune.nocopy.dto.register.LogOutAnswer; +import ru.soune.nocopy.dto.register.LogoutRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; +import ru.soune.nocopy.service.register.AuthService; import java.util.Objects; import java.util.Optional; diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 847c19d..3e655e0 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -7,20 +7,19 @@ import org.springframework.stereotype.Component; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BindingResult; import ru.soune.nocopy.dto.*; +import ru.soune.nocopy.dto.register.RegAnswer; +import ru.soune.nocopy.dto.register.RegRequest; 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.ValidationException; import ru.soune.nocopy.handler.validator.RegRequestValidator; -import ru.soune.nocopy.repository.EmailVerificationTokenRepository; import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.AuthService; -import ru.soune.nocopy.service.EmailService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.mail.EmailService; -import java.time.LocalDateTime; import java.util.Arrays; import java.util.Map; -import java.util.UUID; @Slf4j @Component @@ -36,8 +35,6 @@ public class RegRequestHandler implements RequestHandler { private final EmailService emailService; - private final EmailVerificationTokenRepository emailVerificationTokenRepository; - @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java new file mode 100644 index 0000000..b6163b7 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -0,0 +1,68 @@ +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.register.RegAnswer; +import ru.soune.nocopy.dto.register.VerifyUserRequest; +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.TokenNotFoundException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; +import ru.soune.nocopy.service.user.UserService; + +import java.util.Map; +import java.util.Optional; + +@Component +@Slf4j +@RequiredArgsConstructor +public class VerifyRegisterUserHandler implements RequestHandler{ + + private final ObjectMapper objectMapper; + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + + private final UserService userService; + + private final AuthTokenRepository authTokenRepository; + + @Override + public BaseResponse handle(BaseRequest request) { + VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), + VerifyUserRequest.class); + + String verifyToken = verifyUserRequest.getVerifyToken(); + Long userId = verifyUserRequest.getUserId(); + + EmailVerificationToken emailVerificationToken = + emailVerificationTokenRepository.findByUserIdAndToken(userId, verifyToken); + + if (emailVerificationToken == null) { + throw new NotValidFieldException("Token was incorrected:" + verifyToken, + new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + MessageCode.INVALID_TOKEN.getDescription(), Map.of( + "userId", userId, + "token", verifyToken))); + } + + userService.activateAndVerifyUser(userId); + emailVerificationTokenRepository.delete(emailVerificationToken); + + Optional byUserIdAndIsActive = authTokenRepository.findByUserIdAndIsActive(userId, true); + AuthToken authToken = byUserIdAndIsActive.orElseThrow(() -> + new TokenNotFoundException("Token not found with user id: " + userId)); + + RegAnswer regAnswer = new RegAnswer(); + regAnswer.setToken(authToken.getToken()); + + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), + MessageCode.SUCCESS.getDescription(), regAnswer); + } +} diff --git a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java index e01df8e..71fb43b 100644 --- a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java +++ b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java @@ -1,14 +1,11 @@ package ru.soune.nocopy.handler.validator; -import org.apache.commons.validator.routines.DomainValidator; import org.apache.commons.validator.routines.EmailValidator; import org.springframework.stereotype.Component; import org.springframework.validation.Errors; import org.springframework.validation.Validator; -import ru.soune.nocopy.dto.RegRequest; +import ru.soune.nocopy.dto.register.RegRequest; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.*; @Component diff --git a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java index ed7c734..16e411a 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java @@ -4,7 +4,7 @@ import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; -import ru.soune.nocopy.dto.UserContentUpdateRequest; +import ru.soune.nocopy.dto.user.UserContentUpdateRequest; import ru.soune.nocopy.entity.user.UserContent; @Mapper(componentModel = "spring", diff --git a/src/main/java/ru/soune/nocopy/mapper/UserMapper.java b/src/main/java/ru/soune/nocopy/mapper/UserMapper.java index a6c4de6..9d05f14 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserMapper.java @@ -3,7 +3,7 @@ package ru.soune.nocopy.mapper; import org.mapstruct.Mapper; import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.factory.Mappers; -import ru.soune.nocopy.dto.UserDTO; +import ru.soune.nocopy.dto.user.UserDTO; import ru.soune.nocopy.entity.user.User; @Mapper(componentModel = "spring", diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index b5b0d88..66995d1 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -11,4 +11,5 @@ import java.util.Optional; public interface AuthTokenRepository extends JpaRepository { Optional findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter); Optional findByToken(String token); + Optional findByUserIdAndIsActive(Long userId, boolean isActive); } diff --git a/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java index f67573e..34a3233 100644 --- a/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java @@ -3,8 +3,12 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import ru.soune.nocopy.entity.user.EmailVerificationToken; +import java.time.LocalDateTime; +import java.util.List; import java.util.Optional; public interface EmailVerificationTokenRepository extends JpaRepository { Optional findByToken(String token); + EmailVerificationToken findByUserIdAndToken(Long userId, String token); + List findByExpiresAtBefore(LocalDateTime expiresAtBefore); } diff --git a/src/main/java/ru/soune/nocopy/service/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java similarity index 88% rename from src/main/java/ru/soune/nocopy/service/EmailService.java rename to src/main/java/ru/soune/nocopy/service/mail/EmailService.java index ed18525..cc3b1cb 100644 --- a/src/main/java/ru/soune/nocopy/service/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -1,6 +1,5 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.mail; -import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -29,14 +28,23 @@ public class EmailService { private final EmailVerificationTokenRepository emailVerificationTokenRepository; + @Value("${app.email.verification.standart-subject}") + private String standartSubject; + + @Value("${app.email.verification.standart-address-from}") + private String standartAdressFrom; + + @Value("${app.email.verification.user}") + private String user; + public void sendVerificationEmail(User user, String code) { try { MimeMessage message = mailSender.createMimeMessage(); MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setFrom("NO COPY "); + helper.setFrom(standartAdressFrom); helper.setTo(user.getEmail()); - helper.setSubject("NO COPY - Подтверждение email"); + helper.setSubject(standartSubject); String htmlContent = loadAndProcessTemplate(user.getFullName(), code); helper.setText(htmlContent, true); @@ -55,7 +63,6 @@ public class EmailService { EmailVerificationToken emailToken = new EmailVerificationToken(); emailToken.setUser(authToken.getUser()); emailToken.setToken(generateVerificationCode()); - emailToken.setExpiresAt(LocalDateTime.now().plusMinutes(15)); emailToken.setCreatedAt(LocalDateTime.now()); return emailVerificationTokenRepository.save(emailToken); @@ -71,7 +78,7 @@ public class EmailService { private String loadAndProcessTemplate(String username, String code) throws IOException { ClassPathResource resource = new ClassPathResource("templates/email-verification.html"); String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8); - String safeUsername = escapeHtml(username != null ? username : "Пользователь"); + String safeUsername = escapeHtml(username != null ? username : user); String safeCode = escapeHtml(code); return template diff --git a/src/main/java/ru/soune/nocopy/service/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java similarity index 94% rename from src/main/java/ru/soune/nocopy/service/AuthService.java rename to src/main/java/ru/soune/nocopy/service/register/AuthService.java index ad58219..d410236 100644 --- a/src/main/java/ru/soune/nocopy/service/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.register; import lombok.RequiredArgsConstructor; import org.springframework.context.MessageSource; @@ -6,6 +6,9 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.nocopy.dto.*; +import ru.soune.nocopy.dto.register.LoginAnswer; +import ru.soune.nocopy.dto.register.LoginRequest; +import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; diff --git a/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java new file mode 100644 index 0000000..455d8e8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/register/CleanUpVerifyTokens.java @@ -0,0 +1,31 @@ +package ru.soune.nocopy.service.register; + +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class CleanUpVerifyTokens { + + private final EmailVerificationTokenRepository emailVerificationTokenRepository; + + @Transactional + @Scheduled(fixedDelay = 30000) + public void cleanupExpiredTokens() { + LocalDateTime now = LocalDateTime.now(); + + + List expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now); + + if (!expiredTokens.isEmpty()) { + emailVerificationTokenRepository.deleteAll(expiredTokens); + } + } +} diff --git a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java b/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java similarity index 98% rename from src/main/java/ru/soune/nocopy/service/YandexSearchService.java rename to src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java index d0f4710..d22bbc9 100644 --- a/src/main/java/ru/soune/nocopy/service/YandexSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.search; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -12,7 +12,6 @@ import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.entity.file.FileEntity; -import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.FileEntityRepository; diff --git a/src/main/java/ru/soune/nocopy/service/UserContentService.java b/src/main/java/ru/soune/nocopy/service/user/UserContentService.java similarity index 94% rename from src/main/java/ru/soune/nocopy/service/UserContentService.java rename to src/main/java/ru/soune/nocopy/service/user/UserContentService.java index 3d033e2..63510e8 100644 --- a/src/main/java/ru/soune/nocopy/service/UserContentService.java +++ b/src/main/java/ru/soune/nocopy/service/user/UserContentService.java @@ -1,10 +1,10 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.user; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.dto.UserContentRequest; -import ru.soune.nocopy.dto.UserContentUpdateRequest; +import ru.soune.nocopy.dto.user.UserContentRequest; +import ru.soune.nocopy.dto.user.UserContentUpdateRequest; import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.entity.user.UserContent; diff --git a/src/main/java/ru/soune/nocopy/service/UserService.java b/src/main/java/ru/soune/nocopy/service/user/UserService.java similarity index 83% rename from src/main/java/ru/soune/nocopy/service/UserService.java rename to src/main/java/ru/soune/nocopy/service/user/UserService.java index c590bb9..56c6341 100644 --- a/src/main/java/ru/soune/nocopy/service/UserService.java +++ b/src/main/java/ru/soune/nocopy/service/user/UserService.java @@ -1,18 +1,20 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.user; import lombok.AllArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.dto.ChangePasswordRequest; -import ru.soune.nocopy.dto.UserDTO; -import ru.soune.nocopy.dto.UserRequest; +import ru.soune.nocopy.dto.register.ChangePasswordRequest; +import ru.soune.nocopy.dto.user.UserDTO; +import ru.soune.nocopy.dto.user.UserRequest; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.UserRepository; +import java.util.Optional; + @Service @AllArgsConstructor public class UserService { @@ -44,6 +46,16 @@ public class UserService { return mapToDTO(updatedUser); } + @Transactional + public void activateAndVerifyUser(long userId) { + Optional users = userRepository.findById(userId); + User user = users.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); + user.setIsActive(true); + user.setEmailVerified(true); + + userRepository.save(user); + } + private void updateUserFromRequest(User user, UserRequest request) { if (request.getFullName() != null) { user.setFullName(request.getFullName()); diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 985d913..b509055 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,13 +1,25 @@ spring: mail: - host: postfix - port: 25 - username: noreply@nocopy.com - password: nocopy!nocopy! + host: smtp.gmail.com + port: 587 + username: vlad.popovtsev@gmail.com + password: gocjwvrqrzlaatsr properties: - mail.smtp.auth: false - mail.smtp.starttls.enable: false - mail.debug: true + mail: + smtp: + auth: true + starttls: + enable: true + +# mail: +# host: postfix + # port: 25 + # username: noreply@nocopy.com + # password: nocopy!nocopy! + # properties: + # mail.smtp.auth: false + # mail.smtp.starttls.enable: false + # mail.debug: true datasource: url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} @@ -34,6 +46,8 @@ spring: file: storage: base-path: ${FILE_STORAGE_PATH:/data/uploads} + # chunk-size: ${FILE_CHUNK_SIZE:5242880} # 5MB +# chunk-size: ${FILE_CHUNK_SIZE:1048576} # 1MB chunk-size: ${FILE_CHUNK_SIZE:1000000} # 1MB max-file-size: ${MAX_FILE_SIZE:10737418240} # 10GB max-retry-attempts: ${MAX_RETRY_ATTEMPTS:3} @@ -65,3 +79,10 @@ yandex: api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} + +app: + email: + verification: + standart-subject: NO COPY - Подтверждение email адреса + standart-address-from: NO COPY + user: Пользователь \ No newline at end of file diff --git a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java index 2a3a013..574ab25 100644 --- a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java @@ -8,13 +8,14 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.context.MessageSource; import org.springframework.security.crypto.password.PasswordEncoder; -import ru.soune.nocopy.dto.LoginRequest; +import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.register.AuthService; import java.util.Optional; -- 2.54.0 From f9accd7af1b7cb9712b93878688ac065123df26c Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 20 Jan 2026 15:12:24 +0700 Subject: [PATCH 04/10] NCBACK-33 add verification logic --- .../nocopy/controller/UserController.java | 24 ++--- .../java/ru/soune/nocopy/dto/MessageCode.java | 2 + .../nocopy/dto/register/LoginAnswer.java | 4 + .../soune/nocopy/dto/register/RegAnswer.java | 4 + .../dto/register/VerifyUserRequest.java | 3 + .../entity/user/EmailVerificationToken.java | 4 + .../ru/soune/nocopy/entity/user/User.java | 2 +- .../nocopy/handler/LoginRequestHandler.java | 16 +++- .../nocopy/handler/RegRequestHandler.java | 2 + .../handler/VerifyRegisterUserHandler.java | 92 ++++++++++++++----- .../EmailVerificationTokenRepository.java | 1 + .../nocopy/repository/UserRepository.java | 2 +- .../nocopy/service/register/AuthService.java | 19 +++- .../nocopy/service/user/UserService.java | 6 +- 14 files changed, 137 insertions(+), 44 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index cdb376c..eb12863 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -34,7 +34,7 @@ public class UserController { @GetMapping("/all") public ResponseEntity> getAllUsers() { List allUsers = userRepository.findAll().stream() - .map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.getIsActive(), + .map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.isActive(), u.getPhone(), u.getGenderType(), u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) .toList(); @@ -52,19 +52,19 @@ public class UserController { AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); if (authToken != null) { - Optional user = userRepository.findByEmail(email); + User user = userRepository.findByEmail(email); //TODO add mapper - if (user.isPresent()) { - UserDTO userDTO = userMapper.toDTO(user.get()); + if (user != null) { + UserDTO userDTO = userMapper.toDTO(user); userDTO.setEmail(email); - userDTO.setFullName(user.get().getFullName()); - userDTO.setCompany(user.get().getCompany()); - userDTO.setPhone(user.get().getPhone()); - userDTO.setGenderType(user.get().getGenderType()); - userDTO.setBirthday(user.get().getBirthday()); - userDTO.setCreatedAt(user.get().getCreatedAt()); - userDTO.setSubscriptionType(user.get().getSubscriptionType()); - userDTO.setActive(user.get().getIsActive()); + userDTO.setFullName(user.getFullName()); + userDTO.setCompany(user.getCompany()); + userDTO.setPhone(user.getPhone()); + userDTO.setGenderType(user.getGenderType()); + userDTO.setBirthday(user.getBirthday()); + userDTO.setCreatedAt(user.getCreatedAt()); + userDTO.setSubscriptionType(user.getSubscriptionType()); + userDTO.setActive(user.isActive()); return ResponseEntity.ok(userDTO); } diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 5c75246..e927005 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -6,9 +6,11 @@ public enum MessageCode { REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), INVALID_FIELD(2, "Invalid field"), INVALID_TOKEN(2, "Invalid token"), + TOKEN_IS_ALIVE(2, "Token is alive"), INVALID_ACTION(2, "Invalid action"), FILE_UPLOAD_ERROR(2, "File upload error"), FILE_DOWNLOAD_ERROR(2, "File download error"), + USER_NOT_VERIFIED(2, "User not verified"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), diff --git a/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java index b244fd8..42a8e55 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/LoginAnswer.java @@ -13,5 +13,9 @@ import java.util.Map; public class LoginAnswer { private String Token; + private boolean isActive; + + private boolean isVerified; + private List> fieldErrors; } diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java index 5b265ad..a1cbd01 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java @@ -13,5 +13,9 @@ import java.util.Map; public class RegAnswer { private String Token; + private boolean isActive; + + private boolean isVerified; + private List> fieldErrors; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java b/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java index a0e3472..8bb0c2a 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java @@ -14,4 +14,7 @@ public class VerifyUserRequest { @JsonProperty("user_id") private Long userId; + + @JsonProperty("resend") + private Integer resend; } diff --git a/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java index 1b02bcf..d836e45 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java +++ b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java @@ -28,4 +28,8 @@ public class EmailVerificationToken { @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; + + public boolean isExpired() { + return LocalDateTime.now().isAfter(expiresAt); + } } diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index 0aa4990..c61d15d 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -69,7 +69,7 @@ public class User { private LocalDateTime lastLoginAt; @Column(name = "is_active") - private Boolean isActive = false; + private boolean isActive = false; @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 5c023ce..2df819b 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -7,12 +7,14 @@ import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.register.LoginAnswer; import ru.soune.nocopy.dto.register.LoginRequest; import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.service.register.AuthService; import java.util.Arrays; import java.util.Map; +import java.util.Optional; @Component @RequiredArgsConstructor @@ -25,8 +27,9 @@ public class LoginRequestHandler implements RequestHandler { @Override public BaseResponse handle(BaseRequest request) { LoginRequest loginRequest = objectMapper.convertValue(request.getMessageBody(), LoginRequest.class); + User user = userRepository.findByEmail(loginRequest.getEmail()); - if (!userRepository.existsByEmail(loginRequest.getEmail())) { + if (user != null) { LoginAnswer loginAnswer = new LoginAnswer(); loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail()))); @@ -35,6 +38,17 @@ public class LoginRequestHandler implements RequestHandler { MessageCode.AUTH_EMAIL_NOT_FOUND.getDescription(), loginAnswer)); } + if (!user.isActive() && !user.isEmailVerified()) { + LoginAnswer loginAnswer = new LoginAnswer(); + + loginAnswer.setFieldErrors(Arrays.asList(Map.of( + "verified", String.valueOf(user.isEmailVerified()), + "isActive", String.valueOf(user.isActive())))); + + return new BaseResponse(request.getMsgId(), MessageCode.USER_NOT_VERIFIED.getCode(), + MessageCode.USER_NOT_VERIFIED.getDescription(), loginAnswer); + } + AuthToken authToken = authService.login(loginRequest); LoginAnswer loginAnswer = new LoginAnswer(); diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 3e655e0..7eb6fe0 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -65,6 +65,8 @@ public class RegRequestHandler implements RequestHandler { RegAnswer regAnswer = new RegAnswer(); regAnswer.setToken(authToken.getToken()); + regAnswer.setVerified(false); + regAnswer.setActive(false); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index b6163b7..50f2dc0 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -11,10 +11,14 @@ import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.dto.register.VerifyUserRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.EmailVerificationToken; +import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.NotValidFieldException; -import ru.soune.nocopy.exception.TokenNotFoundException; +import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.EmailVerificationTokenRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.UserService; import java.util.Map; @@ -23,46 +27,92 @@ import java.util.Optional; @Component @Slf4j @RequiredArgsConstructor -public class VerifyRegisterUserHandler implements RequestHandler{ - +public class VerifyRegisterUserHandler implements RequestHandler { private final ObjectMapper objectMapper; private final EmailVerificationTokenRepository emailVerificationTokenRepository; + private final EmailService emailService; + + private final UserRepository userRepository; + private final UserService userService; private final AuthTokenRepository authTokenRepository; + private final AuthService authService; + @Override public BaseResponse handle(BaseRequest request) { - VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), - VerifyUserRequest.class); + VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class); - String verifyToken = verifyUserRequest.getVerifyToken(); Long userId = verifyUserRequest.getUserId(); - EmailVerificationToken emailVerificationToken = - emailVerificationTokenRepository.findByUserIdAndToken(userId, verifyToken); + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); - if (emailVerificationToken == null) { - throw new NotValidFieldException("Token was incorrected:" + verifyToken, - new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), - MessageCode.INVALID_TOKEN.getDescription(), Map.of( - "userId", userId, - "token", verifyToken))); + Optional userAuthTokens = authTokenRepository.findByUserIdAndIsActive(userId, false); + + AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get(); + + if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) { + return handleResend(request, user, authToken); } - userService.activateAndVerifyUser(userId); - emailVerificationTokenRepository.delete(emailVerificationToken); + return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken()); + } - Optional byUserIdAndIsActive = authTokenRepository.findByUserIdAndIsActive(userId, true); - AuthToken authToken = byUserIdAndIsActive.orElseThrow(() -> - new TokenNotFoundException("Token not found with user id: " + userId)); + private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) { + + EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId()); + + if (token != null && !token.isExpired()) { + emailService.sendVerificationEmail(user, token.getToken()); + + log.info("Verification token resent for user {}", user.getEmail()); + return successResponse(request, authToken, "Verification code has been resent", false, false); + } + + if (token != null) { + emailVerificationTokenRepository.delete(token); + } + + EmailVerificationToken newToken = emailService.createEmailVerificationToken(authToken); + + emailService.sendVerificationEmail(user, newToken.getToken()); + + log.info("New verification token generated for user {}", user.getEmail()); + + return successResponse(request, authToken, "New verification code has been sent", + false, false); + } + + + private BaseResponse handleVerify(BaseRequest request, User user, AuthToken authToken, String providedToken) { + + EmailVerificationToken token = emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), providedToken); + + if (token == null || token.isExpired()) { + throw new NotValidFieldException("Invalid or expired token", + new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(), + MessageCode.INVALID_TOKEN.getDescription(), Map.of("userId", user.getId()))); + } + + User updateUser = userService.activateAndVerifyUser(user.getId()); + emailVerificationTokenRepository.delete(token); + + return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(), + updateUser.isEmailVerified()); + } + + private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive, + boolean isVerified) { RegAnswer regAnswer = new RegAnswer(); regAnswer.setToken(authToken.getToken()); + regAnswer.setVerified(isVerified); + regAnswer.setActive(isActive); - return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), - MessageCode.SUCCESS.getDescription(), regAnswer); + return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer); } } diff --git a/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java index 34a3233..91649d8 100644 --- a/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java @@ -11,4 +11,5 @@ public interface EmailVerificationTokenRepository extends JpaRepository findByToken(String token); EmailVerificationToken findByUserIdAndToken(Long userId, String token); List findByExpiresAtBefore(LocalDateTime expiresAtBefore); + EmailVerificationToken findByUserId(Long userId); } diff --git a/src/main/java/ru/soune/nocopy/repository/UserRepository.java b/src/main/java/ru/soune/nocopy/repository/UserRepository.java index af1a3d9..b0e8032 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -7,7 +7,7 @@ import ru.soune.nocopy.entity.user.User; import java.util.Optional; public interface UserRepository extends JpaRepository { - Optional findByEmail(String email); + User findByEmail(String email); boolean existsByEmail(String email); boolean existsByPhone(@Size(min = 11, max = 14) String phone); } diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index d410236..e8d3259 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -50,17 +50,14 @@ public class AuthService { User savedUser = userRepository.save(user); - AuthToken authToken = new AuthToken(); - authToken.setToken(generateAuthToken()); - authToken.setUser(savedUser); + AuthToken authToken = genereateAuthToken(savedUser); return authTokenRepository.save(authToken); } @Transactional public AuthToken login(LoginRequest request) { - Optional userOpt = userRepository.findByEmail(request.getEmail()); - User user = userOpt.get(); + User user = userRepository.findByEmail(request.getEmail()); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { LoginAnswer loginAnswer = new LoginAnswer(); @@ -87,6 +84,18 @@ public class AuthService { .ifPresent(authTokenRepository::delete); } + public AuthToken generateAuthToken(User user) { + return authTokenRepository.save(genereateAuthToken(user)); + } + + private AuthToken genereateAuthToken(User user) { + AuthToken authToken = new AuthToken(); + authToken.setToken(generateAuthToken()); + authToken.setUser(user); + + return authToken; + } + private String generateAuthToken() { byte[] bytes = new byte[32]; secureRandom.nextBytes(bytes); diff --git a/src/main/java/ru/soune/nocopy/service/user/UserService.java b/src/main/java/ru/soune/nocopy/service/user/UserService.java index 56c6341..efce0e4 100644 --- a/src/main/java/ru/soune/nocopy/service/user/UserService.java +++ b/src/main/java/ru/soune/nocopy/service/user/UserService.java @@ -47,13 +47,13 @@ public class UserService { } @Transactional - public void activateAndVerifyUser(long userId) { + public User activateAndVerifyUser(long userId) { Optional users = userRepository.findById(userId); User user = users.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); - user.setIsActive(true); + user.setActive(true); user.setEmailVerified(true); - userRepository.save(user); + return userRepository.save(user); } private void updateUserFromRequest(User user, UserRequest request) { -- 2.54.0 From 251bb3710509890f9bae27e040da744a612c475f Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 21 Jan 2026 12:21:14 +0700 Subject: [PATCH 05/10] fix authService --- .../nocopy/controller/ApiController.java | 3 +- .../nocopy/handler/AuthRequestHandler.java | 2 +- .../nocopy/handler/FileUploadHandler.java | 2 +- .../nocopy/handler/LogoutRequestHandler.java | 6 +- .../handler/VerifyRegisterUserHandler.java | 2 +- .../repository/AuthTokenRepository.java | 1 + .../nocopy/service/auth/AuthService.java | 104 --------------- .../auth/CleanupTokenSessionsService.java | 2 +- .../nocopy/service/register/AuthService.java | 126 ++++++++++++++++++ .../service/user/UserContentService.java | 72 ---------- .../soune/nocopy/service/AuthServiceTest.java | 102 -------------- 11 files changed, 135 insertions(+), 287 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/service/auth/AuthService.java delete mode 100644 src/main/java/ru/soune/nocopy/service/user/UserContentService.java delete mode 100644 src/test/java/ru/soune/nocopy/service/AuthServiceTest.java diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index ca16d18..371a750 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -19,12 +19,11 @@ import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.UploadProgress; -import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; -import ru.soune.nocopy.service.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; diff --git a/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java index c79ec06..aca3926 100644 --- a/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/AuthRequestHandler.java @@ -9,7 +9,7 @@ import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.TokenProcessRequest; import ru.soune.nocopy.exception.NotFoundAuthToken; -import ru.soune.nocopy.service.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import java.util.Map; diff --git a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 193f01b..c4ae60a 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -17,7 +17,7 @@ import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.validator.FileUploadRequestValidator; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.FileUploadSessionRepository; -import ru.soune.nocopy.service.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileUploadService; import java.util.*; diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index 4fa77b1..3216d16 100644 --- a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java @@ -33,16 +33,16 @@ public class LogoutRequestHandler implements RequestHandler { LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class); Optional opToken = authTokenRepository.findByToken(logoutRequest.getToken()); - Optional user = userRepository.findByEmail(logoutRequest.getEmail()); + User user = userRepository.findByEmail(logoutRequest.getEmail()); - if (opToken.isEmpty() || user.isEmpty()) { + if (opToken.isEmpty() || user == null) { throw new NotValidFieldException("User with email or token not found", new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(), new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken()))); } - if (!Objects.equals(opToken.get().getUser().getId(), user.get().getId())) { + if (!Objects.equals(opToken.get().getUser().getId(), user.getId())) { throw new NotValidFieldException("User token mismatch with user", new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(), MessageCode.AUTH_TOKEN_MISMATCH.getDescription(), diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index 50f2dc0..00a1e10 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -51,7 +51,7 @@ public class VerifyRegisterUserHandler implements RequestHandler { User user = userRepository.findById(userId) .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); - Optional userAuthTokens = authTokenRepository.findByUserIdAndIsActive(userId, false); + Optional userAuthTokens = authTokenRepository.findByUserId(userId); AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get(); diff --git a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java index 9dbc08e..08b2ee5 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -14,4 +14,5 @@ public interface AuthTokenRepository extends JpaRepository { Optional findByLastUsedAtBefore(LocalDateTime lastUsedAt); Optional findByToken(String token); Optional findByUserIdAndIsActive(Long userId, boolean isActive); + Optional findByUserId(Long userId); } diff --git a/src/main/java/ru/soune/nocopy/service/auth/AuthService.java b/src/main/java/ru/soune/nocopy/service/auth/AuthService.java deleted file mode 100644 index e8d3259..0000000 --- a/src/main/java/ru/soune/nocopy/service/auth/AuthService.java +++ /dev/null @@ -1,104 +0,0 @@ -package ru.soune.nocopy.service.register; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.MessageSource; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.dto.*; -import ru.soune.nocopy.dto.register.LoginAnswer; -import ru.soune.nocopy.dto.register.LoginRequest; -import ru.soune.nocopy.dto.register.RegRequest; -import ru.soune.nocopy.entity.user.AuthToken; -import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.exception.NotValidFieldException; -import ru.soune.nocopy.repository.AuthTokenRepository; -import ru.soune.nocopy.repository.UserRepository; - -import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.util.*; - -@Service -@RequiredArgsConstructor -public class AuthService { - - private final UserRepository userRepository; - - private final AuthTokenRepository authTokenRepository; - - private final PasswordEncoder passwordEncoder; - - private final MessageSource messageSource; - - private final SecureRandom secureRandom = new SecureRandom(); - - @Transactional - public AuthToken register(RegRequest registerRequest) { - User user = new User(); - user.setFullName(registerRequest.getFullName()); - user.setEmail(registerRequest.getEmail()); - user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); - - if (registerRequest.getCompanyName() != null) { - user.setCompany(registerRequest.getCompanyName()); - } - - if (registerRequest.getPhone() != null) { - user.setPhone(registerRequest.getPhone()); - } - - User savedUser = userRepository.save(user); - - AuthToken authToken = genereateAuthToken(savedUser); - - return authTokenRepository.save(authToken); - } - - @Transactional - public AuthToken login(LoginRequest request) { - User user = userRepository.findByEmail(request.getEmail()); - - if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - LoginAnswer loginAnswer = new LoginAnswer(); - loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword()))); - - throw new NotValidFieldException("Invalid password", new BaseResponse(20003, - MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(), - MessageCode.AUTH_PASSWORD_NOT_MATCHES.getDescription(), loginAnswer)); - } - - user.setLastLoginAt(LocalDateTime.now()); - User savedUser = userRepository.save(user); - - AuthToken authToken = new AuthToken(); - authToken.setToken(generateAuthToken()); - authToken.setUser(savedUser); - - return authTokenRepository.save(authToken); - } - - @Transactional - public void logout(String token) { - authTokenRepository.findByToken(token) - .ifPresent(authTokenRepository::delete); - } - - public AuthToken generateAuthToken(User user) { - return authTokenRepository.save(genereateAuthToken(user)); - } - - private AuthToken genereateAuthToken(User user) { - AuthToken authToken = new AuthToken(); - authToken.setToken(generateAuthToken()); - authToken.setUser(user); - - return authToken; - } - - private String generateAuthToken() { - byte[] bytes = new byte[32]; - secureRandom.nextBytes(bytes); - return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); - } -} diff --git a/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java b/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java index 5ac5128..9d7045b 100644 --- a/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java +++ b/src/main/java/ru/soune/nocopy/service/auth/CleanupTokenSessionsService.java @@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.repository.AuthTokenRepository; import java.time.LocalDateTime; diff --git a/src/main/java/ru/soune/nocopy/service/register/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java index e69de29..1038e63 100644 --- a/src/main/java/ru/soune/nocopy/service/register/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -0,0 +1,126 @@ +package ru.soune.nocopy.service.register; + +import lombok.RequiredArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import ru.soune.nocopy.dto.*; +import ru.soune.nocopy.dto.register.LoginAnswer; +import ru.soune.nocopy.dto.register.LoginRequest; +import ru.soune.nocopy.dto.register.RegRequest; +import ru.soune.nocopy.entity.user.AuthToken; +import ru.soune.nocopy.entity.user.User; +import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.UserRepository; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.*; + +@Service +@RequiredArgsConstructor +public class AuthService { + + private final UserRepository userRepository; + + private final AuthTokenRepository authTokenRepository; + + private final PasswordEncoder passwordEncoder; + + private final MessageSource messageSource; + + private final SecureRandom secureRandom = new SecureRandom(); + + @Transactional + public AuthToken register(RegRequest registerRequest) { + User user = new User(); + user.setFullName(registerRequest.getFullName()); + user.setEmail(registerRequest.getEmail()); + user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); + + if (registerRequest.getCompanyName() != null) { + user.setCompany(registerRequest.getCompanyName()); + } + + if (registerRequest.getPhone() != null) { + user.setPhone(registerRequest.getPhone()); + } + + User savedUser = userRepository.save(user); + + AuthToken authToken = genereateAuthToken(savedUser); + + return authTokenRepository.save(authToken); + } + + @Transactional + public Long useUserAuthToken(String token) { + if (token == null || token.isBlank()) { + throw new NotFoundAuthToken("Token is required"); + } + + if (token.startsWith("Bearer ")) { + token = token.replace("Bearer ", ""); + } + + AuthToken authToken = authTokenRepository.findByToken(token) + .orElseThrow(() -> new NotFoundAuthToken("Token not found")); + authToken.setLastUsedAt(LocalDateTime.now()); + authToken.setExpiresAt(LocalDateTime.now().plusHours(1)); + + authTokenRepository.save(authToken); + + return authToken.getUser().getId(); + } + + + @Transactional + public AuthToken login(LoginRequest request) { + User user = userRepository.findByEmail(request.getEmail()); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + LoginAnswer loginAnswer = new LoginAnswer(); + loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword()))); + + throw new NotValidFieldException("Invalid password", new BaseResponse(20003, + MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(), + MessageCode.AUTH_PASSWORD_NOT_MATCHES.getDescription(), loginAnswer)); + } + + user.setLastLoginAt(LocalDateTime.now()); + User savedUser = userRepository.save(user); + + AuthToken authToken = new AuthToken(); + authToken.setToken(generateAuthToken()); + authToken.setUser(savedUser); + + return authTokenRepository.save(authToken); + } + + @Transactional + public void logout(String token) { + authTokenRepository.findByToken(token) + .ifPresent(authTokenRepository::delete); + } + + public AuthToken generateAuthToken(User user) { + return authTokenRepository.save(genereateAuthToken(user)); + } + + private AuthToken genereateAuthToken(User user) { + AuthToken authToken = new AuthToken(); + authToken.setToken(generateAuthToken()); + authToken.setUser(user); + + return authToken; + } + + private String generateAuthToken() { + byte[] bytes = new byte[32]; + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); + } +} diff --git a/src/main/java/ru/soune/nocopy/service/user/UserContentService.java b/src/main/java/ru/soune/nocopy/service/user/UserContentService.java deleted file mode 100644 index 63510e8..0000000 --- a/src/main/java/ru/soune/nocopy/service/user/UserContentService.java +++ /dev/null @@ -1,72 +0,0 @@ -package ru.soune.nocopy.service.user; - -import lombok.AllArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import ru.soune.nocopy.dto.user.UserContentRequest; -import ru.soune.nocopy.dto.user.UserContentUpdateRequest; -import ru.soune.nocopy.entity.file.FileType; -import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.entity.user.UserContent; -import ru.soune.nocopy.exception.ContentNotFoundException; -import ru.soune.nocopy.exception.UserNotFoundException; -import ru.soune.nocopy.mapper.UserContentMapper; -import ru.soune.nocopy.repository.UserContentRepository; -import ru.soune.nocopy.repository.UserRepository; - -import java.util.List; -import java.util.Optional; - -@Service -@AllArgsConstructor -public class UserContentService { - - private final UserContentRepository userContentRepository; - - private final UserRepository userRepository; - - private final UserContentMapper userContentMapper; - - public List findByUser(long userId) { - return userContentRepository.findByUserId(userId); - } - - @Transactional - public UserContent addUserContent(UserContentRequest userContentRequest) { - UserContent userContent = new UserContent(); - - Optional byEmail = userRepository.findByEmail(userContentRequest.userEmail()); - User user = byEmail.orElseThrow(() -> new UserNotFoundException(userContentRequest.userEmail())); - - userContent.setUser(user); - userContent.setFilename(userContentRequest.fileName()); - userContent.setFileExtension(userContentRequest.fileExtension()); - userContent.setFilePath(userContentRequest.filePath()); - userContent.setFileType(FileType.valueOf(userContentRequest.fileType())); - userContent.setOriginalFilename(userContentRequest.originalFilename()); - userContent.setProtectionLevel(userContentRequest.protectionLevel()); - userContent.setFileSize(userContentRequest.fileSize()); - - //TODO add - // @Column(name = "protection_hash", length = 64) - // private String protectionHash; - // @Column(name = "watermark_id", length = 50) - // private String watermarkId; - - return userContentRepository.save(userContent); - } - - @Transactional - public UserContent updateUserContent(UserContentUpdateRequest userContentRequest) { - UserContent entity = userContentRepository.findById(userContentRequest.id()) - .orElseThrow(() -> new ContentNotFoundException("Content not found with: " + userContentRequest.id())); - - userContentMapper.updateEntityFromDto(userContentRequest, entity); - - return userContentRepository.save(entity); - } - - public void deleteUserContent(long userContentId) { - userContentRepository.deleteById(userContentId); - } -} diff --git a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java b/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java deleted file mode 100644 index 574ab25..0000000 --- a/src/test/java/ru/soune/nocopy/service/AuthServiceTest.java +++ /dev/null @@ -1,102 +0,0 @@ -package ru.soune.nocopy.service; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.springframework.context.MessageSource; -import org.springframework.security.crypto.password.PasswordEncoder; -import ru.soune.nocopy.dto.register.LoginRequest; -import ru.soune.nocopy.entity.user.AuthToken; -import ru.soune.nocopy.entity.user.User; -import ru.soune.nocopy.exception.NotValidationPasswordException; -import ru.soune.nocopy.exception.UserNotFoundException; -import ru.soune.nocopy.repository.AuthTokenRepository; -import ru.soune.nocopy.repository.UserRepository; -import ru.soune.nocopy.service.register.AuthService; - -import java.util.Optional; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class AuthServiceTest { - - @Mock - private UserRepository userRepository; - - @Mock - private AuthTokenRepository authTokenRepository; - - @Mock - private PasswordEncoder passwordEncoder; - - @Mock - private MessageSource messageSource; - - @InjectMocks - private AuthService authService; - - @BeforeEach - void setUp() { - MockitoAnnotations.openMocks(this); - } - - @Test - void login_success() { - LoginRequest request = new LoginRequest("test@mail.com", "password"); - - User user = new User(); - user.setId(1L); - user.setEmail("test@mail.com"); - user.setPassword("encoded_pass"); - - when(userRepository.findByEmail("test@mail.com")) - .thenReturn(Optional.of(user)); - when(passwordEncoder.matches("password", "encoded_pass")) - .thenReturn(true); - when(authTokenRepository.save(any(AuthToken.class))) - .thenAnswer(invocation -> invocation.getArgument(0)); - - AuthToken token = authService.login(request); - - assertNotNull(token); - assertNotNull(token.getToken()); -// assertEquals(user.getTokens().get(0), token.getUser()); - - ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); - verify(authTokenRepository).save(captor.capture()); - -// assertEquals(user, captor.getValue().getUser()); - assertNotNull(captor.getValue().getToken()); - } - - - @Test - void login_userNotFound() { - LoginRequest request = new LoginRequest("missing@mail.com", "pwd"); - - when(userRepository.findByEmail("missing@mail.com")).thenReturn(Optional.empty()); - - assertThrows(UserNotFoundException.class, () -> authService.login(request)); - } - - - @Test - void login_invalidPassword() { - LoginRequest request = new LoginRequest("test@mail.com", "wrong"); - - User user = new User(); - user.setEmail("test@mail.com"); - user.setPassword("encoded_pass"); - - when(userRepository.findByEmail("test@mail.com")).thenReturn(Optional.of(user)); - when(passwordEncoder.matches("wrong", "encoded_pass")).thenReturn(false); - - assertThrows(NotValidationPasswordException.class, () -> authService.login(request)); - } -} -- 2.54.0 From d1b5b6eb5c362d653a57ae2b5fdf2b2789e89526 Mon Sep 17 00:00:00 2001 From: vladp Date: Wed, 21 Jan 2026 12:34:40 +0700 Subject: [PATCH 06/10] add check user --- src/main/java/ru/soune/nocopy/controller/ApiController.java | 3 +++ src/main/java/ru/soune/nocopy/dto/MessageCode.java | 1 + 2 files changed, 4 insertions(+) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 371a750..a377eee 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -70,6 +70,9 @@ public class ApiController { return ResponseEntity.ok().body(response); } catch (ValidationException e) { return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); + } catch (UserNotFoundException e) { + return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.USER_NOT_FOUND.getCode(), + MessageCode.USER_NOT_FOUND.getDescription(), new HashMap<>())); } catch (NotFoundAuthToken e) { return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>())); diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 78612e6..6070af8 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -11,6 +11,7 @@ public enum MessageCode { FILE_UPLOAD_ERROR(2, "File upload error"), FILE_DOWNLOAD_ERROR(2, "File download error"), USER_NOT_VERIFIED(2, "User not verified"), + USER_NOT_FOUND(2, "User not found"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), IMAGE_FOUND_ERROR(2, "Image found error"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"), -- 2.54.0 From 456092518340d5cdff6c2e52f444ee4a3b72634c Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 23 Jan 2026 16:09:51 +0700 Subject: [PATCH 07/10] NCBACK-25 --- .../java/ru/soune/nocopy/dto/MessageCode.java | 3 ++- .../soune/nocopy/dto/register/RegAnswer.java | 4 +++ .../nocopy/handler/RegRequestHandler.java | 14 ++++++++-- .../handler/VerifyRegisterUserHandler.java | 18 ++++++++----- .../nocopy/service/mail/EmailService.java | 26 ++++++++----------- 5 files changed, 40 insertions(+), 25 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/dto/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 6070af8..1f3cc27 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -24,7 +24,8 @@ public enum MessageCode { AUTH_TOKEN_MISMATCH(4, "Token mismatch"), AUTH_TOKEN_NOT_FOUND(4, "Token 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"),; private final Integer code; diff --git a/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java index a1cbd01..32ccda7 100644 --- a/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java +++ b/src/main/java/ru/soune/nocopy/dto/register/RegAnswer.java @@ -17,5 +17,9 @@ public class RegAnswer { private boolean isVerified; + private Long userId; + + private String email; + private List> fieldErrors; } \ No newline at end of file diff --git a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index b220ca5..dca6ca5 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -61,14 +61,24 @@ public class RegRequestHandler implements RequestHandler { AuthToken authToken = authService.register(regRequest); EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); - emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + "message", "not send", + "email", regRequest.getEmail() + )); + } + + String token = authToken.getToken(); authService.useUserAuthToken(token); RegAnswer regAnswer = new RegAnswer(); - regAnswer.setToken(token); regAnswer.setVerified(false); regAnswer.setActive(false); + regAnswer.setUserId(authToken.getUser().getId()); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), MessageCode.SUCCESS.getDescription(), regAnswer); diff --git a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java index 00a1e10..373b953 100644 --- a/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -1,6 +1,7 @@ package ru.soune.nocopy.handler; import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.mail.MessagingException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; @@ -21,6 +22,7 @@ import ru.soune.nocopy.service.mail.EmailService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.UserService; +import java.io.IOException; import java.util.Map; import java.util.Optional; @@ -43,7 +45,7 @@ public class VerifyRegisterUserHandler implements RequestHandler { private final AuthService authService; @Override - public BaseResponse handle(BaseRequest request) { + public BaseResponse handle(BaseRequest request) throws MessagingException, IOException { VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class); Long userId = verifyUserRequest.getUserId(); @@ -62,7 +64,7 @@ public class VerifyRegisterUserHandler implements RequestHandler { return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken()); } - private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) { + private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) throws MessagingException, IOException { EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId()); @@ -70,7 +72,8 @@ public class VerifyRegisterUserHandler implements RequestHandler { emailService.sendVerificationEmail(user, token.getToken()); log.info("Verification token resent for user {}", user.getEmail()); - return successResponse(request, authToken, "Verification code has been resent", false, false); + return successResponse(request, authToken, "Verification code has been resent", false, + false, user.getEmail()); } if (token != null) { @@ -84,7 +87,7 @@ public class VerifyRegisterUserHandler implements RequestHandler { log.info("New verification token generated for user {}", user.getEmail()); return successResponse(request, authToken, "New verification code has been sent", - false, false); + false, false, user.getEmail()); } @@ -99,19 +102,20 @@ public class VerifyRegisterUserHandler implements RequestHandler { } User updateUser = userService.activateAndVerifyUser(user.getId()); + emailVerificationTokenRepository.delete(token); return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(), - updateUser.isEmailVerified()); + updateUser.isEmailVerified(), updateUser.getEmail()); } private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive, - boolean isVerified) { - + boolean isVerified, String email) { RegAnswer regAnswer = new RegAnswer(); regAnswer.setToken(authToken.getToken()); regAnswer.setVerified(isVerified); regAnswer.setActive(isActive); + regAnswer.setEmail(email); return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer); } diff --git a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java index cc3b1cb..d6cf147 100644 --- a/src/main/java/ru/soune/nocopy/service/mail/EmailService.java +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -1,5 +1,6 @@ package ru.soune.nocopy.service.mail; +import jakarta.mail.MessagingException; import jakarta.mail.internet.MimeMessage; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -37,25 +38,20 @@ public class EmailService { @Value("${app.email.verification.user}") private String user; - public void sendVerificationEmail(User user, String code) { - try { - MimeMessage message = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); + public void sendVerificationEmail(User user, String code) throws MessagingException, IOException { + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); - helper.setFrom(standartAdressFrom); - helper.setTo(user.getEmail()); - helper.setSubject(standartSubject); + helper.setFrom(standartAdressFrom); + helper.setTo(user.getEmail()); + helper.setSubject(standartSubject); - String htmlContent = loadAndProcessTemplate(user.getFullName(), code); - helper.setText(htmlContent, true); + String htmlContent = loadAndProcessTemplate(user.getFullName(), code); + helper.setText(htmlContent, true); - mailSender.send(message); + mailSender.send(message); - log.info("Email sent to: {}", user.getEmail()); - - } catch (Exception e) { - log.error("Failed to send email: {}", e.getMessage()); - } + log.info("Email sent to: {}", user.getEmail()); } @Transactional -- 2.54.0 From 8555002415752ade53bc862ba5a815a95879eb93 Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 24 Jan 2026 00:45:57 +0700 Subject: [PATCH 08/10] NCBACK-33 fix check null user --- src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java index 27bfd0f..6271328 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -14,7 +14,6 @@ import ru.soune.nocopy.service.register.AuthService; import java.util.Arrays; import java.util.Map; -import java.util.Optional; @Component @RequiredArgsConstructor @@ -31,7 +30,7 @@ public class LoginRequestHandler implements RequestHandler { LoginRequest loginRequest = objectMapper.convertValue(request.getMessageBody(), LoginRequest.class); User user = userRepository.findByEmail(loginRequest.getEmail()); - if (user != null) { + if (user == null) { LoginAnswer loginAnswer = new LoginAnswer(); loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail()))); -- 2.54.0 From 349edbeb91543301084f06dbdd3233ab2fbe498a Mon Sep 17 00:00:00 2001 From: vladp Date: Sat, 24 Jan 2026 11:32:02 +0700 Subject: [PATCH 09/10] fix --- .../java/ru/soune/nocopy/controller/ApiController.java | 7 +++---- .../ru/soune/nocopy/service/file/FileEntityService.java | 2 +- .../soune/nocopy/service/file/FileUploadServiceImpl.java | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index 3ae0515..854b77a 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -17,16 +17,15 @@ import org.springframework.web.multipart.MultipartFile; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; +import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.register.RegAnswer; -import ru.soune.nocopy.dto.file.ChunkUploadResponse; -import ru.soune.nocopy.dto.file.CompleteUploadResponse; -import ru.soune.nocopy.dto.file.FileEntityResponse; -import ru.soune.nocopy.dto.file.UploadProgress; +import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.exception.*; import ru.soune.nocopy.handler.*; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index 1f3464e..bfc8123 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -57,7 +57,7 @@ public class FileEntityService { if (!duplicatedByHash.isEmpty()) { SimilarImageProjection similarImageProjection = duplicatedByHash.get(0); - throw new DuplicateImageException("Duplicate", similarImageProjection.getFileId(), + throw new DuplicateImageException("Duplicate", similarImageProjection.getId(), similarImageProjection.getUserId()); } } diff --git a/src/main/java/ru/soune/nocopy/service/file/FileUploadServiceImpl.java b/src/main/java/ru/soune/nocopy/service/file/FileUploadServiceImpl.java index 587cdc1..05b5dcd 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileUploadServiceImpl.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileUploadServiceImpl.java @@ -375,7 +375,7 @@ public class FileUploadServiceImpl implements FileUploadService { hash.get("hi"), hash.get("low")); if (!duplicates.isEmpty()) { - throw new DuplicateImageException("Duplicate", duplicates.get(0).getFileId(), + throw new DuplicateImageException("Duplicate", duplicates.get(0).getId(), duplicates.get(0).getUserId()); } } -- 2.54.0 From 5700adafb2b017fe81a59f9dbd877074684cade8 Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 27 Jan 2026 08:09:54 +0700 Subject: [PATCH 10/10] NCBACK-33 --- .../nocopy/controller/ApiController.java | 8 +-- .../controller/UserContentController.java | 11 ---- .../nocopy/controller/UserController.java | 59 +++++++++++++++++++ .../nocopy/entity/file/ImageProtection.java | 0 .../ru/soune/nocopy/entity/user/User.java | 1 - .../nocopy/handler/FileEntityHandler.java | 14 ++++- .../handler/ImageFoundRequestHandler.java | 4 +- .../service/file/FileEntityService.java | 33 +++++++++-- .../GoogleVisionSearchService.java | 2 +- 9 files changed, 103 insertions(+), 29 deletions(-) delete mode 100644 src/main/java/ru/soune/nocopy/controller/UserContentController.java delete mode 100644 src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java rename src/main/java/ru/soune/nocopy/service/{ => search}/GoogleVisionSearchService.java (99%) diff --git a/src/main/java/ru/soune/nocopy/controller/ApiController.java b/src/main/java/ru/soune/nocopy/controller/ApiController.java index d1799fc..7cf63c3 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -114,7 +114,7 @@ public class ApiController { @RequestParam(value = "upload_id", required = false) String uploadId, @RequestParam(value = "chunk_number", required = false) Integer chunkNumber, @RequestParam(value = "chunk", required = false) MultipartFile chunk, - @RequestParam(value = "findSimilar", required = false) Integer findSimilar) { + @RequestParam(value = "findSimilar", required = false, defaultValue = "0") Integer findSimilar) { try { if (chunk == null || chunk.isEmpty()) { return buildErrorResponse(uploadId, chunkNumber, "Chunk file null or empty"); @@ -153,11 +153,11 @@ public class ApiController { } } - @GetMapping("/v{version}/files/{fileId}/similar/{authToken}") + @GetMapping("/v{version}/files/{fileId}/similar") public ResponseEntity findSimilarFiles( @PathVariable("version") int version, @PathVariable String fileId, - @PathVariable(required = false) String authToken, + @RequestParam(value = "auth_token",required = false) String authToken, @RequestParam(required = false) List similarityLevels, @PageableDefault(size = 20, sort = "hammingDistance") Pageable pageable) { SimilarityFilter filter = SimilarityFilter.builder() @@ -443,7 +443,7 @@ public class ApiController { throws IOException { fileEntityService.deleteFromDisk(fileEntity); - fileEntityService.markAsDeleted(fileEntity); + fileEntityService.softDeleteFileWithHash(fileEntity); Optional originalFile = fileEntityRepository.findById(similarFiles.get(0).getFileId()); diff --git a/src/main/java/ru/soune/nocopy/controller/UserContentController.java b/src/main/java/ru/soune/nocopy/controller/UserContentController.java deleted file mode 100644 index 51a4d5c..0000000 --- a/src/main/java/ru/soune/nocopy/controller/UserContentController.java +++ /dev/null @@ -1,11 +0,0 @@ -package ru.soune.nocopy.controller; - -import lombok.AllArgsConstructor; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController -@RequestMapping("v1/api/content") -@AllArgsConstructor -public class UserContentController { -} diff --git a/src/main/java/ru/soune/nocopy/controller/UserController.java b/src/main/java/ru/soune/nocopy/controller/UserController.java index eb12863..f4f0ffd 100644 --- a/src/main/java/ru/soune/nocopy/controller/UserController.java +++ b/src/main/java/ru/soune/nocopy/controller/UserController.java @@ -2,20 +2,29 @@ package ru.soune.nocopy.controller; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.web.bind.annotation.*; +import ru.soune.nocopy.dto.BaseResponse; +import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.register.ChangePasswordRequest; +import ru.soune.nocopy.dto.register.RegAnswer; +import ru.soune.nocopy.dto.register.RegRequest; import ru.soune.nocopy.dto.user.UserDTO; import ru.soune.nocopy.dto.user.UserRequest; import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.user.User; import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.NotFoundAuthToken; +import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.mapper.UserMapper; import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.user.UserService; +import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Optional; @RestController @@ -31,6 +40,10 @@ public class UserController { private final UserMapper userMapper; + private final PasswordEncoder passwordEncoder; + + private final AuthService authService; + @GetMapping("/all") public ResponseEntity> getAllUsers() { List allUsers = userRepository.findAll().stream() @@ -108,4 +121,50 @@ public class UserController { return ResponseEntity.ok(userService.updateUser(userRequest, user)); } + + @PostMapping("/create-user") + public ResponseEntity addUser(@RequestBody RegRequest registerRequest) { + + if (userRepository.existsByEmail(registerRequest.getEmail()) || + userRepository.existsByPhone(registerRequest.getPhone())) { + RegAnswer regAnswer = new RegAnswer(); + regAnswer.setFieldErrors(Arrays.asList(Map.of("email", registerRequest.getEmail()))); + regAnswer.setFieldErrors(Arrays.asList(Map.of("phone", registerRequest.getPhone()))); + + throw new NotValidFieldException("User already exists with email:" + registerRequest.getEmail() + " or phone: " + + registerRequest.getPhone(), new BaseResponse(2, + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getCode(), + MessageCode.REG_EMAIL_OR_PHONE_EXISTS.getDescription(), regAnswer)); + } + + User user = new User(); + user.setFullName(registerRequest.getFullName()); + user.setEmail(registerRequest.getEmail()); + user.setPassword(passwordEncoder.encode(registerRequest.getPassword())); + user.setActive(true); + user.setEmailVerified(true); + + if (registerRequest.getCompanyName() != null) { + user.setCompany(registerRequest.getCompanyName()); + } + + if (registerRequest.getPhone() != null) { + user.setPhone(registerRequest.getPhone()); + } + + User savedUser = userRepository.save(user); + + AuthToken authToken = authService.generateAuthToken(savedUser); + + authTokenRepository.save(authToken); + + return ResponseEntity.ok().body(savedUser); + } + + @DeleteMapping("/delete-user/{userId}") + public ResponseEntity addUser(@PathVariable Long userId) { + userRepository.deleteById(userId); + return ResponseEntity.ok().body(Map.of("userId", userId, "deleted", "true")); + } + } diff --git a/src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java b/src/main/java/ru/soune/nocopy/entity/file/ImageProtection.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/ru/soune/nocopy/entity/user/User.java b/src/main/java/ru/soune/nocopy/entity/user/User.java index d6a1867..6346c0d 100644 --- a/src/main/java/ru/soune/nocopy/entity/user/User.java +++ b/src/main/java/ru/soune/nocopy/entity/user/User.java @@ -6,7 +6,6 @@ import jakarta.validation.constraints.Size; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; -import ru.soune.nocopy.entity.file.ImageProtection; import ru.soune.nocopy.entity.file.Violation; import java.time.LocalDate; diff --git a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 5603581..2d6fcdf 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -6,12 +6,12 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.file.*; -import ru.soune.nocopy.entity.user.AuthToken; import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileStatsService; @@ -34,6 +34,8 @@ public class FileEntityHandler implements RequestHandler { private final FileEntityRepository fileEntityRepository; + private final ImageHashRepository imageHashRepository; + @Override public BaseResponse handle(BaseRequest request) { try { @@ -273,15 +275,21 @@ public class FileEntityHandler implements RequestHandler { FileEntity fileEntity = fileEntityRepository.findById(fileId) .orElseThrow(() -> new FileEntityNotFoundException(fileId)); + int fullDelete = fileRequest.getFullDelete() == null ? 0 : fileRequest.getFullDelete(); - if (fileEntity.getStatus().equals(FileStatus.DELETED) || fileRequest.getFullDelete() == 1) { + if (fileEntity.getStatus().equals(FileStatus.DELETED) || fullDelete == 1) { fileEntityService.deleteFromDisk(fileEntity); response = DeleteFileResponse.builder() .fileId(fileRequest.getFileId()) .message("File deleted from disk") .build(); } else { - fileEntityService.markAsDeleted(fileEntity); + fileEntityService.softDeleteFileWithHash(fileEntity); +// +// imageHashRepository.deleteByFileId(fileEntity.getId()); +// +// fileEntityService.markAsDeleted(fileEntity); + response = DeleteFileResponse.builder() .fileId(fileRequest.getFileId()) .message("File marked as deleted") diff --git a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index 79d6ac6..e82ca62 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -7,12 +7,10 @@ import org.springframework.stereotype.Component; import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.MessageCode; -import ru.soune.nocopy.dto.file.GoogleVisionSearchResponse; import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.service.search.YandexSearchService; -import ru.soune.nocopy.service.GoogleVisionSearchService; -import ru.soune.nocopy.service.YandexSearchService; +import ru.soune.nocopy.service.search.GoogleVisionSearchService; @Slf4j @Component diff --git a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java index fd4c8bc..6aa130a 100644 --- a/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java +++ b/src/main/java/ru/soune/nocopy/service/file/FileEntityService.java @@ -13,6 +13,7 @@ import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.exception.DuplicateImageException; import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.repository.FileEntityRepository; +import ru.soune.nocopy.repository.ImageHashRepository; import ru.soune.nocopy.repository.SimilarImageProjection; import ru.soune.nocopy.service.FileSimilarityService; import ru.soune.nocopy.service.ImageHashService; @@ -38,6 +39,8 @@ public class FileEntityService { private final FileSimilarityService fileSimilarityService; + private final ImageHashRepository imageHashRepository; + @Transactional(noRollbackFor = DuplicateImageException.class) public FileEntity createFromUploadSession(FileUploadSession session, String checksum) { log.info("Creating FileEntity for upload session: {}", session.getUploadId()); @@ -185,27 +188,45 @@ public class FileEntityService { } @Transactional - public void markAsDeleted(FileEntity fileEntity) { + public FileEntity markAsDeleted(FileEntity fileEntity) throws IOException { fileEntity.setStatus(FileStatus.DELETED); fileEntity.setUpdatedAt(LocalDateTime.now()); + fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); + + Path path = Paths.get(fileEntity.getProtectedFilePath()); + Files.deleteIfExists(path); + fileEntity.setProtectedFilePath(""); + + return fileEntityRepository.save(fileEntity); + } + + public void softDeleteFileWithHash(FileEntity fileEntity) throws IOException { + if (fileEntity.getImageHash() != null) { + fileEntity.setImageHash(null); + } + + fileEntity.setStatus(FileStatus.DELETED); + fileEntity.setUpdatedAt(LocalDateTime.now()); + fileEntity.setProtectionStatus(ProtectionStatus.NOT_PROTECTED); + + Path path = Paths.get(fileEntity.getProtectedFilePath()); + Files.deleteIfExists(path); + fileEntity.setProtectedFilePath(""); fileEntityRepository.save(fileEntity); } @Transactional - public boolean deleteFromDisk(FileEntity fileEntity) throws IOException { + public void deleteFromDisk(FileEntity fileEntity) throws IOException { Path path = Paths.get(fileEntity.getFilePath()); if (!Files.exists(path)) { - return true; + return; } Files.delete(path); -// markAsDeleted(fileEntity); fileEntityRepository.delete(fileEntity); - - return true; } @Transactional(readOnly = true) diff --git a/src/main/java/ru/soune/nocopy/service/GoogleVisionSearchService.java b/src/main/java/ru/soune/nocopy/service/search/GoogleVisionSearchService.java similarity index 99% rename from src/main/java/ru/soune/nocopy/service/GoogleVisionSearchService.java rename to src/main/java/ru/soune/nocopy/service/search/GoogleVisionSearchService.java index f0829a1..4e3e997 100644 --- a/src/main/java/ru/soune/nocopy/service/GoogleVisionSearchService.java +++ b/src/main/java/ru/soune/nocopy/service/search/GoogleVisionSearchService.java @@ -1,4 +1,4 @@ -package ru.soune.nocopy.service; +package ru.soune.nocopy.service.search; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -- 2.54.0