From e4299bcfcb12e2785cbe9f0a0bb01bd057afb21a Mon Sep 17 00:00:00 2001 From: Ismailov_Rovshan Date: Thu, 25 May 2023 13:47:28 +0400 Subject: [PATCH] =?UTF-8?q?Lab06=20(react)=20=D0=B3=D0=BE=D1=82=D0=BE?= =?UTF-8?q?=D0=B2=D0=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + data.mv.db | Bin 114688 -> 110592 bytes front/src/components/pages/Login.jsx | 76 +++++++++++++ .../configuration/OpenAPI30Configuration.java | 29 +++++ .../PasswordEncoderConfiguration.java | 2 +- .../configuration/SecurityConfiguration.java | 57 ++++++---- .../sbapp/configuration/WebConfiguration.java | 22 +++- .../sbapp/configuration/jwt/JwtException.java | 11 ++ .../is/sbapp/configuration/jwt/JwtFilter.java | 72 ++++++++++++ .../configuration/jwt/JwtProperties.java | 27 +++++ .../sbapp/configuration/jwt/JwtProvider.java | 107 ++++++++++++++++++ .../controller/MyUserController.java | 68 +++++++++++ .../controller/UserController.java | 88 +++++++------- .../controller/UserMvcController.java | 42 ------- .../is/sbapp/socialNetwork/dto/UserDto.java | 24 ++-- .../sbapp/socialNetwork/dto/UserInfoDTO.java | 27 +++++ .../sbapp/socialNetwork/dto/UsersPageDTO.java | 29 +++++ .../services/UserExistsException.java | 7 ++ .../services/UserNotFoundException.java | 7 ++ .../socialNetwork/services/UserService.java | 38 ++++++- src/main/resources/application.properties | 2 + 21 files changed, 604 insertions(+), 132 deletions(-) create mode 100644 front/src/components/pages/Login.jsx create mode 100644 src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtException.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtFilter.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProperties.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProvider.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/MyUserController.java delete mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserMvcController.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserInfoDTO.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UsersPageDTO.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserExistsException.java create mode 100644 src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserNotFoundException.java diff --git a/build.gradle b/build.gradle index 36cc235..24b4a7f 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5' + implementation 'com.auth0:java-jwt:4.4.0' implementation 'org.springdoc:springdoc-openapi-ui:1.6.5' diff --git a/data.mv.db b/data.mv.db index 3abbc2437c2de97354dffe02ab1204cd370ad7f8..b0c13a3b4bd1975f18c09840774227b916e7e65c 100644 GIT binary patch literal 110592 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 { + setData({ ...data, [e.target.id]: e.target.value }); + } + + const formSubmit = async (e) => { + e.preventDefault(); + const requestParams = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ login: data.login, password: data.password }), + }; + const response = await fetch("http://localhost:8080/jwt/login", requestParams); + const result = await response.text(); + if (response.status === 200) { + setError(false); + localStorage.setItem("token", result); + localStorage.setItem("user", data.login); + getRole(result); + } else { + setError(true); + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("role"); + } + } + + const getRole = async function (token) { + const requestParams = { + method: "GET", + headers: { + "Content-Type": "application/json" + } + }; + const requestUrl = `http://localhost:8080/user?token=${token}`; + const response = await fetch(requestUrl, requestParams); + const result = await response.text(); + localStorage.setItem("role", result); + window.dispatchEvent(new Event("storage")); + navigate("/"); + } + + return ( + <> + {isError && } + +
+ + + + + + Регистрация +
+ + ) +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java new file mode 100644 index 0000000..5274b74 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java @@ -0,0 +1,29 @@ +package ru.ulstu.is.sbapp.configuration; + +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.sbapp.configuration.jwt.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"))); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java index fadfa18..3849ce9 100644 --- a/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java @@ -8,7 +8,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; @Configuration public class PasswordEncoderConfiguration { @Bean - public PasswordEncoder createPasswordEncoder() { + public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } } diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java index 3851f07..664fa9a 100644 --- a/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java @@ -5,26 +5,29 @@ import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; -import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import ru.ulstu.is.sbapp.socialNetwork.controller.UserSignupMvcController; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import ru.ulstu.is.sbapp.configuration.jwt.JwtFilter; +import ru.ulstu.is.sbapp.socialNetwork.controller.MyUserController; +import ru.ulstu.is.sbapp.socialNetwork.controller.UserController; import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; -import ru.ulstu.is.sbapp.socialNetwork.services.MyUserService; import ru.ulstu.is.sbapp.socialNetwork.services.UserService; @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 UserService userService; + private final JwtFilter jwtFilter; public SecurityConfiguration(UserService userService) { this.userService = userService; + this.jwtFilter = new JwtFilter(userService); createAdminOnStartup(); } @@ -38,31 +41,37 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { - http.headers().frameOptions().sameOrigin().and() - .cors().and() + log.info("Creating security configuration"); + http.cors() + .and() .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() .authorizeRequests() - .antMatchers(UserSignupMvcController.SIGNUP_URL).permitAll() - .antMatchers(HttpMethod.GET, LOGIN_URL).permitAll() - .anyRequest().authenticated() + .antMatchers("/", SPA_URL_MASK).permitAll() + .antMatchers(HttpMethod.POST, UserController.URL_SIGNUP).permitAll() + .antMatchers(HttpMethod.POST, UserController.URL_LOGIN).permitAll() + .anyRequest() + .authenticated() .and() - .formLogin() - .loginPage(LOGIN_URL).permitAll() - .and() - .logout().permitAll(); + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous(); } @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.userDetailsService(userService); + protected void configure(AuthenticationManagerBuilder builder) throws Exception { + builder.userDetailsService(userService); } @Override public void configure(WebSecurity web) { - web.ignoring() - .antMatchers("/css/**") - .antMatchers("/js/**") - .antMatchers("/templates/**") - .antMatchers("/webjars/**"); + web + .ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/**/*.{js,html,css,png}") + .antMatchers("/swagger-ui/index.html") + .antMatchers("/webjars/**") + .antMatchers("/swagger-resources/**") + .antMatchers("/v3/api-docs/**"); } -} \ No newline at end of file +} diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java index 91f2516..c46ca53 100644 --- a/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java @@ -1,6 +1,11 @@ package ru.ulstu.is.sbapp.configuration; +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.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; @@ -8,12 +13,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfiguration implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**").allowedMethods("*"); - } public static final String REST_API = "/api"; - @Override public void addViewControllers(ViewControllerRegistry registry) { WebMvcConfigurer.super.addViewControllers(registry); @@ -22,9 +22,19 @@ public class WebConfiguration implements WebMvcConfigurer { registry.addViewController("catalogs"); registry.addViewController("login"); } + + @Bean + public WebServerFactoryCustomizer containerCustomizer() { + return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound")); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedMethods("*"); + } + @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/css/**").addResourceLocations("/static/css/"); } - } \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtException.java b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtException.java new file mode 100644 index 0000000..ba2c2f5 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtException.java @@ -0,0 +1,11 @@ +package ru.ulstu.is.sbapp.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/ru/ulstu/is/sbapp/configuration/jwt/JwtFilter.java b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtFilter.java new file mode 100644 index 0000000..833e741 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtFilter.java @@ -0,0 +1,72 @@ +package ru.ulstu.is.sbapp.configuration.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +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 ru.ulstu.is.sbapp.socialNetwork.services.UserService; + +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 UserService userService; + + public JwtFilter(UserService userService) { + this.userService = userService; + } + + 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 = userService.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/ru/ulstu/is/sbapp/configuration/jwt/JwtProperties.java b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProperties.java new file mode 100644 index 0000000..d93ff16 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.sbapp.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/ru/ulstu/is/sbapp/configuration/jwt/JwtProvider.java b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProvider.java new file mode 100644 index 0000000..9d3db53 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/jwt/JwtProvider.java @@ -0,0 +1,107 @@ +package ru.ulstu.is.sbapp.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/ru/ulstu/is/sbapp/socialNetwork/controller/MyUserController.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/MyUserController.java new file mode 100644 index 0000000..dcc715e --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/MyUserController.java @@ -0,0 +1,68 @@ +package ru.ulstu.is.sbapp.socialNetwork.controller; + +import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.sbapp.configuration.WebConfiguration; +import ru.ulstu.is.sbapp.socialNetwork.dto.MyUserDTO; +import ru.ulstu.is.sbapp.socialNetwork.models.UserModel; +import ru.ulstu.is.sbapp.socialNetwork.services.MyUserService; + + +import javax.validation.Valid; +import java.util.List; + + +@RestController +@RequestMapping(WebConfiguration.REST_API + "/users") +public class MyUserController { + private final MyUserService userService; + + public MyUserController(MyUserService userService){ + this.userService = userService; + } + + @GetMapping("/{id}") + public MyUserDTO getUser(@PathVariable Long id) { + return new MyUserDTO(userService.findUser(id)); + } + + @GetMapping("") + public List getUsers() { + return userService.findAllUsers().stream().map(MyUserDTO::new).toList(); + } + + @PostMapping("") + public MyUserDTO createUser(@RequestBody @Valid MyUserDTO user) { + UserModel result = userService.addUser(user.getName(), user.getCity()); + userService.updateCommunities(result.getId(), user.getCommunity()); + return new MyUserDTO(userService.updateMusics(result.getId(), user.getMusic())); + } + + @PatchMapping("/{id}") + public MyUserDTO updateUser(@PathVariable Long id, + @RequestBody @Valid MyUserDTO user) { + UserModel result = userService.updateUser(id, user.getName()); + userService.updateCommunities(result.getId(), user.getCommunity()); + return new MyUserDTO(userService.updateMusics(result.getId(), user.getMusic())); + } + + @PatchMapping("/add_music/{id}") + public MyUserDTO addMusic(@PathVariable Long id, @RequestParam Long music_id) { + return new MyUserDTO(userService.addMusic(id, music_id)); + } + + @PatchMapping("/add_community/{id}") + public MyUserDTO addCommunity(@PathVariable Long id, @RequestParam Long community_id) { + return new MyUserDTO(userService.addCommunity(id, community_id)); + } + @GetMapping("/search") + public List searchUsers(@RequestParam("city") String city) { + List users = userService.findUserByCity(city); + return users.stream().map(MyUserDTO::new).toList(); + } + + @DeleteMapping("/{id}") + public MyUserDTO deleteUser(@PathVariable Long id) { + return new MyUserDTO(userService.deleteUser(id)); + } + +} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserController.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserController.java index 7f422c0..56a961f 100644 --- a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserController.java +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserController.java @@ -1,68 +1,58 @@ package ru.ulstu.is.sbapp.socialNetwork.controller; +import org.springframework.data.domain.Page; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; -import ru.ulstu.is.sbapp.configuration.WebConfiguration; -import ru.ulstu.is.sbapp.socialNetwork.dto.MyUserDTO; -import ru.ulstu.is.sbapp.socialNetwork.models.UserModel; -import ru.ulstu.is.sbapp.socialNetwork.services.MyUserService; - +import ru.ulstu.is.sbapp.socialNetwork.dto.UserDto; +import ru.ulstu.is.sbapp.socialNetwork.dto.UserInfoDTO; +import ru.ulstu.is.sbapp.socialNetwork.dto.UsersPageDTO; +import ru.ulstu.is.sbapp.socialNetwork.models.User; +import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; +import ru.ulstu.is.sbapp.socialNetwork.services.UserService; import javax.validation.Valid; import java.util.List; - +import java.util.stream.IntStream; @RestController -@RequestMapping(WebConfiguration.REST_API + "/users") public class UserController { - private final MyUserService userService; + public static final String URL_LOGIN = "/jwt/login"; + public static final String URL_SIGNUP = "/jwt/signup"; - public UserController(MyUserService userService){ + private final UserService userService; + + public UserController(UserService userService) { this.userService = userService; } - @GetMapping("/{id}") - public MyUserDTO getUser(@PathVariable Long id) { - return new MyUserDTO(userService.findUser(id)); + @GetMapping("/user") + public String findUser(@RequestParam("token") String token) { + UserDetails userDetails = userService.loadUserByToken(token); + User user = userService.findByLogin(userDetails.getUsername()); + return user.getRole().toString(); + } + @GetMapping("/users") + @Secured({UserRole.AsString.ADMIN}) + public UsersPageDTO getUsers(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "5") int size) { + final Page users = userService.findAllPages(page, size) + .map(UserDto::new); + final int totalPages = users.getTotalPages(); + final List pageNumbers = IntStream.rangeClosed(1, totalPages) + .boxed() + .toList(); + return new UsersPageDTO(users, pageNumbers, totalPages); } - @GetMapping("") - public List getUsers() { - return userService.findAllUsers().stream().map(MyUserDTO::new).toList(); + + @PostMapping(URL_SIGNUP) + public UserInfoDTO signup(@RequestBody @Valid UserDto userDto) { + return userService.signupAndGetToken(userDto); } - @PostMapping("") - public MyUserDTO createUser(@RequestBody @Valid MyUserDTO user) { - UserModel result = userService.addUser(user.getName(), user.getCity()); - userService.updateCommunities(result.getId(), user.getCommunity()); - return new MyUserDTO(userService.updateMusics(result.getId(), user.getMusic())); + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid UserDto userDto) { + return userService.loginAndGetToken(userDto); } - - @PatchMapping("/{id}") - public MyUserDTO updateUser(@PathVariable Long id, - @RequestBody @Valid MyUserDTO user) { - UserModel result = userService.updateUser(id, user.getName()); - userService.updateCommunities(result.getId(), user.getCommunity()); - return new MyUserDTO(userService.updateMusics(result.getId(), user.getMusic())); - } - - @PatchMapping("/add_music/{id}") - public MyUserDTO addMusic(@PathVariable Long id, @RequestParam Long music_id) { - return new MyUserDTO(userService.addMusic(id, music_id)); - } - - @PatchMapping("/add_community/{id}") - public MyUserDTO addCommunity(@PathVariable Long id, @RequestParam Long community_id) { - return new MyUserDTO(userService.addCommunity(id, community_id)); - } - @GetMapping("/search") - public List searchUsers(@RequestParam("city") String city) { - List users = userService.findUserByCity(city); - return users.stream().map(MyUserDTO::new).toList(); - } - - @DeleteMapping("/{id}") - public MyUserDTO deleteUser(@PathVariable Long id) { - return new MyUserDTO(userService.deleteUser(id)); - } - } diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserMvcController.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserMvcController.java deleted file mode 100644 index 54a600b..0000000 --- a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/controller/UserMvcController.java +++ /dev/null @@ -1,42 +0,0 @@ -package ru.ulstu.is.sbapp.socialNetwork.controller; - -import org.springframework.data.domain.Page; -import org.springframework.security.access.annotation.Secured; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import ru.ulstu.is.sbapp.socialNetwork.dto.UserDto; -import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; -import ru.ulstu.is.sbapp.socialNetwork.services.UserService; - -import java.util.List; -import java.util.stream.IntStream; - -@Controller -@RequestMapping("/users") -public class UserMvcController { - private final UserService userService; - - public UserMvcController(UserService userService) { - this.userService = userService; - } - - @GetMapping - @Secured({UserRole.AsString.ADMIN}) - public String getUsers(@RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "5") int size, - Model model) { - final Page users = userService.findAllPages(page, size) - .map(UserDto::new); - model.addAttribute("users", users); - final int totalPages = users.getTotalPages(); - final List pageNumbers = IntStream.rangeClosed(1, totalPages) - .boxed() - .toList(); - model.addAttribute("pages", pageNumbers); - model.addAttribute("totalPages", totalPages); - return "users"; - } -} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserDto.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserDto.java index 6bfc837..e302517 100644 --- a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserDto.java +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserDto.java @@ -3,25 +3,35 @@ package ru.ulstu.is.sbapp.socialNetwork.dto; import ru.ulstu.is.sbapp.socialNetwork.models.User; import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; +import javax.validation.constraints.NotEmpty; + public class UserDto { - private final long id; - private final String login; - private final UserRole role; + @NotEmpty + private String login; + @NotEmpty + private String password; + private UserRole role; + private String passwordConfirm; public UserDto(User user) { - this.id = user.getId(); this.login = user.getLogin(); + this.password = user.getPassword(); this.role = user.getRole(); } - public long getId() { - return id; - } + public UserDto() {} public String getLogin() { return login; } + public String getPassword() { + return password; + } + public String getPasswordConfirm() { + return passwordConfirm; + } + public UserRole getRole() { return role; } diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserInfoDTO.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserInfoDTO.java new file mode 100644 index 0000000..5b027d7 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UserInfoDTO.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.sbapp.socialNetwork.dto; + + +import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; + +public class UserInfoDTO { + private final String token; + private String login; + private final UserRole role; + public UserInfoDTO(String token, String login, UserRole role) { + this.token = token; + this.login = login; + this.role = role; + } + + public String getToken() { + return token; + } + + public UserRole getRole() { + + return role; + } + public String getLogin() { + return login; + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UsersPageDTO.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UsersPageDTO.java new file mode 100644 index 0000000..6983462 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/dto/UsersPageDTO.java @@ -0,0 +1,29 @@ +package ru.ulstu.is.sbapp.socialNetwork.dto; + +import org.springframework.data.domain.Page; + +import java.util.List; + +public class UsersPageDTO { + private Page users; + private List pageNumbers; + private int totalPages; + + public UsersPageDTO(Page users, List pageNumbers, int totalPages) { + this.users = users; + this.pageNumbers = pageNumbers; + this.totalPages = totalPages; + } + + public Page getUsers() { + return users; + } + + public List getPageNumbers() { + return pageNumbers; + } + + public int getTotalPages() { + return totalPages; + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserExistsException.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserExistsException.java new file mode 100644 index 0000000..ab097de --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserExistsException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.sbapp.socialNetwork.services; + +public class UserExistsException extends RuntimeException { + public UserExistsException(String login) { + super(String.format("User '%s' already exists", login)); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserNotFoundException.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserNotFoundException.java new file mode 100644 index 0000000..f5a2745 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.sbapp.socialNetwork.services; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String login) { + super(String.format("User not found '%s'", login)); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserService.java b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserService.java index e9d1112..f8bd5f6 100644 --- a/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserService.java +++ b/src/main/java/ru/ulstu/is/sbapp/socialNetwork/services/UserService.java @@ -8,6 +8,10 @@ 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.sbapp.configuration.jwt.JwtException; +import ru.ulstu.is.sbapp.configuration.jwt.JwtProvider; +import ru.ulstu.is.sbapp.socialNetwork.dto.UserDto; +import ru.ulstu.is.sbapp.socialNetwork.dto.UserInfoDTO; import ru.ulstu.is.sbapp.socialNetwork.models.User; import ru.ulstu.is.sbapp.socialNetwork.models.UserRole; import ru.ulstu.is.sbapp.socialNetwork.repository.UserRepository; @@ -22,13 +26,16 @@ public class UserService implements UserDetailsService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final ValidatorUtil validatorUtil; + private final JwtProvider jwtProvider; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, - ValidatorUtil validatorUtil) { + ValidatorUtil validatorUtil, + JwtProvider jwtProvider) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; this.validatorUtil = validatorUtil; + this.jwtProvider = jwtProvider; } public Page findAllPages(int page, int size) { @@ -45,7 +52,7 @@ public class UserService implements UserDetailsService { public User createUser(String login, String password, String passwordConfirm, UserRole role) { if (findByLogin(login) != null) { - throw new ValidationException(String.format("User '%s' already exists", login)); + throw new UserExistsException(login); } final User user = new User(login, passwordEncoder.encode(password), role); validatorUtil.validate(user); @@ -55,6 +62,26 @@ public class UserService implements UserDetailsService { return userRepository.save(user); } + public String loginAndGetToken(UserDto userDto) { + final User user = findByLogin(userDto.getLogin()); + if (user == null) { + throw new UserNotFoundException(userDto.getLogin()); + } + if (!passwordEncoder.matches(userDto.getPassword(), user.getPassword())) { + throw new UserNotFoundException(user.getLogin()); + } + return jwtProvider.generateToken(user.getLogin()); + } + + 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 User userEntity = findByLogin(username); @@ -64,4 +91,9 @@ public class UserService implements UserDetailsService { return new org.springframework.security.core.userdetails.User( userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getRole())); } -} + + public UserInfoDTO signupAndGetToken(UserDto userDto) { + final User user = createUser(userDto.getLogin(), userDto.getPassword(), userDto.getPasswordConfirm(), UserRole.USER); + return new UserInfoDTO(jwtProvider.generateToken(user.getLogin()), user.getLogin(), UserRole.USER); + } +} \ No newline at end of file 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