10 Commits
Author SHA1 Message Date
vladp a982eef9c6 NCBACK-23 add full logic to reset
Test Workflow / test (push) Successful in 3s
2026-01-22 03:39:49 +07:00
vladp d1b5b6eb5c add check user
Test Workflow / test (push) Successful in 3s
2026-01-21 12:34:40 +07:00
vladp 251bb37105 fix authService
Test Workflow / test (push) Successful in 4s
2026-01-21 12:21:14 +07:00
vladp 1fc065a46b Сохранение 2026-01-21 12:02:38 +07:00
vladp 60678ef889 Dev add support id
Test Workflow / test (push) Successful in 4s
2026-01-20 21:55:42 +07:00
backdev f0a87648cf Merge pull request 'NCBACK-27' (#12) from NCBACK-27 into dev
Test Workflow / test (push) Successful in 3s
Reviewed-on: #12
2026-01-20 16:20:38 +08:00
vladp f9accd7af1 NCBACK-33 add verification logic
Test Workflow / test (push) Successful in 3s
2026-01-20 15:12:24 +07:00
vladp 55fa8b84e2 NCBACK-33 add email send
Test Workflow / test (push) Successful in 4s
2026-01-20 03:47:51 +07:00
vladp 4a8e561258 NCBACK-33 add email send
Test Workflow / test (push) Successful in 2s
2026-01-19 19:26:59 +07:00
vladp 4704f780e5 NCBACK-33 add start logic for verify by token
Test Workflow / test (push) Successful in 4s
2026-01-16 20:20:14 +07:00
68 changed files with 1253 additions and 327 deletions
+10 -1
View File
@@ -1,10 +1,19 @@
# Postgres
POSTGRES_DB=no_copy_ POSTGRES_DB=no_copy_
POSTGRES_USER=ncp_db POSTGRES_USER=ncp_db
POSTGRES_PASSWORD=ncpDbApp POSTGRES_PASSWORD=ncpDbApp
POSTGRES_PORT=5432 POSTGRES_PORT=5432
POSTGRES_HOST=postgres POSTGRES_HOST=postgres
# Redis
REDIS_HOST=redis REDIS_HOST=redis
REDIS_PORT=6379 REDIS_PORT=6379
SERVER_PORT=8080 # 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!
+10
View File
@@ -66,3 +66,13 @@ GRANT ALL ON SEQUENCES TO ncp_db;
Зайти в контейнер: Зайти в контейнер:
docker exec -it {name} bash docker exec -it {name} bash
------
Создать для сущности FileEntity sequence для икремента индетификатора
CREATE SEQUENCE IF NOT EXISTS file_support_id_seq START 1;
ALTER TABLE file_entities
ALTER COLUMN support_id
SET DEFAULT nextval('file_support_id_seq');
+1
View File
@@ -30,6 +30,7 @@ dependencies {
implementation 'org.springframework.security:spring-security-crypto:6.5.3' implementation 'org.springframework.security:spring-security-crypto:6.5.3'
implementation 'org.mapstruct:mapstruct:1.5.5.Final' implementation 'org.mapstruct:mapstruct:1.5.5.Final'
implementation 'commons-validator:commons-validator:1.7' 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 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.17.0'
implementation 'tools.jackson.core:jackson-core:3.0.3' implementation 'tools.jackson.core:jackson-core:3.0.3'
+36 -1
View File
@@ -17,6 +17,29 @@ services:
command: tail -f /dev/null command: tail -f /dev/null
restart: unless-stopped 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: db:
image: postgres:17.7 image: postgres:17.7
restart: always restart: always
@@ -30,6 +53,8 @@ services:
memory: 1G memory: 1G
environment: environment:
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_SHARED_BUFFERS: 512MB POSTGRES_SHARED_BUFFERS: 512MB
@@ -59,8 +84,11 @@ services:
environment: environment:
FILE_STORAGE_PATH: /data/uploads FILE_STORAGE_PATH: /data/uploads
MAX_FILE_SIZE: 10737418240 MAX_FILE_SIZE: 10737418240
# FILE_CHUNK_SIZE: 1048576
FILE_CHUNK_SIZE: 1000000 FILE_CHUNK_SIZE: 1000000
POSTGRES_DB: no_copy_ POSTGRES_DB: no_copy_
# POSTGRES_USER: postgres
# POSTGRES_PASSWORD: postgres
POSTGRES_USER: ${POSTGRES_USER} POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_PORT: 5432 POSTGRES_PORT: 5432
@@ -70,6 +98,13 @@ services:
YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q YANDEX_API_KEY: AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q
YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd YANDEX_FOLDER_ID: b1gokpdbm6qfpsou8pcd
YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image" YANDEX_SEARCH_URL: "https://searchapi.api.cloud.yandex.net/v2/image/search_by_image"
# 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: depends_on:
- db - db
ports: ports:
@@ -238,7 +273,7 @@ volumes:
loki_rules: loki_rules:
uploads_data: uploads_data:
prometheus_data: prometheus_data:
# postfix-data:
networks: networks:
app-network: app-network:
external: true external: true
@@ -18,7 +18,9 @@ public class HandlerConfig {
FileEntityHandler file, FileEntityHandler file,
LogoutRequestHandler logoutHandler, LogoutRequestHandler logoutHandler,
ImageFoundRequestHandler imageFoundRequestHandler, ImageFoundRequestHandler imageFoundRequestHandler,
AuthRequestHandler authRequestHandler VerifyRegisterUserHandler verifyRegisterUser,
AuthRequestHandler authRequestHandler,
ResetPasswordHandler resetPasswordHandler
) { ) {
Map<Integer, RequestHandler> map = new HashMap<>(); Map<Integer, RequestHandler> map = new HashMap<>();
map.put(20001, login); map.put(20001, login);
@@ -28,6 +30,8 @@ public class HandlerConfig {
map.put(20006, logoutHandler); map.put(20006, logoutHandler);
map.put(20007, imageFoundRequestHandler); map.put(20007, imageFoundRequestHandler);
map.put(20008, authRequestHandler); map.put(20008, authRequestHandler);
map.put(20009, verifyRegisterUser);
map.put(20010, resetPasswordHandler);
return map; return map;
} }
@@ -14,7 +14,7 @@ import org.springframework.web.multipart.MultipartFile;
import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.RegAnswer; import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.file.ChunkUploadResponse; import ru.soune.nocopy.dto.file.ChunkUploadResponse;
import ru.soune.nocopy.dto.file.CompleteUploadResponse; import ru.soune.nocopy.dto.file.CompleteUploadResponse;
import ru.soune.nocopy.dto.file.FileEntityResponse; import ru.soune.nocopy.dto.file.FileEntityResponse;
@@ -23,7 +23,7 @@ import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.entity.file.UploadStatus; import ru.soune.nocopy.entity.file.UploadStatus;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.*; import ru.soune.nocopy.handler.*;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileUploadService; import ru.soune.nocopy.service.file.FileUploadService;
@@ -70,6 +70,9 @@ public class ApiController {
return ResponseEntity.ok().body(response); return ResponseEntity.ok().body(response);
} catch (ValidationException e) { } catch (ValidationException e) {
return createValidationErrorResponse(e.getBindingResult(), e.getMsgId()); 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) { } catch (NotFoundAuthToken e) {
return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(), return ResponseEntity.ok().body(new BaseResponse(msgId, MessageCode.AUTH_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>())); MessageCode.AUTH_TOKEN_NOT_FOUND.getDescription(), new HashMap<>()));
@@ -3,17 +3,17 @@ package ru.soune.nocopy.controller;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.UserRequest; import ru.soune.nocopy.dto.user.UserRequest;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.InvalidUserEmail; import ru.soune.nocopy.exception.InvalidUserEmail;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.mapper.UserMapper; import ru.soune.nocopy.mapper.UserMapper;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.UserService; import ru.soune.nocopy.service.user.UserService;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
@@ -34,7 +34,7 @@ public class UserController {
@GetMapping("/all") @GetMapping("/all")
public ResponseEntity<List<UserDTO>> getAllUsers() { public ResponseEntity<List<UserDTO>> getAllUsers() {
List<UserDTO> allUsers = userRepository.findAll().stream() List<UserDTO> allUsers = userRepository.findAll().stream()
.map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.getIsActive(), .map(u -> new UserDTO(u.getFullName(), u.getCompany(), u.getEmail(), u.isActive(),
u.getPhone(), u.getGenderType(), u.getPhone(), u.getGenderType(),
u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType())) u.getBirthday(), u.getCreatedAt(), u.getSubscriptionType()))
.toList(); .toList();
@@ -52,19 +52,19 @@ public class UserController {
AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found")); AuthToken authToken = tokenOptional.orElseThrow(() -> new NotFoundAuthToken("Token: " + token + "not found"));
if (authToken != null) { if (authToken != null) {
Optional<User> user = userRepository.findByEmail(email); User user = userRepository.findByEmail(email);
//TODO add mapper //TODO add mapper
if (user.isPresent()) { if (user != null) {
UserDTO userDTO = userMapper.toDTO(user.get()); UserDTO userDTO = userMapper.toDTO(user);
userDTO.setEmail(email); userDTO.setEmail(email);
userDTO.setFullName(user.get().getFullName()); userDTO.setFullName(user.getFullName());
userDTO.setCompany(user.get().getCompany()); userDTO.setCompany(user.getCompany());
userDTO.setPhone(user.get().getPhone()); userDTO.setPhone(user.getPhone());
userDTO.setGenderType(user.get().getGenderType()); userDTO.setGenderType(user.getGenderType());
userDTO.setBirthday(user.get().getBirthday()); userDTO.setBirthday(user.getBirthday());
userDTO.setCreatedAt(user.get().getCreatedAt()); userDTO.setCreatedAt(user.getCreatedAt());
userDTO.setSubscriptionType(user.get().getSubscriptionType()); userDTO.setSubscriptionType(user.getSubscriptionType());
userDTO.setActive(user.get().getIsActive()); userDTO.setActive(user.isActive());
return ResponseEntity.ok(userDTO); return ResponseEntity.ok(userDTO);
} }
@@ -1,10 +0,0 @@
package ru.soune.nocopy.dto;
import java.time.LocalDateTime;
public record AuthResponse (
boolean success,
String message,
String token,
LocalDateTime expiresAt
) {}
@@ -6,9 +6,12 @@ public enum MessageCode {
REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"), REG_EMAIL_OR_PHONE_EXISTS(1, "Email or phone already registered"),
INVALID_FIELD(2, "Invalid field"), INVALID_FIELD(2, "Invalid field"),
INVALID_TOKEN(2, "Invalid token"), INVALID_TOKEN(2, "Invalid token"),
TOKEN_IS_ALIVE(2, "Token is alive"),
INVALID_ACTION(2, "Invalid action"), INVALID_ACTION(2, "Invalid action"),
FILE_UPLOAD_ERROR(2, "File upload error"), FILE_UPLOAD_ERROR(2, "File upload error"),
FILE_DOWNLOAD_ERROR(2, "File download error"), FILE_DOWNLOAD_ERROR(2, "File download error"),
USER_NOT_VERIFIED(2, "User not verified"),
USER_NOT_FOUND(2, "User not found"),
FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"), FILE_DOWNLOAD_ERROR_NOT_CORRECT_FIELD(2, "Not correct field"),
IMAGE_FOUND_ERROR(2, "Image found error"), IMAGE_FOUND_ERROR(2, "Image found error"),
INVALID_JSON_BODY(2, "Invalid fields in JSON object"), INVALID_JSON_BODY(2, "Invalid fields in JSON object"),
@@ -21,7 +24,8 @@ public enum MessageCode {
AUTH_TOKEN_MISMATCH(4, "Token mismatch"), AUTH_TOKEN_MISMATCH(4, "Token mismatch"),
AUTH_TOKEN_NOT_FOUND(4, "Token not found"), AUTH_TOKEN_NOT_FOUND(4, "Token not found"),
FILE_NOT_FOUND(4, "File not found"), FILE_NOT_FOUND(4, "File not found"),
AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"); AUTH_PASSWORD_NOT_MATCHES(2, "Password does not match"),
USER_NOT_ACTIVE(2, "User not active");
private final Integer code; private final Integer code;
@@ -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;
}
@@ -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
) {}
@@ -1,41 +0,0 @@
package ru.soune.nocopy.dto.file;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class FileApiResponse<T> {
private boolean success;
private String message;
private T data;
private String error;
public static <T> FileApiResponse<T> success(T data) {
return FileApiResponse.<T>builder()
.success(true)
.message("Operation completed successfully")
.data(data)
.build();
}
public static <T> FileApiResponse<T> success(String message, T data) {
return FileApiResponse.<T>builder()
.success(true)
.message(message)
.data(data)
.build();
}
public static <T> FileApiResponse<T> error(String error) {
return FileApiResponse.<T>builder()
.success(false)
.error(error)
.build();
}
}
@@ -30,4 +30,5 @@ public class FileEntityResponse {
private String formattedSize; private String formattedSize;
private String downloadUrl; private String downloadUrl;
private boolean existsOnDisk; private boolean existsOnDisk;
private Integer supportId;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -13,5 +13,9 @@ import java.util.Map;
public class LoginAnswer { public class LoginAnswer {
private String Token; private String Token;
private boolean isActive;
private boolean isVerified;
private List<Map<String, String>> fieldErrors; private List<Map<String, String>> fieldErrors;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -13,5 +13,9 @@ import java.util.Map;
public class RegAnswer { public class RegAnswer {
private String Token; private String Token;
private boolean isActive;
private boolean isVerified;
private List<Map<String, String>> fieldErrors; private List<Map<String, String>> fieldErrors;
} }
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.*; import jakarta.validation.constraints.*;
@@ -0,0 +1,24 @@
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResetPasswordRequest {
@JsonProperty("email")
private String email;
@JsonProperty("verifyToken")
private String verifyToken;
@JsonProperty("action")
private String action;
@JsonProperty("password")
private String password;
}
@@ -0,0 +1,13 @@
package ru.soune.nocopy.dto.register;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
public class ResetPasswordResponse {
@JsonProperty("user_id")
private Long userId;
@JsonProperty("authToken")
private String authToken;
}
@@ -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;
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.user;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@@ -1,11 +1,11 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.user;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import ru.soune.nocopy.entity.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import ru.soune.nocopy.entity.SubscriptionType; import ru.soune.nocopy.entity.user.SubscriptionType;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,11 +1,11 @@
package ru.soune.nocopy.dto; package ru.soune.nocopy.dto.user;
import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Past; import jakarta.validation.constraints.Past;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import ru.soune.nocopy.entity.GenderType; import ru.soune.nocopy.entity.user.GenderType;
import java.time.LocalDate; import java.time.LocalDate;
@@ -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;
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.file;
import lombok.Getter; import lombok.Getter;
@@ -2,10 +2,13 @@ package ru.soune.nocopy.entity.file;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.hibernate.annotations.GenerationTime;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@Data @Data
@@ -16,11 +19,13 @@ import java.time.LocalDateTime;
@Table(name = "file_entities") @Table(name = "file_entities")
@EntityListeners(AuditingEntityListener.class) @EntityListeners(AuditingEntityListener.class)
public class FileEntity { public class FileEntity {
@Id @Id
@GeneratedValue(strategy = GenerationType.UUID) @GeneratedValue(strategy = GenerationType.UUID)
private String id; private String id;
@Column(name = "support_id", unique = true, updatable = false)
private Integer supportId;
@Column(name = "user_id", nullable = false) @Column(name = "user_id", nullable = false)
private Long userId; private Long userId;
@@ -70,9 +75,4 @@ public class FileEntity {
} }
this.updatedAt = LocalDateTime.now(); this.updatedAt = LocalDateTime.now();
} }
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
} }
@@ -1,9 +1,11 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.file;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.entity.user.UserContent;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,9 +1,10 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.file;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.user.User;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
@@ -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);
}
}
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
public enum GenderType { public enum GenderType {
MALE, FEMALE MALE, FEMALE
@@ -1,4 +1,4 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import lombok.Getter; import lombok.Getter;
@@ -1,11 +1,14 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*; import jakarta.persistence.*;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import lombok.*; import lombok.*;
import org.hibernate.annotations.ColumnDefault;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.ImageProtection;
import ru.soune.nocopy.entity.file.Violation;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -33,6 +36,9 @@ public class User {
@Column(name = "email", nullable = false, length = 1024, unique = true) @Column(name = "email", nullable = false, length = 1024, unique = true)
private String email; private String email;
@Column(name = "email_verified")
private boolean emailVerified = false;
@Column(name = "company") @Column(name = "company")
private String company; private String company;
@@ -64,22 +70,22 @@ public class User {
private LocalDateTime lastLoginAt; private LocalDateTime lastLoginAt;
@Column(name = "is_active") @Column(name = "is_active")
private Boolean isActive = true; private boolean isActive = false;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<AuthToken> tokens = new ArrayList<>(); private List<AuthToken> tokens = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<UserContent> userContents = new ArrayList<>(); private UserNotActive userNotActive;
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<UserAction> userActions = new ArrayList<>(); private List<UserContent> userContents = new ArrayList<>();
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
@JsonIgnore @JsonIgnore
@@ -90,4 +96,8 @@ public class User {
@JsonIgnore @JsonIgnore
@ToString.Exclude @ToString.Exclude
private List<ImageProtection> imageProtections = new ArrayList<>(); private List<ImageProtection> imageProtections = new ArrayList<>();
@Column(name = "failed_login_attempts")
@ColumnDefault("0")
private Integer failedLoginAttempts = 0;
} }
@@ -1,9 +1,10 @@
package ru.soune.nocopy.entity; package ru.soune.nocopy.entity.user;
import jakarta.persistence.*; import jakarta.persistence.*;
import lombok.*; import lombok.*;
import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener; import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import ru.soune.nocopy.entity.file.ContentStatus;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -0,0 +1,32 @@
package ru.soune.nocopy.entity.user;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.data.annotation.CreatedDate;
import java.time.LocalDateTime;
@Entity
@Table(name = "user_not_active")
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class UserNotActive {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
private User user;
@CreatedDate
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@Column(name = "blocked_until")
private LocalDateTime blockedUntil;
}
@@ -9,7 +9,7 @@ import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.TokenProcessRequest; import ru.soune.nocopy.dto.TokenProcessRequest;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import java.util.Map; import java.util.Map;
@@ -6,12 +6,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; import ru.soune.nocopy.dto.file.*;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.entity.file.FileStatus; import ru.soune.nocopy.entity.file.FileStatus;
import ru.soune.nocopy.exception.FileEntityNotFoundException; import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import ru.soune.nocopy.service.file.FileEntityService; import ru.soune.nocopy.service.file.FileEntityService;
import ru.soune.nocopy.service.file.FileStatsService; import ru.soune.nocopy.service.file.FileStatsService;
@@ -10,14 +10,14 @@ import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.dto.file.*; 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.FileType;
import ru.soune.nocopy.entity.file.FileUploadSession; import ru.soune.nocopy.entity.file.FileUploadSession;
import ru.soune.nocopy.exception.*; import ru.soune.nocopy.exception.*;
import ru.soune.nocopy.handler.validator.FileUploadRequestValidator; import ru.soune.nocopy.handler.validator.FileUploadRequestValidator;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.FileUploadSessionRepository; 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 ru.soune.nocopy.service.file.FileUploadService;
import java.util.*; import java.util.*;
@@ -3,14 +3,13 @@ package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest; import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse; import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ImageSearchRequest; import ru.soune.nocopy.dto.file.ImageSearchRequest;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.service.YandexSearchService; import ru.soune.nocopy.service.search.YandexSearchService;
@Slf4j @Slf4j
@Component @Component
@@ -4,10 +4,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; 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.exception.NotValidFieldException;
import ru.soune.nocopy.repository.UserRepository; 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.Arrays;
import java.util.Map; import java.util.Map;
@@ -25,8 +28,9 @@ public class LoginRequestHandler implements RequestHandler {
@Override @Override
public BaseResponse handle(BaseRequest request) { public BaseResponse handle(BaseRequest request) {
LoginRequest loginRequest = objectMapper.convertValue(request.getMessageBody(), LoginRequest.class); 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 loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("email", loginRequest.getEmail()))); 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)); 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); AuthToken authToken = authService.login(loginRequest);
String token = authToken.getToken(); String token = authToken.getToken();
@@ -4,12 +4,14 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.dto.register.LogOutAnswer;
import ru.soune.nocopy.entity.User; 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.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.auth.AuthService; import ru.soune.nocopy.service.register.AuthService;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@@ -31,16 +33,16 @@ public class LogoutRequestHandler implements RequestHandler {
LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class); LogoutRequest logoutRequest = objectMapper.convertValue(request.getMessageBody(), LogoutRequest.class);
Optional<AuthToken> opToken = authTokenRepository.findByToken(logoutRequest.getToken()); Optional<AuthToken> opToken = authTokenRepository.findByToken(logoutRequest.getToken());
Optional<User> 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", throw new NotValidFieldException("User with email or token not found",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(), new BaseResponse(request.getMsgId(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getCode(),
MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(), MessageCode.AUTH_EMAIL_OR_TOKEN_NOT_FOUND.getDescription(),
new LogoutRequest(logoutRequest.getEmail(), logoutRequest.getToken()))); 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", throw new NotValidFieldException("User token mismatch with user",
new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(), new BaseResponse(request.getMsgId(), MessageCode.AUTH_TOKEN_MISMATCH.getCode(),
MessageCode.AUTH_TOKEN_MISMATCH.getDescription(), MessageCode.AUTH_TOKEN_MISMATCH.getDescription(),
@@ -7,12 +7,16 @@ import org.springframework.stereotype.Component;
import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import ru.soune.nocopy.dto.*; 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.NotValidFieldException;
import ru.soune.nocopy.exception.ValidationException; import ru.soune.nocopy.exception.ValidationException;
import ru.soune.nocopy.handler.validator.RegRequestValidator; import ru.soune.nocopy.handler.validator.RegRequestValidator;
import ru.soune.nocopy.repository.UserRepository; 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.Arrays;
import java.util.Map; import java.util.Map;
@@ -29,6 +33,8 @@ public class RegRequestHandler implements RequestHandler {
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final EmailService emailService;
@Override @Override
public BaseResponse handle(BaseRequest request) throws ValidationException { public BaseResponse handle(BaseRequest request) throws ValidationException {
RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class); RegRequest regRequest = objectMapper.convertValue(request.getMessageBody(), RegRequest.class);
@@ -45,6 +51,7 @@ public class RegRequestHandler implements RequestHandler {
} }
BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest"); BindingResult bindingResult = new BeanPropertyBindingResult(regRequest, "regRequest");
regRequestValidator.validate(regRequest, bindingResult); regRequestValidator.validate(regRequest, bindingResult);
if (bindingResult.hasErrors()) { if (bindingResult.hasErrors()) {
@@ -52,11 +59,16 @@ public class RegRequestHandler implements RequestHandler {
} }
AuthToken authToken = authService.register(regRequest); AuthToken authToken = authService.register(regRequest);
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
emailService.sendVerificationEmail(authToken.getUser(), emailToken.getToken());
String token = authToken.getToken(); String token = authToken.getToken();
authService.useUserAuthToken(token); authService.useUserAuthToken(token);
RegAnswer regAnswer = new RegAnswer(); RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(token); regAnswer.setToken(token);
regAnswer.setVerified(false);
regAnswer.setActive(false);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(),
MessageCode.SUCCESS.getDescription(), regAnswer); MessageCode.SUCCESS.getDescription(), regAnswer);
@@ -0,0 +1,112 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.ActionResponse;
import ru.soune.nocopy.dto.register.ResetPasswordRequest;
import ru.soune.nocopy.dto.register.ResetPasswordResponse;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.EmailVerificationTokenRepository;
import ru.soune.nocopy.repository.UserRepository;
import ru.soune.nocopy.service.mail.EmailService;
import ru.soune.nocopy.service.register.AuthService;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Component
@Slf4j
@RequiredArgsConstructor
public class ResetPasswordHandler implements RequestHandler{
private final ObjectMapper objectMapper;
private final EmailService emailService;
private final AuthService authService;
private final UserRepository userRepository;
private final EmailVerificationTokenRepository emailVerificationTokenRepository;
private final AuthTokenRepository authTokenRepository;
private final PasswordEncoder passwordEncoder;
@Override
public BaseResponse handle(BaseRequest request) throws Exception {
ResetPasswordRequest resetPasswordRequest = objectMapper.convertValue(request.getMessageBody(),
ResetPasswordRequest.class);
String action = resetPasswordRequest.getAction();
User user = userRepository.findByEmail(resetPasswordRequest.getEmail());
AuthToken authToken;
if (user == null) {
throw new UserNotFoundException("User not found with email: " + resetPasswordRequest.getEmail());
}
switch (action) {
case "resetPassword":
authToken = authService.generateAuthToken(user);
EmailVerificationToken emailToken = emailService.createEmailVerificationToken(authToken);
emailService.sendResetPasswordEmail(authToken.getUser(), emailToken.getToken());
return successResponse(request, authToken, user, MessageCode.SUCCESS.getDescription());
case "confirmVerification":
String verifyToken = resetPasswordRequest.getVerifyToken();
EmailVerificationToken verificationToken =
emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), verifyToken);
if (verificationToken == null) {
throw new NotValidFieldException("Verification token not found",
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
MessageCode.INVALID_TOKEN.getDescription(), Map.of("verifyToken", verifyToken)));
}
List<AuthToken> authTokens = authTokenRepository.findByUserId(user.getId());
authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst();
user.setPassword(passwordEncoder.encode(resetPasswordRequest.getPassword()));
user.setActive(true);
emailVerificationTokenRepository.delete(verificationToken);
User saveUser = userRepository.save(user);
return successResponse(request, authToken, saveUser, MessageCode.SUCCESS.getDescription());
default:
ActionResponse response = ActionResponse.builder()
.action(action)
.availableActions(Arrays.asList("confirmVerification", "resetPassword"))
.build();
return new BaseResponse(request.getMsgId(), MessageCode.INVALID_ACTION.getCode(),
"Invalid action: " + action, response);
}
}
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, User user, String message) {
ResetPasswordResponse resetPasswordResponse = new ResetPasswordResponse();
resetPasswordResponse.setAuthToken(authToken.getToken());
resetPasswordResponse.setUserId(user.getId());
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, resetPasswordResponse);
}
}
@@ -0,0 +1,119 @@
package ru.soune.nocopy.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import ru.soune.nocopy.dto.BaseRequest;
import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.register.RegAnswer;
import ru.soune.nocopy.dto.register.VerifyUserRequest;
import ru.soune.nocopy.entity.user.AuthToken;
import ru.soune.nocopy.entity.user.EmailVerificationToken;
import ru.soune.nocopy.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.util.List;
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) {
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));
List<AuthToken> authTokens = authTokenRepository.findByUserId(user.getId());
AuthToken authToken = authTokens.isEmpty() ? authService.generateAuthToken(user): authTokens.getFirst();
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) {
EmailVerificationToken token = emailVerificationTokenRepository.findByUserId(user.getId());
if (token != null && !token.isExpired()) {
emailService.sendVerificationEmail(user, token.getToken());
log.info("Verification token resent for user {}", user.getEmail());
return successResponse(request, authToken, "Verification code has been resent", false, false);
}
if (token != null) {
emailVerificationTokenRepository.delete(token);
}
EmailVerificationToken newToken = emailService.createEmailVerificationToken(authToken);
emailService.sendVerificationEmail(user, newToken.getToken());
log.info("New verification token generated for user {}", user.getEmail());
return successResponse(request, authToken, "New verification code has been sent",
false, false);
}
private BaseResponse handleVerify(BaseRequest request, User user, AuthToken authToken, String providedToken) {
EmailVerificationToken token = emailVerificationTokenRepository.findByUserIdAndToken(user.getId(), providedToken);
if (token == null || token.isExpired()) {
throw new NotValidFieldException("Invalid or expired token",
new BaseResponse(request.getMsgId(), MessageCode.INVALID_TOKEN.getCode(),
MessageCode.INVALID_TOKEN.getDescription(), Map.of("userId", user.getId())));
}
User updateUser = userService.activateAndVerifyUser(user.getId());
emailVerificationTokenRepository.delete(token);
return successResponse(request, authToken, MessageCode.SUCCESS.getDescription(), updateUser.isActive(),
updateUser.isEmailVerified());
}
private BaseResponse successResponse(BaseRequest request, AuthToken authToken, String message, boolean isActive,
boolean isVerified) {
RegAnswer regAnswer = new RegAnswer();
regAnswer.setToken(authToken.getToken());
regAnswer.setVerified(isVerified);
regAnswer.setActive(isActive);
return new BaseResponse(request.getMsgId(), MessageCode.SUCCESS.getCode(), message, regAnswer);
}
}
@@ -1,14 +1,11 @@
package ru.soune.nocopy.handler.validator; package ru.soune.nocopy.handler.validator;
import org.apache.commons.validator.routines.DomainValidator;
import org.apache.commons.validator.routines.EmailValidator; import org.apache.commons.validator.routines.EmailValidator;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.Errors; import org.springframework.validation.Errors;
import org.springframework.validation.Validator; import org.springframework.validation.Validator;
import ru.soune.nocopy.dto.RegRequest; import ru.soune.nocopy.dto.register.RegRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*; import java.util.*;
@Component @Component
@@ -4,8 +4,8 @@ import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget; import org.mapstruct.MappingTarget;
import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserContentUpdateRequest; import ru.soune.nocopy.dto.user.UserContentUpdateRequest;
import ru.soune.nocopy.entity.UserContent; import ru.soune.nocopy.entity.user.UserContent;
@Mapper(componentModel = "spring", @Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -3,8 +3,8 @@ package ru.soune.nocopy.mapper;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.NullValuePropertyMappingStrategy; import org.mapstruct.NullValuePropertyMappingStrategy;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
@Mapper(componentModel = "spring", @Mapper(componentModel = "spring",
nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
@@ -2,7 +2,7 @@ package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.entity.user.AuthToken;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
@@ -13,4 +13,6 @@ public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore); List<AuthToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt); Optional<AuthToken> findByLastUsedAtBefore(LocalDateTime lastUsedAt);
Optional<AuthToken> findByToken(String token); Optional<AuthToken> findByToken(String token);
Optional<AuthToken> findByUserIdAndIsActive(Long userId, boolean isActive);
List<AuthToken> findByUserId(Long userId);
} }
@@ -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<EmailVerificationToken, Long> {
Optional<EmailVerificationToken> findByToken(String token);
EmailVerificationToken findByUserIdAndToken(Long userId, String token);
List<EmailVerificationToken> findByExpiresAtBefore(LocalDateTime expiresAtBefore);
EmailVerificationToken findByUserId(Long userId);
}
@@ -3,7 +3,7 @@ package ru.soune.nocopy.repository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.file.FileType; import ru.soune.nocopy.entity.file.FileType;
import ru.soune.nocopy.entity.UserContent; import ru.soune.nocopy.entity.user.UserContent;
import java.util.List; import java.util.List;
@@ -0,0 +1,16 @@
package ru.soune.nocopy.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import ru.soune.nocopy.entity.user.UserNotActive;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface UserNotActiveRepository extends JpaRepository<UserNotActive, Long> {
Optional<UserNotActive> findByUserId(Long userId);
List<UserNotActive> findByBlockedUntilBefore(LocalDateTime now);
}
@@ -2,12 +2,12 @@ package ru.soune.nocopy.repository;
import jakarta.validation.constraints.Size; import jakarta.validation.constraints.Size;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import ru.soune.nocopy.entity.User; import ru.soune.nocopy.entity.user.User;
import java.util.Optional; import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email); User findByEmail(String email);
boolean existsByEmail(String email); boolean existsByEmail(String email);
boolean existsByPhone(@Size(min = 11, max = 14) String phone); boolean existsByPhone(@Size(min = 11, max = 14) String phone);
} }
@@ -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<UserContent> findByUser(long userId) {
return userContentRepository.findByUserId(userId);
}
@Transactional
public UserContent addUserContent(UserContentRequest userContentRequest) {
UserContent userContent = new UserContent();
Optional<User> 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);
}
}
@@ -0,0 +1,48 @@
package ru.soune.nocopy.service.auth;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.entity.user.User;
import ru.soune.nocopy.entity.user.UserNotActive;
import ru.soune.nocopy.repository.UserNotActiveRepository;
import ru.soune.nocopy.repository.UserRepository;
import java.time.LocalDateTime;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class CleanUpNotActiveUser {
@Autowired
private UserNotActiveRepository userNotActiveRepository;
@Autowired
private UserRepository userRepository;
@Transactional
@Scheduled(fixedDelay = 600000)
public void cleanupNotActiveUsers() {
LocalDateTime now = LocalDateTime.now();
List<UserNotActive> expiredTokens = userNotActiveRepository.findByBlockedUntilBefore(now);
for (UserNotActive userNotActive : expiredTokens) {
User user = userNotActive.getUser();
user.setActive(true);
user.setFailedLoginAttempts(0);
userRepository.save(user);
}
if (!expiredTokens.isEmpty()) {
userNotActiveRepository.deleteAll(expiredTokens);
}
}
}
@@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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 ru.soune.nocopy.repository.AuthTokenRepository;
import java.time.LocalDateTime; import java.time.LocalDateTime;
@@ -17,7 +17,6 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -223,6 +222,7 @@ public class FileEntityService {
.formattedSize(formatFileSize(fileEntity.getFileSize())) .formattedSize(formatFileSize(fileEntity.getFileSize()))
.downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId()) .downloadUrl("/api/v" + version + "/files/download/" + fileEntity.getId())
.existsOnDisk(existsOnDisk) .existsOnDisk(existsOnDisk)
.supportId(fileEntity.getSupportId())
.build(); .build();
} }
@@ -0,0 +1,123 @@
package ru.soune.nocopy.service.mail;
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.drop-password-subject}")
private String dropPasswordSubject;
@Value("${app.email.verification.standart-address-from}")
private String standartAdressFrom;
@Value("${app.email.verification.user}")
private String user;
public void sendVerificationEmail(User user, String code) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(standartAdressFrom);
helper.setTo(user.getEmail());
helper.setSubject(standartSubject);
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
"templates/email-verification.html");
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("Email sent to: {}", user.getEmail());
} catch (Exception e) {
log.error("Failed to send email: {}", e.getMessage());
}
}
public void sendResetPasswordEmail(User user, String code) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setFrom(standartAdressFrom);
helper.setTo(user.getEmail());
helper.setSubject(dropPasswordSubject);
String htmlContent = loadAndProcessTemplate(user.getFullName(), code,
"templates/reset-password.html");
helper.setText(htmlContent, true);
mailSender.send(message);
log.info("Email sent to: {}", user.getEmail());
} catch (Exception e) {
log.error("Failed to send email: {}", e.getMessage());
}
}
@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, String templatePath) throws IOException {
ClassPathResource resource = new ClassPathResource(templatePath);
String template = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
String safeUsername = escapeHtml(username != null ? username : user);
String safeCode = escapeHtml(code);
return template
.replace("{{username}}", safeUsername)
.replace("{{code}}", safeCode);
}
private String escapeHtml(String text) {
if (text == null) return "";
return text
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&#39;");
}
}
@@ -1,23 +1,26 @@
package ru.soune.nocopy.service.auth; package ru.soune.nocopy.service.register;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.context.MessageSource;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.*; import ru.soune.nocopy.dto.*;
import ru.soune.nocopy.entity.AuthToken; import ru.soune.nocopy.dto.register.LoginAnswer;
import ru.soune.nocopy.entity.User; 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.entity.user.UserNotActive;
import ru.soune.nocopy.exception.NotFoundAuthToken; import ru.soune.nocopy.exception.NotFoundAuthToken;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.AuthTokenRepository; import ru.soune.nocopy.repository.AuthTokenRepository;
import ru.soune.nocopy.repository.UserNotActiveRepository;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.*;
import java.util.Base64;
import java.util.Map;
import java.util.Optional;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -29,6 +32,8 @@ public class AuthService {
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final UserNotActiveRepository userNotActiveRepository;
private final SecureRandom secureRandom = new SecureRandom(); private final SecureRandom secureRandom = new SecureRandom();
@Transactional @Transactional
@@ -48,33 +53,7 @@ public class AuthService {
User savedUser = userRepository.save(user); User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken(); AuthToken authToken = genereateAuthToken(savedUser);
authToken.setToken(generateAuthToken());
authToken.setUser(savedUser);
return authTokenRepository.save(authToken);
}
@Transactional
public AuthToken login(LoginRequest request) {
Optional<User> 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);
return authTokenRepository.save(authToken); return authTokenRepository.save(authToken);
} }
@@ -99,15 +78,86 @@ public class AuthService {
return authToken.getUser().getId(); return authToken.getUser().getId();
} }
public AuthToken login(LoginRequest request) {
User user = userRepository.findByEmail(request.getEmail());
if (!user.isActive()) {
throw new NotValidFieldException("User not active", new BaseResponse(20003,
MessageCode.USER_NOT_ACTIVE.getCode(),
MessageCode.USER_NOT_ACTIVE.getDescription(), Map.of("userId", user.getId())));
}
if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
LoginAnswer loginAnswer = new LoginAnswer();
loginAnswer.setFieldErrors(Arrays.asList(Map.of("password", request.getPassword())));
loginAnswer.setVerified(user.isEmailVerified());
loginAnswer.setActive(user.isActive());
int attempts = user.getFailedLoginAttempts() + 1;
user.setFailedLoginAttempts(attempts);
if (attempts >= 5) {
user.setActive(false);
blockUser(user);
}
userRepository.save(user);
throw new NotValidFieldException("Invalid password", new BaseResponse(20003,
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getCode(),
MessageCode.AUTH_PASSWORD_NOT_MATCHES.getDescription(), loginAnswer));
}
user.setLastLoginAt(LocalDateTime.now());
user.setFailedLoginAttempts(0);
user.setActive(true);
User savedUser = userRepository.save(user);
AuthToken authToken = new AuthToken();
authToken.setToken(generateAuthToken());
authToken.setUser(savedUser);
return authTokenRepository.save(authToken);
}
@Transactional @Transactional
public void logout(String token) { public void logout(String token) {
authTokenRepository.findByToken(token) authTokenRepository.findByToken(token)
.ifPresent(authTokenRepository::delete); .ifPresent(authTokenRepository::delete);
} }
@Transactional
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() { private String generateAuthToken() {
byte[] bytes = new byte[32]; byte[] bytes = new byte[32];
secureRandom.nextBytes(bytes); secureRandom.nextBytes(bytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
} }
public void blockUser(User user) {
Optional<UserNotActive> existing = userNotActiveRepository.findByUserId(user.getId());
if (existing.isEmpty()) {
LocalDateTime now = LocalDateTime.now();
UserNotActive notActive = UserNotActive.builder()
.user(user)
.createdAt(now)
.blockedUntil(now.plusMinutes(15))
.build();
userNotActiveRepository.save(notActive);
}
}
} }
@@ -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<EmailVerificationToken> expiredTokens = emailVerificationTokenRepository.findByExpiresAtBefore(now);
if (!expiredTokens.isEmpty()) {
emailVerificationTokenRepository.deleteAll(expiredTokens);
}
}
}
@@ -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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -12,7 +12,6 @@ import ru.soune.nocopy.dto.BaseResponse;
import ru.soune.nocopy.dto.MessageCode; import ru.soune.nocopy.dto.MessageCode;
import ru.soune.nocopy.dto.file.YandexSearchResponse; import ru.soune.nocopy.dto.file.YandexSearchResponse;
import ru.soune.nocopy.entity.file.FileEntity; import ru.soune.nocopy.entity.file.FileEntity;
import ru.soune.nocopy.exception.FileEntityNotFoundException;
import ru.soune.nocopy.exception.NotValidFieldException; import ru.soune.nocopy.exception.NotValidFieldException;
import ru.soune.nocopy.repository.FileEntityRepository; import ru.soune.nocopy.repository.FileEntityRepository;
@@ -1,18 +1,20 @@
package ru.soune.nocopy.service; package ru.soune.nocopy.service.user;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import ru.soune.nocopy.dto.ChangePasswordRequest; import ru.soune.nocopy.dto.register.ChangePasswordRequest;
import ru.soune.nocopy.dto.UserDTO; import ru.soune.nocopy.dto.user.UserDTO;
import ru.soune.nocopy.dto.UserRequest; import ru.soune.nocopy.dto.user.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.InvalidUserEmail;
import ru.soune.nocopy.exception.NotValidationPasswordException; import ru.soune.nocopy.exception.NotValidationPasswordException;
import ru.soune.nocopy.exception.UserNotFoundException; import ru.soune.nocopy.exception.UserNotFoundException;
import ru.soune.nocopy.repository.UserRepository; import ru.soune.nocopy.repository.UserRepository;
import java.util.Optional;
@Service @Service
@AllArgsConstructor @AllArgsConstructor
public class UserService { public class UserService {
@@ -44,6 +46,16 @@ public class UserService {
return mapToDTO(updatedUser); return mapToDTO(updatedUser);
} }
@Transactional
public User activateAndVerifyUser(long userId) {
Optional<User> 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) { private void updateUserFromRequest(User user, UserRequest request) {
if (request.getFullName() != null) { if (request.getFullName() != null) {
user.setFullName(request.getFullName()); user.setFullName(request.getFullName());
+31 -1
View File
@@ -1,4 +1,26 @@
spring: 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
datasource: datasource:
url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}
username: ${POSTGRES_USER} username: ${POSTGRES_USER}
@@ -55,4 +77,12 @@ logging:
yandex: yandex:
api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q} api-key: ${YANDEX_API_KEY:AQVNyaVaUmgUb1GMCtf5zSEqFxy0woXrcMOOB43q}
folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd} folder-id: ${YANDEX_FOLDER_ID:b1gokpdbm6qfpsou8pcd}
search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image} search-url: ${YANDEX_SEARCH_URL:https://searchapi.api.cloud.yandex.net/v2/image/search_by_image}
app:
email:
verification:
standart-subject: NO COPY - Подтверждение email адреса
drop-password-subject: NO COPY - сброс пароля
standart-address-from: NO COPY <noreply@nocopy.com>
user: Пользователь
@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Подтверждение email</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 40px 20px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 800;
margin-bottom: 10px;
letter-spacing: 1px;
}
.content {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
margin-bottom: 20px;
}
.username {
color: #667eea;
font-weight: bold;
}
.code-container {
background: #f8f9fa;
border: 2px dashed #667eea;
border-radius: 10px;
padding: 25px;
text-align: center;
margin: 30px 0;
}
.verification-code {
font-size: 42px;
font-weight: 800;
letter-spacing: 8px;
color: #333;
margin: 0;
}
.info-text {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 8px;
margin: 25px 0;
font-size: 14px;
}
.footer {
text-align: center;
padding: 25px;
color: #666;
font-size: 12px;
border-top: 1px solid #eee;
background: #f9f9f9;
}
.highlight {
color: #764ba2;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">NO COPY</div>
<h1>Подтверждение email адреса</h1>
</div>
<div class="content">
<p class="greeting">
Здравствуйте, <span class="username">{{username}}</span>!
</p>
<p>Спасибо за регистрацию на платформе NO COPY.
Для завершения регистрации, пожалуйста, введите код подтверждения:</p>
<div class="code-container">
<p class="verification-code">{{code}}</p>
<p class="info-text">Код подтверждения</p>
</div>
<p class="info-text">
Код действителен в течение <span class="highlight">15 минут</span>
</p>
<div class="warning">
<strong>Важно:</strong> Если вы не регистрировались на NO COPY,
просто проигнорируйте это письмо.
</div>
</div>
<div class="footer">
<p>© 2024 NO COPY. Все права защищены.</p>
<p>Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.</p>
<p>Если у вас возникли вопросы, обратитесь в службу поддержки.</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,201 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Восстановление пароля</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
margin: 0;
padding: 0;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
color: white;
padding: 40px 20px;
text-align: center;
}
.logo {
font-size: 32px;
font-weight: 800;
margin-bottom: 10px;
letter-spacing: 1px;
}
.content {
padding: 40px 30px;
}
.greeting {
font-size: 18px;
margin-bottom: 20px;
}
.username {
color: #ee5a52;
font-weight: bold;
}
.code-container {
background: #f8f9fa;
border: 2px dashed #ff6b6b;
border-radius: 10px;
padding: 25px;
text-align: center;
margin: 30px 0;
}
.verification-code {
font-size: 42px;
font-weight: 800;
letter-spacing: 8px;
color: #333;
margin: 0;
}
.info-text {
color: #666;
font-size: 14px;
margin: 10px 0;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 15px;
border-radius: 8px;
margin: 25px 0;
font-size: 14px;
}
.action-button {
display: inline-block;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
color: white;
padding: 14px 32px;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
margin: 20px 0;
text-align: center;
transition: all 0.3s ease;
}
.action-button:hover {
background: linear-gradient(135deg, #ee5a52 0%, #d64541 100%);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(238, 90, 82, 0.3);
}
.steps {
margin: 30px 0;
padding-left: 20px;
}
.steps li {
margin-bottom: 10px;
padding-left: 5px;
}
.footer {
text-align: center;
padding: 25px;
color: #666;
font-size: 12px;
border-top: 1px solid #eee;
background: #f9f9f9;
}
.highlight {
color: #ee5a52;
font-weight: 600;
}
.urgent {
color: #d63031;
font-weight: 700;
}
.link-fallback {
margin-top: 15px;
font-size: 13px;
color: #666;
}
.timer {
background: #ffebee;
border-left: 4px solid #ff6b6b;
padding: 12px 15px;
margin: 20px 0;
border-radius: 0 8px 8px 0;
}
.timer-text {
color: #d32f2f;
font-weight: 600;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">NO COPY</div>
<h1>Восстановление пароля</h1>
<p>Безопасный доступ к вашему аккаунту</p>
</div>
<div class="content">
<p class="greeting">
Здравствуйте, <span class="username">{{username}}</span>!
</p>
<p>Мы получили запрос на восстановление пароля для вашего аккаунта NO COPY.</p>
<p>Для продолжения восстановления доступа, пожалуйста, используйте следующий код подтверждения:</p>
<div class="code-container">
<p class="verification-code">{{code}}</p>
<p class="info-text">Код для восстановления пароля</p>
</div>
<div class="timer">
<p class="timer-text">Код действителен в течение 15 минут</p>
</div>
<h3>Как использовать этот код:</h3>
<ol class="steps">
<li>Вернитесь на страницу восстановления пароля</li>
<li>Введите полученный код в поле подтверждения</li>
<li>Создайте новый надежный пароль</li>
<li>Войдите в аккаунт с новым паролем</li>
</ol>
<div style="text-align: center;">
<a href="{{resetLink}}" class="action-button">Перейти к восстановлению пароля</a>
</div>
<p class="link-fallback">
Если кнопка не работает, скопируйте и вставьте эту ссылку в браузер:<br>
<span style="color: #3498db; word-break: break-all;">{{resetLink}}</span>
</p>
<div class="warning">
<strong>Важная информация:</strong><br>
1. Если вы не запрашивали восстановление пароля, проигнорируйте это письмо.<br>
2. Никому не сообщайте этот код, включая сотрудников поддержки.<br>
3. После успешного восстановления старый пароль будет недействителен.
</div>
<p style="font-size: 13px; color: #7f8c8d;">
<strong>Совет по безопасности:</strong> Используйте пароль длиной не менее 8 символов,
содержащий буквы (заглавные и строчные), цифры и специальные символы.
</p>
</div>
<div class="footer">
<p>© 2024 NO COPY. Все права защищены.</p>
<p>Это письмо отправлено автоматически, пожалуйста, не отвечайте на него.</p>
<p>Если у вас возникли вопросы или вы не запрашивали восстановление пароля,
<a href="mailto:support@nocopy.com" style="color: #ee5a52;">свяжитесь со службой поддержки</a>.</p>
<p style="font-size: 11px; margin-top: 10px;">
IP запроса: {{ipAddress}} • Время: {{timestamp}} • Устройство: {{deviceInfo}}
</p>
</div>
</div>
</body>
</html>