diff --git a/data/giproclientsandfilesdb.mv.db b/data/giproclientsandfilesdb.mv.db new file mode 100644 index 0000000..1051ced Binary files /dev/null and b/data/giproclientsandfilesdb.mv.db differ diff --git a/src/main/java/com/gipro/giprolab/config/Constants.java b/src/main/java/com/gipro/giprolab/config/Constants.java new file mode 100644 index 0000000..0c6842b --- /dev/null +++ b/src/main/java/com/gipro/giprolab/config/Constants.java @@ -0,0 +1,21 @@ +package com.gipro.giprolab.config; + +public class Constants { + public static final String API_1_0 = "/api/1.0/"; + + public static final String MAIL_ACTIVATE = "Account activation"; + public static final String MAIL_RESET = "Password reset"; + public static final int MIN_PASSWORD_LENGTH = 6; + + public static final String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$"; + + public static final String COOKIES_NAME = "JSESSIONID"; + public static final String LOGOUT_URL = "/login?logout"; + public static final String SESSION_ID_ATTR = "sessionId"; + public static final int SESSION_TIMEOUT_SECONDS = 30 * 60; + + public static final String PASSWORD_RESET_REQUEST_PAGE = "/resetRequest"; + public static final String PASSWORD_RESET_PAGE = "/reset"; + + public static final String SYSTEM_ENDPOINT_URL = "/metrics"; +} diff --git a/src/main/java/com/gipro/giprolab/config/PasswordEncoderConfiguration.java b/src/main/java/com/gipro/giprolab/config/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..9a0d06f --- /dev/null +++ b/src/main/java/com/gipro/giprolab/config/PasswordEncoderConfiguration.java @@ -0,0 +1,13 @@ +package com.gipro.giprolab.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/gipro/giprolab/config/SecurityConfiguration.java b/src/main/java/com/gipro/giprolab/config/SecurityConfiguration.java new file mode 100644 index 0000000..7b6c08d --- /dev/null +++ b/src/main/java/com/gipro/giprolab/config/SecurityConfiguration.java @@ -0,0 +1,76 @@ +package com.gipro.giprolab.config; + +import com.gipro.giprolab.models.UserRoleConstants; +import com.gipro.giprolab.services.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) +public class SecurityConfiguration { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + private final UserService userService; + private final BCryptPasswordEncoder bCryptPasswordEncoder; + + public SecurityConfiguration(UserService userService, + BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userService = userService; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + log.debug("Security enabled"); + http + .authorizeHttpRequests((authorize) -> 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; + } +}