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/build.gradle b/build.gradle index 9623738..8b9437b 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,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 group: 'com.google.cloud', name: 'google-cloud-vision', version: '3.55.0' implementation group: 'com.google.api-client', name: 'google-api-client', version: '2.7.2' diff --git a/docker-compose.yaml b/docker-compose.yaml index 46b15b2..b16a68a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -17,6 +17,29 @@ 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 restart: always @@ -30,6 +53,8 @@ services: 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 @@ -60,8 +85,11 @@ services: JAVA_TOOL_OPTIONS: "-Xmx2g -Xms1g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:+UseContainerSupport -XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75" 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 @@ -71,6 +99,13 @@ services: 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 ports: @@ -239,7 +274,7 @@ volumes: loki_rules: uploads_data: prometheus_data: - +# postfix-data: networks: app-network: external: true diff --git a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java index d8cba33..69bbe52 100644 --- a/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java +++ b/src/main/java/ru/soune/nocopy/configuration/HandlerConfig.java @@ -18,6 +18,7 @@ public class HandlerConfig { FileEntityHandler file, LogoutRequestHandler logoutHandler, ImageFoundRequestHandler imageFoundRequestHandler, + VerifyRegisterUserHandler verifyRegisterUser, AuthRequestHandler authRequestHandler ) { Map map = new HashMap<>(); @@ -28,6 +29,7 @@ public class HandlerConfig { map.put(20006, logoutHandler); map.put(20007, imageFoundRequestHandler); map.put(20008, authRequestHandler); + 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 4b66f49..7cf63c3 100644 --- a/src/main/java/ru/soune/nocopy/controller/ApiController.java +++ b/src/main/java/ru/soune/nocopy/controller/ApiController.java @@ -20,13 +20,9 @@ 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.file.*; +import ru.soune.nocopy.dto.register.RegAnswer; import ru.soune.nocopy.entity.file.FileEntity; -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.FileStatus; import ru.soune.nocopy.entity.file.ProtectionStatus; import ru.soune.nocopy.entity.file.UploadStatus; @@ -34,7 +30,7 @@ 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.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.util.FileUtil; @@ -92,6 +88,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<>())); @@ -115,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"); @@ -154,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() @@ -444,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 39efe8e..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.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.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.UserService; +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,10 +40,14 @@ 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() - .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 +65,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); } @@ -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/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/MessageCode.java b/src/main/java/ru/soune/nocopy/dto/MessageCode.java index 3a4c540..8c5168d 100644 --- a/src/main/java/ru/soune/nocopy/dto/MessageCode.java +++ b/src/main/java/ru/soune/nocopy/dto/MessageCode.java @@ -6,10 +6,13 @@ 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"), DUPLICATE_FILE_UPLOAD(2, "Duplicate file upload"), 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"), @@ -23,6 +26,7 @@ public enum MessageCode { AUTH_TOKEN_NOT_FOUND(4, "Token not found"), FILE_NOT_FOUND(4, "File not found"), AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"), + SEND_EMAIL_EXCEPTION(2, "Send email exception"), SIMILAR_FILES_FOUND(0, "Similar files found"); private final Integer code; 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 73% 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..42a8e55 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; @@ -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/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 64% 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..32ccda7 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; @@ -13,5 +13,13 @@ import java.util.Map; public class RegAnswer { private String Token; + private boolean isActive; + + 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/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..8bb0c2a --- /dev/null +++ b/src/main/java/ru/soune/nocopy/dto/register/VerifyUserRequest.java @@ -0,0 +1,20 @@ +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; + + @JsonProperty("resend") + private Integer resend; +} 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 80% rename from src/main/java/ru/soune/nocopy/dto/UserDTO.java rename to src/main/java/ru/soune/nocopy/dto/user/UserDTO.java index 45c19ee..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.GenderType; -import ru.soune.nocopy.entity.SubscriptionType; +import ru.soune.nocopy.entity.user.GenderType; +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 88% rename from src/main/java/ru/soune/nocopy/dto/UserRequest.java rename to src/main/java/ru/soune/nocopy/dto/user/UserRequest.java index 91f7379..9a5e48e 100644 --- a/src/main/java/ru/soune/nocopy/dto/UserRequest.java +++ b/src/main/java/ru/soune/nocopy/dto/user/UserRequest.java @@ -1,11 +1,11 @@ -package ru.soune.nocopy.dto; +package ru.soune.nocopy.dto.user; import com.fasterxml.jackson.annotation.JsonFormat; 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/UserAction.java b/src/main/java/ru/soune/nocopy/entity/UserAction.java deleted file mode 100644 index 661ab3a..0000000 --- a/src/main/java/ru/soune/nocopy/entity/UserAction.java +++ /dev/null @@ -1,44 +0,0 @@ -package ru.soune.nocopy.entity; - -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/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/Violation.java b/src/main/java/ru/soune/nocopy/entity/file/Violation.java similarity index 94% rename from src/main/java/ru/soune/nocopy/entity/Violation.java rename to src/main/java/ru/soune/nocopy/entity/file/Violation.java index dd504bd..c5317bc 100644 --- a/src/main/java/ru/soune/nocopy/entity/Violation.java +++ b/src/main/java/ru/soune/nocopy/entity/file/Violation.java @@ -1,9 +1,10 @@ -package ru.soune.nocopy.entity; +package ru.soune.nocopy.entity.file; import jakarta.persistence.*; import lombok.*; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; +import ru.soune.nocopy.entity.user.User; import java.time.LocalDateTime; 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 96% rename from src/main/java/ru/soune/nocopy/entity/AuthToken.java rename to src/main/java/ru/soune/nocopy/entity/user/AuthToken.java index 6fa1a9c..803a904 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..d836e45 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/entity/user/EmailVerificationToken.java @@ -0,0 +1,35 @@ +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 = LocalDateTime.now().plusMinutes(15); + + @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/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/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.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 b893637..6346c0d 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,7 @@ 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.Violation; import java.time.LocalDate; import java.time.LocalDateTime; @@ -33,6 +34,9 @@ public class User { @Column(name = "email", nullable = false, length = 1024, unique = true) private String email; + @Column(name = "email_verified") + private boolean emailVerified = false; + @Column(name = "company") private String company; @@ -64,7 +68,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 @@ -76,11 +80,6 @@ public class User { @ToString.Exclude private List userContents = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) - @JsonIgnore - @ToString.Exclude - private List userActions = new ArrayList<>(); - @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @JsonIgnore @ToString.Exclude 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..4dcf724 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.file.ContentStatus; import ru.soune.nocopy.entity.file.FileType; import java.time.LocalDateTime; 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/FileEntityHandler.java b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java index 7f6ae8f..2d6fcdf 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileEntityHandler.java @@ -11,7 +11,8 @@ 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.service.auth.AuthService; +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; @@ -33,6 +34,8 @@ public class FileEntityHandler implements RequestHandler { private final FileEntityRepository fileEntityRepository; + private final ImageHashRepository imageHashRepository; + @Override public BaseResponse handle(BaseRequest request) { try { @@ -272,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/FileUploadHandler.java b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java index 7b98da3..67755c0 100644 --- a/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/FileUploadHandler.java @@ -10,14 +10,14 @@ 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.*; 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/ImageFoundRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java index ad7d3ae..e82ca62 100644 --- a/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/ImageFoundRequestHandler.java @@ -7,11 +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.GoogleVisionSearchService; -import ru.soune.nocopy.service.YandexSearchService; +import ru.soune.nocopy.service.search.YandexSearchService; +import ru.soune.nocopy.service.search.GoogleVisionSearchService; @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 6add2a0..6271328 100644 --- a/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/LoginRequestHandler.java @@ -4,10 +4,13 @@ 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.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.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import java.util.Arrays; import java.util.Map; @@ -25,8 +28,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 +39,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); String token = authToken.getToken(); diff --git a/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/LogoutRequestHandler.java index e12284c..3216d16 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.entity.AuthToken; -import ru.soune.nocopy.entity.User; +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.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; import java.util.Objects; import java.util.Optional; @@ -31,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/RegRequestHandler.java b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java index 86fcbc7..dca6ca5 100644 --- a/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java +++ b/src/main/java/ru/soune/nocopy/handler/RegRequestHandler.java @@ -7,12 +7,16 @@ 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.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.UserRepository; -import ru.soune.nocopy.service.auth.AuthService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.mail.EmailService; import java.util.Arrays; import java.util.Map; @@ -29,6 +33,8 @@ public class RegRequestHandler implements RequestHandler { private final ObjectMapper objectMapper; + private final EmailService emailService; + @Override public BaseResponse handle(BaseRequest request) throws ValidationException { RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); @@ -45,6 +51,7 @@ public class RegRequestHandler implements RequestHandler { } BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); + regRequestValidator.validate(regRequest, bindingResult); if (bindingResult.hasErrors()) { @@ -52,11 +59,26 @@ public class RegRequestHandler implements RequestHandler { } AuthToken authToken = authService.register(regRequest); + EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken); + + try { + emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken()); + } catch (Exception e) { + return new BaseResponse(request.getMsgId(), MessageCode.SEND_EMAIL_EXCEPTION.getCode(), + MessageCode.SEND_EMAIL_EXCEPTION.getDescription(), Map.of( + "message", "not send", + "email", regRequest.getEmail() + )); + } + + String token = authToken.getToken(); authService.useUserAuthToken(token); RegAnswer regAnswer = new RegAnswer(); - regAnswer.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 new file mode 100644 index 0000000..373b953 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/handler/VerifyRegisterUserHandler.java @@ -0,0 +1,122 @@ +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; +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.entity.user.User; +import ru.soune.nocopy.exception.NotValidFieldException; +import ru.soune.nocopy.exception.UserNotFoundException; +import ru.soune.nocopy.repository.AuthTokenRepository; +import ru.soune.nocopy.repository.EmailVerificationTokenRepository; +import ru.soune.nocopy.repository.UserRepository; +import ru.soune.nocopy.service.mail.EmailService; +import ru.soune.nocopy.service.register.AuthService; +import ru.soune.nocopy.service.user.UserService; + +import java.io.IOException; +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 EmailService emailService; + + private final UserRepository userRepository; + + private final UserService userService; + + private final AuthTokenRepository authTokenRepository; + + private final AuthService authService; + + @Override + public BaseResponse handle(BaseRequest request) throws MessagingException, IOException { + VerifyUserRequest verifyUserRequest = objectMapper.convertValue(request.getMessageBody(), VerifyUserRequest.class); + + Long userId = verifyUserRequest.getUserId(); + + User user = userRepository.findById(userId) + .orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); + + Optional userAuthTokens = authTokenRepository.findByUserId(userId); + + AuthToken authToken = userAuthTokens.isEmpty() ? authService.generateAuthToken(user): userAuthTokens.get(); + + if (verifyUserRequest.getResend() != null && verifyUserRequest.getResend() == 1) { + return handleResend(request, user, authToken); + } + + return handleVerify(request, user, authToken, verifyUserRequest.getVerifyToken()); + } + + private BaseResponse handleResend(BaseRequest request, User user, AuthToken authToken) throws MessagingException, IOException { + + EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId()); + + if (token != null && !token.isExpired()) { + emailService.sendVerificationEmail(user, token.getToken()); + + log.info("Verification token resent for user {}", user.getEmail()); + return successResponse(request, authToken, "Verification code has been resent", false, + false, user.getEmail()); + } + + if (token != null) { + 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, user.getEmail()); + } + + + 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(), updateUser.getEmail()); + } + + private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive, + 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/handler/validator/RegRequestValidator.java b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java index 22e4b62..c2b2496 100644 --- a/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java +++ b/src/main/java/ru/soune/nocopy/handler/validator/RegRequestValidator.java @@ -4,7 +4,7 @@ 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.util.*; diff --git a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java index 3959f8f..16e411a 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserContentMapper.java @@ -4,8 +4,8 @@ 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.entity.UserContent; +import ru.soune.nocopy.dto.user.UserContentUpdateRequest; +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..9d05f14 100644 --- a/src/main/java/ru/soune/nocopy/mapper/UserMapper.java +++ b/src/main/java/ru/soune/nocopy/mapper/UserMapper.java @@ -3,8 +3,8 @@ 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.entity.User; +import ru.soune.nocopy.dto.user.UserDTO; +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 5f5e8bb..c1b65e2 100644 --- a/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/AuthTokenRepository.java @@ -3,7 +3,7 @@ package ru.soune.nocopy.repository; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; -import ru.soune.nocopy.entity.AuthToken; +import ru.soune.nocopy.entity.user.AuthToken; import java.time.LocalDateTime; import java.util.List; @@ -19,4 +19,6 @@ public interface AuthTokenRepository extends JpaRepository { """, nativeQuery = true) Long findUserIdByToken(String token); + Optional findByUserIdAndIsActive(Long userId, boolean isActive); + Optional findByUserId(Long userId); } 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..91649d8 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/repository/EmailVerificationTokenRepository.java @@ -0,0 +1,15 @@ +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); + EmailVerificationToken findByUserId(Long userId); +} 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..b0e8032 100644 --- a/src/main/java/ru/soune/nocopy/repository/UserRepository.java +++ b/src/main/java/ru/soune/nocopy/repository/UserRepository.java @@ -2,12 +2,12 @@ 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; 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/UserContentService.java b/src/main/java/ru/soune/nocopy/service/UserContentService.java deleted file mode 100644 index e113752..0000000 --- a/src/main/java/ru/soune/nocopy/service/UserContentService.java +++ /dev/null @@ -1,72 +0,0 @@ -package ru.soune.nocopy.service; - -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.entity.file.FileType; -import ru.soune.nocopy.entity.User; -import ru.soune.nocopy.entity.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/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/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/mail/EmailService.java b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java new file mode 100644 index 0000000..d6cf147 --- /dev/null +++ b/src/main/java/ru/soune/nocopy/service/mail/EmailService.java @@ -0,0 +1,94 @@ +package ru.soune.nocopy.service.mail; + +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; +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; + + @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) 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); + + String htmlContent = loadAndProcessTemplate(user.getFullName(), code); + helper.setText(htmlContent, true); + + mailSender.send(message); + + log.info("Email sent to: {}", user.getEmail()); + } + + @Transactional + public EmailVerificationToken createEmailVerificationToken(AuthToken authToken) { + EmailVerificationToken emailToken = new EmailVerificationToken(); + emailToken.setUser(authToken.getUser()); + emailToken.setToken(generateVerificationCode()); + 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 : user); + 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/auth/AuthService.java b/src/main/java/ru/soune/nocopy/service/register/AuthService.java similarity index 81% rename from src/main/java/ru/soune/nocopy/service/auth/AuthService.java rename to src/main/java/ru/soune/nocopy/service/register/AuthService.java index f14d6d1..1038e63 100644 --- a/src/main/java/ru/soune/nocopy/service/auth/AuthService.java +++ b/src/main/java/ru/soune/nocopy/service/register/AuthService.java @@ -1,12 +1,16 @@ -package ru.soune.nocopy.service.auth; +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.entity.AuthToken; -import ru.soune.nocopy.entity.User; +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; @@ -14,10 +18,7 @@ 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 @@ -29,6 +30,8 @@ public class AuthService { private final PasswordEncoder passwordEncoder; + private final MessageSource messageSource; + private final SecureRandom secureRandom = new SecureRandom(); @Transactional @@ -48,33 +51,7 @@ public class AuthService { User savedUser = userRepository.save(user); - AuthToken authToken = new AuthToken(); - authToken.setToken(generateAuthToken()); - authToken.setUser(savedUser); - - return authTokenRepository.save(authToken); - } - - @Transactional - public AuthToken login(LoginRequest request) { - Optional userOpt = userRepository.findByEmail(request.getEmail()); - User user = userOpt.get(); - - 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); + AuthToken authToken = genereateAuthToken(savedUser); return authTokenRepository.save(authToken); } @@ -99,12 +76,48 @@ public class AuthService { 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); 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/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; 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 99% rename from src/main/java/ru/soune/nocopy/service/YandexSearchService.java rename to src/main/java/ru/soune/nocopy/service/search/YandexSearchService.java index 9a8e9b2..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; 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 82% rename from src/main/java/ru/soune/nocopy/service/UserService.java rename to src/main/java/ru/soune/nocopy/service/user/UserService.java index 7c4b79f..efce0e4 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.entity.User; +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 User activateAndVerifyUser(long userId) { + Optional users = userRepository.findById(userId); + User user = users.orElseThrow(() -> new UserNotFoundException("User not found with id: " + userId)); + user.setActive(true); + user.setEmailVerified(true); + + return 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 c460c1f..0a3f966 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,4 +1,26 @@ spring: + mail: + host: smtp.gmail.com + port: 587 + username: vlad.popovtsev@gmail.com + password: gocjwvrqrzlaatsr + properties: + 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 + flyway: enabled: true baseline-on-migrate: true @@ -59,4 +81,11 @@ 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: + standart-subject: NO COPY - Подтверждение email адреса + standart-address-from: NO COPY + user: Пользователь \ 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