diff --git a/Frontend/vue-project/src/components/CatalogCollections.vue b/Frontend/vue-project/src/components/CatalogCollections.vue index bd13872..bc3d0ed 100644 --- a/Frontend/vue-project/src/components/CatalogCollections.vue +++ b/Frontend/vue-project/src/components/CatalogCollections.vue @@ -19,14 +19,46 @@ data() { return{ collections: [], - URL: "http://localhost:8080/", + URL: "http://localhost:8080/api/1.0/", collection: new Collection(), - films: [] + films: [], + postParams: { + method:"POST", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + putParams: { + method:"PUT", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + }, + }, + delParams: { + method:"DELETE", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + getParams: { + method:"GET", + headers:{ + "Authorization": "Bearer " + localStorage.getItem("token"), + } + } + } + }, + beforeCreate() { + if (localStorage.getItem("token") == null) { + this.$router.push("/login"); } }, methods: { getCollections(){ - axios.get(this.URL + "collection") + axios.get(this.URL + "collection", this.getParams) .then(response => { this.collections = response.data; }) @@ -36,7 +68,7 @@ }, addCollection(collection){ console.log(collection); - axios.post(this.URL + "collection", collection) + axios.post(this.URL + "collection", collection, this.postParams) .then(() => { this.getCollections(); this.closeModal(); @@ -46,13 +78,13 @@ }); }, deleteCollection(id){ - axios.delete(this.URL + `collection/${id}`) + axios.delete(this.URL + `collection/${id}`, this.delParams) .then(() =>{ this.getCollections(); }) }, editCollection(collection){ - axios.put(this.URL + `collection/${collection.id}`, collection) + axios.put(this.URL + `collection/${collection.id}`, collection, this.putParams) .then(() =>{ const index = this.collections.findIndex((s) => s.id === collection.id); if (index !== -1) { @@ -91,7 +123,7 @@ document.getElementById("editModal").style.display = "none"; }, getFilms(){ - axios.get(this.URL + "film") + axios.get(this.URL + "film", this.getParams) .then(response => { this.films = response.data; console.log(response.data); @@ -100,11 +132,9 @@ console.log(error); }); }, - addCollectionFilm(id) { let filmId = document.getElementById('films').value; - axios - .post(this.URL + `collection/add_film/${id}?film_id=${filmId}`) + axios.post(this.URL + `collection/add_film/${id}`, filmId, this.postParams) .then(() => { this.closeModalForAdd(); this.getCollections(); @@ -115,8 +145,7 @@ }, delCollectionFilm(id) { let filmId = document.getElementById('films').value; - axios - .delete(this.URL + `collection/del_film/${id}?film_id=${filmId}`) + axios.delete(this.URL + `collection/del_film/${id}?film_id=${filmId}`, this.delParams) .then(() => { this.closeModalForAdd(); this.getCollections(); diff --git a/Frontend/vue-project/src/components/CatalogFilms.vue b/Frontend/vue-project/src/components/CatalogFilms.vue index 827fa9f..027d3b0 100644 --- a/Frontend/vue-project/src/components/CatalogFilms.vue +++ b/Frontend/vue-project/src/components/CatalogFilms.vue @@ -19,16 +19,48 @@ data() { return{ films: [], - URL: "http://localhost:8080/", + URL: "http://localhost:8080/api/1.0/", film: new Film(), genres: [], selectedGenres: [], - open: [] + open: [], + postParams: { + method:"POST", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + putParams: { + method:"PUT", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + }, + }, + delParams: { + method:"DELETE", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + getParams: { + method:"GET", + headers:{ + "Authorization": "Bearer " + localStorage.getItem("token"), + } + } + } + }, + beforeCreate() { + if (localStorage.getItem("token") == null) { + this.$router.push("/login"); } }, methods: { getFilms(){ - axios.get(this.URL + "film") + axios.get(this.URL + "film", this.getParams) .then(response => { this.films = response.data; }) @@ -38,7 +70,7 @@ }, addFilm(film){ console.log(film); - axios.post(this.URL + "film", film) + axios.post(this.URL + "film", film, this.postParams) .then(() => { this.getFilms(); this.closeModal(); @@ -48,13 +80,13 @@ }); }, deleteFilm(id){ - axios.delete(this.URL + `film/${id}`) + axios.delete(this.URL + `film/${id}`, this.delParams) .then(() =>{ this.getFilms(); }) }, editFilm(film){ - axios.put(this.URL + `film/${film.id}`, film) + axios.put(this.URL + `film/${film.id}`, film, this.putParams) .then(() =>{ const index = this.films.findIndex((s) => s.id === film.id); if (index !== -1) { @@ -97,7 +129,7 @@ document.getElementById("editModal").style.display = "none"; }, getGenres(){ - axios.get(this.URL + "genre") + axios.get(this.URL + "genre", this.getParams) .then(response => { this.genres = response.data; console.log(response.data); @@ -116,7 +148,7 @@ addFilmGenre(id, list) { axios - .post(this.URL + `film/add_genres/${id}`, list) + .post(this.URL + `film/add_genres/${id}`, list, this.postParams) .then(() => { this.closeModalForAdd(); this.getFilms(); diff --git a/Frontend/vue-project/src/components/CatalogGenres.vue b/Frontend/vue-project/src/components/CatalogGenres.vue index 2412d6e..5851796 100644 --- a/Frontend/vue-project/src/components/CatalogGenres.vue +++ b/Frontend/vue-project/src/components/CatalogGenres.vue @@ -15,14 +15,46 @@ data() { return { genres: [], - URL: "http://localhost:8080/", + URL: "http://localhost:8080/api/1.0/", genre: new Genre(), - editedGenre: new Genre() + editedGenre: new Genre(), + postParams: { + method:"POST", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + putParams: { + method:"PUT", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + }, + }, + delParams: { + method:"DELETE", + headers:{ + "Content-Type":"application/json", + "Authorization": "Bearer " + localStorage.getItem("token"), + } + }, + getParams: { + method:"GET", + headers:{ + "Authorization": "Bearer " + localStorage.getItem("token"), + } + } + } + }, + beforeCreate() { + if (localStorage.getItem("token") == null) { + this.$router.push("/login"); } }, methods: { getGenres(){ - axios.get(this.URL + "genre") + axios.get(this.URL + "genre", this.getParams) .then(response => { this.genres = response.data; console.log(response.data); @@ -34,7 +66,7 @@ addGenre(genre) { console.log(genre); axios - .post(this.URL + "genre", genre) + .post(this.URL + "genre", genre, this.postParams) .then(() => { this.getGenres(); this.closeModal(); @@ -44,7 +76,7 @@ }); }, deleteGenre(id){ - axios.delete(this.URL + `genre/${id}`) + axios.delete(this.URL + `genre/${id}`, this.delParams) .then(() =>{ this.getGenres(); }) @@ -64,7 +96,7 @@ document.getElementById("editModal").style.display = "none"; }, editGenre(genre) { - axios.put(this.URL + `genre/${genre.id}`, genre) + axios.put(this.URL + `genre/${genre.id}`, genre, this.putParams) .then(() => { const index = this.genres.findIndex((s) => s.id === genre.id); if (index !== -1) { diff --git a/Frontend/vue-project/src/components/Catalogs.vue b/Frontend/vue-project/src/components/Catalogs.vue deleted file mode 100644 index 427bb95..0000000 --- a/Frontend/vue-project/src/components/Catalogs.vue +++ /dev/null @@ -1,23 +0,0 @@ - - - \ No newline at end of file diff --git a/Frontend/vue-project/src/components/DataTable.vue b/Frontend/vue-project/src/components/DataTable.vue deleted file mode 100644 index 0706ab3..0000000 --- a/Frontend/vue-project/src/components/DataTable.vue +++ /dev/null @@ -1,70 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Frontend/vue-project/src/components/Header.vue b/Frontend/vue-project/src/components/Header.vue index 9275459..b74bf60 100644 --- a/Frontend/vue-project/src/components/Header.vue +++ b/Frontend/vue-project/src/components/Header.vue @@ -23,6 +23,12 @@ + + @@ -34,4 +40,15 @@ header{ font-size: 28px; } - \ No newline at end of file + + + \ No newline at end of file diff --git a/Frontend/vue-project/src/components/Index.vue b/Frontend/vue-project/src/components/Index.vue index 6f22571..d3f2f19 100644 --- a/Frontend/vue-project/src/components/Index.vue +++ b/Frontend/vue-project/src/components/Index.vue @@ -11,6 +11,11 @@ export default{ banner: "/src/images/banner1.jpg" } }, + beforeCreate() { + if (localStorage.getItem("token") == null) { + this.$router.push("/login"); + } + }, methods: { change(){ this.banner = this.images[this.count].image; diff --git a/Frontend/vue-project/src/components/Login.vue b/Frontend/vue-project/src/components/Login.vue new file mode 100644 index 0000000..0f45573 --- /dev/null +++ b/Frontend/vue-project/src/components/Login.vue @@ -0,0 +1,96 @@ + + + \ No newline at end of file diff --git a/Frontend/vue-project/src/components/Report.vue b/Frontend/vue-project/src/components/Report.vue index e34b8ac..2729018 100644 --- a/Frontend/vue-project/src/components/Report.vue +++ b/Frontend/vue-project/src/components/Report.vue @@ -12,14 +12,25 @@ data() { return{ films: [], - URL: "http://localhost:8080/", + URL: "http://localhost:8080/api/1.0/", genres: [], - genreId: undefined + genreId: undefined, + getParams: { + method:"GET", + headers:{ + "Authorization": "Bearer " + localStorage.getItem("token"), + } + } + } + }, + beforeCreate() { + if (localStorage.getItem("token") == null) { + this.$router.push("/login"); } }, methods: { getFilms(){ - axios.get(this.URL + "film") + axios.get(this.URL + "film", this.getParams) .then(response => { this.films = response.data; }) @@ -28,7 +39,7 @@ }); }, getGenres(){ - axios.get(this.URL + "genre") + axios.get(this.URL + "genre", this.getParams) .then(response => { this.genres = response.data; console.log(response.data); @@ -39,7 +50,7 @@ }, filter() { let genreId = document.getElementById('genreFilterSelect').value; - axios.get(this.URL + `genre/film/${genreId}`) + axios.get(this.URL + `genre/film/${genreId}`, this.getParams) .then(response => { this.films = response.data; }) diff --git a/Frontend/vue-project/src/components/Signup.vue b/Frontend/vue-project/src/components/Signup.vue new file mode 100644 index 0000000..4fde2eb --- /dev/null +++ b/Frontend/vue-project/src/components/Signup.vue @@ -0,0 +1,52 @@ + + + \ No newline at end of file diff --git a/Frontend/vue-project/src/components/Users.vue b/Frontend/vue-project/src/components/Users.vue new file mode 100644 index 0000000..8581e4d --- /dev/null +++ b/Frontend/vue-project/src/components/Users.vue @@ -0,0 +1,64 @@ + + + \ No newline at end of file diff --git a/Frontend/vue-project/src/main.js b/Frontend/vue-project/src/main.js index 41edc9e..25c887e 100644 --- a/Frontend/vue-project/src/main.js +++ b/Frontend/vue-project/src/main.js @@ -6,6 +6,9 @@ import CatalogGenres from './components/CatalogGenres.vue' import CatalogFilms from './components/CatalogFilms.vue' import CatalogCollections from './components/CatalogCollections.vue' import Report from './components/Report.vue' +import Users from './components/Users.vue' +import Login from './components/Login.vue' +import Signup from './components/Signup.vue' const routes = [ { path: '/', redirect: '/index' }, @@ -13,7 +16,10 @@ const routes = [ { path: '/catalogs/genres', component: CatalogGenres}, { path: '/catalogs/films', component: CatalogFilms}, { path: '/catalogs/collections', component: CatalogCollections}, - { path: '/report', component: Report} + { path: '/report', component: Report}, + { path: '/users', component: Users}, + { path: '/login', component: Login}, + { path: '/signup', component: Signup} ] const router = createRouter({ diff --git a/Frontend/vue-project/src/models/User.js b/Frontend/vue-project/src/models/User.js new file mode 100644 index 0000000..bd4d867 --- /dev/null +++ b/Frontend/vue-project/src/models/User.js @@ -0,0 +1,7 @@ +export default class User { + constructor(data) { + this.id = data?.id; + this.login = data?.login; + this.role = data?.role; + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index b743061..96b4adc 100644 --- a/build.gradle +++ b/build.gradle @@ -12,11 +12,21 @@ 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 'com.h2database:h2:2.1.210' + + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'com.auth0:java-jwt:4.4.0' + implementation 'org.hibernate.validator:hibernate-validator' implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.5' diff --git a/data.mv.db b/data.mv.db index 195bd6e..ebed62d 100644 Binary files a/data.mv.db and b/data.mv.db differ diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/Repository/IUserRepository.java b/src/main/java/ru/ulstu/is/lab1/DataBase/Repository/IUserRepository.java new file mode 100644 index 0000000..d114795 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/Repository/IUserRepository.java @@ -0,0 +1,8 @@ +package ru.ulstu.is.lab1.DataBase.Repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import ru.ulstu.is.lab1.DataBase.model.User; + +public interface IUserRepository extends JpaRepository { + User findOneByLoginIgnoreCase(String login); +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/OpenAPI30Configuration.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/OpenAPI30Configuration.java new file mode 100644 index 0000000..500a7a6 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/OpenAPI30Configuration.java @@ -0,0 +1,28 @@ +package ru.ulstu.is.lab1.DataBase.configuration; + +import ru.ulstu.is.lab1.DataBase.configuration.jwt.JwtFilter; +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class OpenAPI30Configuration { + public static final String API_PREFIX = "/api/1.0"; + + @Bean + public OpenAPI customizeOpenAPI() { + final String securitySchemeName = JwtFilter.TOKEN_BEGIN_STR; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement() + .addList(securitySchemeName)) + .components(new Components() + .addSecuritySchemes(securitySchemeName, new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..c4d7130 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/PasswordEncoderConfiguration.java @@ -0,0 +1,14 @@ +package ru.ulstu.is.lab1.DataBase.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..f8acd6a --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/SecurityConfiguration.java @@ -0,0 +1,74 @@ +package ru.ulstu.is.lab1.DataBase.configuration; + +import ru.ulstu.is.lab1.DataBase.configuration.jwt.JwtFilter; +import ru.ulstu.is.lab1.DataBase.controller.UserController; +import ru.ulstu.is.lab1.DataBase.controller.UserSignupController; +import ru.ulstu.is.lab1.DataBase.model.UserRole; +import ru.ulstu.is.lab1.DataBase.service.UserService; +import org.slf4j.Logger; +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.EnableMethodSecurity; +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 org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration extends WebSecurityConfigurerAdapter { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + 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.createUser(admin, admin, admin, UserRole.ADMIN); + } + } + @Override + protected void configure(HttpSecurity http) throws Exception { + log.info("Creating security configuration"); + http.cors() + .and() + .csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/", SPA_URL_MASK).permitAll() + .antMatchers(HttpMethod.POST, UserController.URL_LOGIN).permitAll() + .antMatchers(HttpMethod.POST, UserSignupController.URL_LOGIN).permitAll() + .anyRequest() + .authenticated() + .and() + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous(); + } + @Override + protected void configure(AuthenticationManagerBuilder builder) throws Exception { + builder.userDetailsService(userService); + } + @Override + public void configure(WebSecurity web) { + web.ignoring() + .antMatchers(HttpMethod.OPTIONS, "/**") + .antMatchers("/**/*.{js,html,css,png,jpg}") + .antMatchers("/swagger-ui/index.html") + .antMatchers("/webjars/**") + .antMatchers("/swagger-resources/**") + .antMatchers("/v3/api-docs/**") + .antMatchers("/h2-console/**"); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/WebConfiguration.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/WebConfiguration.java new file mode 100644 index 0000000..c1bfa11 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/WebConfiguration.java @@ -0,0 +1,28 @@ +package ru.ulstu.is.lab1.DataBase.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 +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("*"); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtException.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtException.java new file mode 100644 index 0000000..6442daf --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtException.java @@ -0,0 +1,10 @@ +package ru.ulstu.is.lab1.DataBase.configuration.jwt; + +public class JwtException extends RuntimeException { + public JwtException(Throwable throwable) { + super(throwable); + } + public JwtException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtFilter.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtFilter.java new file mode 100644 index 0000000..a83e6b7 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtFilter.java @@ -0,0 +1,67 @@ +package ru.ulstu.is.lab1.DataBase.configuration.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ru.ulstu.is.lab1.DataBase.service.UserService; +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 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/lab1/DataBase/configuration/jwt/JwtProperties.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProperties.java new file mode 100644 index 0000000..7eaf614 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.lab1.DataBase.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; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProvider.java b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProvider.java new file mode 100644 index 0000000..d23ef98 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProvider.java @@ -0,0 +1,99 @@ +package ru.ulstu.is.lab1.DataBase.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(); + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/CollectionController.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/CollectionController.java index 1841c54..ee7666f 100644 --- a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/CollectionController.java +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/CollectionController.java @@ -1,13 +1,14 @@ package ru.ulstu.is.lab1.DataBase.controller; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.lab1.DataBase.configuration.OpenAPI30Configuration; import ru.ulstu.is.lab1.DataBase.service.CollectionService; import javax.validation.Valid; import java.util.List; @RestController -@RequestMapping("/collection") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/collection") public class CollectionController { private final CollectionService collectionService; @@ -45,7 +46,7 @@ public class CollectionController { } @PostMapping("/add_film/{id}") - public CollectionDTO addFilm(@PathVariable Long id, @RequestParam Long film_id) { + public CollectionDTO addFilm(@PathVariable Long id, @RequestBody @Valid Long film_id) { return new CollectionDTO(collectionService.addFilm(id, film_id)); } diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/FilmController.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/FilmController.java index 63f6b6c..ff3976e 100644 --- a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/FilmController.java +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/FilmController.java @@ -1,13 +1,14 @@ package ru.ulstu.is.lab1.DataBase.controller; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.lab1.DataBase.configuration.OpenAPI30Configuration; import ru.ulstu.is.lab1.DataBase.service.FilmService; import javax.validation.Valid; import java.util.List; @RestController -@RequestMapping("/film") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/film") public class FilmController { private final FilmService filmService; diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/GenreController.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/GenreController.java index d97f699..639535d 100644 --- a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/GenreController.java +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/GenreController.java @@ -1,13 +1,14 @@ package ru.ulstu.is.lab1.DataBase.controller; import org.springframework.web.bind.annotation.*; +import ru.ulstu.is.lab1.DataBase.configuration.OpenAPI30Configuration; import ru.ulstu.is.lab1.DataBase.service.GenreService; import javax.validation.Valid; import java.util.List; @RestController -@RequestMapping("/genre") +@RequestMapping(OpenAPI30Configuration.API_PREFIX + "/genre") public class GenreController { private final GenreService genreService; diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserController.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserController.java new file mode 100644 index 0000000..c5d6d6f --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserController.java @@ -0,0 +1,29 @@ +package ru.ulstu.is.lab1.DataBase.controller; + +import ru.ulstu.is.lab1.DataBase.configuration.OpenAPI30Configuration; +import ru.ulstu.is.lab1.DataBase.model.User; +import ru.ulstu.is.lab1.DataBase.service.UserService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; +import java.util.List; + +@RestController +public class UserController { + public static final String URL_LOGIN = "/jwt/login"; + private final UserService userService; + public UserController(UserService userService) { + this.userService = userService; + } + @GetMapping(OpenAPI30Configuration.API_PREFIX + "/user") + public List getUsers() { + return userService.findAllUsers(); + } + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid UserDTO userDTO) { + return userService.loginAndGetToken(userDTO); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserDTO.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserDTO.java new file mode 100644 index 0000000..15fd961 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserDTO.java @@ -0,0 +1,23 @@ +package ru.ulstu.is.lab1.DataBase.controller; + +import ru.ulstu.is.lab1.DataBase.model.User; + +import javax.validation.constraints.NotEmpty; + +public class UserDTO { + @NotEmpty + private String login; + @NotEmpty + private String password; + public UserDTO() {} + UserDTO(User user) { + login = user.getLogin(); + password = user.getPassword(); + } + public String getLogin() { + return login; + } + public String getPassword() { + return password; + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupController.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupController.java new file mode 100644 index 0000000..7db7116 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupController.java @@ -0,0 +1,31 @@ +package ru.ulstu.is.lab1.DataBase.controller; + +import ru.ulstu.is.lab1.DataBase.model.User; +import ru.ulstu.is.lab1.DataBase.service.UserService; +import ru.ulstu.is.lab1.util.validation.ValidationException; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import javax.validation.Valid; + +@Controller +@RestController +public class UserSignupController { + public static final String URL_LOGIN = "/signup"; + private final UserService userService; + public UserSignupController(UserService userService) { + this.userService = userService; + } + @PostMapping(URL_LOGIN) + public String signup(@RequestBody @Valid UserSignupDTO userSignupDTO) { + try { + final User user = userService.createUser( + userSignupDTO.getLogin(), userSignupDTO.getPassword(), userSignupDTO.getPasswordConfirm()); + return user.getLogin(); + } catch (ValidationException e) { + return "error"; + } + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupDTO.java b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupDTO.java new file mode 100644 index 0000000..0c449fc --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupDTO.java @@ -0,0 +1,34 @@ +package ru.ulstu.is.lab1.DataBase.controller; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +public class UserSignupDTO { + @NotBlank + @Size(min = 3, max = 64) + private String login; + @NotBlank + @Size(min = 6, max = 64) + private String password; + @NotBlank + @Size(min = 6, max = 64) + private String passwordConfirm; + 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 String getPasswordConfirm() { + return passwordConfirm; + } + public void setPasswordConfirm(String passwordConfirm) { + this.passwordConfirm = passwordConfirm; + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/model/User.java b/src/main/java/ru/ulstu/is/lab1/DataBase/model/User.java new file mode 100644 index 0000000..3f7cecd --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/model/User.java @@ -0,0 +1,70 @@ +package ru.ulstu.is.lab1.DataBase.model; + +import javax.persistence.*; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; +import java.util.Objects; + +@Entity +@Table(name = "users") +public class User { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + @Column(nullable = false, unique = true, length = 64) + @NotBlank + @Size(min = 3, max = 64) + private String login; + @Column(nullable = false, length = 64) + @NotBlank + @Size(min = 6, max = 64) + private String password; + private UserRole role; + public User() { + } + public User(String login, String password) { + this(login, password, UserRole.USER); + } + public User(String login, String password, UserRole 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 UserRole getRole() { + 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/lab1/DataBase/model/UserRole.java b/src/main/java/ru/ulstu/is/lab1/DataBase/model/UserRole.java new file mode 100644 index 0000000..8788472 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/model/UserRole.java @@ -0,0 +1,17 @@ +package ru.ulstu.is.lab1.DataBase.model; + +import org.springframework.security.core.GrantedAuthority; + +public enum UserRole 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"; + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserExistsException.java b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserExistsException.java new file mode 100644 index 0000000..1105976 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserExistsException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.lab1.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/lab1/DataBase/service/UserNotFoundException.java b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserNotFoundException.java new file mode 100644 index 0000000..ffa8788 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.lab1.DataBase.service; + +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String login) { + super(String.format("User not found '%s'", login)); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserService.java b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserService.java new file mode 100644 index 0000000..7def773 --- /dev/null +++ b/src/main/java/ru/ulstu/is/lab1/DataBase/service/UserService.java @@ -0,0 +1,91 @@ +package ru.ulstu.is.lab1.DataBase.service; + +import ru.ulstu.is.lab1.DataBase.configuration.jwt.JwtException; +import ru.ulstu.is.lab1.DataBase.configuration.jwt.JwtProvider; +import ru.ulstu.is.lab1.DataBase.controller.UserDTO; +import ru.ulstu.is.lab1.DataBase.model.User; +import ru.ulstu.is.lab1.DataBase.model.UserRole; +import ru.ulstu.is.lab1.DataBase.service.UserExistsException; +import ru.ulstu.is.lab1.DataBase.service.UserNotFoundException; +import ru.ulstu.is.lab1.DataBase.Repository.IUserRepository; +import ru.ulstu.is.lab1.util.validation.ValidationException; +import ru.ulstu.is.lab1.util.validation.ValidatorUtil; +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 java.util.Collections; +import java.util.List; +import java.util.Objects; + +@Service +public class UserService implements UserDetailsService { + private final IUserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final ValidatorUtil validatorUtil; + private final JwtProvider jwtProvider; + public UserService(IUserRepository userRepository, + PasswordEncoder passwordEncoder, + ValidatorUtil validatorUtil, + JwtProvider jwtProvider) { + this.userRepository = userRepository; + this.passwordEncoder = passwordEncoder; + this.validatorUtil = validatorUtil; + this.jwtProvider = jwtProvider; + } + public Page findAllPages(int page, int size) { + return userRepository.findAll(PageRequest.of(page - 1, size, Sort.by("id").ascending())); + } + public List findAllUsers() { + return userRepository.findAll(); + } + public User findByLogin(String login) { + return userRepository.findOneByLoginIgnoreCase(login); + } + public User createUser(String login, String password, String passwordConfirm) { + return createUser(login, password, passwordConfirm, UserRole.USER); + } + public User createUser(String login, String password, String passwordConfirm, UserRole role) { + if (findByLogin(login) != null) { + throw new UserExistsException(login); + } + final User user = new User(login, passwordEncoder.encode(password), role); + validatorUtil.validate(user); + if (!Objects.equals(password, passwordConfirm)) { + throw new ValidationException("Passwords not equals"); + } + 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); + if (userEntity == null) { + throw new UsernameNotFoundException(username); + } + return new org.springframework.security.core.userdetails.User( + userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getRole())); + } +} diff --git a/src/main/java/ru/ulstu/is/lab1/WebConfiguration.java b/src/main/java/ru/ulstu/is/lab1/WebConfiguration.java deleted file mode 100644 index f847c1d..0000000 --- a/src/main/java/ru/ulstu/is/lab1/WebConfiguration.java +++ /dev/null @@ -1,13 +0,0 @@ -package ru.ulstu.is.lab1; - -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("*"); - } -} diff --git a/src/main/java/ru/ulstu/is/lab1/util/validation/ValidationException.java b/src/main/java/ru/ulstu/is/lab1/util/validation/ValidationException.java index 293032e..225a9e7 100644 --- a/src/main/java/ru/ulstu/is/lab1/util/validation/ValidationException.java +++ b/src/main/java/ru/ulstu/is/lab1/util/validation/ValidationException.java @@ -3,6 +3,9 @@ package ru.ulstu.is.lab1.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 ccc05e8..cd217cd 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 @@ -8,4 +8,6 @@ spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 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 \ No newline at end of file +spring.h2.console.settings.web-allow-others=false +jwt.dev-token=my-secret-jwt +jwt.dev=true \ No newline at end of file