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)); + } }