From 8ad694b12351af935a6c7d464042554ab4820c53 Mon Sep 17 00:00:00 2001 From: "m.dalakov" Date: Tue, 20 Feb 2018 15:30:08 +0200 Subject: [PATCH] [SECARSP-111] +Auth API [SECARSP-115 +Get Devices] Change-Id: Ia891c87e1569ec43eccdc11a626fe3c11aa5aba8 --- server/.gitignore | 2 + server/pom.xml | 11 +- .../java/com/samsung/samserver/SamserverApp.java | 2 +- .../config/MicroserviceSecurityConfiguration.java | 39 ++- .../com/samsung/samserver/domain/Authority.java | 65 +++++ .../java/com/samsung/samserver/domain/User.java | 236 +++++++++++++++++ .../samserver/repository/AuthorityRepository.java | 16 ++ .../samserver/repository/UserRepository.java | 44 ++++ .../security/DomainUserDetailsService.java | 81 ++++++ .../com/samsung/samserver/service/dto/UserDTO.java | 200 ++++++++++++++ .../samsung/samserver/service/dto/UserMapper.java | 79 ++++++ .../samserver/service/impl/MailService.java | 109 ++++++++ .../samserver/service/impl/UserService.java | 286 +++++++++++++++++++++ .../samsung/samserver/web/rest/UserResource.java | 194 ++++++++++++++ .../web/rest/controller/IRestDashboard.java | 80 ++++++ .../samserver/web/rest/controller/IRestDevice.java | 3 - .../web/rest/controller/impl/RestDashboard.java | 72 ++++++ .../web/rest/controller/{ => impl}/RestDevice.java | 3 +- .../samserver/web/rest/service/AccountService.java | 235 +++++++++++++++++ .../samserver/web/rest/service/UserJWTService.java | 122 +++++++++ server/src/main/resources/.h2.server.properties | 2 +- .../resources/config/liquibase/authorities.csv | 3 + .../changelog/00000000000000_initial_schema.xml | 95 ++++++- .../src/main/resources/config/liquibase/users.csv | 5 + .../config/liquibase/users_authorities.csv | 6 + 25 files changed, 1977 insertions(+), 13 deletions(-) create mode 100644 server/src/main/java/com/samsung/samserver/domain/Authority.java create mode 100644 server/src/main/java/com/samsung/samserver/domain/User.java create mode 100644 server/src/main/java/com/samsung/samserver/repository/AuthorityRepository.java create mode 100644 server/src/main/java/com/samsung/samserver/repository/UserRepository.java create mode 100644 server/src/main/java/com/samsung/samserver/security/DomainUserDetailsService.java create mode 100644 server/src/main/java/com/samsung/samserver/service/dto/UserDTO.java create mode 100644 server/src/main/java/com/samsung/samserver/service/dto/UserMapper.java create mode 100644 server/src/main/java/com/samsung/samserver/service/impl/MailService.java create mode 100644 server/src/main/java/com/samsung/samserver/service/impl/UserService.java create mode 100644 server/src/main/java/com/samsung/samserver/web/rest/UserResource.java create mode 100644 server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDashboard.java create mode 100644 server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDashboard.java rename server/src/main/java/com/samsung/samserver/web/rest/controller/{ => impl}/RestDevice.java (94%) create mode 100644 server/src/main/java/com/samsung/samserver/web/rest/service/AccountService.java create mode 100644 server/src/main/java/com/samsung/samserver/web/rest/service/UserJWTService.java create mode 100644 server/src/main/resources/config/liquibase/authorities.csv create mode 100644 server/src/main/resources/config/liquibase/users.csv create mode 100644 server/src/main/resources/config/liquibase/users_authorities.csv diff --git a/server/.gitignore b/server/.gitignore index c2cb540..f70b10c 100644 --- a/server/.gitignore +++ b/server/.gitignore @@ -5,6 +5,8 @@ /src/test/javascript/coverage/ /src/test/javascript/PhantomJS*/ +/src/main/resources/.h2.server.properties + ###################### # Node ###################### diff --git a/server/pom.xml b/server/pom.xml index e2fa264..aff4ed5 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -57,7 +57,7 @@ 1.3.4 0.7.9 3.2.2 - 3.2 + 3.4.0.905 src/main/webapp/content/**/*.*, src/main/webapp/i18n/*.js, target/www/**/*.* @@ -82,6 +82,7 @@ 1.11 2.8.0 + 2.9.4 @@ -336,6 +337,14 @@ ${apache.commons-codec.version} + + + com.fasterxml.jackson.dataformat + jackson-dataformats-binary + ${jackson-dataformats-binary.version} + pom + + diff --git a/server/src/main/java/com/samsung/samserver/SamserverApp.java b/server/src/main/java/com/samsung/samserver/SamserverApp.java index 2ad581e..cc26c39 100644 --- a/server/src/main/java/com/samsung/samserver/SamserverApp.java +++ b/server/src/main/java/com/samsung/samserver/SamserverApp.java @@ -68,7 +68,7 @@ public class SamserverApp { "run with both the 'dev' and 'cloud' profiles at the same time."); } managementDocket.enable(false); - apiDocket.select().paths(PathSelectors.regex("/api/device-service.*")).build(); + apiDocket.select().paths(PathSelectors.regex("/api/device-service.*|/dashboard.*")).build(); } /** diff --git a/server/src/main/java/com/samsung/samserver/config/MicroserviceSecurityConfiguration.java b/server/src/main/java/com/samsung/samserver/config/MicroserviceSecurityConfiguration.java index bb0ff25..bbbe86c 100644 --- a/server/src/main/java/com/samsung/samserver/config/MicroserviceSecurityConfiguration.java +++ b/server/src/main/java/com/samsung/samserver/config/MicroserviceSecurityConfiguration.java @@ -20,6 +20,14 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension; import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.web.filter.CorsFilter; +import javax.annotation.PostConstruct; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + @Configuration @Import(SecurityProblemSupport.class) @EnableWebSecurity @@ -30,11 +38,36 @@ public class MicroserviceSecurityConfiguration extends WebSecurityConfigurerAdap private final SecurityProblemSupport problemSupport; - public MicroserviceSecurityConfiguration(TokenProvider tokenProvider, SecurityProblemSupport problemSupport) { + private final AuthenticationManagerBuilder authenticationManagerBuilder; + + private final UserDetailsService userDetailsService; + + private final CorsFilter corsFilter; + + public MicroserviceSecurityConfiguration(AuthenticationManagerBuilder authenticationManagerBuilder, UserDetailsService userDetailsService,TokenProvider tokenProvider,CorsFilter corsFilter, SecurityProblemSupport problemSupport) { + this.authenticationManagerBuilder = authenticationManagerBuilder; + this.userDetailsService = userDetailsService; this.tokenProvider = tokenProvider; + this.corsFilter = corsFilter; this.problemSupport = problemSupport; } + @PostConstruct + public void init() { + try { + authenticationManagerBuilder + .userDetailsService(userDetailsService) + .passwordEncoder(passwordEncoder()); + } catch (Exception e) { + throw new BeanInitializationException("Security configuration failed", e); + } + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + @Override public void configure(WebSecurity web) throws Exception { web.ignoring() @@ -67,7 +100,9 @@ public class MicroserviceSecurityConfiguration extends WebSecurityConfigurerAdap .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() - //.antMatchers("/api/**").authenticated() + .antMatchers("/dashboard/auth/login").permitAll() + .antMatchers("/dashboard/auth/register").permitAll() + .antMatchers("/dashboard/**").authenticated() .antMatchers("/management/health").permitAll() .antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN) .antMatchers("/swagger-resources/configuration/ui").permitAll() diff --git a/server/src/main/java/com/samsung/samserver/domain/Authority.java b/server/src/main/java/com/samsung/samserver/domain/Authority.java new file mode 100644 index 0000000..6ab74d5 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/domain/Authority.java @@ -0,0 +1,65 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.domain; + +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Column; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.io.Serializable; + +/** + * An authority (a security role) used by Spring Security. + */ +@Entity +@Table(name = "jhi_authority") + +public class Authority implements Serializable { + + private static final long serialVersionUID = 1L; + + @NotNull + @Size(max = 50) + @Id + @Column(length = 50) + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Authority authority = (Authority) o; + + return !(name != null ? !name.equals(authority.name) : authority.name != null); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "Authority{" + + "name='" + name + '\'' + + "}"; + } +} diff --git a/server/src/main/java/com/samsung/samserver/domain/User.java b/server/src/main/java/com/samsung/samserver/domain/User.java new file mode 100644 index 0000000..1827fc6 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/domain/User.java @@ -0,0 +1,236 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.domain; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.samsung.samserver.config.Constants; +import org.apache.commons.lang3.StringUtils; +import org.hibernate.annotations.BatchSize; +import org.hibernate.validator.constraints.Email; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Pattern; +import javax.validation.constraints.Size; +import java.io.Serializable; +import java.util.HashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import java.time.Instant; + +/** + * A user. + */ +@Entity +@Table(name = "jhi_user") + +public class User extends AbstractAuditingEntity implements Serializable { + + private static final long serialVersionUID = 1L; + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator") + @SequenceGenerator(name = "sequenceGenerator") + private Long id; + + @NotNull + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + @Column(length = 50, unique = true, nullable = false) + private String login; + + @JsonIgnore + @NotNull + @Size(min = 60, max = 60) + @Column(name = "password_hash", length = 60) + private String password; + + @Size(max = 50) + @Column(name = "first_name", length = 50) + private String firstName; + + @Size(max = 50) + @Column(name = "last_name", length = 50) + private String lastName; + + @Email + @Size(min = 5, max = 100) + @Column(length = 100, unique = true) + private String email; + + @NotNull + @Column(nullable = false) + private boolean activated = false; + + @Size(min = 2, max = 6) + @Column(name = "lang_key", length = 6) + private String langKey; + + @Size(max = 256) + @Column(name = "image_url", length = 256) + private String imageUrl; + + @Size(max = 20) + @Column(name = "activation_key", length = 20) + @JsonIgnore + private String activationKey; + + @Size(max = 20) + @Column(name = "reset_key", length = 20) + @JsonIgnore + private String resetKey; + + @Column(name = "reset_date") + private Instant resetDate = null; + + @JsonIgnore + @ManyToMany + @JoinTable( + name = "jhi_user_authority", + joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")}, + inverseJoinColumns = {@JoinColumn(name = "authority_name", referencedColumnName = "name")}) + + @BatchSize(size = 20) + private Set authorities = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLogin() { + return login; + } + + // Lowercase the login before saving it in database + public void setLogin(String login) { + this.login = StringUtils.lowerCase(login, Locale.ENGLISH); + } + + 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 String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean getActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getActivationKey() { + return activationKey; + } + + public void setActivationKey(String activationKey) { + this.activationKey = activationKey; + } + + public String getResetKey() { + return resetKey; + } + + public void setResetKey(String resetKey) { + this.resetKey = resetKey; + } + + public Instant getResetDate() { + return resetDate; + } + + public void setResetDate(Instant resetDate) { + this.resetDate = resetDate; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + User user = (User) o; + return !(user.getId() == null || getId() == null) && Objects.equals(getId(), user.getId()); + } + + @Override + public int hashCode() { + return Objects.hashCode(getId()); + } + + @Override + public String toString() { + return "User{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated='" + activated + '\'' + + ", langKey='" + langKey + '\'' + + ", activationKey='" + activationKey + '\'' + + "}"; + } +} diff --git a/server/src/main/java/com/samsung/samserver/repository/AuthorityRepository.java b/server/src/main/java/com/samsung/samserver/repository/AuthorityRepository.java new file mode 100644 index 0000000..b06cb87 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/repository/AuthorityRepository.java @@ -0,0 +1,16 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.repository; + + +import com.samsung.samserver.domain.Authority; +import org.springframework.data.jpa.repository.JpaRepository; + +/** + * Spring Data JPA repository for the Authority entity. + */ +public interface AuthorityRepository extends JpaRepository { +} diff --git a/server/src/main/java/com/samsung/samserver/repository/UserRepository.java b/server/src/main/java/com/samsung/samserver/repository/UserRepository.java new file mode 100644 index 0000000..2ead8d4 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/repository/UserRepository.java @@ -0,0 +1,44 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.repository; + +import com.samsung.samserver.domain.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; +import java.util.Optional; +import java.time.Instant; + +/** + * Spring Data JPA repository for the User entity. + */ +@Repository +public interface UserRepository extends JpaRepository { + + Optional findOneByActivationKey(String activationKey); + + List findAllByActivatedIsFalseAndCreatedDateBefore(Instant dateTime); + + Optional findOneByResetKey(String resetKey); + + Optional findOneByEmailIgnoreCase(String email); + + Optional findOneByLogin(String login); + + @EntityGraph(attributePaths = "authorities") + Optional findOneWithAuthoritiesById(Long id); + + @EntityGraph(attributePaths = "authorities") + Optional findOneWithAuthoritiesByLogin(String login); + + @EntityGraph(attributePaths = "authorities") + Optional findOneWithAuthoritiesByEmail(String email); + + Page findAllByLoginNot(Pageable pageable, String login); +} diff --git a/server/src/main/java/com/samsung/samserver/security/DomainUserDetailsService.java b/server/src/main/java/com/samsung/samserver/security/DomainUserDetailsService.java new file mode 100644 index 0000000..a209ec3 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/security/DomainUserDetailsService.java @@ -0,0 +1,81 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.security; + + +import com.samsung.samserver.domain.User; +import com.samsung.samserver.repository.UserRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +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.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Authenticate a user from the database. + */ +@Component("userDetailsService") +public class DomainUserDetailsService implements UserDetailsService { + + private final Logger log = LoggerFactory.getLogger(DomainUserDetailsService.class); + + private final UserRepository userRepository; + + public DomainUserDetailsService(UserRepository userRepository) { + this.userRepository = userRepository; + } + + @Override + @Transactional + public UserDetails loadUserByUsername(final String login) { + log.debug("Authenticating {}", login); + String lowercaseLogin = login.toLowerCase(Locale.ENGLISH); + Optional userByEmailFromDatabase = userRepository.findOneWithAuthoritiesByEmail(lowercaseLogin); + return userByEmailFromDatabase.map(user -> createSpringSecurityUser(lowercaseLogin, user)).orElseGet(() -> { + Optional userByLoginFromDatabase = userRepository.findOneWithAuthoritiesByLogin(lowercaseLogin); + return userByLoginFromDatabase.map(user -> createSpringSecurityUser(lowercaseLogin, user)) + .orElseThrow(() -> new UsernameNotFoundException("User " + lowercaseLogin + " was not found in the " + + "database")); + }); + } + + private org.springframework.security.core.userdetails.User createSpringSecurityUser(String lowercaseLogin, User user) { + if (!user.getActivated()) { + throw new UserNotActivatedException("User " + lowercaseLogin + " was not activated"); + } + List grantedAuthorities = user.getAuthorities().stream() + .map(authority -> new SimpleGrantedAuthority(authority.getName())) + .collect(Collectors.toList()); + return new org.springframework.security.core.userdetails.User(user.getLogin(), + user.getPassword(), + grantedAuthorities); + } + + + /** + * This exception is thrown in case of a not activated user trying to authenticate. + */ + public static class UserNotActivatedException extends AuthenticationException { + + private static final long serialVersionUID = 1L; + + public UserNotActivatedException(String message) { + super(message); + } + + public UserNotActivatedException(String message, Throwable t) { + super(message, t); + } + } +} diff --git a/server/src/main/java/com/samsung/samserver/service/dto/UserDTO.java b/server/src/main/java/com/samsung/samserver/service/dto/UserDTO.java new file mode 100644 index 0000000..9820909 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/service/dto/UserDTO.java @@ -0,0 +1,200 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.service.dto; + + + +import com.samsung.samserver.config.Constants; +import com.samsung.samserver.domain.Authority; +import com.samsung.samserver.domain.User; +import org.hibernate.validator.constraints.Email; +import org.hibernate.validator.constraints.NotBlank; + +import javax.validation.constraints.*; +import java.time.Instant; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * A DTO representing a user, with his authorities. + */ +public class UserDTO { + + private Long id; + + @NotBlank + @Pattern(regexp = Constants.LOGIN_REGEX) + @Size(min = 1, max = 50) + private String login; + + @Size(max = 50) + private String firstName; + + @Size(max = 50) + private String lastName; + + @Email + @Size(min = 5, max = 100) + private String email; + + @Size(max = 256) + private String imageUrl; + + private boolean activated = false; + + @Size(min = 2, max = 6) + private String langKey; + + private String createdBy; + + private Instant createdDate; + + private String lastModifiedBy; + + private Instant lastModifiedDate; + + private Set authorities; + + public UserDTO() { + // Empty constructor needed for Jackson. + } + + public UserDTO(User user) { + 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.imageUrl = user.getImageUrl(); + this.langKey = user.getLangKey(); + this.authorities = user.getAuthorities().stream() + .map(Authority::getName) + .collect(Collectors.toSet()); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = 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 String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getLangKey() { + return langKey; + } + + public void setLangKey(String langKey) { + this.langKey = langKey; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedDate() { + return createdDate; + } + + public void setCreatedDate(Instant createdDate) { + this.createdDate = createdDate; + } + + public String getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(String lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public Instant getLastModifiedDate() { + return lastModifiedDate; + } + + public void setLastModifiedDate(Instant lastModifiedDate) { + this.lastModifiedDate = lastModifiedDate; + } + + public Set getAuthorities() { + return authorities; + } + + public void setAuthorities(Set authorities) { + this.authorities = authorities; + } + + @Override + public String toString() { + return "UserDTO{" + + "login='" + login + '\'' + + ", firstName='" + firstName + '\'' + + ", lastName='" + lastName + '\'' + + ", email='" + email + '\'' + + ", imageUrl='" + imageUrl + '\'' + + ", activated=" + activated + + ", langKey='" + langKey + '\'' + + ", createdBy=" + createdBy + + ", createdDate=" + createdDate + + ", lastModifiedBy='" + lastModifiedBy + '\'' + + ", lastModifiedDate=" + lastModifiedDate + + ", authorities=" + authorities + + "}"; + } +} diff --git a/server/src/main/java/com/samsung/samserver/service/dto/UserMapper.java b/server/src/main/java/com/samsung/samserver/service/dto/UserMapper.java new file mode 100644 index 0000000..2234334 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/service/dto/UserMapper.java @@ -0,0 +1,79 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.service.dto; + +import com.samsung.samserver.domain.Authority; +import com.samsung.samserver.domain.User; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Mapper for the entity User and its DTO called UserDTO. + * + * Normal mappers are generated using MapStruct, this one is hand-coded as MapStruct + * support is still in beta, and requires a manual step with an IDE. + */ +@Service +public class UserMapper { + + public UserDTO userToUserDTO(User user) { + return new UserDTO(user); + } + + public List usersToUserDTOs(List users) { + return users.stream() + .filter(Objects::nonNull) + .map(this::userToUserDTO) + .collect(Collectors.toList()); + } + + public User userDTOToUser(UserDTO userDTO) { + if (userDTO == null) { + return null; + } else { + User user = new User(); + user.setId(userDTO.getId()); + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set authorities = this.authoritiesFromStrings(userDTO.getAuthorities()); + if (authorities != null) { + user.setAuthorities(authorities); + } + return user; + } + } + + public List userDTOsToUsers(List userDTOs) { + return userDTOs.stream() + .filter(Objects::nonNull) + .map(this::userDTOToUser) + .collect(Collectors.toList()); + } + + public User userFromId(Long id) { + if (id == null) { + return null; + } + User user = new User(); + user.setId(id); + return user; + } + + public Set authoritiesFromStrings(Set strings) { + return strings.stream().map(string -> { + Authority auth = new Authority(); + auth.setName(string); + return auth; + }).collect(Collectors.toSet()); + } +} diff --git a/server/src/main/java/com/samsung/samserver/service/impl/MailService.java b/server/src/main/java/com/samsung/samserver/service/impl/MailService.java new file mode 100644 index 0000000..c97eaee --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/service/impl/MailService.java @@ -0,0 +1,109 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.service.impl; + +import com.samsung.samserver.domain.User; +import io.github.jhipster.config.JHipsterProperties; + +import org.apache.commons.lang3.CharEncoding; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.thymeleaf.context.Context; +import org.thymeleaf.spring4.SpringTemplateEngine; + +import javax.mail.internet.MimeMessage; +import java.util.Locale; + +/** + * Service for sending emails. + *

