From 2b11f55bab2d3975dddf71a8c49991fc630abe3f Mon Sep 17 00:00:00 2001 From: vladp Date: Sun, 23 Nov 2025 23:25:58 +0700 Subject: [PATCH 1/8] NCP-1 add docker-compose.yaml,dockerfile and application.yaml --- Dockerfile | 18 ++++++++++ docker-compose.yaml | 52 +++++++++++++++++++++++++++++ src/main/resources/application.yaml | 21 ++++++++++-- 3 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 Dockerfile create mode 100644 docker-compose.yaml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e8b06f0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM mcr.microsoft.com/openjdk/jdk:17.0.13-ubuntu + +WORKDIR /app + +COPY build/libs/app.jar /app/app.jar + +ENV SERVER_PORT=8080 +ENV DB_HOST=mysql +ENV DB_PORT=3306 +ENV DB_NAME=no-copy-db +ENV DB_USERNAME=mysql_user +ENV DB_PASSWORD=mysql_pass +ENV REDIS_HOST=redis +ENV REDIS_PORT=6379 + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 0000000..c875d80 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,52 @@ +version: "3.8" + +services: + mysql: + image: mysql:8.1 + container_name: no-copy-mysql-db + environment: + MYSQL_ROOT_PASSWORD: root_pass + MYSQL_DATABASE: no-copy-db + MYSQL_USER: mysql_user + MYSQL_PASSWORD: mysql_pass + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + networks: + - app-network + command: --default-authentication-plugin=mysql_native_password + + redis: + image: redis:7 + container_name: no-copy-redis + ports: + - "6379:6379" + networks: + - app-network + + no-copy: + build: . + container_name: no-copy-service + ports: + - "8080:8080" + environment: + DB_HOST: mysql + DB_PORT: 3306 + DB_NAME: no-copy-db + DB_USERNAME: mysql_user + DB_PASSWORD: mysql_pass + SERVER_PORT: 8080 + REDIS_HOST: redis + REDIS_PORT: 6379 + depends_on: + - mysql + - redis + networks: + - app-network + +volumes: + mysql_data: + +networks: + app-network: diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index cd9d05a..bb04d87 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,3 +1,18 @@ -пspring: - application: - name: no-copy +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:no-copy-db}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC + username: ${DB_USERNAME:mysql_user} + password: ${DB_PASSWORD:mysql_pass} + + jpa: + hibernate: + ddl-auto: update + show-sql: true + +server: + port: ${SERVER_PORT:8080} + +logging: + level: + root: INFO -- 2.54.0 From ce57cae1770d7834d76d7e0cf6019be2cd5929ad Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 10:26:19 +0700 Subject: [PATCH 2/8] INC-1 add structer for authtoken users --- .../no_copy/controller/AuthController.java | 7 ++++++ .../ru/soune/no_copy/entity/AuthToken.java | 12 ++++++++++ .../java/ru/soune/no_copy/entity/User.java | 23 +++++++++++++++++++ .../soune/no_copy/exception/ApiException.java | 4 ++++ .../repository/AuthTokenRepository.java | 9 ++++++++ .../no_copy/repository/UserRepository.java | 7 ++++++ .../ru/soune/no_copy/service/AuthService.java | 7 ++++++ .../soune/no_copy/service/TokenService.java | 7 ++++++ 8 files changed, 76 insertions(+) create mode 100644 src/main/java/ru/soune/no_copy/controller/AuthController.java create mode 100644 src/main/java/ru/soune/no_copy/entity/AuthToken.java create mode 100644 src/main/java/ru/soune/no_copy/entity/User.java create mode 100644 src/main/java/ru/soune/no_copy/exception/ApiException.java create mode 100644 src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java create mode 100644 src/main/java/ru/soune/no_copy/repository/UserRepository.java create mode 100644 src/main/java/ru/soune/no_copy/service/AuthService.java create mode 100644 src/main/java/ru/soune/no_copy/service/TokenService.java diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java new file mode 100644 index 0000000..149c1d9 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.controller; + +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AuthController { +} diff --git a/src/main/java/ru/soune/no_copy/entity/AuthToken.java b/src/main/java/ru/soune/no_copy/entity/AuthToken.java new file mode 100644 index 0000000..5568fab --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/AuthToken.java @@ -0,0 +1,12 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.Entity; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@NoArgsConstructor +@Getter +public class AuthToken { +} diff --git a/src/main/java/ru/soune/no_copy/entity/User.java b/src/main/java/ru/soune/no_copy/entity/User.java new file mode 100644 index 0000000..49ada2b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/entity/User.java @@ -0,0 +1,23 @@ +package ru.soune.no_copy.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import lombok.*; + +@Entity +@AllArgsConstructor +@NoArgsConstructor +@ToString +@Getter @Setter +public class User { + @Id + private Long userId; + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getUserId() { + return userId; + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/ApiException.java b/src/main/java/ru/soune/no_copy/exception/ApiException.java new file mode 100644 index 0000000..a2e0059 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/ApiException.java @@ -0,0 +1,4 @@ +package ru.soune.no_copy.exception; + +public class ApiException extends RuntimeException { +} diff --git a/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java new file mode 100644 index 0000000..990b787 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java @@ -0,0 +1,9 @@ +package ru.soune.no_copy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.soune.no_copy.entity.AuthToken; + +@Repository +public interface AuthTokenRepository extends JpaRepository { +} diff --git a/src/main/java/ru/soune/no_copy/repository/UserRepository.java b/src/main/java/ru/soune/no_copy/repository/UserRepository.java new file mode 100644 index 0000000..bfd1b20 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/repository/UserRepository.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.soune.no_copy.entity.User; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java new file mode 100644 index 0000000..1051025 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.service; + +import org.springframework.stereotype.Service; + +@Service +public class AuthService { +} diff --git a/src/main/java/ru/soune/no_copy/service/TokenService.java b/src/main/java/ru/soune/no_copy/service/TokenService.java new file mode 100644 index 0000000..81f83e1 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/service/TokenService.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.service; + +import org.springframework.stereotype.Service; + +@Service +public class TokenService { +} -- 2.54.0 From 19198836286e5eb5c72bc3b3ca68d5a7aefd5a56 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 12:29:32 +0700 Subject: [PATCH 3/8] INC-1 add create user endpoint,generate tokens --- build.gradle | 1 + .../configuration/ApplicationConfig.java | 17 ++++ .../configuration/JpaConfiguration.java | 9 +++ .../no_copy/controller/AuthController.java | 22 ++++++ .../ru/soune/no_copy/dto/AuthResponse.java | 5 ++ .../ru/soune/no_copy/dto/RegisterRequest.java | 13 ++++ .../ru/soune/no_copy/entity/AuthToken.java | 27 ++++++- .../java/ru/soune/no_copy/entity/User.java | 51 ++++++++++-- .../soune/no_copy/exception/ApiException.java | 4 - .../exception/UserAlreadyExistsException.java | 7 ++ .../handler/GlobalExceptionHandler.java | 43 ++++++++++ .../repository/AuthTokenRepository.java | 4 + .../no_copy/repository/UserRepository.java | 5 ++ .../ru/soune/no_copy/service/AuthService.java | 53 +++++++++++++ .../soune/no_copy/service/TokenService.java | 7 -- src/main/resources/meassge.properties | 6 ++ .../no_copy/service/AuthTestService.java | 78 +++++++++++++++++++ 17 files changed, 332 insertions(+), 20 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/configuration/ApplicationConfig.java create mode 100644 src/main/java/ru/soune/no_copy/configuration/JpaConfiguration.java create mode 100644 src/main/java/ru/soune/no_copy/dto/AuthResponse.java create mode 100644 src/main/java/ru/soune/no_copy/dto/RegisterRequest.java delete mode 100644 src/main/java/ru/soune/no_copy/exception/ApiException.java create mode 100644 src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java create mode 100644 src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java delete mode 100644 src/main/java/ru/soune/no_copy/service/TokenService.java create mode 100644 src/main/resources/meassge.properties create mode 100644 src/test/java/ru/soune/no_copy/service/AuthTestService.java diff --git a/build.gradle b/build.gradle index 5101285..6af1b68 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.springframework.security:spring-security-crypto:6.5.3' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc' diff --git a/src/main/java/ru/soune/no_copy/configuration/ApplicationConfig.java b/src/main/java/ru/soune/no_copy/configuration/ApplicationConfig.java new file mode 100644 index 0000000..8b0409b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/configuration/ApplicationConfig.java @@ -0,0 +1,17 @@ +package ru.soune.no_copy.configuration; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +@EnableAutoConfiguration +public class ApplicationConfig { + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/ru/soune/no_copy/configuration/JpaConfiguration.java b/src/main/java/ru/soune/no_copy/configuration/JpaConfiguration.java new file mode 100644 index 0000000..56ae18b --- /dev/null +++ b/src/main/java/ru/soune/no_copy/configuration/JpaConfiguration.java @@ -0,0 +1,9 @@ +package ru.soune.no_copy.configuration; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@Configuration +@EnableJpaAuditing +public class JpaConfiguration { +} diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java index 149c1d9..143659f 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -1,7 +1,29 @@ package ru.soune.no_copy.controller; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import ru.soune.no_copy.dto.AuthResponse; +import ru.soune.no_copy.dto.RegisterRequest; +import ru.soune.no_copy.entity.AuthToken; +import ru.soune.no_copy.service.AuthService; @RestController +@RequestMapping("/v1/api/auth") +@RequiredArgsConstructor public class AuthController { + + private final AuthService authService; + + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest registerRequest) { + AuthToken authToken = authService.register(registerRequest); + + return ResponseEntity.ok(new AuthResponse(true, "success.user.register", authToken.getToken(), + authToken.getExpiresAt())); + } } diff --git a/src/main/java/ru/soune/no_copy/dto/AuthResponse.java b/src/main/java/ru/soune/no_copy/dto/AuthResponse.java new file mode 100644 index 0000000..8e76c1a --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/AuthResponse.java @@ -0,0 +1,5 @@ +package ru.soune.no_copy.dto; + +import java.time.LocalDateTime; + +public record AuthResponse (boolean success, String message, String token, LocalDateTime expiresAt) {} diff --git a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java new file mode 100644 index 0000000..3f4fe6e --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java @@ -0,0 +1,13 @@ +package ru.soune.no_copy.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record RegisterRequest( + @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String firstName, + @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String secondName, + @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String lastName, + @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)String email, + @NotBlank(message = "error.not.blank") @Size(min = 8) String password +) {} diff --git a/src/main/java/ru/soune/no_copy/entity/AuthToken.java b/src/main/java/ru/soune/no_copy/entity/AuthToken.java index 5568fab..ee78fe9 100644 --- a/src/main/java/ru/soune/no_copy/entity/AuthToken.java +++ b/src/main/java/ru/soune/no_copy/entity/AuthToken.java @@ -1,12 +1,37 @@ package ru.soune.no_copy.entity; -import jakarta.persistence.Entity; +import jakarta.persistence.*; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; @Entity @NoArgsConstructor @Getter +@Setter +@EntityListeners(AuditingEntityListener.class) +@Table(name = "auth_tokens") public class AuthToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true, length = 64) + private String token; + + @Column(name = "expires_at", nullable = false) + private LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; } diff --git a/src/main/java/ru/soune/no_copy/entity/User.java b/src/main/java/ru/soune/no_copy/entity/User.java index 49ada2b..808c70f 100644 --- a/src/main/java/ru/soune/no_copy/entity/User.java +++ b/src/main/java/ru/soune/no_copy/entity/User.java @@ -1,23 +1,58 @@ package ru.soune.no_copy.entity; -import jakarta.persistence.Entity; -import jakarta.persistence.Id; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import jakarta.validation.constraints.Size; import lombok.*; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; @Entity @AllArgsConstructor @NoArgsConstructor @ToString +@EqualsAndHashCode @Getter @Setter +@EntityListeners(AuditingEntityListener.class) +@Table(name = "users") public class User { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) private Long userId; - public void setUserId(Long userId) { - this.userId = userId; - } + @Size(max = 64) + @Column(name = "firstName", nullable = false, length = 64) + private String firstName; + + @Size(max = 64) + @Column(name = "lastName", nullable = false, length = 64) + private String lastName; + + @Size(max = 64) + @Column(name = "secondName", length = 64) + private String secondName; + + @Size(max = 1024) + @Column(name = "email", nullable = false, length = 1024, unique = true) + private String email; + + @Column(nullable = false) + @JsonIgnore + private String password; + + @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true) + private List tokens = new ArrayList<>(); + + @CreatedDate + @Column(name = "created_at", updatable = false, nullable = false) + private LocalDateTime createdAt; + + @Column(name = "is_active") + private Boolean isActive; - public Long getUserId() { - return userId; - } } diff --git a/src/main/java/ru/soune/no_copy/exception/ApiException.java b/src/main/java/ru/soune/no_copy/exception/ApiException.java deleted file mode 100644 index a2e0059..0000000 --- a/src/main/java/ru/soune/no_copy/exception/ApiException.java +++ /dev/null @@ -1,4 +0,0 @@ -package ru.soune.no_copy.exception; - -public class ApiException extends RuntimeException { -} diff --git a/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java b/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java new file mode 100644 index 0000000..8815885 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/UserAlreadyExistsException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class UserAlreadyExistsException extends RuntimeException { + public UserAlreadyExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java new file mode 100644 index 0000000..0380ccc --- /dev/null +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -0,0 +1,43 @@ +package ru.soune.no_copy.handler; + +import lombok.AllArgsConstructor; +import org.springframework.context.MessageSource; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.soune.no_copy.exception.UserAlreadyExistsException; + +import java.util.Map; + +@RestControllerAdvice +@AllArgsConstructor +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { + String message = ex.getBindingResult().getFieldErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .findFirst() + .orElse(""); + + return ResponseEntity.badRequest() + .body(Map.of( + "success", false, + "message" ,message)); + } + + @ExceptionHandler(UserAlreadyExistsException.class) + @ResponseStatus(HttpStatus.CONFLICT) + public ResponseEntity handleUserContainsException(UserAlreadyExistsException ex) { + return ResponseEntity. + badRequest() + .body(Map.of( + "success", false, + "message" ,ex.getMessage() + )); + } +} diff --git a/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java b/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java index 990b787..725d8e0 100644 --- a/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java +++ b/src/main/java/ru/soune/no_copy/repository/AuthTokenRepository.java @@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import ru.soune.no_copy.entity.AuthToken; +import java.time.LocalDate; +import java.util.Optional; + @Repository public interface AuthTokenRepository extends JpaRepository { + Optional findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter); } diff --git a/src/main/java/ru/soune/no_copy/repository/UserRepository.java b/src/main/java/ru/soune/no_copy/repository/UserRepository.java index bfd1b20..b5aba82 100644 --- a/src/main/java/ru/soune/no_copy/repository/UserRepository.java +++ b/src/main/java/ru/soune/no_copy/repository/UserRepository.java @@ -3,5 +3,10 @@ package ru.soune.no_copy.repository; import org.springframework.data.jpa.repository.JpaRepository; import ru.soune.no_copy.entity.User; +import java.util.Optional; + public interface UserRepository extends JpaRepository { + Optional findByEmail(String email); + + boolean existsByEmail(String email); } diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java index 1051025..968a777 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -1,7 +1,60 @@ package ru.soune.no_copy.service; +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.no_copy.dto.RegisterRequest; +import ru.soune.no_copy.entity.AuthToken; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.UserAlreadyExistsException; +import ru.soune.no_copy.repository.AuthTokenRepository; +import ru.soune.no_copy.repository.UserRepository; + +import java.security.SecureRandom; +import java.time.LocalDateTime; +import java.util.HexFormat; +import java.util.Locale; @Service +@RequiredArgsConstructor public class AuthService { + + private final UserRepository userRepository; + + private final AuthTokenRepository authTokenRepository; + + private final PasswordEncoder passwordEncoder; + + private final MessageSource messageSource; + + @Transactional + public AuthToken register(RegisterRequest registerRequest) { + if (userRepository.existsByEmail(registerRequest.email())) { + throw new UserAlreadyExistsException(messageSource.getMessage("error.user.exists", null, + Locale.getDefault())); + } + + User user = new User(); + user.setFirstName(registerRequest.firstName()); + user.setSecondName(registerRequest.secondName()); + user.setLastName(registerRequest.lastName()); + user.setEmail(registerRequest.email()); + user.setPassword(passwordEncoder.encode(registerRequest.password())); + + User savedUser = userRepository.save(user); + + AuthToken authToken = new AuthToken(); + authToken.setToken(generateToken()); + authToken.setUser(savedUser); + + return authTokenRepository.save(authToken); + } + + private String generateToken() { + byte[] bytes = new byte[32]; + new SecureRandom().nextBytes(bytes); + return HexFormat.of().formatHex(bytes); + } } diff --git a/src/main/java/ru/soune/no_copy/service/TokenService.java b/src/main/java/ru/soune/no_copy/service/TokenService.java deleted file mode 100644 index 81f83e1..0000000 --- a/src/main/java/ru/soune/no_copy/service/TokenService.java +++ /dev/null @@ -1,7 +0,0 @@ -package ru.soune.no_copy.service; - -import org.springframework.stereotype.Service; - -@Service -public class TokenService { -} diff --git a/src/main/resources/meassge.properties b/src/main/resources/meassge.properties new file mode 100644 index 0000000..9a212bf --- /dev/null +++ b/src/main/resources/meassge.properties @@ -0,0 +1,6 @@ +error.user.exists=User exist with current email. +error.name.length=Value mast be length 2 and max 64 symbols. +error.not.email=Not email format +error.not.blank=Must not be empty + +success.user.register=User was register \ No newline at end of file diff --git a/src/test/java/ru/soune/no_copy/service/AuthTestService.java b/src/test/java/ru/soune/no_copy/service/AuthTestService.java new file mode 100644 index 0000000..e5109c4 --- /dev/null +++ b/src/test/java/ru/soune/no_copy/service/AuthTestService.java @@ -0,0 +1,78 @@ +package ru.soune.no_copy.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.context.MessageSource; +import org.springframework.security.crypto.password.PasswordEncoder; +import ru.soune.no_copy.dto.RegisterRequest; +import ru.soune.no_copy.entity.AuthToken; +import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.UserAlreadyExistsException; +import ru.soune.no_copy.repository.AuthTokenRepository; +import ru.soune.no_copy.repository.UserRepository; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +public class AuthTestService { + + @Mock + private UserRepository userRepository; + + @Mock + private AuthTokenRepository authTokenRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private MessageSource messageSource; + + @InjectMocks + private AuthService authService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void registerSuccess() { + RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","password123"); + + when(userRepository.existsByEmail(request.email())).thenReturn(false); + when(passwordEncoder.encode(request.password())).thenReturn("hashed"); + when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(authTokenRepository.save(any(AuthToken.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + AuthToken token = authService.register(request); + User user = token.getUser(); + String email = user.getEmail(); + + assertNotNull(token); + assertNotNull(token.getToken()); + assertNotNull(token.getUser()); + assertEquals("john@example.com", email); + assertNotNull(token.getExpiresAt()); + } + + @Test + void registerExistingEmailThrows() { + RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","123"); + + when(userRepository.existsByEmail(request.email())).thenReturn(true); + when(messageSource.getMessage(anyString(), any(), any(Locale.class))).thenReturn("User exists"); + + UserAlreadyExistsException ex = assertThrows(UserAlreadyExistsException.class, + () -> authService.register(request)); + + assertEquals("User exists", ex.getMessage()); + } +} -- 2.54.0 From 8a9768bd2ae390c3579d597256633d1666cc47a6 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 21:04:50 +0700 Subject: [PATCH 4/8] INC-1 change db,work dockerfile and docker-compose.yaml --- .env | 10 + Dockerfile | 21 +- build.gradle | 11 +- docker-compose.yaml | 71 +++---- .../no_copy/controller/AuthController.java | 2 +- .../handler/GlobalExceptionHandler.java | 1 - .../ru/soune/no_copy/service/AuthService.java | 1 - src/main/resources/application.yaml | 8 +- .../soune/no_copy/NoCopyApplicationTests.java | 13 -- ...hTestService.java => AuthServiceTest.java} | 2 +- wait-for-it.sh | 182 ++++++++++++++++++ 11 files changed, 243 insertions(+), 79 deletions(-) create mode 100644 .env delete mode 100644 src/test/java/ru/soune/no_copy/NoCopyApplicationTests.java rename src/test/java/ru/soune/no_copy/service/{AuthTestService.java => AuthServiceTest.java} (98%) create mode 100644 wait-for-it.sh diff --git a/.env b/.env new file mode 100644 index 0000000..1133522 --- /dev/null +++ b/.env @@ -0,0 +1,10 @@ +POSTGRES_DB=no_copy_db +POSTGRES_USER=postgres +POSTGRES_PASSWORD=postgres +POSTGRES_PORT=5432 +POSTGRES_HOST=postgres + +REDIS_HOST=redis +REDIS_PORT=6379 + +SERVER_PORT=8080 diff --git a/Dockerfile b/Dockerfile index e8b06f0..8242e14 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,17 @@ -FROM mcr.microsoft.com/openjdk/jdk:17.0.13-ubuntu +FROM gradle:8.14.2-jdk21 AS build WORKDIR /app -COPY build/libs/app.jar /app/app.jar +COPY . . -ENV SERVER_PORT=8080 -ENV DB_HOST=mysql -ENV DB_PORT=3306 -ENV DB_NAME=no-copy-db -ENV DB_USERNAME=mysql_user -ENV DB_PASSWORD=mysql_pass -ENV REDIS_HOST=redis -ENV REDIS_PORT=6379 +RUN gradle --no-daemon clean build -x test + +FROM eclipse-temurin:21-jre + +WORKDIR /app + +COPY --from=build /app/build/libs/*.jar app.jar EXPOSE 8080 -ENTRYPOINT ["java", "-jar", "/app/app.jar"] +CMD ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 6af1b68..4e63b11 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,11 @@ plugins { id 'java' - id 'org.springframework.boot' version '4.0.0' + id 'org.springframework.boot' version '3.5.6' id 'io.spring.dependency-management' version '1.1.7' } group = 'ru.soune' version = '0.0.1-SNAPSHOT' -description = 'Demo project for Spring Boot' java { toolchain { @@ -27,16 +26,16 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-webmvc' + implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.security:spring-security-crypto:6.5.3' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc' runtimeOnly 'com.mysql:mysql-connector-j' + runtimeOnly 'org.postgresql:postgresql' annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-data-jpa-test' - testImplementation 'org.springframework.boot:spring-boot-starter-validation-test' - testImplementation 'org.springframework.boot:spring-boot-starter-webmvc-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.mockito:mockito-core:5.3.1' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' } diff --git a/docker-compose.yaml b/docker-compose.yaml index c875d80..69f9587 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,52 +1,41 @@ -version: "3.8" +version: '3.9' services: - mysql: - image: mysql:8.1 - container_name: no-copy-mysql-db + db: + image: postgres:17 + restart: always environment: - MYSQL_ROOT_PASSWORD: root_pass - MYSQL_DATABASE: no-copy-db - MYSQL_USER: mysql_user - MYSQL_PASSWORD: mysql_pass + POSTGRES_DB: no_copy_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_PORT: 5432 + POSTGRES_HOST: postgres + PGDATA: /data/postgres ports: - - "3306:3306" + - "5432:5432" volumes: - - mysql_data:/var/lib/mysql - networks: - - app-network - command: --default-authentication-plugin=mysql_native_password + - pgdata:/var/lib/postgresql/data + healthcheck: + test: [ "CMD-SHELL", "pg_isready -U postgres -p 5432" ] + interval: 10s + timeout: 5s + retries: 20 + container_name: postgres - redis: - image: redis:7 - container_name: no-copy-redis - ports: - - "6379:6379" - networks: - - app-network - - no-copy: + app: build: . - container_name: no-copy-service + container_name: no_copy_app + environment: + POSTGRES_DB: no_copy_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_PORT: 5432 + POSTGRES_HOST: postgres + depends_on: + db: + condition: service_healthy ports: - "8080:8080" - environment: - DB_HOST: mysql - DB_PORT: 3306 - DB_NAME: no-copy-db - DB_USERNAME: mysql_user - DB_PASSWORD: mysql_pass - SERVER_PORT: 8080 - REDIS_HOST: redis - REDIS_PORT: 6379 - depends_on: - - mysql - - redis - networks: - - app-network volumes: - mysql_data: - -networks: - app-network: + pgdata: diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java index 143659f..c88cfb6 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -13,7 +13,7 @@ import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.service.AuthService; @RestController -@RequestMapping("/v1/api/auth") +@RequestMapping("/api/auth") @RequiredArgsConstructor public class AuthController { diff --git a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java index 0380ccc..daa36ee 100644 --- a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -1,7 +1,6 @@ package ru.soune.no_copy.handler; import lombok.AllArgsConstructor; -import org.springframework.context.MessageSource; import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java index 968a777..431be40 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -13,7 +13,6 @@ import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; import java.security.SecureRandom; -import java.time.LocalDateTime; import java.util.HexFormat; import java.util.Locale; diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index bb04d87..2b84ef5 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -1,9 +1,9 @@ spring: datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:no-copy-db}?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC - username: ${DB_USERNAME:mysql_user} - password: ${DB_PASSWORD:mysql_pass} + url: jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + username: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + driver-class-name: org.postgresql.Driver jpa: hibernate: diff --git a/src/test/java/ru/soune/no_copy/NoCopyApplicationTests.java b/src/test/java/ru/soune/no_copy/NoCopyApplicationTests.java deleted file mode 100644 index 0fe0bc6..0000000 --- a/src/test/java/ru/soune/no_copy/NoCopyApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.soune.no_copy; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class NoCopyApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/ru/soune/no_copy/service/AuthTestService.java b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java similarity index 98% rename from src/test/java/ru/soune/no_copy/service/AuthTestService.java rename to src/test/java/ru/soune/no_copy/service/AuthServiceTest.java index e5109c4..995af88 100644 --- a/src/test/java/ru/soune/no_copy/service/AuthTestService.java +++ b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java @@ -21,7 +21,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.when; -public class AuthTestService { +public class AuthServiceTest { @Mock private UserRepository userRepository; diff --git a/wait-for-it.sh b/wait-for-it.sh new file mode 100644 index 0000000..d990e0d --- /dev/null +++ b/wait-for-it.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi -- 2.54.0 From 0ba5dcc1d8c553456cb451d85d69ba449c40ec98 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 22:39:08 +0700 Subject: [PATCH 5/8] INC-1 --- .env | 2 +- docker-compose.yaml | 21 ++++++--------------- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/.env b/.env index 1133522..d3e4347 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ -POSTGRES_DB=no_copy_db +POSTGRES_DB=no_copy_ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_PORT=5432 diff --git a/docker-compose.yaml b/docker-compose.yaml index 69f9587..dfbd31b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -5,37 +5,28 @@ services: image: postgres:17 restart: always environment: - POSTGRES_DB: no_copy_db + POSTGRES_DB: no_copy_ POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_PORT: 5432 - POSTGRES_HOST: postgres - PGDATA: /data/postgres ports: - - "5432:5432" + - "54320:5432" volumes: - pgdata:/var/lib/postgresql/data - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres -p 5432" ] - interval: 10s - timeout: 5s - retries: 20 container_name: postgres app: build: . container_name: no_copy_app environment: - POSTGRES_DB: no_copy_db + POSTGRES_DB: no_copy_ POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_PORT: 5432 - POSTGRES_HOST: postgres + POSTGRES_HOST: db depends_on: - db: - condition: service_healthy + - db ports: - "8080:8080" volumes: - pgdata: + pgdata: \ No newline at end of file -- 2.54.0 From 633644b1495a187d51f3dda097b6ae143bec8c73 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 22:53:11 +0700 Subject: [PATCH 6/8] INC-1 add docker image for compose --- .dockerignore | 5 +++++ docker-compose.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e03b401 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitignore +README.md +.gradle +build \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index dfbd31b..6e06b4b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -15,7 +15,7 @@ services: container_name: postgres app: - build: . + image: popovtsev/ncp container_name: no_copy_app environment: POSTGRES_DB: no_copy_ -- 2.54.0 From 3626a51b57c5af60a270c1a07de53d4074979ca7 Mon Sep 17 00:00:00 2001 From: vladp Date: Mon, 24 Nov 2025 23:36:26 +0700 Subject: [PATCH 7/8] INC-1 add start logic for login --- .../no_copy/controller/AuthController.java | 12 ++++++ .../ru/soune/no_copy/dto/LoginRequest.java | 12 ++++++ .../ru/soune/no_copy/dto/LoginResponse.java | 13 +++++++ .../ru/soune/no_copy/service/AuthService.java | 37 +++++++++++++++++++ 4 files changed, 74 insertions(+) create mode 100644 src/main/java/ru/soune/no_copy/dto/LoginRequest.java create mode 100644 src/main/java/ru/soune/no_copy/dto/LoginResponse.java diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java index c88cfb6..b2bce62 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -8,6 +8,8 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import ru.soune.no_copy.dto.AuthResponse; +import ru.soune.no_copy.dto.LoginRequest; +import ru.soune.no_copy.dto.LoginResponse; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.service.AuthService; @@ -26,4 +28,14 @@ public class AuthController { return ResponseEntity.ok(new AuthResponse(true, "success.user.register", authToken.getToken(), authToken.getExpiresAt())); } + + @PostMapping("/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + //TODO add try-catch and response code +// try { + return ResponseEntity.ok(authService.login(request)); +// } catch (RuntimeException e) { +// return ResponseEntity.badRequest(); +// } + } } diff --git a/src/main/java/ru/soune/no_copy/dto/LoginRequest.java b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java new file mode 100644 index 0000000..852702c --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java @@ -0,0 +1,12 @@ +package ru.soune.no_copy.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +public class LoginRequest { + + private String email; + + private String password; +} diff --git a/src/main/java/ru/soune/no_copy/dto/LoginResponse.java b/src/main/java/ru/soune/no_copy/dto/LoginResponse.java new file mode 100644 index 0000000..3333272 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/dto/LoginResponse.java @@ -0,0 +1,13 @@ +package ru.soune.no_copy.dto; + +import lombok.Data; +import ru.soune.no_copy.entity.User; + +@Data +public class LoginResponse { + private boolean success; + private String message; + private User user; + private String token; + private String expiresAt; +} diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java index 431be40..a2aa84c 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -5,6 +5,8 @@ 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.no_copy.dto.LoginRequest; +import ru.soune.no_copy.dto.LoginResponse; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.User; @@ -13,8 +15,10 @@ import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; import java.security.SecureRandom; +import java.time.LocalDateTime; import java.util.HexFormat; import java.util.Locale; +import java.util.Optional; @Service @RequiredArgsConstructor @@ -51,6 +55,39 @@ public class AuthService { return authTokenRepository.save(authToken); } + public LoginResponse login(LoginRequest request) { + Optional userOpt = userRepository.findByEmail(request.getEmail()); + if (userOpt.isEmpty()) { + //ADD EXCEPTION + } + + User user = userOpt.get(); + + if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { + //ADD EXCEPTION + } + + String token = generateToken(); + LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); + + AuthToken authToken = new AuthToken(); + authToken.setToken(generateToken()); + authToken.setUser(user); + + authTokenRepository.save(authToken); + + userRepository.save(user); + + LoginResponse response = new LoginResponse(); + response.setSuccess(true); +// response.setMessage(""); add message + response.setUser(user); + response.setToken(token); + response.setExpiresAt(expiresAt.toString()); + + return response; + } +//TODO change logic generate token private String generateToken() { byte[] bytes = new byte[32]; new SecureRandom().nextBytes(bytes); -- 2.54.0 From 74ae769de04b967b6554da43335a0fff67849edc Mon Sep 17 00:00:00 2001 From: vladp Date: Tue, 25 Nov 2025 11:59:37 +0700 Subject: [PATCH 8/8] INC-1 add login function --- .../no_copy/controller/AuthController.java | 12 ++-- .../ru/soune/no_copy/dto/LoginRequest.java | 6 ++ .../ru/soune/no_copy/dto/LoginResponse.java | 5 +- .../ru/soune/no_copy/dto/RegisterRequest.java | 2 +- .../NotValidationPasswordException.java | 7 ++ .../exception/UserNotFoundException.java | 7 ++ .../handler/GlobalExceptionHandler.java | 24 +++++++ .../ru/soune/no_copy/service/AuthService.java | 45 +++++------- .../no_copy/service/AuthServiceTest.java | 72 +++++++++++++++++-- 9 files changed, 139 insertions(+), 41 deletions(-) create mode 100644 src/main/java/ru/soune/no_copy/exception/NotValidationPasswordException.java create mode 100644 src/main/java/ru/soune/no_copy/exception/UserNotFoundException.java diff --git a/src/main/java/ru/soune/no_copy/controller/AuthController.java b/src/main/java/ru/soune/no_copy/controller/AuthController.java index b2bce62..5c4e380 100644 --- a/src/main/java/ru/soune/no_copy/controller/AuthController.java +++ b/src/main/java/ru/soune/no_copy/controller/AuthController.java @@ -30,12 +30,10 @@ public class AuthController { } @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest request) { - //TODO add try-catch and response code -// try { - return ResponseEntity.ok(authService.login(request)); -// } catch (RuntimeException e) { -// return ResponseEntity.badRequest(); -// } + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + AuthToken login = authService.login(request); + + return ResponseEntity.ok(new LoginResponse(true, login.getUser().getEmail(), + login.getToken(),login.getExpiresAt().toString())); } } diff --git a/src/main/java/ru/soune/no_copy/dto/LoginRequest.java b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java index 852702c..47602d2 100644 --- a/src/main/java/ru/soune/no_copy/dto/LoginRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/LoginRequest.java @@ -1,12 +1,18 @@ package ru.soune.no_copy.dto; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Data; @Data +@AllArgsConstructor public class LoginRequest { + @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128) private String email; + @NotBlank(message = "error.not.blank") @Size(min = 8) private String password; } diff --git a/src/main/java/ru/soune/no_copy/dto/LoginResponse.java b/src/main/java/ru/soune/no_copy/dto/LoginResponse.java index 3333272..5fe7d73 100644 --- a/src/main/java/ru/soune/no_copy/dto/LoginResponse.java +++ b/src/main/java/ru/soune/no_copy/dto/LoginResponse.java @@ -1,13 +1,14 @@ package ru.soune.no_copy.dto; +import lombok.AllArgsConstructor; import lombok.Data; import ru.soune.no_copy.entity.User; @Data +@AllArgsConstructor public class LoginResponse { private boolean success; - private String message; - private User user; + private String email; private String token; private String expiresAt; } diff --git a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java index 3f4fe6e..bdf67e9 100644 --- a/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java +++ b/src/main/java/ru/soune/no_copy/dto/RegisterRequest.java @@ -8,6 +8,6 @@ public record RegisterRequest( @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String firstName, @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String secondName, @NotBlank(message = "error.name.length") @Size(min = 2, max = 64) String lastName, - @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128)String email, + @NotBlank(message = "error.not.blank") @Email(message = "error.not.email") @Size(max = 128) String email, @NotBlank(message = "error.not.blank") @Size(min = 8) String password ) {} diff --git a/src/main/java/ru/soune/no_copy/exception/NotValidationPasswordException.java b/src/main/java/ru/soune/no_copy/exception/NotValidationPasswordException.java new file mode 100644 index 0000000..6e24040 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/NotValidationPasswordException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class NotValidationPasswordException extends RuntimeException { + public NotValidationPasswordException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/exception/UserNotFoundException.java b/src/main/java/ru/soune/no_copy/exception/UserNotFoundException.java new file mode 100644 index 0000000..a8d8b03 --- /dev/null +++ b/src/main/java/ru/soune/no_copy/exception/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.soune.no_copy.exception; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java index daa36ee..b25623e 100644 --- a/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java +++ b/src/main/java/ru/soune/no_copy/handler/GlobalExceptionHandler.java @@ -8,7 +8,9 @@ import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; +import ru.soune.no_copy.exception.NotValidationPasswordException; import ru.soune.no_copy.exception.UserAlreadyExistsException; +import ru.soune.no_copy.exception.UserNotFoundException; import java.util.Map; @@ -39,4 +41,26 @@ public class GlobalExceptionHandler { "message" ,ex.getMessage() )); } + + @ExceptionHandler(UserNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public ResponseEntity handleUserNotFoundException(UserNotFoundException ex) { + return ResponseEntity. + badRequest() + .body(Map.of( + "success", false, + "message" ,ex.getMessage() + )); + } + + @ExceptionHandler(NotValidationPasswordException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity handleNotValidationPasswordException(NotValidationPasswordException ex) { + return ResponseEntity. + badRequest() + .body(Map.of( + "success", false, + "message" ,ex.getMessage() + )); + } } diff --git a/src/main/java/ru/soune/no_copy/service/AuthService.java b/src/main/java/ru/soune/no_copy/service/AuthService.java index a2aa84c..524c28c 100644 --- a/src/main/java/ru/soune/no_copy/service/AuthService.java +++ b/src/main/java/ru/soune/no_copy/service/AuthService.java @@ -6,17 +6,17 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import ru.soune.no_copy.dto.LoginRequest; -import ru.soune.no_copy.dto.LoginResponse; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.NotValidationPasswordException; import ru.soune.no_copy.exception.UserAlreadyExistsException; +import ru.soune.no_copy.exception.UserNotFoundException; import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; import java.security.SecureRandom; -import java.time.LocalDateTime; -import java.util.HexFormat; +import java.util.Base64; import java.util.Locale; import java.util.Optional; @@ -32,6 +32,8 @@ public class AuthService { private final MessageSource messageSource; + private final SecureRandom secureRandom = new SecureRandom(); + @Transactional public AuthToken register(RegisterRequest registerRequest) { if (userRepository.existsByEmail(registerRequest.email())) { @@ -49,48 +51,37 @@ public class AuthService { User savedUser = userRepository.save(user); AuthToken authToken = new AuthToken(); - authToken.setToken(generateToken()); + authToken.setToken(generateAuthToken()); authToken.setUser(savedUser); return authTokenRepository.save(authToken); } - public LoginResponse login(LoginRequest request) { + @Transactional + public AuthToken login(LoginRequest request) { Optional userOpt = userRepository.findByEmail(request.getEmail()); + if (userOpt.isEmpty()) { - //ADD EXCEPTION + throw new UserNotFoundException("User with email " + request.getEmail() + " not found"); } User user = userOpt.get(); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { - //ADD EXCEPTION + throw new NotValidationPasswordException("Invalid password"); } - String token = generateToken(); - LocalDateTime expiresAt = LocalDateTime.now().plusDays(30); - AuthToken authToken = new AuthToken(); - authToken.setToken(generateToken()); + authToken.setToken(generateAuthToken()); authToken.setUser(user); - authTokenRepository.save(authToken); - - userRepository.save(user); - - LoginResponse response = new LoginResponse(); - response.setSuccess(true); -// response.setMessage(""); add message - response.setUser(user); - response.setToken(token); - response.setExpiresAt(expiresAt.toString()); - - return response; + return authTokenRepository.save(authToken); } -//TODO change logic generate token - private String generateToken() { + + + private String generateAuthToken() { byte[] bytes = new byte[32]; - new SecureRandom().nextBytes(bytes); - return HexFormat.of().formatHex(bytes); + secureRandom.nextBytes(bytes); + return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes); } } diff --git a/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java index 995af88..6e4c91a 100644 --- a/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java +++ b/src/test/java/ru/soune/no_copy/service/AuthServiceTest.java @@ -2,23 +2,29 @@ package ru.soune.no_copy.service; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.springframework.context.MessageSource; import org.springframework.security.crypto.password.PasswordEncoder; +import ru.soune.no_copy.dto.LoginRequest; import ru.soune.no_copy.dto.RegisterRequest; import ru.soune.no_copy.entity.AuthToken; import ru.soune.no_copy.entity.User; +import ru.soune.no_copy.exception.NotValidationPasswordException; import ru.soune.no_copy.exception.UserAlreadyExistsException; +import ru.soune.no_copy.exception.UserNotFoundException; import ru.soune.no_copy.repository.AuthTokenRepository; import ru.soune.no_copy.repository.UserRepository; import java.util.Locale; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class AuthServiceTest { @@ -45,7 +51,8 @@ public class AuthServiceTest { @Test void registerSuccess() { - RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","password123"); + RegisterRequest request = new RegisterRequest("John","A.","Doe", + "john@example.com", "password123"); when(userRepository.existsByEmail(request.email())).thenReturn(false); when(passwordEncoder.encode(request.password())).thenReturn("hashed"); @@ -65,14 +72,71 @@ public class AuthServiceTest { @Test void registerExistingEmailThrows() { - RegisterRequest request = new RegisterRequest("John","A.","Doe","john@example.com","123"); + RegisterRequest request = new RegisterRequest("John","A.","Doe", + "john@example.com", "password123"); - when(userRepository.existsByEmail(request.email())).thenReturn(true); - when(messageSource.getMessage(anyString(), any(), any(Locale.class))).thenReturn("User exists"); + when(userRepository.existsByEmail(request.email())) + .thenReturn(true); + when(messageSource.getMessage(anyString(), any(), any(Locale.class))) + .thenReturn("User exists"); UserAlreadyExistsException ex = assertThrows(UserAlreadyExistsException.class, () -> authService.register(request)); assertEquals("User exists", ex.getMessage()); } + + @Test + void login_success() { + LoginRequest request = new LoginRequest("test@mail.com", "password"); + + User user = new User(); + user.setUserId(1L); + user.setEmail("test@mail.com"); + user.setPassword("encoded_pass"); + + when(userRepository.findByEmail("test@mail.com")) + .thenReturn(Optional.of(user)); + when(passwordEncoder.matches("password", "encoded_pass")) + .thenReturn(true); + when(authTokenRepository.save(any(AuthToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + AuthToken token = authService.login(request); + + assertNotNull(token); + assertNotNull(token.getToken()); + assertEquals(user, token.getUser()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(AuthToken.class); + verify(authTokenRepository).save(captor.capture()); + + assertEquals(user, captor.getValue().getUser()); + assertNotNull(captor.getValue().getToken()); + } + + + @Test + void login_userNotFound() { + LoginRequest request = new LoginRequest("missing@mail.com", "pwd"); + + when(userRepository.findByEmail("missing@mail.com")).thenReturn(Optional.empty()); + + assertThrows(UserNotFoundException.class, () -> authService.login(request)); + } + + + @Test + void login_invalidPassword() { + LoginRequest request = new LoginRequest("test@mail.com", "wrong"); + + User user = new User(); + user.setEmail("test@mail.com"); + user.setPassword("encoded_pass"); + + when(userRepository.findByEmail("test@mail.com")).thenReturn(Optional.of(user)); + when(passwordEncoder.matches("wrong", "encoded_pass")).thenReturn(false); + + assertThrows(NotValidationPasswordException.class, () -> authService.login(request)); + } } -- 2.54.0