diff --git a/demo/build.gradle b/demo/build.gradle index 367330f..c57e731 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -21,6 +21,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-devtools' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' implementation 'org.webjars:bootstrap:5.1.3' implementation 'org.webjars:jquery:3.6.0' diff --git a/demo/src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java b/demo/src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..6937121 --- /dev/null +++ b/demo/src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java @@ -0,0 +1,14 @@ +package com.example.demo.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public PasswordEncoder createPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/demo/src/main/java/com/example/demo/configuration/SecurityConfiguration.java b/demo/src/main/java/com/example/demo/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..547e73e --- /dev/null +++ b/demo/src/main/java/com/example/demo/configuration/SecurityConfiguration.java @@ -0,0 +1,67 @@ +package com.example.demo.configuration; + +import com.example.demo.supply.User.UserRole; +import com.example.demo.supply.User.UserService; +import com.example.demo.supply.User.UserSignupMvcController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +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.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(securedEnabled = true) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + private static final String LOGIN_URL = "/login"; + private final UserService userService; + + public SecurityConfiguration(UserService userService) { + this.userService = userService; + createAdminOnStartup(); + } + + private void createAdminOnStartup() { + final String admin = "admin"; + if (userService.findByLogin(admin) == null) { + log.info("Admin user successfully created"); + userService.createUser(admin, admin, admin, UserRole.ADMIN); + } + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().frameOptions().sameOrigin().and() + .cors().and() + .csrf().disable() + .authorizeRequests() + .antMatchers(UserSignupMvcController.SIGNUP_URL).permitAll() + .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage(LOGIN_URL).permitAll() + .and() + .logout().permitAll(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userService); + } + + @Override + public void configure(WebSecurity web) { + web.ignoring() + .antMatchers("/css/**") + .antMatchers("/js/**") + .antMatchers("/templates/**") + .antMatchers("/webjars/**"); + } +} \ No newline at end of file diff --git a/demo/src/main/java/com/example/demo/WebConfiguration.java b/demo/src/main/java/com/example/demo/configuration/WebConfiguration.java similarity index 82% rename from demo/src/main/java/com/example/demo/WebConfiguration.java rename to demo/src/main/java/com/example/demo/configuration/WebConfiguration.java index 0ee3e83..a28a3a4 100644 --- a/demo/src/main/java/com/example/demo/WebConfiguration.java +++ b/demo/src/main/java/com/example/demo/configuration/WebConfiguration.java @@ -1,23 +1,24 @@ -package com.example.demo; +package com.example.demo.configuration; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfiguration implements WebMvcConfigurer { + public static final String REST_API = "/api"; @Override public void addViewControllers(ViewControllerRegistry registry) { WebMvcConfigurer.super.addViewControllers(registry); registry.addViewController("rest-test"); + registry.addViewController("login"); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedMethods("*"); } -} \ No newline at end of file +} + diff --git a/demo/src/main/java/com/example/demo/supply/User/User.java b/demo/src/main/java/com/example/demo/supply/User/User.java new file mode 100644 index 0000000..6927f87 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/User.java @@ -0,0 +1,73 @@ +package com.example.demo.supply.User; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Objects; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Column(nullable = false, unique = true, length = 64) + @NotBlank + @Size(min = 3, max = 64) + private String login; + @Column(nullable = false, length = 64) + @NotBlank + @Size(min = 4, max = 64) + private String password; + private UserRole role; + + public User() { + } + + public User(String login, String password) { + this(login, password, UserRole.USER); + } + + public User(String login, String password, UserRole role) { + this.login = login; + this.password = password; + this.role = role; + } + + public Long getId() { + return id; + } + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public UserRole getRole() { + return role; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id) && Objects.equals(login, user.login); + } + + @Override + public int hashCode() { + return Objects.hash(id, login); + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserDto.java b/demo/src/main/java/com/example/demo/supply/User/UserDto.java new file mode 100644 index 0000000..f09f2f6 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserDto.java @@ -0,0 +1,25 @@ +package com.example.demo.supply.User; + +public class UserDto { + private final long id; + private final String login; + private final UserRole role; + + public UserDto(User user) { + this.id = user.getId(); + this.login = user.getLogin(); + this.role = user.getRole(); + } + + public long getId() { + return id; + } + + public String getLogin() { + return login; + } + + public UserRole getRole() { + return role; + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserMvcController.java b/demo/src/main/java/com/example/demo/supply/User/UserMvcController.java new file mode 100644 index 0000000..e716000 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserMvcController.java @@ -0,0 +1,40 @@ +package com.example.demo.supply.User; + +import org.springframework.data.domain.Page; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +import java.util.List; +import java.util.stream.IntStream; + +@Controller +@RequestMapping("/users") +public class UserMvcController { + private final UserService userService; + + public UserMvcController(UserService userService) { + this.userService = userService; + } + + @GetMapping + @Secured({UserRole.AsString.ADMIN}) + public String getUsers(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "5") int size, + Model model) { + final Page users = userService.findAllPages(page, size) + .map(UserDto::new); + model.addAttribute("users", users); + final int totalPages = users.getTotalPages(); + final List pageNumbers = IntStream.rangeClosed(1, totalPages) + .boxed() + .toList(); + model.addAttribute("pages", pageNumbers); + model.addAttribute("totalPages", totalPages); + return "users"; + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserRepository.java b/demo/src/main/java/com/example/demo/supply/User/UserRepository.java new file mode 100644 index 0000000..dcb3897 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserRepository.java @@ -0,0 +1,7 @@ +package com.example.demo.supply.User; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findOneByLoginIgnoreCase(String login); +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserRole.java b/demo/src/main/java/com/example/demo/supply/User/UserRole.java new file mode 100644 index 0000000..19726e7 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserRole.java @@ -0,0 +1,20 @@ +package com.example.demo.supply.User; + +import org.springframework.security.core.GrantedAuthority; + +public enum UserRole implements GrantedAuthority { + ADMIN, + USER; + + private static final String PREFIX = "ROLE_"; + + @Override + public String getAuthority() { + return PREFIX + this.name(); + } + + public static final class AsString { + public static final String ADMIN = PREFIX + "ADMIN"; + public static final String USER = PREFIX + "USER"; + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserService.java b/demo/src/main/java/com/example/demo/supply/User/UserService.java new file mode 100644 index 0000000..4ce59b5 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserService.java @@ -0,0 +1,65 @@ +package com.example.demo.supply.User; + +import com.example.demo.supply.util.validation.ValidationException; +import com.example.demo.supply.util.validation.ValidatorUtil; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; + + +import java.util.Collections; +import java.util.Objects; + +@Service +public class UserService implements UserDetailsService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final ValidatorUtil validatorUtil; + + public UserService(UserRepository userRepository, + PasswordEncoder passwordEncoder, + ValidatorUtil validatorUtil) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.validatorUtil = validatorUtil; + } + + public Page findAllPages(int page, int size) { + return userRepository.findAll(PageRequest.of(page - 1, size, Sort.by("id").ascending())); + } + + public User findByLogin(String login) { + return userRepository.findOneByLoginIgnoreCase(login); + } + + public User createUser(String login, String password, String passwordConfirm) { + return createUser(login, password, passwordConfirm, UserRole.USER); + } + + public User createUser(String login, String password, String passwordConfirm, UserRole role) { + if (findByLogin(login) != null) { + throw new ValidationException(String.format("User '%s' already exists", login)); + } + final User user = new User(login, passwordEncoder.encode(password), role); + validatorUtil.validate(user); + if (!Objects.equals(password, passwordConfirm)) { + throw new ValidationException("Passwords not equals"); + } + return userRepository.save(user); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final User userEntity = findByLogin(username); + if (userEntity == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getRole())); + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserSignupDto.java b/demo/src/main/java/com/example/demo/supply/User/UserSignupDto.java new file mode 100644 index 0000000..353106b --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserSignupDto.java @@ -0,0 +1,40 @@ +package com.example.demo.supply.User; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class UserSignupDto { + @NotBlank + @Size(min = 3, max = 64) + private String login; + @NotBlank + @Size(min = 6, max = 64) + private String password; + @NotBlank + @Size(min = 6, max = 64) + private String passwordConfirm; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public void setPasswordConfirm(String passwordConfirm) { + this.passwordConfirm = passwordConfirm; + } +} diff --git a/demo/src/main/java/com/example/demo/supply/User/UserSignupMvcController.java b/demo/src/main/java/com/example/demo/supply/User/UserSignupMvcController.java new file mode 100644 index 0000000..2f53c64 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/User/UserSignupMvcController.java @@ -0,0 +1,48 @@ +package com.example.demo.supply.User; + +import com.example.demo.supply.util.validation.ValidationException; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import javax.validation.Valid; + +@Controller +@RequestMapping(UserSignupMvcController.SIGNUP_URL) +public class UserSignupMvcController { + public static final String SIGNUP_URL = "/signup"; + + private final UserService userService; + + public UserSignupMvcController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public String showSignupForm(Model model) { + model.addAttribute("userDto", new UserSignupDto()); + return "signup"; + } + + @PostMapping + public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto, + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + model.addAttribute("errors", bindingResult.getAllErrors()); + return "signup"; + } + try { + final User user = userService.createUser( + userSignupDto.getLogin(), userSignupDto.getPassword(), userSignupDto.getPasswordConfirm()); + return "redirect:/login?created=" + user.getLogin(); + } catch (ValidationException e) { + model.addAttribute("errors", e.getMessage()); + return "signup"; + } + } +} diff --git a/demo/src/main/java/com/example/demo/supply/util/StaticPagesMvcController.java b/demo/src/main/java/com/example/demo/supply/util/StaticPagesMvcController.java new file mode 100644 index 0000000..5be7d6b --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/util/StaticPagesMvcController.java @@ -0,0 +1,20 @@ +package com.example.demo.supply.util; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class StaticPagesMvcController { + @RequestMapping("/") + public String indexPage(){ + return "index"; + } + @RequestMapping("/forum") + public String forumPage(){ + return "forum"; + } + @RequestMapping("/login") + public String loginPage(){ + return "login"; + } +} diff --git a/demo/src/main/java/com/example/demo/supply/util/error/AdviceController.java b/demo/src/main/java/com/example/demo/supply/util/error/AdviceController.java new file mode 100644 index 0000000..cb9fb38 --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/util/error/AdviceController.java @@ -0,0 +1,44 @@ +package com.example.demo.supply.util.error; + + +import com.example.demo.supply.Order.OrderNotFoundException; +import com.example.demo.supply.Product.ProductNotFoundException; +import com.example.demo.supply.Supplier.SupplierNotFoundException; +import com.example.demo.supply.util.validation.ValidationException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; + +import java.util.stream.Collectors; + +@ControllerAdvice(annotations = RestController.class) +public class AdviceController { + @ExceptionHandler({ + OrderNotFoundException.class, + ProductNotFoundException.class, + SupplierNotFoundException.class, + ValidationException.class + }) + public ResponseEntity handleException(Throwable e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleBindException(MethodArgumentNotValidException e) { + final ValidationException validationException = new ValidationException( + e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.toSet())); + return handleException(validationException); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleUnknownException(Throwable e) { + e.printStackTrace(); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/demo/src/main/java/com/example/demo/supply/util/validation/ValidationException.java b/demo/src/main/java/com/example/demo/supply/util/validation/ValidationException.java new file mode 100644 index 0000000..d38f1ef --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/util/validation/ValidationException.java @@ -0,0 +1,11 @@ +package com.example.demo.supply.util.validation; +import java.util.Set; +public class ValidationException extends RuntimeException{ + public ValidationException(Set errors) { + super(String.join("\n", errors)); + } + + public ValidationException(String error) { + super(error); + } +} diff --git a/demo/src/main/java/com/example/demo/supply/util/validation/ValidatorUtil.java b/demo/src/main/java/com/example/demo/supply/util/validation/ValidatorUtil.java new file mode 100644 index 0000000..b10919a --- /dev/null +++ b/demo/src/main/java/com/example/demo/supply/util/validation/ValidatorUtil.java @@ -0,0 +1,28 @@ +package com.example.demo.supply.util.validation; + +import org.springframework.stereotype.Component; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ValidatorUtil { + private final Validator validator; + public ValidatorUtil() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + this.validator = factory.getValidator(); + } + } + public void validate(T object) { + final Set> errors = validator.validate(object); + if (!errors.isEmpty()) { + throw new ValidationException(errors.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toSet())); + } + } +} diff --git a/demo/src/main/resources/templates/default.html b/demo/src/main/resources/templates/default.html index cfd096a..93e9023 100644 --- a/demo/src/main/resources/templates/default.html +++ b/demo/src/main/resources/templates/default.html @@ -1,6 +1,7 @@ @@ -36,8 +37,11 @@ th:classappend="${#strings.equals(activeLink, '/order')} ? 'active' : ''">Заказы Доп задание - Документация REST API - Консоль H2 + Пользователи + + Выход () + diff --git a/demo/src/main/resources/templates/login.html b/demo/src/main/resources/templates/login.html new file mode 100644 index 0000000..6c07f67 --- /dev/null +++ b/demo/src/main/resources/templates/login.html @@ -0,0 +1,34 @@ + + +> + + + +
+
+ Пользователь не найден или пароль указан не верно +
+
+ Выход успешно произведен +
+
+ Пользователь '' успешно создан +
+
+

Вход

+

Логин

+ +

Пароль

+ + +
+
+ + \ No newline at end of file diff --git a/demo/src/main/resources/templates/signup.html b/demo/src/main/resources/templates/signup.html new file mode 100644 index 0000000..5018bd8 --- /dev/null +++ b/demo/src/main/resources/templates/signup.html @@ -0,0 +1,29 @@ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + Назад +
+
+
+ + \ No newline at end of file diff --git a/demo/src/main/resources/templates/users.html b/demo/src/main/resources/templates/users.html new file mode 100644 index 0000000..34c6cfa --- /dev/null +++ b/demo/src/main/resources/templates/users.html @@ -0,0 +1,38 @@ + + + +
+
+ + + + + + + + + + + + + + + + + +
#IDЛогинРоль
+
+ +
+ + \ No newline at end of file