+ * We use the @Async annotation to send emails asynchronously. + */ +@Service +public class MailService { + + private final Logger log = LoggerFactory.getLogger(MailService.class); + + private static final String USER = "user"; + + private static final String BASE_URL = "baseUrl"; + + private final JHipsterProperties jHipsterProperties; + + private final JavaMailSender javaMailSender; + + private final MessageSource messageSource; + + private final SpringTemplateEngine templateEngine; + + public MailService(JHipsterProperties jHipsterProperties, JavaMailSender javaMailSender, + MessageSource messageSource, SpringTemplateEngine templateEngine) { + + this.jHipsterProperties = jHipsterProperties; + this.javaMailSender = javaMailSender; + this.messageSource = messageSource; + this.templateEngine = templateEngine; + } + + @Async + public void sendEmail(String to, String subject, String content, boolean isMultipart, boolean isHtml) { + log.debug("Send email[multipart '{}' and html '{}'] to '{}' with subject '{}' and content={}", + isMultipart, isHtml, to, subject, content); + + // Prepare message using a Spring helper + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + try { + MimeMessageHelper message = new MimeMessageHelper(mimeMessage, isMultipart, CharEncoding.UTF_8); + message.setTo(to); + message.setFrom(jHipsterProperties.getMail().getFrom()); + message.setSubject(subject); + message.setText(content, isHtml); + javaMailSender.send(mimeMessage); + log.debug("Sent email to User '{}'", to); + } catch (Exception e) { + if (log.isDebugEnabled()) { + log.warn("Email could not be sent to user '{}'", to, e); + } else { + log.warn("Email could not be sent to user '{}': {}", to, e.getMessage()); + } + } + } + + @Async + public void sendEmailFromTemplate(User user, String templateName, String titleKey) { + Locale locale = Locale.forLanguageTag(user.getLangKey()); + Context context = new Context(locale); + context.setVariable(USER, user); + context.setVariable(BASE_URL, jHipsterProperties.getMail().getBaseUrl()); + String content = templateEngine.process(templateName, context); + String subject = messageSource.getMessage(titleKey, null, locale); + sendEmail(user.getEmail(), subject, content, false, true); + + } + + @Async + public void sendActivationEmail(User user) { + log.debug("Sending activation email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "activationEmail", "email.activation.title"); + } + + @Async + public void sendCreationEmail(User user) { + log.debug("Sending creation email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "creationEmail", "email.activation.title"); + } + + @Async + public void sendPasswordResetMail(User user) { + log.debug("Sending password reset email to '{}'", user.getEmail()); + sendEmailFromTemplate(user, "passwordResetEmail", "email.reset.title"); + } +} diff --git a/server/src/main/java/com/samsung/samserver/service/impl/UserService.java b/server/src/main/java/com/samsung/samserver/service/impl/UserService.java new file mode 100644 index 0000000..8a1f29f --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/service/impl/UserService.java @@ -0,0 +1,286 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.service.impl; + + +import com.samsung.samserver.service.dto.UserDTO; + +import com.samsung.samserver.config.Constants; +import com.samsung.samserver.domain.*; +import com.samsung.samserver.repository.AuthorityRepository; +import com.samsung.samserver.repository.UserRepository; +import com.samsung.samserver.security.AuthoritiesConstants; +import com.samsung.samserver.security.SecurityUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Service class for managing users. + */ +@Service +@Transactional +public class UserService { + + private final Logger log = LoggerFactory.getLogger(UserService.class); + + private final UserRepository userRepository; + + private final PasswordEncoder passwordEncoder; + + private final AuthorityRepository authorityRepository; + + public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, AuthorityRepository authorityRepository) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.authorityRepository = authorityRepository; + } + + public Optional activateRegistration(String key) { + log.debug("Activating user for activation key {}", key); + return userRepository.findOneByActivationKey(key) + .map(user -> { + // activate given user for the registration key. + user.setActivated(true); + user.setActivationKey(null); + log.debug("Activated user: {}", user); + return user; + }); + } + + public Optional completePasswordReset(String newPassword, String key) { + log.debug("Reset user password for reset key {}", key); + + return userRepository.findOneByResetKey(key) + .filter(user -> user.getResetDate().isAfter(Instant.now().minusSeconds(86400))) + .map(user -> { + user.setPassword(passwordEncoder.encode(newPassword)); + user.setResetKey(null); + user.setResetDate(null); + return user; + }); + } + + public Optional requestPasswordReset(String mail) { + return userRepository.findOneByEmailIgnoreCase(mail) + .filter(User::getActivated) + .map(user -> { + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + return user; + }); + } + + public User registerUser(UserDTO userDTO, String password) { + + User newUser = new User(); + Authority authority = authorityRepository.findOne(AuthoritiesConstants.USER); + Set authorities = new HashSet<>(); + String encryptedPassword = passwordEncoder.encode(password); + newUser.setLogin(userDTO.getLogin()); + // new user gets initially a generated password + newUser.setPassword(encryptedPassword); + newUser.setFirstName(userDTO.getFirstName()); + newUser.setLastName(userDTO.getLastName()); + newUser.setEmail(userDTO.getEmail()); + newUser.setImageUrl(userDTO.getImageUrl()); + newUser.setLangKey(userDTO.getLangKey()); + // new user is not active + newUser.setActivated(false); + // new user gets registration key + newUser.setActivationKey(RandomUtil.generateActivationKey()); + authorities.add(authority); + newUser.setAuthorities(authorities); + userRepository.save(newUser); + log.debug("Created Information for User: {}", newUser); + return newUser; + } + + public User createUser(UserDTO userDTO) { + User user = new User(); + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + if (userDTO.getLangKey() == null) { + user.setLangKey(Constants.DEFAULT_LANGUAGE); // default language + } else { + user.setLangKey(userDTO.getLangKey()); + } + if (userDTO.getAuthorities() != null) { + Set authorities = userDTO.getAuthorities().stream() + .map(authorityRepository::findOne) + .collect(Collectors.toSet()); + user.setAuthorities(authorities); + } + String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword()); + user.setPassword(encryptedPassword); + user.setResetKey(RandomUtil.generateResetKey()); + user.setResetDate(Instant.now()); + user.setActivated(true); + userRepository.save(user); + log.debug("Created Information for User: {}", user); + return user; + } + + /** + * Update basic information (first name, last name, email, language) for the current user. + * + * @param firstName first name of user + * @param lastName last name of user + * @param email email id of user + * @param langKey language key + * @param imageUrl image URL of user + */ + public void updateUser(String firstName, String lastName, String email, String langKey, String imageUrl) { + SecurityUtils.getCurrentUserLogin() + .flatMap(userRepository::findOneByLogin) + .ifPresent(user -> { + user.setFirstName(firstName); + user.setLastName(lastName); + user.setEmail(email); + user.setLangKey(langKey); + user.setImageUrl(imageUrl); + log.debug("Changed Information for User: {}", user); + }); + } + + /** + * Update all information for a specific user, and return the modified user. + * + * @param userDTO user to update + * @return updated user + */ + public Optional updateUser(UserDTO userDTO) { + return Optional.of(userRepository + .findOne(userDTO.getId())) + .map(user -> { + user.setLogin(userDTO.getLogin()); + user.setFirstName(userDTO.getFirstName()); + user.setLastName(userDTO.getLastName()); + user.setEmail(userDTO.getEmail()); + user.setImageUrl(userDTO.getImageUrl()); + user.setActivated(userDTO.isActivated()); + user.setLangKey(userDTO.getLangKey()); + Set managedAuthorities = user.getAuthorities(); + managedAuthorities.clear(); + userDTO.getAuthorities().stream() + .map(authorityRepository::findOne) + .forEach(managedAuthorities::add); + log.debug("Changed Information for User: {}", user); + return user; + }) + .map(UserDTO::new); + } + + public void deleteUser(String login) { + userRepository.findOneByLogin(login).ifPresent(user -> { + userRepository.delete(user); + log.debug("Deleted User: {}", user); + }); + } + + public void changePassword(String password) { + SecurityUtils.getCurrentUserLogin() + .flatMap(userRepository::findOneByLogin) + .ifPresent(user -> { + String encryptedPassword = passwordEncoder.encode(password); + user.setPassword(encryptedPassword); + log.debug("Changed password for User: {}", user); + }); + } + + @Transactional(readOnly = true) + public Page getAllManagedUsers(Pageable pageable) { + return userRepository.findAllByLoginNot(pageable, Constants.ANONYMOUS_USER).map(UserDTO::new); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthoritiesByLogin(String login) { + return userRepository.findOneWithAuthoritiesByLogin(login); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthorities(Long id) { + return userRepository.findOneWithAuthoritiesById(id); + } + + @Transactional(readOnly = true) + public Optional getUserWithAuthorities() { + return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneWithAuthoritiesByLogin); + } + + /** + * Not activated users should be automatically deleted after 3 days. + *

+ * This is scheduled to get fired everyday, at 01:00 (am). + */ + @Scheduled(cron = "0 0 1 * * ?") + public void removeNotActivatedUsers() { + List users = userRepository.findAllByActivatedIsFalseAndCreatedDateBefore(Instant.now().minus(3, ChronoUnit.DAYS)); + for (User user : users) { + log.debug("Deleting not activated user {}", user.getLogin()); + userRepository.delete(user); + } + } + + /** + * @return a list of all the authorities + */ + public List getAuthorities() { + return authorityRepository.findAll().stream().map(Authority::getName).collect(Collectors.toList()); + } + + /** + * Utility class for generating random Strings. + */ + public static final class RandomUtil { + + private static final int DEF_COUNT = 20; + + private RandomUtil() { + } + + /** + * Generate a password. + * + * @return the generated password + */ + public static String generatePassword() { + return RandomStringUtils.randomAlphanumeric(DEF_COUNT); + } + + /** + * Generate an activation key. + * + * @return the generated activation key + */ + public static String generateActivationKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + + /** + * Generate a reset key. + * + * @return the generated reset key + */ + public static String generateResetKey() { + return RandomStringUtils.randomNumeric(DEF_COUNT); + } + } +} diff --git a/server/src/main/java/com/samsung/samserver/web/rest/UserResource.java b/server/src/main/java/com/samsung/samserver/web/rest/UserResource.java new file mode 100644 index 0000000..3ddb009 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/web/rest/UserResource.java @@ -0,0 +1,194 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.web.rest; + +import com.codahale.metrics.annotation.Timed; +import com.samsung.samserver.config.Constants; +import com.samsung.samserver.domain.User; +import com.samsung.samserver.repository.UserRepository; +import com.samsung.samserver.security.AuthoritiesConstants; +import com.samsung.samserver.service.impl.MailService; +import com.samsung.samserver.service.impl.UserService; +import com.samsung.samserver.service.dto.UserDTO; +import com.samsung.samserver.web.rest.errors.BadRequestAlertException; +import com.samsung.samserver.web.rest.errors.UserServiceError; +import com.samsung.samserver.web.rest.util.HeaderUtil; +import com.samsung.samserver.web.rest.util.PaginationUtil; +import io.github.jhipster.web.util.ResponseUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.*; + +/** + * REST controller for managing users. + *

+ * This class accesses the User entity, and needs to fetch its collection of authorities. + *

+ * For a normal use-case, it would be better to have an eager relationship between User and Authority, + * and send everything to the client side: there would be no View Model and DTO, a lot less code, and an outer-join + * which would be good for performance. + *

+ * We use a View Model and a DTO for 3 reasons: + *

    + *
  • We want to keep a lazy association between the user and the authorities, because people will + * quite often do relationships with the user, and we don't want them to get the authorities all + * the time for nothing (for performance reasons). This is the #1 goal: we should not impact our users' + * application because of this use-case.
  • + *
  • Not having an outer join causes n+1 requests to the database. This is not a real issue as + * we have by default a second-level cache. This means on the first HTTP call we do the n+1 requests, + * but then all authorities come from the cache, so in fact it's much better than doing an outer join + * (which will get lots of data from the database, for each HTTP call).
  • + *
  • As this manages users, for security reasons, we'd rather have a DTO layer.
  • + *
+ *

+ * Another option would be to have a specific JPA entity graph to handle this case. + */ +@RestController +@RequestMapping("/api") +public class UserResource { + + private final Logger log = LoggerFactory.getLogger(UserResource.class); + + private final UserRepository userRepository; + + private final UserService userService; + + private final MailService mailService; + + public UserResource(UserRepository userRepository, UserService userService, MailService mailService) { + + this.userRepository = userRepository; + this.userService = userService; + this.mailService = mailService; + } + + /** + * POST /users : Creates a new user. + *

+ * Creates a new user if the login and email are not already used, and sends an + * mail with an activation link. + * The user needs to be activated on creation. + * + * @param userDTO the user to create + * @return the ResponseEntity with status 201 (Created) and with body the new user, or with status 400 (Bad Request) if the login or email is already in use + * @throws URISyntaxException if the Location URI syntax is incorrect + * @throws BadRequestAlertException 400 (Bad Request) if the login or email is already in use + */ + @PostMapping("/users") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity createUser(@Valid @RequestBody UserDTO userDTO) throws URISyntaxException { + log.debug("REST request to save User : {}", userDTO); + + if (userDTO.getId() != null) { + throw new BadRequestAlertException("A new user cannot already have an ID", "userManagement", "idexists"); + // Lowercase the user login before comparing with database + } else if (userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).isPresent()) { + throw new UserServiceError.LoginAlreadyUsedException(); + } else if (userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).isPresent()) { + throw new UserServiceError.EmailAlreadyUsedException(); + } else { + User newUser = userService.createUser(userDTO); + mailService.sendCreationEmail(newUser); + return ResponseEntity.created(new URI("/api/users/" + newUser.getLogin())) + .headers(HeaderUtil.createAlert( "A user is created with identifier " + newUser.getLogin(), newUser.getLogin())) + .body(newUser); + } + } + + /** + * PUT /users : Updates an existing User. + * + * @param userDTO the user to update + * @return the ResponseEntity with status 200 (OK) and with body the updated user + * @throws UserServiceError.EmailAlreadyUsedException 400 (Bad Request) if the email is already in use + * @throws UserServiceError.LoginAlreadyUsedException 400 (Bad Request) if the login is already in use + */ + @PutMapping("/users") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity updateUser(@Valid @RequestBody UserDTO userDTO) { + log.debug("REST request to update User : {}", userDTO); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) { + throw new UserServiceError.EmailAlreadyUsedException(); + } + existingUser = userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()); + if (existingUser.isPresent() && (!existingUser.get().getId().equals(userDTO.getId()))) { + throw new UserServiceError.LoginAlreadyUsedException(); + } + Optional updatedUser = userService.updateUser(userDTO); + + return ResponseUtil.wrapOrNotFound(updatedUser, + HeaderUtil.createAlert("A user is updated with identifier " + userDTO.getLogin(), userDTO.getLogin())); + } + + /** + * GET /users : get all users. + * + * @param pageable the pagination information + * @return the ResponseEntity with status 200 (OK) and with body all users + */ + @GetMapping("/users") + @Timed + public ResponseEntity> getAllUsers(Pageable pageable) { + final Page page = userService.getAllManagedUsers(pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(page, "/api/users"); + return new ResponseEntity<>(page.getContent(), headers, HttpStatus.OK); + } + + /** + * @return a string list of the all of the roles + */ + @GetMapping("/users/authorities") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public List getAuthorities() { + return userService.getAuthorities(); + } + + /** + * GET /users/:login : get the "login" user. + * + * @param login the login of the user to find + * @return the ResponseEntity with status 200 (OK) and with body the "login" user, or with status 404 (Not Found) + */ + @GetMapping("/users/{login:" + Constants.LOGIN_REGEX + "}") + @Timed + public ResponseEntity getUser(@PathVariable String login) { + log.debug("REST request to get User : {}", login); + return ResponseUtil.wrapOrNotFound( + userService.getUserWithAuthoritiesByLogin(login) + .map(UserDTO::new)); + } + + /** + * DELETE /users/:login : delete the "login" User. + * + * @param login the login of the user to delete + * @return the ResponseEntity with status 200 (OK) + */ + @DeleteMapping("/users/{login:" + Constants.LOGIN_REGEX + "}") + @Timed + @Secured(AuthoritiesConstants.ADMIN) + public ResponseEntity deleteUser(@PathVariable String login) { + log.debug("REST request to delete User: {}", login); + userService.deleteUser(login); + return ResponseEntity.ok().headers(HeaderUtil.createAlert( "A user is deleted with identifier " + login, login)).build(); + } +} diff --git a/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDashboard.java b/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDashboard.java new file mode 100644 index 0000000..65b11f9 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDashboard.java @@ -0,0 +1,80 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.web.rest.controller; + +import com.samsung.samserver.domain.Device; +import com.samsung.samserver.web.rest.service.AccountService; +import com.samsung.samserver.web.rest.service.UserJWTService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import io.swagger.annotations.*; + +import javax.validation.Valid; +import java.util.List; +import com.samsung.samserver.web.rest.errors.UserServiceError; + +/** + * Rest Interface for dashboard. + * + * @author Mykhailo Dalakov + * @version 1.0 + */ +@RequestMapping("/dashboard") +public interface IRestDashboard { + + /** + * GET /devices : get all the devices. + * + * @return the ResponseEntity with status 200 (OK) and the list of devices in body + */ + @ApiOperation(value = "getAllDevices", notes = "The “getAllDevices” operation is used to obtain all registered devices.") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK. Request executed successfully."), + @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), + @ApiResponse(code = 500, message = "Internal server error. The user has to repeat action.") + }) + @RequestMapping(value ="/devices", method = RequestMethod.GET) + public List getAllDevices(); + + + /** + * POST request login + * + * @return the ResponseEntity with status 200 (OK) and JWT + */ + @ApiOperation(value = "login", notes = "The “login” operation is used to authenticate user and receive JWT token.") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "OK. Login was successful."), + @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), + @ApiResponse(code = 500, message = "Internal server error. The user has to repeat action.") + }) + @RequestMapping(value ="/auth/login", method = RequestMethod.POST) + public ResponseEntity login( + @Valid @ApiParam(value = "Login Data", required = true) @RequestBody UserJWTService.LoginVM loginVM); + + + /** + * POST request register the user. + * + * @param managedUserVM the managed user View Model + * @throws UserServiceError.InvalidPasswordException 400 (Bad Request) if the password is incorrect + * @throws UserServiceError.EmailAlreadyUsedException 400 (Bad Request) if the email is already used + * @throws UserServiceError.LoginAlreadyUsedException 400 (Bad Request) if the login is already used + */ + @ApiOperation(value = "register", notes = "The “register” operation is used to register new user.") + @ApiResponses(value = { + @ApiResponse(code = 201, message = "Register was successful. New User was created."), + @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), + @ApiResponse(code = 500, message = "Internal server error. The user has to repeat action.") + }) + @RequestMapping(value ="/auth/register", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public void registerAccount( + @Valid @ApiParam(value = "Managed User", required = true) @RequestBody AccountService.ManagedUserVM managedUserVM); + + +} diff --git a/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDevice.java b/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDevice.java index b23cae1..2008b0d 100644 --- a/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDevice.java +++ b/server/src/main/java/com/samsung/samserver/web/rest/controller/IRestDevice.java @@ -51,7 +51,6 @@ public interface IRestDevice { @ApiResponse(code = 200, message = "OK. The updates are found and should apply by device."), @ApiResponse(code = 304, message = "Not Modified. The updates aren’t required"), @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), - @ApiResponse(code = 404, message = "Device not found. The device should be registered"), @ApiResponse(code = 500, message = "Internal server error. The device has to repeat action") }) @RequestMapping(value ="/get-updates", method = RequestMethod.GET, produces = { @@ -70,7 +69,6 @@ public interface IRestDevice { @ApiResponses(value = { @ApiResponse(code = 200, message = "OK. Send data was successful."), @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), - @ApiResponse(code = 404, message = "Device not found. The device should be registered"), @ApiResponse(code = 500, message = "Internal server error. The device has to repeat action") }) @RequestMapping(value ="/send-data", method = RequestMethod.POST) @@ -89,7 +87,6 @@ public interface IRestDevice { @ApiResponses(value = { @ApiResponse(code = 200, message = "OK. The data are found and should apply by device."), @ApiResponse(code = 400, message = "Bad Request. The reason is contained in the tags “errorKey”, “message”, “title”"), - @ApiResponse(code = 404, message = "Device not found. The device should be registered"), @ApiResponse(code = 500, message = "Internal server error. The device has to repeat action") }) @RequestMapping(value ="/get-udata", method = RequestMethod.GET) diff --git a/server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDashboard.java b/server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDashboard.java new file mode 100644 index 0000000..21a5e5f --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDashboard.java @@ -0,0 +1,72 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.web.rest.controller.impl; + +import com.samsung.samserver.domain.Device; +import com.samsung.samserver.service.DeviceService; +import com.samsung.samserver.web.rest.controller.IRestDashboard; +import com.samsung.samserver.web.rest.service.AccountService; +import com.samsung.samserver.web.rest.service.UserJWTService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +/** + * REST dashboard controller. + * + * @author Mykhailo Dalakov + * @version 1.0 + */ +@RestController +public class RestDashboard implements IRestDashboard { + + private final Logger log = LoggerFactory.getLogger(RestDashboard.class); + + @Autowired + private DeviceService deviceService; + + @Autowired + private UserJWTService userJWTService; + + @Autowired + private AccountService accountService; + + /** + * GET /devices : get all the devices. + * + * @return the ResponseEntity with status 200 (OK) and the list of devices in body + */ + @Override + public List getAllDevices() { + return deviceService.findAll(); + } + + /** + * POST request login + * + * @return the ResponseEntity with status 200 (OK) and JWT + */ + @Override + public ResponseEntity login(@Valid @RequestBody UserJWTService.LoginVM loginVM) { + return userJWTService.authorize(loginVM); + } + + /** + * POST request register the user. + * + * @param managedUserVM the managed user View Model + */ + @Override + public void registerAccount(@Valid @RequestBody AccountService.ManagedUserVM managedUserVM){ + accountService.registerAccount(managedUserVM); + } + +} diff --git a/server/src/main/java/com/samsung/samserver/web/rest/controller/RestDevice.java b/server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDevice.java similarity index 94% rename from server/src/main/java/com/samsung/samserver/web/rest/controller/RestDevice.java rename to server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDevice.java index 2c4b6b9..ed3af43 100644 --- a/server/src/main/java/com/samsung/samserver/web/rest/controller/RestDevice.java +++ b/server/src/main/java/com/samsung/samserver/web/rest/controller/impl/RestDevice.java @@ -3,8 +3,9 @@ * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. */ -package com.samsung.samserver.web.rest.controller; +package com.samsung.samserver.web.rest.controller.impl; +import com.samsung.samserver.web.rest.controller.IRestDevice; import com.samsung.samserver.web.rest.service.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; diff --git a/server/src/main/java/com/samsung/samserver/web/rest/service/AccountService.java b/server/src/main/java/com/samsung/samserver/web/rest/service/AccountService.java new file mode 100644 index 0000000..06529b6 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/web/rest/service/AccountService.java @@ -0,0 +1,235 @@ +/* + * In Samsung Ukraine R&D Center (SRK under a contract between) + * LLC "Samsung Electronics Co", Ltd (Seoul, Republic of Korea) + * Copyright (C) 2018 Samsung Electronics Co., Ltd. All rights reserved. + */ +package com.samsung.samserver.web.rest.service; + +import com.samsung.samserver.domain.User; +import com.samsung.samserver.repository.UserRepository; +import com.samsung.samserver.security.SecurityUtils; +import com.samsung.samserver.service.impl.MailService; +import com.samsung.samserver.service.impl.UserService; +import com.samsung.samserver.service.dto.UserDTO; + +import com.samsung.samserver.web.rest.errors.InternalServerErrorException; +import com.samsung.samserver.web.rest.errors.UserServiceError; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import javax.validation.Valid; +import javax.validation.constraints.Size; +import java.util.*; + +/** + * REST service for managing the current user's account. + */ +@Service +public class AccountService { + + private final Logger log = LoggerFactory.getLogger(AccountService.class); + + private final UserRepository userRepository; + + private final UserService userService; + + private final MailService mailService; + + public AccountService(UserRepository userRepository, UserService userService, MailService mailService) { + + this.userRepository = userRepository; + this.userService = userService; + this.mailService = mailService; + } + + /** + * POST /register : register the user. + * + * @param managedUserVM the managed user View Model + * @throws UserServiceError.InvalidPasswordException 400 (Bad Request) if the password is incorrect + * @throws UserServiceError.EmailAlreadyUsedException 400 (Bad Request) if the email is already used + * @throws UserServiceError.LoginAlreadyUsedException 400 (Bad Request) if the login is already used + */ + public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) { + if (!checkPasswordLength(managedUserVM.getPassword())) { + throw new UserServiceError.InvalidPasswordException(); + } + userRepository.findOneByLogin(managedUserVM.getLogin().toLowerCase()).ifPresent(u -> {throw new UserServiceError.LoginAlreadyUsedException();}); + userRepository.findOneByEmailIgnoreCase(managedUserVM.getEmail()).ifPresent(u -> {throw new UserServiceError.EmailAlreadyUsedException();}); + User user = userService.registerUser(managedUserVM, managedUserVM.getPassword()); + mailService.sendActivationEmail(user); + } + + /** + * GET /activate : activate the registered user. + * + * @param key the activation key + * @throws RuntimeException 500 (Internal Server Error) if the user couldn't be activated + */ + public void activateAccount(@RequestParam(value = "key") String key) { + Optional user = userService.activateRegistration(key); + if (!user.isPresent()) { + throw new InternalServerErrorException("No user was found for this reset key"); + } + } + + /** + * GET /authenticate : check if the user is authenticated, and return its login. + * + * @param request the HTTP request + * @return the login if the user is authenticated + */ + public String isAuthenticated(HttpServletRequest request) { + log.debug("REST request to check if the current user is authenticated"); + return request.getRemoteUser(); + } + + /** + * GET /account : get the current user. + * + * @return the current user + * @throws RuntimeException 500 (Internal Server Error) if the user couldn't be returned + */ + public UserDTO getAccount() { + return userService.getUserWithAuthorities() + .map(UserDTO::new) + .orElseThrow(() -> new InternalServerErrorException("User could not be found")); + } + + /** + * POST /account : update the current user information. + * + * @param userDTO the current user information + * @throws UserServiceError.EmailAlreadyUsedException 400 (Bad Request) if the email is already used + * @throws RuntimeException 500 (Internal Server Error) if the user login wasn't found + */ + public void saveAccount(@Valid @RequestBody UserDTO userDTO) { + final String userLogin = SecurityUtils.getCurrentUserLogin().orElseThrow(() -> new InternalServerErrorException("Current user login not found")); + Optional existingUser = userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()); + if (existingUser.isPresent() && (!existingUser.get().getLogin().equalsIgnoreCase(userLogin))) { + throw new UserServiceError.EmailAlreadyUsedException(); + } + Optional user = userRepository.findOneByLogin(userLogin); + if (!user.isPresent()) { + throw new InternalServerErrorException("User could not be found"); + } + userService.updateUser(userDTO.getFirstName(), userDTO.getLastName(), userDTO.getEmail(), + userDTO.getLangKey(), userDTO.getImageUrl()); + } + + /** + * POST /account/change-password : changes the current user's password + * + * @param password the new password + * @throws UserServiceError.InvalidPasswordException 400 (Bad Request) if the new password is incorrect + */ + public void changePassword(@RequestBody String password) { + if (!checkPasswordLength(password)) { + throw new UserServiceError.InvalidPasswordException(); + } + userService.changePassword(password); + } + + /** + * POST /account/reset-password/init : Send an email to reset the password of the user + * + * @param mail the mail of the user + * @throws UserServiceError.EmailNotFoundException 400 (Bad Request) if the email address is not registered + */ + public void requestPasswordReset(@RequestBody String mail) { + mailService.sendPasswordResetMail( + userService.requestPasswordReset(mail) + .orElseThrow(UserServiceError.EmailNotFoundException::new) + ); + } + + /** + * POST /account/reset-password/finish : Finish to reset the password of the user + * + * @param keyAndPassword the generated key and the new password + * @throws UserServiceError.InvalidPasswordException 400 (Bad Request) if the password is incorrect + * @throws RuntimeException 500 (Internal Server Error) if the password could not be reset + */ + public void finishPasswordReset(@RequestBody KeyAndPasswordVM keyAndPassword) { + if (!checkPasswordLength(keyAndPassword.getNewPassword())) { + throw new UserServiceError.InvalidPasswordException(); + } + Optional user = + userService.completePasswordReset(keyAndPassword.getNewPassword(), keyAndPassword.getKey()); + + if (!user.isPresent()) { + throw new InternalServerErrorException("No user was found for this reset key"); + } + } + + private static boolean checkPasswordLength(String password) { + return !StringUtils.isEmpty(password) && + password.length() >= ManagedUserVM.PASSWORD_MIN_LENGTH && + password.length() <= ManagedUserVM.PASSWORD_MAX_LENGTH; + } + + + /** + * View Model object for storing the user's key and password. + */ + public static class KeyAndPasswordVM { + + private String key; + + private String newPassword; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + } + + + /** + * View Model extending the UserDTO, which is meant to be used in the user management UI. + */ + public static class ManagedUserVM extends UserDTO { + + public static final int PASSWORD_MIN_LENGTH = 4; + + public static final int PASSWORD_MAX_LENGTH = 100; + + @Size(min = PASSWORD_MIN_LENGTH, max = PASSWORD_MAX_LENGTH) + private String password; + + public ManagedUserVM() { + // Empty constructor needed for Jackson. + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public String toString() { + return "ManagedUserVM{" + + "} " + super.toString(); + } + } + + +} diff --git a/server/src/main/java/com/samsung/samserver/web/rest/service/UserJWTService.java b/server/src/main/java/com/samsung/samserver/web/rest/service/UserJWTService.java new file mode 100644 index 0000000..8be15f1 --- /dev/null +++ b/server/src/main/java/com/samsung/samserver/web/rest/service/UserJWTService.java @@ -0,0 +1,122 @@ +package com.samsung.samserver.web.rest.service; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.samsung.samserver.security.jwt.JWTConfigurer; +import com.samsung.samserver.security.jwt.TokenProvider; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; +import org.springframework.web.bind.annotation.RequestBody; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; + +/** + * REST service implementation for get jwt token. + * + * @author Mykhailo Dalakov + * @version 1.0 + */ +@Service +public class UserJWTService { + + private final TokenProvider tokenProvider; + + private final AuthenticationManager authenticationManager; + + public UserJWTService(TokenProvider tokenProvider, AuthenticationManager authenticationManager) { + this.tokenProvider = tokenProvider; + this.authenticationManager = authenticationManager; + } + + public ResponseEntity authorize(@Valid @RequestBody LoginVM loginVM) { + + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginVM.getUsername(), loginVM.getPassword()); + + Authentication authentication = this.authenticationManager.authenticate(authenticationToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + boolean rememberMe = (loginVM.isRememberMe() == null) ? false : loginVM.isRememberMe(); + String jwt = tokenProvider.createToken(authentication, rememberMe); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(JWTConfigurer.AUTHORIZATION_HEADER, "Bearer " + jwt); + return new ResponseEntity<>(new JWTToken(jwt), httpHeaders, HttpStatus.OK); + } + + + /** + * Object to return as body in JWT Authentication. + */ + public static class JWTToken { + + private String idToken; + + JWTToken(String idToken) { + this.idToken = idToken; + } + + @JsonProperty("id_token") + String getIdToken() { + return idToken; + } + + void setIdToken(String idToken) { + this.idToken = idToken; + } + } + + + /** + * View Model object for storing a user's credentials. + */ + public static class LoginVM { + + @NotNull + @Size(min = 1, max = 50) + private String username; + + @NotNull + @Size(min = AccountService.ManagedUserVM.PASSWORD_MIN_LENGTH, max = AccountService.ManagedUserVM.PASSWORD_MAX_LENGTH) + private String password; + + private Boolean rememberMe; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean isRememberMe() { + return rememberMe; + } + + public void setRememberMe(Boolean rememberMe) { + this.rememberMe = rememberMe; + } + + @Override + public String toString() { + return "LoginVM{" + + "username='" + username + '\'' + + ", rememberMe=" + rememberMe + + '}'; + } + } +} diff --git a/server/src/main/resources/.h2.server.properties b/server/src/main/resources/.h2.server.properties index b4338b3..8703b58 100644 --- a/server/src/main/resources/.h2.server.properties +++ b/server/src/main/resources/.h2.server.properties @@ -1,5 +1,5 @@ #H2 Server Properties -#Wed Feb 14 10:46:33 GMT+02:00 2018 +#Tue Feb 20 15:10:21 GMT+02:00 2018 0=JHipster H2 (Memory)|org.h2.Driver|jdbc\:h2\:mem\:samserver|samserver webAllowOthers=true webPort=8082 diff --git a/server/src/main/resources/config/liquibase/authorities.csv b/server/src/main/resources/config/liquibase/authorities.csv new file mode 100644 index 0000000..af5c6df --- /dev/null +++ b/server/src/main/resources/config/liquibase/authorities.csv @@ -0,0 +1,3 @@ +name +ROLE_ADMIN +ROLE_USER diff --git a/server/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml b/server/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml index d188852..a5605a2 100644 --- a/server/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml +++ b/server/src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml @@ -1,9 +1,9 @@ @@ -18,6 +18,93 @@ The initial schema has the '00000000000001' id, so that it is over-written if we re-generate it. --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/src/main/resources/config/liquibase/users.csv b/server/src/main/resources/config/liquibase/users.csv new file mode 100644 index 0000000..b25922b --- /dev/null +++ b/server/src/main/resources/config/liquibase/users.csv @@ -0,0 +1,5 @@ +id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;system;$2a$10$mE.qmcV0mFU5NcKh73TZx.z4ueI/.bDWbj0T1BYyqP481kGGarKLG;System;System;system@localhost;;true;en;system;system +2;anonymoususer;$2a$10$j8S5d7Sr7.8VTOYNviDPOeWX8KcYILUVJBsYV83Y5NtECayypx9lO;Anonymous;User;anonymous@localhost;;true;en;system;system +3;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +4;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system diff --git a/server/src/main/resources/config/liquibase/users_authorities.csv b/server/src/main/resources/config/liquibase/users_authorities.csv new file mode 100644 index 0000000..06c5fee --- /dev/null +++ b/server/src/main/resources/config/liquibase/users_authorities.csv @@ -0,0 +1,6 @@ +user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +3;ROLE_ADMIN +3;ROLE_USER +4;ROLE_USER -- 2.7.4