diff --git a/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueEmail.java b/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueEmail.java new file mode 100644 index 00000000..4be147c4 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueEmail.java @@ -0,0 +1,26 @@ +package com.zufar.onlinestore.common.validation.annotation; + +import com.zufar.onlinestore.common.validation.validator.UniqueEmailValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD }) +@Retention(RUNTIME) +@Constraint(validatedBy = UniqueEmailValidator.class) +@Documented +public @interface UniqueEmail { + + String message() default ""; + + Class[] groups() default { }; + + Class[] payload() default { }; + +} \ No newline at end of file diff --git a/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueUsername.java b/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueUsername.java new file mode 100644 index 00000000..08d8faa7 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/common/validation/annotation/UniqueUsername.java @@ -0,0 +1,24 @@ +package com.zufar.onlinestore.common.validation.annotation; + +import com.zufar.onlinestore.common.validation.validator.UniqueUsernameValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Target({ FIELD }) +@Retention(RUNTIME) +@Constraint(validatedBy = UniqueUsernameValidator.class) +@Documented +public @interface UniqueUsername { + + String message() default ""; + + Class[] groups() default { }; + + Class[] payload() default { }; +} diff --git a/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueEmailValidator.java b/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueEmailValidator.java new file mode 100644 index 00000000..99c23009 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueEmailValidator.java @@ -0,0 +1,22 @@ +package com.zufar.onlinestore.common.validation.validator; + +import com.zufar.onlinestore.user.repository.UserRepository; +import com.zufar.onlinestore.common.validation.annotation.UniqueEmail; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UniqueEmailValidator implements ConstraintValidator { + + private final UserRepository userCrudRepository; + + @Override + public boolean isValid(String email, ConstraintValidatorContext constraintValidatorContext) { + return userCrudRepository + .findByEmail(email) + .isEmpty(); + } +} diff --git a/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueUsernameValidator.java b/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueUsernameValidator.java new file mode 100644 index 00000000..3bad93f7 --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/common/validation/validator/UniqueUsernameValidator.java @@ -0,0 +1,20 @@ +package com.zufar.onlinestore.common.validation.validator; + +import com.zufar.onlinestore.common.validation.annotation.UniqueUsername; +import com.zufar.onlinestore.user.repository.UserRepository; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class UniqueUsernameValidator implements ConstraintValidator { + + private final UserRepository userCrudRepository; + + @Override + public boolean isValid(String username, ConstraintValidatorContext constraintValidatorContext) { + return userCrudRepository.findUserByUsername(username) == null; + } +} diff --git a/src/main/java/com/zufar/onlinestore/security/dto/registration/UserRegistrationRequest.java b/src/main/java/com/zufar/onlinestore/security/dto/registration/UserRegistrationRequest.java index 2f5c3410..aed6f6df 100644 --- a/src/main/java/com/zufar/onlinestore/security/dto/registration/UserRegistrationRequest.java +++ b/src/main/java/com/zufar/onlinestore/security/dto/registration/UserRegistrationRequest.java @@ -1,5 +1,7 @@ package com.zufar.onlinestore.security.dto.registration; +import com.zufar.onlinestore.common.validation.annotation.UniqueEmail; +import com.zufar.onlinestore.common.validation.annotation.UniqueUsername; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; @@ -15,10 +17,12 @@ public record UserRegistrationRequest( @Size(max = 55, message = "LastName length must be less than 55 characters") String lastName, + @UniqueUsername(message = "User with this username is already registered") @NotBlank(message = "Username is the mandatory attribute") @Size(max = 55, message = "Username length must be less than 55 characters") String username, + @UniqueEmail(message = "User with this email is already registered") @Email(message = "Email should be valid") @NotBlank(message = "Email is the mandatory attribute") String email, diff --git a/src/main/java/com/zufar/onlinestore/user/api/AuthorityService.java b/src/main/java/com/zufar/onlinestore/user/api/AuthorityService.java new file mode 100644 index 00000000..bbc739ad --- /dev/null +++ b/src/main/java/com/zufar/onlinestore/user/api/AuthorityService.java @@ -0,0 +1,19 @@ +package com.zufar.onlinestore.user.api; + +import com.zufar.onlinestore.user.entity.Authority; +import com.zufar.onlinestore.user.entity.UserEntity; +import com.zufar.onlinestore.user.entity.UserGrantedAuthority; +import org.springframework.stereotype.Service; + +@Service +public class AuthorityService { + + public void setDefaultAuthority(UserEntity savedUserEntity) { + UserGrantedAuthority defaultAuthority = UserGrantedAuthority + .builder() + .authority(Authority.USER) + .build(); + + savedUserEntity.addAuthority(defaultAuthority); + } +} diff --git a/src/main/java/com/zufar/onlinestore/user/api/UserService.java b/src/main/java/com/zufar/onlinestore/user/api/UserService.java index c10aac45..6b624038 100644 --- a/src/main/java/com/zufar/onlinestore/user/api/UserService.java +++ b/src/main/java/com/zufar/onlinestore/user/api/UserService.java @@ -19,11 +19,14 @@ public class UserService implements UserApi { private final UserRepository userCrudRepository; private final UserDtoConverter userDtoConverter; + private final AuthorityService authorityService; @Override public UserDto saveUser(final UserDto userDto) { UserEntity userEntity = userDtoConverter.toEntity(userDto); + authorityService.setDefaultAuthority(userEntity); UserEntity userEntityWithId = userCrudRepository.save(userEntity); + return userDtoConverter.toDto(userEntityWithId); } @@ -36,4 +39,5 @@ public UserDto getUserById(final UUID userId) throws UserNotFoundException { } return userDtoConverter.toDto(userEntity.get()); } + } diff --git a/src/main/java/com/zufar/onlinestore/user/converter/UserDtoConverter.java b/src/main/java/com/zufar/onlinestore/user/converter/UserDtoConverter.java index 6740d7d1..d66c51a4 100644 --- a/src/main/java/com/zufar/onlinestore/user/converter/UserDtoConverter.java +++ b/src/main/java/com/zufar/onlinestore/user/converter/UserDtoConverter.java @@ -1,16 +1,10 @@ package com.zufar.onlinestore.user.converter; import com.zufar.onlinestore.user.dto.UserDto; -import com.zufar.onlinestore.user.entity.Authority; import com.zufar.onlinestore.user.entity.UserEntity; -import com.zufar.onlinestore.user.entity.UserGrantedAuthority; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.MappingConstants; -import org.mapstruct.Named; - -import java.util.Collections; -import java.util.Set; @Mapper(componentModel = MappingConstants.ComponentModel.SPRING, uses = AddressDtoConverter.class) public interface UserDtoConverter { @@ -23,32 +17,6 @@ public interface UserDtoConverter { @Mapping(target = "credentialsNonExpired", constant = "true") @Mapping(target = "enabled", constant = "true") @Mapping(target = "address", source = "dto.address", qualifiedByName = "toAddress") - @Mapping(target = "authorities", source = "dto", qualifiedByName = "createAuthorities") UserEntity toEntity(final UserDto dto); - @Named("createAuthorities") - @Mapping(target = "address", source = "dto.address", qualifiedByName = "toAddress") - default Set createAuthorities(UserDto dto) { - UserEntity userEntity = UserEntity.builder() - .firstName(dto.firstName()) - .lastName(dto.lastName()) - .email(dto.email()) - .username(dto.username()) - .password(dto.password()) - .accountNonExpired(true) - .accountNonLocked(true) - .credentialsNonExpired(true) - .enabled(true) - .build(); - - Set authorities = Collections.singleton(UserGrantedAuthority - .builder() - .authority(Authority.USER) - .user(userEntity) - .build()); - - userEntity.setAuthorities(authorities); - - return authorities; - } } diff --git a/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java b/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java index 835b4df9..59af3e20 100644 --- a/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java +++ b/src/main/java/com/zufar/onlinestore/user/entity/UserEntity.java @@ -19,11 +19,11 @@ import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.springframework.security.core.userdetails.UserDetails; - +import java.util.HashSet; +import java.util.Iterator; import java.util.Set; import java.util.UUID; - @Builder @Getter @Setter @@ -44,13 +44,13 @@ public class UserEntity implements UserDetails { @Column(name = "last_name", nullable = false) private String lastName; - @Column(name = "user_name", nullable = false) + @Column(name = "user_name", nullable = false, unique = true) private String username; @Column(name = "stripe_customer_token", nullable = true, unique = true) private String stripeCustomerToken; - @Column(name = "email", nullable = false) + @Column(name = "email", nullable = false, unique = true) private String email; @Column(name = "password", nullable = false) @@ -60,8 +60,9 @@ public class UserEntity implements UserDetails { @JoinColumn(name = "address_id", referencedColumnName = "id") private transient Address address; + @Builder.Default @OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.EAGER) - private Set authorities; + private Set authorities = new HashSet<>(); @Column(name = "account_non_expired", nullable = false) private boolean accountNonExpired; @@ -75,6 +76,21 @@ public class UserEntity implements UserDetails { @Column(name = "enabled", nullable = false) private boolean enabled; + public void addAuthority(UserGrantedAuthority authority) { + this.authorities.add(authority); + authority.setUser(this); + } + + public void removeAuthority(UserGrantedAuthority authority) { + authority.setUser(null); + this.authorities.remove(authority); + } + + public void removeAuthorities() { + authorities.forEach(authority -> authority.setUser(null)); + authorities.clear(); + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/src/main/java/com/zufar/onlinestore/user/entity/UserGrantedAuthority.java b/src/main/java/com/zufar/onlinestore/user/entity/UserGrantedAuthority.java index edffdee8..4ffb14f8 100644 --- a/src/main/java/com/zufar/onlinestore/user/entity/UserGrantedAuthority.java +++ b/src/main/java/com/zufar/onlinestore/user/entity/UserGrantedAuthority.java @@ -12,15 +12,17 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.Builder; import org.springframework.security.core.GrantedAuthority; import java.util.UUID; @Builder @Getter +@Setter @AllArgsConstructor @NoArgsConstructor @Entity diff --git a/src/main/java/com/zufar/onlinestore/user/repository/UserRepository.java b/src/main/java/com/zufar/onlinestore/user/repository/UserRepository.java index 9e64c2a4..5a4ccfde 100644 --- a/src/main/java/com/zufar/onlinestore/user/repository/UserRepository.java +++ b/src/main/java/com/zufar/onlinestore/user/repository/UserRepository.java @@ -3,9 +3,13 @@ import com.zufar.onlinestore.user.entity.UserEntity; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface UserRepository extends JpaRepository { UserEntity findUserByUsername(String username); + + Optional findByEmail(String email); + } diff --git a/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql b/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql index 287ca836..c5d4e1a5 100644 --- a/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql +++ b/src/main/resources/db/changelog/version-1.0/28.07.2023.part4.create-user-details-table.sql @@ -12,7 +12,7 @@ CREATE TABLE user_details account_non_locked BOOLEAN NOT NULL, credentials_non_expired BOOLEAN NOT NULL, enabled BOOLEAN NOT NULL, - UNIQUE(stripe_customer_token), + UNIQUE (user_name, email, stripe_customer_token), CONSTRAINT fk_address FOREIGN KEY (address_id) REFERENCES address (id)