diff --git a/build.gradle b/build.gradle index 7162d35..b560a61 100644 --- a/build.gradle +++ b/build.gradle @@ -12,16 +12,28 @@ repositories { mavenCentral() } +jar { + enabled = false +} + dependencies { + annotationProcessor "org.springframework.boot:spring-boot-configuration-processor" implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'com.auth0:java-jwt:4.4.0' + implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'org.springframework.boot:spring-boot-devtools' + implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' + implementation 'org.webjars:bootstrap:5.1.3' + implementation 'org.webjars:jquery:3.6.0' + implementation 'org.webjars:font-awesome:6.1.0' implementation 'com.h2database:h2:2.1.210' + implementation 'jakarta.validation:jakarta.validation-api:3.0.0' + implementation 'org.hibernate.validator:hibernate-validator:7.0.1.Final' implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.5' - implementation 'org.jetbrains:annotations:24.0.0' - implementation 'org.jetbrains:annotations:24.0.0' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.hibernate.validator:hibernate-validator' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { diff --git a/data.mv.db b/data.mv.db index 353463b..7079d60 100644 Binary files a/data.mv.db and b/data.mv.db differ diff --git a/front/src/App.vue b/front/src/App.vue index faa9919..6ec217e 100644 --- a/front/src/App.vue +++ b/front/src/App.vue @@ -1,16 +1,57 @@ - - + + \ No newline at end of file diff --git a/front/src/components/Header.vue b/front/src/components/Header.vue index 6c54313..9ad7516 100644 --- a/front/src/components/Header.vue +++ b/front/src/components/Header.vue @@ -15,6 +15,12 @@ + + @@ -46,6 +52,10 @@ \ No newline at end of file diff --git a/front/src/pages/error.vue b/front/src/pages/error.vue new file mode 100644 index 0000000..f6d5da1 --- /dev/null +++ b/front/src/pages/error.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/front/src/pages/login.vue b/front/src/pages/login.vue new file mode 100644 index 0000000..394c12b --- /dev/null +++ b/front/src/pages/login.vue @@ -0,0 +1,83 @@ + + + \ No newline at end of file diff --git a/front/src/pages/registration.vue b/front/src/pages/registration.vue new file mode 100644 index 0000000..e75a2e7 --- /dev/null +++ b/front/src/pages/registration.vue @@ -0,0 +1,46 @@ + + \ No newline at end of file diff --git a/front/src/pages/users.vue b/front/src/pages/users.vue new file mode 100644 index 0000000..3543375 --- /dev/null +++ b/front/src/pages/users.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/front/src/router/index.js b/front/src/router/index.js index 046f7ce..3450bef 100644 --- a/front/src/router/index.js +++ b/front/src/router/index.js @@ -2,14 +2,22 @@ import artists from "../pages/artists.vue" import albums from "../pages/albums.vue" import songs from "../pages/songs.vue" import find from "../pages/find.vue" +import users from "../pages/users.vue"; +import login from "../pages/login.vue"; +import registration from "../pages/registration.vue"; +import error from "../pages/error.vue"; import {createRouter, createWebHistory} from "vue-router" const routes = [ - {path: '/artists', component: artists}, - {path: '/albums', component: albums}, - {path: '/songs', component: songs}, - {path: '/find', component: find}, + {path: '/songs', component: songs, meta: { requiresAuth: true }}, + {path: '/albums', component: albums, meta: { requiresAuth: true }}, + {path: '/artists', component: artists, meta: { requiresAuth: true }}, + {path: '/find', component: find, meta: { requiresAuth: true }}, + { path: "/users", component: users, meta: { requiresAuth: true, requiresAdmin: true }}, + { path: "/login", component: login}, + { path: "/registration", component: registration}, + { path: "/error", component: error, meta: { requiresAuth: true }}, ] const router = createRouter({ @@ -18,4 +26,22 @@ const router = createRouter({ routes }) +router.beforeEach((to, from, next) => { + const isAuthenticated = localStorage.getItem("token"); + if (to.matched.some((route) => route.meta.requiresAuth)) { + if (!isAuthenticated) { + next("/login"); + return; + } + } + const isAdmin = localStorage.getItem("role") === "ADMIN"; + if (to.matched.some((route) => route.meta.requiresAdmin)) { + if (!isAdmin) { + next("/error"); + return; + } + } + next(); +}); + export default router; \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/Repository/IUserRepository.java b/src/main/java/ru/ulstu/is/sbapp/Repository/IUserRepository.java new file mode 100644 index 0000000..038ad52 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/Repository/IUserRepository.java @@ -0,0 +1,8 @@ +package ru.ulstu.is.sbapp.Repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.ulstu.is.sbapp.database.model.User; + +public interface IUserRepository extends JpaRepository { + User findOneByLoginIgnoreCase(String login); +} diff --git a/src/main/java/ru/ulstu/is/sbapp/WebConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/WebConfiguration.java deleted file mode 100644 index 35da531..0000000 --- a/src/main/java/ru/ulstu/is/sbapp/WebConfiguration.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.ulstu.is.sbapp; - -import org.springframework.context.annotation.Configuration; -import org.springframework.web.servlet.config.annotation.CorsRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -class WebConfiguration implements WebMvcConfigurer { - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**").allowedMethods("*"); - } -} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/JwtException.java b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtException.java new file mode 100644 index 0000000..0361eca --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtException.java @@ -0,0 +1,11 @@ +package ru.ulstu.is.sbapp.configuration; + +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/JwtFilter.java b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtFilter.java new file mode 100644 index 0000000..bdad217 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtFilter.java @@ -0,0 +1,72 @@ +package ru.ulstu.is.sbapp.configuration; + +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 ru.ulstu.is.sbapp.database.service.UserService; + +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/JwtProperties.java b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtProperties.java new file mode 100644 index 0000000..e407e7c --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.sbapp.configuration; + +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/JwtProvider.java b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtProvider.java new file mode 100644 index 0000000..0659755 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/JwtProvider.java @@ -0,0 +1,108 @@ +package ru.ulstu.is.sbapp.configuration; + +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()); + var temp = JWT.create(); + var temp2 = temp.withIssuer(ISSUER); + var temp3 = temp2.withIssuedAt(issueDate); + var temp4 = temp3.withExpiresAt(expireDate); + var temp5 = temp4.withSubject(login); + var temp6 = temp5.sign(algorithm); + return temp6; + } + + 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/configuration/OpenAPI30Configuration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java new file mode 100644 index 0000000..9516c54 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/OpenAPI30Configuration.java @@ -0,0 +1,27 @@ +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; + +@Configuration +public class OpenAPI30Configuration { + public static final String API_PREFIX = "/api/1.0"; + + @Bean + public OpenAPI customizeOpenAPI() { + final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..fadfa18 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/PasswordEncoderConfiguration.java @@ -0,0 +1,14 @@ +package ru.ulstu.is.sbapp.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public PasswordEncoder createPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..b3362c0 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/SecurityConfiguration.java @@ -0,0 +1,83 @@ +package ru.ulstu.is.sbapp.configuration; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.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; +import ru.ulstu.is.sbapp.controllers.UserController; +import ru.ulstu.is.sbapp.database.model.Role; +import ru.ulstu.is.sbapp.database.service.UserService; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity( + securedEnabled = true +) +public class SecurityConfiguration { + 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(); + } + + private void createAdminOnStartup() { + final String admin = "admin"; + if (userService.findByLogin(admin) == null) { + log.info("Admin user successfully created"); + userService.addUser(admin, admin, admin, Role.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("/", SPA_URL_MASK).permitAll() + .requestMatchers(HttpMethod.POST, UserController.URL_LOGIN).permitAll() + .requestMatchers(HttpMethod.POST, UserController.URL_SIGN_UP).permitAll() + .requestMatchers(HttpMethod.POST, UserController.URL_WHO_AM_I).permitAll() + .anyRequest() + .authenticated() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous(); + return http.userDetailsService(userService).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/**"); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/TypeConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/TypeConfiguration.java deleted file mode 100644 index c61c87e..0000000 --- a/src/main/java/ru/ulstu/is/sbapp/configuration/TypeConfiguration.java +++ /dev/null @@ -1,20 +0,0 @@ -package ru.ulstu.is.sbapp.configuration; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import ru.ulstu.is.sbapp.interfaces.DoubleType; -import ru.ulstu.is.sbapp.interfaces.StringType; - -@Configuration -public class TypeConfiguration { - - @Bean(value = "str") - public StringType createStrType(){ - return new StringType(); - } - - @Bean(value = "double") - public DoubleType createDoubleType(){ - return new DoubleType(); - } -} diff --git a/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java b/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java new file mode 100644 index 0000000..43f40d5 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/configuration/WebConfiguration.java @@ -0,0 +1,29 @@ +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.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfiguration implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController(SecurityConfiguration.SPA_URL_MASK).setViewName("forward:/"); + registry.addViewController("/notFound").setViewName("forward:/"); + } + @Bean + public WebServerFactoryCustomizer containerCustomizer() { + return container -> container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/notFound")); + } + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedMethods("*"); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/AlbumController.java b/src/main/java/ru/ulstu/is/sbapp/controllers/AlbumController.java index bcce229..f66cf45 100644 --- a/src/main/java/ru/ulstu/is/sbapp/controllers/AlbumController.java +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/AlbumController.java @@ -3,6 +3,7 @@ package ru.ulstu.is.sbapp.controllers; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.sbapp.configuration.OpenAPI30Configuration; import ru.ulstu.is.sbapp.database.model.Artist; import ru.ulstu.is.sbapp.database.model.Song; import ru.ulstu.is.sbapp.database.service.AlbumService; @@ -11,7 +12,7 @@ import java.util.List; import java.util.Map; @RestController -@RequestMapping("/album") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/album") public class AlbumController { private final AlbumService albumService; diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/ArtistController.java b/src/main/java/ru/ulstu/is/sbapp/controllers/ArtistController.java index c0f724f..7259af9 100644 --- a/src/main/java/ru/ulstu/is/sbapp/controllers/ArtistController.java +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/ArtistController.java @@ -2,12 +2,13 @@ package ru.ulstu.is.sbapp.controllers; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.sbapp.configuration.OpenAPI30Configuration; import ru.ulstu.is.sbapp.database.service.ArtistService; import java.util.List; @RestController -@RequestMapping("/artist") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/artist") public class ArtistController { private final ArtistService artistService; diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/SearchController.java b/src/main/java/ru/ulstu/is/sbapp/controllers/SearchController.java index 470cf15..45ae60a 100644 --- a/src/main/java/ru/ulstu/is/sbapp/controllers/SearchController.java +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/SearchController.java @@ -4,13 +4,14 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import ru.ulstu.is.sbapp.configuration.OpenAPI30Configuration; import ru.ulstu.is.sbapp.database.service.FindByNameService; import java.util.List; import java.util.Map; @RestController -@RequestMapping("/find") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/find") public class SearchController { private final FindByNameService findService; diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/SongController.java b/src/main/java/ru/ulstu/is/sbapp/controllers/SongController.java index 7267a95..e8fb5f8 100644 --- a/src/main/java/ru/ulstu/is/sbapp/controllers/SongController.java +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/SongController.java @@ -2,13 +2,14 @@ package ru.ulstu.is.sbapp.controllers; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.sbapp.configuration.OpenAPI30Configuration; import ru.ulstu.is.sbapp.database.service.AlbumService; import ru.ulstu.is.sbapp.database.service.SongService; import java.util.List; @RestController -@RequestMapping("/song") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/song") public class SongController { private final SongService songService; private final AlbumService albumService; diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/UserController.java b/src/main/java/ru/ulstu/is/sbapp/controllers/UserController.java new file mode 100644 index 0000000..533e445 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/UserController.java @@ -0,0 +1,64 @@ +package ru.ulstu.is.sbapp.controllers; + +import jakarta.validation.Valid; +import org.springframework.security.access.annotation.Secured; +import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.sbapp.database.model.User; +import ru.ulstu.is.sbapp.database.model.Role; +import ru.ulstu.is.sbapp.database.service.UserService; +import ru.ulstu.is.sbapp.database.util.validation.ValidationException; + +import java.util.List; + +@RestController +public class UserController { + public static final String URL_LOGIN = "/jwt/login"; + public static final String URL_SIGN_UP = "/sign_up"; + public static final String URL_WHO_AM_I = "/who_am_i"; + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid UserDTO userDto) { + return userService.loginAndGetToken(userDto); + } + @GetMapping(URL_WHO_AM_I) + public String role(@RequestParam("userLogin") String userLogin) { + return userService.findByLogin(userLogin).getUserRole().name(); + } + @PostMapping(URL_SIGN_UP) + public String signUp(@RequestBody @Valid UserSignUpDTO userSignupDto) { + try { + final User user = userService.addUser(userSignupDto.getLogin(), + userSignupDto.getPassword(), userSignupDto.getPasswordConfirm(), Role.USER); + return "created " + user.getLogin(); + } catch (ValidationException e) { + return e.getMessage(); + } + } + @GetMapping("/{id}") + @Secured({Role.AsString.ADMIN}) + public UserDTO getUser(@PathVariable Long id) { + return new UserDTO(userService.findUser(id)); + } + @GetMapping("/") + @Secured({Role.AsString.ADMIN}) + public List getUsers() { + return userService.findAllUsers().stream() + .map(UserDTO::new) + .toList(); + } + + @PutMapping("/{id}") + public UserDTO updateUser(@RequestBody @Valid UserDTO userDto){ + return new UserDTO(userService.updateUser(userDto)); + } + + + @DeleteMapping("/{id}") + public void deleteUser(@PathVariable Long id) { + userService.deleteUser(id); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/UserDTO.java b/src/main/java/ru/ulstu/is/sbapp/controllers/UserDTO.java new file mode 100644 index 0000000..04dde8f --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/UserDTO.java @@ -0,0 +1,52 @@ +package ru.ulstu.is.sbapp.controllers; + +import ru.ulstu.is.sbapp.database.model.User; +import ru.ulstu.is.sbapp.database.model.Role; + +public class UserDTO { + private Long id; + private Role role; + private String login; + private String password; + + public UserDTO(){} + + public UserDTO(User user) { + this.id=user.getId(); + this.login= user.getLogin(); + this.password = user.getPassword(); + this.role = user.getUserRole(); + } + + public Long getId() { + return id; + } + + public String getLogin() + { + return login; + } + + public String getPassword() + { + return password; + } + + public Role getUserRole() { + return role; + } + + public void setId(Long id) { + this.id = id; + } + + public void setLogin(String login) + { + this.login = login; + } + + public void setPassword(String password) + { + this.password = password; + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/controllers/UserSignUpDTO.java b/src/main/java/ru/ulstu/is/sbapp/controllers/UserSignUpDTO.java new file mode 100644 index 0000000..471579e --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/controllers/UserSignUpDTO.java @@ -0,0 +1,36 @@ +package ru.ulstu.is.sbapp.controllers; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class UserSignUpDTO { + private String login; + + private String password; + + private String passwordConfirm; + + public String getLogin() { + return login; + } + + public String getPasswordConfirm() { + return passwordConfirm; + } + + public String getPassword() { + return password; + } + + public void setLogin(String login) { + this.login = login; + } + + public void setPassword(String password) { + this.password = password; + } + + public void setPasswordConfirm(String passwordConfirm) { + this.passwordConfirm = passwordConfirm; + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/database/model/Role.java b/src/main/java/ru/ulstu/is/sbapp/database/model/Role.java new file mode 100644 index 0000000..4000824 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/database/model/Role.java @@ -0,0 +1,20 @@ +package ru.ulstu.is.sbapp.database.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum Role implements GrantedAuthority { + ADMIN, + USER; + + private static final String PREFIX = "ROLE_"; + + @Override + public String getAuthority() { + return PREFIX + this.name(); + } + + public static final class AsString { + public static final String ADMIN = PREFIX + "ADMIN"; + public static final String USER = PREFIX + "USER"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/sbapp/database/model/User.java b/src/main/java/ru/ulstu/is/sbapp/database/model/User.java new file mode 100644 index 0000000..b32a3d4 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/database/model/User.java @@ -0,0 +1,72 @@ +package ru.ulstu.is.sbapp.database.model; + +import jakarta.persistence.*; +import ru.ulstu.is.sbapp.controllers.UserSignUpDTO; + +import java.util.Objects; + +@Entity +@Table(name = "users") +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + private String login; + + private String password; + + private Role role; + + public User(){ + } + + public User(UserSignUpDTO userDto){ + this.login = userDto.getLogin(); + this.password = userDto.getPassword(); + this.role = Role.USER; + } + + public User(String login, String password, Role role){ + this.login = login; + this.password = password; + this.role = role; + } + + public Long getId(){return id;} + + public String getLogin(){return login;} + + public void setLogin(String login){this.login = login;} + + public String getPassword(){return password;} + + public void setPassword(String password){this.password = password;} + + public Role getUserRole() { + return role; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(id, user.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return "User{" + + "id=" + id + + ", login='" + login + '\'' + + ", password='" + password + '\'' + + '}'; + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/database/service/UserExistsException.java b/src/main/java/ru/ulstu/is/sbapp/database/service/UserExistsException.java new file mode 100644 index 0000000..bb9fa81 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/database/service/UserExistsException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.sbapp.database.service; + +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/database/service/UserNotFoundException.java b/src/main/java/ru/ulstu/is/sbapp/database/service/UserNotFoundException.java new file mode 100644 index 0000000..8f7c2af --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/database/service/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.sbapp.database.service; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(Long id) { + super(String.format("User not found '%s'", id)); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/database/service/UserService.java b/src/main/java/ru/ulstu/is/sbapp/database/service/UserService.java new file mode 100644 index 0000000..052cdb6 --- /dev/null +++ b/src/main/java/ru/ulstu/is/sbapp/database/service/UserService.java @@ -0,0 +1,130 @@ +package ru.ulstu.is.sbapp.database.service; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.StringUtils; +import ru.ulstu.is.sbapp.Repository.IUserRepository; +import ru.ulstu.is.sbapp.configuration.JwtException; +import ru.ulstu.is.sbapp.configuration.JwtProvider; +import ru.ulstu.is.sbapp.controllers.UserDTO; +import ru.ulstu.is.sbapp.controllers.UserSignUpDTO; +import ru.ulstu.is.sbapp.database.model.User; +import ru.ulstu.is.sbapp.database.model.Role; +import ru.ulstu.is.sbapp.database.util.validation.ValidationException; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +@Service +public class UserService implements UserDetailsService { + private final IUserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final JwtProvider jwtProvider; + + public UserService(IUserRepository userRepository, + PasswordEncoder passwordEncoder, + JwtProvider jwtProvider){ + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.jwtProvider = jwtProvider; + } + public User findByLogin(String login) { + return userRepository.findOneByLoginIgnoreCase(login); + } + public Page findAllPages(int page, int size) { + return userRepository.findAll(PageRequest.of(page - 1, size, Sort.by("id").ascending())); + } + @Transactional + public User addUser(UserSignUpDTO userDto){ + final User user = new User(userDto); + userRepository.save(user); + return user; + } + @Transactional + public User addUser(String login, String password, + String passwordConfirm, + Role role){ + if (findByLogin(login) != null) { + throw new ValidationException(String.format("User '%s' already exists", login)); + } + if (!Objects.equals(password, passwordConfirm)) { + throw new ValidationException("Passwords not equals"); + } + final User user = new User(login,passwordEncoder.encode(password),role); + return userRepository.save(user); + } + @Transactional(readOnly = true) + public User findUser(Long id) { + final Optional user = userRepository.findById(id); + return user.orElseThrow(() -> new UserNotFoundException(id)); + } + + @Transactional(readOnly = true) + public List findAllUsers() { + return userRepository.findAll(); + } + + @Transactional + public User updateUser(UserDTO userDto) { + final User currentUser = findUser(userDto.getId()); + currentUser.setLogin(userDto.getLogin()); + currentUser.setPassword(userDto.getPassword()); + return userRepository.save(currentUser); + } + @Transactional + public User updateUser(Long id,String login, String password) { + if (!StringUtils.hasText(login) || !StringUtils.hasText(password)) { + throw new IllegalArgumentException("User name, login or password is null or empty"); + } + final User currentUser = findUser(id); + currentUser.setLogin(login); + currentUser.setPassword(password); + return userRepository.save(currentUser); + } + @Transactional + public void deleteUser(Long id) { + userRepository.deleteById(id); + } + + @Transactional + public void deleteAllUsers() { + userRepository.deleteAll(); + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + final User userEntity = findByLogin(username); + if (userEntity == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getUserRole())); + } + public String loginAndGetToken(UserDTO userDto) { + final User user = findByLogin(userDto.getLogin()); + if (user == null) { + throw new UserNotFoundException(userDto.getId()); + } + if (!passwordEncoder.matches(userDto.getPassword(), user.getPassword())) { + throw new UserNotFoundException(user.getId()); + } + 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); + } +} diff --git a/src/main/java/ru/ulstu/is/sbapp/database/util/validation/ValidationException.java b/src/main/java/ru/ulstu/is/sbapp/database/util/validation/ValidationException.java index 16871de..37aa8e6 100644 --- a/src/main/java/ru/ulstu/is/sbapp/database/util/validation/ValidationException.java +++ b/src/main/java/ru/ulstu/is/sbapp/database/util/validation/ValidationException.java @@ -3,6 +3,9 @@ package ru.ulstu.is.sbapp.database.util.validation; import java.util.Set; public class ValidationException extends RuntimeException { + public ValidationException(String message) { + super(message); + } public ValidationException(Set errors) { super(String.join("\n", errors)); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da7b0b1..8162553 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ spring.main.banner-mode=off -#server.port=8080 +server.port=8080 spring.datasource.url=jdbc:h2:file:./data spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa @@ -9,3 +9,6 @@ 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 +jwt.secret = my-secret-jwt