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 (
+
+ );
+}
\ 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