INC-1 add create user endpoint,generate tokens
This commit is contained in:
@@ -28,6 +28,7 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
|
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
|
||||||
|
implementation 'org.springframework.security:spring-security-crypto:6.5.3'
|
||||||
compileOnly 'org.projectlombok:lombok'
|
compileOnly 'org.projectlombok:lombok'
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
|
runtimeOnly 'com.microsoft.sqlserver:mssql-jdbc'
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -1,7 +1,29 @@
|
|||||||
package ru.soune.no_copy.controller;
|
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 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
|
@RestController
|
||||||
|
@RequestMapping("/v1/api/auth")
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class AuthController {
|
public class AuthController {
|
||||||
|
|
||||||
|
private final AuthService authService;
|
||||||
|
|
||||||
|
@PostMapping("/register")
|
||||||
|
public ResponseEntity<AuthResponse> register(@Valid @RequestBody RegisterRequest registerRequest) {
|
||||||
|
AuthToken authToken = authService.register(registerRequest);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(new AuthResponse(true, "success.user.register", authToken.getToken(),
|
||||||
|
authToken.getExpiresAt()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {}
|
||||||
@@ -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
|
||||||
|
) {}
|
||||||
@@ -1,12 +1,37 @@
|
|||||||
package ru.soune.no_copy.entity;
|
package ru.soune.no_copy.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.*;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Table(name = "auth_tokens")
|
||||||
public class AuthToken {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,58 @@
|
|||||||
package ru.soune.no_copy.entity;
|
package ru.soune.no_copy.entity;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.*;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
import lombok.*;
|
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
|
@Entity
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@ToString
|
@ToString
|
||||||
|
@EqualsAndHashCode
|
||||||
@Getter @Setter
|
@Getter @Setter
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Table(name = "users")
|
||||||
public class User {
|
public class User {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long userId;
|
private Long userId;
|
||||||
|
|
||||||
public void setUserId(Long userId) {
|
@Size(max = 64)
|
||||||
this.userId = userId;
|
@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<AuthToken> 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
package ru.soune.no_copy.exception;
|
|
||||||
|
|
||||||
public class ApiException extends RuntimeException {
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package ru.soune.no_copy.exception;
|
||||||
|
|
||||||
|
public class UserAlreadyExistsException extends RuntimeException {
|
||||||
|
public UserAlreadyExistsException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,10 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
import ru.soune.no_copy.entity.AuthToken;
|
import ru.soune.no_copy.entity.AuthToken;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
|
public interface AuthTokenRepository extends JpaRepository<AuthToken, Long> {
|
||||||
|
Optional<AuthToken> findByTokenAndExpiresAtAfter(String token, LocalDate expiresAtAfter);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,10 @@ package ru.soune.no_copy.repository;
|
|||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import ru.soune.no_copy.entity.User;
|
import ru.soune.no_copy.entity.User;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByEmail(String email);
|
||||||
|
|
||||||
|
boolean existsByEmail(String email);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,60 @@
|
|||||||
package ru.soune.no_copy.service;
|
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.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
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class AuthService {
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
package ru.soune.no_copy.service;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class TokenService {
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user