diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index da8f02a..e0b2d00 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -14,6 +14,7 @@ services: volumes: - maven-cache:/root/.m2 environment: + - ACTIVE_PROFILE - APP_VERSION - SERVER_URL - JWT_SECRET=${JWT_SECRET} diff --git a/docker-compose.yaml b/docker-compose.yaml index 16708fd..df65ac8 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,6 +9,7 @@ services: volumes: - maven-cache:/root/.m2 environment: + - ACTIVE_PROFILE - APP_VERSION - SERVER_URL - JWT_SECRET=${JWT_SECRET} diff --git a/pom.xml b/pom.xml index 0e282df..81741c5 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,12 @@ + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + org.springdoc springdoc-openapi-starter-webmvc-ui diff --git a/src/main/java/com/api/twitter/common/events/UserCreatedEvent.java b/src/main/java/com/api/twitter/common/events/UserCreatedEvent.java deleted file mode 100644 index 6dc5144..0000000 --- a/src/main/java/com/api/twitter/common/events/UserCreatedEvent.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.api.twitter.common.events; - -import lombok.Getter; -import org.jspecify.annotations.Nullable; -import org.springframework.context.ApplicationEvent; - -import java.time.Clock; -import java.util.UUID; - -@Getter -public class UserCreatedEvent extends ApplicationEvent { - private final UUID userId; - private final String username; - private final String email; - - public UserCreatedEvent( - UUID userId, - String username, - String email, - Object source - ) { - super(source); - this.userId = userId; - this.username = username; - this.email = email; - } -} diff --git a/src/main/java/com/api/twitter/common/events/UserCreatedEvent.kt b/src/main/java/com/api/twitter/common/events/UserCreatedEvent.kt new file mode 100644 index 0000000..6b4e498 --- /dev/null +++ b/src/main/java/com/api/twitter/common/events/UserCreatedEvent.kt @@ -0,0 +1,14 @@ +package com.api.twitter.common.events; + +import org.springframework.context.ApplicationEvent; + +import java.util.UUID; + +data class UserCreatedEvent( + val userId: UUID, + val username: String, + val email: String, + private val objSource: Any +) : ApplicationEvent(objSource) { +} + diff --git a/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.java b/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.java deleted file mode 100644 index d6c2d5c..0000000 --- a/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.java +++ /dev/null @@ -1,71 +0,0 @@ -package com.api.twitter.security.application.usecases; - -import com.api.twitter.common.events.UserCreatedEvent; -import com.api.twitter.common.exception.BadRequestException; -import com.api.twitter.common.model.UserRole; -import com.api.twitter.security.domain.model.UserCredentials; -import com.api.twitter.security.infrastructure.persistence.UserCredentialsRepository; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -import java.time.LocalDateTime; - -@Component -public class RegisterUserCredentialsUseCase { - private UserCredentialsRepository userCredentialsRepository; - - private PasswordEncoder passwordEncoder; - - private ApplicationEventPublisher applicationEventPublisher; - - public RegisterUserCredentialsUseCase( - UserCredentialsRepository userCredentialsRepository, - PasswordEncoder passwordEncoder, - ApplicationEventPublisher applicationEventPublisher - ) { - this.userCredentialsRepository = userCredentialsRepository; - this.passwordEncoder = passwordEncoder; - this.applicationEventPublisher = applicationEventPublisher; - } - - public void execute(String username, String email, String password){ - verifyIfUsernameOrEmailIsAlreadyInUse(username, email); - - UserCredentials userCredentials = new UserCredentials(); - userCredentials.setUsername(username); - userCredentials.setEmail(email); - userCredentials.setPassword(password); - userCredentials.setRole(UserRole.USER); - userCredentials.setCreatedAt(LocalDateTime.now()); - userCredentials.setPassword(password); - userCredentials.validate(); - - UserCredentials user = userCredentialsRepository.save(encodePassword(userCredentials)); - - publishUserCreatedEvent(user); - } - - private void verifyIfUsernameOrEmailIsAlreadyInUse(String username, String email){ - if(userCredentialsRepository.existsByUsername(username)) - throw new BadRequestException("This username is already in use"); - if(userCredentialsRepository.existsByEmail(email)) - throw new BadRequestException("This email is already in use"); - } - - private UserCredentials encodePassword(UserCredentials userCredentials){ - userCredentials.setPassword(passwordEncoder.encode(userCredentials.getPassword())); - return userCredentials; - } - - private void publishUserCreatedEvent(UserCredentials user){ - applicationEventPublisher.publishEvent( - new UserCreatedEvent( - user.getUserId(), - user.getUsername(), - user.getEmail(), - this - ) - ); - } -} diff --git a/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.kt b/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.kt new file mode 100644 index 0000000..d2c6b65 --- /dev/null +++ b/src/main/java/com/api/twitter/security/application/usecases/RegisterUserCredentialsUseCase.kt @@ -0,0 +1,61 @@ +package com.api.twitter.security.application.usecases + +import com.api.twitter.common.events.UserCreatedEvent +import com.api.twitter.common.exception.BadRequestException +import com.api.twitter.common.model.UserRole +import com.api.twitter.security.domain.model.UserCredentials +import com.api.twitter.security.infrastructure.persistence.UserCredentialsRepository +import org.springframework.context.ApplicationEventPublisher +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class RegisterUserCredentialsUseCase( + private val userCredentialsRepository: UserCredentialsRepository, + private val passwordEncoder: PasswordEncoder, + private val applicationEventPublisher: ApplicationEventPublisher +) { + fun execute(username: String, email: String, password: String) { + verifyIfUsernameOrEmailIsAlreadyInUse(username, email) + + val userCredentials = UserCredentials( + email = email, + rawPassword = password, + rawUsername = username, + role = UserRole.USER, + createdAt = LocalDateTime.now(), + ).apply { + this.validate() + }.also { + it.rawPassword = passwordEncoder.encode(password) + ?: throw RuntimeException("Invalid encrypted password") + } + val user = userCredentialsRepository.save(encodePassword(userCredentials)) + publishUserCreatedEvent(user) + } + + private fun verifyIfUsernameOrEmailIsAlreadyInUse(username: String, email: String) { + if (userCredentialsRepository.existsByUsername(username)) + throw BadRequestException("This username is already in use") + if (userCredentialsRepository.existsByEmail(email)) + throw BadRequestException("This email is already in use") + } + + private fun encodePassword(userCredentials: UserCredentials): UserCredentials { + userCredentials.rawPassword = passwordEncoder.encode(userCredentials.rawPassword) + ?: throw RuntimeException("Invalid encrypted password") + return userCredentials + } + + private fun publishUserCreatedEvent(user: UserCredentials) { + applicationEventPublisher.publishEvent( + UserCreatedEvent( + userId = user.userId ?: throw BadRequestException("User id is null"), + username = user.rawUsername, + email = user.email, + objSource = this + ) + ) + } +} diff --git a/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.java b/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.java deleted file mode 100644 index 7b0d893..0000000 --- a/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.api.twitter.security.application.usecases; - -import com.api.twitter.common.exception.BadRequestException; -import com.api.twitter.security.domain.model.UserCredentials; -import com.api.twitter.security.infrastructure.persistence.UserCredentialsRepository; -import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.stereotype.Component; - -import java.util.UUID; - -@Component -public class UpdateUserCredentialsUseCase { - private PasswordEncoder passwordEncoder; - private UserCredentialsRepository userCredentialsRepository; - - public UpdateUserCredentialsUseCase(PasswordEncoder passwordEncoder, UserCredentialsRepository userCredentialsRepository) { - this.passwordEncoder = passwordEncoder; - this.userCredentialsRepository = userCredentialsRepository; - } - - public void execute(UUID userId, String username, String email) { - UserCredentials userCredentials = userCredentialsRepository.findById(userId) - .orElseThrow(() -> new BadRequestException("User not found")); - userCredentials.setUsername(username); - userCredentials.setEmail(email); - userCredentials.validate(); - userCredentialsRepository.save(userCredentials); - } - - public void updatePassword(UUID userId, String password){ - UserCredentials userCredentials = userCredentialsRepository.findById(userId) - .orElseThrow(() -> new RuntimeException("User not found")); - - userCredentials.setPassword(password); - userCredentials.validatePassword(); - userCredentials.setPassword(passwordEncoder.encode(password)); - - userCredentialsRepository.save(userCredentials); - } -} diff --git a/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.kt b/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.kt new file mode 100644 index 0000000..28080e1 --- /dev/null +++ b/src/main/java/com/api/twitter/security/application/usecases/UpdateUserCredentialsUseCase.kt @@ -0,0 +1,39 @@ +package com.api.twitter.security.application.usecases + +import com.api.twitter.common.exception.BadRequestException +import com.api.twitter.security.domain.model.UserCredentials +import com.api.twitter.security.infrastructure.persistence.UserCredentialsRepository +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.stereotype.Component +import java.util.* +import java.util.function.Supplier +import kotlin.jvm.optionals.getOrNull + +@Component +class UpdateUserCredentialsUseCase( + private val passwordEncoder: PasswordEncoder, + private val userCredentialsRepository: UserCredentialsRepository +) { + + fun execute(userId: UUID, username: String, email: String) { + val userCredentials = userCredentialsRepository.findById(userId).getOrNull() + ?: throw BadRequestException("User not found") + + userCredentials.rawUsername = username + userCredentials.email = email + userCredentials.validate() + userCredentialsRepository.save(userCredentials) + } + + fun updatePassword(userId: UUID, password: String) { + val userCredentials = userCredentialsRepository.findById(userId).getOrNull() + ?: throw RuntimeException("User not found") + + userCredentials.rawPassword = password + userCredentials.validatePassword() + userCredentials.rawPassword = this.passwordEncoder.encode(password) + ?: throw BadRequestException("Invalid password") + + userCredentialsRepository.save(userCredentials) + } +} diff --git a/src/main/java/com/api/twitter/security/domain/model/UserCredentials.java b/src/main/java/com/api/twitter/security/domain/model/UserCredentials.java deleted file mode 100644 index 5f9b17d..0000000 --- a/src/main/java/com/api/twitter/security/domain/model/UserCredentials.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.api.twitter.security.domain.model; - -import com.api.twitter.common.model.UserRole; -import com.api.twitter.user.application.exception.UserValidationException; -import jakarta.persistence.*; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.jspecify.annotations.Nullable; -import org.springframework.security.core.GrantedAuthority; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; - -import java.time.LocalDateTime; -import java.util.Collection; -import java.util.List; -import java.util.UUID; - -@Entity @Table(name = "user_credentials") -@Data -@AllArgsConstructor -@NoArgsConstructor -public class UserCredentials implements UserDetails { - @Id @GeneratedValue(strategy = GenerationType.UUID) - private UUID userId; - - private String username; - private String email; - private String password; - private UserRole role; - private LocalDateTime createdAt; - - @Override - public Collection getAuthorities() { - return List.of( - new SimpleGrantedAuthority(role.getName()) - ); - } - - public void validate(){ - this.validateUsername(); - this.validateEmail(); - this.validatePassword(); - } - - // TODO: IMPROVE VALIDATION RESPONSE - public void validateUsername(){ - final int MIN_LEN_USERNAME = 3; - final int MAX_LEN_USERNAME = 25; - - if ( - this.username == null || - this.username.length() < MIN_LEN_USERNAME || - this.username.length() > MAX_LEN_USERNAME - ){ - throw new UserValidationException("Invalid username"); - } - } - - public void validateEmail(){ - // TODO - } - - // TODO: IMPROVE VALIDATION RESPONSE - public void validatePassword(){ - final int MIN_LEN_PASSWORD = 5; - final int MAX_LEN_PASSWORD = 50; - if ( - this.password == null || - this.password.length() < MIN_LEN_PASSWORD || - this.password.length() > MAX_LEN_PASSWORD - ){ - throw new UserValidationException("Invalid password"); - } - } - - public boolean equals(UserCredentials user2){ - return this.userId.compareTo(user2.getUserId()) == 0; - } - - @Override - public @Nullable String getPassword() { - return this.password; - } - - @Override - public String getUsername() { - return this.username; - } - - @Override - public boolean isAccountNonExpired() { -// return UserDetails.super.isAccountNonExpired(); - return true; - } - - @Override - public boolean isAccountNonLocked() { -// return UserDetails.super.isAccountNonLocked(); - return true; - } - - @Override - public boolean isCredentialsNonExpired() { -// return UserDetails.super.isCredentialsNonExpired(); - return true; - } - - @Override - public boolean isEnabled() { -// return UserDetails.super.isEnabled(); - return true; - } -} diff --git a/src/main/java/com/api/twitter/security/domain/model/UserCredentials.kt b/src/main/java/com/api/twitter/security/domain/model/UserCredentials.kt new file mode 100644 index 0000000..0cbeba9 --- /dev/null +++ b/src/main/java/com/api/twitter/security/domain/model/UserCredentials.kt @@ -0,0 +1,107 @@ +package com.api.twitter.security.domain.model; + +import com.api.twitter.common.model.UserRole; +import com.api.twitter.user.application.exception.UserValidationException; +import jakarta.persistence.Entity +import jakarta.persistence.EnumType +import jakarta.persistence.Enumerated +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id +import jakarta.persistence.Table +import jakarta.validation.constraints.Email +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.core.userdetails.UserDetails +import java.time.LocalDateTime +import java.util.UUID + + +@Entity +@Table(name = "user_credentials") +data class UserCredentials ( + @Id @GeneratedValue(strategy = GenerationType.UUID) + val userId: UUID? = null, + + @Email + var email: String, + + var rawUsername: String, + + var rawPassword: String, + + @Enumerated(EnumType.STRING) + var role: UserRole = UserRole.USER, + + val createdAt: LocalDateTime, +) : UserDetails { + + override fun getAuthorities(): kotlin.collections.Collection { + return listOf(SimpleGrantedAuthority(role.name)) + } + + fun validate() { + this.validateUsername(); + this.validateEmail(); + this.validatePassword(); + } + + // TODO: IMPROVE VALIDATION RESPONSE + fun validateUsername(){ + val MIN_LEN_USERNAME: Int = 3; + val MAX_LEN_USERNAME: Int = 25; + + if ( + this.rawUsername.length !in MIN_LEN_USERNAME..MAX_LEN_USERNAME + ){ + throw UserValidationException("Invalid username"); + } + } + + fun validateEmail(){ + TODO() + } + + // TODO: IMPROVE VALIDATION RESPONSE + fun validatePassword(){ + val MIN_LEN_PASSWORD: Int = 5; + val MAX_LEN_PASSWORD: Int = 50; + if ( + this.rawPassword.length !in MIN_LEN_PASSWORD..MAX_LEN_PASSWORD + ){ + throw UserValidationException("Invalid password"); + } + } + +// fun equals(user2: UserCredentials ): Boolean { +// return this.userId.compareTo(user2.userId) == 0; +// } + + override fun getPassword(): String { + return this.rawPassword; + } + + override fun getUsername(): String { + return this.rawUsername; + } + + override fun isAccountNonExpired(): Boolean { +// return UserDetails.super.isAccountNonExpired(); + return true; + } + + override fun isAccountNonLocked(): Boolean { +// return UserDetails.super.isAccountNonLocked(); + return true; + } + + override fun isCredentialsNonExpired(): Boolean { +// return UserDetails.super.isCredentialsNonExpired(); + return true; + } + + override fun isEnabled(): Boolean { +// return UserDetails.super.isEnabled(); + return true; + } +} diff --git a/src/main/resources/application-prod.yaml b/src/main/resources/application-prod.yaml new file mode 100644 index 0000000..4698509 --- /dev/null +++ b/src/main/resources/application-prod.yaml @@ -0,0 +1,37 @@ +server: + port: ${PORT:8080} + +spring: + application: + name: twitter-api + title: "Twitter API" + version: ${APP_VERSION:"x.x.x"} + url: ${SERVER_URL:http://localhost:8080/} + datasource: + url: ${DB_URL} + username: ${DB_USERNAME} + password: ${DB_PASSWORD} + driver-class-name: org.postgresql.Driver + + cache: + type: REDIS + + data: + redis: + url: ${REDIS_URL:redis://localhost:6379} + web: + pageable: + max-page-size: 50 + +api: + security: + token: + secret: ${JWT_SECRET:secret} + issuer: ${JWT_ISSUER:issuer} + expiration-seconds: ${JWT_EXPIRATION_SECONDS:3600} + +springdoc: + writer-with-default-pretty-printer: true + swagger-ui: + syntaxHighlight: + activated: true \ No newline at end of file diff --git a/src/main/resources/application-test.yaml b/src/main/resources/application-test.yaml index d333769..6c98769 100644 --- a/src/main/resources/application-test.yaml +++ b/src/main/resources/application-test.yaml @@ -24,4 +24,11 @@ springdoc: api-docs: enabled: false swagger-ui: - enabled: false \ No newline at end of file + enabled: false + +api: + security: + token: + secret: secret + issuer: issuer + expiration-seconds: 3600 \ No newline at end of file diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 4698509..389719d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -2,36 +2,5 @@ server: port: ${PORT:8080} spring: - application: - name: twitter-api - title: "Twitter API" - version: ${APP_VERSION:"x.x.x"} - url: ${SERVER_URL:http://localhost:8080/} - datasource: - url: ${DB_URL} - username: ${DB_USERNAME} - password: ${DB_PASSWORD} - driver-class-name: org.postgresql.Driver - - cache: - type: REDIS - - data: - redis: - url: ${REDIS_URL:redis://localhost:6379} - web: - pageable: - max-page-size: 50 - -api: - security: - token: - secret: ${JWT_SECRET:secret} - issuer: ${JWT_ISSUER:issuer} - expiration-seconds: ${JWT_EXPIRATION_SECONDS:3600} - -springdoc: - writer-with-default-pretty-printer: true - swagger-ui: - syntaxHighlight: - activated: true \ No newline at end of file + profiles: + active: ${ACTIVE_PROFILE:test} \ No newline at end of file diff --git a/src/test/java/com/api/twitter/user/application/usecases/DeleteUserUseCaseTest.java b/src/test/java/com/api/twitter/user/application/usecases/DeleteUserUseCaseTest.java index 52e0cec..340579c 100644 --- a/src/test/java/com/api/twitter/user/application/usecases/DeleteUserUseCaseTest.java +++ b/src/test/java/com/api/twitter/user/application/usecases/DeleteUserUseCaseTest.java @@ -14,12 +14,14 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.UUID; import static org.junit.jupiter.api.Assertions.*; +@ActiveProfiles("test") class DeleteUserUseCaseTest { @Mock private UserProfileRepository userRepository; diff --git a/src/test/java/com/api/twitter/user/application/usecases/ListUsersUseCaseTest.java b/src/test/java/com/api/twitter/user/application/usecases/ListUsersUseCaseTest.java index cdc9e29..4a1bca1 100644 --- a/src/test/java/com/api/twitter/user/application/usecases/ListUsersUseCaseTest.java +++ b/src/test/java/com/api/twitter/user/application/usecases/ListUsersUseCaseTest.java @@ -18,6 +18,7 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.ArrayList; @@ -28,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.eq; +@ActiveProfiles("test") class ListUsersUseCaseTest { private final UserProfile userMock1 = new UserProfile( UUID.randomUUID(), diff --git a/src/test/java/com/api/twitter/user/application/usecases/UpdateUserUseCaseTest.java b/src/test/java/com/api/twitter/user/application/usecases/UpdateUserUseCaseTest.java index eefcb57..2b10be2 100644 --- a/src/test/java/com/api/twitter/user/application/usecases/UpdateUserUseCaseTest.java +++ b/src/test/java/com/api/twitter/user/application/usecases/UpdateUserUseCaseTest.java @@ -15,6 +15,7 @@ import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.context.ActiveProfiles; import java.time.LocalDateTime; import java.util.Optional; @@ -24,6 +25,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +@ActiveProfiles("test") class UpdateUserUseCaseTest { @Mock private UserProfileRepository userRepository;