From 9a6ba0b70bc1be7870bcafa567d2a70bdef3c84a Mon Sep 17 00:00:00 2001 From: Anton Romanov Date: Mon, 4 Dec 2023 18:58:10 +0400 Subject: [PATCH] add spring security --- data/giproclientsandfilesdb.mv.db | Bin 0 -> 40960 bytes .../com/gipro/giprolab/config/Constants.java | 21 ++ .../config/PasswordEncoderConfiguration.java | 13 + .../config/SecurityConfiguration.java | 76 +++++ .../com/gipro/giprolab/core/BaseEntity.java | 83 ++++++ .../gipro/giprolab/core/PageableItems.java | 21 ++ .../error/EntityIdIsNullException.java | 6 + .../error/EntityNotFoundException.java | 7 + .../giprolab/error/UserActivationError.java | 7 + .../error/UserEmailExistsException.java | 7 + .../giprolab/error/UserIdExistsException.java | 6 + .../giprolab/error/UserIsUndeadException.java | 7 + .../error/UserLoginExistsException.java | 7 + .../error/UserNotActivatedException.java | 6 + .../giprolab/error/UserNotFoundException.java | 7 + ...rPasswordsNotValidOrNotMatchException.java | 6 + .../giprolab/error/UserResetKeyError.java | 7 + .../java/com/gipro/giprolab/models/User.java | 170 ++++++++++++ .../com/gipro/giprolab/models/UserDto.java | 159 +++++++++++ .../giprolab/models/UserResetPasswordDto.java | 29 ++ .../com/gipro/giprolab/models/UserRole.java | 50 ++++ .../giprolab/models/UserRoleConstants.java | 6 + .../gipro/giprolab/models/UserRoleDto.java | 20 ++ .../giprolab/repositories/UserRepository.java | 28 ++ .../repositories/UserRoleRepository.java | 7 + .../gipro/giprolab/services/UserService.java | 262 ++++++++++++++++++ .../com/gipro/giprolab/util/UserUtils.java | 35 +++ 27 files changed, 1053 insertions(+) create mode 100644 data/giproclientsandfilesdb.mv.db create mode 100644 src/main/java/com/gipro/giprolab/config/Constants.java create mode 100644 src/main/java/com/gipro/giprolab/config/PasswordEncoderConfiguration.java create mode 100644 src/main/java/com/gipro/giprolab/config/SecurityConfiguration.java create mode 100644 src/main/java/com/gipro/giprolab/core/BaseEntity.java create mode 100644 src/main/java/com/gipro/giprolab/core/PageableItems.java create mode 100644 src/main/java/com/gipro/giprolab/error/EntityIdIsNullException.java create mode 100644 src/main/java/com/gipro/giprolab/error/EntityNotFoundException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserActivationError.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserEmailExistsException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserIdExistsException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserIsUndeadException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserLoginExistsException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserNotActivatedException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserNotFoundException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserPasswordsNotValidOrNotMatchException.java create mode 100644 src/main/java/com/gipro/giprolab/error/UserResetKeyError.java create mode 100644 src/main/java/com/gipro/giprolab/models/User.java create mode 100644 src/main/java/com/gipro/giprolab/models/UserDto.java create mode 100644 src/main/java/com/gipro/giprolab/models/UserResetPasswordDto.java create mode 100644 src/main/java/com/gipro/giprolab/models/UserRole.java create mode 100644 src/main/java/com/gipro/giprolab/models/UserRoleConstants.java create mode 100644 src/main/java/com/gipro/giprolab/models/UserRoleDto.java create mode 100644 src/main/java/com/gipro/giprolab/repositories/UserRepository.java create mode 100644 src/main/java/com/gipro/giprolab/repositories/UserRoleRepository.java create mode 100644 src/main/java/com/gipro/giprolab/services/UserService.java create mode 100644 src/main/java/com/gipro/giprolab/util/UserUtils.java diff --git a/data/giproclientsandfilesdb.mv.db b/data/giproclientsandfilesdb.mv.db new file mode 100644 index 0000000000000000000000000000000000000000..1051ced469ac83a5108f2132921e92fc93401692 GIT binary patch literal 40960 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 authorize + //.requestMatchers(UserController.ACTIVATE_URL).permitAll() + .requestMatchers(Constants.PASSWORD_RESET_REQUEST_PAGE).permitAll() + .requestMatchers(Constants.PASSWORD_RESET_PAGE).permitAll() + //.requestMatchers(UserController.URL + UserController.REGISTER_URL).permitAll() + //.requestMatchers(UserController.URL + UserController.ACTIVATE_URL).permitAll() + //.requestMatchers(UserController.URL + UserController.PASSWORD_RESET_REQUEST_URL).permitAll() + //.requestMatchers(UserController.URL + UserController.PASSWORD_RESET_URL).permitAll() + .requestMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN) + .anyRequest().authenticated()) + .formLogin(fl -> fl + .loginPage("/login") + //.successHandler(authenticationSuccessHandler) + .permitAll()) + .csrf(AbstractHttpConfigurer::disable) + .logout(l -> l + //.logoutSuccessHandler(logoutSuccessHandler) + .logoutSuccessUrl(Constants.LOGOUT_URL) + .invalidateHttpSession(false) + .clearAuthentication(true) + .deleteCookies(Constants.COOKIES_NAME) + .permitAll()); + return http.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers("/css/**", "/js/**", "/templates/**", "/webjars/**"); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) { + try { + auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder); + } catch (Exception e) { + throw new BeanInitializationException("Security configuration failed", e); + } + } +} diff --git a/src/main/java/com/gipro/giprolab/core/BaseEntity.java b/src/main/java/com/gipro/giprolab/core/BaseEntity.java new file mode 100644 index 0000000..ec604d7 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/core/BaseEntity.java @@ -0,0 +1,83 @@ +package com.gipro.giprolab.core; + +import jakarta.persistence.*; + +import java.io.Serializable; + +@MappedSuperclass +public abstract class BaseEntity implements Serializable, Comparable { + @Id + @GeneratedValue(strategy = GenerationType.TABLE) + private Integer id; + + @Version + private Integer version; + + public BaseEntity() { + } + + public BaseEntity(Integer id, Integer version) { + this.id = id; + this.version = version; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Integer getVersion() { + return version; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!getClass().isAssignableFrom(obj.getClass())) { + return false; + } + BaseEntity other = (BaseEntity) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (id == null ? 0 : id.hashCode()); + return result; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "id=" + id + + ", version=" + version + + '}'; + } + + @Override + public int compareTo(Object o) { + return id != null ? id.compareTo(((BaseEntity) o).getId()) : -1; + } + + public void reset() { + this.id = null; + this.version = null; + } +} diff --git a/src/main/java/com/gipro/giprolab/core/PageableItems.java b/src/main/java/com/gipro/giprolab/core/PageableItems.java new file mode 100644 index 0000000..c887907 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/core/PageableItems.java @@ -0,0 +1,21 @@ +package com.gipro.giprolab.core; + +import java.util.Collection; + +public class PageableItems { + private final long count; + private final Collection items; + + public PageableItems(long count, Collection items) { + this.count = count; + this.items = items; + } + + public long getCount() { + return count; + } + + public Collection getItems() { + return items; + } +} diff --git a/src/main/java/com/gipro/giprolab/error/EntityIdIsNullException.java b/src/main/java/com/gipro/giprolab/error/EntityIdIsNullException.java new file mode 100644 index 0000000..5bd032a --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/EntityIdIsNullException.java @@ -0,0 +1,6 @@ +package com.gipro.giprolab.error; + +public class EntityIdIsNullException extends RuntimeException { + public EntityIdIsNullException() { + } +} diff --git a/src/main/java/com/gipro/giprolab/error/EntityNotFoundException.java b/src/main/java/com/gipro/giprolab/error/EntityNotFoundException.java new file mode 100644 index 0000000..6abca48 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/EntityNotFoundException.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class EntityNotFoundException extends RuntimeException { + public EntityNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserActivationError.java b/src/main/java/com/gipro/giprolab/error/UserActivationError.java new file mode 100644 index 0000000..0659e7b --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserActivationError.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserActivationError extends RuntimeException { + public UserActivationError(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserEmailExistsException.java b/src/main/java/com/gipro/giprolab/error/UserEmailExistsException.java new file mode 100644 index 0000000..a6dde00 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserEmailExistsException.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserEmailExistsException extends RuntimeException { + public UserEmailExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserIdExistsException.java b/src/main/java/com/gipro/giprolab/error/UserIdExistsException.java new file mode 100644 index 0000000..4ccdc82 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserIdExistsException.java @@ -0,0 +1,6 @@ +package com.gipro.giprolab.error; + +public class UserIdExistsException extends RuntimeException { + public UserIdExistsException() { + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserIsUndeadException.java b/src/main/java/com/gipro/giprolab/error/UserIsUndeadException.java new file mode 100644 index 0000000..f320c7c --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserIsUndeadException.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserIsUndeadException extends RuntimeException { + public UserIsUndeadException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserLoginExistsException.java b/src/main/java/com/gipro/giprolab/error/UserLoginExistsException.java new file mode 100644 index 0000000..91623ce --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserLoginExistsException.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserLoginExistsException extends RuntimeException { + public UserLoginExistsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserNotActivatedException.java b/src/main/java/com/gipro/giprolab/error/UserNotActivatedException.java new file mode 100644 index 0000000..aa6802a --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserNotActivatedException.java @@ -0,0 +1,6 @@ +package com.gipro.giprolab.error; + +public class UserNotActivatedException extends RuntimeException { + public UserNotActivatedException() { + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserNotFoundException.java b/src/main/java/com/gipro/giprolab/error/UserNotFoundException.java new file mode 100644 index 0000000..f1b616a --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserNotFoundException.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserPasswordsNotValidOrNotMatchException.java b/src/main/java/com/gipro/giprolab/error/UserPasswordsNotValidOrNotMatchException.java new file mode 100644 index 0000000..6cd9bcd --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserPasswordsNotValidOrNotMatchException.java @@ -0,0 +1,6 @@ +package com.gipro.giprolab.error; + +public class UserPasswordsNotValidOrNotMatchException extends RuntimeException { + public UserPasswordsNotValidOrNotMatchException() { + } +} diff --git a/src/main/java/com/gipro/giprolab/error/UserResetKeyError.java b/src/main/java/com/gipro/giprolab/error/UserResetKeyError.java new file mode 100644 index 0000000..54826c2 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/error/UserResetKeyError.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.error; + +public class UserResetKeyError extends RuntimeException { + public UserResetKeyError(String message) { + super(message); + } +} diff --git a/src/main/java/com/gipro/giprolab/models/User.java b/src/main/java/com/gipro/giprolab/models/User.java new file mode 100644 index 0000000..960b348 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/User.java @@ -0,0 +1,170 @@ +package com.gipro.giprolab.models; + +import com.gipro.giprolab.config.Constants; +import com.gipro.giprolab.core.BaseEntity; +import jakarta.persistence.*; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.Size; + +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +@Entity +@Table(name = "is_users") +public class User extends BaseEntity { + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash", length = 60, nullable = false) + private String password; + + @NotNull + @Size(max = 50) + @Column(name = "first_name", length = 50, nullable = false) + private String firstName; + + @NotNull + @Size(max = 50) + @Column(name = "last_name", length = 50, nullable = false) + private String lastName; + + @NotNull + @Email + @Size(min = 5, max = 100) + @Column(length = 100, nullable = false, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + private String activationKey; + + @Column(name = "activation_date") + @Temporal(TemporalType.TIMESTAMP) + private Date activationDate; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + private String resetKey; + + @Column(name = "reset_date") + @Temporal(TemporalType.TIMESTAMP) + private Date resetDate; + + @ManyToMany + @JoinTable( + name = "is_user_role", + joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "user_role_name", referencedColumnName = "name")}) + private Set roles; + + public User() { + roles = new HashSet<>(); + activated = false; + activationDate = new Date(); + resetDate = null; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login.toLowerCase(); + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean getActivated() { + return activated; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public Date getActivationDate() { + return activationDate; + } + + public void setActivationDate(Date activationDate) { + this.activationDate = activationDate; + } + + public String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public Date getResetDate() { + return resetDate; + } + + public void setResetDate(Date resetDate) { + this.resetDate = resetDate; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } +} diff --git a/src/main/java/com/gipro/giprolab/models/UserDto.java b/src/main/java/com/gipro/giprolab/models/UserDto.java new file mode 100644 index 0000000..1461425 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/UserDto.java @@ -0,0 +1,159 @@ +package com.gipro.giprolab.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.gipro.giprolab.config.Constants; +import jakarta.validation.constraints.*; +import org.springframework.util.StringUtils; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + + +public class UserDto { + private Integer id; + + @NotEmpty + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 4, max = 50) + private String login; + + @NotBlank + @Size(min = 2, max = 50) + private String firstName; + + @NotBlank + @Size(min = 2, max = 50) + private String lastName; + + @Email + @NotBlank + @Size(min = 5, max = 100) + private String email; + + private boolean activated; + + private LinkedHashSet roles; + + @Size(max = 50) + private String oldPassword; + + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String password; + + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String passwordConfirm; + + public UserDto() { + activated = false; + roles = new LinkedHashSet<>(); + } + + public UserDto(User user) { + this(); + this.id = user.getId(); + this.login = user.getLogin(); + this.firstName = user.getFirstName(); + this.lastName = user.getLastName(); + this.email = user.getEmail(); + this.activated = user.getActivated(); + this.roles.addAll(user.getRoles().stream() + .map(UserRoleDto::new) + .collect(Collectors.toList())); + } + + public Integer getId() { + return id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Collection roles) { + this.roles.clear(); + this.roles.addAll(roles); + } + + public String getOldPassword() { + return oldPassword; + } + + public String getPassword() { + return password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + @JsonIgnore + public boolean isPasswordsValid() { + if (StringUtils.isEmpty(password) || StringUtils.isEmpty(passwordConfirm)) { + return false; + } + return Objects.equals(password, passwordConfirm); + } + + @JsonIgnore + public boolean isOldPasswordValid() { + return !StringUtils.isEmpty(oldPassword); + } + + @Override + public String toString() { + return getClass().getSimpleName() + " {" + + "id=" + id + + ", login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", activated=" + activated + + ", roles=" + roles + + ", password='" + password + '\'' + + ", passwordConfirm='" + passwordConfirm + '\'' + + '}'; + } +} diff --git a/src/main/java/com/gipro/giprolab/models/UserResetPasswordDto.java b/src/main/java/com/gipro/giprolab/models/UserResetPasswordDto.java new file mode 100644 index 0000000..0458684 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/UserResetPasswordDto.java @@ -0,0 +1,29 @@ +package com.gipro.giprolab.models; + +import com.gipro.giprolab.config.Constants; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; + +import java.util.Objects; + +public class UserResetPasswordDto { + @NotEmpty + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String password; + + @NotEmpty + @Size(min = Constants.MIN_PASSWORD_LENGTH, max = 50) + private String passwordConfirm; + + public String getPassword() { + return password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public boolean isPasswordsValid() { + return Objects.equals(password, passwordConfirm); + } +} diff --git a/src/main/java/com/gipro/giprolab/models/UserRole.java b/src/main/java/com/gipro/giprolab/models/UserRole.java new file mode 100644 index 0000000..ebb0881 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/UserRole.java @@ -0,0 +1,50 @@ +package com.gipro.giprolab.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@Entity +@Table(name = "is_user_roles") +public class UserRole { + @Id + @NotNull + @Size(max = 50) + @Column(length = 50, nullable = false) + private String name; + + public UserRole() { + } + + public UserRole(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserRole role = (UserRole) o; + return !(name != null ? !name.equals(role.name) : role.name != null); + } + + public void setName(String name) { + this.name = name; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } +} diff --git a/src/main/java/com/gipro/giprolab/models/UserRoleConstants.java b/src/main/java/com/gipro/giprolab/models/UserRoleConstants.java new file mode 100644 index 0000000..0796dfc --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/UserRoleConstants.java @@ -0,0 +1,6 @@ +package com.gipro.giprolab.models; + +public class UserRoleConstants { + public static final String ADMIN = "ROLE_ADMIN"; + public static final String USER = "ROLE_USER"; +} diff --git a/src/main/java/com/gipro/giprolab/models/UserRoleDto.java b/src/main/java/com/gipro/giprolab/models/UserRoleDto.java new file mode 100644 index 0000000..243ccc2 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/models/UserRoleDto.java @@ -0,0 +1,20 @@ +package com.gipro.giprolab.models; + +public class UserRoleDto { + private String id; + + public UserRoleDto() { + } + + public UserRoleDto(UserRole role) { + this.id = role.getName(); + } + + public UserRoleDto(String name) { + this.id = name; + } + + public String getId() { + return id; + } +} diff --git a/src/main/java/com/gipro/giprolab/repositories/UserRepository.java b/src/main/java/com/gipro/giprolab/repositories/UserRepository.java new file mode 100644 index 0000000..1948a8b --- /dev/null +++ b/src/main/java/com/gipro/giprolab/repositories/UserRepository.java @@ -0,0 +1,28 @@ +package com.gipro.giprolab.repositories; + +import com.gipro.giprolab.models.User; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Date; +import java.util.List; + +public interface UserRepository extends JpaRepository { + User findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndActivationDateBefore(Date date); + + User findOneByResetKey(String resetKey); + + List findAllByResetKeyNotNullAndResetDateBefore(Date date); + + User findOneByEmailIgnoreCase(String email); + + User findOneByLoginIgnoreCase(String login); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesById(int id); + + @EntityGraph(attributePaths = "roles") + User findOneWithRolesByLogin(String login); +} diff --git a/src/main/java/com/gipro/giprolab/repositories/UserRoleRepository.java b/src/main/java/com/gipro/giprolab/repositories/UserRoleRepository.java new file mode 100644 index 0000000..334b493 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/repositories/UserRoleRepository.java @@ -0,0 +1,7 @@ +package com.gipro.giprolab.repositories; + +import com.gipro.giprolab.models.UserRole; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRoleRepository extends JpaRepository { +} diff --git a/src/main/java/com/gipro/giprolab/services/UserService.java b/src/main/java/com/gipro/giprolab/services/UserService.java new file mode 100644 index 0000000..eecfa01 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/services/UserService.java @@ -0,0 +1,262 @@ +package com.gipro.giprolab.services; + +import com.gipro.giprolab.core.BaseEntity; +import com.gipro.giprolab.core.PageableItems; +import com.gipro.giprolab.error.*; +import com.gipro.giprolab.models.*; +import com.gipro.giprolab.repositories.UserRepository; +import com.gipro.giprolab.repositories.UserRoleRepository; +import com.gipro.giprolab.util.UserUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +@Transactional +public class UserService implements UserDetailsService { + private final Logger log = LoggerFactory.getLogger(UserService.class); + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final UserRoleRepository userRoleRepository; + + private final UserMapper userMapper; + + public UserService(UserRepository userRepository, + PasswordEncoder passwordEncoder, + UserRoleRepository userRoleRepository, + UserMapper userMapper) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.userRoleRepository = userRoleRepository; + this.userMapper = userMapper; + } + + private User getUserByEmail(String email) { + return userRepository.findOneByEmailIgnoreCase(email); + } + + private User getUserByActivationKey(String activationKey) { + return userRepository.findOneByActivationKey(activationKey); + } + + public User getUserByLogin(String login) { + return userRepository.findOneByLoginIgnoreCase(login); + } + + @Transactional(readOnly = true) + public UserDto getUserWithRolesById(Integer userId) { + final User userEntity = userRepository.findOneWithRolesById(userId); + if (userEntity == null) { + throw new UserNotFoundException(userId.toString()); + } + return userMapper.userEntityToUserDto(userEntity); + } + + + @Transactional(readOnly = true) + public PageableItems getUserRoles() { + final List roles = userRoleRepository.findAll(); + return new PageableItems<>(roles.size(), roles); + } + + public UserDto createUser(UserDto userDto) { + if (userDto.getId() != null) { + throw new UserIdExistsException(); + } + if (getUserByLogin(userDto.getLogin()) != null) { + throw new UserLoginExistsException(userDto.getLogin()); + } + if (getUserByEmail(userDto.getEmail()) != null) { + throw new UserEmailExistsException(userDto.getEmail()); + } + if (!userDto.isPasswordsValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + User user = userMapper.userDtoToUserEntity(userDto); + user.setActivated(false); + user.setActivationKey(UserUtils.generateActivationKey()); + user.setRoles(Collections.singleton(new UserRole(UserRoleConstants.USER))); + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + user = userRepository.save(user); + //TODO: mailService.sendActivationEmail(user); + log.debug("Created Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto activateUser(String activationKey) { + final User user = getUserByActivationKey(activationKey); + if (user == null) { + throw new UserActivationError(activationKey); + } + user.setActivated(true); + user.setActivationKey(null); + user.setActivationDate(null); + log.debug("Activated user: {}", user.getLogin()); + return userMapper.userEntityToUserDto(userRepository.save(user)); + } + + public UserDto updateUser(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!Objects.equals( + Optional.ofNullable(getUserByEmail(userDto.getEmail())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserEmailExistsException(userDto.getEmail()); + } + if (!Objects.equals( + Optional.ofNullable(getUserByLogin(userDto.getLogin())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserLoginExistsException(userDto.getLogin()); + } + User user = userRepository.findById(userDto.getId()).orElse(null); + if (user == null) { + throw new UserNotFoundException(userDto.getId().toString()); + } + user.setLogin(userDto.getLogin()); + user.setFirstName(userDto.getFirstName()); + user.setLastName(userDto.getLastName()); + user.setEmail(userDto.getEmail()); + if (userDto.isActivated() != user.getActivated()) { + if (userDto.isActivated()) { + user.setActivationKey(null); + user.setActivationDate(null); + } else { + user.setActivationKey(UserUtils.generateActivationKey()); + user.setActivationDate(new Date()); + } + } + user.setActivated(userDto.isActivated()); + final Set roles = userMapper.rolesFromDto(userDto.getRoles()); + user.setRoles(roles.isEmpty() + ? Collections.singleton(new UserRole(UserRoleConstants.USER)) + : roles); + if (!StringUtils.isEmpty(userDto.getOldPassword())) { + if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + log.debug("Changed password for User: {}", user.getLogin()); + } + user = userRepository.save(user); + log.debug("Changed Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto updateUserInformation(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!Objects.equals( + Optional.ofNullable(getUserByEmail(userDto.getEmail())) + .map(BaseEntity::getId).orElse(userDto.getId()), + userDto.getId())) { + throw new UserEmailExistsException(userDto.getEmail()); + } + User user = userRepository.findById(userDto.getId()).orElse(null); + if (user == null) { + throw new UserNotFoundException(userDto.getId().toString()); + } + user.setFirstName(userDto.getFirstName()); + user.setLastName(userDto.getLastName()); + user.setEmail(userDto.getEmail()); + user = userRepository.save(user); + log.debug("Updated Information for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + public UserDto changeUserPassword(UserDto userDto) { + if (userDto.getId() == null) { + throw new EntityIdIsNullException(); + } + if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + final User user = getCurrentUser(); + + if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + user.setPassword(passwordEncoder.encode(userDto.getPassword())); + log.debug("Changed password for User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(userRepository.save(user)); + } + + public User getCurrentUser() { + String login = UserUtils.getCurrentUserLogin(); + User user = userRepository.findOneByLoginIgnoreCase(login); + if (user == null) { + throw new UserNotFoundException(login); + } + return user; + } + + public boolean requestUserPasswordReset(String email) { + User user = userRepository.findOneByEmailIgnoreCase(email); + if (user == null) { + throw new UserNotFoundException(email); + } + if (!user.getActivated()) { + throw new UserNotActivatedException(); + } + user.setResetKey(UserUtils.generateResetKey()); + user.setResetDate(new Date()); + user = userRepository.save(user); + // mailService.sendPasswordResetMail(user); + log.debug("Created Reset Password Request for User: {}", user.getLogin()); + return true; + } + + public boolean completeUserPasswordReset(String key, UserResetPasswordDto userResetPasswordDto) { + if (!userResetPasswordDto.isPasswordsValid()) { + throw new UserPasswordsNotValidOrNotMatchException(); + } + User user = userRepository.findOneByResetKey(key); + if (user == null) { + throw new UserResetKeyError(key); + } + user.setPassword(passwordEncoder.encode(userResetPasswordDto.getPassword())); + user.setResetKey(null); + user.setResetDate(null); + user = userRepository.save(user); + log.debug("Reset Password for User: {}", user.getLogin()); + return true; + } + + public UserDto deleteUser(Integer userId) { + final User user = userRepository.findById(userId).orElse(null); + userRepository.delete(user); + log.debug("Deleted User: {}", user.getLogin()); + return userMapper.userEntityToUserDto(user); + } + + @Override + public UserDetails loadUserByUsername(String username) { + final User user = userRepository.findOneByLoginIgnoreCase(username); + if (user == null) { + throw new UserNotFoundException(username); + } + if (!user.getActivated()) { + throw new UserNotActivatedException(); + } + return new org.springframework.security.core.userdetails.User(user.getLogin(), + user.getPassword(), + Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream() + .map(role -> new SimpleGrantedAuthority(role.getName())) + .collect(Collectors.toList())); + } +} diff --git a/src/main/java/com/gipro/giprolab/util/UserUtils.java b/src/main/java/com/gipro/giprolab/util/UserUtils.java new file mode 100644 index 0000000..cfacd57 --- /dev/null +++ b/src/main/java/com/gipro/giprolab/util/UserUtils.java @@ -0,0 +1,35 @@ +package com.gipro.giprolab.util; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; + +public class UserUtils { + private static final int DEF_COUNT = 20; + + public static String generateActivationKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + public static String generateResetKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + public static String getCurrentUserLogin() { + final SecurityContext securityContext = SecurityContextHolder.getContext(); + if (securityContext == null) { + return null; + } + final Authentication authentication = securityContext.getAuthentication(); + if (authentication.getPrincipal() instanceof UserDetails) { + final UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal(); + return springSecurityUser.getUsername(); + } + if (authentication.getPrincipal() instanceof String) { + return (String) authentication.getPrincipal(); + } + return null; + } +}