diff --git a/src/main/java/com/soupulsar/modulith/auth/api/controllers/AuthController.java b/src/main/java/com/soupulsar/modulith/auth/api/controllers/AuthController.java new file mode 100644 index 0000000..a580bb5 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/api/controllers/AuthController.java @@ -0,0 +1,36 @@ +package com.soupulsar.modulith.auth.api.controllers; + +import com.soupulsar.modulith.auth.application.dto.AuthUserRequest; +import com.soupulsar.modulith.auth.application.dto.CreateUserRequest; +import com.soupulsar.modulith.auth.application.dto.CreateUserResponse; +import com.soupulsar.modulith.auth.application.usecase.AuthenticateUserUseCase; +import com.soupulsar.modulith.auth.application.usecase.RegisterUserUseCase; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthenticateUserUseCase authenticateUserUseCase; + private final RegisterUserUseCase registerUserUseCase; + + + @PostMapping(value = {"/login", "/signin"}) + public ResponseEntity login(@RequestBody AuthUserRequest request) { + String token = authenticateUserUseCase.execute(request); + return ResponseEntity.ok(token); + } + + @PostMapping(value = {"/register", "/signup"}) + public ResponseEntity register(@RequestBody CreateUserRequest request) { + var response = registerUserUseCase.execute(request); + return ResponseEntity.ok(response); + + } +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java b/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java new file mode 100644 index 0000000..743a1ad --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/config/AuthUseCaseConfig.java @@ -0,0 +1,24 @@ +package com.soupulsar.modulith.auth.application.config; + +import com.soupulsar.modulith.auth.application.security.PasswordHasher; +import com.soupulsar.modulith.auth.application.usecase.AuthenticateUserUseCase; +import com.soupulsar.modulith.auth.application.usecase.RegisterUserUseCase; +import com.soupulsar.modulith.auth.domain.repository.UserRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AuthUseCaseConfig { + + @Bean + public AuthenticateUserUseCase authenticateUserUseCase(UserRepository userRepository, PasswordHasher passwordHasher) { + return new AuthenticateUserUseCase(userRepository, passwordHasher); + } + + @Bean + public RegisterUserUseCase registerUserUseCase(UserRepository userRepository, PasswordHasher passwordHasher) { + return new RegisterUserUseCase(userRepository, passwordHasher); + } + + +} diff --git a/src/main/java/com/soupulsar/modulith/auth/application/dto/AuthUserRequest.java b/src/main/java/com/soupulsar/modulith/auth/application/dto/AuthUserRequest.java new file mode 100644 index 0000000..cd6ae0e --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/dto/AuthUserRequest.java @@ -0,0 +1,12 @@ +package com.soupulsar.modulith.auth.application.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record AuthUserRequest( + + @Email String email, + @NotBlank String password + +) { +} diff --git a/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserRequest.java b/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserRequest.java new file mode 100644 index 0000000..3f55bc0 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserRequest.java @@ -0,0 +1,24 @@ +package com.soupulsar.modulith.auth.application.dto; + +import com.soupulsar.modulith.auth.domain.model.enums.UserRole; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import org.hibernate.validator.constraints.br.CPF; + +public record CreateUserRequest( + + @NotBlank + String name, + @Email + String email, + @CPF + String cpf, + @NotBlank + String telephone, + @NotBlank + String password, + @NotBlank + UserRole role + +) { +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserResponse.java b/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserResponse.java new file mode 100644 index 0000000..5c8e151 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/dto/CreateUserResponse.java @@ -0,0 +1,8 @@ +package com.soupulsar.modulith.auth.application.dto; + +import java.util.UUID; + +public record CreateUserResponse( + UUID userId +) { +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/application/security/PasswordHasher.java b/src/main/java/com/soupulsar/modulith/auth/application/security/PasswordHasher.java new file mode 100644 index 0000000..6aa709b --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/security/PasswordHasher.java @@ -0,0 +1,8 @@ +package com.soupulsar.modulith.auth.application.security; + +public interface PasswordHasher { + + String hash(String password); + boolean matches(String rawPassword, String hashedPassword); + +} diff --git a/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java b/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java new file mode 100644 index 0000000..5fb4101 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCase.java @@ -0,0 +1,34 @@ +package com.soupulsar.modulith.auth.application.usecase; + +import com.soupulsar.modulith.auth.application.dto.AuthUserRequest; +import com.soupulsar.modulith.auth.application.security.PasswordHasher; +import com.soupulsar.modulith.auth.domain.model.User; +import com.soupulsar.modulith.auth.domain.model.enums.UserStatus; +import com.soupulsar.modulith.auth.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; + +@RequiredArgsConstructor +public class AuthenticateUserUseCase { + + private final UserRepository userRepository; + private final PasswordHasher passwordHasher; + + public String execute(AuthUserRequest request) { + + User user = userRepository.findByEmail(request.email()) + .orElseThrow(() -> new IllegalArgumentException("Invalid email or password")); + + if (user.getStatus() != UserStatus.ACTIVE) { + throw new IllegalArgumentException("User is not active"); + } + + if (!passwordHasher.matches(request.password(), user.getPasswordHash())) { + throw new IllegalArgumentException("Invalid email or password"); + } + + return "JWT-TOKEN"; // Placeholder for actual JWT generation logic + + } + +} diff --git a/src/main/java/com/soupulsar/modulith/auth/application/usecase/RegisterUserUseCase.java b/src/main/java/com/soupulsar/modulith/auth/application/usecase/RegisterUserUseCase.java new file mode 100644 index 0000000..73f3554 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/application/usecase/RegisterUserUseCase.java @@ -0,0 +1,35 @@ +package com.soupulsar.modulith.auth.application.usecase; + +import com.soupulsar.modulith.auth.application.dto.CreateUserRequest; +import com.soupulsar.modulith.auth.application.dto.CreateUserResponse; +import com.soupulsar.modulith.auth.application.security.PasswordHasher; +import com.soupulsar.modulith.auth.domain.model.User; +import com.soupulsar.modulith.auth.domain.repository.UserRepository; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class RegisterUserUseCase { + + private final UserRepository userRepository; + private final PasswordHasher passwordEncoder; + + public CreateUserResponse execute (CreateUserRequest userRequest) { + if (userRepository.existsByEmail(userRequest.email())) { + throw new IllegalArgumentException("Email already in use"); + } + + User user = User.create( + userRequest.name(), + userRequest.cpf(), + userRequest.telephone(), + userRequest.email(), + passwordEncoder.hash(userRequest.password()), + userRequest.role() + ); + + userRepository.save(user); + + return new CreateUserResponse(user.getUserId()); + + } +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/domain/model/User.java b/src/main/java/com/soupulsar/modulith/auth/domain/model/User.java new file mode 100644 index 0000000..cf69a92 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/domain/model/User.java @@ -0,0 +1,56 @@ +package com.soupulsar.modulith.auth.domain.model; + +import com.soupulsar.modulith.auth.domain.model.enums.UserRole; +import com.soupulsar.modulith.auth.domain.model.enums.UserStatus; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.UUID; + +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Getter +public class User { + + private final UUID userId; + private final String name; + private final String cpf; + private String telephone; + private String email; + private String passwordHash; + private final UserRole role; + private UserStatus status; + + + public static User create(String name, String cpf, String telephone, String email, String passwordHash, UserRole role) { + if (name == null || name.isBlank()) throw new IllegalArgumentException("Name cannot be null or blank"); + if (cpf == null || cpf.isBlank()) throw new IllegalArgumentException("CPF cannot be null or blank"); + if (telephone == null || telephone.isBlank()) throw new IllegalArgumentException("Telephone cannot be null or blank"); + if (email == null || email.isBlank()) throw new IllegalArgumentException("Email cannot be null or blank"); + if (passwordHash == null || passwordHash.isBlank()) throw new IllegalArgumentException("Password cannot be null or blank"); + return new User(UUID.randomUUID(), name, cpf, telephone, email, passwordHash, role, UserStatus.ACTIVE); + } + + public static User restore(UUID userId, String name, String cpf, String telephone, String email, String passwordHash, UserRole role, UserStatus status) { + return new User(userId, name, cpf, telephone, email, passwordHash, role, status); + } + + + public void activate() { + if (this.status == UserStatus.ACTIVE) { + throw new IllegalStateException("User is already active."); + } + this.status = UserStatus.ACTIVE; + } + + public void deactivate() { + if (this.status == UserStatus.INACTIVE) { + throw new IllegalStateException("User is already inactive."); + } + this.status = UserStatus.INACTIVE; + } + + + + +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserRole.java b/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserRole.java new file mode 100644 index 0000000..a2d211e --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserRole.java @@ -0,0 +1,8 @@ +package com.soupulsar.modulith.auth.domain.model.enums; + +public enum UserRole { + + CLIENT, + SPECIALIST, + ADMIN +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserStatus.java b/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserStatus.java new file mode 100644 index 0000000..a0ce032 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/domain/model/enums/UserStatus.java @@ -0,0 +1,6 @@ +package com.soupulsar.modulith.auth.domain.model.enums; + +public enum UserStatus { + ACTIVE, + INACTIVE, +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/domain/repository/UserRepository.java b/src/main/java/com/soupulsar/modulith/auth/domain/repository/UserRepository.java new file mode 100644 index 0000000..202cd9b --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/domain/repository/UserRepository.java @@ -0,0 +1,14 @@ +package com.soupulsar.modulith.auth.domain.repository; + +import com.soupulsar.modulith.auth.domain.model.User; + +import java.util.Optional; +import java.util.UUID; + +public interface UserRepository { + + User save(User user); + Optional findByEmail(String email); + Optional findById(UUID userId); + boolean existsByEmail(String email); +} diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java new file mode 100644 index 0000000..4531628 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/entity/UserEntity.java @@ -0,0 +1,44 @@ +package com.soupulsar.modulith.auth.infrastructure.persistence.entity; + +import com.soupulsar.modulith.auth.domain.model.enums.UserRole; +import com.soupulsar.modulith.auth.domain.model.enums.UserStatus; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.UUID; + +@Entity +@Table(name = "users") +@Getter +@Setter +public class UserEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, unique = true) + private UUID userId; + + @Column(nullable = false) + private String name; + + @Column(nullable = false) + private String cpf; + + @Column(nullable = false) + private String email; + + @Column(nullable = false) + private String password; + + @Column(nullable = false) + private String telephone; + + @Column(nullable = false) + private UserRole role; + + @Column(nullable = false) + private UserStatus status; +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java new file mode 100644 index 0000000..277311c --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/mapper/UserMapper.java @@ -0,0 +1,31 @@ +package com.soupulsar.modulith.auth.infrastructure.persistence.mapper; + +import com.soupulsar.modulith.auth.domain.model.User; +import com.soupulsar.modulith.auth.infrastructure.persistence.entity.UserEntity; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class UserMapper { + + public static UserEntity toEntity(User user){ + UserEntity entity = new UserEntity(); + entity.setUserId(user.getUserId()); + entity.setEmail(user.getEmail()); + entity.setPassword(user.getPasswordHash()); + entity.setRole(user.getRole()); + return entity; + } + + public static User toModel(UserEntity entity){ + return User.restore(entity.getUserId(), + entity.getName(), + entity.getEmail(), + entity.getPassword(), + entity.getTelephone(), + entity.getCpf(), + entity.getRole(), + entity.getStatus()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserJpaRepository.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserJpaRepository.java new file mode 100644 index 0000000..8fda46b --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserJpaRepository.java @@ -0,0 +1,14 @@ +package com.soupulsar.modulith.auth.infrastructure.persistence.repository; + +import com.soupulsar.modulith.auth.infrastructure.persistence.entity.UserEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface UserJpaRepository extends JpaRepository { + + Optional findByEmail(String email); + boolean existsByEmail(String email); + +} diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserRepositoryImpl.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserRepositoryImpl.java new file mode 100644 index 0000000..b6a6850 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/persistence/repository/UserRepositoryImpl.java @@ -0,0 +1,41 @@ +package com.soupulsar.modulith.auth.infrastructure.persistence.repository; + +import com.soupulsar.modulith.auth.domain.model.User; +import com.soupulsar.modulith.auth.domain.repository.UserRepository; +import com.soupulsar.modulith.auth.infrastructure.persistence.entity.UserEntity; +import com.soupulsar.modulith.auth.infrastructure.persistence.mapper.UserMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@RequiredArgsConstructor +@Repository +public class UserRepositoryImpl implements UserRepository { + + + private final UserJpaRepository jpaRepository; + + @Override + public User save(User user) { + UserEntity userEntity = UserMapper.toEntity(user); + UserEntity saved = jpaRepository.save(userEntity); + return UserMapper.toModel(saved); + } + + @Override + public Optional findByEmail(String email) { + return jpaRepository.findByEmail(email).map(UserMapper::toModel); + } + + @Override + public Optional findById(UUID userId) { + return jpaRepository.findById(userId).map(UserMapper::toModel); + } + + @Override + public boolean existsByEmail(String email) { + return jpaRepository.existsByEmail(email); + } +} diff --git a/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java new file mode 100644 index 0000000..4e35da0 --- /dev/null +++ b/src/main/java/com/soupulsar/modulith/auth/infrastructure/security/BCryptPasswordHasher.java @@ -0,0 +1,21 @@ +package com.soupulsar.modulith.auth.infrastructure.security; + +import com.soupulsar.modulith.auth.application.security.PasswordHasher; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +@Component +public class BCryptPasswordHasher implements PasswordHasher { + + private final BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + + @Override + public String hash(String password) { + return passwordEncoder.encode(password); + } + + @Override + public boolean matches(String rawPassword, String hashedPassword) { + return passwordEncoder.matches(hashedPassword, rawPassword); + } +} \ No newline at end of file diff --git a/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java b/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java new file mode 100644 index 0000000..830b736 --- /dev/null +++ b/src/test/java/com/soupulsar/modulith/auth/application/usecase/AuthenticateUserUseCaseTest.java @@ -0,0 +1,85 @@ +package com.soupulsar.modulith.auth.application.usecase; + +import com.soupulsar.modulith.auth.application.dto.AuthUserRequest; +import com.soupulsar.modulith.auth.application.security.PasswordHasher; +import com.soupulsar.modulith.auth.domain.model.User; +import com.soupulsar.modulith.auth.domain.model.enums.UserRole; +import com.soupulsar.modulith.auth.domain.model.enums.UserStatus; +import com.soupulsar.modulith.auth.domain.repository.UserRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +class AuthenticateUserUseCaseTest { + + private UserRepository userRepository; + private PasswordHasher passwordHasher; + private AuthenticateUserUseCase useCase; + + @BeforeEach + void setUp() { + userRepository = mock(UserRepository.class); + passwordHasher = mock(PasswordHasher.class); + useCase = new AuthenticateUserUseCase(userRepository, passwordHasher); + } + + @Test + void shouldAuthenticateActiveUserWithCorrectPassword() { + AuthUserRequest request = new AuthUserRequest("user@email.com", "password"); + User user = User.restore( + UUID.randomUUID(), "Test User", "12345678900", "999999999", "user@email.com", + "hashedPassword", UserRole.CLIENT, UserStatus.ACTIVE + ); + + when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(user)); + when(passwordHasher.matches(request.password(), user.getPasswordHash())).thenReturn(true); + + String token = useCase.execute(request); + + assertEquals("JWT-TOKEN", token); + } + + @Test + void shouldThrowWhenUserIsInactive() { + AuthUserRequest request = new AuthUserRequest("user@email.com", "password"); + User user = User.restore( + UUID.randomUUID(), "Test User", "12345678900", "999999999", "user@email.com", + "hashedPassword", UserRole.CLIENT, UserStatus.INACTIVE + ); + + when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(user)); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request)); + assertEquals("User is not active", ex.getMessage()); + } + + @Test + void shouldThrowWhenPasswordIsInvalid() { + AuthUserRequest request = new AuthUserRequest("user@email.com", "wrong"); + User user = User.restore( + UUID.randomUUID(), "Test User", "12345678900", "999999999", "user@email.com", + "hashedPassword", UserRole.CLIENT, UserStatus.ACTIVE + ); + + when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(user)); + when(passwordHasher.matches(request.password(), user.getPasswordHash())).thenReturn(false); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request)); + assertEquals("Invalid email or password", ex.getMessage()); + } + + @Test + void shouldThrowWhenUserNotFound() { + AuthUserRequest request = new AuthUserRequest("notfound@email.com", "password"); + + when(userRepository.findByEmail(request.email())).thenReturn(Optional.empty()); + + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, () -> useCase.execute(request)); + assertEquals("Invalid email or password", ex.getMessage()); + } +}