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 3abbc24..b0c13a3 100644 Binary files a/data.mv.db and b/data.mv.db differ diff --git a/front/src/components/pages/Login.jsx b/front/src/components/pages/Login.jsx new file mode 100644 index 0000000..cbbdf12 --- /dev/null +++ b/front/src/components/pages/Login.jsx @@ -0,0 +1,76 @@ +import { useState } from "react" +import { NavLink, useNavigate } from "react-router-dom"; +import './form.css'; + + +export default function Film() { + const [data, setData] = useState({ + login: "", + password: "" + }); + const navigate = useNavigate(); + + const [isError, setError] = useState(false); + + const handleFormChange = (e) => { + 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