From 24190594f1b82e966fe7b406a3bb915555701807 Mon Sep 17 00:00:00 2001 From: "ityurner02@mail.ru" Date: Sat, 10 Jun 2023 21:06:52 +0400 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=A06(Vue)=20=D0=BE=D0=BD=D0=BE=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CatalogCollections.vue | 53 +++++++--- .../src/components/CatalogFilms.vue | 48 +++++++-- .../src/components/CatalogGenres.vue | 44 ++++++-- .../vue-project/src/components/Catalogs.vue | 23 ---- .../vue-project/src/components/DataTable.vue | 70 ------------- .../vue-project/src/components/Header.vue | 19 +++- Frontend/vue-project/src/components/Index.vue | 5 + Frontend/vue-project/src/components/Login.vue | 96 +++++++++++++++++ .../vue-project/src/components/Report.vue | 21 +++- .../vue-project/src/components/Signup.vue | 52 +++++++++ Frontend/vue-project/src/components/Users.vue | 64 +++++++++++ Frontend/vue-project/src/main.js | 8 +- Frontend/vue-project/src/models/User.js | 7 ++ build.gradle | 10 ++ data.mv.db | Bin 77824 -> 90112 bytes .../DataBase/Repository/IUserRepository.java | 8 ++ .../configuration/OpenAPI30Configuration.java | 28 +++++ .../PasswordEncoderConfiguration.java | 14 +++ .../configuration/SecurityConfiguration.java | 74 +++++++++++++ .../configuration/WebConfiguration.java | 28 +++++ .../configuration/jwt/JwtException.java | 10 ++ .../DataBase/configuration/jwt/JwtFilter.java | 67 ++++++++++++ .../configuration/jwt/JwtProperties.java | 27 +++++ .../configuration/jwt/JwtProvider.java | 99 ++++++++++++++++++ .../controller/CollectionController.java | 5 +- .../DataBase/controller/FilmController.java | 3 +- .../DataBase/controller/GenreController.java | 3 +- .../DataBase/controller/UserController.java | 29 +++++ .../is/lab1/DataBase/controller/UserDTO.java | 23 ++++ .../controller/UserSignupController.java | 31 ++++++ .../DataBase/controller/UserSignupDTO.java | 34 ++++++ .../ru/ulstu/is/lab1/DataBase/model/User.java | 70 +++++++++++++ .../is/lab1/DataBase/model/UserRole.java | 17 +++ .../DataBase/service/UserExistsException.java | 7 ++ .../service/UserNotFoundException.java | 7 ++ .../is/lab1/DataBase/service/UserService.java | 91 ++++++++++++++++ .../ru/ulstu/is/lab1/WebConfiguration.java | 13 --- .../util/validation/ValidationException.java | 3 + src/main/resources/application.properties | 6 +- 39 files changed, 1072 insertions(+), 145 deletions(-) delete mode 100644 Frontend/vue-project/src/components/Catalogs.vue delete mode 100644 Frontend/vue-project/src/components/DataTable.vue create mode 100644 Frontend/vue-project/src/components/Login.vue create mode 100644 Frontend/vue-project/src/components/Signup.vue create mode 100644 Frontend/vue-project/src/components/Users.vue create mode 100644 Frontend/vue-project/src/models/User.js create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/Repository/IUserRepository.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/OpenAPI30Configuration.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/PasswordEncoderConfiguration.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/SecurityConfiguration.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/WebConfiguration.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtException.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtFilter.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProperties.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/configuration/jwt/JwtProvider.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserController.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserDTO.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupController.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/controller/UserSignupDTO.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/model/User.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/model/UserRole.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/service/UserExistsException.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/service/UserNotFoundException.java create mode 100644 src/main/java/ru/ulstu/is/lab1/DataBase/service/UserService.java delete mode 100644 src/main/java/ru/ulstu/is/lab1/WebConfiguration.java 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 195bd6ed7cf026c4d6e2b2edab7cb32e63c2206e..ebed62dea3fc12a08129a0beacf839d3e5482d74 100644 GIT binary patch literal 90112 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 literal 77824 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

vUTMvBO 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 { + 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