From fed78e90b77a825fab5c095be4ef10dca6acee4b Mon Sep 17 00:00:00 2001 From: shadowik Date: Mon, 15 May 2023 13:56:26 +0400 Subject: [PATCH] Lab 6 Jwt start --- build.gradle | 3 + .../configuration/OpenAPI30Configuration.java | 29 +++++ .../PasswordEncoderConfiguration.java | 14 +++ .../configuration/SecurityConfiguration.java | 92 +++++++++++++++ .../{ => configuration}/WebConfiguration.java | 22 ++-- .../demo/configuration/jwt/JwtException.java | 11 ++ .../demo/configuration/jwt/JwtFilter.java | 72 ++++++++++++ .../demo/configuration/jwt/JwtProperties.java | 27 +++++ .../demo/configuration/jwt/JwtProvider.java | 107 ++++++++++++++++++ .../java/com/example/demo/master/Master.java | 18 ++- .../example/demo/master/MasterController.java | 85 +++++++------- .../demo/master/MasterMvcController.java | 84 -------------- .../example/demo/master/MasterRepository.java | 2 + .../com/example/demo/master/MasterRole.java | 20 ++++ .../example/demo/master/MasterService.java | 68 +++++++++-- .../example/demo/master/MasterSignupDto.java | 35 ++++++ .../example/demo/order/OrderController.java | 18 +-- .../demo/order/OrderMvcController.java | 66 ----------- .../demo/product/ProductController.java | 6 +- .../demo/product/ProductMvcController.java | 102 ----------------- .../util/validation/ValidationException.java | 4 + src/main/resources/templates/Header.html | 38 ------- src/main/resources/templates/Login.html | 38 ------- .../resources/templates/MastersOrders.html | 51 --------- src/main/resources/templates/OrderPage.html | 48 -------- .../resources/templates/ProductCreate.html | 27 ----- src/main/resources/templates/Products.html | 38 ------- src/main/resources/templates/UserPage.html | 55 --------- .../resources/templates/UserProducts.html | 37 ------ 29 files changed, 551 insertions(+), 666 deletions(-) create mode 100644 src/main/java/com/example/demo/configuration/OpenAPI30Configuration.java create mode 100644 src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java create mode 100644 src/main/java/com/example/demo/configuration/SecurityConfiguration.java rename src/main/java/com/example/demo/{ => configuration}/WebConfiguration.java (73%) create mode 100644 src/main/java/com/example/demo/configuration/jwt/JwtException.java create mode 100644 src/main/java/com/example/demo/configuration/jwt/JwtFilter.java create mode 100644 src/main/java/com/example/demo/configuration/jwt/JwtProperties.java create mode 100644 src/main/java/com/example/demo/configuration/jwt/JwtProvider.java delete mode 100644 src/main/java/com/example/demo/master/MasterMvcController.java create mode 100644 src/main/java/com/example/demo/master/MasterRole.java create mode 100644 src/main/java/com/example/demo/master/MasterSignupDto.java delete mode 100644 src/main/java/com/example/demo/order/OrderMvcController.java delete mode 100644 src/main/java/com/example/demo/product/ProductMvcController.java delete mode 100644 src/main/resources/templates/Header.html delete mode 100644 src/main/resources/templates/Login.html delete mode 100644 src/main/resources/templates/MastersOrders.html delete mode 100644 src/main/resources/templates/OrderPage.html delete mode 100644 src/main/resources/templates/ProductCreate.html delete mode 100644 src/main/resources/templates/Products.html delete mode 100644 src/main/resources/templates/UserPage.html delete mode 100644 src/main/resources/templates/UserProducts.html diff --git a/build.gradle b/build.gradle index b5f8c19..7745303 100644 --- a/build.gradle +++ b/build.gradle @@ -17,6 +17,7 @@ jar { } dependencies { + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-devtools' @@ -27,6 +28,8 @@ dependencies { implementation 'org.webjars:font-awesome:6.1.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'com.auth0:java-jwt:4.4.0' implementation 'com.h2database:h2:2.1.210' implementation 'org.hibernate.validator:hibernate-validator' diff --git a/src/main/java/com/example/demo/configuration/OpenAPI30Configuration.java b/src/main/java/com/example/demo/configuration/OpenAPI30Configuration.java new file mode 100644 index 0000000..983cdc0 --- /dev/null +++ b/src/main/java/com/example/demo/configuration/OpenAPI30Configuration.java @@ -0,0 +1,29 @@ +package com.example.demo.configuration; + + +import com.example.demo.configuration.jwt.JwtFilter; +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; + +@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"))); + } +} diff --git a/src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java b/src/main/java/com/example/demo/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..6a94e4f --- /dev/null +++ b/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 passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/com/example/demo/configuration/SecurityConfiguration.java b/src/main/java/com/example/demo/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..80e6a43 --- /dev/null +++ b/src/main/java/com/example/demo/configuration/SecurityConfiguration.java @@ -0,0 +1,92 @@ +package com.example.demo.configuration; + + +import com.example.demo.configuration.jwt.JwtFilter; +import com.example.demo.master.MasterController; +import com.example.demo.master.MasterRole; +import com.example.demo.master.MasterService; +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.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity( + securedEnabled = true +) +public class SecurityConfiguration { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + public static final String SPA_URL_MASK = "/{path:[^\\.]*}"; + private final MasterService masterService; + private final JwtFilter jwtFilter; + + public SecurityConfiguration(MasterService masterService) { + this.masterService = masterService; + this.jwtFilter = new JwtFilter(masterService); + createAdminOnStartup(); + } + + private void createAdminOnStartup() { + final String admin = "admin"; + if (masterService.findMaster(admin) == null) { + log.info("Admin user successfully created"); + masterService.addMaster(admin, admin, admin, admin, MasterRole.ADMIN); + } + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.cors() + .and() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeHttpRequests() + .requestMatchers("/", SPA_URL_MASK).permitAll() + .requestMatchers(HttpMethod.POST, MasterController.URL_LOGIN).permitAll() + .requestMatchers(HttpMethod.POST, MasterController.URL_SING_UP).permitAll() + .requestMatchers(HttpMethod.POST, MasterController.URL_WHO_AM_I).permitAll() + .anyRequest() + .authenticated() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous(); + return http.build(); + } + + @Bean + public AuthenticationManager authenticationManagerBean(HttpSecurity http) throws Exception { + AuthenticationManagerBuilder authenticationManagerBuilder = http + .getSharedObject(AuthenticationManagerBuilder.class); + authenticationManagerBuilder.userDetailsService(masterService); + return authenticationManagerBuilder.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring() + .requestMatchers(HttpMethod.OPTIONS, "/**") + .requestMatchers("/*.js") + .requestMatchers("/*.html") + .requestMatchers("/*.css") + .requestMatchers("/assets/**") + .requestMatchers("/favicon.ico") + .requestMatchers("/.js", "/.css") + .requestMatchers("/swagger-ui/index.html") + .requestMatchers("/webjars/**") + .requestMatchers("/swagger-resources/**") + .requestMatchers("/v3/api-docs/**") + .requestMatchers("/h2-console"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/WebConfiguration.java b/src/main/java/com/example/demo/configuration/WebConfiguration.java similarity index 73% rename from src/main/java/com/example/demo/WebConfiguration.java rename to src/main/java/com/example/demo/configuration/WebConfiguration.java index ba35358..d3a5cd4 100644 --- a/src/main/java/com/example/demo/WebConfiguration.java +++ b/src/main/java/com/example/demo/configuration/WebConfiguration.java @@ -1,4 +1,4 @@ -package com.example.demo; +package com.example.demo.configuration; import org.springframework.boot.web.server.ErrorPage; import org.springframework.boot.web.server.WebServerFactoryCustomizer; @@ -12,23 +12,19 @@ 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 addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**").allowedMethods("*"); - } - @Override public void addViewControllers(ViewControllerRegistry registry) { - ViewControllerRegistration registration = registry.addViewController("/notFound"); - registration.setViewName("forward:/index.html"); - registration.setStatusCode(HttpStatus.OK); + 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")); - }; + return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound")); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedMethods("*"); } } \ No newline at end of file diff --git a/src/main/java/com/example/demo/configuration/jwt/JwtException.java b/src/main/java/com/example/demo/configuration/jwt/JwtException.java new file mode 100644 index 0000000..741cc69 --- /dev/null +++ b/src/main/java/com/example/demo/configuration/jwt/JwtException.java @@ -0,0 +1,11 @@ +package com.example.demo.configuration.jwt; + +public class JwtException extends RuntimeException { + public JwtException(Throwable throwable) { + super(throwable); + } + + public JwtException(String message) { + super(message); + } +} diff --git a/src/main/java/com/example/demo/configuration/jwt/JwtFilter.java b/src/main/java/com/example/demo/configuration/jwt/JwtFilter.java new file mode 100644 index 0000000..a0e24ee --- /dev/null +++ b/src/main/java/com/example/demo/configuration/jwt/JwtFilter.java @@ -0,0 +1,72 @@ +package com.example.demo.configuration.jwt; + +import com.example.demo.master.MasterService; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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 java.io.IOException; + +public class JwtFilter extends GenericFilterBean { + private static final String AUTHORIZATION = "Authorization"; + public static final String TOKEN_BEGIN_STR = "Bearer "; + + private final MasterService masterService; + + public JwtFilter(MasterService masterService) { + this.masterService = masterService; + } + + 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 = masterService.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); + } +} diff --git a/src/main/java/com/example/demo/configuration/jwt/JwtProperties.java b/src/main/java/com/example/demo/configuration/jwt/JwtProperties.java new file mode 100644 index 0000000..d9661aa --- /dev/null +++ b/src/main/java/com/example/demo/configuration/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package com.example.demo.configuration.jwt; + +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; + } +} diff --git a/src/main/java/com/example/demo/configuration/jwt/JwtProvider.java b/src/main/java/com/example/demo/configuration/jwt/JwtProvider.java new file mode 100644 index 0000000..d001b3a --- /dev/null +++ b/src/main/java/com/example/demo/configuration/jwt/JwtProvider.java @@ -0,0 +1,107 @@ +package com.example.demo.configuration.jwt; + +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 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/com/example/demo/master/Master.java b/src/main/java/com/example/demo/master/Master.java index 0995eae..db4192b 100644 --- a/src/main/java/com/example/demo/master/Master.java +++ b/src/main/java/com/example/demo/master/Master.java @@ -1,9 +1,8 @@ package com.example.demo.master; -import com.example.demo.order.Order; +import com.example.demo.master.MasterRole; import jakarta.persistence.*; -import java.util.List; import java.util.Objects; @Entity @@ -19,14 +18,17 @@ public class Master { private String email; private String password; + private MasterRole role; + public Master() { } - public Master(String firstName, String lastName, String email, String password) { + public Master(String firstName, String lastName, String email, String password, MasterRole role) { this.firstName = firstName; this.lastName = lastName; this.email = email; this.password = password; + this.role = role; } public Long getId() { @@ -45,6 +47,10 @@ public class Master { public String getPassword() { return password; } + public MasterRole getRole() { + return role; + } + public void setFirstName(String firstName) { this.firstName = firstName; } @@ -61,6 +67,10 @@ public class Master { this.password = password; } + public void setRole(MasterRole role) { + this.role = role; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -84,4 +94,4 @@ public class Master { ", password='" + password + '\'' + '}'; } -} +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/master/MasterController.java b/src/main/java/com/example/demo/master/MasterController.java index ff78f88..976dc7f 100644 --- a/src/main/java/com/example/demo/master/MasterController.java +++ b/src/main/java/com/example/demo/master/MasterController.java @@ -1,74 +1,75 @@ package com.example.demo.master; -import com.example.demo.WebConfiguration; +import com.example.demo.configuration.OpenAPI30Configuration; +import com.example.demo.order.Order; import com.example.demo.order.OrderService; -import com.example.demo.product.ProductDto; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PatchMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import com.example.demo.util.validation.ValidationException; +import jakarta.validation.Valid; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; @RestController -@RequestMapping(WebConfiguration.REST_API + "/master") public class MasterController { private final MasterService masterService; private final OrderService orderService; + public static final String URL_LOGIN = "/jwt/login"; + public static final String URL_SING_UP = "/sing_up"; + public static final String URL_WHO_AM_I = "/who_am_i"; public MasterController(MasterService masterService, OrderService orderService) { this.masterService = masterService; this.orderService = orderService; } - @GetMapping("/") - public MasterDto getCurrentMaster() { - return new MasterDto(masterService.findMaster(masterService.getCurrentMasterId())); - - } - @GetMapping("/{email}/{password}") - public MasterDto getCurrentMaster(@PathVariable("email") String email, - @PathVariable("password") String password) { - var master = new MasterDto(masterService.findMaster(email, password)); - masterService.setCurrentMasterId(master.getId()); - return master; - } - @PostMapping("/") - public MasterDto createMaster(@RequestParam("firstName") String firstName, - @RequestParam("lastName") String lastName, - @RequestParam("email") String email, - @RequestParam("password") String password) { - MasterDto master = new MasterDto(masterService.addMaster(firstName, lastName, email, password)); - masterService.setCurrentMasterId(master.getId()); - orderService.addOrder(master.getId()); - return master; + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid MasterDto userDto) { + return masterService.loginAndGetToken(userDto); } - @PatchMapping("/") + @PostMapping(URL_SING_UP) + public String singUp(@RequestBody MasterSignupDto masterSignupDto) { + try { + final Master master = masterService.addMaster(masterSignupDto.getFirstName(), masterSignupDto.getLastName(), + masterSignupDto.getEmail(), masterSignupDto.getPassword(), MasterRole.USER); + final Order order = orderService.addOrder(master.getId()); + return "created " + master.getEmail(); + } catch (ValidationException e) { + return e.getMessage(); + } + } + + @GetMapping(URL_WHO_AM_I) + public String whoAmI(@RequestParam("token") String token) { + UserDetails userDetails = masterService.loadUserByToken(token); + Master master = masterService.findMaster(userDetails.getUsername()); + return master.getRole().toString(); + } + + @GetMapping(OpenAPI30Configuration.API_PREFIX + "/master") + public MasterDto getCurrentMaster(Principal principal) { + return new MasterDto(masterService.findMaster(principal.getName())); + } + + @PatchMapping(OpenAPI30Configuration.API_PREFIX + "/master") public MasterDto updateMaster(@RequestParam("firstName") String firstName, @RequestParam("lastName") String lastName, @RequestParam("email") String email, - @RequestParam("password") String password) { - return new MasterDto(masterService.updateMaster(masterService.getCurrentMasterId(), + @RequestParam("password") String password, + Principal principal) { + return new MasterDto(masterService.updateMaster(masterService.findMaster(principal.getName()).getId(), firstName, lastName, email, password)); } @DeleteMapping("/") public MasterDto deleteMaster(@PathVariable Long id) { - return new MasterDto(masterService.deleteMaster(masterService.getCurrentMasterId())); + return new MasterDto(masterService.deleteMaster(id)); } - @PostMapping("/log_out") - public void logOut() { - masterService.setCurrentMasterId(0L); - } - - @GetMapping("/all") + @GetMapping(OpenAPI30Configuration.API_PREFIX + "/master/all") public List GetMasters(){ return masterService.findAllMasters().stream().map(MasterDto::new).toList(); } diff --git a/src/main/java/com/example/demo/master/MasterMvcController.java b/src/main/java/com/example/demo/master/MasterMvcController.java deleted file mode 100644 index 115decb..0000000 --- a/src/main/java/com/example/demo/master/MasterMvcController.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.example.demo.master; - -import com.example.demo.order.OrderService; -import com.example.demo.product.ProductDto; -import com.example.demo.product.ProductService; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; - -@Controller -@RequestMapping("/master") -public class MasterMvcController { - private final MasterService masterService; - private final OrderService orderService; - - public MasterMvcController(MasterService masterService, OrderService orderService) { - this.masterService = masterService; - this.orderService = orderService; - } - - @GetMapping("/login") - public String loginPage(Model model) { - if (masterService.getCurrentMasterId() != 0) { - return "redirect:/product"; - } - model.addAttribute("user", new Master()); - return "Login"; - } - - @PostMapping("/login") - public String login(@ModelAttribute Master user) { - var master = new MasterDto(masterService.findMaster(user.getEmail(), user.getPassword())); - masterService.setCurrentMasterId(master.getId()); - return "redirect:/product/my_products"; - } - - @GetMapping("") - public String getUserPage(Model model) { - if (masterService.getCurrentMasterId() != 0) { - model.addAttribute("user", masterService.findMaster(masterService.getCurrentMasterId())); - } else { - return "redirect:/master/register"; - } - return "UserPage"; - } - - @PostMapping(value = "", params = "action=update") - public String updateUserPage(@ModelAttribute Master user) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/master"; - } - masterService.updateMaster( - masterService.getCurrentMasterId(), - user.getFirstName(), - user.getLastName(), - user.getEmail(), - user.getPassword()); - return "redirect:/product"; - } - - @PostMapping(value = "", params = "action=log_out") - public String logOut() { - masterService.setCurrentMasterId(0L); - return "redirect:/product"; - } - - @GetMapping("/register") - public String registerPage(Model model) { - model.addAttribute("user", new Master()); - return "UserPage"; - } - - @PostMapping(value = "/register", params = "action=register") - public String register(@ModelAttribute Master user) { - MasterDto master = new MasterDto(masterService.addMaster( - user.getFirstName(), - user.getLastName(), - user.getEmail(), - user.getPassword())); - masterService.setCurrentMasterId(master.getId()); - orderService.addOrder(master.getId()); - return "redirect:/product/my_products"; - } -} diff --git a/src/main/java/com/example/demo/master/MasterRepository.java b/src/main/java/com/example/demo/master/MasterRepository.java index 1faca4f..5df3d84 100644 --- a/src/main/java/com/example/demo/master/MasterRepository.java +++ b/src/main/java/com/example/demo/master/MasterRepository.java @@ -7,4 +7,6 @@ import java.util.Optional; public interface MasterRepository extends JpaRepository { Optional findByEmail(String email); + + Master findOneByEmailIgnoreCase(String login); } diff --git a/src/main/java/com/example/demo/master/MasterRole.java b/src/main/java/com/example/demo/master/MasterRole.java new file mode 100644 index 0000000..c9ad9ba --- /dev/null +++ b/src/main/java/com/example/demo/master/MasterRole.java @@ -0,0 +1,20 @@ +package com.example.demo.master; + +import org.springframework.security.core.GrantedAuthority; + +public enum MasterRole 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/com/example/demo/master/MasterService.java b/src/main/java/com/example/demo/master/MasterService.java index d45af85..0c2f789 100644 --- a/src/main/java/com/example/demo/master/MasterService.java +++ b/src/main/java/com/example/demo/master/MasterService.java @@ -1,32 +1,51 @@ package com.example.demo.master; +import com.example.demo.configuration.jwt.JwtException; +import com.example.demo.configuration.jwt.JwtProvider; import com.example.demo.order.Order; import com.example.demo.order.OrderController; import com.example.demo.order.OrderService; +import com.example.demo.util.validation.ValidationException; import com.example.demo.util.validation.ValidatorUtil; +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 org.springframework.transaction.annotation.Transactional; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @Service -public class MasterService { +public class MasterService implements UserDetailsService { private final MasterRepository masterRepository; private final ValidatorUtil validatorUtil; + private final PasswordEncoder passwordEncoder; - private Long currentMasterId = 0L; - - public MasterService(MasterRepository masterRepository, ValidatorUtil validatorUtil) { + private final JwtProvider jwtProvider; + public MasterService(MasterRepository masterRepository, ValidatorUtil validatorUtil, + PasswordEncoder passwordEncoder, JwtProvider jwtProvider) { this.masterRepository = masterRepository; this.validatorUtil = validatorUtil; + this.passwordEncoder = passwordEncoder; + this.jwtProvider = jwtProvider; + } + + @Transactional + public Master addMaster(String firstName, String lastName, String email, String password, MasterRole role) { + final Master master = new Master(firstName, lastName, email, passwordEncoder.encode(password), role); + validatorUtil.validate(master); + + return masterRepository.save(master); } @Transactional public Master addMaster(String firstName, String lastName, String email, String password) { - final Master master = new Master(firstName, lastName, email, password); + final Master master = new Master(firstName, lastName, email, passwordEncoder.encode(password), MasterRole.USER); validatorUtil.validate(master); return masterRepository.save(master); } @@ -48,6 +67,11 @@ public class MasterService { return realMaster; } + @Transactional(readOnly = true) + public Master findMaster(String email) { + return masterRepository.findOneByEmailIgnoreCase(email); + } + @Transactional(readOnly = true) public List findAllMasters() { return masterRepository.findAll(); @@ -76,14 +100,34 @@ public class MasterService { masterRepository.deleteAll(); } - @Transactional - public Long getCurrentMasterId (){ - return currentMasterId; + public String loginAndGetToken(MasterDto userDto) { + final Master master = findMaster(userDto.getEmail()); + if (master == null) { + throw new MasterNotFoundException(userDto.getEmail()); + } + if (!passwordEncoder.matches(userDto.getPassword(), master.getPassword())) { + throw new ValidationException("Incorrect password"); + } + return jwtProvider.generateToken(master.getEmail()); } - @Transactional - public void setCurrentMasterId(Long masterId) { - currentMasterId = masterId; + 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); } -} + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final Master userEntity = findMaster(username); + if (userEntity == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + userEntity.getEmail(), userEntity.getPassword(), Collections.singleton(userEntity.getRole())); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/demo/master/MasterSignupDto.java b/src/main/java/com/example/demo/master/MasterSignupDto.java new file mode 100644 index 0000000..fc37f3f --- /dev/null +++ b/src/main/java/com/example/demo/master/MasterSignupDto.java @@ -0,0 +1,35 @@ +package com.example.demo.master; + +public class MasterSignupDto { + private String firstName; + private String lastName; + private String email; + private String password; + + + public String getFirstName() { + return firstName; + } + public String getLastName() { + return lastName; + } + public String getEmail() {return email; } + + public String getPassword() { return password; } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public void setEmail(String email) { + this.email = email; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/src/main/java/com/example/demo/order/OrderController.java b/src/main/java/com/example/demo/order/OrderController.java index a193878..f4a0617 100644 --- a/src/main/java/com/example/demo/order/OrderController.java +++ b/src/main/java/com/example/demo/order/OrderController.java @@ -1,13 +1,15 @@ package com.example.demo.order; -import com.example.demo.WebConfiguration; +import com.example.demo.configuration.OpenAPI30Configuration; +import com.example.demo.configuration.WebConfiguration; import com.example.demo.master.MasterService; import org.springframework.web.bind.annotation.*; +import java.security.Principal; import java.util.List; @RestController -@RequestMapping(WebConfiguration.REST_API + "/order") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/order") public class OrderController { private final OrderService orderService; private final MasterService masterService; @@ -23,8 +25,8 @@ public class OrderController { } @DeleteMapping("/") - public void buyProducts() { - orderService.buyProducts(masterService.getCurrentMasterId()); + public void buyProducts(Principal principal) { + orderService.buyProducts(masterService.findMaster(principal.getName()).getId()); } @GetMapping("/") @@ -38,13 +40,13 @@ public class OrderController { } @PostMapping("/{product}") - public void addProduct(@PathVariable("product") Long productId) { - orderService.addProduct(masterService.getCurrentMasterId(), productId); + public void addProduct(@PathVariable("product") Long productId, Principal principal) { + orderService.addProduct(masterService.findMaster(principal.getName()).getId(), productId); } @DeleteMapping("/{product}") - public void deleteProduct(@PathVariable("product") Long productId) { - orderService.deleteProduct(masterService.getCurrentMasterId(), productId); + public void deleteProduct(@PathVariable("product") Long productId, Principal principal) { + orderService.deleteProduct(masterService.findMaster(principal.getName()).getId(), productId); } @GetMapping("/findOrders/{masterId}") diff --git a/src/main/java/com/example/demo/order/OrderMvcController.java b/src/main/java/com/example/demo/order/OrderMvcController.java deleted file mode 100644 index 1ebbb3a..0000000 --- a/src/main/java/com/example/demo/order/OrderMvcController.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.example.demo.order; - -import com.example.demo.master.MasterService; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; - -import java.util.ArrayList; -import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; - -@Controller -@RequestMapping("/order") -public class OrderMvcController { - private final MasterService masterService; - private final OrderService orderService; - - public OrderMvcController(MasterService masterService, OrderService orderService) { - this.masterService = masterService; - this.orderService = orderService; - } - - @GetMapping("") - public String getOrder(Model model) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/master/login"; - } - model.addAttribute("user", masterService.findMaster(masterService.getCurrentMasterId())); - model.addAttribute("order", orderService.findOrder(masterService.getCurrentMasterId())); - AtomicInteger fullCost = new AtomicInteger(); - orderService.findOrder(masterService.getCurrentMasterId()).getProducts().forEach( - item -> fullCost.addAndGet(item.getCost())); - model.addAttribute("fullCost", fullCost); - return "OrderPage"; - } - - @PostMapping(value = "", params = "action=delete") - public String deleteProduct(@RequestParam(value = "id", required = true) Long id) { - orderService.deleteProduct(masterService.getCurrentMasterId(), id); - return "redirect:/order"; - } - - @PostMapping(value = "", params = "action=buy") - public String buyProducts() { - orderService.buyProducts(masterService.getCurrentMasterId()); - return "redirect:/product"; - } - - @GetMapping("/masters_order") - public String MastersOrders(Model model, @RequestParam(value = "master_id", defaultValue = "-1") String masterId) { - model.addAttribute("user", masterService.findMaster(masterService.getCurrentMasterId())); - model.addAttribute("masters", masterService.findAllMasters()); - - if (!Objects.equals(masterId, "-1")) { - model.addAttribute("orders", orderService.findMastersOrders(Long.valueOf(masterId)). - stream().map(OrderDto::new).toList()); - } - else { - model.addAttribute("orders", new ArrayList()); - } - return "MastersOrders"; - } -} diff --git a/src/main/java/com/example/demo/product/ProductController.java b/src/main/java/com/example/demo/product/ProductController.java index ab975a8..64e6650 100644 --- a/src/main/java/com/example/demo/product/ProductController.java +++ b/src/main/java/com/example/demo/product/ProductController.java @@ -1,7 +1,7 @@ package com.example.demo.product; -import com.example.demo.WebConfiguration; -import org.springframework.beans.factory.annotation.Autowired; +import com.example.demo.configuration.OpenAPI30Configuration; +import com.example.demo.configuration.WebConfiguration; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController -@RequestMapping(WebConfiguration.REST_API + "/product") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/product") public class ProductController { private final ProductService productService; diff --git a/src/main/java/com/example/demo/product/ProductMvcController.java b/src/main/java/com/example/demo/product/ProductMvcController.java deleted file mode 100644 index 4b83fdd..0000000 --- a/src/main/java/com/example/demo/product/ProductMvcController.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.example.demo.product; - -import com.example.demo.master.Master; -import com.example.demo.master.MasterService; -import com.example.demo.order.OrderService; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.*; - - -@Controller -@RequestMapping("/product") -public class ProductMvcController { - private final ProductService productService; - private final MasterService masterService; - private final OrderService orderService; - public ProductMvcController(MasterService masterService, - ProductService productService, - OrderService orderService) { - this.masterService = masterService; - this.productService = productService; - this.orderService = orderService; - } - - @GetMapping("") - public String getProducts(Model model) { - if (masterService.getCurrentMasterId() != 0) { - Master user = masterService.findMaster(masterService.getCurrentMasterId()); - model.addAttribute("user", user); - } - else { - model.addAttribute("user", new Master()); - } - model.addAttribute("products", productService.findAllProducts() - .stream().map(ProductDto::new).toList()); - return "Products"; - } - - @PostMapping("") - public String addProductToOrder(@RequestParam(value = "id", required = true) Long id) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/master/login"; - } - orderService.addProduct(masterService.getCurrentMasterId(), id); - return "redirect:/product"; - } - - - @GetMapping("/my_products") - public String getMasterProduct(Model model) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/product"; - } - model.addAttribute("user", - masterService.findMaster(masterService.getCurrentMasterId())); - model.addAttribute("products", - productService.findProducts(masterService.getCurrentMasterId()).stream().map(ProductDto::new).toList()); - return "UserProducts"; - } - - @GetMapping("/create_product") - public String createProductPage(Model model) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/product"; - } - model.addAttribute("user", masterService.findMaster(masterService.getCurrentMasterId())); - model.addAttribute("product", new Product()); - model.addAttribute("buttonText", "Create"); - return "ProductCreate"; - } - - @PostMapping("/create_product") - public String createProduct(@ModelAttribute Product product) { - productService.addProduct( - product.getName(), - product.getCost(), - masterService.getCurrentMasterId() - ); - return "redirect:/product/my_products"; - } - - @GetMapping("/update_product/{id}") - public String updateProductPage(Model model, @PathVariable("id") Long id) { - if (masterService.getCurrentMasterId() == 0) { - return "redirect:/product"; - } - model.addAttribute("user", masterService.findMaster(masterService.getCurrentMasterId())); - model.addAttribute("product", productService.findProduct(id)); - model.addAttribute("buttonText", "Update"); - return "ProductCreate"; - } - - @PostMapping("/update_product/{id}") - public String createProduct(@ModelAttribute Product product, @PathVariable("id") Long id) { - productService.updateProduct( - id, - product.getName(), - product.getCost() - ); - return "redirect:/product/my_products"; - } -} diff --git a/src/main/java/com/example/demo/util/validation/ValidationException.java b/src/main/java/com/example/demo/util/validation/ValidationException.java index d058613..c458889 100644 --- a/src/main/java/com/example/demo/util/validation/ValidationException.java +++ b/src/main/java/com/example/demo/util/validation/ValidationException.java @@ -6,4 +6,8 @@ public class ValidationException extends RuntimeException { public ValidationException(Set errors) { super(String.join("\n", errors)); } + + public ValidationException(String error) { + super(error); + } } diff --git a/src/main/resources/templates/Header.html b/src/main/resources/templates/Header.html deleted file mode 100644 index faf006d..0000000 --- a/src/main/resources/templates/Header.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Title - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/templates/Login.html b/src/main/resources/templates/Login.html deleted file mode 100644 index 4a492e4..0000000 --- a/src/main/resources/templates/Login.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Login - - - - - - -
-
-
-
- -
-
- -
-
- -
-
- Register -
-
-
- - \ No newline at end of file diff --git a/src/main/resources/templates/MastersOrders.html b/src/main/resources/templates/MastersOrders.html deleted file mode 100644 index 9816fcc..0000000 --- a/src/main/resources/templates/MastersOrders.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - Login - - - - - - -
-
-

Masters

- -
-
-
- - -
-
-
- - - - - - - - - - - - - - -
#Cost
-
- - \ No newline at end of file diff --git a/src/main/resources/templates/OrderPage.html b/src/main/resources/templates/OrderPage.html deleted file mode 100644 index e23acc9..0000000 --- a/src/main/resources/templates/OrderPage.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - Login - - - - - - -
-
-

Order

-
-
-
- -
-

-
-
-

-
-
- -
-
-
-
-

-
-
-

-

-
-
-
-
-
- - \ No newline at end of file diff --git a/src/main/resources/templates/ProductCreate.html b/src/main/resources/templates/ProductCreate.html deleted file mode 100644 index dbb20ad..0000000 --- a/src/main/resources/templates/ProductCreate.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Login - - - - - - -
-
-
- - - -
-
- - \ No newline at end of file diff --git a/src/main/resources/templates/Products.html b/src/main/resources/templates/Products.html deleted file mode 100644 index 9b701d2..0000000 --- a/src/main/resources/templates/Products.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - Title - - - - - - -
- -
-
-
- -
-

-
-
-

-
-
- -
-
-
-
- - - \ No newline at end of file diff --git a/src/main/resources/templates/UserPage.html b/src/main/resources/templates/UserPage.html deleted file mode 100644 index 413e22a..0000000 --- a/src/main/resources/templates/UserPage.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - Title - - - - - - -
-
-
-
- -
-
- -
-
- -
-
- -
-
-
- -
-
- -
-
-
- -
-
- Sing In -
-
-
- - \ No newline at end of file diff --git a/src/main/resources/templates/UserProducts.html b/src/main/resources/templates/UserProducts.html deleted file mode 100644 index 909ce73..0000000 --- a/src/main/resources/templates/UserProducts.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - Title - - - - - - - -
-
-
- Create -
-
-
-

-
-
-

-
-
- Edit -
-
-
- - \ No newline at end of file