diff --git a/build.gradle b/build.gradle index 92f3afb..32a9205 100644 --- a/build.gradle +++ b/build.gradle @@ -25,6 +25,12 @@ dependencies { 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 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' + implementation 'com.auth0:java-jwt:4.4.0' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' + implementation 'javax.xml.bind:jaxb-api:2.3.1' + implementation 'org.hibernate.validator:hibernate-validator' implementation 'org.springdoc:springdoc-openapi-ui:1.6.5' diff --git a/front/package-lock.json b/front/package-lock.json index fa02810..5bdfd7c 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -14,6 +14,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-router": "^6.10.0", "react-router-dom": "^6.6.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" @@ -15157,11 +15158,11 @@ } }, "node_modules/react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.11.1.tgz", + "integrity": "sha512-OZINSdjJ2WgvAi7hgNLazrEV8SGn6xrKA+MkJe9wVDMZ3zQ6fdJocUjpCUCI0cNrelWjcvon0S/QK/j0NzL3KA==", "dependencies": { - "@remix-run/router": "1.3.2" + "@remix-run/router": "1.6.1" }, "engines": { "node": ">=14" @@ -15186,6 +15187,28 @@ "react-dom": ">=16.8" } }, + "node_modules/react-router-dom/node_modules/react-router": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", + "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "dependencies": { + "@remix-run/router": "1.3.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router/node_modules/@remix-run/router": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz", + "integrity": "sha512-YUkWj+xs0oOzBe74OgErsuR3wVn+efrFhXBWrit50kOiED+pvQe2r6MWY0iJMQU/mSVKxvNzL4ZaYvjdX+G7ZA==", + "engines": { + "node": ">=14" + } + }, "node_modules/react-scripts": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.1.tgz", @@ -28839,11 +28862,18 @@ "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==" }, "react-router": { - "version": "6.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", - "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "version": "6.11.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.11.1.tgz", + "integrity": "sha512-OZINSdjJ2WgvAi7hgNLazrEV8SGn6xrKA+MkJe9wVDMZ3zQ6fdJocUjpCUCI0cNrelWjcvon0S/QK/j0NzL3KA==", "requires": { - "@remix-run/router": "1.3.2" + "@remix-run/router": "1.6.1" + }, + "dependencies": { + "@remix-run/router": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.6.1.tgz", + "integrity": "sha512-YUkWj+xs0oOzBe74OgErsuR3wVn+efrFhXBWrit50kOiED+pvQe2r6MWY0iJMQU/mSVKxvNzL4ZaYvjdX+G7ZA==" + } } }, "react-router-dom": { @@ -28853,6 +28883,16 @@ "requires": { "@remix-run/router": "1.3.2", "react-router": "6.8.1" + }, + "dependencies": { + "react-router": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", + "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "requires": { + "@remix-run/router": "1.3.2" + } + } } }, "react-scripts": { diff --git a/front/package.json b/front/package.json index e662b43..03344ec 100644 --- a/front/package.json +++ b/front/package.json @@ -9,6 +9,7 @@ "react": "^18.2.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", + "react-router": "^6.10.0", "react-router-dom": "^6.6.1", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" diff --git a/front/src/App.js b/front/src/App.js index 28a7eab..9a54f90 100644 --- a/front/src/App.js +++ b/front/src/App.js @@ -7,6 +7,9 @@ import DrivingSchools from './components/DrivingSchools.jsx'; import Categories from './components/Categories.jsx'; import OneDrivingSchool from './components/OneDrivingSchool.jsx'; import CountStudInCategory from './components/CountStudInCategory.jsx'; +import Login from './components/Login.jsx'; +import Logout from './components/Logout.jsx'; +import { useState, useEffect } from 'react'; function Router(props) { return useRoutes(props.rootRoute); @@ -21,15 +24,30 @@ function Router(props) { { path: '/categories', element: , label: 'Категории' }, { path: '/studcategory', element: , label: 'Количество студентов в категории' }, { path: '/drivingSchool/:id', element: }, + { path: '/login', element: }, + { path: '/logout', element: }, ]; const links = routes.filter(route => route.hasOwnProperty('label')); + const [token, setToken] = useState(localStorage.getItem('token')); + useEffect(() => { + + function handleStorageChange() { + setToken(localStorage.getItem('token')); + } + + window.addEventListener('storage', handleStorageChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + }; + }, []); const rootRoute = [ { path: '/', element: render(links), children: routes } ]; function render(links) { return (
-
+
diff --git a/front/src/components/Catalog.jsx b/front/src/components/Catalog.jsx index 6c1aec3..52e973e 100644 --- a/front/src/components/Catalog.jsx +++ b/front/src/components/Catalog.jsx @@ -36,7 +36,7 @@ export default function Catalog(props) { return <>
{props.name}
- + {localStorage.getItem("role") === "ADMIN" && } + form={form} + role={props.role}> -} \ No newline at end of file +} +export default withAuth(Categories); \ No newline at end of file diff --git a/front/src/components/DrivingSchools.jsx b/front/src/components/DrivingSchools.jsx index 7f662fc..0ff9d54 100644 --- a/front/src/components/DrivingSchools.jsx +++ b/front/src/components/DrivingSchools.jsx @@ -6,9 +6,9 @@ import Catalog from "./Catalog.jsx"; import DrivingSchool from "../models/DrivingSchool"; import ModalForm from './commons/ModalForm'; import { useNavigate } from "react-router-dom"; +import withAuth from './withAuth'; - -export default function DrivingSchools(props) { +function DrivingSchools(props) { const headers = [ {name: 'name', label: "Название"}, {name: 'countStudents', label: "Студенты"}, @@ -151,4 +151,5 @@ export default function DrivingSchools(props) { -} \ No newline at end of file +} +export default withAuth(DrivingSchools); \ No newline at end of file diff --git a/front/src/components/Login.css b/front/src/components/Login.css new file mode 100644 index 0000000..7b886ac --- /dev/null +++ b/front/src/components/Login.css @@ -0,0 +1,21 @@ +body { + background-color: #f8f9fa; +} + +.card { + margin-bottom: 20px; +} + +.card-title { + margin-bottom: 10px; +} + +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + background-color: #0069d9; + border-color: #0062cc; +} \ No newline at end of file diff --git a/front/src/components/Login.jsx b/front/src/components/Login.jsx new file mode 100644 index 0000000..38d55cf --- /dev/null +++ b/front/src/components/Login.jsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import './Login.css'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import { useNavigate} from "react-router-dom"; + +export default function Login(props) { + const navigate = useNavigate(); + const [loginData, setLoginData] = useState({ login: '', password: '' }); + const hostURL = 'http://localhost:8080'; + const handleLoginSubmit = (e) => { + e.preventDefault(); + + login(loginData.login, loginData.password); + }; + + const login = async function (login, password) { + const requestParams = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({login: login, password: password}), + }; + const response = await fetch(hostURL + "/jwt/login", requestParams); + const result = await response.text(); + if (response.status === 200) { + localStorage.setItem("token", result); + localStorage.setItem("user", login); + let jwtData = result.split('.')[1] + let decodedJwtJsonData = window.atob(jwtData); + let decodedJwtData = JSON.parse(decodedJwtJsonData); + + let role = decodedJwtData.role; + localStorage.setItem("role", role.toUpperCase()); + window.dispatchEvent(new Event("storage")); + navigate("/"); + } else { + localStorage.removeItem("token"); + localStorage.removeItem("user"); + localStorage.removeItem("role"); + alert(result); + } + } + + + return ( +
+
+
+
+
+
Авторизация
+
+
+ + setLoginData({ ...loginData, login: e.target.value })} /> +
+
+ + setLoginData({ ...loginData, password: e.target.value })} /> +
+ + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/front/src/components/Logout.jsx b/front/src/components/Logout.jsx new file mode 100644 index 0000000..2c8c451 --- /dev/null +++ b/front/src/components/Logout.jsx @@ -0,0 +1,20 @@ +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; + +function Logout() { + const navigate = useNavigate(); + + useEffect(() => { + // Удаление токена из localStorage или другого места + localStorage.removeItem('token'); + localStorage.removeItem("user"); + localStorage.removeItem("role"); + window.dispatchEvent(new Event("storage")); + // Перенаправление пользователя на страницу входа или другую страницу + navigate('/login'); + }, [navigate]); + + return null; +} + +export default Logout; \ No newline at end of file diff --git a/front/src/components/OneDrivingSchool.jsx b/front/src/components/OneDrivingSchool.jsx index 72f2826..3a2f44f 100644 --- a/front/src/components/OneDrivingSchool.jsx +++ b/front/src/components/OneDrivingSchool.jsx @@ -8,8 +8,9 @@ import Form from 'react-bootstrap/Form'; import Button from 'react-bootstrap/Button'; import ModalForm from './commons/ModalForm'; import styles from "./OneDrivingSchool.module.css"; +import withAuth from './withAuth'; -export default function OneDrivingSchool(props) { +function OneDrivingSchool(props) { const { id } = useParams(); const url = '/drivingSchool/id'; @@ -225,9 +226,9 @@ export default function OneDrivingSchool(props) {

Название: {drivingSchool.name}

Количество студентов: {drivingSchool.countStudents}

- - - + {localStorage.getItem("role") === "ADMIN" && } + {localStorage.getItem("role") === "ADMIN" && } + {localStorage.getItem("role") === "ADMIN" && }
@@ -253,4 +254,5 @@ export default function OneDrivingSchool(props) { -} \ No newline at end of file +} +export default withAuth(OneDrivingSchool); \ No newline at end of file diff --git a/front/src/components/ReportStudentCategory.jsx b/front/src/components/ReportStudentCategory.jsx index d496dc6..c46ab31 100644 --- a/front/src/components/ReportStudentCategory.jsx +++ b/front/src/components/ReportStudentCategory.jsx @@ -6,7 +6,8 @@ import Form from 'react-bootstrap/Form'; import Button from 'react-bootstrap/Button'; import DrivingSchool from '../models/DrivingSchool'; import { Link} from "react-router-dom"; -export default function ReportStudentCategory(props) { +import withAuth from './withAuth'; +function ReportStudentCategory(props) { const headersEmp = [ {name: 'surname', label: "Фамилия"}, {name: 'name', label: "Имя"}, @@ -69,4 +70,6 @@ export default function ReportStudentCategory(props) {
-} \ No newline at end of file +} + +export default withAuth(ReportStudentCategory); \ No newline at end of file diff --git a/front/src/components/Students.jsx b/front/src/components/Students.jsx index b2c36f3..d9c96ed 100644 --- a/front/src/components/Students.jsx +++ b/front/src/components/Students.jsx @@ -4,7 +4,8 @@ import Button from 'react-bootstrap/Button'; import { useState, useEffect } from 'react'; import DataService from '../services/DataService'; import CatalogSC from "./CatalogSC.jsx"; -export default function Students(props) { +import withAuth from './withAuth'; +function Students(props) { const headers = [ {name: 'surname', label: "Фамилия"}, {name: 'name', label: "Имя"}, @@ -112,4 +113,5 @@ export default function Students(props) { form={form}> -} \ No newline at end of file +} +export default withAuth(Students); \ No newline at end of file diff --git a/front/src/components/commons/Header.jsx b/front/src/components/commons/Header.jsx index 479c283..c92055d 100644 --- a/front/src/components/commons/Header.jsx +++ b/front/src/components/commons/Header.jsx @@ -2,6 +2,8 @@ import { NavLink } from 'react-router-dom'; import Container from 'react-bootstrap/Container'; import Nav from 'react-bootstrap/Nav'; import Navbar from 'react-bootstrap/Navbar'; +import styles from "./Header.module.css"; +import { useState, useEffect } from 'react'; export default function Header(props) { return ( @@ -17,6 +19,15 @@ export default function Header(props) { ) } + {props.token && props.token !== 'undefined' ? + +
Выйти
+
+ : + +
Войти
+
+ } diff --git a/front/src/components/commons/ItemTable.jsx b/front/src/components/commons/ItemTable.jsx index 307c6f4..6b30a63 100644 --- a/front/src/components/commons/ItemTable.jsx +++ b/front/src/components/commons/ItemTable.jsx @@ -14,9 +14,10 @@ export default function ItemTable(props) { { props.headers.map((header) => {props.item[header.name]}) } - {props.isOnlyView || - - - } + {localStorage.getItem("role") !== "ADMIN" ||props.isOnlyView || + {localStorage.getItem("role") === "ADMIN" && } + {localStorage.getItem("role") === "ADMIN" && } + {localStorage.getItem("role") === "ADMIN" && } + } } \ No newline at end of file diff --git a/front/src/components/commons/ItemTableSC.jsx b/front/src/components/commons/ItemTableSC.jsx index 4fec8bb..ae9627b 100644 --- a/front/src/components/commons/ItemTableSC.jsx +++ b/front/src/components/commons/ItemTableSC.jsx @@ -11,8 +11,9 @@ export default function ItemTableSC(props) { { props.headers.map((header) => {props.item[header.name]}) } - {props.isOnlyView || - - } + {localStorage.getItem("role") !== "ADMIN" ||props.isOnlyView || + {localStorage.getItem("role") === "ADMIN" && } + {localStorage.getItem("role") === "ADMIN" && } + } } \ No newline at end of file diff --git a/front/src/components/commons/Table.jsx b/front/src/components/commons/Table.jsx index 0b990d7..15fec78 100644 --- a/front/src/components/commons/Table.jsx +++ b/front/src/components/commons/Table.jsx @@ -17,7 +17,7 @@ export default function Table(props) { { props.headers.map((header) => {header.label}) } - {props.isOnlyView || Элементы управления} + {localStorage.getItem("role") !== "ADMIN" || props.isOnlyView || Элементы управления} diff --git a/front/src/components/commons/TableSC.jsx b/front/src/components/commons/TableSC.jsx index 20c8127..50f9618 100644 --- a/front/src/components/commons/TableSC.jsx +++ b/front/src/components/commons/TableSC.jsx @@ -14,7 +14,7 @@ export default function TableSC(props) { { props.headers.map((header) => {header.label}) } - {props.isOnlyView || Элементы управления} + {localStorage.getItem("role") !== "ADMIN" || props.isOnlyView || Элементы управления} diff --git a/front/src/components/withAuth.js b/front/src/components/withAuth.js new file mode 100644 index 0000000..adf1f7b --- /dev/null +++ b/front/src/components/withAuth.js @@ -0,0 +1,24 @@ +import React from 'react' +import { useNavigate} from "react-router-dom"; +import { useEffect } from 'react'; + +const withAuth = (Component) => { + const AuthenticatedComponent = (props) => { + const navigate = useNavigate(); + const token = localStorage.getItem('token'); + + useEffect(() => { + if (!token || token === 'undefined') { + navigate('/login'); + } + }, [navigate, token]); + + if (token && token !== 'undefined') { + return + } + return null; + } + return AuthenticatedComponent; +} + +export default withAuth; \ No newline at end of file diff --git a/front/src/services/DataService.js b/front/src/services/DataService.js index 84c3858..3918153 100644 --- a/front/src/services/DataService.js +++ b/front/src/services/DataService.js @@ -11,32 +11,45 @@ function toJSON(data) { } return jsonObj; } +const getTokenForHeader = function () { + return "Bearer " + localStorage.getItem("token"); +} export default class DataService { static dataUrlPrefix = 'http://localhost:8080/api'; static async readAll(url, transformer) { - const response = await axios.get(this.dataUrlPrefix + url); + const response = await axios.get(this.dataUrlPrefix + url, {headers: { + "Authorization": getTokenForHeader(), + }}); return response.data.map(item => transformer(item)); } static async read(url, transformer) { - const response = await axios.get(this.dataUrlPrefix + url); + const response = await axios.get(this.dataUrlPrefix + url, {headers: { + "Authorization": getTokenForHeader(), + }}); return transformer(response.data); } static async create(url, data) { - const response = await axios.post(this.dataUrlPrefix + url); + const response = await axios.post(this.dataUrlPrefix + url, {headers: { + "Authorization": getTokenForHeader(), + }}); return true; } - static async update(url, data) { - const response = await axios.put(this.dataUrlPrefix + url); + static async update(url, data) { + const response = await axios.put(this.dataUrlPrefix + url, {headers: { + "Authorization": getTokenForHeader(), + }}); return true; } static async delete(url) { - const response = await axios.delete(this.dataUrlPrefix + url); + const response = await axios.delete(this.dataUrlPrefix + url, {headers: { + "Authorization": getTokenForHeader(), + }}); return response.data.id; } } \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/WebConfiguration.java b/src/main/java/ru/ulstu/is/cbapp/WebConfiguration.java index f5229a0..09b8171 100644 --- a/src/main/java/ru/ulstu/is/cbapp/WebConfiguration.java +++ b/src/main/java/ru/ulstu/is/cbapp/WebConfiguration.java @@ -8,6 +8,12 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; public class WebConfiguration implements WebMvcConfigurer { public static final String REST_API = "/api"; + @Override + public void addViewControllers(ViewControllerRegistry registry) { + WebMvcConfigurer.super.addViewControllers(registry); + registry.addViewController("login"); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**").allowedMethods("*"); diff --git a/src/main/java/ru/ulstu/is/cbapp/configuration/PasswordEncoderConfiguration.java b/src/main/java/ru/ulstu/is/cbapp/configuration/PasswordEncoderConfiguration.java new file mode 100644 index 0000000..d19434b --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/PasswordEncoderConfiguration.java @@ -0,0 +1,14 @@ +package ru.ulstu.is.cbapp.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@Configuration +public class PasswordEncoderConfiguration { + @Bean + public PasswordEncoder createPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/configuration/SecurityConfiguration.java b/src/main/java/ru/ulstu/is/cbapp/configuration/SecurityConfiguration.java new file mode 100644 index 0000000..724f2de --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/SecurityConfiguration.java @@ -0,0 +1,87 @@ +package ru.ulstu.is.cbapp.configuration; + +import ru.ulstu.is.cbapp.configuration.jwt.JwtFilter; +import ru.ulstu.is.cbapp.user.controller.UserController; +import ru.ulstu.is.cbapp.user.controller.UserSignupMvcController; +import ru.ulstu.is.cbapp.user.model.UserRole; +import ru.ulstu.is.cbapp.user.service.UserService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity(securedEnabled = true) +public class SecurityConfiguration { + private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + private static final String LOGIN_URL = "/login"; + public static final String SPA_URL_MASK = "/{path:[^\\.]*}"; + private UserService userService; + private JwtFilter jwtFilter; + + public SecurityConfiguration(UserService userService) { + this.userService = userService; + this.jwtFilter = new JwtFilter(userService); + createAdminOnStartup(); + createTestUsersOnStartup(); + } + + 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); + } + } + + private void createTestUsersOnStartup() { + final String[] userNames = {"user1", "user2", "user3"}; + for (String user : userNames) { + if (userService.findByLogin(user) == null) { + log.info("User %s successfully created".formatted(user)); + userService.createUser(user, user, user, UserRole.USER); + } + } + } + + @Bean + public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception { + http.cors() + .and() + .csrf().disable() + .authorizeHttpRequests((a) -> a + .requestMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN") + .requestMatchers(HttpMethod.PUT, "/api/**").hasRole("ADMIN") + .requestMatchers(HttpMethod.POST, "/api/**").hasRole("ADMIN") + .requestMatchers("/api/**").authenticated() + .requestMatchers(HttpMethod.POST, UserController.URL_LOGIN).permitAll()) + .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) + .anonymous().and().authorizeHttpRequests((a) -> + a.requestMatchers(LOGIN_URL, UserSignupMvcController.SIGNUP_URL, "/h2-console/**") + .permitAll().requestMatchers("/users").hasRole("ADMIN").anyRequest().authenticated()) + .formLogin() + .loginPage(LOGIN_URL).permitAll() + .and() + .logout().permitAll() + .logoutSuccessUrl("/login") + .and() + .userDetailsService(userService); + + return http.build(); + } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return (web) -> web.ignoring().requestMatchers("/css/**", "/js/**", "/templates/**", "/webjars/**", "/styles/**"); } +} diff --git a/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtException.java b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtException.java new file mode 100644 index 0000000..10e4434 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtException.java @@ -0,0 +1,11 @@ +package ru.ulstu.is.cbapp.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/cbapp/configuration/jwt/JwtFilter.java b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtFilter.java new file mode 100644 index 0000000..8c598af --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtFilter.java @@ -0,0 +1,93 @@ +package ru.ulstu.is.cbapp.configuration.jwt; + +import ru.ulstu.is.cbapp.user.service.UserService; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.GenericFilterBean; + +import java.io.IOException; + +public class JwtFilter extends AbstractPreAuthenticatedProcessingFilter { + 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; + } + } + } + HttpServletRequest httpRequest = (HttpServletRequest) request; + if (httpRequest.getRequestURI().startsWith("/api/")) { + // Для URL, начинающихся с /api/, выполняем проверку наличия токена + super.doFilter(request, response, chain); + } else { + // Для остальных URL выполняем авторизацию + chain.doFilter(request, response); + } + } + + @Override + protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) { + String token = getTokenFromRequest(request); + // Возвращаем токен как принципала + return token; + } + + @Override + protected Object getPreAuthenticatedCredentials(HttpServletRequest request) { + return new WebAuthenticationDetailsSource().buildDetails(request); + } +} diff --git a/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtProperties.java b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtProperties.java new file mode 100644 index 0000000..901fb6d --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtProperties.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.cbapp.configuration.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "jwt", ignoreInvalidFields = true) +public class JwtProperties { + private String devToken = ""; + private Boolean isDev = true; + + public String getDevToken() { + return devToken; + } + + public void setDevToken(String devToken) { + this.devToken = devToken; + } + + public Boolean isDev() { + return isDev; + } + + public void setDev(Boolean dev) { + isDev = dev; + } +} diff --git a/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtsProvider.java b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtsProvider.java new file mode 100644 index 0000000..56037cf --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/configuration/jwt/JwtsProvider.java @@ -0,0 +1,51 @@ +package ru.ulstu.is.cbapp.configuration.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; + +@Component +public class JwtsProvider { + + @Value("jwt.secret") + private String secret; + + public String generateToken(String login, String role) { + Date date = Date.from(LocalDate.now().plusDays(15).atStartOfDay(ZoneId.systemDefault()).toInstant()); + + JwtBuilder builder = Jwts.builder() + .setSubject(login) + .setExpiration(date) + .signWith(SignatureAlgorithm.HS512, secret); + Claims claims = Jwts.claims(); + claims.put("role", role); + builder.addClaims(claims); + return builder.compact(); + } + + public boolean validateToken(String token) { + try { + Jwts.parser().setSigningKey(secret).parseClaimsJws(token); + return true; + } catch (Exception e) { + return false; + } + } + + public String getLogin(String token) { + return Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody() + .getSubject(); + } +} diff --git a/src/main/java/ru/ulstu/is/cbapp/user/controller/UserController.java b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserController.java new file mode 100644 index 0000000..8f7fac7 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserController.java @@ -0,0 +1,26 @@ +package ru.ulstu.is.cbapp.user.controller; + +import ru.ulstu.is.cbapp.WebConfiguration; +import ru.ulstu.is.cbapp.user.model.UserDto; +import ru.ulstu.is.cbapp.user.service.UserService; +import jakarta.validation.Valid; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + + +@RestController() +public class UserController { + public static final String URL_LOGIN = "/jwt/login"; + + private final UserService userService; + + public UserController(UserService userService) { + this.userService = userService; + } + + @PostMapping(URL_LOGIN) + public String login(@RequestBody @Valid UserDto userDto) { + return userService.loginAndGetToken(userDto); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/controller/UserMvcController.java b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserMvcController.java new file mode 100644 index 0000000..fcca42e --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserMvcController.java @@ -0,0 +1,44 @@ +package ru.ulstu.is.cbapp.user.controller; + +import ru.ulstu.is.cbapp.user.model.User; +import ru.ulstu.is.cbapp.user.model.UserDto; +import ru.ulstu.is.cbapp.user.model.UserRole; +import ru.ulstu.is.cbapp.user.service.UserService; +import org.springframework.data.domain.Page; +import org.springframework.security.access.annotation.Secured; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + + +import java.util.List; +import java.util.stream.IntStream; + +@Controller +@RequestMapping("/users") +public class UserMvcController { + private final UserService userService; + + public UserMvcController(UserService userService) { + this.userService = userService; + } + + @GetMapping + @Secured({UserRole.AsString.ADMIN}) + public String getUsers(@RequestParam(defaultValue = "1") int page, + @RequestParam(defaultValue = "5") int size, + Model model) { + final Page users = userService.findAllPages(page, size) + .map(UserDto::new); + model.addAttribute("users", users); + final int totalPages = users.getTotalPages(); + final List pageNumbers = IntStream.rangeClosed(1, totalPages) + .boxed() + .toList(); + model.addAttribute("pages", pageNumbers); + model.addAttribute("totalPages", totalPages); + return "users"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/controller/UserSignupMvcController.java b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserSignupMvcController.java new file mode 100644 index 0000000..ad8db68 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/controller/UserSignupMvcController.java @@ -0,0 +1,50 @@ +package ru.ulstu.is.cbapp.user.controller; + +import ru.ulstu.is.cbapp.user.model.User; +import ru.ulstu.is.cbapp.user.model.UserSignupDto; +import ru.ulstu.is.cbapp.user.service.UserService; +import ru.ulstu.is.cbapp.util.validation.ValidationException; +import jakarta.validation.Valid; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +@RequestMapping(UserSignupMvcController.SIGNUP_URL) +public class UserSignupMvcController { + public static final String SIGNUP_URL = "/signup"; + + private final UserService userService; + + public UserSignupMvcController(UserService userService) { + this.userService = userService; + } + + @GetMapping + public String showSignupForm(Model model) { + model.addAttribute("userDto", new UserSignupDto()); + return "signup"; + } + + @PostMapping + public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto, + BindingResult bindingResult, + Model model) { + if (bindingResult.hasErrors()) { + model.addAttribute("errors", bindingResult.getAllErrors()); + return "signup"; + } + try { + final User user = userService.createUser( + userSignupDto.getLogin(), userSignupDto.getPassword(), userSignupDto.getPasswordConfirm()); + return "redirect:/login?created=" + user.getLogin(); + } catch (ValidationException e) { + model.addAttribute("errors", e.getMessage()); + return "signup"; + } + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/model/User.java b/src/main/java/ru/ulstu/is/cbapp/user/model/User.java new file mode 100644 index 0000000..9ca440c --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/model/User.java @@ -0,0 +1,75 @@ +package ru.ulstu.is.cbapp.user.model; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotBlank; +import jakarta.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) && Objects.equals(login, user.login); + } + + @Override + public int hashCode() { + return Objects.hash(id, login); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/model/UserDto.java b/src/main/java/ru/ulstu/is/cbapp/user/model/UserDto.java new file mode 100644 index 0000000..fbfb259 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/model/UserDto.java @@ -0,0 +1,46 @@ +package ru.ulstu.is.cbapp.user.model; + +public class UserDto { + private long id; + private String login; + private UserRole role; + private String password; + + public UserDto(User user) { + this.id = user.getId(); + this.login = user.getLogin(); + this.role = user.getRole(); + this.password = user.getPassword(); + } + + public long getId() { + return id; + } + + public String getLogin() { + return login; + } + + public UserRole getRole() { + return role; + } + + public String getPassword() { + return password; + } + + public UserDto() { + } + + public void setLogin(String login) { + this.login = login; + } + + public void setRole(UserRole role) { + this.role = role; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/model/UserRole.java b/src/main/java/ru/ulstu/is/cbapp/user/model/UserRole.java new file mode 100644 index 0000000..bc20dc7 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/model/UserRole.java @@ -0,0 +1,20 @@ +package ru.ulstu.is.cbapp.user.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"; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/model/UserSignupDto.java b/src/main/java/ru/ulstu/is/cbapp/user/model/UserSignupDto.java new file mode 100644 index 0000000..14080f9 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/model/UserSignupDto.java @@ -0,0 +1,41 @@ +package ru.ulstu.is.cbapp.user.model; + +import jakarta.validation.constraints.NotBlank; +import jakarta.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; + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/repository/UserRepository.java b/src/main/java/ru/ulstu/is/cbapp/user/repository/UserRepository.java new file mode 100644 index 0000000..cfe26fc --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/repository/UserRepository.java @@ -0,0 +1,8 @@ +package ru.ulstu.is.cbapp.user.repository; + +import ru.ulstu.is.cbapp.user.model.User; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { + User findOneByLoginIgnoreCase(String login); +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/user/service/UserNotFoundException.java b/src/main/java/ru/ulstu/is/cbapp/user/service/UserNotFoundException.java new file mode 100644 index 0000000..7ceb619 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/service/UserNotFoundException.java @@ -0,0 +1,7 @@ +package ru.ulstu.is.cbapp.user.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/cbapp/user/service/UserService.java b/src/main/java/ru/ulstu/is/cbapp/user/service/UserService.java new file mode 100644 index 0000000..88efa9f --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/user/service/UserService.java @@ -0,0 +1,96 @@ +package ru.ulstu.is.cbapp.user.service; + +import ru.ulstu.is.cbapp.configuration.jwt.JwtException; +import ru.ulstu.is.cbapp.configuration.jwt.JwtsProvider; +import ru.ulstu.is.cbapp.user.model.User; +import ru.ulstu.is.cbapp.user.model.UserDto; +import ru.ulstu.is.cbapp.user.model.UserRole; +import ru.ulstu.is.cbapp.user.repository.UserRepository; +import ru.ulstu.is.cbapp.util.validation.ValidationException; +import ru.ulstu.is.cbapp.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.Objects; + +@Service +public class UserService implements UserDetailsService { + private final UserRepository userRepository; + private final PasswordEncoder passwordEncoder; + private final ValidatorUtil validatorUtil; + private final JwtsProvider jwtProvider; + + public UserService(UserRepository userRepository, + PasswordEncoder passwordEncoder, + ValidatorUtil validatorUtil, + JwtsProvider 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 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 ValidationException(String.format("User '%s' already exists", 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(), user.getRole().name()); + } + + public UserDetails loadUserByToken(String token) throws UsernameNotFoundException { + if (!jwtProvider.validateToken(token)) { + throw new JwtException("Bad token"); + } + final String userLogin = jwtProvider.getLogin(token); + if (userLogin.isEmpty()) { + throw 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/cbapp/util/error/AdviceController.java b/src/main/java/ru/ulstu/is/cbapp/util/error/AdviceController.java new file mode 100644 index 0000000..edf5618 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/util/error/AdviceController.java @@ -0,0 +1,45 @@ +package ru.ulstu.is.cbapp.util.error; + +import ru.ulstu.is.cbapp.util.validation.ValidationException; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestController; +import java.nio.file.AccessDeniedException; + +import java.util.stream.Collectors; + +@ControllerAdvice(annotations = RestController.class) +public class AdviceController { + @ExceptionHandler({ + ValidationException.class + }) + public ResponseEntity handleException(Throwable e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler({ + AccessDeniedException.class + }) + public ResponseEntity handleAccessDeniedException(Throwable e) { + return new ResponseEntity<>(e.getMessage(), HttpStatus.FORBIDDEN); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleBindException(MethodArgumentNotValidException e) { + final ValidationException validationException = new ValidationException( + e.getBindingResult().getAllErrors().stream() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.toSet())); + return handleException(validationException); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleUnknownException(Throwable e) { + e.printStackTrace(); + return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidationException.java b/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidationException.java new file mode 100644 index 0000000..a953b6e --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidationException.java @@ -0,0 +1,13 @@ +package ru.ulstu.is.cbapp.util.validation; + +import java.util.Set; + +public class ValidationException extends RuntimeException { + public ValidationException(Set errors) { + super(String.join("\n", errors)); + } + + public ValidationException(String error) { + super(error); + } +} \ No newline at end of file diff --git a/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidatorUtil.java b/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidatorUtil.java new file mode 100644 index 0000000..9c3f627 --- /dev/null +++ b/src/main/java/ru/ulstu/is/cbapp/util/validation/ValidatorUtil.java @@ -0,0 +1,27 @@ +package ru.ulstu.is.cbapp.util.validation; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.springframework.stereotype.Component; + +import java.util.Set; +import java.util.stream.Collectors; + +@Component +public class ValidatorUtil { + private final Validator validator; + + public ValidatorUtil() { + this.validator = Validation.buildDefaultValidatorFactory().getValidator(); + } + + public void validate(T object) { + final Set> errors = validator.validate(object); + if (!errors.isEmpty()) { + throw new ValidationException(errors.stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.toSet())); + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index da7b0b1..d4ce457 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -9,3 +9,6 @@ spring.jpa.hibernate.ddl-auto=update spring.h2.console.enabled=true spring.h2.console.settings.trace=false spring.h2.console.settings.web-allow-others=false +jwt.dev-token=my-secret-jwt +jwt.dev=true +jwt.secret = my-secret-jwt diff --git a/src/main/resources/static/styles/login.css b/src/main/resources/static/styles/login.css new file mode 100644 index 0000000..7b886ac --- /dev/null +++ b/src/main/resources/static/styles/login.css @@ -0,0 +1,21 @@ +body { + background-color: #f8f9fa; +} + +.card { + margin-bottom: 20px; +} + +.card-title { + margin-bottom: 10px; +} + +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} + +.btn-primary:hover { + background-color: #0069d9; + border-color: #0062cc; +} \ No newline at end of file diff --git a/src/main/resources/templates/category.html b/src/main/resources/templates/category.html index a7efd99..faee842 100644 --- a/src/main/resources/templates/category.html +++ b/src/main/resources/templates/category.html @@ -7,7 +7,7 @@
Категории
-
+ diff --git a/src/main/resources/templates/drivingSchool-one.html b/src/main/resources/templates/drivingSchool-one.html index 43c81ea..d82c770 100644 --- a/src/main/resources/templates/drivingSchool-one.html +++ b/src/main/resources/templates/drivingSchool-one.html @@ -9,7 +9,7 @@

Название:

Количество студентов:

-
+
Зачислить студента @@ -24,7 +24,7 @@ Имя Номер телефона Категории - + @@ -35,7 +35,7 @@ - + Отчислить diff --git a/src/main/resources/templates/drivingSchool.html b/src/main/resources/templates/drivingSchool.html index 2cfe943..11cf6e0 100644 --- a/src/main/resources/templates/drivingSchool.html +++ b/src/main/resources/templates/drivingSchool.html @@ -7,14 +7,11 @@
Автошколы
-
+
@@ -24,7 +21,7 @@ - + @@ -33,7 +30,7 @@ - + @@ -34,7 +34,7 @@ -
ID Название СтудентыЭлементы управленияЭлементы управления
- +
-
Вроде работает
-
Ошибка :( +
Добро пожаловать! Выберите страницу.
\ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..6675e44 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,43 @@ + + + + + + +
+
+ Пользователь не найден или пароль указан не верно +
+
+ Выход успешно произведен +
+
+ Пользователь '' успешно создан +
+
+
+
+
+
Авторизация
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 0000000..287c11f --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,28 @@ + + + +
+
+
+
+ +
+
+ +
+
+ +
+
+ + Назад +
+
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/student.html b/src/main/resources/templates/student.html index 8aa1d77..f1a0ef2 100644 --- a/src/main/resources/templates/student.html +++ b/src/main/resources/templates/student.html @@ -7,7 +7,7 @@
Имя Номер телефона КатегорииЭлементы управленияЭлементы управления
+