diff --git a/.github/workflows/build-push-container-images.yml b/.github/workflows/build-push-container-images.yml new file mode 100644 index 0000000..f1038d2 --- /dev/null +++ b/.github/workflows/build-push-container-images.yml @@ -0,0 +1,106 @@ +name: Build and Push Container Images + +on: + push: + branches: + - main + +permissions: + contents: read + +env: + JAVA_VERSION: "21" + DISTRIBUTION: "corretto" + CONTAINER_REGISTRY: "ghcr.io" + CONTAINER_GROUP: "cloudeko/services" + +jobs: + build-image: + name: Build Docker Image + environment: development + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.DISTRIBUTION }} + cache: maven + + - name: Get Short SHA + id: short_sha + run: echo "short_sha=$(echo $GITHUB_SHA | head -c7)" >> $GITHUB_OUTPUT + + - name: Set Maven Version + run: | + mvn versions:set -DnewVersion=${{ steps.short_sha.outputs.short_sha }} + + - name: Set IMAGE_NAME env variable + run: | + MODULE_NAME=zenei + TEMP_NAME=$(echo "${MODULE_NAME}") + IMAGE_NAME=$(echo "$TEMP_NAME" | sed -r 's/([a-z])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]') + echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV + + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.CONTAINER_REGISTRY }} -u ${{ github.actor }} --password-stdin + + - name: Build and push native image + run: | + mvn clean install -DskipTests \ + -Dquarkus.container-image.build=true \ + -Dquarkus.container-image.push=true \ + -Dquarkus.container-image.registry=${{ env.CONTAINER_REGISTRY }} \ + -Dquarkus.container-image.group=${{ env.CONTAINER_GROUP }} \ + -Dquarkus.container-image.name=${{ env.IMAGE_NAME }} \ + -Dquarkus.container-image.tag=${{ steps.short_sha.outputs.short_sha }} + + build-native-image: + name: Build Native Docker Image + environment: development + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: ${{ env.DISTRIBUTION }} + cache: maven + + - name: Get Short SHA + id: short_sha + run: echo "short_sha=$(echo $GITHUB_SHA | head -c7)" >> $GITHUB_OUTPUT + + - name: Set Maven Version + run: | + mvn versions:set -DnewVersion=${{ steps.short_sha.outputs.short_sha }} + + - name: Set IMAGE_NAME env variable + run: | + MODULE_NAME=zenei + TEMP_NAME=$(echo "${MODULE_NAME}") + IMAGE_NAME=$(echo "$TEMP_NAME" | sed -r 's/([a-z])([A-Z])/\1-\2/g' | tr '[:upper:]' '[:lower:]') + echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV + + - name: Log in to registry + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ${{ env.CONTAINER_REGISTRY }} -u ${{ github.actor }} --password-stdin + + - name: Build and push native image + run: | + mvn clean install -DskipTests \ + -Dnative \ + -Dquarkus.native.container-build=true \ + -Dquarkus.container-image.build=true \ + -Dquarkus.container-image.push=true \ + -Dquarkus.container-image.registry=${{ env.CONTAINER_REGISTRY }} \ + -Dquarkus.container-image.group=${{ env.CONTAINER_GROUP }}/native \ + -Dquarkus.container-image.name=${{ env.IMAGE_NAME }} \ + -Dquarkus.container-image.tag=${{ steps.short_sha.outputs.short_sha }} diff --git a/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUserResponse.java b/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUserResponse.java index 6f01c35..ee2e2e4 100644 --- a/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUserResponse.java +++ b/src/main/java/dev/cloudeko/zenei/application/web/model/response/PrivateUserResponse.java @@ -21,6 +21,12 @@ public class PrivateUserResponse { @Schema(description = "Username of the user") private String username; + @Schema(description = "First name of the user") + private String firstName; + + @Schema(description = "Last name of the user") + private String lastName; + @Schema(description = "Primary email of the user") private String primaryEmailAddress; @@ -30,6 +36,9 @@ public class PrivateUserResponse { @Schema(description = "Whether the user is an admin") private Boolean admin; + @Schema(description = "Whether the user has a password") + private Boolean passwordEnabled; + @Schema(description = "Timestamp when the user was created") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") private LocalDateTime createdAt; @@ -41,10 +50,12 @@ public class PrivateUserResponse { public PrivateUserResponse(User user) { this.id = user.getId().toString(); this.username = user.getUsername(); - this.email = user.getEmail(); - this.emailVerified = user.isEmailVerified(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.primaryEmailAddress = user.getPrimaryEmailAddress().getEmail(); this.image = user.getImage(); this.admin = user.isAdmin(); + this.passwordEnabled = user.isPasswordEnabled(); this.createdAt = user.getCreatedAt(); this.updatedAt = user.getUpdatedAt(); } diff --git a/src/main/java/dev/cloudeko/zenei/application/web/resource/AuthenticationResource.java b/src/main/java/dev/cloudeko/zenei/application/web/resource/AuthenticationResource.java index 26b85b6..d594f82 100644 --- a/src/main/java/dev/cloudeko/zenei/application/web/resource/AuthenticationResource.java +++ b/src/main/java/dev/cloudeko/zenei/application/web/resource/AuthenticationResource.java @@ -26,7 +26,7 @@ public class AuthenticationResource { private final CreateUser createUser; private final VerifyEmail verifyEmail; private final RefreshAccessToken refreshAccessToken; - private final CreateEmailAddress createEmailAddress; + private final SendMagicLinkVerifyEmail sendMagicLinkVerifyEmail; private final LoginUserWithPassword loginUserWithPassword; @POST @@ -37,7 +37,7 @@ public Response signup(@BeanParam @Valid SignupRequest request) { final var emailAddress = user.getPrimaryEmailAddress(); if (!emailAddress.getEmailVerified() && emailAddress.getEmailVerificationToken() != null) { - createEmailAddress.handle(new EmailAddressInput(emailAddress)); + sendMagicLinkVerifyEmail.handle(new EmailAddressInput(emailAddress)); } if (request.getRedirectTo() != null) { diff --git a/src/main/java/dev/cloudeko/zenei/domain/exception/EmailNotFoundException.java b/src/main/java/dev/cloudeko/zenei/domain/exception/EmailNotFoundException.java new file mode 100644 index 0000000..5206daf --- /dev/null +++ b/src/main/java/dev/cloudeko/zenei/domain/exception/EmailNotFoundException.java @@ -0,0 +1,8 @@ +package dev.cloudeko.zenei.domain.exception; + +public class EmailNotFoundException extends BusinessException { + + public EmailNotFoundException() { + super(3, "email not found"); + } +} diff --git a/src/main/java/dev/cloudeko/zenei/domain/exception/InvalidUserLoginException.java b/src/main/java/dev/cloudeko/zenei/domain/exception/InvalidUserLoginException.java new file mode 100644 index 0000000..284b4ce --- /dev/null +++ b/src/main/java/dev/cloudeko/zenei/domain/exception/InvalidUserLoginException.java @@ -0,0 +1,7 @@ +package dev.cloudeko.zenei.domain.exception; + +public class InvalidUserLoginException extends BusinessException { + public InvalidUserLoginException() { + super(4, "invalid user login"); + } +} diff --git a/src/main/java/dev/cloudeko/zenei/domain/feature/CreateEmailAddress.java b/src/main/java/dev/cloudeko/zenei/domain/feature/SendMagicLinkVerifyEmail.java similarity index 77% rename from src/main/java/dev/cloudeko/zenei/domain/feature/CreateEmailAddress.java rename to src/main/java/dev/cloudeko/zenei/domain/feature/SendMagicLinkVerifyEmail.java index 8017bc4..628c7b0 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/feature/CreateEmailAddress.java +++ b/src/main/java/dev/cloudeko/zenei/domain/feature/SendMagicLinkVerifyEmail.java @@ -2,6 +2,6 @@ import dev.cloudeko.zenei.domain.model.email.EmailAddressInput; -public interface CreateEmailAddress { +public interface SendMagicLinkVerifyEmail { void handle(EmailAddressInput input); } diff --git a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateUserImpl.java b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateUserImpl.java index 899442a..dc303ce 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateUserImpl.java +++ b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateUserImpl.java @@ -6,26 +6,42 @@ import dev.cloudeko.zenei.domain.model.email.EmailAddress; import dev.cloudeko.zenei.domain.model.user.*; import dev.cloudeko.zenei.domain.provider.HashProvider; +import dev.cloudeko.zenei.domain.provider.StringTokenProvider; import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; import lombok.AllArgsConstructor; +import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; +import java.util.UUID; @ApplicationScoped @AllArgsConstructor public class CreateUserImpl implements CreateUser { + private final HashProvider hashProvider; + private final StringTokenProvider stringTokenProvider; + private final UserRepository userRepository; private final UserPasswordRepository userPasswordRepository; - private final HashProvider hashProvider; @Override + @Transactional public User handle(CreateUserInput createUserInput) { final var emailAddress = EmailAddress.builder().email(createUserInput.getEmail()).build(); + if (true) { //Will be based on configuration + final var token = stringTokenProvider.generateToken("mail", emailAddress.getEmail() + UUID.randomUUID()); + + emailAddress.setEmailVerificationToken(token); + emailAddress.setEmailVerificationTokenExpiresAt(LocalDateTime.now().plusDays(1)); + emailAddress.setEmailVerified(false); + } + final var user = User.builder() .username(createUserInput.getUsername()) .primaryEmailAddress(emailAddress.getEmail()) - .emailAddresses(List.of(emailAddress)).build(); + .emailAddresses(new ArrayList<>(List.of(emailAddress))).build(); checkExistingUsername(user.getUsername()); checkExistingEmail(user.getPrimaryEmailAddress().getEmail()); diff --git a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithPasswordImpl.java b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithPasswordImpl.java index 0b25893..8e98fd1 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithPasswordImpl.java +++ b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/LoginUserWithPasswordImpl.java @@ -1,11 +1,13 @@ package dev.cloudeko.zenei.domain.feature.impl; +import dev.cloudeko.zenei.domain.exception.InvalidPasswordException; import dev.cloudeko.zenei.domain.exception.UserNotFoundException; import dev.cloudeko.zenei.domain.feature.LoginUserWithPassword; import dev.cloudeko.zenei.domain.feature.util.TokenUtil; import dev.cloudeko.zenei.domain.model.Token; import dev.cloudeko.zenei.domain.model.token.LoginPasswordInput; import dev.cloudeko.zenei.domain.model.token.RefreshTokenRepository; +import dev.cloudeko.zenei.domain.model.user.UserPassword; import dev.cloudeko.zenei.domain.model.user.UserPasswordRepository; import dev.cloudeko.zenei.domain.model.user.UserRepository; import dev.cloudeko.zenei.domain.provider.HashProvider; @@ -45,7 +47,7 @@ public Token handle(LoginPasswordInput loginPasswordInput) { return TokenUtil.createToken(user, accessTokenData, refreshToken); } else { - throw new UserNotFoundException(); + throw new InvalidPasswordException(); } } } diff --git a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateEmailAddressImpl.java b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/SendMagicLinkVerifyEmailImpl.java similarity index 55% rename from src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateEmailAddressImpl.java rename to src/main/java/dev/cloudeko/zenei/domain/feature/impl/SendMagicLinkVerifyEmailImpl.java index 8e1967b..c5ef160 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/CreateEmailAddressImpl.java +++ b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/SendMagicLinkVerifyEmailImpl.java @@ -1,9 +1,8 @@ package dev.cloudeko.zenei.domain.feature.impl; -import dev.cloudeko.zenei.domain.exception.UserNotFoundException; -import dev.cloudeko.zenei.domain.feature.CreateEmailAddress; -import dev.cloudeko.zenei.domain.model.email.ConfirmationTokenRepository; +import dev.cloudeko.zenei.domain.feature.SendMagicLinkVerifyEmail; import dev.cloudeko.zenei.domain.model.email.EmailAddressInput; +import dev.cloudeko.zenei.domain.model.email.EmailAddressRepository; import dev.cloudeko.zenei.domain.model.user.UserRepository; import dev.cloudeko.zenei.domain.provider.MailTemplateProvider; import dev.cloudeko.zenei.domain.provider.StringTokenProvider; @@ -13,35 +12,24 @@ import lombok.AllArgsConstructor; import lombok.extern.jbosslog.JBossLog; -import java.util.UUID; - @JBossLog @ApplicationScoped @AllArgsConstructor -public class CreateEmailAddressImpl implements CreateEmailAddress { +public class SendMagicLinkVerifyEmailImpl implements SendMagicLinkVerifyEmail { private final Mailer mailer; private final StringTokenProvider stringTokenProvider; private final MailTemplateProvider mailTemplateProvider; private final UserRepository userRepository; - private final ConfirmationTokenRepository confirmationTokenRepository; + private final EmailAddressRepository emailAddressRepository; @Override public void handle(EmailAddressInput input) { - final var user = userRepository.getUserByEmail(input.getEmailAddress()); - - if (user.isEmpty()) { - throw new UserNotFoundException(); - } - - final var token = stringTokenProvider.generateToken("mail", input.getEmailAddress() + UUID.randomUUID()); - - final var content = mailTemplateProvider.defaultCreateConfirmationMailTemplate( "http://localhost:8080/user/verify-email", - input.getEmailVerificationToken()); - final var mail = Mail.withHtml(input.getEmailAddress(), "Welcome to Zenei", content); + input.getEmailAddress().getEmailVerificationToken()); + final var mail = Mail.withHtml(input.getEmailAddress().getEmail(), "Welcome to Zenei", content); mailer.send(mail); } diff --git a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/VerifyEmailImpl.java b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/VerifyEmailImpl.java index e7a7d17..69b1582 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/feature/impl/VerifyEmailImpl.java +++ b/src/main/java/dev/cloudeko/zenei/domain/feature/impl/VerifyEmailImpl.java @@ -1,46 +1,21 @@ package dev.cloudeko.zenei.domain.feature.impl; -import dev.cloudeko.zenei.domain.exception.InvalidConfirmationTokenException; import dev.cloudeko.zenei.domain.feature.VerifyEmail; import dev.cloudeko.zenei.domain.model.email.ConfirmEmailInput; -import dev.cloudeko.zenei.domain.model.email.ConfirmationTokenRepository; -import dev.cloudeko.zenei.domain.model.user.UserRepository; -import dev.cloudeko.zenei.domain.provider.MailTemplateProvider; -import dev.cloudeko.zenei.domain.provider.StringTokenProvider; -import io.quarkus.mailer.Mailer; +import dev.cloudeko.zenei.domain.model.email.EmailAddressRepository; import jakarta.enterprise.context.ApplicationScoped; import lombok.AllArgsConstructor; import lombok.extern.jbosslog.JBossLog; -import java.time.LocalDateTime; - @JBossLog @ApplicationScoped @AllArgsConstructor public class VerifyEmailImpl implements VerifyEmail { - private final Mailer mailer; - private final StringTokenProvider stringTokenProvider; - private final MailTemplateProvider mailTemplateProvider; - - private final UserRepository userRepository; - private final ConfirmationTokenRepository confirmationTokenRepository; + private final EmailAddressRepository emailAddressRepository; @Override public void handle(ConfirmEmailInput input) { - final var confirmationToken = confirmationTokenRepository.findByToken(input.getToken()); - - if (confirmationToken.isEmpty()) { - throw new InvalidConfirmationTokenException(); - } - - if (confirmationToken.get().getExpiresAt().isBefore(LocalDateTime.now())) { - throw new InvalidConfirmationTokenException(); - } - - final var user = confirmationToken.get().getUser(); - - userRepository.updateEmailVerified(user.getEmail(), true); - confirmationTokenRepository.deleteConfirmationToken(input.getToken()); + emailAddressRepository.confirmEmailAddress(input.getToken()); } } diff --git a/src/main/java/dev/cloudeko/zenei/domain/mapping/ConfirmationTokenMapper.java b/src/main/java/dev/cloudeko/zenei/domain/mapping/ConfirmationTokenMapper.java deleted file mode 100644 index 7b4ac95..0000000 --- a/src/main/java/dev/cloudeko/zenei/domain/mapping/ConfirmationTokenMapper.java +++ /dev/null @@ -1,18 +0,0 @@ -package dev.cloudeko.zenei.domain.mapping; - -import dev.cloudeko.zenei.domain.model.email.ConfirmationToken; -import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ConfirmationTokenEntity; -import org.mapstruct.Mapper; -import org.mapstruct.MappingTarget; - -import java.util.List; - -@Mapper(config = QuarkusMappingConfig.class, uses = UserMapper.class) -public interface ConfirmationTokenMapper { - - List toDomainList(List entities); - - ConfirmationToken toDomain(ConfirmationTokenEntity entity); - - void updateDomainFromEntity(ConfirmationTokenEntity entity, @MappingTarget ConfirmationToken domain); -} diff --git a/src/main/java/dev/cloudeko/zenei/domain/mapping/EmailAddressMapper.java b/src/main/java/dev/cloudeko/zenei/domain/mapping/EmailAddressMapper.java index 8a728b4..d41c1f2 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/mapping/EmailAddressMapper.java +++ b/src/main/java/dev/cloudeko/zenei/domain/mapping/EmailAddressMapper.java @@ -1,8 +1,6 @@ package dev.cloudeko.zenei.domain.mapping; -import dev.cloudeko.zenei.domain.model.email.ConfirmationToken; import dev.cloudeko.zenei.domain.model.email.EmailAddress; -import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ConfirmationTokenEntity; import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.EmailAddressEntity; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; diff --git a/src/main/java/dev/cloudeko/zenei/domain/mapping/UserMapper.java b/src/main/java/dev/cloudeko/zenei/domain/mapping/UserMapper.java index 7d69ba0..f9c66c9 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/mapping/UserMapper.java +++ b/src/main/java/dev/cloudeko/zenei/domain/mapping/UserMapper.java @@ -1,5 +1,6 @@ package dev.cloudeko.zenei.domain.mapping; +import dev.cloudeko.zenei.domain.model.email.EmailAddress; import dev.cloudeko.zenei.domain.model.user.User; import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.UserEntity; import org.mapstruct.Mapper; @@ -17,4 +18,8 @@ public interface UserMapper { void updateDomainFromEntity(UserEntity entity, @MappingTarget User domain); UserEntity toEntity(User domain); + + default String map(EmailAddress emailAddress) { + return emailAddress.getEmail(); + } } diff --git a/src/main/java/dev/cloudeko/zenei/domain/mapping/UserPasswordMapper.java b/src/main/java/dev/cloudeko/zenei/domain/mapping/UserPasswordMapper.java index ff1b290..43152f9 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/mapping/UserPasswordMapper.java +++ b/src/main/java/dev/cloudeko/zenei/domain/mapping/UserPasswordMapper.java @@ -1,8 +1,6 @@ package dev.cloudeko.zenei.domain.mapping; -import dev.cloudeko.zenei.domain.model.user.User; import dev.cloudeko.zenei.domain.model.user.UserPassword; -import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.UserEntity; import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.UserPasswordEntity; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationToken.java b/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationToken.java deleted file mode 100644 index 572d5c3..0000000 --- a/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationToken.java +++ /dev/null @@ -1,21 +0,0 @@ -package dev.cloudeko.zenei.domain.model.email; - -import dev.cloudeko.zenei.domain.model.user.User; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ConfirmationToken { - private String token; - private User user; - private LocalDateTime expiresAt; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationTokenRepository.java b/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationTokenRepository.java deleted file mode 100644 index 6d64d5e..0000000 --- a/src/main/java/dev/cloudeko/zenei/domain/model/email/ConfirmationTokenRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.cloudeko.zenei.domain.model.email; - -import java.time.LocalDateTime; -import java.util.Optional; - -public interface ConfirmationTokenRepository { - - ConfirmationToken createConfirmationToken(String email, String token, LocalDateTime expiresAt); - - void deleteConfirmationToken(String token); - - Optional findByToken(String token); -} diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddress.java b/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddress.java index 6abaf7c..8445308 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddress.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddress.java @@ -15,7 +15,7 @@ public class EmailAddress { private Long id; private String email; - private Boolean emailVerified; + private Boolean emailVerified = true; private String emailVerificationToken; private LocalDateTime emailVerificationTokenExpiresAt; diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddressRepository.java b/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddressRepository.java index 14f5084..315f2d4 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddressRepository.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/email/EmailAddressRepository.java @@ -9,4 +9,6 @@ public interface EmailAddressRepository { void deleteEmailAddress(String emailAddress); Optional findByEmailAddress(String emailAddress); + + void confirmEmailAddress(String token); } diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/session/Session.java b/src/main/java/dev/cloudeko/zenei/domain/model/session/Session.java index 3958414..9fb1ecb 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/session/Session.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/session/Session.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; -import java.util.UUID; @Data @Builder diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/token/RefreshToken.java b/src/main/java/dev/cloudeko/zenei/domain/model/token/RefreshToken.java index 46b8654..91a41c5 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/token/RefreshToken.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/token/RefreshToken.java @@ -7,7 +7,6 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; -import java.util.UUID; @Data @Builder diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/user/User.java b/src/main/java/dev/cloudeko/zenei/domain/model/user/User.java index 14316e9..cfd4292 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/user/User.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/user/User.java @@ -8,7 +8,6 @@ import java.time.LocalDateTime; import java.util.List; -import java.util.UUID; @Data @Builder diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/user/UserPassword.java b/src/main/java/dev/cloudeko/zenei/domain/model/user/UserPassword.java index 4aac87c..0b2e9d7 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/user/UserPassword.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/user/UserPassword.java @@ -5,8 +5,6 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.UUID; - @Data @Builder @NoArgsConstructor diff --git a/src/main/java/dev/cloudeko/zenei/domain/model/user/UserRepository.java b/src/main/java/dev/cloudeko/zenei/domain/model/user/UserRepository.java index 4262e2c..ff7974c 100644 --- a/src/main/java/dev/cloudeko/zenei/domain/model/user/UserRepository.java +++ b/src/main/java/dev/cloudeko/zenei/domain/model/user/UserRepository.java @@ -1,14 +1,11 @@ package dev.cloudeko.zenei.domain.model.user; import java.util.Optional; -import java.util.UUID; public interface UserRepository { void createUser(User user); - void updateEmailVerified(String email, boolean verified); - boolean existsByEmail(String email); boolean existsByUsername(String username); diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/provider/BCryptRefreshTokenProvider.java b/src/main/java/dev/cloudeko/zenei/infrastructure/provider/BCryptRefreshTokenProvider.java index b37bb22..c2b380a 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/provider/BCryptRefreshTokenProvider.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/provider/BCryptRefreshTokenProvider.java @@ -19,6 +19,6 @@ public String generateRefreshToken(User user) { } private String generateHash(User user) { - return BcryptUtil.bcryptHash(user.getEmail() + System.currentTimeMillis() + UUID.randomUUID().toString()); + return BcryptUtil.bcryptHash(user.getPrimaryEmailAddress().getEmail() + System.currentTimeMillis() + UUID.randomUUID().toString()); } } diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/provider/JwtTokenProvider.java b/src/main/java/dev/cloudeko/zenei/infrastructure/provider/JwtTokenProvider.java index 607c414..1a35ad8 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/provider/JwtTokenProvider.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/provider/JwtTokenProvider.java @@ -10,7 +10,7 @@ public class JwtTokenProvider implements TokenProvider { @Override public String generateToken(User user) { return Jwt.issuer("https://zenei.cloudeko.dev/") - .subject(user.getEmail()) + .subject(user.getPrimaryEmailAddress().getEmail()) .groups(user.isAdmin() ? "admin" : "user") .claim("username", user.getUsername()) .sign(); diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ConfirmationTokenEntity.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ConfirmationTokenEntity.java deleted file mode 100644 index 97d30a1..0000000 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/ConfirmationTokenEntity.java +++ /dev/null @@ -1,33 +0,0 @@ -package dev.cloudeko.zenei.infrastructure.repository.hibernate.entity; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.*; -import lombok.Data; -import org.hibernate.annotations.CreationTimestamp; -import org.hibernate.annotations.UpdateTimestamp; - -import java.time.LocalDateTime; - -@Data -@Entity -@Table(name = "confirmation_tokens") -public class ConfirmationTokenEntity extends PanacheEntity { - - @Column(name = "token", nullable = false) - private String token; - - @ManyToOne - @JoinColumn(name = "user_id", referencedColumnName = "id", nullable = false) - private UserEntity user; - - @Column(name = "expires_at", nullable = false) - private LocalDateTime expiresAt; - - @CreationTimestamp - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - @UpdateTimestamp - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; -} diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/EmailAddressEntity.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/EmailAddressEntity.java index d6109de..3c63f93 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/EmailAddressEntity.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/EmailAddressEntity.java @@ -11,6 +11,7 @@ @Data @Entity @Table(name = "email_addresses") +@NamedQuery(name = "EmailAddressEntity.confirmEmail", query = "UPDATE EmailAddressEntity e SET e.emailVerified = true, e.emailVerificationToken = null, e.emailVerificationTokenExpiresAt = null WHERE e.emailVerificationToken = :token AND e.emailVerificationTokenExpiresAt > CURRENT_TIMESTAMP") public class EmailAddressEntity extends PanacheEntity { @ManyToOne diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserEntity.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserEntity.java index cbe74f1..89c29b9 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserEntity.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserEntity.java @@ -8,12 +8,10 @@ import lombok.NoArgsConstructor; import org.hibernate.annotations.CreationTimestamp; import org.hibernate.annotations.UpdateTimestamp; -import org.hibernate.annotations.UuidGenerator; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; -import java.util.UUID; @Data @Entity diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserPasswordEntity.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserPasswordEntity.java index 481cf47..c365ebf 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserPasswordEntity.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/entity/UserPasswordEntity.java @@ -4,12 +4,10 @@ import jakarta.persistence.*; import lombok.Data; -import java.util.UUID; - @Data @Entity @Table(name = "user_passwords") -@NamedQuery(name = "UserPasswordEntity.isValidPassword", query = "SELECT u FROM UserPasswordEntity u WHERE u.user.email = ?1 AND u.passwordHash = ?2") +@NamedQuery(name = "UserPasswordEntity.isValidPassword", query = "SELECT u FROM UserPasswordEntity u WHERE u.user.primaryEmailAddress = ?1 AND u.passwordHash = ?2") public class UserPasswordEntity extends PanacheEntity { @MapsId diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AbstractPanacheRepositoryBase.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AbstractPanacheRepositoryBase.java index 1eea40b..188f866 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AbstractPanacheRepositoryBase.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/AbstractPanacheRepositoryBase.java @@ -5,7 +5,6 @@ import jakarta.persistence.NoResultException; import java.util.Optional; -import java.util.UUID; public abstract class AbstractPanacheRepositoryBase implements PanacheRepositoryBase { diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ConfirmationTokenRepositoryPanache.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ConfirmationTokenRepositoryPanache.java deleted file mode 100644 index 293e790..0000000 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/ConfirmationTokenRepositoryPanache.java +++ /dev/null @@ -1,54 +0,0 @@ -package dev.cloudeko.zenei.infrastructure.repository.hibernate.panache; - -import dev.cloudeko.zenei.domain.exception.UserNotFoundException; -import dev.cloudeko.zenei.domain.mapping.ConfirmationTokenMapper; -import dev.cloudeko.zenei.domain.model.email.ConfirmationToken; -import dev.cloudeko.zenei.domain.model.email.ConfirmationTokenRepository; -import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.ConfirmationTokenEntity; -import jakarta.enterprise.context.ApplicationScoped; -import lombok.AllArgsConstructor; - -import java.time.LocalDateTime; -import java.util.Optional; - -@ApplicationScoped -@AllArgsConstructor -public class ConfirmationTokenRepositoryPanache extends AbstractPanacheRepository implements - ConfirmationTokenRepository { - - private final ConfirmationTokenMapper confirmationTokenMapper; - - @Override - public ConfirmationToken createConfirmationToken(String email, String token, LocalDateTime expiresAt) { - final var user = findUserEntityByEmail(email); - if (user.isEmpty()) { - throw new UserNotFoundException(); - } - - final var confirmationTokenEntity = new ConfirmationTokenEntity(); - - confirmationTokenEntity.setToken(token); - confirmationTokenEntity.setUser(user.get()); - confirmationTokenEntity.setExpiresAt(expiresAt); - - persist(confirmationTokenEntity); - - return confirmationTokenMapper.toDomain(confirmationTokenEntity); - } - - @Override - public void deleteConfirmationToken(String token) { - delete("token", token); - } - - @Override - public Optional findByToken(String token) { - final var confirmationTokenEntity = find("token", token).firstResult(); - - if (confirmationTokenEntity == null) { - return Optional.empty(); - } - - return Optional.of(confirmationTokenMapper.toDomain(confirmationTokenEntity)); - } -} diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/EmailAddressRepositoryPanache.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/EmailAddressRepositoryPanache.java index 0bf21bb..fb15345 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/EmailAddressRepositoryPanache.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/EmailAddressRepositoryPanache.java @@ -1,10 +1,12 @@ package dev.cloudeko.zenei.infrastructure.repository.hibernate.panache; +import dev.cloudeko.zenei.domain.exception.InvalidConfirmationTokenException; import dev.cloudeko.zenei.domain.exception.UserNotFoundException; import dev.cloudeko.zenei.domain.mapping.EmailAddressMapper; import dev.cloudeko.zenei.domain.model.email.EmailAddress; import dev.cloudeko.zenei.domain.model.email.EmailAddressRepository; import dev.cloudeko.zenei.infrastructure.repository.hibernate.entity.EmailAddressEntity; +import io.quarkus.panache.common.Parameters; import jakarta.enterprise.context.ApplicationScoped; import lombok.AllArgsConstructor; @@ -45,4 +47,12 @@ public Optional findByEmailAddress(String emailAddress) { return Optional.of(emailAddressMapper.toDomain(emailAddressEntity)); } + + @Override + public void confirmEmailAddress(String token) { + final var updated = update("#EmailAddressEntity.confirmEmail", Parameters.with("token", token)); + if (updated == 0) { + throw new InvalidConfirmationTokenException(); + } + } } diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserPasswordRepositoryPanache.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserPasswordRepositoryPanache.java index 9583544..60f7383 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserPasswordRepositoryPanache.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserPasswordRepositoryPanache.java @@ -44,7 +44,7 @@ public void updateUserPassword(UserPassword userPassword) { @Override public Optional getUserPasswordByEmail(String email) { - final var userPasswordEntity = find("user.email", email).firstResult(); + final var userPasswordEntity = find("user.primaryEmailAddress", email).firstResult(); if (userPasswordEntity == null) { return Optional.empty(); diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java index ab9189a..297c2ca 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/repository/hibernate/panache/UserRepositoryPanache.java @@ -8,7 +8,6 @@ import lombok.AllArgsConstructor; import java.util.Optional; -import java.util.UUID; @ApplicationScoped @AllArgsConstructor @@ -19,16 +18,11 @@ public class UserRepositoryPanache extends AbstractPanacheRepository @Override public void createUser(User user) { final var userEntity = userMapper.toEntity(user); + userEntity.getEmailAddresses().forEach(emailAddressEntity -> emailAddressEntity.setUser(userEntity)); + persist(userEntity); - userMapper.updateDomainFromEntity(userEntity, user); - } - @Override - public void updateEmailVerified(String email, boolean verified) { - getEntityManager().createNamedQuery("UserEntity.updateEmailVerified") - .setParameter("emailVerified", verified) - .setParameter("email", email) - .executeUpdate(); + userMapper.updateDomainFromEntity(userEntity, user); } @Override diff --git a/src/main/java/dev/cloudeko/zenei/infrastructure/web/mapper/BusinessExceptionMapper.java b/src/main/java/dev/cloudeko/zenei/infrastructure/web/mapper/BusinessExceptionMapper.java index f4761a8..38a1f28 100644 --- a/src/main/java/dev/cloudeko/zenei/infrastructure/web/mapper/BusinessExceptionMapper.java +++ b/src/main/java/dev/cloudeko/zenei/infrastructure/web/mapper/BusinessExceptionMapper.java @@ -27,6 +27,7 @@ public BusinessExceptionMapper() { new HashMap, Function>(); handlerMap.put(EmailAlreadyExistsException.class, this::conflict); + handlerMap.put(EmailNotFoundException.class, this::notFound); handlerMap.put(UserNotFoundException.class, this::notFound); handlerMap.put(InvalidPasswordException.class, this::unauthorized); handlerMap.put(InvalidConfirmationTokenException.class, this::unauthorized); diff --git a/src/main/java/dev/cloudeko/zenei/service/AuthenticationService.java b/src/main/java/dev/cloudeko/zenei/service/AuthenticationService.java deleted file mode 100644 index 59393d8..0000000 --- a/src/main/java/dev/cloudeko/zenei/service/AuthenticationService.java +++ /dev/null @@ -1,42 +0,0 @@ -package dev.cloudeko.zenei.service; - -import jakarta.enterprise.context.ApplicationScoped; -import lombok.extern.jbosslog.JBossLog; - -@JBossLog -@ApplicationScoped -public class AuthenticationService { - - /*@Inject - UserService userService; - - @Inject - TokenService tokenService; - - @Inject - RefreshTokenService refreshTokenService; - - @Inject - RefreshTokenRepository refreshTokenRepository; - - @Inject - UserPasswordRepository userPasswordRepository; - - @WithTransaction - public Uni registerAttempt(String name, String email, String password) { - return userService.getUserByEmail(email) - .onItem().ifNotNull().failWith(() -> new BadRequestException("User already exists")) - .onItem().ifNull().switchTo(() -> userService.createUser(name, email, password, null)); - } - - @WithTransaction - public Uni login(String email, String password) { - return userPasswordRepository.findByValidPassword(email, BcryptUtil.bcryptHash(password)) - .onItem().ifNotNull().transformToUni(userPassword -> tokenService.createToken(userPassword.getUser())); - } - - @WithTransaction - public Uni refreshToken(String refreshToken) { - return tokenService.refreshToken(refreshToken); - }*/ -} diff --git a/src/main/java/dev/cloudeko/zenei/service/RefreshTokenService.java b/src/main/java/dev/cloudeko/zenei/service/RefreshTokenService.java deleted file mode 100644 index 6bb7202..0000000 --- a/src/main/java/dev/cloudeko/zenei/service/RefreshTokenService.java +++ /dev/null @@ -1,36 +0,0 @@ -package dev.cloudeko.zenei.service; - -import jakarta.enterprise.context.ApplicationScoped; - -@ApplicationScoped -public class RefreshTokenService { - - /*@Inject - RefreshTokenRepository refreshTokenRepository; - - @WithTransaction - public Uni swapRefreshToken(String refreshToken) { - return refreshTokenRepository.findByValidRefreshToken(refreshToken) - .onItem().ifNotNull().transformToUni(this::finishTokenSwap) - .onItem().ifNull().failWith(new UnauthorizedException("Invalid refresh token")); - } - - private Uni finishTokenSwap(RefreshTokenEntity refreshToken) { - if (refreshToken.isRevoked() || refreshToken.getExpiresAt().isBefore(LocalDateTime.now())) { - return Uni.createFrom().nullItem(); - } - - refreshToken.setRevoked(true); - return createRefreshToken(refreshToken.getUser()); - } - - @WithTransaction - public Uni createRefreshToken(UserEntity user) { - RefreshTokenEntity refreshToken = new RefreshTokenEntity(); - refreshToken.setUser(user); - refreshToken.setExpiresAt(LocalDateTime.now().plusDays(30)); - refreshToken.setToken(BcryptUtil.bcryptHash(user.getEmail() + UUID.randomUUID().toString())); - - return refreshTokenRepository.persist(refreshToken); - }*/ -} diff --git a/src/main/java/dev/cloudeko/zenei/service/TokenService.java b/src/main/java/dev/cloudeko/zenei/service/TokenService.java deleted file mode 100644 index e4ba8ea..0000000 --- a/src/main/java/dev/cloudeko/zenei/service/TokenService.java +++ /dev/null @@ -1,48 +0,0 @@ -package dev.cloudeko.zenei.service; - -import jakarta.enterprise.context.ApplicationScoped; - -@ApplicationScoped -public class TokenService { - - /*@Inject - UserRepository userRepository; - - @Inject - RefreshTokenService refreshTokenService; - - public Uni createToken(UserEntity user) { - return Uni.createFrom().item(issueToken(user)) - .onItem().transformToUni(token -> refreshTokenService.createRefreshToken(user) - .onItem().ifNotNull().transform(refreshToken -> { - token.setRefreshToken(refreshToken.getToken()); - return token; - })); - } - - public Uni refreshToken(String refreshToken) { - return refreshTokenService.swapRefreshToken(refreshToken) - .onItem().ifNotNull().transformToUni(swappedToken -> { - return createToken(swappedToken.getUser()); - }); - } - - private Token issueToken(UserEntity user) { - return issueToken(user.getEmail(), user.getName(), user.isAdmin()); - } - - private Token issueToken(String email, String name, boolean admin) { - String accessToken = Jwt.issuer("https://zenei.cloudeko.dev/") - .subject(email) - .groups(admin ? "admin" : "user") - .claim("name", name) - .sign(); - - Token token = new Token(); - token.setAccessToken(accessToken); - token.setTokenType("bearer"); - token.setExpiresIn(3600); - - return token; - }*/ -} diff --git a/src/main/java/dev/cloudeko/zenei/service/UserService.java b/src/main/java/dev/cloudeko/zenei/service/UserService.java deleted file mode 100644 index 88afc13..0000000 --- a/src/main/java/dev/cloudeko/zenei/service/UserService.java +++ /dev/null @@ -1,47 +0,0 @@ -package dev.cloudeko.zenei.service; - -import jakarta.enterprise.context.ApplicationScoped; - -@ApplicationScoped -public class UserService { - - /*@Inject - UserMapper userMapper; - - @Inject - UserRepository userRepository; - - @Inject - UserPasswordRepository userPasswordRepository; - - public Uni createUser(String name, String email, String password, String image) { - return createUser(name, email, password, image, false); - } - - @WithTransaction - public Uni createUser(String name, String email, String password, String image, boolean admin) { - UserEntity user = new UserEntity(); - user.setName(name); - user.setEmail(email); - user.setImage(image); - user.setAdmin(admin); - - UserPasswordEntity userPassword = new UserPasswordEntity(); - userPassword.setPasswordHash(BcryptUtil.bcryptHash(password)); - - return userRepository.persist(user) - .onItem().ifNotNull().invoke(userPassword::setUser) - .onItem().ifNotNull().transformToUni(u -> userPasswordRepository.persist(userPassword)) - .onItem().ifNotNull().transform(passwordEntity -> userMapper.toDomain(passwordEntity.getUser())); - } - - @WithSession - public Uni getUserByEmail(String email) { - return userRepository.findByEmail(email).onItem().ifNotNull().transform(userMapper::toDomain); - } - - @WithSession - public Uni getUserByName(String name) { - return userRepository.findByName(name).onItem().ifNotNull().transform(userMapper::toDomain); - }*/ -} diff --git a/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowTest.java b/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowTest.java index 97717e8..41513bb 100644 --- a/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowTest.java +++ b/src/test/java/dev/cloudeko/zenei/auth/AuthenticationFlowTest.java @@ -32,7 +32,7 @@ static void setup() { @Test @Order(1) - @DisplayName("Create user via registerAttempt (POST /registerAttempt) should return (200 OK)") + @DisplayName("Create user via email and password (POST /user) should return (200 OK)") void testCreateUser() { given() .contentType(MediaType.APPLICATION_FORM_URLENCODED) @@ -46,13 +46,13 @@ void testCreateUser() { .body( "id", notNullValue(), "username", notNullValue(), - "email", notNullValue() + "primaryEmailAddress", notNullValue() ); } @Test @Order(2) - @DisplayName("Validate user email (POST /verify-email) should return redirect (303 SEE_OTHER)") + @DisplayName("Validate user email (POST /user/verify-email) should return redirect (303 SEE_OTHER)") void testVerifyEmail() { assertEquals(1, mailbox.getTotalMessagesSent()); @@ -81,7 +81,7 @@ void testVerifyEmail() { @Test @Order(3) - @DisplayName("Retrieve a access token using username and password (POST /token) should return (200 OK)") + @DisplayName("Retrieve a access token using username and password (POST /user/token) should return (200 OK)") void testGetAccessToken() { given() .contentType(MediaType.APPLICATION_JSON) @@ -97,9 +97,23 @@ void testGetAccessToken() { ); } + @Test + @Order(3) + @DisplayName("Try to retrieve a access token using invalid username and password (POST /user/token) should return (401 UNAUTHORIZED)") + void testGetAccessTokenInvalid() { + given() + .contentType(MediaType.APPLICATION_JSON) + .queryParam("grant_type", "password") + .queryParam("username", "test@test.com") + .queryParam("password", "invalid-password") + .post("/user/token") + .then() + .statusCode(Response.Status.UNAUTHORIZED.getStatusCode()); + } + @Test @Order(4) - @DisplayName("Retrieve a new access token using refresh token (POST /token) should return (200 OK)") + @DisplayName("Retrieve a new access token using refresh token (POST /user/token) should return (200 OK)") void testGetAccessTokenUsingRefreshToken() { final var token = given() .contentType(MediaType.APPLICATION_JSON) @@ -130,7 +144,7 @@ void testGetAccessTokenUsingRefreshToken() { @Test @Order(5) - @DisplayName("Retrieve a new access token using invalid refresh token (POST /token) should return (401 UNAUTHORIZED)") + @DisplayName("Try to retrieve a new access token using invalid refresh token (POST /user/token) should return (401 UNAUTHORIZED)") void testGetAccessTokenUsingInvalidRefreshToken() { given() .contentType(MediaType.APPLICATION_JSON)