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 @@
+
+
+
+
+ ID |
+ Логин |
+ Роль |
+
+
+
+
+ {{ user.id }} |
+ {{ user.login }} |
+ {{ user.role }} |
+
+
+
+
+
+
\ 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