From a26f0f5f72164d422f4d425aa7bb5e259cc62259 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Wed, 18 Dec 2024 17:27:18 +0300 Subject: [PATCH 01/12] #22 feat(backend): implements user registration with creating jwt token --- backend/pom.xml | 27 ++++ .../LenkaMessengerApplication.java | 2 +- .../config/SecurityConfiguration.java | 69 +++++++++ .../controller/auth/AuthController.java | 26 ++++ .../dto/user/JwtAuthenticationResponse.java | 20 +++ .../dto/user/SignUpRequest.java | 92 ++++++++++++ .../exception/BadRequestException.java | 7 + .../exception/ExceptionsHandler.java | 95 +++++++++++++ .../exception/model/ApiError.java | 68 +++++++++ .../lenkamessenger/mapper/UserMapper.java | 23 +++ .../lenkamessenger/model/user/Role.java | 6 + .../lenkamessenger/model/user/User.java | 134 ++++++++++++++++++ .../repository/UserRepository.java | 17 +++ .../service/auth/AuthenticationService.java | 9 ++ .../auth/AuthenticationServiceImpl.java | 35 +++++ .../service/jwt/JwtService.java | 15 ++ .../service/jwt/JwtServiceImpl.java | 48 +++++++ .../service/user/UserService.java | 13 ++ .../service/user/UserServiceImpl.java | 45 ++++++ .../src/main/resources/application.properties | 12 -- backend/src/main/resources/application.yml | 25 ++++ .../LenkaMessengerApplicationTests.java | 2 +- docker-compose.dev.yaml | 1 + docker-compose.prod.yaml | 1 + 24 files changed, 778 insertions(+), 14 deletions(-) rename backend/src/main/java/ru/pominov/{lenka_messenger => lenkamessenger}/LenkaMessengerApplication.java (89%) create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/dto/user/JwtAuthenticationResponse.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/exception/BadRequestException.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/mapper/UserMapper.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/model/user/Role.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/model/user/User.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/repository/UserRepository.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java delete mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/application.yml rename backend/src/test/java/ru/pominov/{lenka_messenger => lenkamessenger}/LenkaMessengerApplicationTests.java (84%) diff --git a/backend/pom.xml b/backend/pom.xml index 3b7b262..f1dff09 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -29,10 +29,37 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-validation + org.springframework.boot spring-boot-starter-security + + org.apache.commons + commons-lang3 + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + + + org.slf4j + slf4j-api + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + org.springframework.boot spring-boot-starter-web diff --git a/backend/src/main/java/ru/pominov/lenka_messenger/LenkaMessengerApplication.java b/backend/src/main/java/ru/pominov/lenkamessenger/LenkaMessengerApplication.java similarity index 89% rename from backend/src/main/java/ru/pominov/lenka_messenger/LenkaMessengerApplication.java rename to backend/src/main/java/ru/pominov/lenkamessenger/LenkaMessengerApplication.java index 5a47dc2..4d130e3 100644 --- a/backend/src/main/java/ru/pominov/lenka_messenger/LenkaMessengerApplication.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/LenkaMessengerApplication.java @@ -1,4 +1,4 @@ -package ru.pominov.lenka_messenger; +package ru.pominov.lenkamessenger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java b/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java new file mode 100644 index 0000000..e2a017f --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java @@ -0,0 +1,69 @@ +package ru.pominov.lenkamessenger.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import ru.pominov.lenkamessenger.service.user.UserService; + +import java.util.List; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfiguration { + + private final UserService userService; + + public SecurityConfiguration(UserService userService) { + this.userService = userService; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .cors(cors -> cors.configurationSource(request -> { + var corsConfiguration = new CorsConfiguration(); + corsConfiguration.setAllowedOriginPatterns(List.of("*")); + corsConfiguration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS")); + corsConfiguration.setAllowedHeaders(List.of("*")); + corsConfiguration.setAllowCredentials(true); + return corsConfiguration; + })) + .authorizeHttpRequests(request -> request + .requestMatchers("/auth/**").permitAll() + .anyRequest().authenticated()) + .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider()); + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userService.userDetailsService()); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java b/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java new file mode 100644 index 0000000..a5331e7 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java @@ -0,0 +1,26 @@ +package ru.pominov.lenkamessenger.controller.auth; + +import jakarta.validation.Valid; +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.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; +import ru.pominov.lenkamessenger.service.auth.AuthenticationService; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + private final AuthenticationService authenticationService; + + public AuthController(AuthenticationService authenticationService) { + this.authenticationService = authenticationService; + } + + @PostMapping("/register") + public JwtAuthenticationResponse signUp(@RequestBody @Valid SignUpRequest request) { + return authenticationService.signUp(request); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/JwtAuthenticationResponse.java b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/JwtAuthenticationResponse.java new file mode 100644 index 0000000..73e859d --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/JwtAuthenticationResponse.java @@ -0,0 +1,20 @@ +package ru.pominov.lenkamessenger.dto.user; + +public class JwtAuthenticationResponse { + + public JwtAuthenticationResponse() {} + + public JwtAuthenticationResponse(String token) { + this.token = token; + } + + private String token; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java new file mode 100644 index 0000000..ead1ade --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java @@ -0,0 +1,92 @@ +package ru.pominov.lenkamessenger.dto.user; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import java.util.Objects; + +public class SignUpRequest { + + public SignUpRequest(String username, String password, String email, String firstName, String lastName) { + this.username = username; + this.password = password; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + } + + @NotBlank + @Size(min = 5, max = 50) + private String username; + + @NotBlank + @Size(min = 8, max = 255) + private String password; + + @Email + private String email; + + @NotBlank + private String firstName; + + @NotBlank + private String lastName; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SignUpRequest that = (SignUpRequest) o; + return Objects.equals(username, that.username) && + Objects.equals(password, that.password) && + Objects.equals(email, that.email) && + Objects.equals(firstName, that.firstName) && + Objects.equals(lastName, that.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(username, password, email, firstName, lastName); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/exception/BadRequestException.java b/backend/src/main/java/ru/pominov/lenkamessenger/exception/BadRequestException.java new file mode 100644 index 0000000..14744e7 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/exception/BadRequestException.java @@ -0,0 +1,7 @@ +package ru.pominov.lenkamessenger.exception; + +public class BadRequestException extends RuntimeException { + public BadRequestException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java b/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java new file mode 100644 index 0000000..94fa986 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java @@ -0,0 +1,95 @@ +package ru.pominov.lenkamessenger.exception; + +import jakarta.validation.ConstraintViolationException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import ru.pominov.lenkamessenger.exception.model.ApiError; + +import java.time.LocalDateTime; + +@RestControllerAdvice +public class ExceptionsHandler { + + private final static Logger log = LoggerFactory.getLogger(ExceptionsHandler.class); + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final MethodArgumentNotValidException ex) { + return badRequest(ex); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final ConstraintViolationException ex) { + return badRequest(ex); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final HttpMessageNotReadableException ex) { + return badRequest(ex); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleIllegalArgumentException(final IllegalArgumentException validException) { + return badRequest(validException); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final MethodArgumentTypeMismatchException mismatchException) { + return badRequest(mismatchException); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleBadRequest(final MissingServletRequestParameterException missingParameterException) { + return badRequest(missingParameterException); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.CONFLICT) + public ApiError handleConflict(final DataIntegrityViolationException violationException) { + return conflict(violationException); + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ApiError handleNotFound(final BadRequestException badRequestException) { + return badRequest(badRequestException); + } + + private ApiError badRequest(final Exception e) { + log.warn("400 {}", e.getMessage()); + + ApiError error = new ApiError(); + error.setStatus(HttpStatus.BAD_REQUEST); + error.setReason("Incorrectly made request."); + error.setMessage(e.getMessage()); + error.setTimestamp(LocalDateTime.now().toString()); + + return error; + } + + private ApiError conflict(final Exception e) { + log.warn("409 {}", e.getMessage()); + + ApiError error = new ApiError(); + error.setStatus(HttpStatus.CONFLICT); + error.setReason("Integrity constraint has been violated."); + error.setMessage(e.getMessage()); + error.setTimestamp(LocalDateTime.now().toString()); + + return error; + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java b/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java new file mode 100644 index 0000000..e120d53 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java @@ -0,0 +1,68 @@ +package ru.pominov.lenkamessenger.exception.model; + +import org.springframework.http.HttpStatus; + +import java.util.Objects; + +public class ApiError { + + public ApiError() {} + + public ApiError(HttpStatus status, String reason, String message, String timestamp) { + this.status = status; + this.reason = reason; + this.message = message; + this.timestamp = timestamp; + } + + private HttpStatus status; + private String reason; + private String message; + + private String timestamp; + + public HttpStatus getStatus() { + return status; + } + + public void setStatus(HttpStatus status) { + this.status = status; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ApiError apiError = (ApiError) o; + return status == apiError.status && Objects.equals(reason, apiError.reason) && Objects.equals(message, apiError.message) && Objects.equals(timestamp, apiError.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(status, reason, message, timestamp); + } +} \ No newline at end of file diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/mapper/UserMapper.java b/backend/src/main/java/ru/pominov/lenkamessenger/mapper/UserMapper.java new file mode 100644 index 0000000..8e40f12 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/mapper/UserMapper.java @@ -0,0 +1,23 @@ +package ru.pominov.lenkamessenger.mapper; + +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; +import ru.pominov.lenkamessenger.model.user.Role; +import ru.pominov.lenkamessenger.model.user.User; + +public final class UserMapper { + + private UserMapper() { + throw new UnsupportedOperationException("UserMapper is utility class"); + } + + public static User toUser(SignUpRequest request) { + User user = new User(); + user.setUsername(request.getUsername()); + user.setEmail(request.getEmail()); + user.setFirstName(request.getFirstName()); + user.setLastName(request.getLastName()); + user.setRole(Role.ROLE_USER); + + return user; + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/model/user/Role.java b/backend/src/main/java/ru/pominov/lenkamessenger/model/user/Role.java new file mode 100644 index 0000000..fca4e47 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/model/user/Role.java @@ -0,0 +1,6 @@ +package ru.pominov.lenkamessenger.model.user; + +public enum Role { + ROLE_USER, + ROLE_ADMIN +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/model/user/User.java b/backend/src/main/java/ru/pominov/lenkamessenger/model/user/User.java new file mode 100644 index 0000000..df7bab2 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/model/user/User.java @@ -0,0 +1,134 @@ +package ru.pominov.lenkamessenger.model.user; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "users") +public class User implements UserDetails { + + public User() {} + + public User(Long id, String username, String password, String email, String firstName, String lastName, Role role) { + this.id = id; + this.username = username; + this.password = password; + this.email = email; + this.firstName = firstName; + this.lastName = lastName; + this.role = role; + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "username", unique = true, nullable = false) + private String username; + + @Column(name = "password", nullable = false) + private String password; + + @Column(name = "email", unique = true, nullable = false) + private String email; + + @Column(name = "first_name") + private String firstName; + + @Column(name = "last_name") + private String lastName; + + @Enumerated(EnumType.STRING) + @Column(name = "role", nullable = false) + private Role role; + + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority(role.name())); + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public Role getRole() { + return role; + } + + public void setRole(Role role) { + this.role = role; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/repository/UserRepository.java b/backend/src/main/java/ru/pominov/lenkamessenger/repository/UserRepository.java new file mode 100644 index 0000000..4b61bf0 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/repository/UserRepository.java @@ -0,0 +1,17 @@ +package ru.pominov.lenkamessenger.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import ru.pominov.lenkamessenger.model.user.User; + +import java.util.Optional; + +@Repository +public interface UserRepository extends JpaRepository { + + Optional findByUsername(String username); + + boolean existsByUsername(String username); + + boolean existsByEmail(String email); +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java new file mode 100644 index 0000000..38e12b4 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java @@ -0,0 +1,9 @@ +package ru.pominov.lenkamessenger.service.auth; + +import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; + +public interface AuthenticationService { + + JwtAuthenticationResponse signUp(SignUpRequest request); +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java new file mode 100644 index 0000000..aa10fcd --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java @@ -0,0 +1,35 @@ +package ru.pominov.lenkamessenger.service.auth; + +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; +import ru.pominov.lenkamessenger.mapper.UserMapper; +import ru.pominov.lenkamessenger.model.user.User; +import ru.pominov.lenkamessenger.service.jwt.JwtService; +import ru.pominov.lenkamessenger.service.user.UserService; + +@Service +public class AuthenticationServiceImpl implements AuthenticationService { + + private final UserService userService; + private final JwtService jwtService; + private final PasswordEncoder passwordEncoder; + + public AuthenticationServiceImpl(UserService userService, JwtService jwtService, PasswordEncoder passwordEncoder) { + this.userService = userService; + this.jwtService = jwtService; + this.passwordEncoder = passwordEncoder; + } + + @Override + public JwtAuthenticationResponse signUp(SignUpRequest request) { + User user = UserMapper.toUser(request); + user.setPassword(passwordEncoder.encode(request.getPassword())); + + userService.create(user); + + var jwt = jwtService.generateToken(user); + return new JwtAuthenticationResponse(jwt); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java new file mode 100644 index 0000000..303f1f0 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java @@ -0,0 +1,15 @@ +package ru.pominov.lenkamessenger.service.jwt; + +import org.springframework.security.core.userdetails.UserDetails; + +import java.security.Key; +import java.util.Map; + +public interface JwtService { + + String generateToken(UserDetails userDetails); + + String generateToken(Map extraClaims, UserDetails userDetails); + + Key getSigningKey(); +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java new file mode 100644 index 0000000..d37826a --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java @@ -0,0 +1,48 @@ +package ru.pominov.lenkamessenger.service.jwt; + +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; +import ru.pominov.lenkamessenger.model.user.User; + +import java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class JwtServiceImpl implements JwtService { + + @Value("${token.signing.key}") + private String jwtSigningKey; + + @Override + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + if (userDetails instanceof User customUserDetails) { + claims.put("id", customUserDetails.getId()); + claims.put("email", customUserDetails.getEmail()); + claims.put("role", customUserDetails.getRole()); + } + return generateToken(claims, userDetails); + } + + @Override + public String generateToken(Map extraClaims, UserDetails userDetails) { + return Jwts.builder().claims(extraClaims).subject(userDetails.getUsername()) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + 100000 * 60 * 24)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + @Override + public Key getSigningKey() { + byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); + return Keys.hmacShaKeyFor(keyBytes); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java new file mode 100644 index 0000000..a7003c1 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java @@ -0,0 +1,13 @@ +package ru.pominov.lenkamessenger.service.user; + +import org.springframework.security.core.userdetails.UserDetailsService; +import ru.pominov.lenkamessenger.model.user.User; + +public interface UserService { + + User create(User user); + + User getByUsername(String username); + + UserDetailsService userDetailsService(); +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java new file mode 100644 index 0000000..cc7d973 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java @@ -0,0 +1,45 @@ +package ru.pominov.lenkamessenger.service.user; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import ru.pominov.lenkamessenger.exception.BadRequestException; +import ru.pominov.lenkamessenger.model.user.User; +import ru.pominov.lenkamessenger.repository.UserRepository; + +@Service +public class UserServiceImpl implements UserService { + + private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); + + private final UserRepository userRepository; + + public UserServiceImpl(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + public User create(User user) { + if (userRepository.existsByUsername(user.getUsername())) { + throw new BadRequestException("Пользователь с логином " + user.getUsername() + " уже существует"); + } + if (userRepository.existsByEmail(user.getEmail())) { + throw new BadRequestException("Пользователь с почтой " + user.getEmail() + " уже существует"); + } + User createdUser = userRepository.save(user); + log.info("Saved new user with ID: {}", createdUser.getId()); + return createdUser; + } + + @Override + public User getByUsername(String username) { + return userRepository.findByUsername(username).orElseThrow(() -> new UsernameNotFoundException(username)); + } + + @Override + public UserDetailsService userDetailsService() { + return this::getByUsername; + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties deleted file mode 100644 index 62c3ecf..0000000 --- a/backend/src/main/resources/application.properties +++ /dev/null @@ -1,12 +0,0 @@ -server.port=8080 -spring.application.name=lenka-messenger - -spring.jpa.hibernate.ddl-auto=none -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect -spring.jpa.properties.hibernate.format_sql=true -spring.sql.init.mode=always - -spring.datasource.driverClassName=org.postgresql.Driver -spring.datasource.url=${SPRING_DATASOURCE_URL} -spring.datasource.username=${POSTGRES_USER} -spring.datasource.password=${POSTGRES_PASSWORD} \ No newline at end of file diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml new file mode 100644 index 0000000..aeea85a --- /dev/null +++ b/backend/src/main/resources/application.yml @@ -0,0 +1,25 @@ +server: + port: 8080 + +spring: + application: + name: lenka-messenger + datasource: + driver-class-name: org.postgresql.Driver + url: ${SPRING_DATASOURCE_URL} + username: ${POSTGRES_USER} + password: ${POSTGRES_PASSWORD} + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + sql: + init: + mode: always + +token: + signing: + key: ${SIGNING_TOKEN} \ No newline at end of file diff --git a/backend/src/test/java/ru/pominov/lenka_messenger/LenkaMessengerApplicationTests.java b/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java similarity index 84% rename from backend/src/test/java/ru/pominov/lenka_messenger/LenkaMessengerApplicationTests.java rename to backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java index 0edc35e..479129f 100644 --- a/backend/src/test/java/ru/pominov/lenka_messenger/LenkaMessengerApplicationTests.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java @@ -1,4 +1,4 @@ -package ru.pominov.lenka_messenger; +package ru.pominov.lenkamessenger; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml index 8264936..47824e7 100644 --- a/docker-compose.dev.yaml +++ b/docker-compose.dev.yaml @@ -13,6 +13,7 @@ services: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/lenkamessenger - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + - SIGNING_TOKEN=dGhpc2lzdGVzc2lnbmluZ3Rva2VuZm9ybGVua2FtZXNzZW5nZXJmb3JkZXZwcm9kdWN0aW9ubWFkZWluMjAyNA== networks: - app-network restart: unless-stopped diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index 198fdd3..1329db6 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -12,6 +12,7 @@ services: - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/lenkamessenger - POSTGRES_USER=${POSTGRES_USER} - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - SIGNING_TOKEN=${SIGNING_TOKEN} networks: - app-network restart: unless-stopped From 656bb4d4e36b38e90d7e3d9f5959a98b9038bb00 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 14:28:34 +0300 Subject: [PATCH 02/12] #22 refactor(backend): change jwt token expiration date --- .../ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java index d37826a..c0a64b1 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java @@ -35,7 +35,7 @@ public String generateToken(UserDetails userDetails) { public String generateToken(Map extraClaims, UserDetails userDetails) { return Jwts.builder().claims(extraClaims).subject(userDetails.getUsername()) .issuedAt(new Date(System.currentTimeMillis())) - .expiration(new Date(System.currentTimeMillis() + 100000 * 60 * 24)) + .expiration(new Date(System.currentTimeMillis() + 1000L * 60 * 60 * 48)) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } From 19470df3a68709165658252bf1224685db56b22b Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 14:35:25 +0300 Subject: [PATCH 03/12] #22 style(backend): minor fixes --- .../java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java | 1 + .../java/ru/pominov/lenkamessenger/exception/model/ApiError.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java index ead1ade..1d1b53a 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java @@ -24,6 +24,7 @@ public SignUpRequest(String username, String password, String email, String firs @Size(min = 8, max = 255) private String password; + @NotBlank @Email private String email; diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java b/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java index e120d53..47682cf 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/exception/model/ApiError.java @@ -18,7 +18,6 @@ public ApiError(HttpStatus status, String reason, String message, String timesta private HttpStatus status; private String reason; private String message; - private String timestamp; public HttpStatus getStatus() { From 386c9df782f4d7a32a0bc41bf3fabf806a5b1e56 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 15:38:51 +0300 Subject: [PATCH 04/12] #22 fix(backend): fix build --- backend/pom.xml | 4 ++++ backend/src/main/resources/application-test.yml | 12 ++++++++++++ .../LenkaMessengerApplicationTests.java | 2 ++ backend/src/test/resources/application.yml | 3 +++ 4 files changed, 21 insertions(+) create mode 100644 backend/src/main/resources/application-test.yml create mode 100644 backend/src/test/resources/application.yml diff --git a/backend/pom.xml b/backend/pom.xml index f1dff09..1cb53f8 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -74,6 +74,10 @@ postgresql runtime + + com.h2database + h2 + org.projectlombok lombok diff --git a/backend/src/main/resources/application-test.yml b/backend/src/main/resources/application-test.yml new file mode 100644 index 0000000..5aa6483 --- /dev/null +++ b/backend/src/main/resources/application-test.yml @@ -0,0 +1,12 @@ +spring: + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:testdb + username: root + password: root + jpa: + hibernate: + ddl-auto: create-drop +token: + signing: + key: 5bccab9f98efe3213906d638b520662aa6280978ff3d639fbd78b81f51355d1a \ No newline at end of file diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java b/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java index 479129f..adb2773 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/LenkaMessengerApplicationTests.java @@ -2,8 +2,10 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; @SpringBootTest +@ActiveProfiles("test") class LenkaMessengerApplicationTests { @Test diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml new file mode 100644 index 0000000..027b4e3 --- /dev/null +++ b/backend/src/test/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: test \ No newline at end of file From a7c2cf720a8c4bdeb86882175f69becade9d2b3e Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 16:12:21 +0300 Subject: [PATCH 05/12] #22 fix(backend): fix Sonar issue --- .../ru/pominov/lenkamessenger/exception/ExceptionsHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java b/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java index 94fa986..ae5715d 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/exception/ExceptionsHandler.java @@ -19,7 +19,7 @@ @RestControllerAdvice public class ExceptionsHandler { - private final static Logger log = LoggerFactory.getLogger(ExceptionsHandler.class); + private static final Logger log = LoggerFactory.getLogger(ExceptionsHandler.class); @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) From a54781697bfde257dc448c586d203e4a5b409c65 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 17:17:31 +0300 Subject: [PATCH 06/12] #23 feat(backend): implements authentication --- .../config/SecurityConfiguration.java | 9 ++- .../controller/auth/AuthController.java | 13 ++-- .../dto/user/SignInRequest.java | 53 +++++++++++++++ .../dto/user/SignUpRequest.java | 2 + .../filter/JwtAuthenticationFilter.java | 64 +++++++++++++++++++ .../service/auth/AuthenticationService.java | 3 + .../auth/AuthenticationServiceImpl.java | 20 +++++- .../service/jwt/JwtService.java | 15 +++++ .../service/jwt/JwtServiceImpl.java | 34 ++++++++++ .../service/user/UserService.java | 2 + .../service/user/UserServiceImpl.java | 7 ++ 11 files changed, 215 insertions(+), 7 deletions(-) create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignInRequest.java create mode 100644 backend/src/main/java/ru/pominov/lenkamessenger/filter/JwtAuthenticationFilter.java diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java b/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java index e2a017f..46b1df6 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/config/SecurityConfiguration.java @@ -14,7 +14,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; +import ru.pominov.lenkamessenger.filter.JwtAuthenticationFilter; import ru.pominov.lenkamessenger.service.user.UserService; import java.util.List; @@ -25,9 +27,11 @@ public class SecurityConfiguration { private final UserService userService; + private final JwtAuthenticationFilter jwtAuthenticationFilter; - public SecurityConfiguration(UserService userService) { + public SecurityConfiguration(UserService userService, JwtAuthenticationFilter jwtAuthenticationFilter) { this.userService = userService; + this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean @@ -45,7 +49,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers("/auth/**").permitAll() .anyRequest().authenticated()) .sessionManagement(manager -> manager.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) - .authenticationProvider(authenticationProvider()); + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java b/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java index a5331e7..e5b1cd6 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/controller/auth/AuthController.java @@ -1,11 +1,10 @@ package ru.pominov.lenkamessenger.controller.auth; import jakarta.validation.Valid; -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.http.HttpStatus; +import org.springframework.web.bind.annotation.*; import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignInRequest; import ru.pominov.lenkamessenger.dto.user.SignUpRequest; import ru.pominov.lenkamessenger.service.auth.AuthenticationService; @@ -20,7 +19,13 @@ public AuthController(AuthenticationService authenticationService) { } @PostMapping("/register") + @ResponseStatus(HttpStatus.CREATED) public JwtAuthenticationResponse signUp(@RequestBody @Valid SignUpRequest request) { return authenticationService.signUp(request); } + + @PostMapping("/login") + public JwtAuthenticationResponse singIn(@RequestBody @Valid SignInRequest request) { + return authenticationService.signIn(request); + } } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignInRequest.java b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignInRequest.java new file mode 100644 index 0000000..7312f6d --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignInRequest.java @@ -0,0 +1,53 @@ +package ru.pominov.lenkamessenger.dto.user; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +import java.util.Objects; + +public class SignInRequest { + + public SignInRequest() {} + + public SignInRequest(String username, String password) { + this.username = username; + this.password = password; + } + + @NotBlank + @Size(min = 5, max = 50) + private String username; + + @NotBlank + @Size(min = 8, max = 255) + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SignInRequest that = (SignInRequest) o; + return Objects.equals(username, that.username) && Objects.equals(password, that.password); + } + + @Override + public int hashCode() { + return Objects.hash(username, password); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java index 1d1b53a..a357e6c 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/dto/user/SignUpRequest.java @@ -8,6 +8,8 @@ public class SignUpRequest { + public SignUpRequest() {} + public SignUpRequest(String username, String password, String email, String firstName, String lastName) { this.username = username; this.password = password; diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/filter/JwtAuthenticationFilter.java b/backend/src/main/java/ru/pominov/lenkamessenger/filter/JwtAuthenticationFilter.java new file mode 100644 index 0000000..8d9a7b2 --- /dev/null +++ b/backend/src/main/java/ru/pominov/lenkamessenger/filter/JwtAuthenticationFilter.java @@ -0,0 +1,64 @@ +package ru.pominov.lenkamessenger.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import ru.pominov.lenkamessenger.service.jwt.JwtService; +import ru.pominov.lenkamessenger.service.user.UserService; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + public static final String BEARER_PREFIX = "Bearer "; + public static final String HEADER_NAME = "Authorization"; + + private final JwtService jwtService; + private final UserService userService; + + public JwtAuthenticationFilter(JwtService jwtService, UserService userService) { + this.jwtService = jwtService; + this.userService = userService; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + var authHeader = request.getHeader(HEADER_NAME); + if (StringUtils.isEmpty(authHeader) || !StringUtils.startsWith(authHeader, BEARER_PREFIX)) { + filterChain.doFilter(request, response); + return; + } + + var jwt = authHeader.substring(BEARER_PREFIX.length()); + var username = jwtService.extractUsername(jwt); + + if (StringUtils.isNoneEmpty(username) && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = userService.userDetailsService().loadUserByUsername(username); + + if (jwtService.isTokenValid(jwt, userDetails)) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( + userDetails, + null, + userDetails.getAuthorities() + ); + + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + context.setAuthentication(authToken); + SecurityContextHolder.setContext(context); + } + } + filterChain.doFilter(request, response); + } +} diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java index 38e12b4..d633791 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationService.java @@ -1,9 +1,12 @@ package ru.pominov.lenkamessenger.service.auth; import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignInRequest; import ru.pominov.lenkamessenger.dto.user.SignUpRequest; public interface AuthenticationService { JwtAuthenticationResponse signUp(SignUpRequest request); + + JwtAuthenticationResponse signIn(SignInRequest request); } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java index aa10fcd..8e9b53a 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/auth/AuthenticationServiceImpl.java @@ -1,8 +1,11 @@ package ru.pominov.lenkamessenger.service.auth; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignInRequest; import ru.pominov.lenkamessenger.dto.user.SignUpRequest; import ru.pominov.lenkamessenger.mapper.UserMapper; import ru.pominov.lenkamessenger.model.user.User; @@ -15,11 +18,13 @@ public class AuthenticationServiceImpl implements AuthenticationService { private final UserService userService; private final JwtService jwtService; private final PasswordEncoder passwordEncoder; + private final AuthenticationManager authenticationManager; - public AuthenticationServiceImpl(UserService userService, JwtService jwtService, PasswordEncoder passwordEncoder) { + public AuthenticationServiceImpl(UserService userService, JwtService jwtService, PasswordEncoder passwordEncoder, AuthenticationManager authenticationManager) { this.userService = userService; this.jwtService = jwtService; this.passwordEncoder = passwordEncoder; + this.authenticationManager = authenticationManager; } @Override @@ -32,4 +37,17 @@ public JwtAuthenticationResponse signUp(SignUpRequest request) { var jwt = jwtService.generateToken(user); return new JwtAuthenticationResponse(jwt); } + + @Override + public JwtAuthenticationResponse signIn(SignInRequest request) { + authenticationManager.authenticate(new UsernamePasswordAuthenticationToken( + request.getUsername(), + request.getPassword() + )); + + var user = userService.userDetailsService().loadUserByUsername(request.getUsername()); + var jwt = jwtService.generateToken(user); + + return new JwtAuthenticationResponse(jwt); + } } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java index 303f1f0..d2194d2 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtService.java @@ -1,9 +1,12 @@ package ru.pominov.lenkamessenger.service.jwt; +import io.jsonwebtoken.Claims; import org.springframework.security.core.userdetails.UserDetails; import java.security.Key; +import java.util.Date; import java.util.Map; +import java.util.function.Function; public interface JwtService { @@ -12,4 +15,16 @@ public interface JwtService { String generateToken(Map extraClaims, UserDetails userDetails); Key getSigningKey(); + + String extractUsername(String token); + + boolean isTokenValid(String token, UserDetails userDetails); + + boolean isTokenExpired(String token); + + Date extractExpiration(String token); + + T extractClaim(String token, Function claimsResolvers); + + Claims extractAllClaims(String token); } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java index c0a64b1..e6f249e 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/jwt/JwtServiceImpl.java @@ -1,5 +1,6 @@ package ru.pominov.lenkamessenger.service.jwt; +import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.io.Decoders; @@ -13,6 +14,7 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.function.Function; @Service public class JwtServiceImpl implements JwtService { @@ -45,4 +47,36 @@ public Key getSigningKey() { byte[] keyBytes = Decoders.BASE64.decode(jwtSigningKey); return Keys.hmacShaKeyFor(keyBytes); } + + @Override + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + @Override + public boolean isTokenValid(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername())) && !isTokenExpired(token); + } + + @Override + public boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + @Override + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + @Override + public T extractClaim(String token, Function claimsResolvers) { + final Claims claims = extractAllClaims(token); + return claimsResolvers.apply(claims); + } + + @Override + public Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java index a7003c1..d7ba6da 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserService.java @@ -10,4 +10,6 @@ public interface UserService { User getByUsername(String username); UserDetailsService userDetailsService(); + + User getCurrentUser(); } diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java index cc7d973..7a94b20 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java @@ -2,6 +2,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; @@ -42,4 +43,10 @@ public User getByUsername(String username) { public UserDetailsService userDetailsService() { return this::getByUsername; } + + @Override + public User getCurrentUser() { + var username = SecurityContextHolder.getContext().getAuthentication().getName(); + return getByUsername(username); + } } From c6c5467b5924d5c9168e13199a5758d1bba2b175 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Thu, 19 Dec 2024 20:49:29 +0300 Subject: [PATCH 07/12] #52 test(backend): add integration tests for AuthController --- .../service/user/UserServiceImpl.java | 4 +- .../auth/AuthControllerIntegrationTest.java | 129 ++++++++++++++++++ 2 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java diff --git a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java index 7a94b20..97f94e8 100644 --- a/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java +++ b/backend/src/main/java/ru/pominov/lenkamessenger/service/user/UserServiceImpl.java @@ -24,10 +24,10 @@ public UserServiceImpl(UserRepository userRepository) { @Override public User create(User user) { if (userRepository.existsByUsername(user.getUsername())) { - throw new BadRequestException("Пользователь с логином " + user.getUsername() + " уже существует"); + throw new BadRequestException("User with username - " + user.getUsername() + " already exists"); } if (userRepository.existsByEmail(user.getEmail())) { - throw new BadRequestException("Пользователь с почтой " + user.getEmail() + " уже существует"); + throw new BadRequestException("User with email - " + user.getEmail() + " already exists"); } User createdUser = userRepository.save(user); log.info("Saved new user with ID: {}", createdUser.getId()); diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java new file mode 100644 index 0000000..3cb338a --- /dev/null +++ b/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java @@ -0,0 +1,129 @@ +package ru.pominov.lenkamessenger.controller.auth; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import ru.pominov.lenkamessenger.dto.user.SignInRequest; +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + + +@SpringBootTest +@AutoConfigureMockMvc +public class AuthControllerIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper mapper; + + SignUpRequest signUpRequest = new SignUpRequest("usertest123", "password123", "test@test.ru", "John", "Doe"); + SignInRequest signInRequest = new SignInRequest("loginuser", "password123"); + + @Test + public void signUp_shouldRegisterSuccessfully() throws Exception { + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isCreated()); + } + + @Test + public void signUp_shouldReturnBadRequestWhenPasswordIsShorterThan8() throws Exception { + signUpRequest.setPassword("123"); + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + public void signUp_shouldReturnBadRequestWhenUsernameIsNotUnique() throws Exception { + signUpRequest.setUsername("uniqueusername"); + signUpRequest.setEmail("unique@super.ru"); + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isCreated()); + + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.message").value("User with username - uniqueusername already exists")) + .andExpect(status().isBadRequest()); + } + + @Test + public void signUp_shouldReturnBadRequestWhenEmailIsNotUnique() throws Exception { + signUpRequest.setUsername("iambot"); + signUpRequest.setEmail("botornot@quiz.ru"); + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isCreated()); + + signUpRequest.setUsername("iamnotbot"); + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.message").value("User with email - botornot@quiz.ru already exists")) + .andExpect(status().isBadRequest()); + } + + @Test + public void signIn_shouldLoginSuccessfully() throws Exception { + signUpRequest.setUsername("loginuser"); + signUpRequest.setEmail("login@user.ru"); + + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isCreated()); + + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signInRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isOk()); + } + + @Test + public void signIn_shouldReturnForbiddenWhenCredentialsNotMatch() throws Exception { + signUpRequest.setUsername("baduser"); + signUpRequest.setEmail("badlogin@user.ru"); + + mockMvc.perform(post("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signUpRequest))) + .andExpect(jsonPath("$.token").exists()) + .andExpect(status().isCreated()); + + signInRequest.setUsername("gooduser"); + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signInRequest))) + .andExpect(status().isForbidden()); + } + + @Test + public void signIn_shouldReturnBadRequestOnLoginWhenPasswordIsShorterThan8() throws Exception { + signInRequest.setPassword("123"); + + mockMvc.perform(post("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(signInRequest))) + .andExpect(status().isBadRequest()); + } +} \ No newline at end of file From d0aff4b8a2d2e43ee4acd8a24a17f01768705d58 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Fri, 20 Dec 2024 09:18:43 +0300 Subject: [PATCH 08/12] #52 test(backend): add unit tests for UserService --- .../service/UserServiceTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java new file mode 100644 index 0000000..bce59c6 --- /dev/null +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java @@ -0,0 +1,103 @@ +package ru.pominov.lenkamessenger.service; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import ru.pominov.lenkamessenger.exception.BadRequestException; +import ru.pominov.lenkamessenger.model.user.Role; +import ru.pominov.lenkamessenger.model.user.User; +import ru.pominov.lenkamessenger.repository.UserRepository; +import ru.pominov.lenkamessenger.service.user.UserService; +import ru.pominov.lenkamessenger.service.user.UserServiceImpl; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +public class UserServiceTest { + + @InjectMocks + private UserServiceImpl userServiceMock; + + @Autowired + private UserService userService; + + @Mock + private UserRepository userRepository; + + User user = new User(null, "testuser123", "password123", "test@test.ru", "John", "Doe", Role.ROLE_USER); + + @Test + public void create_ShouldCreateUser() { + User createdUser = userService.create(user); + assertEquals(1, createdUser.getId()); + } + + @Test + public void create_shouldThrowExceptionWhenUsernameNotUnique() { + when(userRepository.existsByUsername(user.getUsername())).thenReturn(true); + + assertThrows(BadRequestException.class, () -> userServiceMock.create(user)); + + verify(userRepository, never()).save(any(User.class)); + } + + @Test + public void create_shouldThrowExceptionWhenEmailNotUnique() { + when(userRepository.existsByEmail(user.getEmail())).thenReturn(true); + + assertThrows(BadRequestException.class, () -> userServiceMock.create(user)); + + verify(userRepository, never()).save(any(User.class)); + } + + @Test + public void getByUsername_shouldReturnUser() { + user.setUsername("newtest"); + user.setEmail("email@email.ru"); + + User createdUser = userService.create(user); + User returnedUser = userService.getByUsername(user.getUsername()); + + assertEquals(createdUser.getId(), returnedUser.getId()); + assertEquals("newtest", returnedUser.getUsername()); + assertEquals("email@email.ru", returnedUser.getEmail()); + } + + @Test + public void getByUsername_shouldThrowUsernameNotFoundException() { + assertThrows(UsernameNotFoundException.class, () -> userService.getByUsername("unknown")); + } + + @Test + public void getCurrentUser_shouldReturnAuthenticatedUser() { + var authentication = mock(Authentication.class); + when(authentication.getName()).thenReturn("authuser"); + + var securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(authentication); + + SecurityContextHolder.setContext(securityContext); + + user.setUsername("authuser"); + user.setEmail("auth@user.ru"); + + when(userRepository.findByUsername(user.getUsername())).thenReturn(Optional.of(user)); + + User currentUser = userServiceMock.getCurrentUser(); + + assertNotNull(currentUser); + assertEquals("authuser", currentUser.getUsername()); + assertEquals("auth@user.ru", currentUser.getEmail()); + + verify(userRepository, atLeastOnce()).findByUsername(user.getUsername()); + } +} From 91c9234cd40465b3a4d18f79a497780f5d41fead Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Fri, 20 Dec 2024 09:37:20 +0300 Subject: [PATCH 09/12] #52 test(backend): add unit tests for JwtService --- .../service/JwtServiceTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java new file mode 100644 index 0000000..3937ab7 --- /dev/null +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java @@ -0,0 +1,30 @@ +package ru.pominov.lenkamessenger.service; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import ru.pominov.lenkamessenger.model.user.Role; +import ru.pominov.lenkamessenger.model.user.User; +import ru.pominov.lenkamessenger.service.jwt.JwtService; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestPropertySource(properties = {"token.signing.key=SWD1mQ+vpshov3M63EIe6d8Thyf0AJ1toqSR32wZpPo="}) +public class JwtServiceTest { + + @Autowired + private JwtService jwtService; + + User user = new User(1L, "testuser123", "password123", "test@test.ru", "John", "Doe", Role.ROLE_USER); + + @Test + public void shouldGenerateAndCheckToken() { + String token = jwtService.generateToken(user); + + assertNotNull(token); + assertEquals("testuser123", jwtService.extractUsername(token)); + assertTrue(jwtService.isTokenValid(token, user)); + } +} From 3cfa4986e51ac3704f2b05f3b2a8d927ff6e10d2 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Fri, 20 Dec 2024 10:06:16 +0300 Subject: [PATCH 10/12] #52 test(backend): add unit tests for AuthenticationService --- .../service/AuthenticationServiceTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java new file mode 100644 index 0000000..0d2d2d2 --- /dev/null +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java @@ -0,0 +1,72 @@ +package ru.pominov.lenkamessenger.service; + +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.crypto.password.PasswordEncoder; +import ru.pominov.lenkamessenger.dto.user.JwtAuthenticationResponse; +import ru.pominov.lenkamessenger.dto.user.SignInRequest; +import ru.pominov.lenkamessenger.dto.user.SignUpRequest; +import ru.pominov.lenkamessenger.model.user.User; +import ru.pominov.lenkamessenger.service.auth.AuthenticationServiceImpl; +import ru.pominov.lenkamessenger.service.jwt.JwtService; +import ru.pominov.lenkamessenger.service.user.UserService; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@SpringBootTest +public class AuthenticationServiceTest { + + @InjectMocks + private AuthenticationServiceImpl authenticationService; + + @Mock + private UserService userService; + + @Mock + private JwtService jwtService; + + @Mock + private PasswordEncoder passwordEncoder; + + @Mock + private AuthenticationManager authenticationManager; + + @Test + public void signUp_ShouldReturnJwtToken() { + SignUpRequest request = new SignUpRequest("testuser", "password123", "test@test.ru", "John", "Doe"); + + when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); + when(jwtService.generateToken(any(User.class))).thenReturn("mockJwtToken"); + + JwtAuthenticationResponse response = authenticationService.signUp(request); + + verify(userService, times(1)).create(any(User.class)); + verify(passwordEncoder, times(1)).encode("password123"); + verify(jwtService, times(1)).generateToken(any(User.class)); + + assertNotNull(response); + assertEquals("mockJwtToken", response.getToken()); + } + + @Test + public void signIn_ShouldReturnJwtToken() { + SignInRequest request = new SignInRequest("testuser", "password123"); + User user = new User(); + user.setUsername("testuser"); + user.setPassword("password123"); + + when(userService.userDetailsService()).thenReturn(username -> user); + when(jwtService.generateToken(user)).thenReturn("mockJwtToken"); + + JwtAuthenticationResponse response = authenticationService.signIn(request); + + verify(authenticationManager, times(1)).authenticate(any(UsernamePasswordAuthenticationToken.class)); + assertNotNull(response); + assertEquals("mockJwtToken", response.getToken()); + } +} From 2aef36a18d49b8e3888cf81658ed5716905215e8 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Fri, 20 Dec 2024 10:21:55 +0300 Subject: [PATCH 11/12] #52 build(backend): remove Lombok, add Jacoco --- backend/pom.xml | 40 +++++++++---------- .../service/JwtServiceTest.java | 3 ++ 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/backend/pom.xml b/backend/pom.xml index 1cb53f8..147ac97 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -78,11 +78,6 @@ com.h2database h2 - - org.projectlombok - lombok - true - org.springframework.boot spring-boot-starter-test @@ -100,26 +95,29 @@ org.apache.maven.plugins maven-compiler-plugin - - - - org.projectlombok - lombok - - - org.springframework.boot spring-boot-maven-plugin - - - - org.projectlombok - lombok - - - + + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + + prepare-agent + + + + report + prepare-package + + report + + + diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java index 3937ab7..152ea6e 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java @@ -26,5 +26,8 @@ public void shouldGenerateAndCheckToken() { assertNotNull(token); assertEquals("testuser123", jwtService.extractUsername(token)); assertTrue(jwtService.isTokenValid(token, user)); + + user.setUsername("anotheruser"); + assertFalse(jwtService.isTokenValid(token, user)); } } From 5e5a19078a42a74af52a84d5cbcb073233bf6474 Mon Sep 17 00:00:00 2001 From: NikitaBuffy Date: Fri, 20 Dec 2024 10:39:50 +0300 Subject: [PATCH 12/12] #52 fix(backend): fix Sonar issues, remove 'public' modifiers --- .../auth/AuthControllerIntegrationTest.java | 16 ++++++++-------- .../service/AuthenticationServiceTest.java | 6 +++--- .../lenkamessenger/service/JwtServiceTest.java | 4 ++-- .../lenkamessenger/service/UserServiceTest.java | 14 +++++++------- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java index 3cb338a..2b366ff 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/controller/auth/AuthControllerIntegrationTest.java @@ -17,7 +17,7 @@ @SpringBootTest @AutoConfigureMockMvc -public class AuthControllerIntegrationTest { +class AuthControllerIntegrationTest { @Autowired private MockMvc mockMvc; @@ -29,7 +29,7 @@ public class AuthControllerIntegrationTest { SignInRequest signInRequest = new SignInRequest("loginuser", "password123"); @Test - public void signUp_shouldRegisterSuccessfully() throws Exception { + void signUp_shouldRegisterSuccessfully() throws Exception { mockMvc.perform(post("/auth/register") .contentType(MediaType.APPLICATION_JSON) .content(mapper.writeValueAsString(signUpRequest))) @@ -38,7 +38,7 @@ public void signUp_shouldRegisterSuccessfully() throws Exception { } @Test - public void signUp_shouldReturnBadRequestWhenPasswordIsShorterThan8() throws Exception { + void signUp_shouldReturnBadRequestWhenPasswordIsShorterThan8() throws Exception { signUpRequest.setPassword("123"); mockMvc.perform(post("/auth/register") .contentType(MediaType.APPLICATION_JSON) @@ -47,7 +47,7 @@ public void signUp_shouldReturnBadRequestWhenPasswordIsShorterThan8() throws Exc } @Test - public void signUp_shouldReturnBadRequestWhenUsernameIsNotUnique() throws Exception { + void signUp_shouldReturnBadRequestWhenUsernameIsNotUnique() throws Exception { signUpRequest.setUsername("uniqueusername"); signUpRequest.setEmail("unique@super.ru"); mockMvc.perform(post("/auth/register") @@ -64,7 +64,7 @@ public void signUp_shouldReturnBadRequestWhenUsernameIsNotUnique() throws Except } @Test - public void signUp_shouldReturnBadRequestWhenEmailIsNotUnique() throws Exception { + void signUp_shouldReturnBadRequestWhenEmailIsNotUnique() throws Exception { signUpRequest.setUsername("iambot"); signUpRequest.setEmail("botornot@quiz.ru"); mockMvc.perform(post("/auth/register") @@ -82,7 +82,7 @@ public void signUp_shouldReturnBadRequestWhenEmailIsNotUnique() throws Exception } @Test - public void signIn_shouldLoginSuccessfully() throws Exception { + void signIn_shouldLoginSuccessfully() throws Exception { signUpRequest.setUsername("loginuser"); signUpRequest.setEmail("login@user.ru"); @@ -100,7 +100,7 @@ public void signIn_shouldLoginSuccessfully() throws Exception { } @Test - public void signIn_shouldReturnForbiddenWhenCredentialsNotMatch() throws Exception { + void signIn_shouldReturnForbiddenWhenCredentialsNotMatch() throws Exception { signUpRequest.setUsername("baduser"); signUpRequest.setEmail("badlogin@user.ru"); @@ -118,7 +118,7 @@ public void signIn_shouldReturnForbiddenWhenCredentialsNotMatch() throws Excepti } @Test - public void signIn_shouldReturnBadRequestOnLoginWhenPasswordIsShorterThan8() throws Exception { + void signIn_shouldReturnBadRequestOnLoginWhenPasswordIsShorterThan8() throws Exception { signInRequest.setPassword("123"); mockMvc.perform(post("/auth/login") diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java index 0d2d2d2..8bc6721 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/AuthenticationServiceTest.java @@ -19,7 +19,7 @@ import static org.mockito.Mockito.*; @SpringBootTest -public class AuthenticationServiceTest { +class AuthenticationServiceTest { @InjectMocks private AuthenticationServiceImpl authenticationService; @@ -37,7 +37,7 @@ public class AuthenticationServiceTest { private AuthenticationManager authenticationManager; @Test - public void signUp_ShouldReturnJwtToken() { + void signUp_ShouldReturnJwtToken() { SignUpRequest request = new SignUpRequest("testuser", "password123", "test@test.ru", "John", "Doe"); when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); @@ -54,7 +54,7 @@ public void signUp_ShouldReturnJwtToken() { } @Test - public void signIn_ShouldReturnJwtToken() { + void signIn_ShouldReturnJwtToken() { SignInRequest request = new SignInRequest("testuser", "password123"); User user = new User(); user.setUsername("testuser"); diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java index 152ea6e..e73c2b9 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/JwtServiceTest.java @@ -12,7 +12,7 @@ @SpringBootTest @TestPropertySource(properties = {"token.signing.key=SWD1mQ+vpshov3M63EIe6d8Thyf0AJ1toqSR32wZpPo="}) -public class JwtServiceTest { +class JwtServiceTest { @Autowired private JwtService jwtService; @@ -20,7 +20,7 @@ public class JwtServiceTest { User user = new User(1L, "testuser123", "password123", "test@test.ru", "John", "Doe", Role.ROLE_USER); @Test - public void shouldGenerateAndCheckToken() { + void shouldGenerateAndCheckToken() { String token = jwtService.generateToken(user); assertNotNull(token); diff --git a/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java b/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java index bce59c6..5f31c26 100644 --- a/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java +++ b/backend/src/test/java/ru/pominov/lenkamessenger/service/UserServiceTest.java @@ -22,7 +22,7 @@ import static org.mockito.Mockito.*; @SpringBootTest -public class UserServiceTest { +class UserServiceTest { @InjectMocks private UserServiceImpl userServiceMock; @@ -36,13 +36,13 @@ public class UserServiceTest { User user = new User(null, "testuser123", "password123", "test@test.ru", "John", "Doe", Role.ROLE_USER); @Test - public void create_ShouldCreateUser() { + void create_ShouldCreateUser() { User createdUser = userService.create(user); assertEquals(1, createdUser.getId()); } @Test - public void create_shouldThrowExceptionWhenUsernameNotUnique() { + void create_shouldThrowExceptionWhenUsernameNotUnique() { when(userRepository.existsByUsername(user.getUsername())).thenReturn(true); assertThrows(BadRequestException.class, () -> userServiceMock.create(user)); @@ -51,7 +51,7 @@ public void create_shouldThrowExceptionWhenUsernameNotUnique() { } @Test - public void create_shouldThrowExceptionWhenEmailNotUnique() { + void create_shouldThrowExceptionWhenEmailNotUnique() { when(userRepository.existsByEmail(user.getEmail())).thenReturn(true); assertThrows(BadRequestException.class, () -> userServiceMock.create(user)); @@ -60,7 +60,7 @@ public void create_shouldThrowExceptionWhenEmailNotUnique() { } @Test - public void getByUsername_shouldReturnUser() { + void getByUsername_shouldReturnUser() { user.setUsername("newtest"); user.setEmail("email@email.ru"); @@ -73,12 +73,12 @@ public void getByUsername_shouldReturnUser() { } @Test - public void getByUsername_shouldThrowUsernameNotFoundException() { + void getByUsername_shouldThrowUsernameNotFoundException() { assertThrows(UsernameNotFoundException.class, () -> userService.getByUsername("unknown")); } @Test - public void getCurrentUser_shouldReturnAuthenticatedUser() { + void getCurrentUser_shouldReturnAuthenticatedUser() { var authentication = mock(Authentication.class); when(authentication.getName()).thenReturn("authuser");