From 4704f780e5d47391044d41bcd42340e608c7c905 Mon Sep 17 00:00:00 2001 From: vladp Date: Fri, 16 Jan 2026 20:20:14 +0700 Subject: [PATCH] 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;