From 02268b2b1b4535b7a70a309ad3feac95164f314c Mon Sep 17 00:00:00 2001 From: Safgerd Date: Fri, 16 Jun 2023 13:14:53 +0400 Subject: [PATCH] =?UTF-8?q?LabWork06:=20=D0=BD=D0=B5=20=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 8 +- .../is/labwork/Lab4/DTO/UserSignupDto.java | 29 +++++ .../Exceptions/CommentNotFoundException.java | 7 ++ .../Exceptions/CustomerNotFoundException.java | 7 ++ .../labwork/Lab4/Exceptions/JwtException.java | 11 ++ .../Exceptions/PostNotFoundException.java | 7 ++ .../Lab4/controller/CommentController.java | 2 +- .../Lab4/controller/CustomerController.java | 32 ++++- .../Lab4/controller/PostController.java | 13 +- .../ulstu/is/labwork/Lab4/model/Comment.java | 3 +- .../ulstu/is/labwork/Lab4/model/Customer.java | 20 ++- .../ru/ulstu/is/labwork/Lab4/model/Post.java | 3 +- .../ulstu/is/labwork/Lab4/model/UserRole.java | 20 +++ .../ru/ulstu/is/labwork/Lab4/mvc/Admin.java | 70 +++++++++++ .../ulstu/is/labwork/Lab4/mvc/Comments.java | 14 ++- .../ulstu/is/labwork/Lab4/mvc/Customers.java | 17 ++- .../ru/ulstu/is/labwork/Lab4/mvc/Feed.java | 17 +-- .../ru/ulstu/is/labwork/Lab4/mvc/Posts.java | 12 +- .../ru/ulstu/is/labwork/Lab4/mvc/Session.java | 16 --- .../ulstu/is/labwork/Lab4/mvc/UserSignUp.java | 53 ++++++++ .../Lab4/repositories/CommentRepository.java | 2 +- .../Lab4/repositories/CustomerRepository.java | 1 + .../Lab4/repositories/PostRepository.java | 6 +- .../is/labwork/Lab4/security/JwtFilter.java | 73 +++++++++++ .../labwork/Lab4/security/JwtProperties.java | 27 ++++ .../is/labwork/Lab4/security/JwtProvider.java | 108 ++++++++++++++++ .../PasswordEncoderConfiguration.java | 14 +++ .../Lab4/security/SecurityConfiguration.java | 116 ++++++++++++++++++ .../labwork/Lab4/services/CommentService.java | 43 +++---- .../Lab4/services/CustomerService.java | 100 ++++++++++++--- .../is/labwork/Lab4/services/PostService.java | 109 ++++++++-------- .../Lab4/util/error/AdviceController.java | 43 +++++++ .../util/validation/ValidationException.java | 9 ++ .../Lab4/util/validation/ValidatorUtil.java | 30 +++++ .../is/labwork/OpenAPI30Configuration.java | 28 +++++ .../ru/ulstu/is/labwork/WebConfiguration.java | 23 +++- src/main/resources/application.properties | 2 + src/main/resources/templates/admin.html | 103 ++++++++++++++++ src/main/resources/templates/customers.html | 28 +---- src/main/resources/templates/default.html | 30 ++--- src/main/resources/templates/error.html | 13 ++ src/main/resources/templates/feed.html | 16 +-- src/main/resources/templates/login.html | 31 +++++ src/main/resources/templates/signup.html | 44 +++++++ 44 files changed, 1152 insertions(+), 208 deletions(-) create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/DTO/UserSignupDto.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CommentNotFoundException.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CustomerNotFoundException.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/JwtException.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/PostNotFoundException.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/model/UserRole.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Admin.java delete mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Session.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/mvc/UserSignUp.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtFilter.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProperties.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProvider.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/security/PasswordEncoderConfiguration.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/security/SecurityConfiguration.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/util/error/AdviceController.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidationException.java create mode 100644 src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidatorUtil.java create mode 100644 src/main/java/ru/ulstu/is/labwork/OpenAPI30Configuration.java create mode 100644 src/main/resources/templates/admin.html create mode 100644 src/main/resources/templates/error.html create mode 100644 src/main/resources/templates/login.html create mode 100644 src/main/resources/templates/signup.html diff --git a/build.gradle b/build.gradle index 47c857c..fac791a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.0.2' + id 'org.springframework.boot' version '2.6.3' id 'io.spring.dependency-management' version '1.1.0' } @@ -14,7 +14,6 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' - testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'com.h2database:h2:2.1.210' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' @@ -23,9 +22,12 @@ dependencies { implementation 'org.webjars:bootstrap:5.1.3' implementation 'org.webjars:jquery:3.6.0' implementation 'org.webjars:font-awesome:6.1.0' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'com.auth0:java-jwt:4.4.0' implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.5' + testImplementation 'org.springframework.boot:spring-boot-starter-test' implementation 'org.hibernate.validator:hibernate-validator' - implementation 'org.springdoc:springdoc-openapi-ui:1.6.5' } tasks.named('test') { diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/DTO/UserSignupDto.java b/src/main/java/ru/ulstu/is/labwork/Lab4/DTO/UserSignupDto.java new file mode 100644 index 0000000..20c7b6d --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/DTO/UserSignupDto.java @@ -0,0 +1,29 @@ +package ru.ulstu.is.labwork.Lab4.DTO; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class UserSignupDto { + @NotBlank + @Size(min = 3, max = 64) + private String username; + @NotBlank + @Size(min = 6, max = 64) + private String password; + + 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; + } +} diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CommentNotFoundException.java b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CommentNotFoundException.java new file mode 100644 index 0000000..f1d2924 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CommentNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.labwork.Lab4.Exceptions; + +public class CommentNotFoundException extends RuntimeException { + public CommentNotFoundException(Long id) { + super(String.format("Comment with id [%s] is not found", id)); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CustomerNotFoundException.java b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CustomerNotFoundException.java new file mode 100644 index 0000000..203d048 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/CustomerNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.labwork.Lab4.Exceptions; + +public class CustomerNotFoundException extends RuntimeException { + public CustomerNotFoundException(Long id) { + super(String.format("Customer with id [%s] is not found", id)); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/JwtException.java b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/JwtException.java new file mode 100644 index 0000000..b406c30 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/JwtException.java @@ -0,0 +1,11 @@ +package ru.ulstu.is.labwork.Lab4.Exceptions; + +public class JwtException extends RuntimeException { + public JwtException(Throwable throwable) { + super(throwable); + } + + public JwtException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/PostNotFoundException.java b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/PostNotFoundException.java new file mode 100644 index 0000000..a297e7d --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/Exceptions/PostNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.labwork.Lab4.Exceptions; + +public class PostNotFoundException extends RuntimeException { + public PostNotFoundException(Long id) { + super(String.format("Post with id [%s] is not found", id)); + } +} diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CommentController.java b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CommentController.java index 9e9cb1d..02d6d9e 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CommentController.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CommentController.java @@ -1,6 +1,6 @@ package ru.ulstu.is.labwork.Lab4.controller; -import jakarta.validation.Valid; +import javax.validation.Valid; import org.springframework.web.bind.annotation.*; import ru.ulstu.is.labwork.Lab4.DTO.CommentDto; import ru.ulstu.is.labwork.Lab4.model.Comment; diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CustomerController.java b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CustomerController.java index 5c8e6a8..003394b 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CustomerController.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/CustomerController.java @@ -1,6 +1,8 @@ package ru.ulstu.is.labwork.Lab4.controller; -import jakarta.validation.Valid; +import javax.validation.Valid; + +import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.*; import ru.ulstu.is.labwork.Lab4.DTO.CustomerDto; import ru.ulstu.is.labwork.Lab4.model.Customer; @@ -12,6 +14,7 @@ import java.util.List; @RestController @RequestMapping(WebConfiguration.REST_API + "/customer") public class CustomerController { + public static final String URL_LOGIN = "/jwt/login"; private final CustomerService customerService; public CustomerController(CustomerService customerService) { @@ -50,4 +53,31 @@ public class CustomerController { public void deleteAllCustomers(){ customerService.deleteAllCustomers(); } + + @GetMapping("/find/{username}") + public CustomerDto getCustomerByUsername(@PathVariable String username) { + return new CustomerDto(customerService.findByUsername(username)); + } + + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid CustomerDto customerDto) { + return customerService.loginAndGetToken(customerDto); + } + + @GetMapping ("/me") + CustomerDto getCurrentCustomer(@RequestHeader(HttpHeaders.AUTHORIZATION) String token) { + return new CustomerDto(customerService.findByUsername(customerService.loadUserByToken(token.substring(7)).getUsername())); + } + + @GetMapping("role/{token}") + public String getRoleByToken(@PathVariable String token) { + var userDetails = customerService.loadUserByToken(token); + Customer customer = customerService.findByUsername(userDetails.getUsername()); + + if (customer != null) { + return customer.getRole().toString(); + } + + return null; + } } \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/PostController.java b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/PostController.java index 326cf43..0667ffc 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/controller/PostController.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/controller/PostController.java @@ -1,6 +1,6 @@ package ru.ulstu.is.labwork.Lab4.controller; -import jakarta.validation.Valid; +import javax.validation.Valid; import org.springframework.web.bind.annotation.*; import ru.ulstu.is.labwork.Lab4.DTO.CommentDto; import ru.ulstu.is.labwork.Lab4.DTO.PostDto; @@ -57,4 +57,15 @@ public class PostController { public void deleteAllPosts() { postService.deleteAllPosts(); } + + @GetMapping("/search") + public List searchPosts(@RequestParam(required = false) String query) { + if (query == null || query.isBlank()) { + return postService.findAllPosts().stream() + .map(PostDto::new) + .toList(); + } else { + return postService.searchPosts(query); + } + } } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Comment.java b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Comment.java index 7769e08..989afff 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Comment.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Comment.java @@ -1,6 +1,7 @@ package ru.ulstu.is.labwork.Lab4.model; -import jakarta.persistence.*; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; import java.util.Objects; @Entity diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Customer.java b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Customer.java index 14319bd..e05e9a4 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Customer.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Customer.java @@ -1,6 +1,9 @@ package ru.ulstu.is.labwork.Lab4.model; -import jakarta.persistence.*; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; + +import org.h2.engine.User; import java.util.*; @Entity @@ -17,6 +20,8 @@ public class Customer { @OneToMany(fetch = FetchType.EAGER, mappedBy = "customer", cascade = CascadeType.ALL) private List posts; + private UserRole role; + public Customer() { } @@ -24,6 +29,15 @@ public class Customer { public Customer(String username, String password) { this.username = username; this.password = password; + this.role = UserRole.USER; + this.comments = new ArrayList<>(); + this.posts = new ArrayList<>(); + } + + public Customer(String username, String password, UserRole role) { + this.username = username; + this.password = password; + this.role = role; this.comments = new ArrayList<>(); this.posts = new ArrayList<>(); } @@ -56,6 +70,10 @@ public class Customer { this.password = password; } + public UserRole getRole() { + return role; + } + @Override public boolean equals(Object obj) { if (this == obj) return true; diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Post.java b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Post.java index 0b585e1..ac7a66b 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/model/Post.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/model/Post.java @@ -1,6 +1,7 @@ package ru.ulstu.is.labwork.Lab4.model; -import jakarta.persistence.*; +import javax.persistence.*; +import javax.validation.constraints.NotBlank; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/model/UserRole.java b/src/main/java/ru/ulstu/is/labwork/Lab4/model/UserRole.java new file mode 100644 index 0000000..66229dd --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/model/UserRole.java @@ -0,0 +1,20 @@ +package ru.ulstu.is.labwork.Lab4.model; + +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"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Admin.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Admin.java new file mode 100644 index 0000000..753f30f --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Admin.java @@ -0,0 +1,70 @@ +package ru.ulstu.is.labwork.Lab4.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.validation.Valid; +import ru.ulstu.is.labwork.Lab4.DTO.CustomerDto; +import ru.ulstu.is.labwork.Lab4.model.UserRole; +import ru.ulstu.is.labwork.Lab4.services.CustomerService; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.*; + +@Controller +@RequestMapping("/admin") +@Secured({UserRole.AsString.ADMIN}) +public class Admin { + private final CustomerService customerService; + + public Admin(CustomerService customerService) { + this.customerService = customerService; + } + + @GetMapping(value = { "/", "/{id}" }) + @Secured({UserRole.AsString.ADMIN}) + public String getCustomers(@PathVariable(required = false) Long id, HttpServletRequest request, Model model) { + model.addAttribute("request", request); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); + if (id == null || id <= 0) { + model.addAttribute("customers", customerService.findAllCustomers().stream().map(CustomerDto::new).toList()); + return "admin"; + } else { + return "redirect:/customers/" + id; + } + } + + @PostMapping("/delete/{id}") + public String deleteCustomer(@PathVariable Long id) { + customerService.deleteCustomer(id); + return "redirect:/admin/"; + } + + @PostMapping(value = { "/", "/{id}"}) + public String manipulateCustomer(@PathVariable(required = false) Long id, @ModelAttribute @Valid CustomerDto customerDto, + HttpServletRequest request, + BindingResult bindingResult, + Model model) { + model.addAttribute("request", request); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); + if (bindingResult.hasErrors()) { + model.addAttribute("errors", bindingResult.getAllErrors()); + return "/admin"; + } + + if (id == null || id <= 0) { + customerService.addCustomer(customerDto.username, customerDto.password); + } else { + customerService.updateCustomer(id, customerDto.username, customerDto.password); + } + + return "redirect:/admin/"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Comments.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Comments.java index c2e471b..414ba35 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Comments.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Comments.java @@ -1,13 +1,15 @@ package ru.ulstu.is.labwork.Lab4.mvc; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import jakarta.validation.Valid; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.validation.Valid; import ru.ulstu.is.labwork.Lab4.DTO.CommentDto; import ru.ulstu.is.labwork.Lab4.DTO.PostDto; import ru.ulstu.is.labwork.Lab4.services.CommentService; import ru.ulstu.is.labwork.Lab4.services.CustomerService; import ru.ulstu.is.labwork.Lab4.services.PostService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -28,9 +30,11 @@ public class Comments { @PostMapping(value = { "/", "/{id}"}) public String manipulateComment(@PathVariable(required = false) Long id, @ModelAttribute @Valid CommentDto commentDto, - HttpServletRequest request, HttpSession session, BindingResult bindingResult, Model model) { + HttpServletRequest request, BindingResult bindingResult, Model model) { model.addAttribute("request", request); - model.addAttribute("session", session); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); model.addAttribute("posts", postService.findAllPosts().stream().map(PostDto::new).toList()); if (bindingResult.hasErrors()) { diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Customers.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Customers.java index 7d33374..d269a7d 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Customers.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Customers.java @@ -1,10 +1,12 @@ package ru.ulstu.is.labwork.Lab4.mvc; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; -import jakarta.validation.Valid; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.validation.Valid; import ru.ulstu.is.labwork.Lab4.DTO.CustomerDto; import ru.ulstu.is.labwork.Lab4.services.CustomerService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.validation.BindingResult; @@ -22,7 +24,9 @@ public class Customers { @GetMapping(value = {"", "/", "/{id}" }) public String getCustomers(@PathVariable(required = false) Long id, HttpServletRequest request, HttpSession session, Model model) { model.addAttribute("request", request); - model.addAttribute("session", session); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); if (id == null || id <= 0) { model.addAttribute("customers", customerService.findAllCustomers().stream().map(CustomerDto::new).toList()); } else { @@ -34,7 +38,6 @@ public class Customers { @PostMapping("/delete/{id}") public String deleteCustomer(@PathVariable Long id, HttpSession session) { - session.setAttribute("currentCustomerId", -1); customerService.deleteCustomer(id); return "redirect:/customers/"; } @@ -45,7 +48,9 @@ public class Customers { BindingResult bindingResult, Model model) { model.addAttribute("request", request); - model.addAttribute("session", session); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); if (bindingResult.hasErrors()) { model.addAttribute("errors", bindingResult.getAllErrors()); return "/customers"; diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Feed.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Feed.java index 36d731d..2c4d189 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Feed.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Feed.java @@ -1,13 +1,15 @@ package ru.ulstu.is.labwork.Lab4.mvc; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import org.springframework.web.bind.annotation.RequestParam; import ru.ulstu.is.labwork.Lab4.DTO.CustomerDto; import ru.ulstu.is.labwork.Lab4.DTO.PostDto; import ru.ulstu.is.labwork.Lab4.services.CommentService; import ru.ulstu.is.labwork.Lab4.services.CustomerService; import ru.ulstu.is.labwork.Lab4.services.PostService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -16,7 +18,7 @@ import java.util.ArrayList; import java.util.Objects; @Controller -@RequestMapping("/feed") +@RequestMapping(value = { "", "/feed" }) public class Feed { private final PostService postService; private final CustomerService customerService; @@ -32,16 +34,15 @@ public class Feed { public String getPosts(@RequestParam(required = false) String search, HttpServletRequest request, HttpSession session, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "5") int size, Model model) { model.addAttribute("request", request); - model.addAttribute("session", session); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); if (search == null) { model.addAttribute("posts", postService.findAllPosts().stream().map(PostDto::new).toList()); } else { - var posts = postService.searchPosts(page, size, search); - model.addAttribute("posts", posts.stream().map(PostDto::new).toList()); + model.addAttribute("posts", postService.searchPosts(search)); } model.addAttribute("customers", customerService.findAllCustomers().stream().map(CustomerDto::new).toList()); diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Posts.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Posts.java index 78c9c15..6fa389d 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Posts.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Posts.java @@ -1,15 +1,17 @@ package ru.ulstu.is.labwork.Lab4.mvc; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpSession; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; import ru.ulstu.is.labwork.Lab4.services.CommentService; import ru.ulstu.is.labwork.Lab4.services.CustomerService; import ru.ulstu.is.labwork.Lab4.services.PostService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.validation.BindingResult; -import jakarta.validation.Valid; +import javax.validation.Valid; import ru.ulstu.is.labwork.Lab4.DTO.PostDto; @Controller @@ -37,7 +39,9 @@ public class Posts { BindingResult bindingResult, Model model) { model.addAttribute("request", request); - model.addAttribute("session", session); + model.addAttribute("currentCustomerId", customerService.findByUsername( + ((UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUsername() + ).getId()); model.addAttribute("posts", postService.findAllPosts().stream().map(PostDto::new).toList()); if (bindingResult.hasErrors()) { diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Session.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Session.java deleted file mode 100644 index 38650b9..0000000 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/Session.java +++ /dev/null @@ -1,16 +0,0 @@ -package ru.ulstu.is.labwork.Lab4.mvc; - -import jakarta.servlet.http.HttpSession; -import org.springframework.http.ResponseEntity; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestParam; - -@Controller -public class Session { - @PostMapping("/update-session") - public ResponseEntity updateSession(@RequestParam("currentCustomerId") int currentCustomerId, HttpSession session) { - session.setAttribute("currentCustomerId", currentCustomerId); - return ResponseEntity.ok().build(); - } -} diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/UserSignUp.java b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/UserSignUp.java new file mode 100644 index 0000000..f8b4136 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/mvc/UserSignUp.java @@ -0,0 +1,53 @@ +package ru.ulstu.is.labwork.Lab4.mvc; + +import ru.ulstu.is.labwork.Lab4.DTO.UserSignupDto; +import ru.ulstu.is.labwork.Lab4.model.Customer; +import ru.ulstu.is.labwork.Lab4.services.CustomerService; +import ru.ulstu.is.labwork.Lab4.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.servlet.http.HttpServletRequest; +import javax.validation.Valid; + +@Controller +@RequestMapping(UserSignUp.SIGNUP_URL) +public class UserSignUp { + public static final String SIGNUP_URL = "/signup"; + + private final CustomerService customerService; + + public UserSignUp(CustomerService customerService) { + this.customerService = customerService; + } + + @GetMapping + public String showSignupForm(Model model) { + model.addAttribute("userDto", new UserSignupDto()); + return "signup"; + } + + @PostMapping + public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto, + BindingResult bindingResult, HttpServletRequest request, + Model model) { + model.addAttribute("request", request); + if (bindingResult.hasErrors()) { + model.addAttribute("errors", bindingResult.getAllErrors()); + return "signup"; + } + try { + final Customer customer = customerService.addCustomer( + userSignupDto.getUsername(), userSignupDto.getPassword()); + return "redirect:/login?created=" + customer.getUsername(); + } catch (ValidationException e) { + model.addAttribute("errors", e.getMessage()); + return "signup"; + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CommentRepository.java b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CommentRepository.java index 5198093..314591b 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CommentRepository.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CommentRepository.java @@ -13,5 +13,5 @@ import java.util.List; public interface CommentRepository extends JpaRepository { @Query("SELECT DISTINCT c FROM Comment c WHERE c.content LIKE %:tag%") - Page searchComments(@Param("tag") String tag, Pageable pageable); + List searchComments(@Param("tag") String tag); } \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CustomerRepository.java b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CustomerRepository.java index fd255e4..55c3e2b 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CustomerRepository.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/CustomerRepository.java @@ -4,4 +4,5 @@ import org.springframework.data.jpa.repository.JpaRepository; import ru.ulstu.is.labwork.Lab4.model.Customer; public interface CustomerRepository extends JpaRepository { + Customer findByUsername(String username); } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/PostRepository.java b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/PostRepository.java index 9fd26c0..959716e 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/PostRepository.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/repositories/PostRepository.java @@ -10,10 +10,6 @@ import org.springframework.data.repository.query.Param; import java.util.List; public interface PostRepository extends JpaRepository { - //@Query("SELECT DISTINCT p FROM Post p WHERE p.title LIKE %:tag% OR p.content LIKE %:tag%") - //List searchPosts(@Param("tag") String tag); @Query("SELECT DISTINCT p FROM Post p WHERE p.title LIKE %:tag% OR p.content LIKE %:tag%") - Page searchPosts(@Param("tag") String tag, Pageable pageable); - - //Page findDistinctByContentOrTitleLikeIgnoreCase + List searchPosts(@Param("tag") String tag); } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtFilter.java b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtFilter.java new file mode 100644 index 0000000..b79589b --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtFilter.java @@ -0,0 +1,73 @@ +package ru.ulstu.is.labwork.Lab4.security; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ru.ulstu.is.labwork.Lab4.Exceptions.JwtException; +import ru.ulstu.is.labwork.Lab4.services.CustomerService; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JwtFilter extends GenericFilterBean { + private static final String AUTHORIZATION = "Authorization"; + public static final String TOKEN_BEGIN_STR = "Bearer "; + + private final CustomerService customerService; + + public JwtFilter(CustomerService customerService) { + this.customerService = customerService; + } + + private String getTokenFromRequest(HttpServletRequest request) { + String bearer = request.getHeader(AUTHORIZATION); + if (StringUtils.hasText(bearer) && bearer.startsWith(TOKEN_BEGIN_STR)) { + return bearer.substring(TOKEN_BEGIN_STR.length()); + } + return null; + } + + private void raiseException(ServletResponse response, int status, String message) throws IOException { + if (response instanceof final HttpServletResponse httpResponse) { + httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); + httpResponse.setStatus(status); + final byte[] body = new ObjectMapper().writeValueAsBytes(message); + response.getOutputStream().write(body); + } + } + + @Override + public void doFilter(ServletRequest request, + ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (request instanceof final HttpServletRequest httpRequest) { + final String token = getTokenFromRequest(httpRequest); + if (StringUtils.hasText(token)) { + try { + final UserDetails user = customerService.loadUserByToken(token); + final UsernamePasswordAuthenticationToken auth = + new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); + } catch (JwtException e) { + raiseException(response, HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); + return; + } catch (Exception e) { + e.printStackTrace(); + raiseException(response, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, + String.format("Internal error: %s", e.getMessage())); + return; + } + } + } + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProperties.java b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProperties.java new file mode 100644 index 0000000..c224188 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.labwork.Lab4.security; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true) +public class JwtProperties { + private String devToken = ""; + private Boolean isDev = true; + + public String getDevToken() { + return devToken; + } + + public void setDevToken(String devToken) { + this.devToken = devToken; + } + + public Boolean isDev() { + return isDev; + } + + public void setDev(Boolean dev) { + isDev = dev; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProvider.java b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProvider.java new file mode 100644 index 0000000..06278d7 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/security/JwtProvider.java @@ -0,0 +1,108 @@ +package ru.ulstu.is.labwork.Lab4.security; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.auth0.jwt.interfaces.JWTVerifier; +import ru.ulstu.is.labwork.Lab4.Exceptions.JwtException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +@Component +public class JwtProvider { + private final static Logger LOG = LoggerFactory.getLogger(JwtProvider.class); + + private final static byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); + private final static String ISSUER = "auth0"; + + private final Algorithm algorithm; + private final JWTVerifier verifier; + + public JwtProvider(JwtProperties jwtProperties) { + if (!jwtProperties.isDev()) { + LOG.info("Generate new JWT key for prod"); + try { + final MessageDigest salt = MessageDigest.getInstance("SHA-256"); + salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); + LOG.info("Use generated JWT key for prod \n{}", bytesToHex(salt.digest())); + algorithm = Algorithm.HMAC256(bytesToHex(salt.digest())); + } catch (NoSuchAlgorithmException e) { + throw new JwtException(e); + } + } else { + LOG.info("Use default JWT key for dev \n{}", jwtProperties.getDevToken()); + algorithm = Algorithm.HMAC256(jwtProperties.getDevToken()); + } + verifier = JWT.require(algorithm) + .withIssuer(ISSUER) + .build(); + } + + private static String bytesToHex(byte[] bytes) { + byte[] hexChars = new byte[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = HEX_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; + } + return new String(hexChars, StandardCharsets.UTF_8); + } + + public String generateToken(String login) { + final Date issueDate = Date.from(LocalDate.now() + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + final Date expireDate = Date.from(LocalDate.now() + .plusDays(15) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + return JWT.create() + .withIssuer(ISSUER) + .withIssuedAt(issueDate) + .withExpiresAt(expireDate) + .withSubject(login) + .sign(algorithm); + } + + private DecodedJWT validateToken(String token) { + try { + return verifier.verify(token); + } catch (JWTVerificationException e) { + throw new JwtException(String.format("Token verification error: %s", e.getMessage())); + } + } + + public boolean isTokenValid(String token) { + if (!StringUtils.hasText(token)) { + return false; + } + try { + validateToken(token); + return true; + } catch (JwtException e) { + LOG.error(e.getMessage()); + return false; + } + } + + public Optional getLoginFromToken(String token) { + try { + return Optional.ofNullable(validateToken(token).getSubject()); + } catch (JwtException e) { + LOG.error(e.getMessage()); + return Optional.empty(); + } + } +} diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/security/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/is/labwork/Lab4/security/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..7ea0cf9 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/security/PasswordEncoderConfiguration.java @@ -0,0 +1,14 @@ +package ru.ulstu.is.labwork.Lab4.security; + +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(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/security/SecurityConfiguration.java b/src/main/java/ru/ulstu/is/labwork/Lab4/security/SecurityConfiguration.java new file mode 100644 index 0000000..f004b66 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/security/SecurityConfiguration.java @@ -0,0 +1,116 @@ +package ru.ulstu.is.labwork.Lab4.security; + +import ru.ulstu.is.labwork.Lab4.controller.CustomerController; +import ru.ulstu.is.labwork.Lab4.model.UserRole; +import ru.ulstu.is.labwork.Lab4.mvc.UserSignUp; +import ru.ulstu.is.labwork.Lab4.services.CustomerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +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; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; +import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import java.util.LinkedHashMap; + +@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"; + public static final String SPA_URL_MASK = "/{path:[^\\.]*}"; + private final CustomerService customerService; + private final JwtFilter jwtFilter; + + public SecurityConfiguration(CustomerService customerService) { + this.customerService = customerService; + this.jwtFilter = new JwtFilter(customerService); + createAdminOnStartup(); + } + + private void createAdminOnStartup() { + final String admin = "admin"; + if (customerService.findByUsername(admin) == null) { + log.info("Admin user successfully created"); + customerService.addCustomer(admin, admin, UserRole.ADMIN); + } + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.exceptionHandling().authenticationEntryPoint(delegatingEntryPoint()); + http.headers().frameOptions().sameOrigin().and() + .cors().and() + .csrf().disable() + .authorizeRequests() + .antMatchers(UserSignUp.SIGNUP_URL).permitAll() + .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll() + .anyRequest().authenticated() + .and() + .formLogin() + .loginPage(LOGIN_URL).permitAll() + .and() + .logout().permitAll(); + } + +// @Override +// protected void configure(HttpSecurity http) throws Exception { +// log.info("Creating security configuration"); +// http.cors() +// .and() +// .csrf().disable() +// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) +// .and() +// .authorizeRequests() +// .antMatchers("/", SPA_URL_MASK).permitAll() +// .antMatchers(HttpMethod.POST, WebConfiguration.REST_API + "/customer" + CustomerController.URL_LOGIN).permitAll() +// .antMatchers(HttpMethod.POST, WebConfiguration.REST_API + "/customer").permitAll() +// .anyRequest() +// .authenticated() +// .and() +// .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) +// .anonymous(); +// } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(customerService); + } + + @Override + public void configure(WebSecurity web) { + web.ignoring() + .antMatchers("/css/**") + .antMatchers("/js/**") + .antMatchers("/templates/**") + .antMatchers("/webjars/**") + .antMatchers("/swagger-resources/**") + .antMatchers("/v3/api-docs/**"); + } + + @Bean + public AuthenticationEntryPoint delegatingEntryPoint() { + final LinkedHashMap map = new LinkedHashMap(); + map.put(new AntPathRequestMatcher("/"), new LoginUrlAuthenticationEntryPoint("/login")); + map.put(new AntPathRequestMatcher("/api/1.0/**"), new Http403ForbiddenEntryPoint()); + + final DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map); + entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")); + + return entryPoint; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/services/CommentService.java b/src/main/java/ru/ulstu/is/labwork/Lab4/services/CommentService.java index 62c7bdc..f466472 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/services/CommentService.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/services/CommentService.java @@ -1,53 +1,44 @@ package ru.ulstu.is.labwork.Lab4.services; -import jakarta.persistence.EntityNotFoundException; -import jakarta.transaction.Transactional; -import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.annotation.Transactional; +import ru.ulstu.is.labwork.Lab4.Exceptions.CommentNotFoundException; import ru.ulstu.is.labwork.Lab4.model.Comment; import ru.ulstu.is.labwork.Lab4.model.Customer; import ru.ulstu.is.labwork.Lab4.model.Post; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import ru.ulstu.is.labwork.Lab4.repositories.CommentRepository; +import ru.ulstu.is.labwork.Lab4.util.validation.ValidatorUtil; +import javax.persistence.EntityNotFoundException; import java.util.List; import java.util.Optional; @Service public class CommentService { private final CommentRepository commentRepository; + private final ValidatorUtil validatorUtil; - - public CommentService(CommentRepository commentRepository) { + public CommentService(CommentRepository commentRepository, + ValidatorUtil validatorUtil) { this.commentRepository = commentRepository; + this.validatorUtil = validatorUtil; } - //@jakarta.transaction.Transactional - //public List searchComments(String tag) { - // return commentRepository.searchComments(tag, PageRequest.of(0,10)).stream().toList(); - //} - - - @Transactional + @Transactional(readOnly = true) public Comment findComment(Long id) { final Optional comment = commentRepository.findById(id); - return comment.orElseThrow(EntityNotFoundException::new); + return comment.orElseThrow(() -> new CommentNotFoundException(id)); } - @Transactional + @Transactional(readOnly = true) public List findAllComments() { return commentRepository.findAll(); } @Transactional public Comment addComment(Customer customer, Post post, String content) { - if (customer == null || post == null) { - throw new IllegalArgumentException("Invalid customer or post"); - } - if (!StringUtils.hasText(content)) { - throw new IllegalArgumentException("Invalid comment's content"); - } - final Comment comment = new Comment(customer, post, content); + validatorUtil.validate(comment); customer.getComments().add(comment); post.getComments().add(comment); return commentRepository.save(comment); @@ -55,12 +46,9 @@ public class CommentService { @Transactional public Comment updateComment(Long id, String content) { - if (!StringUtils.hasText(content)) { - throw new IllegalArgumentException("Comment's content is empty"); - } - final Comment currentComment = findComment(id); currentComment.setContent(content); + validatorUtil.validate(currentComment); return commentRepository.save(currentComment); } @@ -77,4 +65,9 @@ public class CommentService { public void deleteAllComments() { commentRepository.deleteAll(); } + + @Transactional + public List searchComments(String tag) { + return commentRepository.searchComments(tag); + } } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/services/CustomerService.java b/src/main/java/ru/ulstu/is/labwork/Lab4/services/CustomerService.java index 53ef72c..99e282d 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/services/CustomerService.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/services/CustomerService.java @@ -1,26 +1,47 @@ package ru.ulstu.is.labwork.Lab4.services; -import jakarta.persistence.EntityNotFoundException; -import jakarta.transaction.Transactional; -import ru.ulstu.is.labwork.Lab4.model.Customer; -import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; -import ru.ulstu.is.labwork.Lab4.repositories.CustomerRepository; +import javax.persistence.EntityNotFoundException; +import javax.transaction.Transactional; + +import org.springframework.util.StringUtils; +import ru.ulstu.is.labwork.Lab4.DTO.CustomerDto; +import ru.ulstu.is.labwork.Lab4.Exceptions.CustomerNotFoundException; +import ru.ulstu.is.labwork.Lab4.Exceptions.JwtException; +import ru.ulstu.is.labwork.Lab4.model.Customer; +import ru.ulstu.is.labwork.Lab4.model.UserRole; +import ru.ulstu.is.labwork.Lab4.repositories.CustomerRepository; +import ru.ulstu.is.labwork.Lab4.security.JwtProvider; +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 ru.ulstu.is.labwork.Lab4.util.validation.ValidatorUtil; + +import java.util.Collections; import java.util.List; import java.util.Optional; @Service -public class CustomerService { +public class CustomerService implements UserDetailsService { private final CustomerRepository customerRepository; + private final ValidatorUtil validatorUtil; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; - public CustomerService(CustomerRepository customerRepository) { + public CustomerService(CustomerRepository customerRepository, + ValidatorUtil validatorUtil, + PasswordEncoder passwordEncoder, + JwtProvider jwtProvider) { this.customerRepository = customerRepository; + this.validatorUtil = validatorUtil; + this.passwordEncoder = passwordEncoder; + this.jwtProvider = jwtProvider; } @Transactional public Customer findCustomer(Long id) { - final Optional customer = customerRepository.findById(id); - return customer.orElseThrow(EntityNotFoundException::new); + return customerRepository.findById(id).orElseThrow(() -> new CustomerNotFoundException(id)); } @Transactional @@ -30,21 +51,25 @@ public class CustomerService { @Transactional public Customer addCustomer(String username, String password) { - if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { - throw new IllegalArgumentException("Customer's username or password is empty"); - } - Customer customer = new Customer(username, password); + Customer customer = new Customer(username, passwordEncoder.encode(password)); + validatorUtil.validate(customer); return customerRepository.save(customer); } + @Transactional + public Customer addCustomer(String username, String password, UserRole role) { + Customer customer = new Customer(username, passwordEncoder.encode(password), role); + validatorUtil.validate(customer); + return customerRepository.save(customer); + } + + @Transactional public Customer updateCustomer(Long id, String username, String password) { - if (!StringUtils.hasText(username) || !StringUtils.hasText(password)) { - throw new IllegalArgumentException("Customer's username or password is empty"); - } Customer customer = findCustomer(id); customer.setUsername(username); - customer.setPassword(password); + customer.setPassword(passwordEncoder.encode(password)); + validatorUtil.validate(customer); return customerRepository.save(customer); } @@ -59,4 +84,43 @@ public class CustomerService { public void deleteAllCustomers() { customerRepository.deleteAll(); } + + public Customer findByUsername(String username) { + return customerRepository.findByUsername(username); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final Customer customerEntity = findByUsername(username); + if (customerEntity == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + customerEntity.getUsername(), customerEntity.getPassword(), Collections.singleton(customerEntity.getRole())); + } + + public String loginAndGetToken(CustomerDto customerDto) { + try { + final Customer customer = findByUsername(customerDto.getUsername()); + if (customer == null) { + throw new Exception("Login not found" + customerDto.getUsername()); + } + if (!passwordEncoder.matches(customerDto.getPassword(), customer.getPassword())) { + throw new Exception("User not found" + customer.getUsername()); + } + return jwtProvider.generateToken(customer.getUsername()); + } + catch (Exception e) { + return null; + } + } + + public UserDetails loadUserByToken(String token) throws UsernameNotFoundException { + if (!jwtProvider.isTokenValid(token)) { + throw new JwtException("Bad token"); + } + final String userLogin = jwtProvider.getLoginFromToken(token) + .orElseThrow(() -> new JwtException("Token is not contain Login")); + return loadUserByUsername(userLogin); + } } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/services/PostService.java b/src/main/java/ru/ulstu/is/labwork/Lab4/services/PostService.java index b343497..4bef2d2 100644 --- a/src/main/java/ru/ulstu/is/labwork/Lab4/services/PostService.java +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/services/PostService.java @@ -1,10 +1,11 @@ package ru.ulstu.is.labwork.Lab4.services; -import jakarta.persistence.EntityNotFoundException; -import jakarta.transaction.Transactional; +import javax.persistence.EntityNotFoundException; +import javax.transaction.Transactional; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import ru.ulstu.is.labwork.Lab4.DTO.PostDto; +import ru.ulstu.is.labwork.Lab4.Exceptions.PostNotFoundException; import ru.ulstu.is.labwork.Lab4.model.Comment; import ru.ulstu.is.labwork.Lab4.model.Customer; import ru.ulstu.is.labwork.Lab4.model.Post; @@ -12,6 +13,7 @@ import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import ru.ulstu.is.labwork.Lab4.repositories.CommentRepository; import ru.ulstu.is.labwork.Lab4.repositories.PostRepository; +import ru.ulstu.is.labwork.Lab4.util.validation.ValidatorUtil; import java.util.ArrayList; import java.util.List; @@ -22,16 +24,59 @@ import java.util.Optional; public class PostService { private final PostRepository postRepository; private final CommentRepository commentRepository; + private final ValidatorUtil validatorUtil; - public PostService(PostRepository postRepository, CommentRepository commentRepository) { + public PostService(PostRepository postRepository, + CommentRepository commentRepository, ValidatorUtil validatorUtil) { this.postRepository = postRepository; this.commentRepository = commentRepository; + this.validatorUtil = validatorUtil; } + @Transactional - public List searchPosts(int page, int size, String tag) { - //var posts = postRepository.searchPosts(tag); - var posts = new ArrayList<>(postRepository.searchPosts(tag, PageRequest.of(page, size)).getContent().stream().toList()); - var comments = new ArrayList<>(commentRepository.searchComments(tag, PageRequest.of(page, size)).getContent().stream().toList()); + public Post findPost(Long id) { + return postRepository.findById(id).orElseThrow(() -> new PostNotFoundException(id)); + } + + @Transactional + public List findAllPosts() { + return postRepository.findAll(); + } + + @Transactional + public Post addPost(Customer customer, String title, String content) { + Post post = new Post(customer, title, content); + validatorUtil.validate(post); + customer.getPosts().add(post); + return postRepository.save(post); + } + + @Transactional + public Post updatePost(Long id, String title, String content) { + Post post = findPost(id); + post.setTitle(title); + post.setContent(content); + validatorUtil.validate(post); + return postRepository.save(post); + } + + @Transactional + public Post deletePost(Long id) { + Post post = findPost(id); + post.getCustomer().getPosts().remove(post); + postRepository.delete(post); + return post; + } + + @Transactional + public void deleteAllPosts() { + postRepository.deleteAll(); + } + + @Transactional + public List searchPosts(String search) { + var posts = new ArrayList<>(postRepository.searchPosts(search)); + var comments = commentRepository.searchComments(search); for (var post: posts) { post.getComments().clear(); } @@ -51,54 +96,6 @@ public class PostService { posts.add(newPost); } } - return posts; - } - - @Transactional - public Post findPost(Long id) { - final Optional post = postRepository.findById(id); - return post.orElseThrow(EntityNotFoundException::new); - } - - @Transactional - public List findAllPosts() { - return postRepository.findAll(); - } - - @Transactional - public Post addPost(Customer customer, String title, String content) { - if (customer == null) { - throw new IllegalArgumentException("Invalid customer"); - } - if (!StringUtils.hasText(title) | !StringUtils.hasText(content)) { - throw new IllegalArgumentException("Invalid post's content or title"); - } - Post post = new Post(customer, title, content); - customer.getPosts().add(post); - return postRepository.save(post); - } - - @Transactional - public Post updatePost(Long id, String title, String content) { - if (!StringUtils.hasText(content) | !StringUtils.hasText(title)) { - throw new IllegalArgumentException("Post's content or title is empty"); - } - Post post = findPost(id); - post.setTitle(title); - post.setContent(content); - return postRepository.save(post); - } - - @Transactional - public Post deletePost(Long id) { - Post post = findPost(id); - post.getCustomer().getPosts().remove(post); - postRepository.delete(post); - return post; - } - - @Transactional - public void deleteAllPosts() { - postRepository.deleteAll(); + return posts.stream().map(PostDto::new).toList(); } } diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/util/error/AdviceController.java b/src/main/java/ru/ulstu/is/labwork/Lab4/util/error/AdviceController.java new file mode 100644 index 0000000..08d997e --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/util/error/AdviceController.java @@ -0,0 +1,43 @@ +package ru.ulstu.is.labwork.Lab4.util.error; + +import ru.ulstu.is.labwork.Lab4.Exceptions.CommentNotFoundException; +import ru.ulstu.is.labwork.Lab4.Exceptions.CustomerNotFoundException; +import ru.ulstu.is.labwork.Lab4.Exceptions.PostNotFoundException; +import ru.ulstu.is.labwork.Lab4.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({ + CommentNotFoundException.class, + CustomerNotFoundException.class, + PostNotFoundException.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); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidationException.java b/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidationException.java new file mode 100644 index 0000000..5cbaf83 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidationException.java @@ -0,0 +1,9 @@ +package ru.ulstu.is.labwork.Lab4.util.validation; + +import java.util.Set; + +public class ValidationException extends RuntimeException { + public ValidationException(Set errors) { + super(String.join("\n", errors)); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidatorUtil.java b/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidatorUtil.java new file mode 100644 index 0000000..edcf418 --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/Lab4/util/validation/ValidatorUtil.java @@ -0,0 +1,30 @@ +package ru.ulstu.is.labwork.Lab4.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())); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/OpenAPI30Configuration.java b/src/main/java/ru/ulstu/is/labwork/OpenAPI30Configuration.java new file mode 100644 index 0000000..2d51cce --- /dev/null +++ b/src/main/java/ru/ulstu/is/labwork/OpenAPI30Configuration.java @@ -0,0 +1,28 @@ +package ru.ulstu.is.labwork; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.ulstu.is.labwork.Lab4.security.JwtFilter; + +@Configuration +public class OpenAPI30Configuration { + public static final String API_PREFIX = "/api/1.0"; + + @Bean + public OpenAPI customizeOpenAPI() { + final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/labwork/WebConfiguration.java b/src/main/java/ru/ulstu/is/labwork/WebConfiguration.java index ec33660..7c948ad 100644 --- a/src/main/java/ru/ulstu/is/labwork/WebConfiguration.java +++ b/src/main/java/ru/ulstu/is/labwork/WebConfiguration.java @@ -1,12 +1,33 @@ package ru.ulstu.is.labwork; +import ru.ulstu.is.labwork.Lab4.security.SecurityConfiguration; +import org.springframework.boot.web.server.ErrorPage; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpStatus; import org.springframework.web.servlet.config.annotation.CorsRegistry; +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"; + public static final String REST_API = OpenAPI30Configuration.API_PREFIX; + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + WebMvcConfigurer.super.addViewControllers(registry); + registry.addViewController("login"); + registry.addViewController(SecurityConfiguration.SPA_URL_MASK).setViewName("forward:/"); + registry.addViewController("/notFound").setViewName("forward:/"); + } + + @Bean + public WebServerFactoryCustomizer containerCustomizer() { + return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound")); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedMethods("*"); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da7b0b1..055f74b 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,5 @@ spring.jpa.hibernate.ddl-auto=update spring.h2.console.enabled=true spring.h2.console.settings.trace=false spring.h2.console.settings.web-allow-others=false +jwt.dev-token=my-secret-jwt +jwt.dev=true diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html new file mode 100644 index 0000000..231ba38 --- /dev/null +++ b/src/main/resources/templates/admin.html @@ -0,0 +1,103 @@ + + + + + +
+
+
+
+

Профили

+
+
+
+
+ +
+
+
+ +

Список профилей

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + +
+ +
+
+
+
+
+
+
+ + + + + +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/customers.html b/src/main/resources/templates/customers.html index 5dc9af5..4259089 100644 --- a/src/main/resources/templates/customers.html +++ b/src/main/resources/templates/customers.html @@ -6,11 +6,8 @@

Профили

- -

Список

+
@@ -37,7 +34,7 @@
-
+
@@ -48,27 +45,6 @@
- - -